Skip to content

Commit 52e0797

Browse files
authored
Extend _sqrtprod() to cover the full range of inputs. Add tests. (GH-107855)
1 parent 637f7ff commit 52e0797

File tree

2 files changed

+97
-6
lines changed

2 files changed

+97
-6
lines changed

Lib/statistics.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
from itertools import count, groupby, repeat
138138
from bisect import bisect_left, bisect_right
139139
from math import hypot, sqrt, fabs, exp, erf, tau, log, fsum, sumprod
140+
from math import isfinite, isinf
140141
from functools import reduce
141142
from operator import itemgetter
142143
from collections import Counter, namedtuple, defaultdict
@@ -1005,14 +1006,25 @@ def _mean_stdev(data):
10051006
return float(xbar), float(xbar) / float(ss)
10061007

10071008
def _sqrtprod(x: float, y: float) -> float:
1008-
"Return sqrt(x * y) computed with high accuracy."
1009-
# Square root differential correction:
1010-
# https://www.wolframalpha.com/input/?i=Maclaurin+series+sqrt%28h**2+%2B+x%29+at+x%3D0
1009+
"Return sqrt(x * y) computed with improved accuracy and without overflow/underflow."
10111010
h = sqrt(x * y)
1011+
if not isfinite(h):
1012+
if isinf(h) and not isinf(x) and not isinf(y):
1013+
# Finite inputs overflowed, so scale down, and recompute.
1014+
scale = 2.0 ** -512 # sqrt(1 / sys.float_info.max)
1015+
return _sqrtprod(scale * x, scale * y) / scale
1016+
return h
10121017
if not h:
1013-
return 0.0
1014-
x = sumprod((x, h), (y, -h))
1015-
return h + x / (2.0 * h)
1018+
if x and y:
1019+
# Non-zero inputs underflowed, so scale up, and recompute.
1020+
# Scale: 1 / sqrt(sys.float_info.min * sys.float_info.epsilon)
1021+
scale = 2.0 ** 537
1022+
return _sqrtprod(scale * x, scale * y) / scale
1023+
return h
1024+
# Improve accuracy with a differential correction.
1025+
# https://www.wolframalpha.com/input/?i=Maclaurin+series+sqrt%28h**2+%2B+x%29+at+x%3D0
1026+
d = sumprod((x, h), (y, -h))
1027+
return h + d / (2.0 * h)
10161028

10171029

10181030
# === Statistics for relations between two inputs ===

Lib/test/test_statistics.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828

2929
# === Helper functions and class ===
3030

31+
# Test copied from Lib/test/test_math.py
32+
# detect evidence of double-rounding: fsum is not always correctly
33+
# rounded on machines that suffer from double rounding.
34+
x, y = 1e16, 2.9999 # use temporary values to defeat peephole optimizer
35+
HAVE_DOUBLE_ROUNDING = (x + y == 1e16 + 4)
36+
3137
def sign(x):
3238
"""Return -1.0 for negatives, including -0.0, otherwise +1.0."""
3339
return math.copysign(1, x)
@@ -2564,6 +2570,79 @@ def test_different_scales(self):
25642570
self.assertAlmostEqual(statistics.correlation(x, y), 1)
25652571
self.assertAlmostEqual(statistics.covariance(x, y), 0.1)
25662572

2573+
def test_sqrtprod_helper_function_fundamentals(self):
2574+
# Verify that results are close to sqrt(x * y)
2575+
for i in range(100):
2576+
x = random.expovariate()
2577+
y = random.expovariate()
2578+
expected = math.sqrt(x * y)
2579+
actual = statistics._sqrtprod(x, y)
2580+
with self.subTest(x=x, y=y, expected=expected, actual=actual):
2581+
self.assertAlmostEqual(expected, actual)
2582+
2583+
x, y, target = 0.8035720646477457, 0.7957468097636939, 0.7996498651651661
2584+
self.assertEqual(statistics._sqrtprod(x, y), target)
2585+
self.assertNotEqual(math.sqrt(x * y), target)
2586+
2587+
# Test that range extremes avoid underflow and overflow
2588+
smallest = sys.float_info.min * sys.float_info.epsilon
2589+
self.assertEqual(statistics._sqrtprod(smallest, smallest), smallest)
2590+
biggest = sys.float_info.max
2591+
self.assertEqual(statistics._sqrtprod(biggest, biggest), biggest)
2592+
2593+
# Check special values and the sign of the result
2594+
special_values = [0.0, -0.0, 1.0, -1.0, 4.0, -4.0,
2595+
math.nan, -math.nan, math.inf, -math.inf]
2596+
for x, y in itertools.product(special_values, repeat=2):
2597+
try:
2598+
expected = math.sqrt(x * y)
2599+
except ValueError:
2600+
expected = 'ValueError'
2601+
try:
2602+
actual = statistics._sqrtprod(x, y)
2603+
except ValueError:
2604+
actual = 'ValueError'
2605+
with self.subTest(x=x, y=y, expected=expected, actual=actual):
2606+
if isinstance(expected, str) and expected == 'ValueError':
2607+
self.assertEqual(actual, 'ValueError')
2608+
continue
2609+
self.assertIsInstance(actual, float)
2610+
if math.isnan(expected):
2611+
self.assertTrue(math.isnan(actual))
2612+
continue
2613+
self.assertEqual(actual, expected)
2614+
self.assertEqual(sign(actual), sign(expected))
2615+
2616+
@requires_IEEE_754
2617+
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
2618+
"accuracy not guaranteed on machines with double rounding")
2619+
@support.cpython_only # Allow for a weaker sumprod() implmentation
2620+
def test_sqrtprod_helper_function_improved_accuracy(self):
2621+
# Test a known example where accuracy is improved
2622+
x, y, target = 0.8035720646477457, 0.7957468097636939, 0.7996498651651661
2623+
self.assertEqual(statistics._sqrtprod(x, y), target)
2624+
self.assertNotEqual(math.sqrt(x * y), target)
2625+
2626+
def reference_value(x: float, y: float) -> float:
2627+
x = decimal.Decimal(x)
2628+
y = decimal.Decimal(y)
2629+
with decimal.localcontext() as ctx:
2630+
ctx.prec = 200
2631+
return float((x * y).sqrt())
2632+
2633+
# Verify that the new function with improved accuracy
2634+
# agrees with a reference value more often than old version.
2635+
new_agreements = 0
2636+
old_agreements = 0
2637+
for i in range(10_000):
2638+
x = random.expovariate()
2639+
y = random.expovariate()
2640+
new = statistics._sqrtprod(x, y)
2641+
old = math.sqrt(x * y)
2642+
ref = reference_value(x, y)
2643+
new_agreements += (new == ref)
2644+
old_agreements += (old == ref)
2645+
self.assertGreater(new_agreements, old_agreements)
25672646

25682647
def test_correlation_spearman(self):
25692648
# https://statistics.laerd.com/statistical-guides/spearmans-rank-order-correlation-statistical-guide-2.php

0 commit comments

Comments
 (0)