127 std::size_t k = groups.size();
129 throw std::invalid_argument(
"statcpp::one_way_anova: need at least 2 groups");
133 std::vector<std::size_t> group_sizes(k);
134 std::vector<double> group_means(k);
135 std::size_t n_total = 0;
136 double grand_sum = 0.0;
138 for (std::size_t i = 0; i < k; ++i) {
139 if (groups[i].empty()) {
140 throw std::invalid_argument(
"statcpp::one_way_anova: empty group detected");
142 group_sizes[i] = groups[i].size();
143 n_total += group_sizes[i];
144 double group_sum = std::accumulate(groups[i].begin(), groups[i].end(), 0.0);
145 group_means[i] =
group_sum /
static_cast<double>(group_sizes[i]);
150 throw std::invalid_argument(
"statcpp::one_way_anova: need more observations than groups");
153 double grand_mean = grand_sum /
static_cast<double>(n_total);
156 double ss_between = 0.0;
157 double ss_within = 0.0;
159 for (std::size_t i = 0; i < k; ++i) {
160 double diff = group_means[i] - grand_mean;
161 ss_between +=
static_cast<double>(group_sizes[i]) *
diff *
diff;
163 for (
double x : groups[i]) {
164 double diff_within = x - group_means[i];
165 ss_within += diff_within * diff_within;
169 double ss_total = ss_between + ss_within;
172 double df_between =
static_cast<double>(k - 1);
173 double df_within =
static_cast<double>(n_total - k);
174 double df_total =
static_cast<double>(n_total - 1);
177 double ms_between = ss_between / df_between;
178 double ms_within = ss_within / df_within;
183 if (ms_within == 0.0) {
184 f_statistic = (ms_between == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
185 p_value = (ms_between == 0.0) ? 1.0 : 0.0;
187 f_statistic = ms_between / ms_within;
188 p_value = 1.0 -
f_cdf(f_statistic, df_between, df_within);
191 anova_row between{
"Between Groups", ss_between, df_between, ms_between, f_statistic, p_value};
192 anova_row within{
"Within Groups", ss_within, df_within, ms_within, 0.0, 0.0};
194 return {between, within, ss_total, df_total, k, n_total, grand_mean, group_means, group_sizes};
216 const std::vector<std::vector<std::vector<double>>>& data)
218 std::size_t a = data.size();
220 throw std::invalid_argument(
"statcpp::two_way_anova: need at least 2 levels for factor A");
223 std::size_t b = data[0].size();
225 throw std::invalid_argument(
"statcpp::two_way_anova: need at least 2 levels for factor B");
229 std::size_t n_rep = data[0][0].size();
231 throw std::invalid_argument(
"statcpp::two_way_anova: at least 2 replications per cell are required (n_rep >= 2)");
233 std::size_t n_total = 0;
234 double grand_sum = 0.0;
236 for (std::size_t i = 0; i < a; ++i) {
237 if (data[i].size() != b) {
238 throw std::invalid_argument(
"statcpp::two_way_anova: inconsistent number of levels for factor B");
240 for (std::size_t j = 0; j < b; ++j) {
241 if (data[i][j].size() != n_rep) {
242 throw std::invalid_argument(
"statcpp::two_way_anova: unequal cell sizes not supported");
245 for (
double x : data[i][j]) {
251 double grand_mean = grand_sum /
static_cast<double>(n_total);
252 double n_rep_d =
static_cast<double>(n_rep);
253 double a_d =
static_cast<double>(a);
254 double b_d =
static_cast<double>(b);
257 std::vector<double> mean_a(a, 0.0);
258 std::vector<double> mean_b(b, 0.0);
259 std::vector<std::vector<double>> mean_ab(a, std::vector<double>(b, 0.0));
261 for (std::size_t i = 0; i < a; ++i) {
262 for (std::size_t j = 0; j < b; ++j) {
263 double cell_sum = std::accumulate(data[i][j].begin(), data[i][j].end(), 0.0);
264 mean_ab[i][j] = cell_sum / n_rep_d;
265 mean_a[i] += cell_sum;
266 mean_b[j] += cell_sum;
268 mean_a[i] /= (b_d * n_rep_d);
270 for (std::size_t j = 0; j < b; ++j) {
271 mean_b[j] /= (a_d * n_rep_d);
278 double ss_error = 0.0;
280 for (std::size_t i = 0; i < a; ++i) {
281 double diff_a = mean_a[i] - grand_mean;
282 ss_a += diff_a * diff_a;
284 ss_a *= b_d * n_rep_d;
286 for (std::size_t j = 0; j < b; ++j) {
287 double diff_b = mean_b[j] - grand_mean;
288 ss_b += diff_b * diff_b;
290 ss_b *= a_d * n_rep_d;
292 for (std::size_t i = 0; i < a; ++i) {
293 for (std::size_t j = 0; j < b; ++j) {
294 double interaction = mean_ab[i][j] - mean_a[i] - mean_b[j] + grand_mean;
295 ss_ab += interaction * interaction;
297 for (
double x : data[i][j]) {
298 double error = x - mean_ab[i][j];
299 ss_error += error * error;
305 double ss_total = ss_a + ss_b + ss_ab + ss_error;
308 double df_a = a_d - 1.0;
309 double df_b = b_d - 1.0;
310 double df_ab = df_a * df_b;
311 double df_error =
static_cast<double>(n_total) - a_d * b_d;
312 double df_total =
static_cast<double>(n_total) - 1.0;
315 double ms_a = ss_a / df_a;
316 double ms_b = ss_b / df_b;
317 double ms_ab = ss_ab / df_ab;
318 double ms_error = ss_error / df_error;
321 double f_a, f_b, f_ab;
322 double p_a, p_b, p_ab;
323 if (ms_error == 0.0) {
324 f_a = (ms_a == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
325 f_b = (ms_b == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
326 f_ab = (ms_ab == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
327 p_a = (ms_a == 0.0) ? 1.0 : 0.0;
328 p_b = (ms_b == 0.0) ? 1.0 : 0.0;
329 p_ab = (ms_ab == 0.0) ? 1.0 : 0.0;
331 f_a = ms_a / ms_error;
332 f_b = ms_b / ms_error;
333 f_ab = ms_ab / ms_error;
334 p_a = 1.0 -
f_cdf(f_a, df_a, df_error);
335 p_b = 1.0 -
f_cdf(f_b, df_b, df_error);
336 p_ab = 1.0 -
f_cdf(f_ab, df_ab, df_error);
339 anova_row factor_a{
"Factor A", ss_a, df_a, ms_a, f_a, p_a};
340 anova_row factor_b{
"Factor B", ss_b, df_b, ms_b, f_b, p_b};
341 anova_row interaction{
"A x B", ss_ab, df_ab, ms_ab, f_ab, p_ab};
342 anova_row error{
"Error", ss_error, df_error, ms_error, 0.0, 0.0};
344 return {factor_a, factor_b, interaction, error,
345 ss_total, df_total, a, b, n_total, grand_mean};
371 const std::vector<std::vector<double>>& groups,
376 if (alpha <= 0.0 || alpha >= 1.0) {
377 throw std::invalid_argument(
"statcpp::tukey_hsd: alpha must be in (0, 1)");
380 std::size_t k = anova_result.
n_groups;
382 double df_error = anova_result.
within.
df;
383 double k_d =
static_cast<double>(k);
387 std::vector<posthoc_comparison> comparisons;
390 for (std::size_t i = 0; i < k; ++i) {
391 for (std::size_t j = i + 1; j < k; ++j) {
393 double n_i =
static_cast<double>(anova_result.
group_sizes[i]);
394 double n_j =
static_cast<double>(anova_result.
group_sizes[j]);
397 double se = std::sqrt(
mse * 0.5 * (1.0 / n_i + 1.0 / n_j));
399 double q_stat, p_value, lower, upper;
404 if (mean_diff == 0.0) {
405 q_stat = 0.0; p_value = 1.0;
406 lower = 0.0; upper = 0.0; significant =
false;
408 q_stat = std::numeric_limits<double>::infinity();
410 lower = mean_diff; upper = mean_diff; significant =
true;
414 q_stat = std::abs(mean_diff) / se;
418 p_value = std::max(0.0, std::min(1.0, p_value));
420 double margin = q_crit * se;
421 lower = mean_diff - margin;
422 upper = mean_diff + margin;
423 significant = (q_stat > q_crit);
426 comparisons.push_back({i, j, mean_diff, se, q_stat, p_value, lower, upper, significant});
430 return {
"Tukey HSD", comparisons, alpha,
mse, df_error};
447 if (alpha <= 0.0 || alpha >= 1.0) {
448 throw std::invalid_argument(
"statcpp::bonferroni_posthoc: alpha must be in (0, 1)");
451 std::size_t k = anova_result.
n_groups;
453 double df_error = anova_result.
within.
df;
455 double n_comparisons =
static_cast<double>(k * (k - 1) / 2);
456 double alpha_adj = alpha / n_comparisons;
457 double t_crit =
t_quantile(1.0 - alpha_adj / 2.0, df_error);
459 std::vector<posthoc_comparison> comparisons;
461 for (std::size_t i = 0; i < k; ++i) {
462 for (std::size_t j = i + 1; j < k; ++j) {
464 double n_i =
static_cast<double>(anova_result.
group_sizes[i]);
465 double n_j =
static_cast<double>(anova_result.
group_sizes[j]);
467 double se = std::sqrt(
mse * (1.0 / n_i + 1.0 / n_j));
469 double t_stat, p_value, lower, upper;
474 if (mean_diff == 0.0) {
475 t_stat = 0.0; p_value = 1.0;
476 lower = 0.0; upper = 0.0; significant =
false;
478 t_stat = std::copysign(std::numeric_limits<double>::infinity(), mean_diff);
480 lower = mean_diff; upper = mean_diff; significant =
true;
483 t_stat = mean_diff / se;
485 p_value = 2.0 * (1.0 -
t_cdf(std::abs(t_stat), df_error));
486 p_value = std::min(1.0, p_value * n_comparisons);
488 double margin = t_crit * se;
489 lower = mean_diff - margin;
490 upper = mean_diff + margin;
491 significant = (std::abs(t_stat) > t_crit);
494 comparisons.push_back({i, j, mean_diff, se, t_stat, p_value, lower, upper, significant});
498 return {
"Bonferroni", comparisons, alpha,
mse, df_error};
515 std::size_t control_group = 0,
518 if (alpha <= 0.0 || alpha >= 1.0) {
519 throw std::invalid_argument(
"statcpp::dunnett_posthoc: alpha must be in (0, 1)");
521 if (control_group >= anova_result.
n_groups) {
522 throw std::invalid_argument(
"statcpp::dunnett_posthoc: invalid control group index");
525 std::size_t k = anova_result.
n_groups;
527 double df_error = anova_result.
within.
df;
529 double n_comparisons =
static_cast<double>(k - 1);
530 double alpha_adj = alpha / n_comparisons;
531 double t_crit =
t_quantile(1.0 - alpha_adj / 2.0, df_error);
533 std::vector<posthoc_comparison> comparisons;
535 for (std::size_t i = 0; i < k; ++i) {
536 if (i == control_group)
continue;
539 double n_i =
static_cast<double>(anova_result.
group_sizes[i]);
540 double n_c =
static_cast<double>(anova_result.
group_sizes[control_group]);
542 double se = std::sqrt(
mse * (1.0 / n_i + 1.0 / n_c));
544 double t_stat, p_value, lower, upper;
549 if (mean_diff == 0.0) {
550 t_stat = 0.0; p_value = 1.0;
551 lower = 0.0; upper = 0.0; significant =
false;
553 t_stat = std::copysign(std::numeric_limits<double>::infinity(), mean_diff);
555 lower = mean_diff; upper = mean_diff; significant =
true;
558 t_stat = mean_diff / se;
560 p_value = 2.0 * (1.0 -
t_cdf(std::abs(t_stat), df_error));
561 p_value = std::min(1.0, p_value * n_comparisons);
563 double margin = t_crit * se;
564 lower = mean_diff - margin;
565 upper = mean_diff + margin;
566 significant = (std::abs(t_stat) > t_crit);
569 comparisons.push_back({i, control_group, mean_diff, se, t_stat, p_value, lower, upper, significant});
572 return {
"Dunnett (Bonferroni approximation)", comparisons, alpha,
mse, df_error};
594 if (alpha <= 0.0 || alpha >= 1.0) {
595 throw std::invalid_argument(
"statcpp::scheffe_posthoc: alpha must be in (0, 1)");
598 std::size_t k = anova_result.
n_groups;
600 double df_between = anova_result.
between.
df;
601 double df_error = anova_result.
within.
df;
604 double f_crit =
f_quantile(1.0 - alpha, df_between, df_error);
605 double scheffe_crit = std::sqrt(df_between * f_crit);
607 std::vector<posthoc_comparison> comparisons;
609 for (std::size_t i = 0; i < k; ++i) {
610 for (std::size_t j = i + 1; j < k; ++j) {
612 double n_i =
static_cast<double>(anova_result.
group_sizes[i]);
613 double n_j =
static_cast<double>(anova_result.
group_sizes[j]);
615 double se = std::sqrt(
mse * (1.0 / n_i + 1.0 / n_j));
617 double t_stat, p_value, lower, upper;
622 if (mean_diff == 0.0) {
623 t_stat = 0.0; p_value = 1.0;
624 lower = 0.0; upper = 0.0; significant =
false;
626 t_stat = std::copysign(std::numeric_limits<double>::infinity(), mean_diff);
628 lower = mean_diff; upper = mean_diff; significant =
true;
631 t_stat = mean_diff / se;
634 double f_stat = (t_stat * t_stat) / df_between;
635 p_value = 1.0 -
f_cdf(f_stat, df_between, df_error);
637 double margin = scheffe_crit * se;
638 lower = mean_diff - margin;
639 upper = mean_diff + margin;
640 significant = (std::abs(t_stat) > scheffe_crit);
643 comparisons.push_back({i, j, mean_diff, se, t_stat, p_value, lower, upper, significant});
647 return {
"Scheffe", comparisons, alpha,
mse, df_error};
689 const std::vector<std::vector<std::pair<double, double>>>& groups)
691 std::size_t k = groups.size();
693 throw std::invalid_argument(
"statcpp::one_way_ancova: need at least 2 groups");
697 std::size_t n_total = 0;
698 for (
const auto& g : groups) {
700 throw std::invalid_argument(
"statcpp::one_way_ancova: empty group detected");
705 if (n_total <= k + 1) {
706 throw std::invalid_argument(
"statcpp::one_way_ancova: insufficient observations");
710 double sum_y = 0.0, sum_x = 0.0;
711 for (
const auto& g : groups) {
712 for (
const auto& pair : g) {
714 sum_x += pair.second;
717 double grand_mean_y = sum_y /
static_cast<double>(n_total);
718 double grand_mean_x = sum_x /
static_cast<double>(n_total);
721 std::vector<double> group_mean_y(k);
722 std::vector<double> group_mean_x(k);
723 std::vector<std::size_t> group_sizes(k);
725 for (std::size_t i = 0; i < k; ++i) {
726 group_sizes[i] = groups[i].size();
727 double sy = 0.0, sx = 0.0;
728 for (
const auto& pair : groups[i]) {
732 group_mean_y[i] = sy /
static_cast<double>(group_sizes[i]);
733 group_mean_x[i] = sx /
static_cast<double>(group_sizes[i]);
744 for (std::size_t i = 0; i < k; ++i) {
745 for (
const auto& pair : groups[i]) {
746 double dy_t = pair.first - grand_mean_y;
747 double dx_t = pair.second - grand_mean_x;
748 sst_y += dy_t * dy_t;
749 sst_x += dx_t * dx_t;
752 double dy_w = pair.first - group_mean_y[i];
753 double dx_w = pair.second - group_mean_x[i];
754 ssw_y += dy_w * dy_w;
755 ssw_x += dx_w * dx_w;
761 double b_within = (ssw_x > 0.0) ? spw / ssw_x : 0.0;
764 double ss_error = ssw_y - b_within * spw;
765 double ss_covariate = b_within * spw;
768 double b_total = (sst_x > 0.0) ? spt / sst_x : 0.0;
769 double ss_total_adj = sst_y - b_total * spt;
772 double ss_treatment = ss_total_adj - ss_error;
775 double df_covariate = 1.0;
776 double df_treatment =
static_cast<double>(k - 1);
777 double df_error =
static_cast<double>(n_total - k - 1);
780 double ms_covariate = ss_covariate / df_covariate;
781 double ms_treatment = ss_treatment / df_treatment;
782 double ms_error = ss_error / df_error;
785 double f_covariate, f_treatment;
786 double p_covariate, p_treatment;
787 if (ms_error == 0.0) {
788 f_covariate = (ms_covariate == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
789 f_treatment = (ms_treatment == 0.0) ? 0.0 : std::numeric_limits<double>::infinity();
790 p_covariate = (ms_covariate == 0.0) ? 1.0 : 0.0;
791 p_treatment = (ms_treatment == 0.0) ? 1.0 : 0.0;
793 f_covariate = ms_covariate / ms_error;
794 f_treatment = ms_treatment / ms_error;
795 p_covariate = 1.0 -
f_cdf(f_covariate, df_covariate, df_error);
796 p_treatment = 1.0 -
f_cdf(f_treatment, df_treatment, df_error);
800 std::vector<double> adjusted_means(k);
801 for (std::size_t i = 0; i < k; ++i) {
802 adjusted_means[i] = group_mean_y[i] - b_within * (group_mean_x[i] - grand_mean_x);
806 ss_covariate, ss_treatment, ss_error,
807 df_covariate, df_treatment, df_error,
808 ms_covariate, ms_treatment, ms_error,
809 f_covariate, f_treatment,
810 p_covariate, p_treatment,
903 double ms_within = result.
within.
ms;
906 double denom = ss_total + ms_within;
910 return (ss_between - df_between * ms_within) / denom;
926 return std::numeric_limits<double>::infinity();
928 return std::sqrt(eta_sq / (1.0 - eta_sq));
Continuous distribution functions.
posthoc_result scheffe_posthoc(const one_way_anova_result &anova_result, double alpha=0.05)
Perform Scheffe's method for multiple comparisons.
double omega_squared(const one_way_anova_result &result)
Calculate Omega-squared for one-way ANOVA.
double partial_eta_squared_interaction(const two_way_anova_result &result)
Calculate Partial eta-squared for interaction in two-way ANOVA.
double cohens_f(const one_way_anova_result &result)
Calculate Cohen's f for one-way ANOVA.
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 t_cdf(double x, double df)
t-distribution cumulative distribution function (CDF)
ancova_result one_way_ancova(const std::vector< std::vector< std::pair< double, double > > > &groups)
Perform one-way analysis of covariance.
double partial_eta_squared_a(const two_way_anova_result &result)
Calculate Partial eta-squared for factor A in two-way ANOVA.
double eta_squared(const one_way_anova_result &result)
Calculate Eta-squared for one-way ANOVA.
two_way_anova_result two_way_anova(const std::vector< std::vector< std::vector< double > > > &data)
Perform two-way analysis of variance (with replication)
double t_quantile(double p, double df)
t-distribution quantile function (Newton-Raphson method)
double partial_eta_squared_b(const two_way_anova_result &result)
Calculate Partial eta-squared for factor B in two-way ANOVA.
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
posthoc_result dunnett_posthoc(const one_way_anova_result &anova_result, std::size_t control_group=0, double alpha=0.05)
Perform Dunnett's test for multiple comparisons against a control group.
posthoc_result tukey_hsd(const one_way_anova_result &anova_result, const std::vector< std::vector< double > > &groups, double alpha=0.05)
Perform Tukey's Honestly Significant Difference (HSD) test.
one_way_anova_result one_way_anova(const std::vector< std::vector< double > > &groups)
Perform one-way analysis of variance.
double mse(Iterator1 first1, Iterator1 last1, Iterator2 first2)
Mean Squared Error (MSE)
posthoc_result bonferroni_posthoc(const one_way_anova_result &anova_result, double alpha=0.05)
Perform Bonferroni method for multiple comparisons.
aggregation_result< K > group_sum(const std::vector< K > &keys, const std::vector< double > &values)
Sum per group.
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)
Structure storing ANCOVA (Analysis of Covariance) results.
double ss_covariate
Sum of squares for covariate.
double ms_treatment
Mean square for treatment.
double df_treatment
Degrees of freedom for treatment.
double ms_covariate
Mean square for covariate.
double ss_error
Error sum of squares.
double f_covariate
F statistic for covariate.
double p_treatment
p-value for treatment
double df_covariate
Degrees of freedom for covariate.
double f_treatment
F statistic for treatment.
std::vector< double > adjusted_means
Adjusted group means.
double ss_treatment
Sum of squares for treatment effect.
double ms_error
Mean square for error.
double p_covariate
p-value for covariate
double df_error
Degrees of freedom for error.
Structure representing a row in the ANOVA table.
double f_statistic
F statistic.
double df
Degrees of freedom.
std::string source
Name of the source of variation.
Structure storing one-way ANOVA results.
std::vector< std::size_t > group_sizes
Size of each group.
anova_row within
Within-group variation (residual)
double df_total
Total degrees of freedom.
std::size_t n_groups
Number of groups.
double ss_total
Total sum of squares.
std::size_t n_total
Total number of observations.
std::vector< double > group_means
Mean of each group.
anova_row between
Between-group variation.
double grand_mean
Grand mean.
Structure storing individual pairwise comparison result in post-hoc tests.
double mean_diff
Difference in means.
double lower
Confidence interval lower bound.
std::size_t group2
Index of group 2.
double statistic
Test statistic.
double upper
Confidence interval upper bound.
bool significant
Whether significant.
std::size_t group1
Index of group 1.
Structure storing post-hoc comparison results.
std::string method
Method name.
double alpha
Significance level.
std::vector< posthoc_comparison > comparisons
All comparisons.
double df_error
Error degrees of freedom.
double mse
Mean square error.
Structure storing two-way ANOVA results.
double grand_mean
Grand mean.
anova_row factor_a
Effect of factor A.
anova_row interaction
Interaction effect.
anova_row error
Error (residual)
std::size_t levels_a
Number of levels for factor A.
std::size_t levels_b
Number of levels for factor B.
double df_total
Total degrees of freedom.
std::size_t n_total
Total number of observations.
double ss_total
Total sum of squares.
anova_row factor_b
Effect of factor B.