Skip to content

Commit 6463ba3

Browse files
authored
bpo-27181: Add statistics.geometric_mean() (GH-12638)
1 parent 9d7b2c0 commit 6463ba3

5 files changed

Lines changed: 132 additions & 1 deletion

File tree

Doc/library/statistics.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ or sample.
4040
======================= ===============================================================
4141
:func:`mean` Arithmetic mean ("average") of data.
4242
:func:`fmean` Fast, floating point arithmetic mean.
43+
:func:`geometric_mean` Geometric mean of data.
4344
:func:`harmonic_mean` Harmonic mean of data.
4445
:func:`median` Median (middle value) of data.
4546
:func:`median_low` Low median of data.
@@ -130,6 +131,24 @@ However, for reading convenience, most of the examples show sorted sequences.
130131
.. versionadded:: 3.8
131132

132133

134+
.. function:: geometric_mean(data)
135+
136+
Convert *data* to floats and compute the geometric mean.
137+
138+
Raises a :exc:`StatisticsError` if the input dataset is empty,
139+
if it contains a zero, or if it contains a negative value.
140+
141+
No special efforts are made to achieve exact results.
142+
(However, this may change in the future.)
143+
144+
.. doctest::
145+
146+
>>> round(geometric_mean([54, 24, 36]), 9)
147+
36.0
148+
149+
.. versionadded:: 3.8
150+
151+
133152
.. function:: harmonic_mean(data)
134153

135154
Return the harmonic mean of *data*, a sequence or iterator of

Doc/whatsnew/3.8.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ Added :func:`statistics.fmean` as a faster, floating point variant of
322322
:func:`statistics.mean()`. (Contributed by Raymond Hettinger and
323323
Steven D'Aprano in :issue:`35904`.)
324324

325+
Added :func:`statistics.geometric_mean()`
326+
(Contributed by Raymond Hettinger in :issue:`27181`.)
327+
325328
Added :func:`statistics.multimode` that returns a list of the most
326329
common values. (Contributed by Raymond Hettinger in :issue:`35892`.)
327330

Lib/statistics.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
Function Description
1212
================== =============================================
1313
mean Arithmetic mean (average) of data.
14+
geometric_mean Geometric mean of data.
1415
harmonic_mean Harmonic mean of data.
1516
median Median (middle value) of data.
1617
median_low Low median of data.
1718
median_high High median of data.
1819
median_grouped Median, or 50th percentile, of grouped data.
1920
mode Mode (most common value) of data.
20-
multimode List of modes (most common values of data)
21+
multimode List of modes (most common values of data).
2122
================== =============================================
2223
2324
Calculate the arithmetic mean ("the average") of data:
@@ -81,6 +82,7 @@
8182
'pstdev', 'pvariance', 'stdev', 'variance',
8283
'median', 'median_low', 'median_high', 'median_grouped',
8384
'mean', 'mode', 'multimode', 'harmonic_mean', 'fmean',
85+
'geometric_mean',
8486
]
8587

8688
import math
@@ -328,6 +330,24 @@ def count(x):
328330
except ZeroDivisionError:
329331
raise StatisticsError('fmean requires at least one data point') from None
330332

