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 2535c20a STATISTICS-102: Fix the Cauchy distribution quantile function 
in the tails (#140)
2535c20a is described below

commit 2535c20ad85ce756c0d396151d83e7fc4e2c904c
Author: 陈志谦 <[email protected]>
AuthorDate: Fri Nov 6 17:36:48 2026 +0800

    STATISTICS-102: Fix the Cauchy distribution quantile function in the tails 
(#140)
    
    CauchyDistribution.inverseCumulativeProbability(p) computes
    location + scale * tan(pi * (p - 0.5)). For a probability close to 0 or 1 
the
    subtraction of one half cancels the significant digits of p, so the tangent
    argument is rounded to +/- pi / 2 and the result saturates at about
    +/- 1.633e16 * scale whatever the probability:
    
        CauchyDistribution.of(0.5, 2).inverseCumulativeProbability(1e-20)
        // -3.266247870639074e16, expected -6.366197723675814e19
        CauchyDistribution.of(0.5, 2).inverseCumulativeProbability(1 - 1e-16)
        // 3.957875932190438e15, expected 5.73416113922266e15
    
    The loss of accuracy starts in the middle of the tail, not just at the
    saturation: the value at p = 1e-5 is out by 5e-12 relative (the value at
    p = 1e-10 by 5e-7). The function is not even monotonic (successive
    probabilities below 1e-18 can return the same value) and the median is off 
by
    a rounding of pi (2.1e-16 relative for the scale 2, location 1.2 test).
    
    Use the equivalent forms tan(pi (p - 1/2)) = -cot(pi p) and
    cot(pi (1 - p)) for the lower and upper tail. The cotangent is computed with
    1 / tan(x) or cos(x) / sin(x) to avoid the relative error of sin(x) for 
small
    x, and is exactly zero at the median.
    
    Assisted-By: DeepSeek dsv41
    
    Signed-off-by: simpleqt <[email protected]>
---
 .../distribution/CauchyDistribution.java           | 45 ++++++++++++-
 .../distribution/CauchyDistributionTest.java       | 74 ++++++++++++++++++++++
 src/changes/changes.xml                            |  7 ++
 3 files changed, 124 insertions(+), 2 deletions(-)

diff --git 
a/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/CauchyDistribution.java
 
b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/CauchyDistribution.java
index b412db41..30670ae1 100644
--- 
a/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/CauchyDistribution.java
+++ 
b/commons-statistics-distribution/src/main/java/org/apache/commons/statistics/distribution/CauchyDistribution.java
@@ -34,6 +34,8 @@ import 
org.apache.commons.rng.sampling.distribution.StableSampler;
  * @see <a href="https://mathworld.wolfram.com/CauchyDistribution.html";>Cauchy 
distribution (MathWorld)</a>
  */
 public final class CauchyDistribution extends AbstractContinuousDistribution {
+    /** The value of one half. */
+    private static final double HALF = 0.5;
     /** The location of this distribution. */
     private final double location;
     /** The scale of this distribution. */
@@ -136,7 +138,7 @@ public final class CauchyDistribution extends 
AbstractContinuousDistribution {
         } else  if (p == 1) {
             return Double.POSITIVE_INFINITY;
         }
-        return location + scale * Math.tan(Math.PI * (p - 0.5));
+        return location + scale * tailTangent(p);
     }
 
     /**
@@ -153,7 +155,46 @@ public final class CauchyDistribution extends 
AbstractContinuousDistribution {
         } else  if (p == 0) {
             return Double.POSITIVE_INFINITY;
         }
-        return location - scale * Math.tan(Math.PI * (p - 0.5));
+        return location - scale * tailTangent(p);
+    }
+
+    /**
+     * Compute the value of \( \tan(\pi (p - 1/2)) \) for a probability \( p 
\) strictly within (0, 1).
+     *
+     * <p>The subtraction of one half from \( p \) loses the significant 
digits of a probability
+     * close to 0 or 1, so the tails are computed using the equivalent forms
+     * \( -\cot(\pi p) \) and \( \cot(\pi (1 - p)) \) respectively.
+     *
+     * @param p Probability.
+     * @return tan(pi (p - 1/2))
+     */
+    private static double tailTangent(double p) {
+        // The tangent is exactly zero at the median; this also avoids the 
rounding
+        // of pi (p - 1/2) to a non-zero value.
+        if (p == HALF) {
+            return 0;
+        }
+        // Avoid the cancellation of the significant digits of p in (p - 1/2).
+        // Assume p != 1/2; the two branches agree at the rounding of pi p.
+        return p < HALF ?
+            -cotPi(p) :
+            cotPi(1 - p);
+    }
+
+    /**
+     * Compute the cotangent of {@code pi x} for a value {@code x} in (0, 0.5].
+     *
+     * <p>For small \( x \), \( \sin(\pi x) \) has a relative error of
+     * \( (\pi x)^2 / 6 \) so the division by it is avoided.
+     *
+     * @param x Value.
+     * @return cot(pi x)
+     */
+    private static double cotPi(double x) {
+        final double px = Math.PI * x;
+        return px < HALF ?
+            1 / Math.tan(px) :
+            Math.cos(px) / Math.sin(px);
     }
 
     /**
diff --git 
a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/CauchyDistributionTest.java
 
b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/CauchyDistributionTest.java
index f2a894e6..833112cd 100644
--- 
a/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/CauchyDistributionTest.java
+++ 
b/commons-statistics-distribution/src/test/java/org/apache/commons/statistics/distribution/CauchyDistributionTest.java
@@ -17,6 +17,9 @@
 
 package org.apache.commons.statistics.distribution;
 
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
 /**
  * Test cases for {@link CauchyDistribution}.
  * Extends {@link BaseContinuousDistributionTest}. See javadoc of that class 
for details.
@@ -48,4 +51,75 @@ class CauchyDistributionTest extends 
BaseContinuousDistributionTest {
     protected double getRelativeTolerance() {
         return 5e-15;
     }
+
+    //-------------------- Additional test cases 
-------------------------------
+
+    /**
+     * Test the quantile function in the tails of the distribution.
+     *
+     * <p>The standard computation \( x_0 + \gamma \tan(\pi (p - 1/2)) \) 
loses every
+     * significant digit of a probability close to 0 or 1: the tangent 
argument is
+     * rounded to \( \pm \pi / 2 \) and the result saturates at
+     * \( \pm 1.633 \times 10^{16} \gamma \). The expected values are computed 
using
+     * {@code scipy.stats.cauchy(loc=x0, scale=gamma)}.
+     */
+    @Test
+    void testInverseProbabilityTails() {
+        // Position and scale with location 0.5 and scale 2
+        final CauchyDistribution dist = CauchyDistribution.of(0.5, 2);
+        final double[] p = {
+            0.5, 1e-1, 1e-5, 1e-10, 1e-20, 1e-100, 1e-300,
+            1 - 1e-7, 1 - 1e-12, 1 - 1e-16, 1 - 2 * 0x1.0p-53, 0x1.0p-53, 
0x1.0p-60
+        };
+        final double[] icdf = {
+            0.5, -5.655367074350508, -63661.47721581417, -6366197723.175813,
+            -6.366197723675814e19, -6.366197723675814e99, 
-6.366197723675814e299,
+            6366198.22702649, 636633855804.0593, 5734161139222660.0, 
2867080569611330.0,
+            -5734161139222658.0, -7.339726258205004e17
+        };
+        for (int i = 0; i < p.length; i++) {
+            final double pi = p[i];
+            final double expected = icdf[i];
+            final double actual = dist.inverseCumulativeProbability(pi);
+            final double tolerance = Math.abs(expected) * 4e-16 + 1e-15;
+            Assertions.assertEquals(expected, actual, tolerance, () -> "icdf(" 
+ pi + ")");
+            // The survival function is the reflection of the cumulative value 
in the location
+            Assertions.assertEquals(2 * dist.getMedian() - expected, 
dist.inverseSurvivalProbability(pi),
+                    tolerance, () -> "isf(" + pi + ")");
+        }
+    }
+
+    /**
+     * Test the quantile function in the tails is monotonic and antisymmetric.
+     *
+     * <p>The tail values can not be checked with a round trip through the 
cumulative
+     * probability: \( 1/2 + \arctan(x) / \pi \) cancels by \( \pi / 2 \) so 
the relative
+     * accuracy of {@code cdf(icdf(p))} is \( \mathrm{ulp}(\pi/2) / p \) and 
is zero for
+     * the smallest probabilities.
+     */
+    @Test
+    void testInverseProbabilityTailsAreMonotonic() {
+        // The quantile function of the standard distribution is cot(pi p)
+        final CauchyDistribution dist = CauchyDistribution.of(0, 1);
+        double previous = dist.inverseCumulativeProbability(0.5);
+        Assertions.assertEquals(0, previous, "icdf(0.5)");
+        for (int i = -1; i >= -300; i--) {
+            final double p = Math.pow(10, i);
+            final double x = dist.inverseCumulativeProbability(p);
+            // Strictly decreasing towards the lower tail and finite
+            Assertions.assertTrue(x < previous, () -> "icdf(" + p + ")");
+            Assertions.assertTrue(x < 0, () -> "icdf(" + p + ")");
+            // The survival value is the reflection in the support centre
+            Assertions.assertEquals(-x, dist.inverseSurvivalProbability(p), 
Math.abs(x) * 1e-15, () -> "isf(" + p + ")");
+            previous = x;
+        }
+        // And towards the upper tail
+        previous = dist.inverseCumulativeProbability(0.5);
+        for (int i = 1; i <= 16; i++) {
+            final double p = 1 - Math.pow(10, -i);
+            final double x = dist.inverseCumulativeProbability(p);
+            Assertions.assertTrue(x > 0 && x > previous, () -> "icdf(" + p + 
")");
+            previous = x;
+        }
+    }
 }
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 35284f03..24645a30 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -106,6 +106,13 @@ If the output is not quite correct, check for invisible 
trailing spaces!
       <action dev="aherbert" type="fix" due-to="Security scan, Alex Herbert" 
issue="STATISTICS-95">
         "NormalConfidenceInterval": Validate mean is finite and variance is 
positive finite.
       </action>
+      <action dev="aherbert" type="fix" due-to="simpleqt" 
issue="STATISTICS-102">
+        "CauchyDistribution": Correct the inverse cumulative probability 
function.
+        Subtraction of one half from a probability close to 0 or 1 discarded 
the
+        significant digits, so the result saturated in the tails and was not
+        monotonic. The tails are evaluated using the equivalent cotangent forms
+        of the tangent.
+      </action>
     </release>
 
     <release version="1.3" date="2026-05-01" description="Adds new features 
(requires Java 8).">

Reply via email to