Skip to content

Commit c021753

Browse files
committed
gh-150318: Fix quantiles(method='exclusive') returning unsorted cut points for duplicate floats
When all data points are identical floats, the interpolation formula in the exclusive branch can produce results off by 1 ULP due to floating point rounding. Hence, adjacent cut points differ and the returned list violates the non-decreasing rule. This short-circuits the interpolation, returning the data value directly instead.
1 parent fad0674 commit c021753

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

Lib/statistics.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,13 @@ def quantiles(data, *, n=4, method='exclusive'):
12171217
j = i * m // n # rescale i to m/n
12181218
j = 1 if j < 1 else ld-1 if j > ld-1 else j # clamp to 1 .. ld-1
12191219
delta = i*m - j*n # exact integer math
1220-
interpolated = (data[j - 1] * (n - delta) + data[j] * delta) / n
1220+
# When the endpoints are equal or delta is zero, avoid
1221+
# the interpolation formula which can be off by 1 ULP
1222+
# due to floating-point rounding
1223+
if (data[j - 1] == data[j]) or not delta:
1224+
interpolated = data[j - 1]
1225+
else:
1226+
interpolated = (data[j - 1] * (n - delta) + data[j] * delta) / n
12211227
result.append(interpolated)
12221228
return result
12231229

Lib/test/test_statistics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,24 @@ def test_equal_inputs(self):
26522652
self.assertEqual(quantiles(data, method='inclusive'),
26532653
[10.0, 10.0, 10.0])
26542654

2655+
def test_monotonic_with_duplicate_floats(self):
2656+
quantiles = statistics.quantiles
2657+
for x in (3.141592653589793, # irrational-ish
2658+
1/3, # repeating binary fraction
2659+
0.1, # non-exact decimal
2660+
2.0, # exact power of two
2661+
1e300, # large magnitude
2662+
1e-300, # small magnitude
2663+
float.fromhex('0x1.fffffffffffffp+1023'), # near max float
2664+
sys.float_info.min, # smallest normal
2665+
):
2666+
for n in range(2, 20):
2667+
result = quantiles([x, x], n=n, method='exclusive')
2668+
self.assertEqual(result, sorted(result),
2669+
msg=f'x={x}, n={n}')
2670+
self.assertTrue(all(v == x for v in result),
2671+
msg=f'x={x}, n={n}')
2672+
26552673
def test_equal_sized_groups(self):
26562674
quantiles = statistics.quantiles
26572675
total = 10_000

0 commit comments

Comments
 (0)