Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bbd2da9
Merge pull request #1 from python/master
rhettinger Mar 16, 2021
74bdf1b
Merge branch 'master' of github.com:python/cpython
rhettinger Mar 22, 2021
6c53f1a
Merge branch 'master' of github.com:python/cpython
rhettinger Mar 22, 2021
a487c4f
.
rhettinger Mar 24, 2021
eb56423
.
rhettinger Mar 25, 2021
cc7ba06
.
rhettinger Mar 26, 2021
d024dd0
.
rhettinger Apr 22, 2021
b10f912
merge
rhettinger May 5, 2021
fb6744d
merge
rhettinger May 6, 2021
7f21a1c
Merge branch 'main' of github.com:python/cpython
rhettinger Aug 15, 2021
7da42d4
Merge branch 'main' of github.com:rhettinger/cpython
rhettinger Aug 25, 2021
e31757b
Merge branch 'main' of github.com:python/cpython
rhettinger Aug 31, 2021
f058a6f
Merge branch 'main' of github.com:python/cpython
rhettinger Aug 31, 2021
1fc29bd
Merge branch 'main' of github.com:python/cpython
rhettinger Sep 4, 2021
e5c0184
Merge branch 'main' of github.com:python/cpython
rhettinger Oct 30, 2021
3c86ec1
Merge branch 'main' of github.com:python/cpython
rhettinger Nov 9, 2021
96675e4
Merge branch 'main' of github.com:rhettinger/cpython
rhettinger Nov 9, 2021
de558c6
Merge branch 'main' of github.com:python/cpython
rhettinger Nov 9, 2021
418a07f
Merge branch 'main' of github.com:python/cpython
rhettinger Nov 14, 2021
ea23a8b
Merge branch 'main' of github.com:python/cpython
rhettinger Nov 21, 2021
71f7dcd
Inlined code from variance functions
rhettinger Nov 23, 2021
139e19f
Added helper functions for the float square root of a fraction
rhettinger Nov 23, 2021
06c2080
Call helper functions
rhettinger Nov 23, 2021
e7ff885
Add blurb
rhettinger Nov 23, 2021
34d59a5
Fix over-specified test
rhettinger Nov 24, 2021
f6c8a97
Add a test for the _sqrt_frac() helper function
rhettinger Nov 24, 2021
e439945
Increase the tested range
rhettinger Nov 24, 2021
520e216
Add type hints to the internal function.
rhettinger Nov 24, 2021
8c9d78e
Fix test for correct rounding
rhettinger Nov 25, 2021
d36ea9b
Simplify ⌊√(n/m)⌋ calculation
rhettinger Nov 25, 2021
28fdccf
Add comment and beef-up tests
rhettinger Nov 25, 2021
a17f2d9
Test for zero denominator
rhettinger Nov 25, 2021
2aa88b0
Add algorithmic references
rhettinger Nov 25, 2021
64bff13
Add test for the _isqrt_frac_rto() helper function.
rhettinger Nov 26, 2021
4fde157
Compute the 109 instead of hard-wiring it
rhettinger Nov 26, 2021
acc58ff
Stronger test for _isqrt_frac_rto()
rhettinger Nov 26, 2021
eba6e8d
Bigger range
rhettinger Nov 26, 2021
a4354c7
Bigger range
rhettinger Nov 26, 2021
95330ee
Replace float() call with int/int division to be parallel with the ot…
rhettinger Nov 26, 2021
14ff0d1
Factor out division. Update proof link. Remove internal type declaration
rhettinger Nov 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions Lib/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
import math
import numbers
import random
import sys

from fractions import Fraction
from decimal import Decimal
Expand Down Expand Up @@ -304,6 +305,27 @@ def _fail_neg(values, errmsg='negative value'):
raise StatisticsError(errmsg)
yield x

