diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 8b5f8e539..090969c1f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -54,8 +54,8 @@ If the output is not quite correct, check for invisible trailing spaces! - - "ClopperPearsonInterval": Fixed case where number of trials equals number of successes. + + "ClopperPearsonInterval": Missing cases. "MillerUpdatingRegression": Fixed "ArrayIndexOutOfBounds" exception. diff --git a/src/main/java/org/apache/commons/math4/stat/interval/ClopperPearsonInterval.java b/src/main/java/org/apache/commons/math4/stat/interval/ClopperPearsonInterval.java index 5bf642c5c..27cdc8495 100644 --- a/src/main/java/org/apache/commons/math4/stat/interval/ClopperPearsonInterval.java +++ b/src/main/java/org/apache/commons/math4/stat/interval/ClopperPearsonInterval.java @@ -35,26 +35,24 @@ public class ClopperPearsonInterval implements BinomialConfidenceInterval { double confidenceLevel) { IntervalUtils.checkParameters(numberOfTrials, numberOfSuccesses, confidenceLevel); double lowerBound = 0; - double upperBound = 0; + double upperBound = 1; + + final double alpha = 0.5 * (1 - confidenceLevel); if (numberOfSuccesses > 0) { - final double alpha = 0.5 * (1 - confidenceLevel); - final FDistribution distributionLowerBound = new FDistribution(2 * (numberOfTrials - numberOfSuccesses + 1), 2 * numberOfSuccesses); final double fValueLowerBound = distributionLowerBound.inverseCumulativeProbability(1 - alpha); lowerBound = numberOfSuccesses / (numberOfSuccesses + (numberOfTrials - numberOfSuccesses + 1) * fValueLowerBound); + } - if (numberOfSuccesses != numberOfTrials) { - final FDistribution distributionUpperBound = new FDistribution(2 * (numberOfSuccesses + 1), - 2 * (numberOfTrials - numberOfSuccesses)); - final double fValueUpperBound = distributionUpperBound.inverseCumulativeProbability(1 - alpha); - upperBound = (numberOfSuccesses + 1) * fValueUpperBound / - (numberOfTrials - numberOfSuccesses + (numberOfSuccesses + 1) * fValueUpperBound); - } else { - upperBound = 1; - } + if (numberOfSuccesses < numberOfTrials) { + final FDistribution distributionUpperBound = new FDistribution(2 * (numberOfSuccesses + 1), + 2 * (numberOfTrials - numberOfSuccesses)); + final double fValueUpperBound = distributionUpperBound.inverseCumulativeProbability(1 - alpha); + upperBound = (numberOfSuccesses + 1) * fValueUpperBound / + (numberOfTrials - numberOfSuccesses + (numberOfSuccesses + 1) * fValueUpperBound); } return new ConfidenceInterval(lowerBound, upperBound, confidenceLevel); diff --git a/src/test/java/org/apache/commons/math4/stat/interval/ClopperPearsonIntervalTest.java b/src/test/java/org/apache/commons/math4/stat/interval/ClopperPearsonIntervalTest.java index a8a1db1a3..6a06f97df 100644 --- a/src/test/java/org/apache/commons/math4/stat/interval/ClopperPearsonIntervalTest.java +++ b/src/test/java/org/apache/commons/math4/stat/interval/ClopperPearsonIntervalTest.java @@ -46,4 +46,68 @@ public class ClopperPearsonIntervalTest extends BinomialConfidenceIntervalAbstra Assert.assertEquals(0.025, interval.getLowerBound(), 1e-16); Assert.assertEquals(1, interval.getUpperBound(), 0d); } + + // number of successes = 0, number of trials = N + @Test + public void testCase1() { + // Check correctness against values obtained with the Python statsmodels.stats.proportion.proportion_confint + final int successes = 0; + final int trials = 10; + final double confidenceLevel = 0.95; + + // proportion_confint(0,10,method='beta') = (0, 0.3084971078187608) + final ConfidenceInterval expected = new ConfidenceInterval(0, + 0.3084971078187608, + confidenceLevel); + + check(expected, createBinomialConfidenceInterval().createInterval(trials, successes, confidenceLevel)); + } + + // number of successes = number of trials = N + @Test + public void testCase2() { + // Check correctness against values obtained with the Python statsmodels.stats.proportion.proportion_confint + final int successes = 10; + final int trials = 10; + final double confidenceLevel = 0.95; + + // prop.proportion_confint(10,10,method='beta') = (0.6915028921812392, 1) + final ConfidenceInterval expected = new ConfidenceInterval(0.6915028921812392, + 1, + confidenceLevel); + + check(expected, createBinomialConfidenceInterval().createInterval(trials, successes, confidenceLevel)); + } + + // number of successes = k, number of trials = N, 0 < k < N + @Test + public void testCase3() { + // Check correctness against values obtained with the Python statsmodels.stats.proportion.proportion_confint + final int successes = 3; + final int trials = 10; + final double confidenceLevel = 0.95; + + // prop.proportion_confint(3,10,method='beta') = (0.06673951117773447, 0.6524528500599972) + final ConfidenceInterval expected = new ConfidenceInterval(0.06673951117773447, + 0.6524528500599972, + confidenceLevel); + + check(expected, createBinomialConfidenceInterval().createInterval(trials, successes, confidenceLevel)); + } + + private void check(ConfidenceInterval expected, + ConfidenceInterval actual) { + final double relTol = 1.0e-6; // Reasonable relative tolerance for floating point comparison + // Compare bounds using a relative tolerance + Assert.assertEquals(expected.getLowerBound(), + actual.getLowerBound(), + relTol * (1.0 + Math.abs(expected.getLowerBound()))); + Assert.assertEquals(expected.getUpperBound(), + actual.getUpperBound(), + relTol * (1.0 + Math.abs(expected.getUpperBound()))); + // The confidence level must be exact + Assert.assertEquals(expected.getConfidenceLevel(), + actual.getConfidenceLevel(), + 0.0); + } }