The OpenD Programming Language

1 /++
2 This module contains algorithms for the $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution).
3 
4 This module uses the shape/scale parameterization of the gamma distribution. To
5 use the shape/rate parameterization, apply the inverse to the rate and pass it
6 as the scale parameter.
7 
8 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
9 
10 Authors: Ilia Ki, John Michael Hall
11 
12 Copyright: 2022-3 Mir Stat Authors.
13 
14 +/
15 
16 module mir.stat.distribution.gamma;
17 
18 import mir.internal.utility: isFloatingPoint;
19 
20 /++
21 Computes the gamma probability density function (PDF).
22 
23 `shape` values less than `1` are supported when it is a floating point type.
24 
25 If `shape` is passed as a `size_t` type (or a type convertible to that), then the
26 PDF is calculated using the relationship with the poisson distribution (i.e.
27 replacing the `gamma` function with the `factorial`).
28 
29 Params:
30     x = value to evaluate PDF
31     shape = shape parameter
32     scale = scale parameter
33 
34 See_also:
35     $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution)
36 +/
37 @safe pure nothrow @nogc
38 T gammaPDF(T)(const T x, const T shape, const T scale = 1)
39     if (isFloatingPoint!T)
40     in (x >= 0, "x must be greater than or equal to 0")
41     in (shape > 0, "shape must be greater than zero")
42     in (scale > 0, "scale must be greater than zero")
43 {
44     import mir.math.common: exp, pow;
45     import std.mathspecial: gamma;
46     
47     if (x == 0) {
48         if (shape > 1) {
49             return 0;
50         } else if (shape < 1) {
51             return T.infinity;
52         } else {
53             return 1 / scale;
54         }
55     }
56 
57     T x_scale = x / scale;
58     return exp(-x_scale) * pow(x_scale, shape - 1) / (cast(T) gamma(shape)) / scale;
59 }
60 
61 /// ditto
62 @safe pure nothrow @nogc
63 T gammaPDF(T)(const T x, const size_t shape, const T scale = 1)
64     if (isFloatingPoint!T)
65     in (x >= 0, "x must be greater than or equal to 0")
66     in (shape > 0, "shape must be greater than zero")
67     in (scale > 0, "scale must be greater than zero")
68 {
69     import mir.stat.distribution.poisson: poissonPMF;
70 
71     if (x == 0) {
72         if (shape > 1) {
73             return 0;
74         } else {
75             return 1 / scale;
76         }
77     }
78 
79     return poissonPMF!"direct"(shape - 1, x / scale) / scale;
80 }
81 
82 ///
83 version(mir_stat_test)
84 @safe pure nothrow @nogc
85 unittest
86 {
87     import mir.test: shouldApprox;
88 
89     2.0.gammaPDF(3.0).shouldApprox == 0.2706706;
90     2.0.gammaPDF(3.0, 4.0).shouldApprox == 0.01895408;
91     // Calling with `size_t` uses factorial function instead of gamma, but
92     // produces same results
93     2.0.gammaPDF(3).shouldApprox == 2.0.gammaPDF(3.0);
94     2.0.gammaPDF(3, 4.0).shouldApprox == 2.0.gammaPDF(3.0, 4.0);
95 }
96 
97 //
98 version(mir_stat_test)
99 @safe pure nothrow @nogc
100 unittest
101 {
102     import mir.test: should, shouldApprox;
103 
104     // check integer, x = 0
105     0.0.gammaPDF(2).should == 0;
106     0.0.gammaPDF(1).should == 1;
107     0.0.gammaPDF(1, 4).shouldApprox == 0.25;
108 
109     // check float, x = 0
110     0.0.gammaPDF(2.0).should == 0;
111     0.0.gammaPDF(1.0).should == 1;
112     0.0.gammaPDF(0.5).should == double.infinity;
113     0.0.gammaPDF(2.0, 4.0).should == 0;
114     0.0.gammaPDF(1.0, 4.0).shouldApprox == 0.25;
115     0.0.gammaPDF(0.5, 4.0).should == double.infinity;
116 
117     // check integer
118     0.5.gammaPDF(3).shouldApprox == 0.07581633;
119     3.5.gammaPDF(4, 2.5).shouldApprox == 0.0451108;
120 
121     // check float, shape >= 1
122     1.25.gammaPDF(1.0).shouldApprox == 0.2865048;
123     1.25.gammaPDF(1.0, 0.5).shouldApprox == 0.16417;
124     1.5.gammaPDF(2.5, 0.5).shouldApprox == 0.3892174;
125 
126     // check float, shape < 1
127     0.005.gammaPDF(0.01).shouldApprox == 1.898102;
128     1.5.gammaPDF(0.25).shouldApprox == 0.04540553;
129     3.0.gammaPDF(0.5, 2.0).shouldApprox == 0.05139344;
130 }
131 
132 /++
133 Computes the gamma cumulative distribution function (CDF).
134 
135 Params:
136     x = value to evaluate CDF
137     shape = shape parameter
138     scale = scale parameter
139 
140 See_also:
141     $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution)
142 +/
143 @safe pure nothrow @nogc
144 T gammaCDF(T)(const T x, const T shape, const T scale = 1)
145     if (isFloatingPoint!T)
146     in (x >= 0, "x must be greater than or equal to 0")
147     in (shape > 0, "shape must be greater than zero")
148     in (scale > 0, "scale must be greater than zero")
149 {
150     import std.mathspecial: gammaIncomplete;
151     return gammaIncomplete(shape, x / scale);
152 }
153 
154 ///
155 version(mir_stat_test)
156 @safe pure nothrow @nogc
157 unittest
158 {
159     import mir.test: shouldApprox;
160 
161     2.0.gammaCDF(5).shouldApprox == 0.05265302;
162     1.0.gammaCDF(5, 0.5).shouldApprox == 0.05265302;
163 }
164 
165 // checking some more extreme values for shape and others
166 version(mir_stat_test)
167 @safe pure nothrow @nogc
168 unittest
169 {
170     import mir.test: shouldApprox;
171     0.5.gammaCDF(2, 1.5).shouldApprox == 0.04462492;
172     0.25.gammaCDF(0.5, 4).shouldApprox == 0.276326;
173     0.0625.gammaCDF(0.5).shouldApprox == 0.276326;
174     0.0625.gammaCDF(2).shouldApprox == 0.001873621;
175     0.00007854393.gammaCDF(0.5).shouldApprox == 0.01;
176     10.gammaCDF(2, 1.5).shouldApprox == 0.9902431;
177     5.gammaCDF(0.5, 1.5).shouldApprox == 0.9901767;
178     6.666666.gammaCDF(2).shouldApprox == 0.9902431;
179     3.333333.gammaCDF(0.5).shouldApprox == 0.9901767;
180 }
181 
182 /++
183 Computes the gamma complementary cumulative distribution function (CCDF).
184 
185 Params:
186     x = value to evaluate CCDF
187     shape = shape parameter
188     scale = scale parameter
189 
190 See_also:
191     $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution)
192 +/
193 @safe pure nothrow @nogc
194 T gammaCCDF(T)(const T x, const T shape, const T scale = 1)
195     if (isFloatingPoint!T)
196     in (x >= 0, "x must be greater than or equal to 0")
197     in (shape > 0, "shape must be greater than zero")
198     in (scale > 0, "scale must be greater than zero")
199 {
200     import std.mathspecial: gammaIncompleteCompl;
201     return gammaIncompleteCompl(shape, x / scale);
202 }
203 
204 ///
205 version(mir_stat_test)
206 @safe pure nothrow @nogc
207 unittest
208 {
209     import mir.test: shouldApprox;
210 
211     2.0.gammaCCDF(5).shouldApprox == 0.947347;
212     1.0.gammaCCDF(5, 0.5).shouldApprox == 0.947347;
213 }
214 
215 // checking some more extreme values for shape and others
216 version(mir_stat_test)
217 @safe pure nothrow @nogc
218 unittest
219 {
220     import mir.test: shouldApprox;
221 
222     0.5.gammaCCDF(2, 1.5).shouldApprox == 0.9553751;
223     0.25.gammaCCDF(0.5, 4).shouldApprox == 0.7236736;
224     0.0625.gammaCCDF(0.5).shouldApprox == 0.7236736;
225     0.0625.gammaCCDF(2).shouldApprox == 0.9981264;
226     0.00007854393.gammaCCDF(0.5).shouldApprox == 0.99;
227     10.gammaCCDF(2, 1.5).shouldApprox == 0.009756859;
228     5.gammaCCDF(0.5, 1.5).shouldApprox == 0.009823275;
229     6.666666.gammaCCDF(2).shouldApprox == 0.009756865;
230     3.333333.gammaCCDF(0.5).shouldApprox == 0.009823278;
231 }
232 
233 /++
234 Computes the gamma inverse cumulative distribution function (InvCDF).
235 
236 Params:
237     p = value to evaluate InvCDF
238     shape = shape parameter
239     scale = scale parameter
240 
241 See_also:
242     $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution)
243 +/
244 @safe pure nothrow @nogc
245 T gammaInvCDF(T)(const T p, const T shape, const T scale = 1)
246     if (isFloatingPoint!T)
247     in (p >= 0, "p must be greater than or equal to 0")
248     in (p <= 1, "p must be less than or equal to 1")
249     in (shape > 0, "shape must be greater than zero")
250     in (scale > 0, "scale must be greater than zero")
251 {
252     import std.mathspecial: gammaIncompleteComplInverse;
253     return gammaIncompleteComplInverse(shape, 1 - p) * scale;
254 }
255 
256 ///
257 version(mir_stat_test)
258 @safe pure nothrow @nogc
259 unittest
260 {
261     import mir.test: shouldApprox;
262 
263     0.05.gammaInvCDF(5).shouldApprox == 1.97015;
264     0.05.gammaInvCDF(5, 0.5).shouldApprox == 0.9850748;
265 }
266 
267 // checking some more extreme values for shape and others
268 version(mir_stat_test)
269 @safe pure nothrow @nogc
270 unittest
271 {
272     import mir.test: shouldApprox;
273     0.04.gammaInvCDF(2, 1.5).shouldApprox == 0.4703589;
274     0.27.gammaInvCDF(0.5, 4).shouldApprox == 0.2382233;
275     0.27.gammaInvCDF(0.5).shouldApprox == 0.05955582;
276     0.002.gammaInvCDF(2).shouldApprox == 0.06461886;
277     0.01.gammaInvCDF(0.5).shouldApprox == 0.00007854393;
278     0.99.gammaInvCDF(2, 1.5).shouldApprox == 9.957528;
279     0.99.gammaInvCDF(0.5, 1.5).shouldApprox == 4.976172;
280     0.99.gammaInvCDF(2).shouldApprox == 6.638352;
281     0.99.gammaInvCDF(0.5).shouldApprox == 3.317448;
282 }
283 
284 // confirming consistency with gammaCDF
285 version(mir_stat_test)
286 @safe pure nothrow @nogc
287 unittest
288 {
289     import mir.test: shouldApprox;
290 
291     2.0.gammaCDF(5).gammaInvCDF(5).shouldApprox == 2;
292     1.0.gammaCDF(5, 0.5).gammaInvCDF(5, 0.5).shouldApprox == 1;
293     0.5.gammaCDF(2, 1.5).gammaInvCDF(2, 1.5).shouldApprox == 0.5;
294     0.5.gammaCDF(2, 1.5).gammaInvCDF(2, 1.5).shouldApprox == 0.5;
295     0.25.gammaCDF(0.5, 4).gammaInvCDF(0.5, 4).shouldApprox == 0.25;
296     0.0625.gammaCDF(0.5).gammaInvCDF(0.5).shouldApprox == 0.0625;
297     0.0625.gammaCDF(2).gammaInvCDF(2).shouldApprox == 0.0625;
298     0.00007854393.gammaCDF(0.5).gammaInvCDF(0.5).shouldApprox == 0.00007854393;
299     10.gammaCDF(2, 1.5).gammaInvCDF(2, 1.5).shouldApprox == 10;
300     5.gammaCDF(0.5, 1.5).gammaInvCDF(0.5, 1.5).shouldApprox == 5;
301     6.666666.gammaCDF(2).gammaInvCDF(2).shouldApprox == 6.666666;
302     3.333333.gammaCDF(0.5).gammaInvCDF(0.5).shouldApprox == 3.333333;
303 }
304 
305 /++
306 Computes the gamma log probability density function (LPDF).
307 
308 `shape` values less than `1` are supported when it is a floating point type.
309 
310 If `shape` is passed as a `size_t` type (or a type convertible to that), then the
311 LPDF is calculated using the relationship with the poisson distribution (i.e.
312 replacing the `logGamma` function with the `logFactorial`).
313 
314 Params:
315     x = value to evaluate LPDF
316     shape = shape parameter
317     scale = scale parameter
318 
319 See_also:
320     $(LINK2 https://en.wikipedia.org/wiki/Gamma_distribution, Gamma Distribution)
321 +/
322 @safe pure nothrow @nogc
323 T gammaLPDF(T)(const T x, const T shape, const T scale = 1)
324     if (isFloatingPoint!T)
325     in (x >= 0, "x must be greater than or equal to 0")
326     in (shape > 0, "shape must be greater than zero")
327     in (scale > 0, "scale must be greater than zero")
328 {
329     import mir.math.common: log;
330     import std.mathspecial: logGamma;
331 
332     if (x == 0) {
333         if (shape > 1) {
334             return -T.infinity;
335         } else if (shape < 1) {
336             return T.infinity;
337         } else {
338             return -log(scale);
339         }
340     }
341 
342     T x_scale = x / scale;
343     return (shape - 1) * log(x_scale) - x_scale - cast(T) logGamma(shape) - log(scale);
344 }
345 
346 /// ditto
347 @safe pure nothrow @nogc
348 T gammaLPDF(T)(const T x, const size_t shape, const T scale = 1)
349     if (isFloatingPoint!T)
350     in (x >= 0, "x must be greater than or equal to 0")
351     in (shape > 0, "shape must be greater than zero")
352     in (scale > 0, "scale must be greater than zero")
353 {
354     import mir.math.common: log;
355     import mir.stat.distribution.poisson: poissonLPMF;
356 
357     if (x == 0) {
358         if (shape > 1) {
359             return -T.infinity;
360         } else {
361             return -log(scale);
362         } // note: shape cannot be equal to zero or less than 1 because it is size_t
363     }
364 
365     return poissonLPMF(shape - 1, x / scale) - log(scale);
366 }
367 
368 ///
369 version(mir_stat_test)
370 @safe pure nothrow @nogc
371 unittest
372 {
373     import mir.test: shouldApprox;
374 
375     2.0.gammaLPDF(3.0).shouldApprox == -1.306853;
376     2.0.gammaLPDF(3.0, 4.0).shouldApprox == -3.965736;
377     // Calling with `size_t` uses log factorial function instead of log gamma,
378     // but produces same results
379     2.0.gammaLPDF(3).shouldApprox == 2.0.gammaLPDF(3.0);
380     2.0.gammaLPDF(3, 4.0).shouldApprox == 2.0.gammaLPDF(3.0, 4.0);
381 }
382 
383 // test floating point version
384 version(mir_stat_test)
385 @safe pure nothrow @nogc
386 unittest {
387     import mir.math.common: exp;
388     import mir.test: shouldApprox;
389 
390     for (double x = 0; x <= 10; x = x + 0.5) {
391         x.gammaLPDF(5.0).exp.shouldApprox == x.gammaPDF(5.0);
392         x.gammaLPDF(5.0, 1.5).exp.shouldApprox == x.gammaPDF(5.0, 1.5);
393         x.gammaLPDF(1.0).exp.shouldApprox == x.gammaPDF(1.0);
394         x.gammaLPDF(1.0, 1.5).exp.shouldApprox == x.gammaPDF(1.0, 1.5);
395         x.gammaLPDF(0.5).exp.shouldApprox == x.gammaPDF(0.5);
396         x.gammaLPDF(0.5, 1.5).exp.shouldApprox == x.gammaPDF(0.5, 1.5);
397     }
398 }
399 
400 // test size_t version
401 version(mir_stat_test)
402 @safe pure nothrow @nogc
403 unittest {
404     import mir.math.common: exp;
405     import mir.test: shouldApprox;
406 
407     for (double x = 0; x <= 10; x = x + 0.5) {
408         x.gammaLPDF(5).exp.shouldApprox == x.gammaPDF(5);
409         x.gammaLPDF(5, 1.5).exp.shouldApprox == x.gammaPDF(5, 1.5);
410         x.gammaLPDF(1).exp.shouldApprox == x.gammaPDF(1);
411         x.gammaLPDF(1, 1.5).exp.shouldApprox == x.gammaPDF(1, 1.5);
412     }
413 }