statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
continuous_distributions.hpp
Go to the documentation of this file.
1
10#pragma once
11
14
15#include <algorithm>
16#include <cmath>
17#include <limits>
18#include <random>
19#include <stdexcept>
20
21namespace statcpp {
22
23// ============================================================================
24// Uniform Distribution
25// ============================================================================
26
38inline double uniform_pdf(double x, double a = 0.0, double b = 1.0)
39{
40 if (a >= b) {
41 throw std::invalid_argument("statcpp::uniform_pdf: a must be less than b");
42 }
43 if (x < a || x > b) {
44 return 0.0;
45 }
46 return 1.0 / (b - a);
47}
48
60inline double uniform_cdf(double x, double a = 0.0, double b = 1.0)
61{
62 if (a >= b) {
63 throw std::invalid_argument("statcpp::uniform_cdf: a must be less than b");
64 }
65 if (x < a) return 0.0;
66 if (x > b) return 1.0;
67 return (x - a) / (b - a);
68}
69
81inline double uniform_quantile(double p, double a = 0.0, double b = 1.0)
82{
83 if (a >= b) {
84 throw std::invalid_argument("statcpp::uniform_quantile: a must be less than b");
85 }
86 if (p < 0.0 || p > 1.0) {
87 throw std::invalid_argument("statcpp::uniform_quantile: p must be in [0, 1]");
88 }
89 return a + p * (b - a);
90}
91
102template <typename Engine = default_random_engine>
103double uniform_rand(double a, double b, Engine& engine)
104{
105 if (a >= b) {
106 throw std::invalid_argument("statcpp::uniform_rand: a must be less than b");
107 }
108 std::uniform_real_distribution<double> dist(a, b);
109 return dist(engine);
110}
111
119inline double uniform_rand(double a = 0.0, double b = 1.0)
120{
121 return uniform_rand(a, b, get_random_engine());
122}
123
124// ============================================================================
125// Normal Distribution
126// ============================================================================
127
139inline double normal_pdf(double x, double mu = 0.0, double sigma = 1.0)
140{
141 if (sigma <= 0.0) {
142 throw std::invalid_argument("statcpp::normal_pdf: sigma must be positive");
143 }
144 double z = (x - mu) / sigma;
145 return std::exp(-0.5 * z * z) / (sigma * sqrt_2_pi);
146}
147
159inline double normal_cdf(double x, double mu = 0.0, double sigma = 1.0)
160{
161 if (sigma <= 0.0) {
162 throw std::invalid_argument("statcpp::normal_cdf: sigma must be positive");
163 }
164 return norm_cdf((x - mu) / sigma);
165}
166
185inline double normal_quantile(double p, double mu = 0.0, double sigma = 1.0)
186{
187 if (sigma <= 0.0) {
188 throw std::invalid_argument("statcpp::normal_quantile: sigma must be positive");
189 }
190 if (p <= 0.0 || p >= 1.0) {
191 if (p == 0.0) return -std::numeric_limits<double>::infinity();
192 if (p == 1.0) return std::numeric_limits<double>::infinity();
193 throw std::invalid_argument("statcpp::normal_quantile: p must be in (0, 1)");
194 }
195 return mu + sigma * norm_quantile(p);
196}
197
208template <typename Engine = default_random_engine>
209double normal_rand(double mu, double sigma, Engine& engine)
210{
211 if (sigma <= 0.0) {
212 throw std::invalid_argument("statcpp::normal_rand: sigma must be positive");
213 }
214 std::normal_distribution<double> dist(mu, sigma);
215 return dist(engine);
216}
217
225inline double normal_rand(double mu = 0.0, double sigma = 1.0)
226{
227 return normal_rand(mu, sigma, get_random_engine());
228}
229
230// ============================================================================
231// Exponential Distribution
232// ============================================================================
233
244inline double exponential_pdf(double x, double lambda = 1.0)
245{
246 if (lambda <= 0.0) {
247 throw std::invalid_argument("statcpp::exponential_pdf: lambda must be positive");
248 }
249 if (x < 0.0) return 0.0;
250 return lambda * std::exp(-lambda * x);
251}
252
263inline double exponential_cdf(double x, double lambda = 1.0)
264{
265 if (lambda <= 0.0) {
266 throw std::invalid_argument("statcpp::exponential_cdf: lambda must be positive");
267 }
268 if (x < 0.0) return 0.0;
269 return -std::expm1(-lambda * x);
270}
271
282inline double exponential_quantile(double p, double lambda = 1.0)
283{
284 if (lambda <= 0.0) {
285 throw std::invalid_argument("statcpp::exponential_quantile: lambda must be positive");
286 }
287 if (p < 0.0 || p >= 1.0) {
288 if (p == 1.0) return std::numeric_limits<double>::infinity();
289 throw std::invalid_argument("statcpp::exponential_quantile: p must be in [0, 1)");
290 }
291 return -std::log(1.0 - p) / lambda;
292}
293
303template <typename Engine = default_random_engine>
304double exponential_rand(double lambda, Engine& engine)
305{
306 if (lambda <= 0.0) {
307 throw std::invalid_argument("statcpp::exponential_rand: lambda must be positive");
308 }
309 std::exponential_distribution<double> dist(lambda);
310 return dist(engine);
311}
312
319inline double exponential_rand(double lambda = 1.0)
320{
321 return exponential_rand(lambda, get_random_engine());
322}
323
324// ============================================================================
325// Gamma Distribution
326// ============================================================================
327
340inline double gamma_pdf(double x, double shape, double rate = 1.0)
341{
342 if (shape <= 0.0) {
343 throw std::invalid_argument("statcpp::gamma_pdf: shape must be positive");
344 }
345 if (rate <= 0.0) {
346 throw std::invalid_argument("statcpp::gamma_pdf: rate must be positive");
347 }
348 if (x < 0.0) return 0.0;
349 if (x == 0.0) {
350 if (shape < 1.0) return std::numeric_limits<double>::infinity();
351 if (shape == 1.0) return rate; // Exponential distribution PDF at x=0
352 return 0.0; // shape > 1
353 }
354
355 return std::exp(shape * std::log(rate) + (shape - 1.0) * std::log(x) - rate * x - lgamma(shape));
356}
357
369inline double gamma_cdf(double x, double shape, double rate = 1.0)
370{
371 if (shape <= 0.0) {
372 throw std::invalid_argument("statcpp::gamma_cdf: shape must be positive");
373 }
374 if (rate <= 0.0) {
375 throw std::invalid_argument("statcpp::gamma_cdf: rate must be positive");
376 }
377 if (x <= 0.0) return 0.0;
378
379 return gammainc_lower(shape, rate * x);
380}
381
391inline double gamma_quantile(double p, double shape, double rate = 1.0)
392{
393 if (shape <= 0.0) {
394 throw std::invalid_argument("statcpp::gamma_quantile: shape must be positive");
395 }
396 if (rate <= 0.0) {
397 throw std::invalid_argument("statcpp::gamma_quantile: rate must be positive");
398 }
399 if (p < 0.0 || p > 1.0) {
400 throw std::invalid_argument("statcpp::gamma_quantile: p must be in [0, 1]");
401 }
402 if (p == 0.0) return 0.0;
403 if (p == 1.0) return std::numeric_limits<double>::infinity();
404
405 return gammainc_lower_inv(shape, p) / rate;
406}
407
418template <typename Engine = default_random_engine>
419double gamma_rand(double shape, double rate, Engine& engine)
420{
421 if (shape <= 0.0) {
422 throw std::invalid_argument("statcpp::gamma_rand: shape must be positive");
423 }
424 if (rate <= 0.0) {
425 throw std::invalid_argument("statcpp::gamma_rand: rate must be positive");
426 }
427 // std::gamma_distribution uses shape and scale (1/rate)
428 std::gamma_distribution<double> dist(shape, 1.0 / rate);
429 return dist(engine);
430}
431
439inline double gamma_rand(double shape, double rate = 1.0)
440{
441 return gamma_rand(shape, rate, get_random_engine());
442}
443
444// ============================================================================
445// Beta Distribution
446// ============================================================================
447
459inline double beta_pdf(double x, double alpha, double beta_param)
460{
461 if (alpha <= 0.0) {
462 throw std::invalid_argument("statcpp::beta_pdf: alpha must be positive");
463 }
464 if (beta_param <= 0.0) {
465 throw std::invalid_argument("statcpp::beta_pdf: beta must be positive");
466 }
467 if (x < 0.0 || x > 1.0) return 0.0;
468 if (x == 0.0) {
469 if (alpha < 1.0) return std::numeric_limits<double>::infinity();
470 if (alpha == 1.0) return beta_param; // Beta(1, beta) PDF at x=0
471 return 0.0; // alpha > 1
472 }
473 if (x == 1.0) {
474 if (beta_param < 1.0) return std::numeric_limits<double>::infinity();
475 if (beta_param == 1.0) return alpha; // Beta(alpha, 1) PDF at x=1
476 return 0.0; // beta > 1
477 }
478
479 return std::exp((alpha - 1.0) * std::log(x) + (beta_param - 1.0) * std::log(1.0 - x) - lbeta(alpha, beta_param));
480}
481
493inline double beta_cdf(double x, double alpha, double beta_param)
494{
495 if (alpha <= 0.0) {
496 throw std::invalid_argument("statcpp::beta_cdf: alpha must be positive");
497 }
498 if (beta_param <= 0.0) {
499 throw std::invalid_argument("statcpp::beta_cdf: beta must be positive");
500 }
501 if (x <= 0.0) return 0.0;
502 if (x >= 1.0) return 1.0;
503
504 return betainc(alpha, beta_param, x);
505}
506
516inline double beta_quantile(double p, double alpha, double beta_param)
517{
518 if (alpha <= 0.0) {
519 throw std::invalid_argument("statcpp::beta_quantile: alpha must be positive");
520 }
521 if (beta_param <= 0.0) {
522 throw std::invalid_argument("statcpp::beta_quantile: beta must be positive");
523 }
524 if (p < 0.0 || p > 1.0) {
525 throw std::invalid_argument("statcpp::beta_quantile: p must be in [0, 1]");
526 }
527 if (p == 0.0) return 0.0;
528 if (p == 1.0) return 1.0;
529
530 return betaincinv(alpha, beta_param, p);
531}
532
543template <typename Engine = default_random_engine>
544double beta_rand(double alpha, double beta_param, Engine& engine)
545{
546 if (alpha <= 0.0) {
547 throw std::invalid_argument("statcpp::beta_rand: alpha must be positive");
548 }
549 if (beta_param <= 0.0) {
550 throw std::invalid_argument("statcpp::beta_rand: beta must be positive");
551 }
552
553 std::gamma_distribution<double> dist_a(alpha, 1.0);
554 std::gamma_distribution<double> dist_b(beta_param, 1.0);
555
556 double x = dist_a(engine);
557 double y = dist_b(engine);
558 // Extremely small shape parameters can underflow both gamma variates to 0,
559 // yielding 0/0 = NaN. Re-draw until the sum is positive.
560 while (x + y <= 0.0) {
561 x = dist_a(engine);
562 y = dist_b(engine);
563 }
564 return x / (x + y);
565}
566
574inline double beta_rand(double alpha, double beta_param)
575{
576 return beta_rand(alpha, beta_param, get_random_engine());
577}
578
579// ============================================================================
580// Chi-Square Distribution
581// ============================================================================
582
593inline double chisq_pdf(double x, double df)
594{
595 if (df <= 0.0) {
596 throw std::invalid_argument("statcpp::chisq_pdf: df must be positive");
597 }
598 return gamma_pdf(x, df / 2.0, 0.5);
599}
600
613inline double chisq_cdf(double x, double df)
614{
615 if (df <= 0.0) {
616 throw std::invalid_argument("statcpp::chisq_cdf: df must be positive");
617 }
618 return gamma_cdf(x, df / 2.0, 0.5);
619}
620
629inline double chisq_quantile(double p, double df)
630{
631 if (df <= 0.0) {
632 throw std::invalid_argument("statcpp::chisq_quantile: df must be positive");
633 }
634 return gamma_quantile(p, df / 2.0, 0.5);
635}
636
646template <typename Engine = default_random_engine>
647double chisq_rand(double df, Engine& engine)
648{
649 if (df <= 0.0) {
650 throw std::invalid_argument("statcpp::chisq_rand: df must be positive");
651 }
652 return gamma_rand(df / 2.0, 0.5, engine);
653}
654
661inline double chisq_rand(double df)
662{
663 return chisq_rand(df, get_random_engine());
664}
665
666// ============================================================================
667// Student's t-Distribution
668// ============================================================================
669
680inline double t_pdf(double x, double df)
681{
682 if (df <= 0.0) {
683 throw std::invalid_argument("statcpp::t_pdf: df must be positive");
684 }
685
686 double coef = std::exp(lgamma((df + 1.0) / 2.0) - lgamma(df / 2.0)) / std::sqrt(df * pi);
687 return coef * std::pow(1.0 + x * x / df, -(df + 1.0) / 2.0);
688}
689
707inline double t_cdf(double x, double df)
708{
709 if (df <= 0.0) {
710 throw std::invalid_argument("statcpp::t_cdf: df must be positive");
711 }
712
713 double t2 = x * x;
714 double p = betainc(df / 2.0, 0.5, df / (df + t2));
715
716 if (x >= 0.0) {
717 return 1.0 - 0.5 * p;
718 } else {
719 return 0.5 * p;
720 }
721}
722
739inline double t_quantile(double p, double df)
740{
741 if (df <= 0.0) {
742 throw std::invalid_argument("statcpp::t_quantile: df must be positive");
743 }
744 if (p <= 0.0 || p >= 1.0) {
745 if (p == 0.0) return -std::numeric_limits<double>::infinity();
746 if (p == 1.0) return std::numeric_limits<double>::infinity();
747 throw std::invalid_argument("statcpp::t_quantile: p must be in (0, 1)");
748 }
749
750 // Use normal quantile as initial guess for large df
751 double x = norm_quantile(p);
752
753 // For small df (but df > 2), adjust initial guess using variance scaling
754 if (df > 2.0 && df < 4.0) {
755 x *= std::sqrt(df / (df - 2.0));
756 }
757
758 const double eps = 1e-10;
759 const int max_iter = 50;
760
761 for (int i = 0; i < max_iter; ++i) {
762 if (!std::isfinite(x)) {
763 x = norm_quantile(p);
764 }
765 double f = t_cdf(x, df) - p;
766 if (std::abs(f) < eps) {
767 return x;
768 }
769 double fprime = t_pdf(x, df);
770 if (fprime == 0.0) break;
771
772 double x_new = x - f / fprime;
773
774 if (std::abs(x_new - x) < eps * (1.0 + std::abs(x))) {
775 return x_new;
776 }
777
778 x = x_new;
779 }
780
781 if (!std::isfinite(x)) {
782 x = norm_quantile(p);
783 }
784 return x;
785}
786
796template <typename Engine = default_random_engine>
797double t_rand(double df, Engine& engine)
798{
799 if (df <= 0.0) {
800 throw std::invalid_argument("statcpp::t_rand: df must be positive");
801 }
802 std::student_t_distribution<double> dist(df);
803 return dist(engine);
804}
805
812inline double t_rand(double df)
813{
814 return t_rand(df, get_random_engine());
815}
816
817// ============================================================================
818// F-Distribution
819// ============================================================================
820
832inline double f_pdf(double x, double df1, double df2)
833{
834 if (df1 <= 0.0) {
835 throw std::invalid_argument("statcpp::f_pdf: df1 must be positive");
836 }
837 if (df2 <= 0.0) {
838 throw std::invalid_argument("statcpp::f_pdf: df2 must be positive");
839 }
840 if (x <= 0.0) return 0.0;
841
842 double log_pdf = (df1 / 2.0) * std::log(df1) + (df2 / 2.0) * std::log(df2)
843 + (df1 / 2.0 - 1.0) * std::log(x)
844 - ((df1 + df2) / 2.0) * std::log(df1 * x + df2)
845 - lbeta(df1 / 2.0, df2 / 2.0);
846
847 return std::exp(log_pdf);
848}
849
862inline double f_cdf(double x, double df1, double df2)
863{
864 if (df1 <= 0.0) {
865 throw std::invalid_argument("statcpp::f_cdf: df1 must be positive");
866 }
867 if (df2 <= 0.0) {
868 throw std::invalid_argument("statcpp::f_cdf: df2 must be positive");
869 }
870 if (x <= 0.0) return 0.0;
871
872 double z = df1 * x / (df1 * x + df2);
873 return betainc(df1 / 2.0, df2 / 2.0, z);
874}
875
893inline double f_quantile(double p, double df1, double df2)
894{
895 if (df1 <= 0.0) {
896 throw std::invalid_argument("statcpp::f_quantile: df1 must be positive");
897 }
898 if (df2 <= 0.0) {
899 throw std::invalid_argument("statcpp::f_quantile: df2 must be positive");
900 }
901 if (p < 0.0 || p > 1.0) {
902 throw std::invalid_argument("statcpp::f_quantile: p must be in [0, 1]");
903 }
904 if (p == 0.0) return 0.0;
905 if (p == 1.0) return std::numeric_limits<double>::infinity();
906
907 // Initial guess using beta quantile
908 double z = betaincinv(df1 / 2.0, df2 / 2.0, p);
909 double x = df2 * z / (df1 * (1.0 - z));
910 if (!std::isfinite(x) || x <= 0.0) {
911 x = 1.0;
912 }
913
914 const double eps = 1e-10;
915 const int max_iter = 50;
916
917 for (int i = 0; i < max_iter; ++i) {
918 if (!std::isfinite(x) || x <= 0.0) {
919 x = 1.0;
920 }
921 double f = f_cdf(x, df1, df2) - p;
922 if (std::abs(f) < eps) {
923 return x;
924 }
925 double fprime = f_pdf(x, df1, df2);
926 if (fprime == 0.0) break;
927
928 double x_new = x - f / fprime;
929 if (x_new <= 0.0) {
930 x_new = x / 2.0;
931 }
932
933 if (std::abs(x_new - x) < eps * x) {
934 return x_new;
935 }
936
937 x = x_new;
938 }
939
940 if (!std::isfinite(x) || x <= 0.0) {
941 x = 1.0;
942 }
943 return x;
944}
945
956template <typename Engine = default_random_engine>
957double f_rand(double df1, double df2, Engine& engine)
958{
959 if (df1 <= 0.0) {
960 throw std::invalid_argument("statcpp::f_rand: df1 must be positive");
961 }
962 if (df2 <= 0.0) {
963 throw std::invalid_argument("statcpp::f_rand: df2 must be positive");
964 }
965 std::fisher_f_distribution<double> dist(df1, df2);
966 return dist(engine);
967}
968
976inline double f_rand(double df1, double df2)
977{
978 return f_rand(df1, df2, get_random_engine());
979}
980
981// ============================================================================
982// Log-normal Distribution
983// ============================================================================
984
996inline double lognormal_pdf(double x, double mu = 0.0, double sigma = 1.0)
997{
998 if (sigma <= 0.0) {
999 throw std::invalid_argument("statcpp::lognormal_pdf: sigma must be positive");
1000 }
1001 if (x <= 0.0) {
1002 return 0.0;
1003 }
1004 double log_x = std::log(x);
1005 double z = (log_x - mu) / sigma;
1006 return std::exp(-0.5 * z * z) / (x * sigma * sqrt_2_pi);
1007}
1008
1020inline double lognormal_cdf(double x, double mu = 0.0, double sigma = 1.0)
1021{
1022 if (sigma <= 0.0) {
1023 throw std::invalid_argument("statcpp::lognormal_cdf: sigma must be positive");
1024 }
1025 if (x <= 0.0) {
1026 return 0.0;
1027 }
1028 double log_x = std::log(x);
1029 return norm_cdf((log_x - mu) / sigma);
1030}
1031
1043inline double lognormal_quantile(double p, double mu = 0.0, double sigma = 1.0)
1044{
1045 if (sigma <= 0.0) {
1046 throw std::invalid_argument("statcpp::lognormal_quantile: sigma must be positive");
1047 }
1048 if (p <= 0.0 || p >= 1.0) {
1049 if (p == 0.0) return 0.0;
1050 if (p == 1.0) return std::numeric_limits<double>::infinity();
1051 throw std::invalid_argument("statcpp::lognormal_quantile: p must be in (0, 1)");
1052 }
1053 return std::exp(mu + sigma * norm_quantile(p));
1054}
1055
1066template <typename Engine = default_random_engine>
1067double lognormal_rand(double mu, double sigma, Engine& engine)
1068{
1069 if (sigma <= 0.0) {
1070 throw std::invalid_argument("statcpp::lognormal_rand: sigma must be positive");
1071 }
1072 std::lognormal_distribution<double> dist(mu, sigma);
1073 return dist(engine);
1074}
1075
1083inline double lognormal_rand(double mu = 0.0, double sigma = 1.0)
1084{
1085 return lognormal_rand(mu, sigma, get_random_engine());
1086}
1087
1088// ============================================================================
1089// Weibull Distribution
1090// ============================================================================
1091
1103inline double weibull_pdf(double x, double shape, double scale = 1.0)
1104{
1105 if (shape <= 0.0) {
1106 throw std::invalid_argument("statcpp::weibull_pdf: shape must be positive");
1107 }
1108 if (scale <= 0.0) {
1109 throw std::invalid_argument("statcpp::weibull_pdf: scale must be positive");
1110 }
1111 if (x < 0.0) {
1112 return 0.0;
1113 }
1114 if (x == 0.0) {
1115 if (shape < 1.0) {
1116 return std::numeric_limits<double>::infinity();
1117 } else if (shape == 1.0) {
1118 return 1.0 / scale;
1119 } else {
1120 return 0.0;
1121 }
1122 }
1123 double z = x / scale;
1124 return (shape / scale) * std::pow(z, shape - 1.0) * std::exp(-std::pow(z, shape));
1125}
1126
1138inline double weibull_cdf(double x, double shape, double scale = 1.0)
1139{
1140 if (shape <= 0.0) {
1141 throw std::invalid_argument("statcpp::weibull_cdf: shape must be positive");
1142 }
1143 if (scale <= 0.0) {
1144 throw std::invalid_argument("statcpp::weibull_cdf: scale must be positive");
1145 }
1146 if (x <= 0.0) {
1147 return 0.0;
1148 }
1149 double z = x / scale;
1150 return 1.0 - std::exp(-std::pow(z, shape));
1151}
1152
1164inline double weibull_quantile(double p, double shape, double scale = 1.0)
1165{
1166 if (shape <= 0.0) {
1167 throw std::invalid_argument("statcpp::weibull_quantile: shape must be positive");
1168 }
1169 if (scale <= 0.0) {
1170 throw std::invalid_argument("statcpp::weibull_quantile: scale must be positive");
1171 }
1172 if (p < 0.0 || p > 1.0) {
1173 throw std::invalid_argument("statcpp::weibull_quantile: p must be in [0, 1]");
1174 }
1175 if (p == 0.0) return 0.0;
1176 if (p == 1.0) return std::numeric_limits<double>::infinity();
1177 return scale * std::pow(-std::log(1.0 - p), 1.0 / shape);
1178}
1179
1190template <typename Engine = default_random_engine>
1191double weibull_rand(double shape, double scale, Engine& engine)
1192{
1193 if (shape <= 0.0) {
1194 throw std::invalid_argument("statcpp::weibull_rand: shape must be positive");
1195 }
1196 if (scale <= 0.0) {
1197 throw std::invalid_argument("statcpp::weibull_rand: scale must be positive");
1198 }
1199 std::weibull_distribution<double> dist(shape, scale);
1200 return dist(engine);
1201}
1202
1210inline double weibull_rand(double shape, double scale = 1.0)
1211{
1212 return weibull_rand(shape, scale, get_random_engine());
1213}
1214
1215// ============================================================================
1216// Studentized Range Distribution
1217// ============================================================================
1218
1235inline double studentized_range_cdf(double q, double k, double df)
1236{
1237 if (k < 2.0) {
1238 throw std::invalid_argument("statcpp::studentized_range_cdf: k must be >= 2");
1239 }
1240 if (df <= 0.0) {
1241 throw std::invalid_argument("statcpp::studentized_range_cdf: df must be positive");
1242 }
1243 if (q <= 0.0) return 0.0;
1244
1245 // k=2 special case: exact via t-distribution
1246 // F(q; 2, v) = 2 * t_cdf(q / sqrt(2), v) - 1
1247 if (k == 2.0) {
1248 double result = 2.0 * t_cdf(q / sqrt_2, df) - 1.0;
1249 return std::max(0.0, std::min(1.0, result));
1250 }
1251
1252 // 12-point Gauss-Legendre half-nodes and half-weights (for inner integral)
1253 // Following R ptukey.c: only half-nodes stored, symmetric evaluation
1254 static constexpr int ihalf = 6;
1255 static constexpr double xleg[ihalf] = {
1256 0.981560634246719250690549090149,
1257 0.904117256370474856678465866119,
1258 0.769902674194304687036893833213,
1259 0.587317954286617447296702418941,
1260 0.367831498998180193752691536644,
1261 0.125233408511468915472441369464
1262 };
1263 static constexpr double aleg[ihalf] = {
1264 0.047175336386511827194615961485,
1265 0.106939325995318430960254718194,
1266 0.160078328543346226334652529543,
1267 0.203167426723065921749064455810,
1268 0.233492536538354808760849898925,
1269 0.249147045813402785000562436043
1270 };
1271
1272 // Inner function: range probability (Hartley's form)
1273 // Faithful port of R's wprob()
1274 auto wprob = [&](double w, double cc) -> double {
1275 static constexpr double C1 = -30.0;
1276 static constexpr double C2 = -50.0;
1277 static constexpr double C3 = 60.0;
1278 static constexpr double bb = 8.0;
1279 static constexpr double wlar = 3.0;
1280 static constexpr double wincr1 = 2.0;
1281 static constexpr double wincr2 = 3.0;
1282 static constexpr double M_1_SQRT_2PI = 0.398942280401432677939946059934;
1283
1284 double qsqz = w * 0.5;
1285 if (qsqz >= bb) return 1.0;
1286
1287 // First term: [2*Phi(w/2) - 1]^cc
1288 double pr_w = 2.0 * norm_cdf(qsqz) - 1.0;
1289 if (pr_w >= std::exp(C2 / cc))
1290 pr_w = std::pow(pr_w, cc);
1291 else
1292 pr_w = 0.0;
1293
1294 // Interval subdivision for second integral
1295 double wincr = (w > wlar) ? wincr1 : wincr2;
1296
1297 // Integrate from w/2 to 8 using Gauss-Legendre
1298 double blb = qsqz;
1299 double binc = (bb - qsqz) / wincr;
1300 double bub = blb + binc;
1301 double einsum = 0.0;
1302 double cc1 = cc - 1.0;
1303
1304 for (int wi = 0; wi < static_cast<int>(wincr); ++wi) {
1305 double elsum = 0.0;
1306 double a = 0.5 * (bub + blb);
1307 double b = 0.5 * (bub - blb);
1308
1309 for (int jj = 1; jj <= 12; ++jj) {
1310 double xx;
1311 int j;
1312 if (ihalf < jj) {
1313 j = 12 - jj;
1314 xx = xleg[j];
1315 } else {
1316 j = jj - 1;
1317 xx = -xleg[j];
1318 }
1319 double ac = a + b * xx;
1320
1321 double qexpo = ac * ac;
1322 if (qexpo > C3) break;
1323
1324 double pplus = 2.0 * norm_cdf(ac);
1325 double pminus = 2.0 * norm_cdf(ac - w);
1326 double rinsum = (pplus * 0.5) - (pminus * 0.5);
1327
1328 if (rinsum >= std::exp(C1 / cc1)) {
1329 rinsum = aleg[jj <= ihalf ? jj - 1 : 12 - jj]
1330 * std::exp(-0.5 * qexpo) * std::pow(rinsum, cc1);
1331 elsum += rinsum;
1332 }
1333 }
1334 elsum *= (2.0 * b) * cc * M_1_SQRT_2PI;
1335 einsum += elsum;
1336 blb = bub;
1337 bub += binc;
1338 }
1339
1340 pr_w += einsum;
1341 if (pr_w <= std::exp(C1)) return 0.0;
1342
1343 pr_w = std::pow(pr_w, 1.0); // rr=1 for Tukey HSD
1344 return std::max(0.0, std::min(1.0, pr_w));
1345 };
1346
1347 // Asymptotic case: df > 25000
1348 if (df > 25000.0) {
1349 return wprob(q, k);
1350 }
1351
1352 // General case: integrate over chi-squared density
1353 // 16-point Gauss-Legendre half-nodes and half-weights (outer integral)
1354 static constexpr int ihalfq = 8;
1355 static constexpr double xlegq[ihalfq] = {
1356 0.989400934991649932596154244360,
1357 0.944575023073232576077988415535,
1358 0.865631202387831743880467897712,
1359 0.755404408355003033895101194847,
1360 0.617876244402643748446671764049,
1361 0.458016777657227386342419442984,
1362 0.281603550779258913230460501460,
1363 0.095012509837637440185319335425
1364 };
1365 static constexpr double alegq[ihalfq] = {
1366 0.027152459411754094851780572456,
1367 0.062253523938647892862843836994,
1368 0.095158511682492784809925107602,
1369 0.124628971255533872052476282192,
1370 0.149595988816576732081501730547,
1371 0.169156519395002538189312079030,
1372 0.182603415044923588866763667969,
1373 0.189450610455068496285396723208
1374 };
1375
1376 // Precompute chi-squared density constants (following R ptukey.c)
1377 double f2 = df * 0.5;
1378 double f2lf = (f2 * std::log(df)) - (df * std::log(2.0)) - lgamma(f2);
1379 double f21 = f2 - 1.0;
1380 double ff4 = df * 0.25;
1381
1382 // Adaptive interval width
1383 double ulen;
1384 if (df <= 100.0) ulen = 1.0;
1385 else if (df <= 800.0) ulen = 0.5;
1386 else if (df <= 5000.0) ulen = 0.25;
1387 else ulen = 0.125;
1388
1389 f2lf += std::log(ulen);
1390
1391 double ans = 0.0;
1392
1393 for (int i = 1; i <= 50; ++i) {
1394 double otsum = 0.0;
1395 double twa1 = (2.0 * i - 1.0) * ulen;
1396
1397 for (int jj = 1; jj <= 16; ++jj) {
1398 double t1;
1399 int j;
1400 double t_point;
1401
1402 if (ihalfq < jj) {
1403 j = jj - ihalfq - 1;
1404 t_point = twa1 + xlegq[j] * ulen;
1405 t1 = f2lf + f21 * std::log(t_point) - t_point * ff4;
1406 } else {
1407 j = jj - 1;
1408 t_point = twa1 - xlegq[j] * ulen;
1409 t1 = f2lf + f21 * std::log(t_point) - t_point * ff4;
1410 }
1411
1412 if (t_point <= 0.0) continue;
1413 if (t1 >= -30.0) {
1414 double qsqz;
1415 if (ihalfq < jj) {
1416 qsqz = q * std::sqrt((xlegq[j] * ulen + twa1) * 0.5);
1417 } else {
1418 qsqz = q * std::sqrt((-(xlegq[j] * ulen) + twa1) * 0.5);
1419 }
1420
1421 double wprb = wprob(qsqz, k);
1422 otsum += (wprb * alegq[j]) * std::exp(t1);
1423 }
1424 }
1425
1426 // Convergence check (R: if i*ulen >= 1.0 && otsum <= 1e-14)
1427 if (static_cast<double>(i) * ulen >= 1.0 && otsum <= 1e-14) {
1428 break;
1429 }
1430
1431 ans += otsum;
1432 }
1433
1434 return std::max(0.0, std::min(1.0, ans));
1435}
1436
1460inline double studentized_range_quantile(double p, double k, double df)
1461{
1462 if (k < 2.0) {
1463 throw std::invalid_argument("statcpp::studentized_range_quantile: k must be >= 2");
1464 }
1465 if (df <= 0.0) {
1466 throw std::invalid_argument("statcpp::studentized_range_quantile: df must be positive");
1467 }
1468 if (p < 0.0 || p > 1.0) {
1469 throw std::invalid_argument("statcpp::studentized_range_quantile: p must be in [0, 1]");
1470 }
1471 if (p == 0.0) return 0.0;
1472 if (p == 1.0) return std::numeric_limits<double>::infinity();
1473
1474 // Initial guess: heuristic based on normal quantile
1475 double q0 = norm_quantile(1.0 - (1.0 - p) * 0.5);
1476 double x = q0 * (1.0 + 0.2 * (k - 2.0) / std::sqrt(df));
1477 if (!std::isfinite(x) || x < 1.0) x = 1.0;
1478
1479 // Newton-Raphson iteration
1480 const double eps = 1e-10;
1481 const int max_iter = 50;
1482
1483 for (int i = 0; i < max_iter; ++i) {
1484 if (!std::isfinite(x) || x <= 0.0) {
1485 x = 1.0;
1486 }
1487 double f = studentized_range_cdf(x, k, df) - p;
1488 if (std::abs(f) < eps) return x;
1489
1490 // Numerical derivative (central difference)
1491 double dx = std::max(1e-6, x * 1e-6);
1492 double fprime = (studentized_range_cdf(x + dx, k, df)
1493 - studentized_range_cdf(x - dx, k, df)) / (2.0 * dx);
1494 if (fprime <= 0.0) break;
1495
1496 double x_new = x - f / fprime;
1497 if (x_new <= 0.0) x_new = x * 0.5;
1498
1499 if (std::abs(x_new - x) < eps * (1.0 + std::abs(x))) return x_new;
1500 x = x_new;
1501 }
1502
1503 if (!std::isfinite(x) || x <= 0.0) {
1504 x = 1.0;
1505 }
1506 return x;
1507}
1508
1509} // namespace statcpp
double betainc(double a, double b, double x)
Regularized incomplete beta function.
double lognormal_rand(double mu, double sigma, Engine &engine)
Log-normal distribution random number generation.
double t_rand(double df, Engine &engine)
t-distribution random number generation
double gamma_cdf(double x, double shape, double rate=1.0)
Gamma distribution cumulative distribution function (CDF)
double normal_quantile(double p, double mu=0.0, double sigma=1.0)
Normal distribution quantile function (inverse CDF, percent point function)
double chisq_rand(double df, Engine &engine)
Chi-square distribution random number generation.
double lbeta(double a, double b)
Log-beta function.
double studentized_range_cdf(double q, double k, double df)
CDF of the studentized range distribution.
double f_quantile(double p, double df1, double df2)
F-distribution quantile function (Newton-Raphson method)
double gamma_pdf(double x, double shape, double rate=1.0)
Gamma distribution probability density function (PDF)
constexpr double pi
Pi constant.
double chisq_cdf(double x, double df)
Chi-square distribution cumulative distribution function (CDF)
double t_pdf(double x, double df)
t-distribution probability density function (PDF)
double normal_cdf(double x, double mu=0.0, double sigma=1.0)
Normal distribution cumulative distribution function (CDF)
double normal_pdf(double x, double mu=0.0, double sigma=1.0)
Normal distribution probability density function (PDF)
double t_cdf(double x, double df)
t-distribution cumulative distribution function (CDF)
double normal_rand(double mu, double sigma, Engine &engine)
Normal distribution random number generation.
double norm_cdf(double x)
Standard normal CDF.
double weibull_rand(double shape, double scale, Engine &engine)
Weibull distribution random number generation.
double norm_quantile(double p)
Standard normal quantile function.
constexpr double sqrt_2
Square root of 2.
double beta_pdf(double x, double alpha, double beta_param)
Beta distribution probability density function (PDF)
constexpr double sqrt_2_pi
Square root of 2*pi.
double lognormal_cdf(double x, double mu=0.0, double sigma=1.0)
Log-normal distribution cumulative distribution function (CDF)
double gammainc_lower_inv(double a, double p)
Inverse lower regularized incomplete gamma function.
double weibull_pdf(double x, double shape, double scale=1.0)
Weibull distribution probability density function (PDF)
double uniform_rand(double a, double b, Engine &engine)
Uniform distribution random number generation.
double weibull_cdf(double x, double shape, double scale=1.0)
Weibull distribution cumulative distribution function (CDF)
double betaincinv(double a, double b, double p)
Inverse regularized incomplete beta function.
double chisq_pdf(double x, double df)
Chi-square distribution probability density function (PDF)
double exponential_cdf(double x, double lambda=1.0)
Exponential distribution cumulative distribution function (CDF)
double chisq_quantile(double p, double df)
Chi-square distribution quantile function.
double f_pdf(double x, double df1, double df2)
F-distribution probability density function (PDF)
double gammainc_lower(double a, double x)
Lower regularized incomplete gamma function.
double uniform_cdf(double x, double a=0.0, double b=1.0)
Uniform distribution cumulative distribution function (CDF)
double t_quantile(double p, double df)
t-distribution quantile function (Newton-Raphson method)
double gamma_rand(double shape, double rate, Engine &engine)
Gamma distribution random number generation.
double weibull_quantile(double p, double shape, double scale=1.0)
Weibull distribution quantile function.
double lognormal_pdf(double x, double mu=0.0, double sigma=1.0)
Log-normal distribution probability density function (PDF)
double gamma_quantile(double p, double shape, double rate=1.0)
Gamma distribution quantile function.
double beta_cdf(double x, double alpha, double beta_param)
Beta distribution cumulative distribution function (CDF)
default_random_engine & get_random_engine()
Singleton accessor for global random engine.
double exponential_quantile(double p, double lambda=1.0)
Exponential distribution quantile function.
double uniform_quantile(double p, double a=0.0, double b=1.0)
Uniform distribution quantile function (inverse CDF)
double uniform_pdf(double x, double a=0.0, double b=1.0)
Uniform distribution probability density function (PDF)
double f_rand(double df1, double df2, Engine &engine)
F-distribution random number generation.
double beta_rand(double alpha, double beta_param, Engine &engine)
Beta distribution random number generation (using gamma variates)
double exponential_rand(double lambda, Engine &engine)
Exponential distribution random number generation.
double lognormal_quantile(double p, double mu=0.0, double sigma=1.0)
Log-normal distribution quantile function.
double studentized_range_quantile(double p, double k, double df)
Quantile function of the studentized range distribution.
double f_cdf(double x, double df1, double df2)
F-distribution cumulative distribution function (CDF)
double lgamma(double x)
Log-gamma function.
double exponential_pdf(double x, double lambda=1.0)
Exponential distribution probability density function (PDF)
double beta_quantile(double p, double alpha, double beta_param)
Beta distribution quantile function.
Random engine wrapper and utilities.
Special mathematical functions implementation.