Add harmonic mean and tests.

This commit is contained in:
Steven D'Aprano 2016-08-09 12:49:01 +10:00
parent 95e0df8389
commit a474afdddc
2 changed files with 211 additions and 14 deletions

View file

@ -21,6 +21,10 @@ import statistics
# === Helper functions and class ===
def sign(x):
"""Return -1.0 for negatives, including -0.0, otherwise +1.0."""
return math.copysign(1, x)
def _nan_equal(a, b):
"""Return True if a and b are both the same kind of NAN.
@ -264,6 +268,13 @@ class NumericTestCase(unittest.TestCase):
# === Test the helpers ===
# ========================
class TestSign(unittest.TestCase):
"""Test that the helper function sign() works correctly."""
def testZeroes(self):
# Test that signed zeroes report their sign correctly.
self.assertEqual(sign(0.0), +1)
self.assertEqual(sign(-0.0), -1)
# --- Tests for approx_equal ---
@ -659,7 +670,7 @@ class DocTests(unittest.TestCase):
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -OO and above")
def test_doc_tests(self):
failed, tried = doctest.testmod(statistics)
failed, tried = doctest.testmod(statistics, optionflags=doctest.ELLIPSIS)
self.assertGreater(tried, 0)
self.assertEqual(failed, 0)
@ -971,6 +982,34 @@ class ConvertTest(unittest.TestCase):
self.assertTrue(_nan_equal(x, nan))
class FailNegTest(unittest.TestCase):
"""Test _fail_neg private function."""
def test_pass_through(self):
# Test that values are passed through unchanged.
values = [1, 2.0, Fraction(3), Decimal(4)]
new = list(statistics._fail_neg(values))
self.assertEqual(values, new)
def test_negatives_raise(self):
# Test that negatives raise an exception.
for x in [1, 2.0, Fraction(3), Decimal(4)]:
seq = [-x]
it = statistics._fail_neg(seq)
self.assertRaises(statistics.StatisticsError, next, it)
def test_error_msg(self):
# Test that a given error message is used.
msg = "badness #%d" % random.randint(10000, 99999)
try:
next(statistics._fail_neg([-1], msg))
except statistics.StatisticsError as e:
errmsg = e.args[0]
else:
self.fail("expected exception, but it didn't happen")
self.assertEqual(errmsg, msg)
# === Tests for public functions ===
class UnivariateCommonMixin:
@ -1082,13 +1121,13 @@ class UnivariateTypeMixin:
Not all tests to do with types need go in this class. Only those that
rely on the function returning the same type as its input data.
"""
def test_types_conserved(self):
# Test that functions keeps the same type as their data points.
# (Excludes mixed data types.) This only tests the type of the return
# result, not the value.
def prepare_types_for_conservation_test(self):
"""Return the types which are expected to be conserved."""
class MyFloat(float):
def __truediv__(self, other):
return type(self)(super().__truediv__(other))
def __rtruediv__(self, other):
return type(self)(super().__rtruediv__(other))
def __sub__(self, other):
return type(self)(super().__sub__(other))
def __rsub__(self, other):
@ -1098,9 +1137,14 @@ class UnivariateTypeMixin:
def __add__(self, other):
return type(self)(super().__add__(other))
__radd__ = __add__
return (float, Decimal, Fraction, MyFloat)
def test_types_conserved(self):
# Test that functions keeps the same type as their data points.
# (Excludes mixed data types.) This only tests the type of the return
# result, not the value.
data = self.prepare_data()
for kind in (float, Decimal, Fraction, MyFloat):
for kind in self.prepare_types_for_conservation_test():
d = [kind(x) for x in data]
result = self.func(d)
self.assertIs(type(result), kind)
@ -1275,12 +1319,16 @@ class AverageMixin(UnivariateCommonMixin):
for x in (23, 42.5, 1.3e15, Fraction(15, 19), Decimal('0.28')):
self.assertEqual(self.func([x]), x)
def prepare_values_for_repeated_single_test(self):
return (3.5, 17, 2.5e15, Fraction(61, 67), Decimal('4.9712'))
def test_repeated_single_value(self):
# The average of a single repeated value is the value itself.
for x in (3.5, 17, 2.5e15, Fraction(61, 67), Decimal('4.9712')):
for x in self.prepare_values_for_repeated_single_test():
for count in (2, 5, 10, 20):
data = [x]*count
self.assertEqual(self.func(data), x)
with self.subTest(x=x, count=count):
data = [x]*count
self.assertEqual(self.func(data), x)
class TestMean(NumericTestCase, AverageMixin, UnivariateTypeMixin):
@ -1304,7 +1352,7 @@ class TestMean(NumericTestCase, AverageMixin, UnivariateTypeMixin):
self.assertEqual(self.func(data), 22.015625)
def test_decimals(self):
# Test mean with ints.
# Test mean with Decimals.
D = Decimal
data = [D("1.634"), D("2.517"), D("3.912"), D("4.072"), D("5.813")]
random.shuffle(data)
@ -1379,6 +1427,97 @@ class TestMean(NumericTestCase, AverageMixin, UnivariateTypeMixin):
self.assertEqual(statistics.mean([tiny]*n), tiny)
class TestHarmonicMean(NumericTestCase, AverageMixin, UnivariateTypeMixin):
def setUp(self):
self.func = statistics.harmonic_mean
def prepare_data(self):
# Override mixin method.
values = super().prepare_data()
values.remove(0)
return values
def prepare_values_for_repeated_single_test(self):
# Override mixin method.
return (3.5, 17, 2.5e15, Fraction(61, 67), Decimal('4.125'))
def test_zero(self):
# Test that harmonic mean returns zero when given zero.
values = [1, 0, 2]
self.assertEqual(self.func(values), 0)
def test_negative_error(self):
# Test that harmonic mean raises when given a negative value.
exc = statistics.StatisticsError
for values in ([-1], [1, -2, 3]):
with self.subTest(values=values):
self.assertRaises(exc, self.func, values)
def test_ints(self):
# Test harmonic mean with ints.
data = [2, 4, 4, 8, 16, 16]
random.shuffle(data)
self.assertEqual(self.func(data), 6*4/5)
def test_floats_exact(self):
# Test harmonic mean with some carefully chosen floats.
data = [1/8, 1/4, 1/4, 1/2, 1/2]
random.shuffle(data)
self.assertEqual(self.func(data), 1/4)
self.assertEqual(self.func([0.25, 0.5, 1.0, 1.0]), 0.5)
def test_singleton_lists(self):
# Test that harmonic mean([x]) returns (approximately) x.
for x in range(1, 101):
if x in (49, 93, 98, 99):
self.assertApproxEqual(self.func([x]), x, tol=2e-14)
else:
self.assertEqual(self.func([x]), x)
def test_decimals_exact(self):
# Test harmonic mean with some carefully chosen Decimals.
D = Decimal
self.assertEqual(self.func([D(15), D(30), D(60), D(60)]), D(30))
data = [D("0.05"), D("0.10"), D("0.20"), D("0.20")]
random.shuffle(data)
self.assertEqual(self.func(data), D("0.10"))
data = [D("1.68"), D("0.32"), D("5.94"), D("2.75")]
random.shuffle(data)
self.assertEqual(self.func(data), D(66528)/70723)
def test_fractions(self):
# Test harmonic mean with Fractions.
F = Fraction
data = [F(1, 2), F(2, 3), F(3, 4), F(4, 5), F(5, 6), F(6, 7), F(7, 8)]
random.shuffle(data)
self.assertEqual(self.func(data), F(7*420, 4029))
def test_inf(self):
# Test harmonic mean with infinity.
values = [2.0, float('inf'), 1.0]
self.assertEqual(self.func(values), 2.0)
def test_nan(self):
# Test harmonic mean with NANs.
values = [2.0, float('nan'), 1.0]
self.assertTrue(math.isnan(self.func(values)))
def test_multiply_data_points(self):
# Test multiplying every data point by a constant.
c = 111
data = [3.4, 4.5, 4.9, 6.7, 6.8, 7.2, 8.0, 8.1, 9.4]
expected = self.func(data)*c
result = self.func([x*c for x in data])
self.assertEqual(result, expected)
def test_doubled_data(self):
# Harmonic mean of [a,b...z] should be same as for [a,a,b,b...z,z].
data = [random.uniform(1, 5) for _ in range(1000)]
expected = self.func(data)
actual = self.func(data*2)
self.assertApproxEqual(actual, expected)
class TestMedian(NumericTestCase, AverageMixin):
# Common tests for median and all median.* functions.
def setUp(self):