From 0b1c731b78db5aed7bdb4d0b89fcf657fb837655 Mon Sep 17 00:00:00 2001 From: Mahammad Insaf Date: Fri, 6 Nov 2026 23:23:17 +0530 Subject: [PATCH] fix percentile reordering data in DescriptiveStatistics and StatUtils The Commons Statistics Quantile used by Statistics.Percentile and StatUtils was built with withCopy(false), so it partially sorts the array it is given. DescriptiveStatistics passes its stored window, so getPercentile (and toString) reordered the values: getValues no longer returned them in insertion order, and a rolling window then discarded the smallest value instead of the oldest. StatUtils.percentile reordered the caller's array. The Percentile class replaced in MATH-1671 worked on a copy. --- .../commons/math4/legacy/stat/StatUtils.java | 2 +- .../legacy/stat/descriptive/Statistics.java | 2 +- .../commons/math4/legacy/stat/StatUtilsTest.java | 8 ++++++++ .../descriptive/DescriptiveStatisticsTest.java | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/StatUtils.java b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/StatUtils.java index 841c465648..b40347049b 100644 --- a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/StatUtils.java +++ b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/StatUtils.java @@ -41,7 +41,7 @@ public final class StatUtils { /** percentile. */ - private static final Quantile QUANTILE = Quantile.withDefaults().with(EstimationMethod.HF6).withCopy(false); + private static final Quantile QUANTILE = Quantile.withDefaults().with(EstimationMethod.HF6).withCopy(true); /** * Private Constructor. diff --git a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/descriptive/Statistics.java b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/descriptive/Statistics.java index 9445ea4c37..a097e7df0a 100644 --- a/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/descriptive/Statistics.java +++ b/commons-math-legacy/src/main/java/org/apache/commons/math4/legacy/stat/descriptive/Statistics.java @@ -316,7 +316,7 @@ static Sum getInstance() { static final class Percentile implements UnivariateStatistic { /** Delegate percentile implementation. */ private static final Quantile QUANTILE = - Quantile.withDefaults().with(EstimationMethod.HF6).withCopy(false); + Quantile.withDefaults().with(EstimationMethod.HF6).withCopy(true); /** Probability. */ private double p; diff --git a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/StatUtilsTest.java b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/StatUtilsTest.java index b5a8dcd5c8..5bc4bfb657 100644 --- a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/StatUtilsTest.java +++ b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/StatUtilsTest.java @@ -456,6 +456,14 @@ public void testPercentile() { TestUtils.assertEquals(2.5, StatUtils.percentile(x, 1, 3, 62.5), TOLERANCE); } + @Test + public void testPercentileDoesNotModifyInput() { + final double[] x = {THREE, ONE, TWO, TWO}; + StatUtils.percentile(x, 50); + StatUtils.percentile(x, 1, 3, 50); + Assert.assertArrayEquals(new double[] {THREE, ONE, TWO, TWO}, x, 0); + } + @Test public void testDifferenceStats() { double sample1[] = {1d, 2d, 3d, 4d}; diff --git a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/descriptive/DescriptiveStatisticsTest.java b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/descriptive/DescriptiveStatisticsTest.java index c47e7f57f0..0fdfba57c9 100644 --- a/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/descriptive/DescriptiveStatisticsTest.java +++ b/commons-math-legacy/src/test/java/org/apache/commons/math4/legacy/stat/descriptive/DescriptiveStatisticsTest.java @@ -125,6 +125,22 @@ public void testGetValues() { Assert.assertEquals(12.0, stats.getElement(88), 1.0e-10); } + @Test + public void testPercentileDoesNotReorderValues() { + DescriptiveStatistics stats = createDescriptiveStatistics(); + stats.setWindowSize(5); + for (double v : new double[] {5, 4, 3, 2, 1}) { + stats.addValue(v); + } + Assert.assertEquals(3, stats.getPercentile(50), 0); + // toString() reports the median + Assert.assertNotNull(stats.toString()); + Assert.assertArrayEquals(new double[] {5, 4, 3, 2, 1}, stats.getValues(), 0); + // The oldest value is discarded + stats.addValue(100); + Assert.assertArrayEquals(new double[] {4, 3, 2, 1, 100}, stats.getValues(), 0); + } + @Test public void testQuadraticMean() { final double[] values = { 1.2, 3.4, 5.6, 7.89 };