333+
def geometric_mean(data):
334+
"""Convert data to floats and compute the geometric mean.
335+
336+
Raises a StatisticsError if the input dataset is empty,
337+
if it contains a zero, or if it contains a negative value.
338+
339+
No special efforts are made to achieve exact results.
340+
(However, this may change in the future.)
341+
342+
>>> round(geometric_mean([54, 24, 36]), 9)
343+
36.0
344+
"""
345+
try:
346+
return exp(fmean(map(log, data)))
347+
except ValueError:
348+
raise StatisticsError('geometric mean requires a non-empty dataset '
349+
' containing positive numbers') from None
350+
331351
def harmonic_mean(data):
332352
"""Return the harmonic mean of data.
333353

Lib/test/test_statistics.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,94 @@ def test_compare_to_variance(self):
20382038
expected = math.sqrt(statistics.variance(data))
20392039
self.assertEqual(self.func(data), expected)
20402040

2041+
class TestGeometricMean(unittest.TestCase):
2042+
2043+
def test_basics(self):
2044+
geometric_mean = statistics.geometric_mean
2045+
self.assertAlmostEqual(geometric_mean([54, 24, 36]), 36.0)
2046+
self.assertAlmostEqual(geometric_mean([4.0, 9.0]), 6.0)
2047+
self.assertAlmostEqual(geometric_mean([17.625]), 17.625)
2048+
2049+
random.seed(86753095551212)
2050+
for rng in [
2051+
range(1, 100),
2052+
range(1, 1_000),
2053+
range(1, 10_000),
2054+
range(500, 10_000, 3),
2055+
range(10_000, 500, -3),
2056+
[12, 17, 13, 5, 120, 7],
2057+
[random.expovariate(50.0) for i in range(1_000)],
2058+
[random.lognormvariate(20.0, 3.0) for i in range(2_000)],
2059+
[random.triangular(2000, 3000, 2200) for i in range(3_000)],
2060+
]:
2061+
gm_decimal = math.prod(map(Decimal, rng)) ** (Decimal(1) / len(rng))
2062+
gm_float = geometric_mean(rng)
2063+
self.assertTrue(math.isclose(gm_float, float(gm_decimal)))
2064+
2065+
def test_various_input_types(self):
2066+
geometric_mean = statistics.geometric_mean
2067+
D = Decimal
2068+
F = Fraction
2069+
# https://www.wolframalpha.com/input/?i=geometric+mean+3.5,+4.0,+5.25
2070+
expected_mean = 4.18886
2071+
for data, kind in [
2072+
([3.5, 4.0, 5.25], 'floats'),
2073+
([D('3.5'), D('4.0'), D('5.25')], 'decimals'),
2074+
([F(7, 2), F(4, 1), F(21, 4)], 'fractions'),
2075+
([3.5, 4, F(21, 4)], 'mixed types'),
2076+
((3.5, 4.0, 5.25), 'tuple'),
2077+
(iter([3.5, 4.0, 5.25]), 'iterator'),
2078+
]:
2079+
actual_mean = geometric_mean(data)
2080+
self.assertIs(type(actual_mean), float, kind)
2081+
self.assertAlmostEqual(actual_mean, expected_mean, places=5)
2082+
2083+
def test_big_and_small(self):
2084+
geometric_mean = statistics.geometric_mean
2085+
2086+
# Avoid overflow to infinity
2087+
large = 2.0 ** 1000
2088+
big_gm = geometric_mean([54.0 * large, 24.0 * large, 36.0 * large])
2089+
self.assertTrue(math.isclose(big_gm, 36.0 * large))
2090+
self.assertFalse(math.isinf(big_gm))
2091+
2092+
# Avoid underflow to zero
2093+
small = 2.0 ** -1000
2094+
small_gm = geometric_mean([54.0 * small, 24.0 * small, 36.0 * small])
2095+
self.assertTrue(math.isclose(small_gm, 36.0 * small))
2096+
self.assertNotEqual(small_gm, 0.0)
2097+
2098+
def test_error_cases(self):
2099+
geometric_mean = statistics.geometric_mean
2100+
StatisticsError = statistics.StatisticsError
2101+
with self.assertRaises(StatisticsError):
2102+
geometric_mean([]) # empty input
2103+
with self.assertRaises(StatisticsError):
2104+
geometric_mean([3.5, 0.0, 5.25]) # zero input
2105+
with self.assertRaises(StatisticsError):
2106+
geometric_mean([3.5, -4.0, 5.25]) # negative input
2107+
with self.assertRaises(StatisticsError):
2108+
geometric_mean(iter([])) # empty iterator
2109+
with self.assertRaises(TypeError):
2110+
geometric_mean(None) # non-iterable input
2111+
with self.assertRaises(TypeError):
2112+
geometric_mean([10, None, 20]) # non-numeric input
2113+
with self.assertRaises(TypeError):
2114+
geometric_mean() # missing data argument
2115+
with self.assertRaises(TypeError):
2116+
geometric_mean([10, 20, 60], 70) # too many arguments
2117+
2118+
def test_special_values(self):
2119+
# Rules for special values are inherited from math.fsum()
2120+
geometric_mean = statistics.geometric_mean
2121+
NaN = float('Nan')
2122+
Inf = float('Inf')
2123+
self.assertTrue(math.isnan(geometric_mean([10, NaN])), 'nan')
2124+
self.assertTrue(math.isnan(geometric_mean([NaN, Inf])), 'nan and infinity')
2125+
self.assertTrue(math.isinf(geometric_mean([10, Inf])), 'infinity')
2126+
with self.assertRaises(ValueError):
2127+
geometric_mean([Inf, -Inf])
2128+
20412129
class TestNormalDist(unittest.TestCase):
20422130

20432131
# General note on precision: The pdf(), cdf(), and overlap() methods
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add statistics.geometric_mean().

0 commit comments

Comments
 (0)