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    *      http://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 org.apache.commons.math3.analysis.UnivariateFunction;
20  import org.apache.commons.math3.analysis.integration.RombergIntegrator;
21  import org.apache.commons.math3.analysis.integration.UnivariateIntegrator;
22  import org.junit.jupiter.api.Assertions;
23  import org.junit.jupiter.api.Test;
24  
25  /**
26   * Test cases for AbstractContinuousDistribution default implementations.
27   */
28  class AbstractContinuousDistributionTest {
29  
30      /** Various tests related to MATH-699. */
31      @Test
32      void testContinuous() {
33          final double x0 = 0.0;
34          final double x1 = 1.0;
35          final double x2 = 2.0;
36          final double x3 = 3.0;
37          final double p12 = 0.5;
38          final AbstractContinuousDistribution distribution;
39          distribution = new AbstractContinuousDistribution() {
40              @Override
41              public double cumulativeProbability(final double x) {
42                  if (x < x0 ||
43                      x > x3) {
44                      throw new DistributionException(DistributionException.OUT_OF_RANGE, x, x0, x3);
45                  }
46                  if (x <= x1) {
47                      return p12 * (x - x0) / (x1 - x0);
48                  } else if (x <= x2) {
49                      return p12;
50                  } else if (x <= x3) {
51                      return p12 + (1.0 - p12) * (x - x2) / (x3 - x2);
52                  }
53                  return 0.0;
54              }
55  
56              @Override
57              public double density(final double x) {
58                  if (x < x0 ||
59                      x > x3) {
60                      throw new DistributionException(DistributionException.OUT_OF_RANGE, x, x0, x3);
61                  }
62                  if (x <= x1) {
63                      return p12 / (x1 - x0);
64                  } else if (x <= x2) {
65                      return 0.0;
66                  } else if (x <= x3) {
67                      return (1.0 - p12) / (x3 - x2);
68                  }
69                  return 0.0;
70              }
71  
72              @Override
73              public double getMean() {
74                  return ((x0 + x1) * p12 + (x2 + x3) * (1.0 - p12)) / 2.0;
75              }
76  
77              @Override
78              public double getVariance() {
79                  final double meanX = getMean();
80                  final double meanX2;
81                  meanX2 = ((x0 * x0 + x0 * x1 + x1 * x1) * p12 +
82                            (x2 * x2 + x2 * x3 + x3 * x3) * (1.0 - p12)) / 3.0;
83                  return meanX2 - meanX * meanX;
84              }
85  
86              @Override
87              public double getSupportLowerBound() {
88                  return x0;
89              }
90  
91              @Override
92              public double getSupportUpperBound() {
93                  return x3;
94              }
95  
96              @Override
97              boolean isSupportConnected() {
98                  // This is deliberately false; the functionality is the subject of this test
99                  return false;
100             }
101         };
102         // CDF is continuous before x1 and after x2. Plateau in between:
103         // CDF(x1 <= X <= x2) = p12.
104         // The inverse returns the infimum.
105         double expected = x1;
106         double actual = distribution.inverseCumulativeProbability(p12);
107         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
108 
109         actual = distribution.inverseSurvivalProbability(1 - p12);
110         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
111 
112         // Test the continuous region
113         expected = 0.5 * (x1 - x0);
114         actual = distribution.inverseCumulativeProbability(p12 * 0.5);
115         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
116 
117         actual = distribution.inverseSurvivalProbability(1 - p12 * 0.5);
118         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
119 
120         // Edge case where the result is within the solver accuracy of the lower bound
121         expected = x0;
122         actual = distribution.inverseCumulativeProbability(Double.MIN_VALUE);
123         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
124 
125         actual = distribution.inverseSurvivalProbability(Math.nextDown(1.0));
126         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
127     }
128 
129     /** Various tests related to MATH-699. */
130     @Test
131     void testDiscontinuous() {
132         final double x0 = 0.0;
133         final double x1 = 0.25;
134         final double x2 = 0.5;
135         final double x3 = 0.75;
136         final double x4 = 1.0;
137         final double p12 = 1.0 / 3.0;
138         final double p23 = 2.0 / 3.0;
139         final AbstractContinuousDistribution distribution;
140         distribution = new AbstractContinuousDistribution() {
141             @Override
142             public double cumulativeProbability(final double x) {
143                 if (x < x0 ||
144                     x > x4) {
145                     throw new DistributionException(DistributionException.OUT_OF_RANGE, x, x0, x4);
146                 }
147                 if (x <= x1) {
148                     return p12 * (x - x0) / (x1 - x0);
149                 } else if (x <= x2) {
150                     return p12;
151                 } else if (x <= x3) {
152                     return p23;
153                 } else {
154                     return (1.0 - p23) * (x - x3) / (x4 - x3) + p23;
155                 }
156             }
157 
158             @Override
159             public double density(final double x) {
160                 if (x < x0 ||
161                     x > x4) {
162                     throw new DistributionException(DistributionException.OUT_OF_RANGE, x, x0, x4);
163                 }
164                 if (x <= x1) {
165                     return p12 / (x1 - x0);
166                 } else if (x <= x2) {
167                     return 0.0;
168                 } else if (x <= x3) {
169                     return 0.0;
170                 } else {
171                     return (1.0 - p23) / (x4 - x3);
172                 }
173             }
174 
175             @Override
176             public double getMean() {
177                 final UnivariateFunction f = x -> x * density(x);
178                 final UnivariateIntegrator integrator = new RombergIntegrator();
179                 return integrator.integrate(Integer.MAX_VALUE, f, x0, x4);
180             }
181 
182             @Override
183             public double getVariance() {
184                 final double meanX = getMean();
185                 final UnivariateFunction f = x -> x * x * density(x);
186                 final UnivariateIntegrator integrator = new RombergIntegrator();
187                 final double meanX2 = integrator.integrate(Integer.MAX_VALUE,
188                                                            f, x0, x4);
189                 return meanX2 - meanX * meanX;
190             }
191 
192             @Override
193             public double getSupportLowerBound() {
194                 return x0;
195             }
196 
197             @Override
198             public double getSupportUpperBound() {
199                 return x4;
200             }
201 
202             @Override
203             boolean isSupportConnected() {
204                 // This is deliberately false; the functionality is the subject of this test
205                 return false;
206             }
207         };
208         // CDF continuous before x1 and after x3. Two plateuas in between stepped at x2:
209         // CDF(x1 <= X <= x2) = p12.
210         // CDF(x2 <= X <= x3) = p23. The inverse returns the infimum.
211         double expected = x2;
212         double actual = distribution.inverseCumulativeProbability(p23);
213         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
214 
215         actual = distribution.inverseSurvivalProbability(1 - p23);
216         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
217 
218         // Test the continuous region
219         expected = 0.5 * (x1 - x0);
220         actual = distribution.inverseCumulativeProbability(p12 * 0.5);
221         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
222 
223         actual = distribution.inverseSurvivalProbability(1 - p12 * 0.5);
224         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
225 
226         // Edge case where the result is within the solver accuracy of the lower bound
227         expected = x0;
228         actual = distribution.inverseCumulativeProbability(Double.MIN_VALUE);
229         Assertions.assertEquals(expected, actual, 1e-8, "Inverse CDF");
230 
231         actual = distribution.inverseSurvivalProbability(Math.nextDown(1.0));
232         Assertions.assertEquals(expected, actual, 1e-8, "Inverse SF");
233     }
234 
235     /**
236      * Test zero variance. This invalidates the Chebyshev inequality. If the mean is at
237      * one bound and the other bound is infinite then the inequality sets the other bound
238      * to the mean. This results in no bracket for the solver.
239      *
240      * <p>If the distribution is reporting the variance incorrectly then options are
241      * to throw an exception, or safely fall back to manual bracketing. This test verifies
242      * the solver reverts to manual bracketing and raises no exception.
243      */
244     @Test
245     void testZeroVariance() {
246         // Distribution must have an infinite bound but no variance.
247         // This is an invalid case for the Chebyshev inequality.
248         // E.g. It may occur in the Pareto distribution as it approaches a Dirac function.
249 
250         // Create a Dirac function at x=10
251         final double x0 = 10.0;
252         final AbstractContinuousDistribution distribution;
253         distribution = new AbstractContinuousDistribution() {
254             @Override
255             public double cumulativeProbability(final double x) {
256                 return x <= x0 ? 0.0 : 1.0;
257             }
258 
259             @Override
260             public double density(final double x) {
261                 throw new AssertionError();
262             }
263 
264             @Override
265             public double getMean() {
266                 return x0;
267             }
268 
269             @Override
270             public double getVariance() {
271                 return 0.0;
272             }
273 
274             @Override
275             public double getSupportLowerBound() {
276                 return x0;
277             }
278 
279             @Override
280             public double getSupportUpperBound() {
281                 return Double.POSITIVE_INFINITY;
282             }
283         };
284         double x = distribution.inverseCumulativeProbability(0.5);
285         // The value can be anything other than x0
286         Assertions.assertNotEquals(x0, x, "Inverse CDF");
287         // Ideally it would be the next value after x0 but accuracy is dependent
288         // on the tolerance of the solver
289         Assertions.assertEquals(x0, x, 1e-8, "Inverse CDF");
290 
291         // The same functionality should be supported for the inverse survival probability
292         x = distribution.inverseSurvivalProbability(0.5);
293         Assertions.assertNotEquals(x0, x, "Inverse SF");
294         Assertions.assertEquals(x0, x, 1e-8, "Inverse SF");
295     }
296 
297     /**
298      * Test infinite variance. This invalidates the Chebyshev inequality.
299      *
300      * <p>This test verifies the solver reverts to manual bracketing and raises no exception.
301      */
302     @Test
303     void testInfiniteVariance() {
304         // Distribution must have an infinite bound and infinite variance.
305         // This is an invalid case for the Chebyshev inequality.
306 
307         // Create a triangle distribution: (a, c, b); a=lower, c=mode, b=upper
308         // (-10, 0, 10)
309         // Area of the first triangle [-10, 0] is set assuming the height is 10
310         // => 10 * 10 * 2 / 2 = 100
311         // Length of triangle to achieve half the area:
312         // x = sqrt(50) = 7.07..
313 
314         final AbstractContinuousDistribution distribution;
315         distribution = new AbstractContinuousDistribution() {
316             @Override
317             public double cumulativeProbability(final double x) {
318                 if (x > 0) {
319                     // Use symmetry for the upper triangle
320                     return 1 - cumulativeProbability(-x);
321                 }
322                 if (x < -10) {
323                     return 0;
324                 }
325                 return Math.pow(x + 10, 2) / 200;
326             }
327 
328             @Override
329             public double density(final double x) {
330                 throw new AssertionError();
331             }
332 
333             @Override
334             public double getMean() {
335                 return 0;
336             }
337 
338             @Override
339             public double getVariance() {
340                 // Report variance incorrectly
341                 return Double.POSITIVE_INFINITY;
342             }
343 
344             @Override
345             public double getSupportLowerBound() {
346                 // Report lower bound incorrectly (it should be -10) to test cdf(0)
347                 return Double.NEGATIVE_INFINITY;
348             }
349 
350             @Override
351             public double getSupportUpperBound() {
352                 // Report upper bound incorrectly (it should be 10) to test cdf(1)
353                 return Double.POSITIVE_INFINITY;
354             }
355         };
356 
357         // Accuracy is dependent on the tolerance of the solver
358         final double tolerance = 1e-8;
359 
360         final double x = Math.sqrt(50);
361         Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.inverseCumulativeProbability(0), "Inverse CDF");
362         Assertions.assertEquals(x - 10, distribution.inverseCumulativeProbability(0.25), tolerance, "Inverse CDF");
363         Assertions.assertEquals(0, distribution.inverseCumulativeProbability(0.5), tolerance, "Inverse CDF");
364         Assertions.assertEquals(10 - x, distribution.inverseCumulativeProbability(0.75), tolerance, "Inverse CDF");
365         Assertions.assertEquals(Double.POSITIVE_INFINITY, distribution.inverseCumulativeProbability(1), "Inverse CDF");
366 
367         // The same functionality should be supported for the inverse survival probability
368         Assertions.assertEquals(Double.NEGATIVE_INFINITY, distribution.inverseSurvivalProbability(1), "Inverse CDF");
369         Assertions.assertEquals(x - 10, distribution.inverseSurvivalProbability(0.75), tolerance, "Inverse SF");
370         Assertions.assertEquals(0, distribution.inverseSurvivalProbability(0.5), tolerance, "Inverse SF");
371         Assertions.assertEquals(10 - x, distribution.inverseSurvivalProbability(0.25), tolerance, "Inverse SF");
372         Assertions.assertEquals(Double.POSITIVE_INFINITY, distribution.inverseSurvivalProbability(0), "Inverse CDF");
373     }
374 
375     /**
376      * Create a distribution near positive infinity so that it is truncated by MAX_VALUE.
377      * This distribution reports the upper bound as infinite.
378      */
379     @Test
380     void testTruncatedDistributionAtPositiveInfinity() {
381         assertTruncatedDistributionAtPositiveInfinity(false);
382     }
383 
384     /**
385      * Create a distribution near positive infinity so that it is truncated by MAX_VALUE.
386      * This distribution reports the upper bound as finite.
387      */
388     @Test
389     void testTruncatedDistributionAtPositiveInfinity2() {
390         assertTruncatedDistributionAtPositiveInfinity(true);
391     }
392 
393     private static void assertTruncatedDistributionAtPositiveInfinity(boolean finiteBound) {
394         final double mean = Double.MAX_VALUE;
395         final double width = Double.MAX_VALUE / 2;
396         final double bound = finiteBound ? Double.MAX_VALUE : Double.POSITIVE_INFINITY;
397         final CentredUniformDistribution dist = new CentredUniformDistribution(mean, width) {
398             @Override
399             public double getSupportUpperBound() {
400                 return bound;
401             }
402         };
403         final double x = mean - width / 2;
404         Assertions.assertEquals(0, dist.cumulativeProbability(x));
405         Assertions.assertTrue(dist.cumulativeProbability(Math.nextUp(x)) > 0);
406         Assertions.assertEquals(0.25, dist.cumulativeProbability(mean - width / 4));
407         Assertions.assertEquals(0.5, dist.cumulativeProbability(mean));
408 
409         // Truncated
410         Assertions.assertEquals(1.0, dist.cumulativeProbability(Math.nextUp(mean)));
411 
412         // Inversion should be robust to return the upper infinite bound
413         Assertions.assertEquals(mean, dist.inverseCumulativeProbability(0.5));
414         Assertions.assertEquals(mean, dist.inverseSurvivalProbability(0.5));
415         Assertions.assertEquals(bound, dist.inverseCumulativeProbability(0.75));
416         Assertions.assertEquals(bound, dist.inverseSurvivalProbability(0.25));
417     }
418 
419     /**
420      * Create a distribution near negative infinity so that it is truncated by -MAX_VALUE.
421      * This distribution reports the lower bound as infinite.
422      */
423     @Test
424     void testTruncatedDistributionAtNegativeInfinity() {
425         assertTruncatedDistributionAtNegativeInfinity(false);
426     }
427 
428     /**
429      * Create a distribution near negative infinity so that it is truncated by -MAX_VALUE.
430      * This distribution reports the lower bound as finite.
431      */
432     @Test
433     void testTruncatedDistributionAtNegativeInfinity2() {
434         assertTruncatedDistributionAtNegativeInfinity(true);
435     }
436 
437     private static void assertTruncatedDistributionAtNegativeInfinity(boolean finiteBound) {
438         final double mean = -Double.MAX_VALUE;
439         final double width = Double.MAX_VALUE / 2;
440         final double bound = finiteBound ? -Double.MAX_VALUE : Double.NEGATIVE_INFINITY;
441         final CentredUniformDistribution dist = new CentredUniformDistribution(mean, width) {
442             @Override
443             public double getSupportLowerBound() {
444                 return bound;
445             }
446         };
447         final double x = mean + width / 2;
448         Assertions.assertEquals(1, dist.cumulativeProbability(x));
449         Assertions.assertTrue(dist.cumulativeProbability(Math.nextDown(x)) < 1);
450         Assertions.assertEquals(0.75, dist.cumulativeProbability(mean + width / 4));
451         Assertions.assertEquals(0.5, dist.cumulativeProbability(mean));
452 
453         // Truncated
454         Assertions.assertEquals(0.0, dist.cumulativeProbability(Math.nextDown(mean)));
455 
456         // Inversion should be robust to return the lower infinite bound
457         Assertions.assertEquals(mean, dist.inverseCumulativeProbability(0.5));
458         Assertions.assertEquals(mean, dist.inverseSurvivalProbability(0.5));
459         Assertions.assertEquals(bound, dist.inverseCumulativeProbability(0.25));
460         Assertions.assertEquals(bound, dist.inverseSurvivalProbability(0.75));
461     }
462 
463     /**
464      * Uniform distribution described with a centre and width.
465      * This can be use to place the centre of the distribution at the limit of a finite double
466      * value so that the distribution is truncated.
467      */
468     static class CentredUniformDistribution extends AbstractContinuousDistribution {
469         private final double mean;
470         private final double width;
471         private final double density;
472         private final double lo;
473         private final double hi;
474 
475         /**
476          * @param mean Mean
477          * @param width Width
478          */
479         CentredUniformDistribution(double mean, double width) {
480             this.mean = mean;
481             this.width = width;
482             density = 1 / width;
483             lo = mean - width / 2;
484             hi = mean + width / 2;
485         }
486 
487         @Override
488         public double density(double x) {
489             return density;
490         }
491 
492         @Override
493         public double cumulativeProbability(double x) {
494             if (x <= lo) {
495                 return 0;
496             }
497             if (x >= hi) {
498                 return 1;
499             }
500             return 0.5 + density * (x - mean);
501         }
502 
503         @Override
504         public double getMean() {
505             return mean;
506         }
507 
508         @Override
509         public double getVariance() {
510             return width * width / 12;
511         }
512 
513         @Override
514         public double getSupportLowerBound() {
515             return lo;
516         }
517 
518         @Override
519         public double getSupportUpperBound() {
520             return hi;
521         }
522     }
523 }