This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-statistics.git


The following commit(s) were added to refs/heads/master by this push:
     new 5ad1a09f Compute survival function using summation
5ad1a09f is described below

commit 5ad1a09f7caf84a79cb000d3205629d551ec2309
Author: Alex Herbert <[email protected]>
AuthorDate: Fri Aug 28 17:37:43 2026 +0100

    Compute survival function using summation
    
    This change sums the terms excluded from the CDF so the CDF + SF raw
    sums are the N-th harmonic number.
    
    Requires updates to the high precision SF test. The expected results
    have been independently computed in high precision. The previous values
    from scipy.stats.zipfian had differences in the second significant digit
    to these updated results.
    
    Increases precision when x approaches n. The previous method computed
    the n-th harmonic number minus the (x+1)-th harmonic number. These may
    be very close and there is a loss of precision in the difference
    compared to summing the upper terms to create the difference.
---
 .../statistics/distribution/ZipfDistribution.java  | 34 +++++++++-------------
 .../distribution/ZipfDistributionTest.java         | 28 +++++++++++++-----
 .../statistics/distribution/test.zipf.3.properties |  2 ++
 3 files changed, 37 insertions(+), 27 deletions(-)

diff --git 
a/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/ZipfDistribution.java
 
b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/ZipfDistribution.java
index 2f1d12fd..49d0ae0f 100644
--- 
a/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/ZipfDistribution.java
+++ 
b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/ZipfDistribution.java
@@ -72,7 +72,7 @@ public final class ZipfDistribution extends 
AbstractDiscreteDistribution {
                              double exponent) {
         this.numberOfElements = numberOfElements;
         this.exponent = exponent;
-        this.nthHarmonic = generalizedHarmonic(numberOfElements, exponent);
+        this.nthHarmonic = generalizedHarmonic(1, numberOfElements, exponent);
         logNthHarmonic = Math.log(nthHarmonic);
     }
 
@@ -151,7 +151,7 @@ public final class ZipfDistribution extends 
AbstractDiscreteDistribution {
             return 1;
         }
 
-        return generalizedHarmonic(x, exponent) / nthHarmonic;
+        return generalizedHarmonic(1, x, exponent) / nthHarmonic;
     }
 
     /** {@inheritDoc} */
@@ -163,18 +163,9 @@ public final class ZipfDistribution extends 
AbstractDiscreteDistribution {
             return 0;
         }
 
-        // See http://www.math.wm.edu/~leemis/chart/UDR/PDFs/Zipf.pdf
-        // S(x) = P(X > x) = ((x+1)^a Hn,a - (x+1)^a Hx+1,a + 1) / ((x+1)^a 
Hn,a)
-        // where a = exponent and Hx,a is the generalized harmonic for x with 
exponent a.
-        final double z = Math.pow(x + 1.0, exponent);
-        // Compute generalizedHarmonic(x, exponent) and 
generalizedHarmonic(x+1, exponent)
-        final double hx = generalizedHarmonic(x, exponent);
-        final double hx1 = hx + Math.pow(x + 1.0, -exponent);
-        // Compute the survival function
-        final double p = (z * (nthHarmonic - hx1) + 1) / (z * nthHarmonic);
-        // May overflow for large exponent so validate the probability.
-        // If this occurs revert to 1 - CDF(x), reusing the generalized 
harmonic for x
-        return p <= 1.0 ? p : 1.0 - hx / nthHarmonic;
+        // Compute summation of terms omitted in the CDF.
+        // The raw sums in CDF(x) + SF(x) = N-th harmonic
+        return generalizedHarmonic(x + 1, numberOfElements, exponent) / 
nthHarmonic;
     }
 
     /**
@@ -249,26 +240,29 @@ public final class ZipfDistribution extends 
AbstractDiscreteDistribution {
     }
 
     /**
-     * Calculates the Nth generalized harmonic number. See
+     * Calculates the sum of terms of the
      * <a href="https://mathworld.wolfram.com/HarmonicSeries.html";>Harmonic
      * Series</a>.
      *
      * <pre>
      *          1
-     *   sum  -----  for k in [1, n]
+     *   sum  -----  for k in [from, to]
      *         k^m
      * </pre>
      *
+     * <p>When {@code from = 1} the result is the N-th harmonic number where 
{@code N = to}.
+     *
      * <p>Assumes {@code exponent > 0} to arrange the terms to sum from small 
to large.
      *
-     * @param n Term in the series to calculate (must be larger than 0)
+     * @param from First term in the series to calculate.
+     * @param to Last term in the series to calculate.
      * @param m Exponent (special case {@code m = 1} is the harmonic series).
-     * @return the n<sup>th</sup> generalized harmonic number.
+     * @return the sum
      */
