[SPARK-57982][SQL] Make exact percentile monotonic for large values#57241
[SPARK-57982][SQL] Make exact percentile monotonic for large values#57241Ma77Ball wants to merge 7 commits into
Conversation
Compute linear interpolation as lower + fraction * (higher - lower) instead of (higher - position) * lower + (position - lower) * higher, which for large-magnitude values rounded non-monotonically and let percentile(x, 0.04) exceed percentile(x, 0.05).
|
Hi @MaxGekk / @peter-toth, PTAL at this small percentile monotonicity fix (SPARK-57982) when you get a chance. |
|
cc @cloud-fan |
yadavay-amzn
left a comment
There was a problem hiding this comment.
Nice fix, and the monotonicity argument holds up
I checked the numeric claim and it looks solid: the old (1-t)*lo + t*hi form is bounded but not monotonic, and switching to lo + t*(hi-lo) fixes that. One optional non-blocking thought inline. Thanks for tracking this down @Ma77Ball.
What I verified:
- Reproduced the described break: on the
1e18 + i*128.0data the old form givespercentile(0.04)=1.0000000000000001e18which is greater thanpercentile(0.05)=1e18, and across the 0..100 sweep the old form has 6 monotonicity violations while the new form has 0. - The common path isn't regressed:
2012.3is the exactly-rounded result for thecourseSalescase, so the golden update looks like an improvement rather than a regression. - The new test does fail on master and pass with the change.
percentile_cont/median/WITHIN GROUPand thefrequencyvariant all route through this samegetPercentile, so they're covered too.
| // Linear interpolation to get the exact percentile | ||
| (higher - position) * toDoubleValue(lowerKey) + (position - lower) * toDoubleValue(higherKey) | ||
| toDoubleValue(lowerKey) + | ||
| (position - lower) * (toDoubleValue(higherKey) - toDoubleValue(lowerKey)) |
There was a problem hiding this comment.
Optional, non-blocking. Computing higher - lower here can overflow to Infinity when the two adjacent keys straddle zero with magnitudes near Double.MaxValue (e.g. lower=-9e307, higher=9e307 makes the delta overflow, so p50 comes back as Infinity where the old form returned a finite value). It's an extremely unlikely input and the old form had its own accuracy issues in that range, so I don't think it should block anything. Might be worth a one-line comment noting the tradeoff, or nothing at all. Your call.
There was a problem hiding this comment.
Hello @yadavay-amzn, thank you for the review. I've added a comment noting the tradeoff so it's flagged if the overflow ever surfaces for values near Double.MaxValue.
…near Double.MaxValue
|
The fix looks correct and the shape is right -- keeping it in the shared One thing the IMO this reads more as a numerical-robustness improvement than a correctness bug fix: the old form was a standard, valid interpolation, results only move in the last ULP, and no documented monotonicity or exact-rounding guarantee is being broken. So shipping it in the next feature release without backporting to maintenance branches seems right. Suggested migration-guide bullet, under
|
…from Spark SQL 4.2 to 4.3' as suggested
…centile-monotonicity
|
Hello @peter-toth, thank you for the review. I've added the migration-guide bullet point as suggested under 4.2 to 4.3. |
|
Hey @Ma77Ball, thanks for the fix and quick iteration. |
yadavay-amzn
left a comment
There was a problem hiding this comment.
LGTM, thanks for making this change
### What changes were proposed in this pull request?
Rewrite the exact `percentile` linear interpolation in `PercentileBase.getPercentile` from `(higher - position) * lower + (position - lower) * higher` to the equivalent `lower + (position - lower) * (higher - lower)`. This method is shared by `percentile`, `percentile_cont`, `median`, and their `WITHIN GROUP` forms.
### Why are the changes needed?
The two forms are equal in exact arithmetic, but the old one scales both (potentially large) endpoint values by the weights independently, so rounding error on the order of the gap between adjacent doubles can make the result non-monotonic in the percentage:
```
val df = (0 until 20).map(i => 1e18 + i * 128.0).toDF("x")
df.selectExpr("percentile(x, 0.04)", "percentile(x, 0.05)").show(false)
// before: 1.00000000000000013E18, 1.0E18 (p04 > p05, not monotonic)
// after: 1.00000000000000013E18, 1.00000000000000013E18
```
The new form scales only the small `(higher - lower)` delta, so the result stays monotonic.
### Does this PR introduce _any_ user-facing change?
Yes. Exact `percentile` / `percentile_cont` / `median` on large-magnitude inputs can change in the last ULP and are now monotonic in the percentage.
### How was this patch tested?
Added UT in `PercentileQuerySuite`; existing percentile suites and the `percentiles.sql` golden file pass unchanged.
### Was this patch authored or co-authored using generative AI tooling?
Yes, co-authored with Claude Opus 4.8
Closes #57241 from Ma77Ball/SPARK-57982-percentile-monotonicity.
Authored-by: Matthew B. <mgball@uci.edu>
Signed-off-by: Peter Toth <peter.toth@gmail.com>
(cherry picked from commit 7353508)
Signed-off-by: Peter Toth <peter.toth@gmail.com>
|
Thank you @Ma77Ball for the fix and congratulations to your first Apache Spark contribution! Thank you @yadavay-amzn for the review! |
|
Thank you, @peter-toth and @yadavay-amzn, for reviewing and helping me finish my first contribution. I greatly appreciate your help. JIRA ID: ma77ball |
What changes were proposed in this pull request?
Rewrite the exact
percentilelinear interpolation inPercentileBase.getPercentilefrom(higher - position) * lower + (position - lower) * higherto the equivalentlower + (position - lower) * (higher - lower). This method is shared bypercentile,percentile_cont,median, and theirWITHIN GROUPforms.Why are the changes needed?
The two forms are equal in exact arithmetic, but the old one scales both (potentially large) endpoint values by the weights independently, so rounding error on the order of the gap between adjacent doubles can make the result non-monotonic in the percentage:
The new form scales only the small
(higher - lower)delta, so the result stays monotonic.Does this PR introduce any user-facing change?
Yes. Exact
percentile/percentile_cont/medianon large-magnitude inputs can change in the last ULP and are now monotonic in the percentage.How was this patch tested?
Added UT in
PercentileQuerySuite; existing percentile suites and thepercentiles.sqlgolden file pass unchanged.Was this patch authored or co-authored using generative AI tooling?
Yes, co-authored with Claude Opus 4.8