1 /++ 2 This module contains algorithms for the $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution). 3 4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0) 5 6 Authors: John Michael Hall 7 8 Copyright: 2022-3 Mir Stat Authors. 9 10 +/ 11 12 module mir.stat.distribution.poisson; 13 14 import mir.bignum.fp: Fp; 15 import mir.internal.utility: isFloatingPoint; 16 17 /++ 18 Algorithms used to calculate poisson dstribution. 19 20 `PoissonAlgo.direct` can be more time-consuming for large values of the number 21 of events (`k`) or the rate of occurences (`lambda`). Additional algorithms are 22 provided to the user to choose the trade-off between running time and accuracy. 23 24 See_also: 25 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 26 +/ 27 enum PoissonAlgo { 28 /++ 29 Direct 30 +/ 31 direct, 32 /++ 33 Gamma Incomplete function 34 +/ 35 gamma, 36 /++ 37 Approximates poisson distribution with normal distribution. Generally a better approximation when 38 `lambda > 1000`. 39 +/ 40 approxNormal, 41 /++ 42 Approximates poisson distribution with normal distribution (including continuity correction). More 43 accurate than `PoissonAlgo.approxNormal`. Generally a better approximation when `lambda > 10`. 44 +/ 45 approxNormalContinuityCorrection 46 } 47 48 private 49 @safe pure nothrow @nogc 50 T poissonPMFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 51 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.direct) 52 in (lambda > 0, "lambda must be greater than or equal to 0") 53 { 54 import mir.math.common: exp, pow; 55 import mir.math.numeric: factorial; 56 57 return exp(-lambda) * pow(lambda, k) / (cast(T) factorial(k)); 58 } 59 60 private 61 @safe pure nothrow @nogc 62 T poissonPMFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 63 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.gamma) 64 in (lambda > 0, "lambda must be greater than or equal to 0") 65 { 66 import std.mathspecial: gammaIncompleteCompl; 67 68 return gammaIncompleteCompl(k + 1, lambda) - gammaIncompleteCompl(k, lambda); 69 } 70 71 private 72 @safe pure nothrow @nogc 73 T poissonPMFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 74 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.approxNormal) 75 in (lambda > 0, "lambda must be greater than or equal to 0") 76 { 77 import mir.math.common: sqrt; 78 import mir.stat.distribution.normal: normalPDF; 79 80 return normalPDF(k, lambda, sqrt(lambda)); 81 } 82 83 private 84 @safe pure nothrow @nogc 85 T poissonPMFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 86 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) 87 in (lambda > 0, "lambda must be greater than or equal to 0") 88 { 89 import mir.math.common: sqrt; 90 import mir.stat.distribution.normal: normalCDF; 91 92 return normalCDF(cast(T) k + 0.5, lambda, sqrt(lambda)) - normalCDF(cast(T) k - 0.5, lambda, sqrt(lambda)); 93 } 94 95 /++ 96 Computes the poisson probability mass function (PMF). 97 98 Params: 99 poissonAlgo = algorithm for calculating PMF (default: PoissonAlgo.direct) 100 101 See_also: 102 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 103 +/ 104 @safe pure nothrow @nogc 105 template poissonPMF(PoissonAlgo poissonAlgo = PoissonAlgo.direct) 106 { 107 /++ 108 Params: 109 k = value to evaluate PMF (e.g. number of events) 110 lambda = expected rate of occurence 111 +/ 112 T poissonPMF(T)(const size_t k, const T lambda) 113 if (isFloatingPoint!T) 114 in (lambda > 0, "lambda must be greater than or equal to 0") 115 { 116 return poissonPMFImpl!(T, poissonAlgo)(k, lambda); 117 } 118 } 119 120 /// ditto 121 @safe pure nothrow @nogc 122 template poissonPMF(string poissonAlgo) 123 { 124 mixin("alias poissonPMF = .poissonPMF!(PoissonAlgo." ~ poissonAlgo ~ ");"); 125 } 126 127 /// 128 version(mir_stat_test) 129 @safe pure nothrow @nogc 130 unittest { 131 import mir.math.common: approxEqual, exp; 132 133 assert(3.poissonPMF(6.0).approxEqual(exp(-6.0) * 216 / 6)); 134 // Can compute directly with differences of upper incomplete gamma function 135 assert(3.poissonPMF!"gamma"(6.0).approxEqual(poissonPMF(3, 6.0))); 136 // For large values of k or lambda, can approximate with normal distribution 137 assert(1_000_000.poissonPMF!"approxNormal"(1_000_000.0).approxEqual(poissonPMF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 138 // Or closer with continuity correction 139 assert(1_000_000.poissonPMF!"approxNormalContinuityCorrection"(1_000_000.0).approxEqual(poissonPMF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 140 } 141 142 // test PoissonAlgo.direct 143 version(mir_stat_test) 144 @safe pure nothrow @nogc 145 unittest { 146 import mir.math.common: approxEqual, exp; 147 148 assert(0.poissonPMF(5.0).approxEqual(exp(-5.0))); 149 assert(1.poissonPMF(5.0).approxEqual(exp(-5.0) * 5)); 150 assert(2.poissonPMF(5.0).approxEqual(exp(-5.0) * 25 / 2)); 151 assert(3.poissonPMF(5.0).approxEqual(exp(-5.0) * 125 / 6)); 152 assert(4.poissonPMF(5.0).approxEqual(exp(-5.0) * 625 / 24)); 153 assert(5.poissonPMF(5.0).approxEqual(exp(-5.0) * 3125 / 120)); 154 assert(6.poissonPMF(5.0).approxEqual(exp(-5.0) * 15625 / 720)); 155 assert(7.poissonPMF(5.0).approxEqual(exp(-5.0) * 78125 / 5040)); 156 assert(8.poissonPMF(5.0).approxEqual(exp(-5.0) * 390625 / 40320)); 157 assert(9.poissonPMF(5.0).approxEqual(exp(-5.0) * 1953125 / 362880)); 158 assert(10.poissonPMF(5.0).approxEqual(exp(-5.0) * 9765625 / 3628800)); 159 } 160 161 // test PoissonAlgo.gamma 162 version(mir_stat_test) 163 @safe pure nothrow @nogc 164 unittest { 165 import mir.math.common: approxEqual; 166 for (size_t i; i < 20; i++) { 167 assert(i.poissonPMF!"gamma"(5.0).approxEqual(poissonPMF(i, 5.0))); 168 } 169 } 170 171 // test PoissonAlgo.approxNormal / approxNormalContinuityCorrection 172 version(mir_stat_test) 173 @safe pure nothrow @nogc 174 unittest { 175 import mir.math.common: approxEqual, sqrt; 176 import mir.stat.distribution.normal: normalCDF, normalPDF; 177 for (size_t i; i < 20; i++) { 178 assert(i.poissonPMF!"approxNormal"(5.0).approxEqual(normalPDF(i, 5.0, sqrt(5.0)))); 179 assert(i.poissonPMF!"approxNormalContinuityCorrection"(5.0).approxEqual(normalCDF(i + 0.5, 5.0, sqrt(5.0)) - normalCDF(i - 0.5, 5.0, sqrt(5.0)))); 180 } 181 } 182 183 /++ 184 Computes the poisson probability mass function (PMF) directly with extended 185 floating point types (e.g. `Fp!128`), which provides additional accuracy for 186 large values of `lambda` or `k`. 187 188 Params: 189 k = value to evaluate PMF (e.g. number of "heads") 190 lambda = expected rate of occurence 191 192 See_also: 193 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 194 +/ 195 @safe pure nothrow @nogc 196 T fp_poissonPMF(T)(const size_t k, const T lambda) 197 if (is(T == Fp!size, size_t size)) 198 in (cast(double) lambda > 0, "lambda must be greater than or equal to 0") 199 { 200 import mir.math.common: exp; 201 import mir.math.internal.fp_powi: fp_powi; 202 import mir.math.numeric: factorial; 203 204 return T(exp(-cast(double) lambda)) * fp_powi(lambda, k) / factorial(k); 205 } 206 207 /// 208 version(mir_stat_test) 209 @safe pure nothrow @nogc 210 unittest { 211 import mir.bignum.fp: Fp; 212 import mir.conv: to; 213 import mir.math.common: approxEqual, exp; 214 215 for (size_t i; i <= 10; i++) { 216 assert(i.fp_poissonPMF(Fp!128(5.0)).to!double.approxEqual(poissonPMF(i, 5.0))); 217 } 218 } 219 220 private 221 @safe pure nothrow @nogc 222 T poissonCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 223 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.direct) 224 in (lambda > 0, "lambda must be greater than or equal to 0") 225 { 226 import mir.math.common: exp; 227 import mir.math.numeric: factorial; 228 229 T output = 1; 230 if (k > 0) { 231 T multiplier = 1; 232 for (size_t i = 1; i < (k + 1); i++) { 233 multiplier *= (lambda / i); 234 output += multiplier; 235 } 236 } 237 return output * exp(-lambda); 238 } 239 240 private 241 @safe pure nothrow @nogc 242 T poissonCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 243 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.gamma) 244 in (lambda > 0, "lambda must be greater than or equal to 0") 245 { 246 import std.mathspecial: gammaIncompleteCompl; 247 return cast(T) gammaIncompleteCompl(k + 1, lambda); 248 } 249 250 private 251 @safe pure nothrow @nogc 252 T poissonCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 253 if (isFloatingPoint!T && 254 (poissonAlgo == PoissonAlgo.approxNormal || 255 poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection)) 256 in (lambda > 0, "lambda must be greater than or equal to 0") 257 { 258 import mir.math.common: sqrt; 259 import mir.stat.distribution.normal: normalCDF; 260 261 T l = k; 262 static if (poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) { 263 l += 0.5; 264 } 265 return normalCDF(l, lambda, sqrt(lambda)); 266 } 267 268 /++ 269 Computes the poisson cumulative distrivution function (CDF). 270 271 Params: 272 poissonAlgo = algorithm for calculating CDF (default: PoissonAlgo.direct) 273 274 See_also: 275 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 276 +/ 277 @safe pure nothrow @nogc 278 template poissonCDF(PoissonAlgo poissonAlgo = PoissonAlgo.direct) 279 { 280 /++ 281 Params: 282 k = value to evaluate CDF (e.g. number of events) 283 lambda = expected rate of occurence 284 +/ 285 T poissonCDF(T)(const size_t k, const T lambda) 286 if (isFloatingPoint!T) 287 in (lambda > 0, "lambda must be greater than or equal to 0") 288 { 289 return poissonCDFImpl!(T, poissonAlgo)(k, lambda); 290 } 291 } 292 293 /// ditto 294 @safe pure nothrow @nogc 295 template poissonCDF(string poissonAlgo) 296 { 297 mixin("alias poissonCDF = .poissonCDF!(PoissonAlgo." ~ poissonAlgo ~ ");"); 298 } 299 300 /// 301 version(mir_stat_test) 302 @safe pure nothrow @nogc 303 unittest { 304 import mir.math.common: approxEqual; 305 306 assert(3.poissonCDF(6.0).approxEqual(poissonPMF(0, 6.0) + poissonPMF(1, 6.0) + poissonPMF(2, 6.0) + poissonPMF(3, 6.0))); 307 // Can compute directly with upper incomplete gamma function 308 assert(3.poissonCDF!"gamma"(6.0).approxEqual(poissonCDF(3, 6.0))); 309 // For large values of k or lambda, can approximate with normal distribution 310 assert(1_000_000.poissonCDF!"approxNormal"(1_000_000.0).approxEqual(poissonCDF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 311 // Or closer with continuity correction 312 assert(1_000_000.poissonCDF!"approxNormalContinuityCorrection"(1_000_000.0).approxEqual(poissonCDF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 313 } 314 315 // test PoissonAlgo.direct 316 version(mir_stat_test) 317 @safe pure nothrow @nogc 318 unittest { 319 import mir.math.common: approxEqual; 320 321 static double sumOfPoissonPMFs(size_t k, double lambda) { 322 double output = 0; 323 for (size_t i; i < (k + 1); i++) { 324 output += poissonPMF(i, lambda); 325 } 326 return output; 327 } 328 329 for (size_t i; i < 20; i++) { 330 assert(i.poissonCDF(5.0).approxEqual(sumOfPoissonPMFs(i, 5.0))); 331 } 332 } 333 334 // test PoissonAlgo.gamma 335 version(mir_stat_test) 336 @safe pure nothrow @nogc 337 unittest { 338 import mir.math.common: approxEqual; 339 for (size_t i; i < 20; i++) { 340 assert(i.poissonCDF!"gamma"(5.0).approxEqual(poissonCDF(i, 5.0))); 341 } 342 } 343 344 // test PoissonAlgo.approxNormal / approxNormalContinuityCorrection 345 version(mir_stat_test) 346 @safe pure nothrow @nogc 347 unittest { 348 import mir.math.common: approxEqual, sqrt; 349 import mir.stat.distribution.normal: normalCDF; 350 for (size_t i; i < 20; i++) { 351 assert(i.poissonCDF!"approxNormal"(5.0).approxEqual(normalCDF(i, 5.0, sqrt(5.0)))); 352 assert(i.poissonCDF!"approxNormalContinuityCorrection"(5.0).approxEqual(normalCDF(i + 0.5, 5.0, sqrt(5.0)))); 353 } 354 } 355 356 private 357 @safe pure nothrow @nogc 358 T poissonCCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 359 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.direct) 360 in (lambda > 0, "lambda must be greater than or equal to 0") 361 { 362 return T(1) - poissonCDFImpl!(T, poissonAlgo)(k, lambda); 363 } 364 365 private 366 @safe pure nothrow @nogc 367 T poissonCCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 368 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.gamma) 369 in (lambda > 0, "lambda must be greater than or equal to 0") 370 { 371 import std.mathspecial: gammaIncomplete; 372 return cast(T) gammaIncomplete(k + 1, lambda); 373 } 374 375 private 376 @safe pure nothrow @nogc 377 T poissonCCDFImpl(T, PoissonAlgo poissonAlgo)(const size_t k, const T lambda) 378 if (isFloatingPoint!T && 379 (poissonAlgo == PoissonAlgo.approxNormal || 380 poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection)) 381 in (lambda > 0, "lambda must be greater than or equal to 0") 382 { 383 import mir.math.common: sqrt; 384 import mir.stat.distribution.normal: normalCCDF; 385 386 T l = k; 387 static if (poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) { 388 l += 0.5; 389 } 390 return normalCCDF(l, lambda, sqrt(lambda)); 391 } 392 393 /++ 394 Computes the poisson complementary cumulative distrivution function (CCDF). 395 396 Params: 397 poissonAlgo = algorithm for calculating CCDF (default: PoissonAlgo.direct) 398 399 See_also: 400 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 401 +/ 402 @safe pure nothrow @nogc 403 template poissonCCDF(PoissonAlgo poissonAlgo = PoissonAlgo.direct) 404 { 405 /++ 406 Params: 407 k = value to evaluate CCDF (e.g. number of events) 408 lambda = expected rate of occurence 409 +/ 410 T poissonCCDF(T)(const size_t k, const T lambda) 411 if (isFloatingPoint!T) 412 in (lambda > 0, "lambda must be greater than or equal to 0") 413 { 414 return poissonCCDFImpl!(T, poissonAlgo)(k, lambda); 415 } 416 } 417 418 /// ditto 419 @safe pure nothrow @nogc 420 template poissonCCDF(string poissonAlgo) 421 { 422 mixin("alias poissonCCDF = .poissonCCDF!(PoissonAlgo." ~ poissonAlgo ~ ");"); 423 } 424 425 /// 426 version(mir_stat_test) 427 @safe pure nothrow @nogc 428 unittest { 429 import mir.math.common: approxEqual; 430 431 assert(3.poissonCCDF(6.0).approxEqual(1.0 - (poissonPMF(0, 6.0) + poissonPMF(1, 6.0) + poissonPMF(2, 6.0) + poissonPMF(3, 6.0)))); 432 // Can compute directly with upper incomplete gamma function 433 assert(3.poissonCCDF!"gamma"(6.0).approxEqual(poissonCCDF(3, 6.0))); 434 // For large values of k or lambda, can approximate with normal distribution 435 assert(1_000_000.poissonCCDF!"approxNormal"(1_000_000.0).approxEqual(poissonCCDF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 436 // Or closer with continuity correction 437 assert(1_000_000.poissonCCDF!"approxNormalContinuityCorrection"(1_000_000.0).approxEqual(poissonCCDF!"gamma"(1_000_000, 1_000_000.0), 10e-3)); 438 } 439 440 // test PoissonAlgo.direct 441 version(mir_stat_test) 442 @safe pure nothrow @nogc 443 unittest { 444 import mir.math.common: approxEqual; 445 446 for (size_t i; i < 20; i++) { 447 assert(i.poissonCCDF(5.0).approxEqual(1.0 - poissonCDF(i, 5.0))); 448 } 449 } 450 451 // test PoissonAlgo.gamma 452 version(mir_stat_test) 453 @safe pure nothrow @nogc 454 unittest { 455 import mir.math.common: approxEqual; 456 for (size_t i; i < 20; i++) { 457 assert(i.poissonCCDF!"gamma"(5.0).approxEqual(poissonCCDF(i, 5.0))); 458 } 459 } 460 461 // test PoissonAlgo.approxNormal / approxNormalContinuityCorrection 462 version(mir_stat_test) 463 @safe pure nothrow @nogc 464 unittest { 465 import mir.math.common: approxEqual, sqrt; 466 import mir.stat.distribution.normal: normalCCDF; 467 for (size_t i; i < 20; i++) { 468 assert(i.poissonCCDF!"approxNormal"(5.0).approxEqual(normalCCDF(i, 5.0, sqrt(5.0)))); 469 assert(i.poissonCCDF!"approxNormalContinuityCorrection"(5.0).approxEqual(normalCCDF(i + 0.5, 5.0, sqrt(5.0)))); 470 } 471 } 472 473 private 474 @safe pure nothrow @nogc 475 size_t poissonInvCDFImpl(T, PoissonAlgo poissonAlgo)(const T p, const T lambda) 476 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.direct) 477 in (p >= 0, "p must be greater than or equal to 0") 478 in (p < 1, "p must be less than 1") 479 in (lambda > 0, "lambda must be greater than or equal to 0") 480 { 481 if (p == 0) { 482 return 0; 483 } 484 485 size_t guess = 0; 486 if (lambda > 16) { 487 guess = poissonInvCDFImpl!(T, PoissonAlgo.approxNormalContinuityCorrection)(p, lambda); 488 } 489 T cdfGuess = poissonCDF!(poissonAlgo)(guess, lambda); 490 491 if (p <= cdfGuess) { 492 if (guess == 0) { 493 return guess; 494 } 495 for (size_t i = (guess - 1); guess >= 0; i--) { 496 cdfGuess -= poissonPMF!(poissonAlgo)(i + 1, lambda); 497 if (p > cdfGuess) { 498 guess = i + 1; 499 break; 500 } 501 } 502 } else { 503 while(p > cdfGuess) { 504 guess++; 505 cdfGuess += poissonPMF!(poissonAlgo)(guess, lambda); 506 } 507 } 508 return guess; 509 } 510 511 private 512 @safe pure nothrow @nogc 513 T poissonInvCDFImpl(T, PoissonAlgo poissonAlgo)(const T p, const size_t k) 514 if (isFloatingPoint!T && poissonAlgo == PoissonAlgo.gamma) 515 in (p >= 0, "p must be greater than or equal to 0") 516 in (p < 1, "p must be less than 1") 517 { 518 import std.mathspecial: gammaIncompleteComplInverse; 519 520 if (p == 0) { 521 return T.infinity; 522 } 523 return gammaIncompleteComplInverse(k + 1, p); 524 } 525 526 private 527 @safe pure nothrow @nogc 528 size_t poissonInvCDFImpl(T, PoissonAlgo poissonAlgo)(const T p, const T lambda) 529 if (isFloatingPoint!T && 530 (poissonAlgo == PoissonAlgo.approxNormal || 531 poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection)) 532 in (p >= 0, "p must be greater than or equal to 0") 533 in (p < 1, "p must be less than 1") 534 in (lambda > 0, "lambda must be greater than or equal to 0") 535 { 536 import mir.math.common: floor, sqrt; 537 import mir.stat.distribution.normal: normalCDF, normalInvCDF; 538 539 T std = sqrt(lambda); 540 541 // Handles case where p is small better than just using pLowerBound = 0 542 T pLowerBound = 0; 543 T lowerValue = 0; 544 static if (poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) 545 lowerValue += 0.5; 546 pLowerBound = normalCDF(lowerValue, lambda, std); 547 if (p <= pLowerBound) { 548 return 0; 549 } 550 551 auto result = normalInvCDF(p, lambda, std); 552 static if (poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) { 553 result = result - 0.5; 554 } 555 return cast(size_t) floor(result); 556 } 557 558 /++ 559 Computes the poisson inverse cumulative distrivution function (InvCDF). 560 561 For algorithms `PoissonAlgo.direct`, `PoissonAlgo.approxNormal`, and 562 `PoissonAlgo.approxNormalContinuityCorrection`, the inverse CDF returns the 563 number of events (`k`) given the probability (`p`) and rate of occurence 564 (`lambda`). For the `Poisson.gamma` algorithm, the inverse CDF returns the rate 565 of occurence (`lambda`) given the probability (`p`) and the number of events (`k`). 566 567 For `PoissonAlgo.direct`, if the value of `lambda` is larger than 16, then an 568 initial guess is made based on `PoissonAlgo.approxNormalContinuityCorrection`. 569 570 Params: 571 poissonAlgo = algorithm for calculating InvCDF (default: PoissonAlgo.direct) 572 573 See_also: 574 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 575 +/ 576 @safe pure nothrow @nogc 577 template poissonInvCDF(PoissonAlgo poissonAlgo = PoissonAlgo.direct) 578 if (poissonAlgo == PoissonAlgo.direct || 579 poissonAlgo == PoissonAlgo.approxNormal || 580 poissonAlgo == PoissonAlgo.approxNormalContinuityCorrection) 581 { 582 /++ 583 Params: 584 p = value to evaluate InvCDF 585 lambda = expected rate of occurence 586 +/ 587 size_t poissonInvCDF(T)(const T p, const T lambda) 588 if (isFloatingPoint!T) 589 in (p >= 0, "p must be greater than or equal to 0") 590 in (p <= 1, "p must be less than or equal to 1") 591 in (lambda > 0, "lambda must be greater than or equal to 0") 592 { 593 return poissonInvCDFImpl!(T, poissonAlgo)(p, lambda); 594 } 595 } 596 597 /// ditto 598 @safe pure nothrow @nogc 599 template poissonInvCDF(PoissonAlgo poissonAlgo) 600 if (poissonAlgo == PoissonAlgo.gamma) 601 { 602 /++ 603 Params: 604 p = value to evaluate InvCDF 605 k = number of events 606 +/ 607 T poissonInvCDF(T)(const T p, const size_t k) 608 if (isFloatingPoint!T) 609 in (p >= 0, "p must be greater than or equal to 0") 610 in (p <= 1, "p must be less than or equal to 1") 611 { 612 return poissonInvCDFImpl!(T, poissonAlgo)(p, k); 613 } 614 } 615 616 /// ditto 617 @safe pure nothrow @nogc 618 template poissonInvCDF(string poissonAlgo) 619 { 620 mixin("alias poissonInvCDF = .poissonInvCDF!(PoissonAlgo." ~ poissonAlgo ~ ");"); 621 } 622 623 /// 624 version(mir_stat_test) 625 @safe pure nothrow @nogc 626 unittest { 627 import mir.math.common: approxEqual; 628 629 assert(0.15.poissonInvCDF(6.0) == 3); 630 // Passing `gamma` returns the rate of occurnece 631 assert(0.151204.poissonInvCDF!"gamma"(3).approxEqual(6)); 632 // For large values of k or lambda, can approximate with normal distribution 633 assert(0.5.poissonInvCDF!"approxNormal"(1_000_000.0) == 1_000_000); 634 // Or closer with continuity correction 635 assert(0.5009974.poissonInvCDF!"approxNormalContinuityCorrection"(1_000_000.0) == 1_000_002); 636 } 637 638 // test PoissonAlgo.direct 639 version(mir_stat_test) 640 @safe pure nothrow @nogc 641 unittest { 642 assert(0.poissonInvCDF(5.0) == 0); 643 for (double x = 0.05; x < 1; x = x + 0.05) { 644 size_t value = x.poissonInvCDF(5.0); 645 assert(value.poissonCDF(5.0) >= x); 646 assert((value - 1).poissonCDF(5.0) < x); 647 } 648 } 649 650 // test PoissonAlgo.direct, large lambda branch, check small p 651 version(mir_stat_test) 652 @safe pure nothrow @nogc 653 unittest { 654 double x = 1.0e-9; 655 assert(x.poissonInvCDF(20.0) == 0); 656 } 657 658 // test PoissonAlgo.direct, large lambda branch 659 version(mir_stat_test) 660 @safe pure nothrow @nogc 661 unittest { 662 assert(0.poissonInvCDF(25.0) == 0); 663 for (double x = 0.01; x < 1; x = x + 0.01) { 664 size_t value = x.poissonInvCDF(25.0); 665 assert(value.poissonCDF(25.0) >= x); 666 assert((value - 1).poissonCDF(25.0) < x); 667 } 668 } 669 670 // test PoissonAlgo.gamma (note the difference in how it is tested) 671 version(mir_stat_test) 672 @safe pure nothrow @nogc 673 unittest { 674 import mir.math.common: approxEqual; 675 676 assert(0.0.poissonInvCDF!"gamma"(5) == double.infinity); 677 for (double x = 0.05; x < 1; x = x + 0.05) { 678 for (size_t i; i < 10; i++) { 679 assert(poissonCDF!"gamma"(i, poissonInvCDF!"gamma"(x, i)).approxEqual(x)); 680 } 681 } 682 } 683 684 // test PoissonAlgo.approxNormal / approxNormalContinuityCorrection 685 version(mir_stat_test) 686 @safe pure nothrow @nogc 687 unittest { 688 import mir.math.common: floor, sqrt; 689 import mir.stat.distribution.normal: normalInvCDF; 690 691 assert(0.poissonInvCDF!"approxNormal"(5.0) == 0); 692 assert(0.poissonInvCDF!"approxNormalContinuityCorrection"(5.0) == 0); 693 double checkValue; 694 for (double x = 0.05; x < 1; x = x + 0.05) { 695 checkValue = normalInvCDF(x, 5.0, sqrt(5.0)); 696 assert(x.poissonInvCDF!"approxNormal"(5.0) == floor(checkValue)); 697 assert(x.poissonInvCDF!"approxNormalContinuityCorrection"(5.0) == floor(checkValue - 0.5)); 698 } 699 } 700 701 /++ 702 Computes the poisson log probability mass function (LPMF). 703 704 Params: 705 k = value to evaluate LPMF (e.g. number of "heads") 706 lambda = expected rate of occurence 707 708 See_also: 709 $(LINK2 https://en.wikipedia.org/wiki/Poisson_distribution, Poisson Distribution) 710 +/ 711 @safe pure nothrow @nogc 712 T poissonLPMF(T)(const size_t k, const T lambda) 713 if (isFloatingPoint!T) 714 in (lambda > 0, "lambda must be greater than or equal to 0") 715 { 716 import mir.math.common: log; 717 import mir.math.internal.log_binomial: logFactorial; 718 719 return k * log(lambda) - (logFactorial!T(k) + lambda); 720 } 721 722 /// 723 version(mir_stat_test) 724 @safe pure nothrow @nogc 725 unittest { 726 import mir.math.common: approxEqual, exp; 727 728 for (size_t i; i <= 10; i++) { 729 assert(i.poissonLPMF(5.0).exp.approxEqual(poissonPMF(i, 5.0))); 730 } 731 }