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  
18  package org.apache.commons.statistics.distribution;
19  
20  import org.apache.commons.rng.UniformRandomProvider;
21  import org.apache.commons.rng.simple.RandomSource;
22  import org.junit.jupiter.api.Assertions;
23  import org.junit.jupiter.api.Test;
24  import org.junit.jupiter.params.ParameterizedTest;
25  import org.junit.jupiter.params.provider.CsvSource;
26  
27  /**
28   * Test cases for {@link ParetoDistribution}.
29   * Extends {@link BaseContinuousDistributionTest}. See javadoc of that class for details.
30   */
31  class ParetoDistributionTest extends BaseContinuousDistributionTest {
32      /**
33       * The difference each of the 2^53 dyadic rationals in [0, 1).
34       * This is the smallest non-zero value for p to use when inverse transform sampling.
35       * Equal to 2^-53.
36       */
37      private static final double U = 0x1.0p-53;
38  
39      @Override
40      ContinuousDistribution makeDistribution(Object... parameters) {
41          final double scale = (Double) parameters[0];
42          final double shape = (Double) parameters[1];
43          return ParetoDistribution.of(scale, shape);
44      }
45  
46      @Override
47      Object[][] makeInvalidParameters() {
48          return new Object[][] {
49              {0.0, 1.0},
50              {-0.1, 1.0},
51              {1.0, 0.0},
52              {1.0, -0.1},
53              {Double.POSITIVE_INFINITY, 1.0},
54          };
55      }
56  
57      @Override
58      String[] getParameterNames() {
59          return new String[] {"Scale", "Shape"};
60      }
61  
62      @Override
63      protected double getRelativeTolerance() {
64          return 5e-15;
65      }
66  
67      //-------------------- Additional test cases -------------------------------
68  
69      @ParameterizedTest
70      @CsvSource({
71          "1, 1, Infinity, Infinity",
72          "2.2, 2.4, 3.771428571428, 14.816326530",
73      })
74      void testAdditionalMoments(double scale, double shape, double mean, double variance) {
75          final ParetoDistribution dist = ParetoDistribution.of(scale, shape);
76          testMoments(dist, mean, variance, createRelTolerance(1e-9));
77      }
78  
79      @Test
80      void testAdditionalCumulativeProbabilityHighPrecision() {
81          final double scale = 2.1;
82          // 2.1000000000000005, 2.100000000000001
83          final double[] x = {Math.nextUp(scale), Math.nextUp(Math.nextUp(scale))};
84  
85          // R and Wolfram alpha do not match for high precision CDF at small x.
86          // The answers were computed using BigDecimal with a math context precision of 100.
87          // Note that the results using double are limited by intermediate rounding and the
88          // CDF is not high precision as the number of bits of accuracy is low:
89          //
90          // x = Math.nextUp(scale)
91          // 1.0 - pow(scale/x, 0.75)                    ==> 1.1102230246251565E-16
92          // -expm1(shape * log(scale/x))                ==> 1.665334536937735E-16
93          // -expm1(shape * log(scale) - shape * log(x)) ==> 2.2204460492503128E-16
94          //
95          // x = Math.nextUp(Math.nextUp(scale))
96          // 1.0 - pow(scale/x, 0.75)                    ==> 3.3306690738754696E-16
97          // -expm1(shape * log(scale/x))                ==> 3.33066907387547E-16
98          // -expm1(shape * log(scale) - shape * log(x)) ==> 4.440892098500625E-16
99  
100         final ParetoDistribution dist = ParetoDistribution.of(scale, 0.75);
101         // BigDecimal: 1 - (scale/x).pow(3).sqrt().sqrt()
102         // MathContext mc = new MathContext(100)
103         // BigDecimal.ONE.subtract(
104         //   new BigDecimal(2.1).divide(new BigDecimal(Math.nextUp(Math.nextUp(2.1))), mc)
105         //    .pow(3).sqrt(mc).sqrt(mc)).doubleValue()
106         final double[] values = {1.5860328923216517E-16, 3.172065784643303E-16};
107         testCumulativeProbabilityHighPrecision(dist, x, values, createRelTolerance(0.05));
108     }
109 
110     @Test
111     void testAdditionalCumulativeProbabilityHighPrecision2() {
112         final double scale = 3;
113         // 3.0000000000000004, 3.000000000000001
114         final double[] x = {Math.nextUp(scale), Math.nextUp(Math.nextUp(scale))};
115 
116         // The current implementation is closer to the answer than either R or Wolfram but
117         // the relative error is typically 0.25 (error in the first or second digit).
118         // The absolute tolerance checks the result to a closer tolerance than
119         // the answer computed using 1 - Math.pow(scale/x, shape), which is zero.
120 
121         final ParetoDistribution dist = ParetoDistribution.of(3, 0.25);
122         // BigDecimal: 1 - (scale/x).sqrt().sqrt()
123         final double[] values = {3.700743415417188E-17, 7.401486830834375E-17};
124         testCumulativeProbabilityHighPrecision(dist, x, values, createAbsTolerance(1e-17));
125 
126         final ParetoDistribution dist2 = ParetoDistribution.of(3, 1.5);
127         // BigDecimal: 1 - (scale/x).pow(3).sqrt()
128         final double[] values2 = {2.2204460492503126E-16, 4.4408920985006247E-16};
129         testCumulativeProbabilityHighPrecision(dist2, x, values2, createAbsTolerance(6e-17));
130     }
131 
132     @Test
133     void testAdditionalSurvivalProbabilityHighPrecision() {
134         final ParetoDistribution dist = ParetoDistribution.of(2.1, 1.4);
135         testSurvivalProbabilityHighPrecision(
136             dist,
137             new double[] {42e11, 64e11},
138             new double[] {6.005622169907148e-18, 3.330082930386111e-18},
139             DoubleTolerances.relative(5e-14));
140     }
141 
142     /**
143      * Check to make sure top-coding of extreme values works correctly.
144      */
145     @Test
146     void testExtremeValues() {
147         final ParetoDistribution dist = ParetoDistribution.of(1, 1);
148         for (int i = 0; i < 10000; i++) { // make sure no convergence exception
149             final double upperTail = dist.cumulativeProbability(i);
150             if (i <= 1000) { // make sure not top-coded
151                 Assertions.assertTrue(upperTail < 1.0d);
152             } else { // make sure top coding not reversed
153                 Assertions.assertTrue(upperTail > 0.999);
154             }
155         }
156 
157         Assertions.assertEquals(1, dist.cumulativeProbability(Double.MAX_VALUE));
158         Assertions.assertEquals(0, dist.cumulativeProbability(-Double.MAX_VALUE));
159         Assertions.assertEquals(1, dist.cumulativeProbability(Double.POSITIVE_INFINITY));
160         Assertions.assertEquals(0, dist.cumulativeProbability(Double.NEGATIVE_INFINITY));
161     }
162 
163     /**
164      * Test extreme parameters to the distribution. This uses the same computation to precompute
165      * factors for the PMF and log PMF as performed by the distribution. When the factors are
166      * not finite then the edges cases must be appropriately handled.
167      */
168     @Test
169     void testExtremeParameters() {
170         double scale;
171         double shape;
172 
173         // Overflow of standard computation. Log computation OK.
174         scale = 10;
175         shape = 306;
176         Assertions.assertEquals(Double.POSITIVE_INFINITY, shape * Math.pow(scale, shape));
177         Assertions.assertTrue(Double.isFinite(Math.log(shape) + Math.log(scale) * shape));
178 
179         // ---
180 
181         // Overflow of standard computation. Overflow of Log computation.
182         scale = 10;
183         shape = Double.POSITIVE_INFINITY;
184         Assertions.assertEquals(Double.POSITIVE_INFINITY, shape * Math.pow(scale, shape));
185         Assertions.assertEquals(Double.POSITIVE_INFINITY, Math.log(shape) + Math.log(scale) * shape);
186 
187         // This case can compute as if shape is big (Dirac delta function)
188         shape = 1e300;
189         Assertions.assertEquals(Double.POSITIVE_INFINITY, shape * Math.pow(scale, shape));
190         Assertions.assertTrue(Double.isFinite(Math.log(shape) + Math.log(scale) * shape));
191 
192         // ---
193 
194         // NaN of standard computation. NaN of Log computation.
195         scale = 1;
196         shape = Double.POSITIVE_INFINITY;
197         // 1^inf == NaN
198         Assertions.assertEquals(Double.NaN, shape * Math.pow(scale, shape));
199         // 0 * inf == NaN
200         Assertions.assertEquals(Double.NaN, Math.log(shape) + Math.log(scale) * shape);
201 
202         // This case can compute as if shape is big (Dirac delta function)
203         shape = 1e300;
204         Assertions.assertEquals(shape, shape * Math.pow(scale, shape));
205         Assertions.assertTrue(Double.isFinite(Math.log(shape) + Math.log(scale) * shape));
206 
207         // ---
208 
209         // Underflow of standard computation. Log computation OK.
210         scale = 0.1;
211         shape = 324;
212         Assertions.assertEquals(0.0, shape * Math.pow(scale, shape));
213         Assertions.assertTrue(Double.isFinite(Math.log(shape) + Math.log(scale) * shape));
214 
215         // ---
216 
217         // Underflow of standard computation. Underflow of Log computation.
218         scale = 0.1;
219         shape = Double.MAX_VALUE;
220         Assertions.assertEquals(0.0, shape * Math.pow(scale, shape));
221         Assertions.assertEquals(Double.NEGATIVE_INFINITY, Math.log(shape) + Math.log(scale) * shape);
222 
223         // This case can compute as if shape is big (Dirac delta function)
224 
225         // ---
226 
227         // Underflow of standard computation to NaN. NaN of Log computation.
228         scale = 0.1;
229         shape = Double.POSITIVE_INFINITY;
230         Assertions.assertEquals(Double.NaN, shape * Math.pow(scale, shape));
231         Assertions.assertEquals(Double.NaN, Math.log(shape) + Math.log(scale) * shape);
232 
233         // This case can compute as if shape is big (Dirac delta function)
234 
235         // ---
236 
237         // Smallest possible value of shape is OK.
238         // The Math.pow function -> 1 as the exponent -> 0.
239         shape = Double.MIN_VALUE;
240         for (final double scale2 : new double[] {Double.MIN_VALUE, 0.1, 1, 10, 100}) {
241             Assertions.assertEquals(shape, shape * Math.pow(scale2, shape));
242             Assertions.assertTrue(Double.isFinite(Math.log(shape) + Math.log(scale2) * shape));
243         }
244     }
245 
246     /**
247      * Test sampling with a large shape. As {@code shape -> inf} then the distribution
248      * approaches a delta function with {@code CDF(x=scale)} = 0 and {@code CDF(x>scale) = 1}.
249      * This test verifies that a large shape is effectively sampled from p in [0, 1) to avoid
250      * spurious infinite samples when p=1.
251      *
252      * <p>Sampling Details
253      *
254      * <p>Note that sampling is using inverse transform sampling by inverting the CDF:
255      * <pre>
256      * CDF(x) = 1 - (scale / x)^shape
257      * x = scale / (1 - p)^(1 / shape)
258      *   = scale / exp(log(1 - p) / shape)
259      * </pre>
260      *
261      * <p>The sampler in Commons RNG is inverting the CDF function using Math.pow:
262      * <pre>
263      * x = scale / Math.pow(1 - p, 1 / shape)
264      * </pre>
265      *
266      * <p>The Pareto distribution uses log functions to achieve the same result:
267      * <pre>
268      * x = scale / Math.exp(Math.log1p(-p) / shape);
269      * </pre>
270      *
271      * <p>Inversion will return the scale when Math.exp(X) == 1 where X (in [-inf, 0]) is:
272      * <pre>
273      * X = log(1 - p) / shape
274      * </pre>
275      *
276      * <p>This occurs when {@code X > log(1.0 - epsilon)}, or larger (closer to zero) than
277      * {@code Math.log(Math.nextDown(1.0))}; X is approximately -1.11e-16.
278      * During sampling p is bounded to the 2^53 dyadic rationals in [0, 1). The largest
279      * finite value for the logarithm is log(2^-53) thus the critical size for shape is around:
280      * <pre>
281      * shape = log(2^-53) / -1.1102230246251565e-16 = 3.3089568271276403e17
282      * </pre>
283      *
284      * <p>Note that if the p-value is 1 then inverseCumulativeProbability(1.0) == inf.
285      * However using the power function to invert this ignores this possibility when the shape
286      * is infinite and will always return scale / x^0 = scale / 1 = scale. If the inversion
287      * using logarithms is directly used then a log(0) / inf == -inf / inf == NaN occurs.
288      */
289     @ParameterizedTest
290     @CsvSource({
291         // Scale values match those from the test resource files where the sampling test is disabled
292         "10, Infinity",
293         "1, Infinity",
294         "0.1, Infinity",
295         // This behaviour occurs even when the shape is not infinite due to limited precision
296         // of double values. Shape is set to twice the limit derived above to account for rounding:
297         // double p = 0x1.0p-53
298         // Math.pow(p, 1 / (Math.log(p) / -p))     ==> 0.9999999999999999
299         // Math.pow(p, 1 / (2 * Math.log(p) / -p)) ==> 1.0
300         // shape = (2 * Math.log(p) / -p)
301         "10, 6.6179136542552806e17",
302         "1, 6.6179136542552806e17",
303         "0.1, 6.6179136542552806e17",
304     })
305     void testSamplingWithLargeShape(double scale, double shape) {
306         final ParetoDistribution dist = ParetoDistribution.of(scale, shape);
307 
308         // Sampling should act as if inverting p in [0, 1)
309         final double x0 = dist.inverseCumulativeProbability(0);
310         final double x1 = dist.inverseCumulativeProbability(1 - U);
311         Assertions.assertEquals(scale, x0);
312         Assertions.assertEquals(x0, x1, "Test parameters did not create an extreme distribution");
313 
314         // Sampling for p in [0, 1): returns scale when shape is large
315         assertSampler(dist, scale);
316     }
317 
318     /**
319      * Test sampling with a tiny shape. As {@code shape -> 0} then the distribution
320      * approaches a function with {@code CDF(x=inf) = 1} and {@code CDF(x>=scale) = 0}.
321      * This test verifies that a tiny shape is effectively sampled from p in (0, 1] to avoid
322      * spurious NaN samples when p=0.
323      *
324      * <p>Sampling Details
325      *
326      * <p>The sampler in Commons RNG is inverting the CDF function using Math.pow:
327      * <pre>
328      * x = scale / Math.pow(1 - p, 1 / shape)
329      * </pre>
330      *
331      * <p>However Math.pow(1, infinity) == NaN. This can be avoided if p=0 is not used.
332      * For all other values Math.pow(1 - p, infinity) == 0 and the sample is infinite.
333      */
334     @ParameterizedTest
335     @CsvSource({
336         // 1 / shape is infinite
337         // Scale values match those from the test resource files where the sampling test is disabled
338         "10, 4.9e-324",
339         "1, 4.9e-324",
340         "0.1, 4.9e-324",
341         // This behaviour occurs even when 1 / shape is not infinite due to limited precision
342         // of double values. Shape provides the largest possible finite value from 1 / shape:
343         // shape = (1.0 + Math.ulp(1.0)*2) / Double.MAX_VALUE
344         // 1 / shape = 1.7976931348623143e308
345         // 1 / Math.nextDown(shape) = Infinity
346         "10, 5.56268464626801E-309",
347         "1, 5.56268464626801E-309",
348         "0.1, 5.56268464626801E-309",
349         // Lower limit is where pow(1 - p, 1 / shape) < Double.MIN_VALUE:
350         // shape < log(1 - p) / log(MIN_VALUE)
351         // Shape is set to half this limit to account for rounding:
352         // double p = 0x1.0p-53
353         // Math.pow(1 - p, 1 / (Math.log(1 - p) / Math.log(Double.MIN_VALUE))) ==> 4.9e-324
354         // Math.pow(1 - p, 2 / (Math.log(1 - p) / Math.log(Double.MIN_VALUE))) ==> 0.0
355         // shape = 0.5 * Math.log(1 - p) / Math.log(Double.MIN_VALUE)
356         "10, 7.456765604783329e-20",
357         "1, 7.456765604783329e-20",
358         // Use smallest possible scale: test will fail if shape is not half the limit
359         "4.9e-324, 7.456765604783329e-20",
360     })
361     void testSamplingWithTinyShape(double scale, double shape) {
362         final ParetoDistribution dist = ParetoDistribution.of(scale, shape);
363 
364         // Sampling should act as if inverting p in (0, 1]
365         final double x0 = dist.inverseCumulativeProbability(U);
366         final double x1 = dist.inverseCumulativeProbability(1);
367         Assertions.assertEquals(Double.POSITIVE_INFINITY, x1);
368         Assertions.assertEquals(x1, x0, "Test parameters did not create an extreme distribution");
369 
370         // Sampling for p in [0, 1): returns infinity when shape is tiny
371         assertSampler(dist, Double.POSITIVE_INFINITY);
372     }
373 
374     /**
375      * Assert the sampler produces the expected sample value irrespective of the values from the RNG.
376      *
377      * @param dist Distribution
378      * @param expected Expected sample value
379      */
380     private static void assertSampler(ParetoDistribution dist, double expected) {
381         // Extreme random numbers using no bits or all bits, then combinations
382         // that may be used to generate a double from the lower or upper 53-bits
383         final long[] values = {0, -1, 1, 1L << 11, -2, -2L << 11};
384         final UniformRandomProvider rng = createRNG(values);
385         ContinuousDistribution.Sampler s = dist.createSampler(rng);
386         for (final long l : values) {
387             Assertions.assertEquals(expected, s.sample(), () -> "long bits = " + l);
388         }
389         // Any random number
390         final long seed = RandomSource.createLong();
391         s = dist.createSampler(RandomSource.SPLIT_MIX_64.create(seed));
392         for (int i = 0; i < 100; i++) {
393             Assertions.assertEquals(expected, s.sample(), () -> "seed = " + seed);
394         }
395     }
396 
397     /**
398      * Creates the RNG to return the given values from the nextLong() method.
399      *
400      * @param values Long values
401      * @return the RNG
402      */
403     private static UniformRandomProvider createRNG(long... values) {
404         return new UniformRandomProvider() {
405             private int i;
406 
407             @Override
408             public long nextLong() {
409                 return values[i++];
410             }
411 
412             @Override
413             public double nextDouble() {
414                 throw new IllegalStateException("nextDouble cannot be trusted to be in [0, 1) and should be ignored");
415             }
416         };
417     }
418 }