-    private static double generalizedHarmonic(final int n, final double m) {
+    private static double generalizedHarmonic(final int from, final int to, 
final double m) {
         double value = 0;
         // Sum small to large
-        for (int k = n; k >= 1; k--) {
+        for (int k = to; k >= from; k--) {
             value += Math.pow(k, -m);
         }
         return value;
diff --git 
a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ZipfDistributionTest.java
 
b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ZipfDistributionTest.java
index a7f0c723..fcb349d5 100644
--- 
a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ZipfDistributionTest.java
+++ 
b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/ZipfDistributionTest.java
@@ -87,20 +87,34 @@ class ZipfDistributionTest extends 
BaseDiscreteDistributionTest {
     }
 
     static Stream<Arguments> testAdditionlSurvivalProbabilityHighPrecision() {
-        // computed using scipy.stats (1.7.1) zipfian
+        // Computed from the generalized harmonic number and the upper
+        // series of terms using Matlab R2023a VPA, e.g.:
+        // vpa(symsum(1/k^10, k, 1, 60), 30)
+        // vpa(symsum(1/k^10, k, 58, 60), 30)
+        // vpa(symsum(1/k^10, k, 60, 60), 30)
+        // Generalized harmonic numbers, the upper summations are inlined below
+        final double k60e10 = 1.00099457512781807511565108861;
+        final double k60e505 = 1.00000000000000062803698427773;
+        final double k60e1005 = 1.0;
         return Stream.of(
             Arguments.of(60, 10,
                 new int[] {57, 59},
-                new double[] {2.3189337454689757e-18, 1.6521739576668957e-18},
-                DoubleTolerances.absolute(1e-25)),
+                new double[] {
+                    0.00000000000000000593155740928262062795573723513 / k60e10,
+                    0.0000000000000000016538171687920201866246676489 / k60e10},
+                DoubleTolerances.relative(1e-14)),
             Arguments.of(60, 50.5,
                 new int[] {57, 59},
-                new double[] {8.8488396450491320e-90, 1.5972093932264611e-90},
-                DoubleTolerances.absolute(1e-95)),
+                new double[] {
+                    1.41783221158702775324465028711e-89 / k60e505,
+                    1.59720939322646230873883366414e-90 / k60e505},
+                DoubleTolerances.relative(1e-14)),
             Arguments.of(60, 100.5,
                 new int[] {57, 59},
-                new double[] {5.9632998443758656e-178, 
1.9760564023408183e-179},
-                DoubleTolerances.absolute(1e-185))
+                new double[] {
+                    7.23087851232732627244617172568e-178 / k60e1005,
+                    1.9760564023408181841715991846e-179 / k60e1005},
+                DoubleTolerances.relative(1e-14))
         );
     }
 
diff --git 
a/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.zipf.3.properties
 
b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.zipf.3.properties
index 265da1ef..47679093 100644
--- 
a/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.zipf.3.properties
+++ 
b/commons-statistics-distribution/src/test/resources/org/apache/commons/statistics/distribution/test.zipf.3.properties
@@ -45,3 +45,5 @@ sf.values =  \
   0.01185995655971061  , 0.008563148175057236 ,                                
                                                  \
   0.0058478019355387234, 0.003576338667001895 ,                                
                                                  \
   0.0016508814447150884, 0.
+
+sf.relative = 5e-14

Reply via email to