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 = 1.0 - norm_cdf(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
311template <typename Iterator>
312test_result lilliefors_test(Iterator first, Iterator last)
313{
314 auto n = statcpp::count(first, last);
315 if (n < 2) {
316 throw std::invalid_argument("statcpp::lilliefors_test: need at least 2 elements");
317 }
318
319 // Standardize data
320 double mean_val = statcpp::mean(first, last);
321 double sd = statcpp::sample_stddev(first, last);
322
323 if (sd == 0.0) {
324 throw std::invalid_argument("statcpp::lilliefors_test: zero variance");
325 }
326
327 std::vector<double> standardized;
328 standardized.reserve(n);
329 for (auto it = first; it != last; ++it) {
330 standardized.push_back((static_cast<double>(*it) - mean_val) / sd);
331 }
332 std::sort(standardized.begin(), standardized.end());
333
334 // Compute D statistic
335 double d_plus = 0.0;
336 double d_minus = 0.0;
337
338 for (std::size_t i = 0; i < n; ++i) {
339 double f_x = norm_cdf(standardized[i]);
340 double f_n_upper = static_cast<double>(i + 1) / static_cast<double>(n);
341 double f_n_lower = static_cast<double>(i) / static_cast<double>(n);
342
343 d_plus = std::max(d_plus, f_n_upper - f_x);
344 d_minus = std::max(d_minus, f_x - f_n_lower);
345 }
346
347 double d = std::max(d_plus, d_minus);
348
349 // Lilliefors correction for estimated parameters
350 // Use asymptotic approximation
351 // Critical values use an empirical approximation optimized for n<=50.
352 // For larger samples, standard tables should be consulted.
353 double sqrt_n = std::sqrt(static_cast<double>(n));
354 double d_adj = (d - 0.01 + 0.85 / sqrt_n) * (sqrt_n + 0.05 + 0.82 / sqrt_n);
355
356 // Asymptotic p-value approximation
357 double p_value = 2.0 * std::exp(-2.0 * d_adj * d_adj);
358 p_value = std::max(0.0, std::min(1.0, p_value));
359
360 return {d, p_value, static_cast<double>(n), alternative_hypothesis::greater};
361}
362
370template <typename Iterator>
371[[deprecated("Use lilliefors_test() instead. ks_test_normal() will be removed in a future version.")]]
372test_result ks_test_normal(Iterator first, Iterator last)
373{
374 return lilliefors_test(first, last);
375}
376
377// ============================================================================
378// Levene's Test for Homogeneity of Variance
379// ============================================================================
380
397inline test_result levene_test(const std::vector<std::vector<double>>& groups)
398{
399 std::size_t k = groups.size();
400 if (k < 2) {
401 throw std::invalid_argument("statcpp::levene_test: need at least 2 groups");
402 }
403
404 // Compute group medians and deviations from median
405 std::vector<std::vector<double>> z_values(k);
406 std::size_t total_n = 0;
407
408 for (std::size_t i = 0; i < k; ++i) {
409 if (groups[i].size() < 2) {
410 throw std::invalid_argument("statcpp::levene_test: each group needs at least 2 elements");
411 }
412
413 std::vector<double> sorted = groups[i];
414 std::sort(sorted.begin(), sorted.end());
415 double med = statcpp::median(sorted.begin(), sorted.end());
416
417 for (double x : groups[i]) {
418 z_values[i].push_back(std::abs(x - med));
419 }
420 total_n += groups[i].size();
421 }
422
423 // Compute group means of z values
424 std::vector<double> z_means(k);
425 double z_grand_mean = 0.0;
426
427 for (std::size_t i = 0; i < k; ++i) {
428 z_means[i] = statcpp::mean(z_values[i].begin(), z_values[i].end());
429 z_grand_mean += z_means[i] * static_cast<double>(z_values[i].size());
430 }
431 z_grand_mean /= static_cast<double>(total_n);
432
433 // Compute test statistic
434 double ss_between = 0.0;
435 double ss_within = 0.0;
436
437 for (std::size_t i = 0; i < k; ++i) {
438 double ni = static_cast<double>(z_values[i].size());
439 ss_between += ni * (z_means[i] - z_grand_mean) * (z_means[i] - z_grand_mean);
440
441 for (double z : z_values[i]) {
442 ss_within += (z - z_means[i]) * (z - z_means[i]);
443 }
444 }
445
446 double df1 = static_cast<double>(k - 1);
447 double df2 = static_cast<double>(total_n - k);
448
449 // Guard: if all deviations are zero (all values identical within each group),
450 // variances are trivially equal → F = 0, p = 1
451 if (ss_within == 0.0) {
452 return {0.0, 1.0, df1, alternative_hypothesis::greater};
453 }
454
455 double f = (ss_between / df1) / (ss_within / df2);
456 double p_value = 1.0 - f_cdf(f, df1, df2);
457
458 return {f, p_value, df1, alternative_hypothesis::greater};
459}
460
461// ============================================================================
462// Bartlett's Test for Homogeneity of Variance
463// ============================================================================
464
482inline test_result bartlett_test(const std::vector<std::vector<double>>& groups)
483{
484 std::size_t k = groups.size();
485 if (k < 2) {
486 throw std::invalid_argument("statcpp::bartlett_test: need at least 2 groups");
487 }
488
489 std::vector<double> vars(k);
490 std::vector<std::size_t> ns(k);
491 std::size_t total_n = 0;
492 double pooled_var_num = 0.0;
493
494 for (std::size_t i = 0; i < k; ++i) {
495 if (groups[i].size() < 2) {
496 throw std::invalid_argument("statcpp::bartlett_test: each group needs at least 2 elements");
497 }
498 ns[i] = groups[i].size();
499 total_n += ns[i];
500 vars[i] = statcpp::sample_variance(groups[i].begin(), groups[i].end());
501
502 if (vars[i] <= 0.0) {
503 throw std::invalid_argument("statcpp::bartlett_test: zero or negative variance in group");
504 }
505
506 pooled_var_num += (ns[i] - 1) * vars[i];
507 }
508
509 double pooled_var = pooled_var_num / static_cast<double>(total_n - k);
510
511 // Compute Bartlett's statistic
512 double sum_log = 0.0;
513 double sum_inv = 0.0;
514
515 for (std::size_t i = 0; i < k; ++i) {
516 double df_i = static_cast<double>(ns[i] - 1);
517 sum_log += df_i * std::log(vars[i]);
518 sum_inv += 1.0 / df_i;
519 }
520
521 double df_total = static_cast<double>(total_n - k);
522 double chi2 = df_total * std::log(pooled_var) - sum_log;
523
524 // Correction factor
525 double c = 1.0 + (sum_inv - 1.0 / df_total) / (3.0 * (k - 1));
526 chi2 /= c;
527
528 double df = static_cast<double>(k - 1);
529 double p_value = 1.0 - chisq_cdf(chi2, df);
530
531 return {chi2, p_value, df, alternative_hypothesis::greater};
532}
533
534// ============================================================================
535// Wilcoxon Signed-Rank Test
536// ============================================================================
537
558template <typename Iterator>
559test_result wilcoxon_signed_rank_test(Iterator first, Iterator last, double mu0 = 0.0,
561{
562 auto n = statcpp::count(first, last);
563 if (n < 2) {
564 throw std::invalid_argument("statcpp::wilcoxon_signed_rank_test: need at least 2 elements");
565 }
566
567 // Compute differences from mu0, excluding zeros
568 std::vector<double> diffs;
569 for (auto it = first; it != last; ++it) {
570 double d = static_cast<double>(*it) - mu0;
571 if (d != 0.0) {
572 diffs.push_back(d);
573 }
574 }
575
576 std::size_t n_nonzero = diffs.size();
577 if (n_nonzero < 2) {
578 throw std::invalid_argument("statcpp::wilcoxon_signed_rank_test: need at least 2 non-zero differences");
579 }
580
581 // Compute ranks of absolute differences
582 std::vector<double> abs_diffs(n_nonzero);
583 for (std::size_t i = 0; i < n_nonzero; ++i) {
584 abs_diffs[i] = std::abs(diffs[i]);
585 }
586
587 auto ranks = compute_ranks_with_ties(abs_diffs.begin(), abs_diffs.end());
588
589 // Compute W+ (sum of ranks of positive differences)
590 double w = 0.0;
591
592 for (std::size_t i = 0; i < n_nonzero; ++i) {
593 if (diffs[i] > 0.0) {
594 w += ranks[i];
595 }
596 }
597
598 // Normal approximation for p-value (with continuity correction)
599 double nn = static_cast<double>(n_nonzero);
600 double mean_w = nn * (nn + 1.0) / 4.0;
601 double var_w = nn * (nn + 1.0) * (2.0 * nn + 1.0) / 24.0;
602
603 // Tie correction for variance
604 // Sort absolute differences to compute tie groups
605 std::vector<double> sorted_abs(abs_diffs.begin(), abs_diffs.end());
606 std::sort(sorted_abs.begin(), sorted_abs.end());
607 auto tie_groups = compute_tie_groups(sorted_abs);
608 // Subtract tie correction: sum(t^3 - t) / 48
609 double tie_correction = 0.0;
610 for (auto t : tie_groups) {
611 double td = static_cast<double>(t);
612 tie_correction += td * td * td - td;
613 }
614 var_w -= tie_correction / 48.0;
615
616 double se = std::sqrt(std::max(0.0, var_w));
617
618 if (se == 0.0) {
619 return {w, 1.0, static_cast<double>(n), alt};
620 }
621
622 double z;
623 double p_value;
624
625 switch (alt) {
627 z = (w - mean_w + 0.5) / se;
628 p_value = norm_cdf(z);
629 break;
631 z = (w - mean_w - 0.5) / se;
632 p_value = 1.0 - norm_cdf(z);
633 break;
635 default:
636 z = (w - mean_w) / se;
637 if (z < 0) z = (w - mean_w + 0.5) / se;
638 else z = (w - mean_w - 0.5) / se;
639 p_value = 2.0 * std::min(norm_cdf(z), 1.0 - norm_cdf(z));
640 break;
641 }
642
643 return {w, p_value, static_cast<double>(n_nonzero), alt};
644}
645
646// ============================================================================
647// Mann-Whitney U Test
648// ============================================================================
649
671template <typename Iterator1, typename Iterator2>
672test_result mann_whitney_u_test(Iterator1 first1, Iterator1 last1,
673 Iterator2 first2, Iterator2 last2,
675 bool correct = true)
676{
677 auto n1 = statcpp::count(first1, last1);
678 auto n2 = statcpp::count(first2, last2);
679
680 if (n1 < 2 || n2 < 2) {
681 throw std::invalid_argument("statcpp::mann_whitney_u_test: need at least 2 elements in each sample");
682 }
683
684 // Combine and rank all observations
685 std::vector<std::pair<double, int>> combined;
686 combined.reserve(n1 + n2);
687
688 for (auto it = first1; it != last1; ++it) {
689 combined.push_back({static_cast<double>(*it), 1}); // group 1
690 }
691 for (auto it = first2; it != last2; ++it) {
692 combined.push_back({static_cast<double>(*it), 2}); // group 2
693 }
694
695 // Sort by value
696 std::sort(combined.begin(), combined.end(),
697 [](const auto& a, const auto& b) { return a.first < b.first; });
698
699 // Assign ranks with tie handling
700 std::size_t total_n = n1 + n2;
701 std::vector<double> ranks(total_n);
702 std::size_t i = 0;
703 while (i < total_n) {
704 std::size_t j = i;
705 while (j < total_n && combined[j].first == combined[i].first) {
706 ++j;
707 }
708 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
709 for (std::size_t k = i; k < j; ++k) {
710 ranks[k] = avg_rank;
711 }
712 i = j;
713 }
714
715 // Compute R1 (sum of ranks in group 1)
716 double r1 = 0.0;
717 for (std::size_t k = 0; k < total_n; ++k) {
718 if (combined[k].second == 1) {
719 r1 += ranks[k];
720 }
721 }
722
723 // Compute U1
724 double u1 = r1 - n1 * (n1 + 1.0) / 2.0;
725 (void)(static_cast<double>(n1 * n2) - u1); // u2 not used but computed for reference
726
727 // Normal approximation with tie correction
728 double N = static_cast<double>(total_n);
729 double mean_u = static_cast<double>(n1 * n2) / 2.0;
730 double var_u = static_cast<double>(n1 * n2) / 12.0 * (N + 1.0);
731
732 // Tie correction: compute tie groups from sorted combined values
733 {
734 std::vector<double> sorted_vals;
735 sorted_vals.reserve(total_n);
736 for (const auto& p : combined) {
737 sorted_vals.push_back(p.first);
738 }
739 // combined is already sorted by value
740 auto tie_groups = compute_tie_groups(sorted_vals);
741 double tie_sum = 0.0;
742 for (auto t : tie_groups) {
743 double td = static_cast<double>(t);
744 tie_sum += td * td * td - td;
745 }
746 // Corrected variance: n1*n2/(12*N*(N-1)) * (N^3 - N - tie_sum)
747 // which equals n1*n2/12 * (N+1) - n1*n2/(12*N*(N-1)) * tie_sum
748 if (N > 1.0 && tie_sum > 0.0) {
749 var_u = static_cast<double>(n1 * n2) / (12.0 * N * (N - 1.0))
750 * (N * N * N - N - tie_sum);
751 }
752 }
753
754 double se = std::sqrt(std::max(0.0, var_u));
755
756 if (se == 0.0) {
757 return {u1, 1.0, static_cast<double>(n1 + n2), alt};
758 }
759
760 // Continuity correction (Yates)
761 double diff = u1 - mean_u;
762 if (correct) {
763 if (diff > 0) diff -= 0.5;
764 else if (diff < 0) diff += 0.5;
765 }
766 double z = diff / se;
767
768 double p_value;
769 switch (alt) {
771 p_value = norm_cdf(z);
772 break;
774 p_value = 1.0 - norm_cdf(z);
775 break;
777 default:
778 p_value = 2.0 * std::min(norm_cdf(z), 1.0 - norm_cdf(z));
779 break;
780 }
781
782 return {u1, p_value, static_cast<double>(n1 + n2), alt};
783}
784
785// ============================================================================
786// Kruskal-Wallis Test
787// ============================================================================
788
805inline test_result kruskal_wallis_test(const std::vector<std::vector<double>>& groups)
806{
807 std::size_t k = groups.size();
808 if (k < 2) {
809 throw std::invalid_argument("statcpp::kruskal_wallis_test: need at least 2 groups");
810 }
811
812 // Combine all observations with group labels
813 std::vector<std::pair<double, std::size_t>> combined;
814 std::vector<std::size_t> ns(k);
815 std::size_t total_n = 0;
816
817 for (std::size_t i = 0; i < k; ++i) {
818 if (groups[i].empty()) {
819 throw std::invalid_argument("statcpp::kruskal_wallis_test: empty group");
820 }
821 ns[i] = groups[i].size();
822 total_n += ns[i];
823 for (double x : groups[i]) {
824 combined.push_back({x, i});
825 }
826 }
827
828 // Sort by value
829 std::sort(combined.begin(), combined.end(),
830 [](const auto& a, const auto& b) { return a.first < b.first; });
831
832 // Assign ranks with tie handling
833 std::vector<double> ranks(total_n);
834 std::size_t i = 0;
835 while (i < total_n) {
836 std::size_t j = i;
837 while (j < total_n && combined[j].first == combined[i].first) {
838 ++j;
839 }
840 double avg_rank = (static_cast<double>(i + 1) + static_cast<double>(j)) / 2.0;
841 for (std::size_t m = i; m < j; ++m) {
842 ranks[m] = avg_rank;
843 }
844 i = j;
845 }
846
847 // Compute sum of ranks for each group
848 std::vector<double> rank_sums(k, 0.0);
849 for (std::size_t m = 0; m < total_n; ++m) {
850 rank_sums[combined[m].second] += ranks[m];
851 }
852
853 // Compute H statistic
854 double n_d = static_cast<double>(total_n);
855 double sum_term = 0.0;
856 for (std::size_t g = 0; g < k; ++g) {
857 double r_bar = rank_sums[g] / static_cast<double>(ns[g]);
858 sum_term += static_cast<double>(ns[g]) * r_bar * r_bar;
859 }
860
861 double h = (12.0 / (n_d * (n_d + 1.0))) * sum_term - 3.0 * (n_d + 1.0);
862
863 // Tie correction: H_corrected = H / (1 - sum(t^3 - t) / (N^3 - N))
864 {
865 std::vector<double> sorted_vals;
866 sorted_vals.reserve(total_n);
867 for (const auto& p : combined) {
868 sorted_vals.push_back(p.first);
869 }
870 auto tie_groups = compute_tie_groups(sorted_vals);
871 double tie_sum = 0.0;
872 for (auto t : tie_groups) {
873 double td = static_cast<double>(t);
874 tie_sum += td * td * td - td;
875 }
876 double denom = n_d * n_d * n_d - n_d;
877 if (tie_sum > 0.0 && denom > 0.0) {
878 double correction = 1.0 - tie_sum / denom;
879 if (correction > 0.0) {
880 h /= correction;
881 } else {
882 // All observations are tied: the corrected statistic is 0 (avoid /0).
883 h = 0.0;
884 }
885 }
886 }
887
888 // Approximate p-value using chi-square distribution
889 double df = static_cast<double>(k - 1);
890 double p_value = 1.0 - chisq_cdf(h, df);
891
892 return {h, p_value, df, alternative_hypothesis::greater};
893}
894
895// ============================================================================
896// Fisher's Exact Test (2x2 table)
897// ============================================================================
898
925inline test_result fisher_exact_test(std::uint64_t a, std::uint64_t b,
926 std::uint64_t c, std::uint64_t d,
928{
929 // 2x2 contingency table:
930 // | Col1 | Col2 | Row Total
931 // Row1 | a | b | a+b
932 // Row2 | c | d | c+d
933 // Col | a+c | b+d | n
934
935 std::uint64_t n = a + b + c + d;
936 std::uint64_t row1 = a + b;
937 (void)(c + d); // row2 not used
938 std::uint64_t col1 = a + c;
939 std::uint64_t col2 = b + d;
940
941 // Compute p-value under hypergeometric distribution
942 // P(X = a) where X ~ Hypergeometric(n, col1, row1)
943
944 // Probability of observed table
945 double log_p_obs = log_binomial_coef(col1, a) + log_binomial_coef(col2, b) - log_binomial_coef(n, row1);
946 double p_obs = std::exp(log_p_obs);
947
948 double p_value = 0.0;
949
950 // Compute range of possible values for a
951 std::uint64_t a_min = (row1 > col2) ? row1 - col2 : 0;
952 std::uint64_t a_max = std::min(row1, col1);
953
954 switch (alt) {
956 for (std::uint64_t x = a_min; x <= a; ++x) {
957 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
958 p_value += std::exp(log_p);
959 }
960 break;
962 for (std::uint64_t x = a; x <= a_max; ++x) {
963 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
964 p_value += std::exp(log_p);
965 }
966 break;
968 default:
969 // Sum probabilities of tables as extreme or more extreme
970 for (std::uint64_t x = a_min; x <= a_max; ++x) {
971 double log_p = log_binomial_coef(col1, x) + log_binomial_coef(col2, row1 - x) - log_binomial_coef(n, row1);
972 double p = std::exp(log_p);
973 if (p <= p_obs + 1e-10) {
974 p_value += p;
975 }
976 }
977 break;
978 }
979
980 p_value = std::min(1.0, p_value);
981
982 // Odds ratio as test statistic
983 double odds_ratio = (b == 0 || c == 0) ? std::numeric_limits<double>::infinity()
984 : (static_cast<double>(a) * d) / (static_cast<double>(b) * c);
985
986 return {odds_ratio, p_value, std::numeric_limits<double>::quiet_NaN(), alt};
987}
988
989} // 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.
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.