statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
parametric_tests.hpp
Go to the documentation of this file.
1
9#pragma once
10
14
15#include <algorithm>
16#include <cmath>
17#include <cstddef>
18#include <limits>
19#include <stdexcept>
20#include <utility>
21#include <vector>
22
23namespace statcpp {
24
25// ============================================================================
26// Test Result Structure
27// ============================================================================
28
35 two_sided,
36 less,
37 greater
38};
39
46 double statistic;
47 double p_value;
48 double df;
50 double df2 = std::numeric_limits<double>::quiet_NaN();
51};
52
53// ============================================================================
54// Z-Test for Mean (known variance)
55// ============================================================================
56
71template <typename Iterator>
72test_result z_test(Iterator first, Iterator last, double mu0, double sigma,
74{
75 if (sigma <= 0.0) {
76 throw std::invalid_argument("statcpp::z_test: sigma must be positive");
77 }
78
79 auto n = statcpp::count(first, last);
80 if (n == 0) {
81 throw std::invalid_argument("statcpp::z_test: empty range");
82 }
83
84 double mean_val = statcpp::mean(first, last);
85 double se = sigma / std::sqrt(static_cast<double>(n));
86 double z = (mean_val - mu0) / se;
87
88 double p_value;
89 switch (alt) {
91 p_value = norm_cdf(z);
92 break;
94 p_value = 1.0 - norm_cdf(z);
95 break;
97 default:
98 p_value = 2.0 * (1.0 - norm_cdf(std::abs(z)));
99 break;
100 }
101
102 return {z, p_value, std::numeric_limits<double>::infinity(), alt};
103}
104
105// ============================================================================
106// Z-Test for Proportion
107// ============================================================================
108
121inline test_result z_test_proportion(std::size_t successes, std::size_t trials, double p0,
123{
124 if (p0 <= 0.0 || p0 >= 1.0) {
125 throw std::invalid_argument("statcpp::z_test_proportion: p0 must be in (0, 1)");
126 }
127 if (trials == 0) {
128 throw std::invalid_argument("statcpp::z_test_proportion: trials must be positive");
129 }
130 if (successes > trials) {
131 throw std::invalid_argument("statcpp::z_test_proportion: successes cannot exceed trials");
132 }
133
134 double n = static_cast<double>(trials);
135 double p_hat = static_cast<double>(successes) / n;
136 double se = std::sqrt(p0 * (1.0 - p0) / n);
137 double z = (p_hat - p0) / se;
138
139 double p_value;
140 switch (alt) {
142 p_value = norm_cdf(z);
143 break;
145 p_value = 1.0 - norm_cdf(z);
146 break;
148 default:
149 p_value = 2.0 * (1.0 - norm_cdf(std::abs(z)));
150 break;
151 }
152
153 return {z, p_value, std::numeric_limits<double>::infinity(), alt};
154}
155
169inline test_result z_test_proportion_two_sample(std::size_t successes1, std::size_t trials1,
170 std::size_t successes2, std::size_t trials2,
172{
173 if (trials1 == 0 || trials2 == 0) {
174 throw std::invalid_argument("statcpp::z_test_proportion_two_sample: trials must be positive");
175 }
176 if (successes1 > trials1 || successes2 > trials2) {
177 throw std::invalid_argument("statcpp::z_test_proportion_two_sample: successes cannot exceed trials");
178 }
179
180 double n1 = static_cast<double>(trials1);
181 double n2 = static_cast<double>(trials2);
182 double p1 = static_cast<double>(successes1) / n1;
183 double p2 = static_cast<double>(successes2) / n2;
184
185 // Pooled proportion
186 double p_pooled = static_cast<double>(successes1 + successes2) / (n1 + n2);
187 double se = std::sqrt(p_pooled * (1.0 - p_pooled) * (1.0 / n1 + 1.0 / n2));
188
189 if (se == 0.0) {
190 // p_pooled is 0 or 1, all successes or all failures
191 return {0.0, 1.0, static_cast<double>(trials1 + trials2), alt};
192 }
193
194 double z = (p1 - p2) / se;
195
196 double p_value;
197 switch (alt) {
199 p_value = norm_cdf(z);
200 break;
202 p_value = 1.0 - norm_cdf(z);
203 break;
205 default:
206 p_value = 2.0 * (1.0 - norm_cdf(std::abs(z)));
207 break;
208 }
209
210 return {z, p_value, std::numeric_limits<double>::infinity(), alt};
211}
212
213// ============================================================================
214// T-Test for Mean
215// ============================================================================
216
230template <typename Iterator>
231test_result t_test(Iterator first, Iterator last, double mu0,
233{
234 auto n = statcpp::count(first, last);
235 if (n < 2) {
236 throw std::invalid_argument("statcpp::t_test: need at least 2 elements");
237 }
238
239 double mean_val = statcpp::mean(first, last);
240 double s = statcpp::sample_stddev(first, last);
241
242 if (s == 0.0) {
243 throw std::invalid_argument("statcpp::t_test: zero variance");
244 }
245
246 double se = s / std::sqrt(static_cast<double>(n));
247 double t = (mean_val - mu0) / se;
248 double df = static_cast<double>(n - 1);
249
250 double p_value;
251 switch (alt) {
253 p_value = t_cdf(t, df);
254 break;
256 p_value = 1.0 - t_cdf(t, df);
257 break;
259 default:
260 p_value = 2.0 * (1.0 - t_cdf(std::abs(t), df));
261 break;
262 }
263
264 return {t, p_value, df, alt};
265}
266
283template <typename Iterator1, typename Iterator2>
284test_result t_test_two_sample(Iterator1 first1, Iterator1 last1,
285 Iterator2 first2, Iterator2 last2,
287{
288 auto n1 = statcpp::count(first1, last1);
289 auto n2 = statcpp::count(first2, last2);
290
291 if (n1 < 2 || n2 < 2) {
292 throw std::invalid_argument("statcpp::t_test_two_sample: need at least 2 elements in each sample");
293 }
294
295 double mean1 = statcpp::mean(first1, last1);
296 double mean2 = statcpp::mean(first2, last2);
297 double var1 = statcpp::sample_variance(first1, last1);
298 double var2 = statcpp::sample_variance(first2, last2);
299
300 // Pooled variance
301 double df = static_cast<double>(n1 + n2 - 2);
302 double sp2 = ((n1 - 1) * var1 + (n2 - 1) * var2) / df;
303 double se = std::sqrt(sp2 * (1.0 / n1 + 1.0 / n2));
304
305 if (se == 0.0) {
306 throw std::invalid_argument("statcpp::t_test_two_sample: zero variance");
307 }
308
309 double t = (mean1 - mean2) / se;
310
311 double p_value;
312 switch (alt) {
314 p_value = t_cdf(t, df);
315 break;
317 p_value = 1.0 - t_cdf(t, df);
318 break;
320 default:
321 p_value = 2.0 * (1.0 - t_cdf(std::abs(t), df));
322 break;
323 }
324
325 return {t, p_value, df, alt};
326}
327
344template <typename Iterator1, typename Iterator2>
345test_result t_test_welch(Iterator1 first1, Iterator1 last1,
346 Iterator2 first2, Iterator2 last2,
348{
349 auto n1 = statcpp::count(first1, last1);
350 auto n2 = statcpp::count(first2, last2);
351
352 if (n1 < 2 || n2 < 2) {
353 throw std::invalid_argument("statcpp::t_test_welch: need at least 2 elements in each sample");
354 }
355
356 double mean1 = statcpp::mean(first1, last1);
357 double mean2 = statcpp::mean(first2, last2);
358 double var1 = statcpp::sample_variance(first1, last1);
359 double var2 = statcpp::sample_variance(first2, last2);
360
361 double se1 = var1 / n1;
362 double se2 = var2 / n2;
363 double se = std::sqrt(se1 + se2);
364
365 if (se == 0.0) {
366 throw std::invalid_argument("statcpp::t_test_welch: zero variance");
367 }
368
369 // Welch-Satterthwaite approximation
370 double num = (se1 + se2) * (se1 + se2);
371 double denom = (se1 * se1) / (n1 - 1) + (se2 * se2) / (n2 - 1);
372
373 // Protection when denominator is zero (when both variances are zero)
374 if (denom == 0.0) {
375 throw std::invalid_argument("statcpp::t_test_welch: cannot compute degrees of freedom with zero variances");
376 }
377
378 double df = num / denom;
379
380 double t = (mean1 - mean2) / se;
381
382 double p_value;
383 switch (alt) {
385 p_value = t_cdf(t, df);
386 break;
388 p_value = 1.0 - t_cdf(t, df);
389 break;
391 default:
392 p_value = 2.0 * (1.0 - t_cdf(std::abs(t), df));
393 break;
394 }
395
396 return {t, p_value, df, alt};
397}
398
414template <typename Iterator1, typename Iterator2>
415test_result t_test_paired(Iterator1 first1, Iterator1 last1,
416 Iterator2 first2, Iterator2 last2,
418{
419 auto n1 = statcpp::count(first1, last1);
420 auto n2 = statcpp::count(first2, last2);
421
422 if (n1 != n2) {
423 throw std::invalid_argument("statcpp::t_test_paired: samples must have equal length");
424 }
425 if (n1 < 2) {
426 throw std::invalid_argument("statcpp::t_test_paired: need at least 2 pairs");
427 }
428
429 // Compute differences
430 std::vector<double> diffs;
431 diffs.reserve(n1);
432
433 auto it1 = first1;
434 auto it2 = first2;
435 while (it1 != last1) {
436 diffs.push_back(static_cast<double>(*it1) - static_cast<double>(*it2));
437 ++it1;
438 ++it2;
439 }
440
441 // Apply one-sample t-test on differences
442 return t_test(diffs.begin(), diffs.end(), 0.0, alt);
443}
444
445// ============================================================================
446// Chi-Square Test for Goodness of Fit
447// ============================================================================
448
463template <typename Iterator1, typename Iterator2>
464test_result chisq_test_gof(Iterator1 observed_first, Iterator1 observed_last,
465 Iterator2 expected_first, Iterator2 expected_last)
466{
467 auto n_obs = statcpp::count(observed_first, observed_last);
468 auto n_exp = statcpp::count(expected_first, expected_last);
469
470 if (n_obs != n_exp) {
471 throw std::invalid_argument("statcpp::chisq_test_gof: observed and expected must have same length");
472 }
473 if (n_obs < 2) {
474 throw std::invalid_argument("statcpp::chisq_test_gof: need at least 2 categories");
475 }
476
477 double chi2 = 0.0;
478 auto it_obs = observed_first;
479 auto it_exp = expected_first;
480
481 while (it_obs != observed_last) {
482 double o = static_cast<double>(*it_obs);
483 double e = static_cast<double>(*it_exp);
484
485 if (e <= 0.0) {
486 throw std::invalid_argument("statcpp::chisq_test_gof: expected values must be positive");
487 }
488
489 chi2 += (o - e) * (o - e) / e;
490
491 ++it_obs;
492 ++it_exp;
493 }
494
495 double df = static_cast<double>(n_obs - 1);
496 double p_value = 1.0 - chisq_cdf(chi2, df);
497
498 return {chi2, p_value, df, alternative_hypothesis::greater};
499}
500
512template <typename Iterator>
513test_result chisq_test_gof_uniform(Iterator observed_first, Iterator observed_last)
514{
515 auto n = statcpp::count(observed_first, observed_last);
516 if (n < 2) {
517 throw std::invalid_argument("statcpp::chisq_test_gof_uniform: need at least 2 categories");
518 }
519
520 double total = 0.0;
521 for (auto it = observed_first; it != observed_last; ++it) {
522 total += static_cast<double>(*it);
523 }
524
525 if (total == 0.0) {
526 throw std::invalid_argument("statcpp::chisq_test_gof_uniform: total of observed frequencies is zero");
527 }
528
529 double expected = total / static_cast<double>(n);
530
531 double chi2 = 0.0;
532 for (auto it = observed_first; it != observed_last; ++it) {
533 double o = static_cast<double>(*it);
534 chi2 += (o - expected) * (o - expected) / expected;
535 }
536
537 double df = static_cast<double>(n - 1);
538 double p_value = 1.0 - chisq_cdf(chi2, df);
539
540 return {chi2, p_value, df, alternative_hypothesis::greater};
541}
542
543// ============================================================================
544// Chi-Square Test for Independence
545// ============================================================================
546
556inline test_result chisq_test_independence(const std::vector<std::vector<double>>& contingency_table)
557{
558 std::size_t rows = contingency_table.size();
559 if (rows < 2) {
560 throw std::invalid_argument("statcpp::chisq_test_independence: need at least 2 rows");
561 }
562
563 std::size_t cols = contingency_table[0].size();
564 if (cols < 2) {
565 throw std::invalid_argument("statcpp::chisq_test_independence: need at least 2 columns");
566 }
567
568 // Check all rows have same number of columns
569 for (const auto& row : contingency_table) {
570 if (row.size() != cols) {
571 throw std::invalid_argument("statcpp::chisq_test_independence: inconsistent column count");
572 }
573 }
574
575 // Compute row and column totals
576 std::vector<double> row_totals(rows, 0.0);
577 std::vector<double> col_totals(cols, 0.0);
578 double grand_total = 0.0;
579
580 for (std::size_t i = 0; i < rows; ++i) {
581 for (std::size_t j = 0; j < cols; ++j) {
582 double val = contingency_table[i][j];
583 if (val < 0.0) {
584 throw std::invalid_argument("statcpp::chisq_test_independence: negative cell value");
585 }
586 row_totals[i] += val;
587 col_totals[j] += val;
588 grand_total += val;
589 }
590 }
591
592 if (grand_total == 0.0) {
593 throw std::invalid_argument("statcpp::chisq_test_independence: empty table");
594 }
595
596 // Compute chi-square statistic
597 double chi2 = 0.0;
598 for (std::size_t i = 0; i < rows; ++i) {
599 for (std::size_t j = 0; j < cols; ++j) {
600 double expected = row_totals[i] * col_totals[j] / grand_total;
601 if (expected > 0.0) {
602 double observed = contingency_table[i][j];
603 chi2 += (observed - expected) * (observed - expected) / expected;
604 }
605 }
606 }
607
608 double df = static_cast<double>((rows - 1) * (cols - 1));
609 double p_value = 1.0 - chisq_cdf(chi2, df);
610
611 return {chi2, p_value, df, alternative_hypothesis::greater};
612}
613
614// ============================================================================
615// F-Test for Variance Ratio
616// ============================================================================
617
633template <typename Iterator1, typename Iterator2>
634test_result f_test(Iterator1 first1, Iterator1 last1,
635 Iterator2 first2, Iterator2 last2,
637{
638 auto n1 = statcpp::count(first1, last1);
639 auto n2 = statcpp::count(first2, last2);
640
641 if (n1 < 2 || n2 < 2) {
642 throw std::invalid_argument("statcpp::f_test: need at least 2 elements in each sample");
643 }
644
645 double var1 = statcpp::sample_variance(first1, last1);
646 double var2 = statcpp::sample_variance(first2, last2);
647
648 if (var2 == 0.0) {
649 throw std::invalid_argument("statcpp::f_test: second sample has zero variance");
650 }
651
652 double f = var1 / var2;
653 double df1 = static_cast<double>(n1 - 1);
654 double df2 = static_cast<double>(n2 - 1);
655
656 double p_value;
657 switch (alt) {
659 p_value = f_cdf(f, df1, df2);
660 break;
662 p_value = 1.0 - f_cdf(f, df1, df2);
663 break;
665 default:
666 {
667 double p1 = f_cdf(f, df1, df2);
668 double p2 = 1.0 - p1;
669 p_value = 2.0 * std::min(p1, p2);
670 }
671 break;
672 }
673
674 return {f, p_value, df1, alt, df2};
675}
676
677// ============================================================================
678// Multiple Testing Correction
679// ============================================================================
680
690inline std::vector<double> bonferroni_correction(const std::vector<double>& p_values)
691{
692 std::size_t n = p_values.size();
693 std::vector<double> adjusted(n);
694
695 for (std::size_t i = 0; i < n; ++i) {
696 adjusted[i] = std::min(1.0, p_values[i] * static_cast<double>(n));
697 }
698
699 return adjusted;
700}
701
711inline std::vector<double> benjamini_hochberg_correction(const std::vector<double>& p_values)
712{
713 std::size_t n = p_values.size();
714 if (n == 0) return {};
715
716 // Create index-pvalue pairs and sort by p-value
717 std::vector<std::pair<std::size_t, double>> indexed(n);
718 for (std::size_t i = 0; i < n; ++i) {
719 indexed[i] = {i, p_values[i]};
720 }
721
722 std::sort(indexed.begin(), indexed.end(),
723 [](const auto& a, const auto& b) { return a.second < b.second; });
724
725 std::vector<double> adjusted(n);
726
727 // Compute adjusted p-values
728 double prev_adj = 1.0;
729 for (std::size_t i = n; i > 0; --i) {
730 std::size_t idx = indexed[i - 1].first;
731 double p = indexed[i - 1].second;
732 double adj = p * static_cast<double>(n) / static_cast<double>(i);
733 adj = std::min(adj, prev_adj);
734 adj = std::min(adj, 1.0);
735 adjusted[idx] = adj;
736 prev_adj = adj;
737 }
738
739 return adjusted;
740}
741
751inline std::vector<double> holm_correction(const std::vector<double>& p_values)
752{
753 std::size_t n = p_values.size();
754 if (n == 0) return {};
755
756 // Create index-pvalue pairs and sort by p-value
757 std::vector<std::pair<std::size_t, double>> indexed(n);
758 for (std::size_t i = 0; i < n; ++i) {
759 indexed[i] = {i, p_values[i]};
760 }
761
762 std::sort(indexed.begin(), indexed.end(),
763 [](const auto& a, const auto& b) { return a.second < b.second; });
764
765 std::vector<double> adjusted(n);
766
767 double max_adj = 0.0;
768 for (std::size_t i = 0; i < n; ++i) {
769 std::size_t idx = indexed[i].first;
770 double p = indexed[i].second;
771 double adj = p * static_cast<double>(n - i);
772 adj = std::max(adj, max_adj);
773 adj = std::min(adj, 1.0);
774 adjusted[idx] = adj;
775 max_adj = adj;
776 }
777
778 return adjusted;
779}
780
781} // namespace statcpp
Basic statistical computation functions.
Continuous distribution functions.
Dispersion and variance calculation functions.
alternative_hypothesis
Enumeration representing the type of alternative hypothesis.
@ greater
One-sided test (greater than)
@ less
One-sided test (less than)
contingency_table_result contingency_table(const std::vector< std::size_t > &row_data, const std::vector< std::size_t > &col_data)
Create a contingency table.
double sample_stddev(Iterator first, Iterator last)
Sample standard deviation.
double sample_variance(Iterator first, Iterator last)
Sample variance (unbiased variance)
test_result t_test(Iterator first, Iterator last, double mu0, alternative_hypothesis alt=alternative_hypothesis::two_sided)
One-sample t-test.
double chisq_cdf(double x, double df)
Chi-square distribution cumulative distribution function (CDF)
double t_cdf(double x, double df)
t-distribution cumulative distribution function (CDF)
test_result chisq_test_independence(const std::vector< std::vector< double > > &contingency_table)
Chi-square test for independence.
double norm_cdf(double x)
Standard normal CDF.
std::vector< double > holm_correction(const std::vector< double > &p_values)
Holm correction (step-down Bonferroni method)
double mean(Iterator first, Iterator last)
Arithmetic mean.
test_result z_test(Iterator first, Iterator last, double mu0, double sigma, alternative_hypothesis alt=alternative_hypothesis::two_sided)
One-sample z-test (known variance)
std::vector< double > benjamini_hochberg_correction(const std::vector< double > &p_values)
Benjamini-Hochberg correction (FDR control)
test_result t_test_welch(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Two-sample t-test (Welch's method)
test_result chisq_test_gof_uniform(Iterator observed_first, Iterator observed_last)
Chi-square goodness of fit test (uniform expected frequencies)
test_result t_test_paired(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Paired t-test.
test_result f_test(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, alternative_hypothesis alt=alternative_hypothesis::two_sided)
F-test (variance comparison)
test_result chisq_test_gof(Iterator1 observed_first, Iterator1 observed_last, Iterator2 expected_first, Iterator2 expected_last)
Chi-square goodness of fit test.
std::size_t count(Iterator first, Iterator last)
Data count.
test_result t_test_two_sample(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Two-sample t-test (independent samples, pooled variance)
test_result z_test_proportion_two_sample(std::size_t successes1, std::size_t trials1, std::size_t successes2, std::size_t trials2, alternative_hypothesis alt=alternative_hypothesis::two_sided)
Two-sample proportion z-test.
test_result z_test_proportion(std::size_t successes, std::size_t trials, double p0, alternative_hypothesis alt=alternative_hypothesis::two_sided)
One-sample proportion z-test.
double f_cdf(double x, double df1, double df2)
F-distribution cumulative distribution function (CDF)
std::vector< double > bonferroni_correction(const std::vector< double > &p_values)
Bonferroni correction.
Structure to store statistical test results.
double df
Degrees of freedom.
double df2
Second degrees of freedom (used by F-test)
alternative_hypothesis alternative
Type of alternative hypothesis.
double statistic
Test statistic.