From c31ff6df42747d0c8dd9c52864f85f5c49b059e4 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 8 Dec 2023 11:21:04 -0600 Subject: [PATCH 1/2] gh-112540: Support zero inputs in geometric_mean() --- Lib/statistics.py | 30 ++++++++++++++----- Lib/test/test_statistics.py | 12 ++++++-- ...-12-08-11-17-17.gh-issue-112540.Pm5egX.rst | 2 ++ 3 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-12-08-11-17-17.gh-issue-112540.Pm5egX.rst diff --git a/Lib/statistics.py b/Lib/statistics.py index 4da06889c6db469..51688f66292ca36 100644 --- a/Lib/statistics.py +++ b/Lib/statistics.py @@ -527,8 +527,10 @@ def count(iterable): def geometric_mean(data): """Convert data to floats and compute the geometric mean. - Raises a StatisticsError if the input dataset is empty, - if it contains a zero, or if it contains a negative value. + Raises a StatisticsError if the input dataset is empty + of if it contains a negative value. + + Returns zero if the product of inputs is zero. No special efforts are made to achieve exact results. (However, this may change in the future.) @@ -536,11 +538,25 @@ def geometric_mean(data): >>> round(geometric_mean([54, 24, 36]), 9) 36.0 """ - try: - return exp(fmean(map(log, data))) - except ValueError: - raise StatisticsError('geometric mean requires a non-empty dataset ' - 'containing positive numbers') from None + n = 0 + found_zero = False + def count_positive(iterable): + nonlocal n, found_zero + for n, x in enumerate(iterable, start=1): + if x > 0.0 or math.isnan(x): + yield x + elif x == 0.0: + found_zero = True + else: + raise StatisticsError('No negative inputs allowed', x) + total = fsum(map(log, count_positive(data))) + if not n: + raise StatisticsError('Must have a non-empty dataset') + if math.isnan(total): + return math.nan + if found_zero: + return math.nan if total == math.inf else 0.0 + return exp(total / n) def harmonic_mean(data, weights=None): diff --git a/Lib/test/test_statistics.py b/Lib/test/test_statistics.py index b24fc3c3d077fe4..bf2c254c9ee7d9a 100644 --- a/Lib/test/test_statistics.py +++ b/Lib/test/test_statistics.py @@ -2302,10 +2302,12 @@ def test_error_cases(self): StatisticsError = statistics.StatisticsError with self.assertRaises(StatisticsError): geometric_mean([]) # empty input - with self.assertRaises(StatisticsError): - geometric_mean([3.5, 0.0, 5.25]) # zero input with self.assertRaises(StatisticsError): geometric_mean([3.5, -4.0, 5.25]) # negative input + with self.assertRaises(StatisticsError): + geometric_mean([0.0, -4.0, 5.25]) # negative input with zero + with self.assertRaises(StatisticsError): + geometric_mean([3.5, -math.inf, 5.25]) # negative infinity with self.assertRaises(StatisticsError): geometric_mean(iter([])) # empty iterator with self.assertRaises(TypeError): @@ -2328,6 +2330,12 @@ def test_special_values(self): with self.assertRaises(ValueError): geometric_mean([Inf, -Inf]) + # Cases with zero + self.assertEqual(geometric_mean([3, 0.0, 5]), 0.0) # Any zero gives a zero + self.assertEqual(geometric_mean([3, -0.0, 5]), 0.0) # Negative zero allowed + self.assertTrue(math.isnan(geometric_mean([0, NaN]))) # NaN beats zero + self.assertTrue(math.isnan(geometric_mean([0, Inf]))) # Because 0.0 * Inf -> NaN + def test_mixed_int_and_float(self): # Regression test for b.p.o. issue #28327 geometric_mean = statistics.geometric_mean diff --git a/Misc/NEWS.d/next/Library/2023-12-08-11-17-17.gh-issue-112540.Pm5egX.rst b/Misc/NEWS.d/next/Library/2023-12-08-11-17-17.gh-issue-112540.Pm5egX.rst new file mode 100644 index 000000000000000..263b13d1762bf1e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-12-08-11-17-17.gh-issue-112540.Pm5egX.rst @@ -0,0 +1,2 @@ +The statistics.geometric_mean() function now returns zero for datasets +containing a zero. Formerly, it would raise an exception. From 7bb47cbd7b4d7751842c40a849964e9a3713507d Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 8 Dec 2023 11:46:26 -0600 Subject: [PATCH 2/2] of -> or --- Lib/statistics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/statistics.py b/Lib/statistics.py index 51688f66292ca36..83aaedb04515e02 100644 --- a/Lib/statistics.py +++ b/Lib/statistics.py @@ -528,7 +528,7 @@ def geometric_mean(data): """Convert data to floats and compute the geometric mean. Raises a StatisticsError if the input dataset is empty - of if it contains a negative value. + or if it contains a negative value. Returns zero if the product of inputs is zero.