def _isqrt_frac_rto(n: int, m: int) -> float:
"""Square root of n/m, rounded to the nearest integer using round-to-odd."""
# Reference: https://www.lri.fr/~melquion/doc/05-imacs17_1-expose.pdf
a = math.isqrt(n // m)
return a | (a*a*m != n)

# For 53 bit precision floats, the _sqrt_frac() shift is 109.
_sqrt_shift: int = 2 * sys.float_info.mant_dig + 3

def _sqrt_frac(n: int, m: int) -> float:
Comment thread
rhettinger marked this conversation as resolved.
"""Square root of n/m as a float, correctly rounded."""
# See principle and proof sketch at: https://bugs.python.org/msg407078
q = (n.bit_length() - m.bit_length() - _sqrt_shift) // 2
if q >= 0:
numerator = _isqrt_frac_rto(n, m << 2 * q) << q
denominator = 1
else:
numerator = _isqrt_frac_rto(n << -2 * q, m)
denominator = 1 << -q
return numerator / denominator # Convert to float


# === Measures of central tendency (averages) ===

Expand Down Expand Up @@ -837,14 +859,17 @@ def stdev(data, xbar=None):
1.0810874155219827

"""
# Fixme: Despite the exact sum of squared deviations, some inaccuracy
# remain because there are two rounding steps. The first occurs in
# the _convert() step for variance(), the second occurs in math.sqrt().
var = variance(data, xbar)
try:
if iter(data) is data:
data = list(data)
n = len(data)
if n < 2:
raise StatisticsError('stdev requires at least two data points')
T, ss = _ss(data, xbar)
mss = ss / (n - 1)
if hasattr(T, 'sqrt'):
var = _convert(mss, T)
return var.sqrt()
except AttributeError:
return math.sqrt(var)
return _sqrt_frac(mss.numerator, mss.denominator)


def pstdev(data, mu=None):
Expand All @@ -856,14 +881,17 @@ def pstdev(data, mu=None):
0.986893273527251

"""
# Fixme: Despite the exact sum of squared deviations, some inaccuracy
# remain because there are two rounding steps. The first occurs in
# the _convert() step for pvariance(), the second occurs in math.sqrt().
var = pvariance(data, mu)
try:
if iter(data) is data:
data = list(data)
n = len(data)
if n < 1:
raise StatisticsError('pstdev requires at least one data point')
T, ss = _ss(data, mu)
mss = ss / n
if hasattr(T, 'sqrt'):
var = _convert(mss, T)
return var.sqrt()
except AttributeError:
return math.sqrt(var)
return _sqrt_frac(mss.numerator, mss.denominator)


# === Statistics for relations between two inputs ===
Expand Down
65 changes: 63 additions & 2 deletions Lib/test/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import copy
import decimal
import doctest
import itertools
import math
import pickle
import random
import sys
import unittest
from test import support
from test.support import import_helper
from test.support import import_helper, requires_IEEE_754

from decimal import Decimal
from fractions import Fraction
Expand Down Expand Up @@ -2161,6 +2162,66 @@ def test_center_not_at_mean(self):
self.assertEqual(self.func(data), 2.5)
self.assertEqual(self.func(data, mu=0.5), 6.5)

class TestSqrtHelpers(unittest.TestCase):

def test_isqrt_frac_rto(self):
for n, m in itertools.product(range(100), range(1, 1000)):
r = statistics._isqrt_frac_rto(n, m)
self.assertIsInstance(r, int)
if r*r*m == n:
# Root is exact
continue
# Inexact, so the root should be odd
self.assertEqual(r&1, 1)
# Verify correct rounding
self.assertTrue(m * (r - 1)**2 < n < m * (r + 1)**2)

@requires_IEEE_754
def test_sqrt_frac(self):

def is_root_correctly_rounded(x: Fraction, root: float) -> bool:
if not x:
return root == 0.0

# Extract adjacent representable floats
r_up: float = math.nextafter(root, math.inf)
r_down: float = math.nextafter(root, -math.inf)
assert r_down < root < r_up

# Convert to fractions for exact arithmetic
frac_root: Fraction = Fraction(root)
half_way_up: Fraction = (frac_root + Fraction(r_up)) / 2
half_way_down: Fraction = (frac_root + Fraction(r_down)) / 2

# Check a closed interval.
# Does not test for a midpoint rounding rule.
return half_way_down ** 2 <= x <= half_way_up ** 2

randrange = random.randrange

for i in range(60_000):
numerator: int = randrange(10 ** randrange(50))
denonimator: int = randrange(10 ** randrange(50)) + 1
with self.subTest(numerator=numerator, denonimator=denonimator):
x: Fraction = Fraction(numerator, denonimator)
root: float = statistics._sqrt_frac(numerator, denonimator)
self.assertTrue(is_root_correctly_rounded(x, root))

# Verify that corner cases and error handling match math.sqrt()
self.assertEqual(statistics._sqrt_frac(0, 1), 0.0)
with self.assertRaises(ValueError):
statistics._sqrt_frac(-1, 1)
with self.assertRaises(ValueError):
statistics._sqrt_frac(1, -1)

# Error handling for zero denominator matches that for Fraction(1, 0)
with self.assertRaises(ZeroDivisionError):
statistics._sqrt_frac(1, 0)

# The result is well defined if both inputs are negative
self.assertAlmostEqual(statistics._sqrt_frac(-2, -1), math.sqrt(2.0))


class TestStdev(VarianceStdevMixin, NumericTestCase):
# Tests for sample standard deviation.
def setUp(self):
Expand All @@ -2175,7 +2236,7 @@ def test_compare_to_variance(self):
# Test that stdev is, in fact, the square root of variance.
data = [random.uniform(-2, 9) for _ in range(1000)]
expected = math.sqrt(statistics.variance(data))
self.assertEqual(self.func(data), expected)
self.assertAlmostEqual(self.func(data), expected)

def test_center_not_at_mean(self):
data = (1.0, 2.0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve the accuracy of stdev() and pstdev() in the statistics module. When
the inputs are floats or fractions, the output is a correctly rounded float