bpo-36546: Add statistics.quantiles() (#12710)

This commit is contained in:
Raymond Hettinger 2019-04-23 00:06:35 -07:00 committed by GitHub
parent d437012cdd
commit 9013ccf6d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 7 deletions

View file

@ -3,6 +3,7 @@ approx_equal function.
"""
import bisect
import collections
import collections.abc
import copy
@ -2038,6 +2039,7 @@ class TestStdev(VarianceStdevMixin, NumericTestCase):
expected = math.sqrt(statistics.variance(data))
self.assertEqual(self.func(data), expected)
class TestGeometricMean(unittest.TestCase):
def test_basics(self):
@ -2126,6 +2128,146 @@ class TestGeometricMean(unittest.TestCase):
with self.assertRaises(ValueError):
geometric_mean([Inf, -Inf])
class TestQuantiles(unittest.TestCase):
def test_specific_cases(self):
# Match results computed by hand and cross-checked
# against the PERCENTILE.EXC function in MS Excel.
quantiles = statistics.quantiles
data = [120, 200, 250, 320, 350]
random.shuffle(data)
for n, expected in [
(1, []),
(2, [250.0]),
(3, [200.0, 320.0]),
(4, [160.0, 250.0, 335.0]),
(5, [136.0, 220.0, 292.0, 344.0]),
(6, [120.0, 200.0, 250.0, 320.0, 350.0]),
(8, [100.0, 160.0, 212.5, 250.0, 302.5, 335.0, 357.5]),
(10, [88.0, 136.0, 184.0, 220.0, 250.0, 292.0, 326.0, 344.0, 362.0]),
(12, [80.0, 120.0, 160.0, 200.0, 225.0, 250.0, 285.0, 320.0, 335.0,
350.0, 365.0]),
(15, [72.0, 104.0, 136.0, 168.0, 200.0, 220.0, 240.0, 264.0, 292.0,
320.0, 332.0, 344.0, 356.0, 368.0]),
]:
self.assertEqual(expected, quantiles(data, n=n))
self.assertEqual(len(quantiles(data, n=n)), n - 1)
self.assertEqual(list(map(float, expected)),
quantiles(map(Decimal, data), n=n))
self.assertEqual(list(map(Decimal, expected)),
quantiles(map(Decimal, data), n=n))
self.assertEqual(list(map(Fraction, expected)),
quantiles(map(Fraction, data), n=n))
# Invariant under tranlation and scaling
def f(x):
return 3.5 * x - 1234.675
exp = list(map(f, expected))
act = quantiles(map(f, data), n=n)
self.assertTrue(all(math.isclose(e, a) for e, a in zip(exp, act)))
# Quartiles of a standard normal distribution
for n, expected in [
(1, []),
(2, [0.0]),
(3, [-0.4307, 0.4307]),
(4 ,[-0.6745, 0.0, 0.6745]),
]:
actual = quantiles(statistics.NormalDist(), n=n)
self.assertTrue(all(math.isclose(e, a, abs_tol=0.0001)
for e, a in zip(expected, actual)))
def test_specific_cases_inclusive(self):
# Match results computed by hand and cross-checked
# against the PERCENTILE.INC function in MS Excel
# and against the quaatile() function in SciPy.
quantiles = statistics.quantiles
data = [100, 200, 400, 800]
random.shuffle(data)
for n, expected in [
(1, []),
(2, [300.0]),
(3, [200.0, 400.0]),
(4, [175.0, 300.0, 500.0]),
(5, [160.0, 240.0, 360.0, 560.0]),
(6, [150.0, 200.0, 300.0, 400.0, 600.0]),
(8, [137.5, 175, 225.0, 300.0, 375.0, 500.0,650.0]),
(10, [130.0, 160.0, 190.0, 240.0, 300.0, 360.0, 440.0, 560.0, 680.0]),
(12, [125.0, 150.0, 175.0, 200.0, 250.0, 300.0, 350.0, 400.0,
500.0, 600.0, 700.0]),
(15, [120.0, 140.0, 160.0, 180.0, 200.0, 240.0, 280.0, 320.0, 360.0,
400.0, 480.0, 560.0, 640.0, 720.0]),
]:
self.assertEqual(expected, quantiles(data, n=n, method="inclusive"))
self.assertEqual(len(quantiles(data, n=n, method="inclusive")), n - 1)
self.assertEqual(list(map(float, expected)),
quantiles(map(Decimal, data), n=n, method="inclusive"))
self.assertEqual(list(map(Decimal, expected)),
quantiles(map(Decimal, data), n=n, method="inclusive"))
self.assertEqual(list(map(Fraction, expected)),
quantiles(map(Fraction, data), n=n, method="inclusive"))
# Invariant under tranlation and scaling
def f(x):
return 3.5 * x - 1234.675
exp = list(map(f, expected))
act = quantiles(map(f, data), n=n, method="inclusive")
self.assertTrue(all(math.isclose(e, a) for e, a in zip(exp, act)))
# Quartiles of a standard normal distribution
for n, expected in [
(1, []),
(2, [0.0]),
(3, [-0.4307, 0.4307]),
(4 ,[-0.6745, 0.0, 0.6745]),
]:
actual = quantiles(statistics.NormalDist(), n=n, method="inclusive")
self.assertTrue(all(math.isclose(e, a, abs_tol=0.0001)
for e, a in zip(expected, actual)))
def test_equal_sized_groups(self):
quantiles = statistics.quantiles
total = 10_000
data = [random.expovariate(0.2) for i in range(total)]
while len(set(data)) != total:
data.append(random.expovariate(0.2))
data.sort()
# Cases where the group size exactly divides the total
for n in (1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000):
group_size = total // n
self.assertEqual(
[bisect.bisect(data, q) for q in quantiles(data, n=n)],
list(range(group_size, total, group_size)))
# When the group sizes can't be exactly equal, they should
# differ by no more than one
for n in (13, 19, 59, 109, 211, 571, 1019, 1907, 5261, 9769):
group_sizes = {total // n, total // n + 1}
pos = [bisect.bisect(data, q) for q in quantiles(data, n=n)]
sizes = {q - p for p, q in zip(pos, pos[1:])}
self.assertTrue(sizes <= group_sizes)
def test_error_cases(self):
quantiles = statistics.quantiles
StatisticsError = statistics.StatisticsError
with self.assertRaises(TypeError):
quantiles() # Missing arguments
with self.assertRaises(TypeError):
quantiles([10, 20, 30], 13, n=4) # Too many arguments
with self.assertRaises(TypeError):
quantiles([10, 20, 30], 4) # n is a positional argument
with self.assertRaises(StatisticsError):
quantiles([10, 20, 30], n=0) # n is zero
with self.assertRaises(StatisticsError):
quantiles([10, 20, 30], n=-1) # n is negative
with self.assertRaises(TypeError):
quantiles([10, 20, 30], n=1.5) # n is not an integer
with self.assertRaises(ValueError):
quantiles([10, 20, 30], method='X') # method is unknown
with self.assertRaises(StatisticsError):
quantiles([10], n=4) # not enough data points
with self.assertRaises(TypeError):
quantiles([10, None, 30], n=4) # data is non-numeric
class TestNormalDist(unittest.TestCase):
# General note on precision: The pdf(), cdf(), and overlap() methods