View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.statistics.distribution;
18  
19  import java.util.Arrays;
20  import java.util.stream.Stream;
21  import org.junit.jupiter.api.Assertions;
22  import org.junit.jupiter.api.Test;
23  import org.junit.jupiter.params.ParameterizedTest;
24  import org.junit.jupiter.params.provider.Arguments;
25  import org.junit.jupiter.params.provider.MethodSource;
26  import org.junit.jupiter.params.provider.ValueSource;
27  
28  /**
29   * Test cases for {@link GeometricDistribution}.
30   * Extends {@link BaseDiscreteDistributionTest}. See javadoc of that class for details.
31   */
32  class GeometricDistributionTest extends BaseDiscreteDistributionTest {
33      @Override
34      DiscreteDistribution makeDistribution(Object... parameters) {
35          final double p = (Double) parameters[0];
36          return GeometricDistribution.of(p);
37      }
38  
39      @Override
40      Object[][] makeInvalidParameters() {
41          return new Object[][] {
42              {-0.1},
43              {0.0},
44              {1.1},
45          };
46      }
47  
48      @Override
49      String[] getParameterNames() {
50          return new String[] {"ProbabilityOfSuccess"};
51      }
52  
53      @Override
54      protected double getRelativeTolerance() {
55          return 2 * RELATIVE_EPS;
56      }
57  
58      //-------------------- Additional test cases -------------------------------
59  
60      @ParameterizedTest
61      @MethodSource
62      void testAdditionalMoments(double p, double mean, double variance) {
63          final GeometricDistribution dist = GeometricDistribution.of(p);
64          testMoments(dist, mean, variance, DoubleTolerances.ulps(1));
65      }
66  
67      static Stream<Arguments> testAdditionalMoments() {
68          return Stream.of(
69              Arguments.of(0.5, (1.0 - 0.5) / 0.5, (1.0 - 0.5) / (0.5 * 0.5)),
70              Arguments.of(0.3, (1.0 - 0.3) / 0.3, (1.0 - 0.3) / (0.3 * 0.3))
71          );
72      }
73  
74      /**
75       * Test the PMF is computed using the power function when p is above 0.5.
76       * <p>Note: The geometric distribution PMF is defined as:
77       * <pre>
78       *   pmf(x) = (1-p)^x * p
79       * </pre>
80       * <p>As {@code p -> 0} use of the power function should be avoided as it will
81       * propagate the inexact computation of {@code 1 - p}. The implementation can
82       * switch to using a rearrangement with the exponential function which avoid
83       * computing {@code 1 - p}.
84       * <p>See STATISTICS-34.
85       *
86       * @param p Probability of success
87       */
88      @ParameterizedTest
89      @ValueSource(doubles = {0.5, 0.6658665, 0.75, 0.8125347, 0.9, 0.95, 0.99})
90      void testPMF(double p) {
91          final GeometricDistribution dist = GeometricDistribution.of(p);
92          final int[] x = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40};
93          final double[] values = Arrays.stream(x).mapToDouble(k -> p * Math.pow(1 - p, k)).toArray();
94          // The PMF should be an exact match to the direct implementation with Math.pow.
95          testProbability(dist, x, values, DoubleTolerances.equals());
96      }
97  
98      /**
99       * Test the inverse CDF returns the correct x from the CDF result.
100      * Cases were identified using various probabilities to discover a mismatch
101      * of x != icdf(cdf(x)). This occurs due to rounding errors on the inversion.
102      */
103     @ParameterizedTest
104     @ValueSource(doubles = {
105         0.2,
106         0.8,
107         // icdf(cdf(x)) requires rounding up
108         0.07131208016887369,
109         0.14441285445326058,
110         0.272118157703929,
111         0.424656239093432,
112         0.00899452845634574,
113         // icdf(cdf(x)) requires rounding down
114         0.3441320118140774,
115         0.5680886873083258,
116         0.8738746761971425,
117         0.17373328785967923,
118         0.09252030895185881,
119     })
120     void testInverseCDF(double p) {
121         final GeometricDistribution dist = GeometricDistribution.of(p);
122         final int[] x = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
123         testCumulativeProbabilityInverseMapping(dist, x);
124     }
125 
126     /**
127      * Test the inverse SF returns the correct x from the SF result.
128      * Cases were identified using various probabilities to discover a mismatch
129      * of x != isf(sf(x)). This occurs due to rounding errors on the inversion.
130      */
131     @ParameterizedTest
132     @ValueSource(doubles = {
133         0.2,
134         0.8,
135         // isf(sf(x)) requires rounding up
136         0.9625911263689207,
137         0.2858964038911178,
138         0.31872883511135996,
139         0.46149078212832284,
140         0.3701613946505057,
141         // isf(sf(x)) requires rounding down
142         0.3796493606864414,
143         0.1113177920615187,
144         0.2587259503484439,
145         0.8996839434455458,
146         0.450704136259792,
147     })
148     void testInverseSF(double p) {
149         final GeometricDistribution dist = GeometricDistribution.of(p);
150         final int[] x = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
151         testSurvivalProbabilityInverseMapping(dist, x);
152     }
153 
154     /**
155      * Test the most extreme parameters. Uses a small enough value of p that the distribution is
156      * truncated by the maximum integer value. This creates a case where (x+1) will overflow.
157      * This occurs in the cumulative and survival function computations.
158      */
159     @Test
160     void testExtremeParameters() {
161         final double p = Double.MIN_VALUE;
162         final GeometricDistribution dist = GeometricDistribution.of(p);
163 
164         final int x = Integer.MAX_VALUE;
165         // CDF = 1 - (1-p)^(x+1)
166         // Compute with log for accuracy with small p
167         final double cdf = -Math.expm1(Math.log1p(-p) * (x + 1.0));
168         Assertions.assertNotEquals(1.0, cdf);
169         Assertions.assertEquals(cdf, dist.cumulativeProbability(x));
170         for (int i = 0; i < 5; i++) {
171             Assertions.assertEquals(x - i, dist.inverseCumulativeProbability(dist.cumulativeProbability(x - i)));
172         }
173 
174         // CDF(x=0) = p
175         Assertions.assertEquals(p, dist.cumulativeProbability(0));
176         Assertions.assertEquals(0, dist.inverseCumulativeProbability(p));
177         Assertions.assertEquals(1, dist.inverseCumulativeProbability(Math.nextUp(p)));
178         for (int i = 1; i < 5; i++) {
179             Assertions.assertEquals(i, dist.inverseCumulativeProbability(dist.cumulativeProbability(i)));
180         }
181 
182         // SF = (1-p)^(x+1)
183         // Compute with log for accuracy with small p
184         final double sf = Math.exp(Math.log1p(-p) * (x + 1.0));
185         Assertions.assertEquals(1.0 - cdf, sf);
186         Assertions.assertEquals(sf, dist.survivalProbability(x));
187         // SF is too close to 1 to be able to invert
188         Assertions.assertEquals(1.0, sf);
189         Assertions.assertEquals(x, dist.inverseSurvivalProbability(Math.nextDown(1.0)));
190     }
191 
192     /**
193      * Test the most extreme parameters. Uses a large enough value of p that the distribution is
194      * compacted to x=0.
195      *
196      * <p>p is one ULP down from 1.0.
197      */
198     @Test
199     void testExtremeParameters2() {
200         final double p = Math.nextDown(1.0);
201         final GeometricDistribution dist = GeometricDistribution.of(p);
202 
203         final int x = 0;
204         // CDF = 1 - (1-p)^(x+1)
205         // CDF(x=0) = p
206         Assertions.assertEquals(p, dist.cumulativeProbability(0));
207         Assertions.assertEquals(0, dist.inverseCumulativeProbability(p));
208         // CDF is too close to 1 to be able to invert next value
209         Assertions.assertEquals(Integer.MAX_VALUE, dist.inverseCumulativeProbability(Math.nextUp(p)));
210 
211         // SF = (1-p)^(x+1)
212         final double sf = 1 - p;
213         Assertions.assertNotEquals(0.0, sf);
214         Assertions.assertEquals(sf, dist.survivalProbability(x));
215         for (int i = 1; i < 5; i++) {
216             Assertions.assertEquals(i, dist.inverseSurvivalProbability(dist.survivalProbability(i)));
217         }
218     }
219 
220     /**
221      * Test the most extreme parameters. Uses a large enough value of p that the distribution is
222      * compacted to x=0.
223      *
224      * <p>p is two ULP down from 1.0.
225      */
226     @Test
227     void testExtremeParameters3() {
228         final double p = Math.nextDown(Math.nextDown(1.0));
229         final GeometricDistribution dist = GeometricDistribution.of(p);
230 
231         final int x = 0;
232         // CDF = 1 - (1-p)^(x+1)
233         // CDF(x=0) = p
234         Assertions.assertEquals(p, dist.cumulativeProbability(0));
235         Assertions.assertEquals(0, dist.inverseCumulativeProbability(p));
236         Assertions.assertEquals(1, dist.inverseCumulativeProbability(Math.nextUp(p)));
237         // CDF is too close to 1 to be able to invert next value
238         Assertions.assertEquals(Integer.MAX_VALUE, dist.inverseCumulativeProbability(Math.nextUp(Math.nextUp(p))));
239 
240         // SF = (1-p)^(x+1)
241         final double sf = 1 - p;
242         Assertions.assertNotEquals(0.0, sf);
243         Assertions.assertEquals(sf, dist.survivalProbability(x));
244         for (int i = 1; i < 5; i++) {
245             Assertions.assertEquals(i, dist.inverseSurvivalProbability(dist.survivalProbability(i)));
246         }
247     }
248 }