statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
nonparametric_tests.hpp
Go to the documentation of this file.
1
19#pragma once
20
26
27#include <algorithm>
28#include <cmath>
29#include <cstddef>
30#include <cstdint>
31#include <limits>
32#include <stdexcept>
33#include <utility>
34#include <vector>
35
36namespace statcpp {
37
38// ============================================================================
39// Helper: Compute Ranks with Tie Handling
40// ============================================================================
41
55template <typename Iterator>
56std::vector<double> compute_ranks_with_ties(Iterator first, Iterator last)
57{
58 auto n = statcpp::count(first, last);
59 if (n == 0) return {};
60
61 // Create index-value pairs
62 std::vector<std::pair<double, std::size_t>> indexed(n);
63 std::size_t i = 0;
64 for (auto it = first; it != last; ++it, ++i) {
65 indexed[i] = {static_cast<double>(*it), i};
66 }
67
68 // Sort by value
69 std::sort(indexed.begin(), indexed.end(),
70 [](const auto& a, const auto& b) { return a.first < b.first; });
71
72 // Assign ranks with tie handling (average rank)
73 std::vector<double> ranks(n);
74 std::size_t j = 0;
75 while (j < n) {
76 std::size_t k = j;
77 // Find all elements with same value
78 while (k < n && indexed[k].first == indexed[j].first) {
79 ++k;
80 }
81 // Average rank for tied elements
82 double avg_rank = (static_cast<double>(j + 1) + static_cast<double>(k)) / 2.0;
83 for (std::size_t m = j; m < k; ++m) {
84 ranks[indexed[m].second] = avg_rank;
85 }
86 j = k;
87 }
88
89 return ranks;
90}
91
101inline std::vector<std::size_t> compute_tie_groups(const std::vector<double>& sorted_values)
102{
103 std::vector<std::size_t> tie_groups;
104 std::size_t n = sorted_values.size();
105 std::size_t i = 0;
106 while (i < n) {
107 std::size_t j = i;
108 while (j < n && sorted_values[j] == sorted_values[i]) {
109 ++j;
110 }
111 std::size_t t = j - i;
112 if (t > 1) {
113 tie_groups.push_back(t);
114 }
115 i = j;
116 }
117 return tie_groups;
118}
119
120// ============================================================================
121// Shapiro-Wilk Test for Normality
122// ============================================================================
123
143template <typename Iterator>
144test_result shapiro_wilk_test(Iterator first, Iterator last)
145{
146 auto n = statcpp::count(first, last);
147 if (n < 3) {
148 throw std::invalid_argument("statcpp::shapiro_wilk_test: need at least 3 elements");
149 }
150 if (n > 5000) {
151 throw std::invalid_argument("statcpp::shapiro_wilk_test: n > 5000 not supported");
152 }
153
154 // Copy and sort data
155 std::vector<double> sorted_data;
156 sorted_data.reserve(n);
157 for (auto it = first; it != last; ++it) {
158 sorted_data.push_back(static_cast<double>(*it));
159 }
160 std::sort(sorted_data.begin(), sorted_data.end());
161
162 double mean_val = statcpp::mean(sorted_data.begin(), sorted_data.end());
163
164 // Compute SS (sum of squared deviations)
165 double ss = 0.0;
166 for (double x : sorted_data) {
167 double d = x - mean_val;
168 ss += d * d;
169 }
170
171 if (ss == 0.0) {
172 throw std::invalid_argument("statcpp::shapiro_wilk_test: zero variance");
173 }
174
175 // Compute W statistic using Royston's approximation
176 // First compute all m_i values (expected order statistics of standard normal)
177 std::vector<double> m_vals(n);
178 for (std::size_t i = 0; i < n; ++i) {
179 double p = (static_cast<double>(i + 1) - 0.375) / (static_cast<double>(n) + 0.25);
180 m_vals[i] = norm_quantile(p);
181 }
182
183 // Compute sum of squared m values
184 double sum_m2 = 0.0;
185 for (std::size_t i = 0; i < n; ++i) {
186 sum_m2 += m_vals[i] * m_vals[i];
187 }
188
189 // Compute a coefficients using Royston's algorithm
190 std::vector<double> a(n, 0.0);
191
192 if (n <= 5) {
193 // For very small n, use simple approximation
194 double sqrt_sum_m2 = std::sqrt(sum_m2);
195 for (std::size_t i = 0; i < n; ++i) {
196 a[i] = m_vals[i] / sqrt_sum_m2;
197 }
198 } else {
199 // Royston's polynomial approximation for a_n
200 double sqrt_n = std::sqrt(static_cast<double>(n));
201 double u = 1.0 / sqrt_n;
202
203 // a_n coefficient (for largest order statistic)
204 double a_n = -2.706056 * std::pow(u, 5) + 4.434685 * std::pow(u, 4)
205 - 2.071190 * std::pow(u, 3) - 0.147981 * std::pow(u, 2)
206 + 0.221157 * u + m_vals[n - 1] / std::sqrt(sum_m2);
207
208 // a_{n-1} coefficient
209 double a_n1 = -3.582633 * std::pow(u, 5) + 5.682633 * std::pow(u, 4)
210 - 1.752461 * std::pow(u, 3) - 0.293762 * std::pow(u, 2)
211 + 0.042981 * u + m_vals[n - 2] / std::sqrt(sum_m2);
212
213 // Set the extreme coefficients
214 a[n - 1] = a_n;
215 a[0] = -a_n;
216 if (n > 3) {
217 a[n - 2] = a_n1;
218 a[1] = -a_n1;
219 }
220
221 // Compute phi for intermediate coefficients
222 double phi = (sum_m2 - 2.0 * m_vals[n - 1] * m_vals[n - 1]
223 - 2.0 * m_vals[n - 2] * m_vals[n - 2])
224 / (1.0 - 2.0 * a_n * a_n - 2.0 * a_n1 * a_n1);
225
226 if (phi > 0) {
227 double sqrt_phi = std::sqrt(phi);
228 for (std::size_t i = 2; i < n - 2; ++i) {
229 a[i] = m_vals[i] / sqrt_phi;
230 }
231 }
232 }
233
234 // Compute W = (sum(a_i * x_(i)))^2 / SS
235 double b = 0.0;
236 for (std::size_t i = 0; i < n; ++i) {
237 b += a[i] * sorted_data[i];
238 }
239
240 double w = (b * b) / ss;
241
242 // Clamp W to valid range [0, 1]
243 w = std::max(0.0, std::min(1.0, w));
244
245 // Approximation for p-value using transformation to normal
246 double ln_n = std::log(static_cast<double>(n));
247 double mu, sigma, gamma;
248
249 if (n <= 11) {
250 gamma = 0.459 * static_cast<double>(n) - 2.273;
251 mu = -0.0006714 * std::pow(static_cast<double>(n), 3)
252 + 0.025054 * std::pow(static_cast<double>(n), 2)
253 - 0.39978 * static_cast<double>(n) + 0.5440;
254 sigma = std::exp(-0.0020322 * std::pow(static_cast<double>(n), 3)
255 + 0.062767 * std::pow(static_cast<double>(n), 2)
256 - 0.77857 * static_cast<double>(n) + 1.3822);
257 } else {
258 gamma = 0.0;
259 mu = 0.0038915 * std::pow(ln_n, 3) - 0.083751 * std::pow(ln_n, 2)
260 - 0.31082 * ln_n - 1.5861;
261 sigma = std::exp(0.0030302 * std::pow(ln_n, 2) - 0.082676 * ln_n - 0.4803);
262 }
263
264 double z;
265 if (gamma != 0.0 && w < 1.0) {
266 double arg = gamma - std::log(1.0 - w);
267 if (arg > 0) {
268 z = (-std::log(arg) - mu) / sigma;
269 } else {
270 z = -8.0; // Very high W (very normal): p -> 1
271 }
272 } else if (w < 1.0) {
273 z = (std::log(1.0 - w) - mu) / sigma;
274 } else {
275 z = -8.0; // Perfect W = 1 (maximally normal): p -> 1
276 }
277
278 double p_value = norm_sf(z);
279 p_value = std::max(0.0, std::min(1.0, p_value));
280
281 return {w, p_value, static_cast<double>(n), alternative_hypothesis::less};
282}
283
284// ============================================================================
285// Lilliefors Test for Normality
286// ============================================================================
287
312template <typename Iterator>
313test_result lilliefors_test(Iterator first, Iterator last)
314{
315 auto n = statcpp::count(first, last);
316 if (n < 2) {
317 throw std::invalid_argument("statcpp::lilliefors_test: need at least 2 elements");
318 }
319
320 // Standardize data
321 double mean_val = statcpp::mean(first, last);
322 double sd = statcpp::sample_stddev(first, last);
323
324 if (sd == 0.0) {
325 throw std::invalid_argument("statcpp::lilliefors_test: zero variance");
326 }
327
328 std::vector<double> standardized;
329 standardized.reserve(n);
330 for (auto it = first; it != last; ++it) {
331 standardized.push_back((static_cast<double>(*it) - mean_val) / sd);
332 }
333 std::sort(standardized.begin(), standardized.end());
334
335 // Compute D statistic
336 double d_plus = 0.0;
337 double d_minus = 0.0;
338
339 for (std::size_t i = 0; i < n; ++i) {
340 double f_x = norm_cdf(standardized[i]);
341 double f_n_upper = static_cast<double>(i + 1) / static_cast<double>(n);
342 double f_n_lower = static_cast<double>(i) / static_cast<double>(n);
343
344 d_plus = std::max(d_plus, f_n_upper - f_x);
345 d_minus = std::max(d_minus, f_x - f_n_lower);
346 }
347
348 double d = std::max(d_plus, d_minus);
349
350 // p-value from the Dallal and Wilkinson (1986) analytic approximation to the
351 // null distribution of D when the mean and variance are estimated from the
352 // sample. See "An analytic approximation to the distribution of Lilliefors's
353 // test statistic for normality", The American Statistician 40:294-296.
354 //
355 // The approximation is published for p <= 0.10 and is calibrated for n >= 5.
356 // Every conventional significance level (0.10, 0.05, 0.01) falls inside that
357 // range; above 0.10 the returned value only indicates that the sample is
358 // consistent with normality and should not be read as an accurate p-value.
359 const double n_d = static_cast<double>(n);
360 const double a = n_d + 2.78019;
361
362 // As a function of d the exponent is a downward parabola, so to the left of
363 // its vertex the approximation would increase with d. Clamping at the vertex
364 // keeps the p-value monotonically non-increasing in d over the whole domain.
365 const double d_vertex = 2.99587 / (2.0 * 7.01256 * std::sqrt(a));
366 const double d_eff = std::max(d, d_vertex);
367
368 double p_value = std::exp(-7.01256 * d_eff * d_eff * a + 2.99587 * d_eff * std::sqrt(a) -
369 0.122119 + 0.974598 / std::sqrt(n_d) + 1.67997 / n_d);
370 p_value = std::max(0.0, std::min(1.0, p_value));
371
372 return {d, p_value, static_cast<double>(n), alternative_hypothesis::greater};
373}
374
382template <typename Iterator>
383[[deprecated("Use lilliefors_test() instead. ks_test_normal() will be removed in a future version.")]]
384test_result ks_test_normal(Iterator first, Iterator last)
385{
386 return lilliefors_test(first, last);
387}
388
389// ============================================================================
390// Levene's Test for Homogeneity of Variance
391// ============================================================================
392
409inline test_result levene_test(const std::vector<std::vector<double>>& groups)
410{
411 std::size_t k = groups.size();
412 if (k < 2) {
413 throw std::invalid_argument("statcpp::levene_test: need at least 2 groups");
414 }
415
416 // Compute group medians and deviations from median
417 std::vector<std::vector<double>> z_values(k);
418 std::size_t total_n = 0;
419
420 for (std::size_t i = 0; i < k; ++i) {
421 if (groups[i].size() < 2) {
422 throw std::invalid_argument("statcpp::levene_test: each group needs at least 2 elements");
423 }
424
425 std::vector<double> sorted = groups[i];
426 std::sort(sorted.begin(), sorted.end());
427 double med = statcpp::median(sorted.begin(), sorted.end());
428
429 for (double x : groups[i]) {
430 z_values[i].push_back(std::abs(x - med));
431 }
432 total_n += groups[i].size();
433 }
434
435 // Compute group means of z values
436 std::vector<double> z_means(k);
437 double z_grand_mean = 0.0;
438
439 for (std::size_t i = 0; i < k; ++i) {
440 z_means[i] = statcpp::mean(z_values[i].begin(), z_values[i].end());
441 z_grand_mean += z_means[i] * static_cast<double>(z_values[i].size());
442 }
443 z_grand_mean /= static_cast<double>(total_n);
444
445 // Compute test statistic
446 double ss_between = 0.0;
447 double ss_within = 0.0;
448
449 for (std::size_t i = 0; i < k; ++i) {
450 double ni = static_cast<double>(z_values[i].size());
451 ss_between += ni * (z_means[i] - z_grand_mean) * (z_means[i] - z_grand_mean);
452
453 for (double z : z_values[i]) {
454 ss_within += (z - z_means[i]) * (z - z_means[i]);
455 }
456 }
457
458 double df1 = static_cast<double>(k - 1);
459 double df2 = static_cast<double>(total_n - k);
460
461 // Guard: if all deviations are zero (all values identical within each group),
462 // variances are trivially equal → F = 0, p = 1
463 if (ss_within == 0.0) {
464 return {0.0, 1.0, df1, alternative_hypothesis::greater};
465 }
466
467 double f = (ss_between / df1) / (ss_within / df2);
468 double p_value = 1.0 - f_cdf(f, df1, df2);
469
470 return {f, p_value, df1, alternative_hypothesis::greater};
471}
472
473// ============================================================================
474// Bartlett's Test for Homogeneity of Variance
475// ============================================================================
476
494inline test_result bartlett_test(const std::vector<std::vector<double>>& groups)
495{
496 std::size_t k = groups.size();
497 if (k < 2) {
498 throw std::invalid_argument("statcpp::bartlett_test: need at least 2 groups");
499 }
500
501 std::vector<double> vars(k);
502 std::vector<std::size_t> ns(k);
503 std::size_t total_n = 0;
504 double pooled_var_num = 0.0;
505
506 for (std::size_t i = 0; i < k; ++i) {
507 if (groups[i].size() < 2) {
508 throw std::invalid_argument("statcpp::bartlett_test: each group needs at least 2 elements");
509 }
510 ns[i] = groups[i].size();
511 total_n += ns[i];
512 vars[i] = statcpp::sample_variance(groups[i].begin(), groups[i].end());
513
514 if (vars[i] <= 0.0) {
515 throw std::invalid_argument("statcpp::bartlett_test: zero or negative variance in group");
516 }
517
518 pooled_var_num += (ns[i] - 1) * vars[i];
519 }
520
521 double pooled_var = pooled_var_num / static_cast<double>(total_n - k);
522
523 // Compute Bartlett's statistic
524 double sum_log = 0.0;
525 double sum_inv = 0.0;
526
527 for (std::size_t i = 0; i < k; ++i) {
528 double df_i = static_cast<double>(ns[i] - 1);
529 sum_log += df_i * std::log(vars[i]);
530 sum_inv += 1.0 / df_i;
531 }
532
533 double df_total = static_cast<double>(total_n - k);
534 double chi2 = df_total * std::log(pooled_var) - sum_log;
535
536 // Correction factor
537 double c = 1.0 + (sum_inv - 1.0 / df_total) / (3.0 * (k - 1));
538 chi2 /= c;
539
540 double df = static_cast<double>(k - 1);
541 double p_value = 1.0 - chisq_cdf(chi2, df);
542
543 return {chi2, p_value, df, alternative_hypothesis::greater};
544}
545
546// ============================================================================
547// Wilcoxon Signed-Rank Test
548// ============================================================================
549
570template <typename Iterator>
571test_result wilcoxon_signed_rank_test(Iterator first, Iterator last, double mu0 = 0.0,
573{
574 auto n = statcpp::count(first, last);
575 if (n < 2) {
576 throw std::invalid_argument("statcpp::wilcoxon_signed_rank_test: need at least 2 elements");
577 }
578
579 // Compute differences from mu0, excluding zeros
580 std::vector<double> diffs;
581 for (auto it = first; it != last; ++it) {
582 double d = static_cast<double>(*it) - mu0;
583 if (d != 0.0) {
584 diffs.push_back(d);
585 }
586 }
587
588 std::size_t n_nonzero = diffs.size();
589 if (n_nonzero < 2) {
590 throw std::invalid_argument("statcpp::wilcoxon_signed_rank_test: need at least 2 non-zero differences");
591 }
592
593 // Compute ranks of absolute differences
594 std::vector<double> abs_diffs(n_nonzero);
595 for (std::size_t i = 0; i < n_nonzero; ++i) {
596 abs_diffs[i] = std::abs(diffs[i]);
597 }
598
599 auto ranks = compute_ranks_with_ties(abs_diffs.begin(), abs_diffs.end());
600
601 // Compute W+ (sum of ranks of positive differences)
602 double w = 0.0;
603
604 for (std::size_t i = 0; i < n_nonzero; ++i) {
605 if (diffs[i] > 0.0) {
606 w += ranks[i];
607 }
608 }
609
610 // Normal approximation for p-value (with continuity correction)
611 double nn = static_cast<double>(n_nonzero);
612 double mean_w = nn * (nn + 1.0) / 4.0;
613 double var_w = nn * (nn + 1.0) * (2.0 * nn + 1.0) / 24.0;
614
615 // Tie correction for variance
616 // Sort absolute differences to compute tie groups
617 std::vector<double> sorted_abs(abs_diffs.begin(), abs_diffs.end());
618 std::sort(sorted_abs.begin(), sorted_abs.end());
619 auto tie_groups = compute_tie_groups(sorted_abs);
620 // Subtract tie correction: sum(t^3 - t) / 48
621 double tie_correction = 0.0;
622 for (auto t : tie_groups) {
623 double td = static_cast<double>(t);
624 tie_correction += td * td * td - td;
625 }
626 var_w -= tie_correction / 48.0;
627
628 double se = std::sqrt(std::max(0.0, var_w));
629
630 if (se == 0.0) {
631 return {w, 1.0, static_cast<double>(n), alt};
632 }
633
634 double z;
635 double p_value;
636
637 switch (alt) {
639 z = (w - mean_w + 0.5) / se;
640 p_value = norm_cdf(z);
641 break;
643 z = (w - mean_w - 0.5) / se;
644 p_value = norm_sf(z);
645 break;
647 default:
648 z = (w - mean_w) / se;
649 if (z < 0) z = (w - mean_w + 0.5) / se;
650 else z = (w - mean_w - 0.5) / se;
651 p_value = 2.0 * std::min(norm_cdf(z), norm_sf(z));
652 break;
653 }
654
655 return {w, p_value, static_cast<double>(n_nonzero), alt};
656}
657
658// ============================================================================
659// Mann-Whitney U Test
660// ============================================================================
661
683template <typename Iterator1, typename Iterator2>
684test_result mann_whitney_u_test(Iterator1 first1, Iterator1 last1,
685 Iterator2 first2, Iterator2 last2,
687 bool correct = true)
688{
689 auto n1 = statcpp::count(first1, last1);
690 auto n2 = statcpp::count(first2, last2);
691
692 if (n1 < 2 || n2 < 2) {
693 throw std::invalid_argument("statcpp::mann_whitney_u_test: need at least 2 elements in each sample");
694 }
695
696 // Combine and rank all observations
697 std::vector<std::pair<double, int>> combined;
698 combined.reserve(n1 + n2);
699
700 for (auto it = first1; it != last1; ++it) {
701 combined.push_back({static_cast<double>(*it), 1}); // group 1
702 }
703 for (auto it = first2; it != last2; ++it) {
704 combined.push_back({static_cast<double>(*it), 2}); // group 2
705 }
706
707 // Sort by value
708 std::sort(combined.begin(), combined.end(),
709 [](const auto& a, const auto& b) { return a.first < b.first; });
710
711 // Assign ranks with tie handling
712 std::size_t total_n = n1 + n2;
713 std::vector<double> ranks(total_n);
714 std::size_t i = 0;
715 while (i < total_n) {
716 std::size_t j = i;
717 while (j < total_n && combined[j].first == combined[i].first) {
718 ++j;
719 }
720 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
721 for (std::size_t k = i; k < j; ++k) {
722 ranks[k] = avg_rank;
723 }
724 i = j;
725 }
726
727 // Compute R1 (sum of ranks in group 1)
728 double r1 = 0.0;
729 for (std::size_t k = 0; k < total_n; ++k) {
730 if (combined[k].second == 1) {
731 r1 += ranks[k];
732 }
733 }
734
735 // Compute U1
736 double u1 = r1 - n1 * (n1 + 1.0) / 2.0;
737 (void)(static_cast<double>(n1 * n2) - u1); // u2 not used but computed for reference
738
739 // Normal approximation with tie correction
740 double N = static_cast<double>(total_n);
741 double mean_u = static_cast<double>(n1 * n2) / 2.0;
742 double var_u = static_cast<double>(n1 * n2) / 12.0 * (N + 1.0);
743
744 // Tie correction: compute tie groups from sorted combined values
745 {
746 std::vector<double> sorted_vals;
747 sorted_vals.reserve(total_n);
748 for (const auto& p : combined) {
749 sorted_vals.push_back(p.first);
750 }
751 // combined is already sorted by value
752 auto tie_groups = compute_tie_groups(sorted_vals);
753 double tie_sum = 0.0;
754 for (auto t : tie_groups) {
755 double td = static_cast<double>(t);
756 tie_sum += td * td * td - td;
757 }
758 // Corrected variance: n1*n2/(12*N*(N-1)) * (N^3 - N - tie_sum)
759 // which equals n1*n2/12 * (N+1) - n1*n2/(12*N*(N-1)) * tie_sum
760 if (N > 1.0 && tie_sum > 0.0) {
761 var_u = static_cast<double>(n1 * n2) / (12.0 * N * (N - 1.0))
762 * (N * N * N - N - tie_sum);
763 }
764 }
765
766 double se = std::sqrt(std::max(0.0, var_u));
767
768 if (se == 0.0) {
769 return {u1, 1.0, static_cast<double>(n1 + n2), alt};
770 }
771
772 // Continuity correction (Yates)
773 double diff = u1 - mean_u;
774 if (correct) {
775 if (diff > 0) diff -= 0.5;
776 else if (diff < 0) diff += 0.5;
777 }
778 double z = diff / se;
779
780 double p_value;
781 switch (alt) {
783 p_value = norm_cdf(z);
784 break;
786 p_value = norm_sf(z);
787 break;
789 default:
790 p_value = 2.0 * std::min(norm_cdf(z), norm_sf(z));
791 break;
792 }
793
794 return {u1, p_value, static_cast<double>(n1 + n2), alt};
795}
796
797// ============================================================================
798// Kruskal-Wallis Test
799// ============================================================================
800
817inline test_result kruskal_wallis_test(const std::vector<std::vector<double>>& groups)
818{
819 std::size_t k = groups.size();
820 if (k < 2) {
821 throw std::invalid_argument("statcpp::kruskal_wallis_test: need at least 2 groups");
822 }
823
824 // Combine all observations with group labels
825 std::vector<std::pair<double, std::size_t>> combined;
826 std::vector<std::size_t> ns(k);
827 std::size_t total_n = 0;
828
829 for (std::size_t i = 0; i < k; ++i) {
830 if (groups[i].empty()) {
831 throw std::invalid_argument("statcpp::kruskal_wallis_test: empty group");
832 }
833 ns[i] = groups[i].size();
834 total_n += ns[i];
835 for (double x : groups[i]) {
836 combined.push_back({x, i});
837 }
838 }
839
840 // Sort by value
841 std::sort(combined.begin(), combined.end(),
842 [](const auto& a, const auto& b) { return a.first < b.first; });
843
844 // Assign ranks with tie handling
845 std::vector<double> ranks(total_n);
846 std::size_t i = 0;
847 while (i < total_n) {
848 std::size_t j = i;
849 while (j < total_n && combined[j].first == combined[i].first) {
850 ++j;
851 }
852 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
853 for (std::size_t m = i; m < j; ++m) {
854 ranks[m] = avg_rank;
855 }
856 i = j;
857 }
858
859 // Compute sum of ranks for each group
860 std::vector<double> rank_sums(k, 0.0);
861 for (std::size_t m = 0; m < total_n; ++m) {
862 rank_sums[combined[m].second] += ranks[m];
863 }
864
865 // Compute H statistic
866 double n_d = static_cast<double>(total_n);
867 double sum_term = 0.0;
868 for (std::size_t g = 0; g < k; ++g) {
869 double r_bar = rank_sums[g] / static_cast<double>(ns[g]);
870 sum_term += static_cast<double>(ns[g]) * r_bar * r_bar;
871 }
872
873 double h = (12.0 / (n_d * (n_d + 1.0))) * sum_term - 3.0 * (n_d + 1.0);
874
875 // Tie correction: H_corrected = H / (1 - sum(t^3 - t) / (N^3 - N))
876 {
877 std::vector<double> sorted_vals;
878 sorted_vals.reserve(total_n);
879 for (const auto& p : combined) {
880 sorted_vals.push_back(p.first);
881 }
882 auto tie_groups = compute_tie_groups(sorted_vals);
883 double tie_sum = 0.0;
884 for (auto t : tie_groups) {
885 double td = static_cast<double>(t);
886 tie_sum += td * td * td - td;
887 }
888 double denom = n_d * n_d * n_d - n_d;
889 if (tie_sum > 0.0 && denom > 0.0) {
890 double correction = 1.0 - tie_sum / denom;
891 if (correction > 0.0) {
892 h /= correction;
893 } else {
894 // All observations are tied: the corrected statistic is 0 (avoid /0).
895 h = 0.0;
896 }
897 }
898 }
899
900 // Approximate p-value using chi-square distribution
901 double df = static_cast<double>(k - 1);
902 double p_value = 1.0 - chisq_cdf(h, df);
903
904 return {h, p_value, df, alternative_hypothesis::greater};
905}
906
907// ============================================================================
908// Fisher's Exact Test (2x2 table)
909// ============================================================================
910
937inline test_result fisher_exact_test(std::uint64_t a, std::uint64_t b,
938 std::uint64_t c, std::uint64_t d,
940{
941 // 2x2 contingency table:
942 // | Col1 | Col2 | Row Total
943 // Row1 | a | b | a+b
944 // Row2 | c | d | c+d
945 // Col | a+c | b+d | n
946
947 std::uint64_t n = a + b + c + d;
948 std::uint64_t row1 = a + b;
949 (void)(c + d); // row2 not used
950 std::uint64_t col1 = a + c;
951 std::uint64_t col2 = b + d;
952
953 // Compute p-value under hypergeometric distribution
954 // P(X = a) where X ~ Hypergeometric(n, col1, row1)
955
956 // Probability of observed table
957 double log_p_obs = log_binomial_coef(col1, a) + log_binomial_coef(col2, b) - log_binomial_coef(n, row1);
958 double p_obs = std::exp(log_p_obs);
959
960 double p_value = 0.0;
961
962 // Compute range of possible values for a
963 std::uint64_t a_min = (row1 > col2) ? row1 - col2 : 0;
964 std::uint64_t a_max = std::min(row1, col1);
965
966 switch (alt) {
968 for (std::uint64_t x = a_min; x <= a; ++x) {
969 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
970 p_value += std::exp(log_p);
971 }
972 break;
974 for (std::uint64_t x = a; x <= a_max; ++x) {
975 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
976 p_value += std::exp(log_p);
977 }
978 break;
980 default:
981 // Sum probabilities of tables as extreme or more extreme
982 for (std::uint64_t x = a_min; x <= a_max; ++x) {
983 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
984 double p = std::exp(log_p);
985 if (p <= p_obs + 1e-10) {
986 p_value += p;
987 }
988 }
989 break;
990 }
991
992 p_value = std::min(1.0, p_value);
993
994 // Odds ratio as test statistic
995 double odds_ratio = (b == 0 || c == 0) ? std::numeric_limits<double>::infinity()
996 : (static_cast<double>(a) * d) / (static_cast<double>(b) * c);
997
998 return {odds_ratio, p_value, std::numeric_limits<double>::quiet_NaN(), alt};
999}
1000
1001} // namespace statcpp
Basic statistical computation functions.
Continuous distribution functions.
Discrete probability distribution functions.
alternative_hypothesis
Enumeration representing the type of alternative hypothesis.
@ greater
One-sided test (greater than)
@ less
One-sided test (less than)
double log_binomial_coef(std::uint64_t n, std::uint64_t k)
Calculate log binomial coefficient.
double norm_sf(double x)
Standard normal survival function.
std::vector< std::size_t > compute_tie_groups(const std::vector< double > &sorted_values)
Compute tie group sizes from sorted data.
test_result bartlett_test(const std::vector< std::vector< double > > &groups)
Perform Bartlett test (homogeneity of variance test)
double sample_stddev(Iterator first, Iterator last)
Sample standard deviation.
double sample_variance(Iterator first, Iterator last)
Sample variance (unbiased variance)
double chisq_cdf(double x, double df)
Chi-square distribution cumulative distribution function (CDF)
odds_ratio_result odds_ratio(const std::vector< std::vector< std::size_t > > &table)
Calculate odds ratio from a 2x2 contingency table.
double norm_cdf(double x)
Standard normal CDF.
double norm_quantile(double p)
Standard normal quantile function.
test_result levene_test(const std::vector< std::vector< double > > &groups)
Perform Levene test (homogeneity of variance test)
test_result lilliefors_test(Iterator first, Iterator last)
Perform Lilliefors test for normality.
double mean(Iterator first, Iterator last)
Arithmetic mean.
std::vector< double > compute_ranks_with_ties(Iterator first, Iterator last)
Compute ranks with tie handling.
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
test_result shapiro_wilk_test(Iterator first, Iterator last)
Perform Shapiro-Wilk test.
test_result kruskal_wallis_test(const std::vector< std::vector< double > > &groups)
Perform Kruskal-Wallis test (k-sample)
test_result mann_whitney_u_test(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, alternative_hypothesis alt=alternative_hypothesis::two_sided, bool correct=true)
Perform Mann-Whitney U test (two-sample)
double median(Iterator first, Iterator last)
Median (accepts a sorted range)
test_result fisher_exact_test(std::uint64_t a, std::uint64_t b, std::uint64_t c, std::uint64_t d, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Perform Fisher's exact test (2x2 contingency table)
test_result ks_test_normal(Iterator first, Iterator last)
Perform Kolmogorov-Smirnov test for normality (deprecated)
std::size_t count(Iterator first, Iterator last)
Data count.
double f_cdf(double x, double df1, double df2)
F-distribution cumulative distribution function (CDF)
test_result wilcoxon_signed_rank_test(Iterator first, Iterator last, double mu0=0.0, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Perform Wilcoxon signed-rank test (one-sample)
Order statistics implementation.
Parametric test functions.
Structure to store statistical test results.