statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
discrete_distributions.hpp
Go to the documentation of this file.
1
10#pragma once
11
14
15#include <algorithm>
16#include <cmath>
17#include <cstdint>
18#include <limits>
19#include <random>
20#include <stdexcept>
21
22namespace statcpp {
23
24// ============================================================================
25// Helper: Log factorial and binomial coefficient
26// ============================================================================
27
34inline double log_factorial(std::uint64_t n)
35{
36 if (n <= 1) return 0.0;
37 return lgamma(static_cast<double>(n + 1));
38}
39
49inline double log_binomial_coef(std::uint64_t n, std::uint64_t k)
50{
51 if (k > n) return -std::numeric_limits<double>::infinity();
52 if (k == 0 || k == n) return 0.0;
53 return log_factorial(n) - log_factorial(k) - log_factorial(n - k);
54}
55
65inline double binomial_coef(std::uint64_t n, std::uint64_t k)
66{
67 if (k > n) return 0.0;
68 return std::exp(log_binomial_coef(n, k));
69}
70
71// ============================================================================
72// Binomial Distribution
73// ============================================================================
74
86inline double binomial_pmf(std::uint64_t k, std::uint64_t n, double p)
87{
88 if (p < 0.0 || p > 1.0) {
89 throw std::invalid_argument("statcpp::binomial_pmf: p must be in [0, 1]");
90 }
91 if (k > n) return 0.0;
92
93 if (p == 0.0) return (k == 0) ? 1.0 : 0.0;
94 if (p == 1.0) return (k == n) ? 1.0 : 0.0;
95
96 double log_pmf = log_binomial_coef(n, k) + k * std::log(p) + (n - k) * std::log(1.0 - p);
97 return std::exp(log_pmf);
98}
99
111inline double binomial_cdf(std::uint64_t k, std::uint64_t n, double p)
112{
113 if (p < 0.0 || p > 1.0) {
114 throw std::invalid_argument("statcpp::binomial_cdf: p must be in [0, 1]");
115 }
116 if (k >= n) return 1.0;
117
118 // Use regularized incomplete beta function
119 // P(X <= k) = I_{1-p}(n-k, k+1)
120 return betainc(static_cast<double>(n - k), static_cast<double>(k + 1), 1.0 - p);
121}
122
134inline std::uint64_t binomial_quantile(double prob, std::uint64_t n, double p)
135{
136 if (p < 0.0 || p > 1.0) {
137 throw std::invalid_argument("statcpp::binomial_quantile: p must be in [0, 1]");
138 }
139 if (prob < 0.0 || prob > 1.0) {
140 throw std::invalid_argument("statcpp::binomial_quantile: prob must be in [0, 1]");
141 }
142 if (prob == 0.0) return 0;
143 if (prob == 1.0) return n;
144
145 // Binary search
146 std::uint64_t lo = 0;
147 std::uint64_t hi = n;
148
149 while (lo < hi) {
150 std::uint64_t mid = lo + (hi - lo) / 2;
151 if (binomial_cdf(mid, n, p) < prob) {
152 lo = mid + 1;
153 } else {
154 hi = mid;
155 }
156 }
157
158 return lo;
159}
160
171template <typename Engine = default_random_engine>
172std::uint64_t binomial_rand(std::uint64_t n, double p, Engine& engine)
173{
174 if (p < 0.0 || p > 1.0) {
175 throw std::invalid_argument("statcpp::binomial_rand: p must be in [0, 1]");
176 }
177 std::binomial_distribution<std::uint64_t> dist(n, p);
178 return dist(engine);
179}
180
188inline std::uint64_t binomial_rand(std::uint64_t n, double p)
189{
190 return binomial_rand(n, p, get_random_engine());
191}
192
193// ============================================================================
194// Poisson Distribution
195// ============================================================================
196
207inline double poisson_pmf(std::uint64_t k, double lambda)
208{
209 if (lambda < 0.0) {
210 throw std::invalid_argument("statcpp::poisson_pmf: lambda must be non-negative");
211 }
212 if (lambda == 0.0) return (k == 0) ? 1.0 : 0.0;
213
214 double log_pmf = k * std::log(lambda) - lambda - log_factorial(k);
215 return std::exp(log_pmf);
216}
217
228inline double poisson_cdf(std::uint64_t k, double lambda)
229{
230 if (lambda < 0.0) {
231 throw std::invalid_argument("statcpp::poisson_cdf: lambda must be non-negative");
232 }
233 if (lambda == 0.0) return 1.0;
234
235 // P(X <= k) = Q(k+1, lambda) = 1 - P(k+1, lambda) (upper regularized incomplete gamma)
236 return gammainc_upper(static_cast<double>(k + 1), lambda);
237}
238
247inline std::uint64_t poisson_quantile(double p, double lambda)
248{
249 if (lambda < 0.0) {
250 throw std::invalid_argument("statcpp::poisson_quantile: lambda must be non-negative");
251 }
252 if (p < 0.0 || p > 1.0) {
253 throw std::invalid_argument("statcpp::poisson_quantile: p must be in [0, 1]");
254 }
255 if (p == 0.0) return 0;
256 // Infinite support: prob == 1.0 has no finite quantile. Return the maximum
257 // representable value instead of casting +inf to uint64 (undefined behavior).
258 if (p == 1.0) return std::numeric_limits<std::uint64_t>::max();
259 if (lambda == 0.0) return 0;
260
261 // Start with Gaussian approximation
262 double z = norm_quantile(p);
263 double guess = lambda + z * std::sqrt(lambda);
264 std::uint64_t k = static_cast<std::uint64_t>(std::max(0.0, guess));
265
266 // Adjust up or down
267 constexpr std::uint64_t kMaxIter = 1000000;
268 std::uint64_t iter = 0;
269 while (k > 0 && poisson_cdf(k - 1, lambda) >= p) {
270 --k;
271 if (++iter >= kMaxIter) return k;
272 }
273 while (poisson_cdf(k, lambda) < p) {
274 ++k;
275 if (++iter >= kMaxIter) return k;
276 }
277
278 return k;
279}
280
290template <typename Engine = default_random_engine>
291std::uint64_t poisson_rand(double lambda, Engine& engine)
292{
293 if (lambda < 0.0) {
294 throw std::invalid_argument("statcpp::poisson_rand: lambda must be non-negative");
295 }
296 std::poisson_distribution<std::uint64_t> dist(lambda);
297 return dist(engine);
298}
299
306inline std::uint64_t poisson_rand(double lambda)
307{
308 return poisson_rand(lambda, get_random_engine());
309}
310
311// ============================================================================
312// Geometric Distribution
313// ============================================================================
314
326inline double geometric_pmf(std::uint64_t k, double p)
327{
328 if (p <= 0.0 || p > 1.0) {
329 throw std::invalid_argument("statcpp::geometric_pmf: p must be in (0, 1]");
330 }
331 if (p == 1.0) return (k == 0) ? 1.0 : 0.0;
332
333 return std::pow(1.0 - p, static_cast<double>(k)) * p;
334}
335
346inline double geometric_cdf(std::uint64_t k, double p)
347{
348 if (p <= 0.0 || p > 1.0) {
349 throw std::invalid_argument("statcpp::geometric_cdf: p must be in (0, 1]");
350 }
351 if (p == 1.0) return 1.0;
352
353 return 1.0 - std::pow(1.0 - p, static_cast<double>(k + 1));
354}
355
364inline std::uint64_t geometric_quantile(double prob, double p)
365{
366 if (p <= 0.0 || p > 1.0) {
367 throw std::invalid_argument("statcpp::geometric_quantile: p must be in (0, 1]");
368 }
369 if (prob < 0.0 || prob > 1.0) {
370 throw std::invalid_argument("statcpp::geometric_quantile: prob must be in [0, 1]");
371 }
372 if (prob == 0.0) return 0;
373 // Infinite support: prob == 1.0 has no finite quantile. Return the maximum
374 // representable value instead of casting +inf to uint64 (undefined behavior).
375 if (prob == 1.0) return std::numeric_limits<std::uint64_t>::max();
376 if (p == 1.0) return 0;
377
378 // Q(prob) = ceil(log(1 - prob) / log(1 - p)) - 1
379 double k_real = std::ceil(std::log(1.0 - prob) / std::log(1.0 - p)) - 1.0;
380 return static_cast<std::uint64_t>(std::max(0.0, k_real));
381}
382
392template <typename Engine = default_random_engine>
393std::uint64_t geometric_rand(double p, Engine& engine)
394{
395 if (p <= 0.0 || p > 1.0) {
396 throw std::invalid_argument("statcpp::geometric_rand: p must be in (0, 1]");
397 }
398 std::geometric_distribution<std::uint64_t> dist(p);
399 return dist(engine);
400}
401
408inline std::uint64_t geometric_rand(double p)
409{
411}
412
413// ============================================================================
414// Hypergeometric Distribution
415// ============================================================================
416
429inline double hypergeom_pmf(std::uint64_t k, std::uint64_t N, std::uint64_t K, std::uint64_t n)
430{
431 if (K > N) {
432 throw std::invalid_argument("statcpp::hypergeom_pmf: K must be <= N");
433 }
434 if (n > N) {
435 throw std::invalid_argument("statcpp::hypergeom_pmf: n must be <= N");
436 }
437
438 // k must be in valid range
439 std::uint64_t k_min = (n > N - K) ? n - (N - K) : 0;
440 std::uint64_t k_max = std::min(n, K);
441
442 if (k < k_min || k > k_max) return 0.0;
443
444 double log_pmf = log_binomial_coef(K, k) + log_binomial_coef(N - K, n - k) - log_binomial_coef(N, n);
445 return std::exp(log_pmf);
446}
447
460inline double hypergeom_cdf(std::uint64_t k, std::uint64_t N, std::uint64_t K, std::uint64_t n)
461{
462 if (K > N) {
463 throw std::invalid_argument("statcpp::hypergeom_cdf: K must be <= N");
464 }
465 if (n > N) {
466 throw std::invalid_argument("statcpp::hypergeom_cdf: n must be <= N");
467 }
468
469 std::uint64_t k_min = (n > N - K) ? n - (N - K) : 0;
470 std::uint64_t k_max = std::min(n, K);
471
472 if (k >= k_max) return 1.0;
473
474 double sum = 0.0;
475 for (std::uint64_t i = k_min; i <= k; ++i) {
476 sum += hypergeom_pmf(i, N, K, n);
477 }
478 return std::min(1.0, sum);
479}
480
491inline std::uint64_t hypergeom_quantile(double p, std::uint64_t N, std::uint64_t K, std::uint64_t n)
492{
493 if (K > N) {
494 throw std::invalid_argument("statcpp::hypergeom_quantile: K must be <= N");
495 }
496 if (n > N) {
497 throw std::invalid_argument("statcpp::hypergeom_quantile: n must be <= N");
498 }
499 if (p < 0.0 || p > 1.0) {
500 throw std::invalid_argument("statcpp::hypergeom_quantile: p must be in [0, 1]");
501 }
502
503 std::uint64_t k_min = (n > N - K) ? n - (N - K) : 0;
504 std::uint64_t k_max = std::min(n, K);
505
506 if (p == 0.0) return k_min;
507 if (p == 1.0) return k_max;
508
509 double cum = 0.0;
510 for (std::uint64_t k = k_min; k <= k_max; ++k) {
511 cum += hypergeom_pmf(k, N, K, n);
512 if (cum >= p) return k;
513 }
514
515 return k_max;
516}
517
531template <typename Engine = default_random_engine>
532std::uint64_t hypergeom_rand(std::uint64_t N, std::uint64_t K, std::uint64_t n, Engine& engine)
533{
534 if (K > N) {
535 throw std::invalid_argument("statcpp::hypergeom_rand: K must be <= N");
536 }
537 if (n > N) {
538 throw std::invalid_argument("statcpp::hypergeom_rand: n must be <= N");
539 }
540
541 // Direct sampling (for moderate n)
542 std::uint64_t successes = 0;
543 std::uint64_t population = N;
544 std::uint64_t success_states = K;
545
546 std::uniform_real_distribution<double> uniform(0.0, 1.0);
547
548 for (std::uint64_t i = 0; i < n; ++i) {
549 double p = static_cast<double>(success_states) / static_cast<double>(population);
550 if (uniform(engine) < p) {
551 ++successes;
552 --success_states;
553 }
554 --population;
555 }
556
557 return successes;
558}
559
568inline std::uint64_t hypergeom_rand(std::uint64_t N, std::uint64_t K, std::uint64_t n)
569{
570 return hypergeom_rand(N, K, n, get_random_engine());
571}
572
573// ============================================================================
574// Negative Binomial Distribution
575// ============================================================================
576
603inline double nbinom_pmf(std::uint64_t k, double r, double p)
604{
605 if (r <= 0.0) {
606 throw std::invalid_argument("statcpp::nbinom_pmf: r must be positive");
607 }
608 if (p <= 0.0 || p > 1.0) {
609 throw std::invalid_argument("statcpp::nbinom_pmf: p must be in (0, 1]");
610 }
611 if (p == 1.0) return (k == 0) ? 1.0 : 0.0;
612
613 double log_pmf = lgamma(k + r) - log_factorial(k) - lgamma(r)
614 + r * std::log(p) + k * std::log(1.0 - p);
615 return std::exp(log_pmf);
616}
617
629inline double nbinom_cdf(std::uint64_t k, double r, double p)
630{
631 if (r <= 0.0) {
632 throw std::invalid_argument("statcpp::nbinom_cdf: r must be positive");
633 }
634 if (p <= 0.0 || p > 1.0) {
635 throw std::invalid_argument("statcpp::nbinom_cdf: p must be in (0, 1]");
636 }
637 if (p == 1.0) return 1.0;
638
639 return betainc(r, static_cast<double>(k + 1), p);
640}
641
651inline std::uint64_t nbinom_quantile(double prob, double r, double p)
652{
653 if (r <= 0.0) {
654 throw std::invalid_argument("statcpp::nbinom_quantile: r must be positive");
655 }
656 if (p <= 0.0 || p > 1.0) {
657 throw std::invalid_argument("statcpp::nbinom_quantile: p must be in (0, 1]");
658 }
659 if (prob < 0.0 || prob > 1.0) {
660 throw std::invalid_argument("statcpp::nbinom_quantile: prob must be in [0, 1]");
661 }
662 if (prob == 0.0) return 0;
663 // Infinite support: prob == 1.0 has no finite quantile. Return the maximum
664 // representable value instead of casting +inf to uint64 (undefined behavior).
665 if (prob == 1.0) return std::numeric_limits<std::uint64_t>::max();
666 if (p == 1.0) return 0;
667
668 // Start with Gaussian approximation
669 double mean_val = r * (1.0 - p) / p;
670 double var_val = r * (1.0 - p) / (p * p);
671 double z = norm_quantile(prob);
672 double guess = mean_val + z * std::sqrt(var_val);
673 std::uint64_t k = static_cast<std::uint64_t>(std::max(0.0, guess));
674
675 // Adjust
676 constexpr std::uint64_t kMaxIter = 1000000;
677 std::uint64_t iter = 0;
678 while (k > 0 && nbinom_cdf(k - 1, r, p) >= prob) {
679 --k;
680 if (++iter >= kMaxIter) return k;
681 }
682 while (nbinom_cdf(k, r, p) < prob) {
683 ++k;
684 if (++iter >= kMaxIter) return k;
685 }
686
687 return k;
688}
689
702template <typename Engine = default_random_engine>
703std::uint64_t nbinom_rand(double r, double p, Engine& engine)
704{
705 if (r <= 0.0) {
706 throw std::invalid_argument("statcpp::nbinom_rand: r must be positive");
707 }
708 if (p <= 0.0 || p > 1.0) {
709 throw std::invalid_argument("statcpp::nbinom_rand: p must be in (0, 1]");
710 }
711
712 // Negative binomial as Poisson-Gamma mixture
713 // X ~ NB(r, p) can be generated as Poisson(Y) where Y ~ Gamma(r, p/(1-p))
714 std::gamma_distribution<double> gamma_dist(r, (1.0 - p) / p);
715 double y = gamma_dist(engine);
716 std::poisson_distribution<std::uint64_t> poisson_dist(y);
717 return poisson_dist(engine);
718}
719
727inline std::uint64_t nbinom_rand(double r, double p)
728{
729 return nbinom_rand(r, p, get_random_engine());
730}
731
732// ============================================================================
733// Bernoulli Distribution
734// ============================================================================
735
746inline double bernoulli_pmf(std::uint64_t k, double p)
747{
748 if (p < 0.0 || p > 1.0) {
749 throw std::invalid_argument("statcpp::bernoulli_pmf: p must be in [0, 1]");
750 }
751 if (k == 0) {
752 return 1.0 - p;
753 } else if (k == 1) {
754 return p;
755 } else {
756 return 0.0;
757 }
758}
759
768inline double bernoulli_cdf(std::uint64_t k, double p)
769{
770 if (p < 0.0 || p > 1.0) {
771 throw std::invalid_argument("statcpp::bernoulli_cdf: p must be in [0, 1]");
772 }
773 if (k == 0) {
774 return 1.0 - p;
775 } else {
776 return 1.0;
777 }
778}
779
787inline std::uint64_t bernoulli_quantile(double prob, double p)
788{
789 if (prob < 0.0 || prob > 1.0) {
790 throw std::invalid_argument("statcpp::bernoulli_quantile: prob must be in [0, 1]");
791 }
792 if (p < 0.0 || p > 1.0) {
793 throw std::invalid_argument("statcpp::bernoulli_quantile: p must be in [0, 1]");
794 }
795 if (prob <= 1.0 - p) return 0;
796 return 1;
797}
798
808template <typename Engine = default_random_engine>
809std::uint64_t bernoulli_rand(double p, Engine& engine)
810{
811 if (p < 0.0 || p > 1.0) {
812 throw std::invalid_argument("statcpp::bernoulli_rand: p must be in [0, 1]");
813 }
814 std::bernoulli_distribution dist(p);
815 return dist(engine) ? 1 : 0;
816}
817
824inline std::uint64_t bernoulli_rand(double p)
825{
827}
828
829// ============================================================================
830// Discrete Uniform Distribution
831// ============================================================================
832
844inline double discrete_uniform_pmf(std::int64_t k, std::int64_t a, std::int64_t b)
845{
846 if (a > b) {
847 throw std::invalid_argument("statcpp::discrete_uniform_pmf: a must be <= b");
848 }
849 if (k < a || k > b) {
850 return 0.0;
851 }
852 return 1.0 / static_cast<double>(b - a + 1);
853}
854
864inline double discrete_uniform_cdf(std::int64_t k, std::int64_t a, std::int64_t b)
865{
866 if (a > b) {
867 throw std::invalid_argument("statcpp::discrete_uniform_cdf: a must be <= b");
868 }
869 if (k < a) {
870 return 0.0;
871 }
872 if (k >= b) {
873 return 1.0;
874 }
875 return static_cast<double>(k - a + 1) / static_cast<double>(b - a + 1);
876}
877
887inline std::int64_t discrete_uniform_quantile(double p, std::int64_t a, std::int64_t b)
888{
889 if (a > b) {
890 throw std::invalid_argument("statcpp::discrete_uniform_quantile: a must be <= b");
891 }
892 if (p < 0.0 || p > 1.0) {
893 throw std::invalid_argument("statcpp::discrete_uniform_quantile: p must be in [0, 1]");
894 }
895 std::int64_t range = b - a + 1;
896 std::int64_t k = a + static_cast<std::int64_t>(std::ceil(p * static_cast<double>(range) - 1.0));
897 if (k < a) k = a;
898 if (k > b) k = b;
899 return k;
900}
901
912template <typename Engine = default_random_engine>
913std::int64_t discrete_uniform_rand(std::int64_t a, std::int64_t b, Engine& engine)
914{
915 if (a > b) {
916 throw std::invalid_argument("statcpp::discrete_uniform_rand: a must be <= b");
917 }
918 std::uniform_int_distribution<std::int64_t> dist(a, b);
919 return dist(engine);
920}
921
929inline std::int64_t discrete_uniform_rand(std::int64_t a, std::int64_t b)
930{
932}
933
934} // namespace statcpp
std::int64_t discrete_uniform_rand(std::int64_t a, std::int64_t b, Engine &engine)
Discrete uniform distribution random number generation.
double log_binomial_coef(std::uint64_t n, std::uint64_t k)
Calculate log binomial coefficient.
double betainc(double a, double b, double x)
Regularized incomplete beta function.
double range(Iterator first, Iterator last)
Range (maximum - minimum)
double geometric_pmf(std::uint64_t k, double p)
Geometric distribution probability mass function (PMF)
std::uint64_t hypergeom_quantile(double p, std::uint64_t N, std::uint64_t K, std::uint64_t n)
Hypergeometric distribution quantile function (inverse CDF)
double hypergeom_cdf(std::uint64_t k, std::uint64_t N, std::uint64_t K, std::uint64_t n)
Hypergeometric distribution cumulative distribution function (CDF)
auto sum(Iterator first, Iterator last)
Sum.
double poisson_cdf(std::uint64_t k, double lambda)
Poisson distribution cumulative distribution function (CDF)
double norm_quantile(double p)
Standard normal quantile function.
std::uint64_t bernoulli_quantile(double prob, double p)
Bernoulli distribution quantile function.
std::uint64_t bernoulli_rand(double p, Engine &engine)
Bernoulli distribution random number generation.
double binomial_pmf(std::uint64_t k, std::uint64_t n, double p)
Binomial distribution probability mass function (PMF)
std::uint64_t nbinom_quantile(double prob, double r, double p)
Negative binomial distribution quantile function (inverse CDF)
std::uint64_t poisson_quantile(double p, double lambda)
Poisson distribution quantile function.
std::uint64_t geometric_quantile(double prob, double p)
Geometric distribution quantile function (inverse CDF)
std::uint64_t binomial_quantile(double prob, std::uint64_t n, double p)
Binomial distribution quantile function.
std::uint64_t poisson_rand(double lambda, Engine &engine)
Poisson distribution random number generation.
default_random_engine & get_random_engine()
Singleton accessor for global random engine.
double hypergeom_pmf(std::uint64_t k, std::uint64_t N, std::uint64_t K, std::uint64_t n)
Hypergeometric distribution probability mass function (PMF)
std::uint64_t hypergeom_rand(std::uint64_t N, std::uint64_t K, std::uint64_t n, Engine &engine)
Hypergeometric distribution random number generation.
std::uint64_t binomial_rand(std::uint64_t n, double p, Engine &engine)
Binomial distribution random number generation.
std::uint64_t geometric_rand(double p, Engine &engine)
Geometric distribution random number generation.
double log_factorial(std::uint64_t n)
Calculate log factorial.
double nbinom_cdf(std::uint64_t k, double r, double p)
Negative binomial distribution cumulative distribution function (CDF)
double discrete_uniform_cdf(std::int64_t k, std::int64_t a, std::int64_t b)
Discrete uniform distribution cumulative distribution function (CDF)
std::uint64_t nbinom_rand(double r, double p, Engine &engine)
Negative binomial distribution random number generation.
double gammainc_upper(double a, double x)
Upper regularized incomplete gamma function.
double geometric_cdf(std::uint64_t k, double p)
Geometric distribution cumulative distribution function (CDF)
double bernoulli_cdf(std::uint64_t k, double p)
Bernoulli distribution cumulative distribution function (CDF)
double binomial_coef(std::uint64_t n, std::uint64_t k)
Calculate binomial coefficient.
std::int64_t discrete_uniform_quantile(double p, std::int64_t a, std::int64_t b)
Discrete uniform distribution quantile function.
double binomial_cdf(std::uint64_t k, std::uint64_t n, double p)
Binomial distribution cumulative distribution function (CDF)
double poisson_pmf(std::uint64_t k, double lambda)
Poisson distribution probability mass function (PMF)
double discrete_uniform_pmf(std::int64_t k, std::int64_t a, std::int64_t b)
Discrete uniform distribution probability mass function (PMF)
double bernoulli_pmf(std::uint64_t k, double p)
Bernoulli distribution probability mass function (PMF)
double nbinom_pmf(std::uint64_t k, double r, double p)
Negative binomial distribution probability mass function (PMF)
double lgamma(double x)
Log-gamma function.
Random engine wrapper and utilities.
Special mathematical functions implementation.