bpo-36324: NormalDist() add more tests and update comments (GH-12476)



* Improve coverage.
* Note inherent limitations of the accuracy tests


https://bugs.python.org/issue36324
diff --git a/Lib/test/test_statistics.py b/Lib/test/test_statistics.py
index 02cbebd..485ffe2 100644
--- a/Lib/test/test_statistics.py
+++ b/Lib/test/test_statistics.py
@@ -2040,6 +2040,13 @@
 
 class TestNormalDist(unittest.TestCase):
 
+    # General note on precision: The pdf(), cdf(), and overlap() methods
+    # depend on functions in the math libraries that do not make
+    # explicit accuracy guarantees.  Accordingly, some of the accuracy
+    # tests below may fail if the underlying math functions are
+    # inaccurate.  There isn't much we can do about this short of
+    # implementing our own implementations from scratch.
+
     def test_slots(self):
         nd = statistics.NormalDist(300, 23)
         with self.assertRaises(TypeError):
@@ -2062,6 +2069,12 @@
         with self.assertRaises(statistics.StatisticsError):
             statistics.NormalDist(500, -10)
 
+        # verify that subclass type is honored
+        class NewNormalDist(statistics.NormalDist):
+            pass
+        nnd = NewNormalDist(200, 5)
+        self.assertEqual(type(nnd), NewNormalDist)
+
     def test_alternative_constructor(self):
         NormalDist = statistics.NormalDist
         data = [96, 107, 90, 92, 110]
@@ -2077,6 +2090,12 @@
         with self.assertRaises(statistics.StatisticsError):
             NormalDist.from_samples([10])                    # only one input
 
+        # verify that subclass type is honored
+        class NewNormalDist(NormalDist):
+            pass
+        nnd = NewNormalDist.from_samples(data)
+        self.assertEqual(type(nnd), NewNormalDist)
+
     def test_sample_generation(self):
         NormalDist = statistics.NormalDist
         mu, sigma = 10_000, 3.0
@@ -2099,12 +2118,6 @@
         self.assertEqual(data2, data4)
         self.assertNotEqual(data1, data2)
 
-        # verify that subclass type is honored
-        class NewNormalDist(NormalDist):
-            pass
-        nnd = NewNormalDist(200, 5)
-        self.assertEqual(type(nnd), NewNormalDist)
-
     def test_pdf(self):
         NormalDist = statistics.NormalDist
         X = NormalDist(100, 15)
@@ -2151,8 +2164,8 @@
         self.assertEqual(set(map(type, cdfs)), {float})
         # Verify montonic
         self.assertEqual(cdfs, sorted(cdfs))
-        # Verify center
-        self.assertAlmostEqual(X.cdf(100), 0.50)
+        # Verify center (should be exact)
+        self.assertEqual(X.cdf(100), 0.50)
         # Check against a table of known values
         # https://en.wikipedia.org/wiki/Standard_normal_table#Cumulative
         Z = NormalDist()
@@ -2216,10 +2229,11 @@
             p = 1.0 - p
             self.assertAlmostEqual(iq.cdf(iq.inv_cdf(p)), p)
 
-        # Now apply cdf() first.  At six sigmas, the round-trip
-        # loses a lot of precision, so only check to 6 places.
-        for x in range(10, 190):
-            self.assertAlmostEqual(iq.inv_cdf(iq.cdf(x)), x, places=6)
+        # Now apply cdf() first.  Near the tails, the round-trip loses
+        # precision and is ill-conditioned (small changes in the inputs
+        # give large changes in the output), so only check to 5 places.
+        for x in range(200):
+            self.assertAlmostEqual(iq.inv_cdf(iq.cdf(x)), x, places=5)
 
         # Error cases:
         with self.assertRaises(statistics.StatisticsError):
@@ -2237,6 +2251,9 @@
             iq.sigma = -0.1                         # sigma under zero
             iq.inv_cdf(0.5)
 
+        # Special values
+        self.assertTrue(math.isnan(Z.inv_cdf(float('NaN'))))
+
     def test_overlap(self):
         NormalDist = statistics.NormalDist
 
@@ -2275,6 +2292,7 @@
                 (NormalDist(-100, 15), NormalDist(110, 15)),
                 (NormalDist(-100, 15), NormalDist(-110, 15)),
                 # Misc cases with unequal standard deviations
+                (NormalDist(100, 12), NormalDist(100, 15)),
                 (NormalDist(100, 12), NormalDist(110, 15)),
                 (NormalDist(100, 12), NormalDist(150, 15)),
                 (NormalDist(100, 12), NormalDist(150, 35)),
@@ -2305,18 +2323,6 @@
         self.assertEqual(X.stdev, 15)
         self.assertEqual(X.variance, 225)
 
-    def test_unary_operations(self):
-        NormalDist = statistics.NormalDist
-        X = NormalDist(100, 12)
-        Y = +X
-        self.assertIsNot(X, Y)
-        self.assertEqual(X.mu, Y.mu)
-        self.assertEqual(X.sigma, Y.sigma)
-        Y = -X
-        self.assertIsNot(X, Y)
-        self.assertEqual(X.mu, -Y.mu)
-        self.assertEqual(X.sigma, Y.sigma)
-
     def test_same_type_addition_and_subtraction(self):
         NormalDist = statistics.NormalDist
         X = NormalDist(100, 12)
@@ -2340,13 +2346,27 @@
         with self.assertRaises(TypeError):                  # __rtruediv__
             y / X
 
+    def test_unary_operations(self):
+        NormalDist = statistics.NormalDist
+        X = NormalDist(100, 12)
+        Y = +X
+        self.assertIsNot(X, Y)
+        self.assertEqual(X.mu, Y.mu)
+        self.assertEqual(X.sigma, Y.sigma)
+        Y = -X
+        self.assertIsNot(X, Y)
+        self.assertEqual(X.mu, -Y.mu)
+        self.assertEqual(X.sigma, Y.sigma)
+
     def test_equality(self):
         NormalDist = statistics.NormalDist
         nd1 = NormalDist()
         nd2 = NormalDist(2, 4)
         nd3 = NormalDist()
+        nd4 = NormalDist(2, 4)
         self.assertNotEqual(nd1, nd2)
         self.assertEqual(nd1, nd3)
+        self.assertEqual(nd2, nd4)
 
         # Test NotImplemented when types are different
         class A: