Skip to content

Commit 679c407

Browse files
authored
Clamp exponential histogram percentiles to min/max (#135632)
1 parent b15b23b commit 679c407

File tree

2 files changed

+46
-0
lines changed

2 files changed

+46
-0
lines changed

libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramQuantile.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class ExponentialHistogramQuantile {
3434
* It returns the value of the element at rank {@code max(0, min(n - 1, (quantile * (n + 1)) - 1))}, where n is the total number of
3535
* values and rank starts at 0. If the rank is fractional, the result is linearly interpolated from the values of the two
3636
* neighboring ranks.
37+
* The result is clamped to the histogram's minimum and maximum values.
3738
*
3839
* @param histo the histogram representing the distribution
3940
* @param quantile the quantile to query, in the range [0, 1]
@@ -60,6 +61,8 @@ public static double getQuantile(ExponentialHistogram histo, double quantile) {
6061
double upperFactor = exactRank - lowerRank;
6162

6263
ValueAndPreviousValue values = getElementAtRank(histo, upperRank);
64+
// Ensure we don't return values outside the histogram's range
65+
values = values.clampTo(histo.min(), histo.max());
6366

6467
double result;
6568
if (lowerRank == upperRank) {
@@ -158,8 +161,13 @@ private static ValueAndPreviousValue getBucketMidpointForRank(BucketIterator buc
158161
* @param valueAtRank the value at the desired rank
159162
*/
160163
private record ValueAndPreviousValue(double valueAtPreviousRank, double valueAtRank) {
164+
161165
ValueAndPreviousValue negateAndSwap() {
162166
return new ValueAndPreviousValue(-valueAtRank, -valueAtPreviousRank);
163167
}
168+
169+
ValueAndPreviousValue clampTo(double min, double max) {
170+
return new ValueAndPreviousValue(Math.clamp(valueAtPreviousRank, min, max), Math.clamp(valueAtRank, min, max));
171+
}
164172
}
165173
}

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/QuantileAccuracyTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ public void testNoNegativeZeroReturned() {
6363
assertThat(median, equalTo(0.0));
6464
}
6565

66+
public void testPercentilesClampedToMinMax() {
67+
ExponentialHistogram histogram = createAutoReleasedHistogram(
68+
b -> b.scale(0).setNegativeBucket(1, 1).setPositiveBucket(1, 1).max(0.00001).min(-0.00002)
69+
);
70+
double p0 = ExponentialHistogramQuantile.getQuantile(histogram, 0.0);
71+
double p100 = ExponentialHistogramQuantile.getQuantile(histogram, 1.0);
72+
assertThat(p0, equalTo(-0.00002));
73+
assertThat(p100, equalTo(0.00001));
74+
}
75+
76+
public void testMinMaxClampedPercentileAccuracy() {
77+
ExponentialHistogram histogram = createAutoReleasedHistogram(
78+
b -> b.scale(0)
79+
.setPositiveBucket(0, 1) // bucket 0 covers (1, 2]
80+
.setPositiveBucket(1, 1) // bucket 1 covers (2, 4]
81+
.min(1.1)
82+
.max(2.1)
83+
);
84+
85+
// The 0.5 percentile linearly interpolates between the two buckets.
86+
// For the (1, 2] bucket, the point of least relative error will be used (1.33333)
87+
// For the (2, 4] bucket, the max of the histogram should be used instead (2.1)
88+
double expectedResult = (4.0 / 3 + 2.1) / 2;
89+
double p50 = ExponentialHistogramQuantile.getQuantile(histogram, 0.5);
90+
assertThat(p50, equalTo(expectedResult));
91+
92+
// Test the same for min instead of max
93+
histogram = createAutoReleasedHistogram(
94+
b -> b.scale(0)
95+
.setNegativeBucket(0, 1) // bucket 0 covers (1, 2]
96+
.setNegativeBucket(1, 1) // bucket 1 covers (2, 4]
97+
.min(-2.1)
98+
.max(-1.1)
99+
);
100+
p50 = ExponentialHistogramQuantile.getQuantile(histogram, 0.5);
101+
assertThat(p50, equalTo(-expectedResult));
102+
}
103+
66104
public void testUniformDistribution() {
67105
testDistributionQuantileAccuracy(new UniformRealDistribution(new Well19937c(randomInt()), 0, 100));
68106
}

0 commit comments

Comments
 (0)