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 }