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
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 }