#![allow(clippy::needless_range_loop)]
use crate::error::{Result, StatError};
use crate::utils::math::{mean, variance};
use statrs::distribution::{ChiSquared, ContinuousCDF, FisherSnedecor};
type GroupStats = (Vec<usize>, Vec<f64>, Vec<f64>, usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnovaKind {
Fisher,
Welch,
}
#[derive(Debug, Clone)]
pub struct OneWayAnovaResult {
pub statistic: f64,
pub df_between: f64,
pub df_within: f64,
pub p_value: f64,
pub ss_between: Option<f64>,
pub ss_within: Option<f64>,
pub ss_total: Option<f64>,
pub ms_between: Option<f64>,
pub ms_within: Option<f64>,
pub n_groups: usize,
pub group_sizes: Vec<usize>,
pub group_means: Vec<f64>,
pub grand_mean: Option<f64>,
}
fn validate_anova_groups(groups: &[&[f64]]) -> Result<GroupStats> {
if groups.len() < 2 {
return Err(StatError::InvalidParameter(
"ANOVA requires at least 2 groups".to_string(),
));
}
let mut group_sizes: Vec<usize> = Vec::with_capacity(groups.len());
let mut group_means: Vec<f64> = Vec::with_capacity(groups.len());
let mut group_vars: Vec<f64> = Vec::with_capacity(groups.len());
let mut n_total = 0usize;
for (i, group) in groups.iter().enumerate() {
if group.len() < 2 {
return Err(StatError::InsufficientData {
needed: 2,
got: group.len(),
});
}
if group.iter().any(|x| x.is_nan()) {
return Err(StatError::InvalidParameter(format!(
"Group {} contains NaN values",
i + 1
)));
}
group_sizes.push(group.len());
group_means.push(mean(group)?);
group_vars.push(variance(group)?);
n_total += group.len();
}
Ok((group_sizes, group_means, group_vars, n_total))
}
fn fisher_anova(
groups: &[&[f64]],
group_sizes: &[usize],
group_means: &[f64],
n_total: usize,
) -> Result<OneWayAnovaResult> {
let k = groups.len();
let n_f = n_total as f64;
let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
let grand_mean = grand_sum / n_f;
let ss_between: f64 = group_sizes
.iter()
.zip(group_means.iter())
.map(|(&n_i, &mean_i)| n_i as f64 * (mean_i - grand_mean).powi(2))
.sum();
let ss_within: f64 = groups
.iter()
.zip(group_means.iter())
.map(|(group, &group_mean)| group.iter().map(|&x| (x - group_mean).powi(2)).sum::<f64>())
.sum();
let ss_total = ss_between + ss_within;
let df_between = (k - 1) as f64;
let df_within = (n_total - k) as f64;
let ms_between = ss_between / df_between;
let ms_within = ss_within / df_within;
let f_stat = ms_between / ms_within;
let f_dist = FisherSnedecor::new(df_between, df_within).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_value = f_dist.sf(f_stat);
Ok(OneWayAnovaResult {
statistic: f_stat,
df_between,
df_within,
p_value,
ss_between: Some(ss_between),
ss_within: Some(ss_within),
ss_total: Some(ss_total),
ms_between: Some(ms_between),
ms_within: Some(ms_within),
n_groups: k,
group_sizes: group_sizes.to_vec(),
group_means: group_means.to_vec(),
grand_mean: Some(grand_mean),
})
}
fn welch_anova(
group_sizes: &[usize],
group_means: &[f64],
group_vars: &[f64],
) -> Result<OneWayAnovaResult> {
let k = group_sizes.len();
let weights: Vec<f64> = group_sizes
.iter()
.zip(group_vars.iter())
.map(|(&n_i, &var_i)| {
if var_i < 1e-15 {
f64::MAX / (k as f64)
} else {
n_i as f64 / var_i
}
})
.collect();
let sum_weights: f64 = weights.iter().sum();
let weighted_grand_mean: f64 = weights
.iter()
.zip(group_means.iter())
.map(|(&w, &mean)| w * mean)
.sum::<f64>()
/ sum_weights;
let f_numerator: f64 = weights
.iter()
.zip(group_means.iter())
.map(|(&w, &mean)| w * (mean - weighted_grand_mean).powi(2))
.sum::<f64>()
/ (k - 1) as f64;
let lambda: f64 = group_sizes
.iter()
.zip(weights.iter())
.map(|(&n_i, &w_i)| {
let term = 1.0 - w_i / sum_weights;
term * term / (n_i - 1) as f64
})
.sum::<f64>()
* 3.0
/ ((k * k - 1) as f64);
let f_stat = f_numerator / (1.0 + 2.0 * lambda * (k - 2) as f64 / 3.0);
let df_between = (k - 1) as f64;
let df_within = ((k * k - 1) as f64)
/ (3.0
* group_sizes
.iter()
.zip(weights.iter())
.map(|(&n_i, &w_i)| {
let term = 1.0 - w_i / sum_weights;
term * term / (n_i - 1) as f64
})
.sum::<f64>());
let f_dist = FisherSnedecor::new(df_between, df_within).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_value = f_dist.sf(f_stat);
Ok(OneWayAnovaResult {
statistic: f_stat,
df_between,
df_within,
p_value,
ss_between: None,
ss_within: None,
ss_total: None,
ms_between: None,
ms_within: None,
n_groups: k,
group_sizes: group_sizes.to_vec(),
group_means: group_means.to_vec(),
grand_mean: None,
})
}
pub fn one_way_anova(groups: &[&[f64]], kind: AnovaKind) -> Result<OneWayAnovaResult> {
let (group_sizes, group_means, group_vars, n_total) = validate_anova_groups(groups)?;
match kind {
AnovaKind::Fisher => fisher_anova(groups, &group_sizes, &group_means, n_total),
AnovaKind::Welch => welch_anova(&group_sizes, &group_means, &group_vars),
}
}
#[derive(Debug, Clone)]
pub struct AnovaTableRow {
pub ss: f64,
pub df: f64,
pub ms: f64,
pub f_statistic: Option<f64>,
pub p_value: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct TwoWayAnovaResult {
pub factor_a: AnovaTableRow,
pub factor_b: AnovaTableRow,
pub interaction: AnovaTableRow,
pub residual: AnovaTableRow,
pub total: AnovaTableRow,
pub levels_a: usize,
pub levels_b: usize,
pub n: usize,
pub grand_mean: f64,
pub cell_means: Vec<Vec<f64>>,
pub marginal_means_a: Vec<f64>,
pub marginal_means_b: Vec<f64>,
}
struct TwoWayData {
cells: Vec<Vec<Vec<f64>>>,
levels_a: usize,
levels_b: usize,
n_total: usize,
cell_n: Vec<Vec<usize>>,
}
fn organize_two_way_data(
values: &[f64],
factor_a: &[usize],
factor_b: &[usize],
) -> Result<TwoWayData> {
let n = values.len();
if factor_a.len() != n || factor_b.len() != n {
return Err(StatError::InvalidParameter(format!(
"Mismatched lengths: values ({}), factor_a ({}), factor_b ({})",
n,
factor_a.len(),
factor_b.len()
)));
}
if n == 0 {
return Err(StatError::EmptyData);
}
if values.iter().any(|x| x.is_nan()) {
return Err(StatError::InvalidParameter(
"Values contain NaN".to_string(),
));
}
let levels_a = factor_a.iter().max().map(|m| m + 1).unwrap_or(0);
let levels_b = factor_b.iter().max().map(|m| m + 1).unwrap_or(0);
if levels_a < 2 {
return Err(StatError::InvalidParameter(
"Factor A must have at least 2 levels".to_string(),
));
}
if levels_b < 2 {
return Err(StatError::InvalidParameter(
"Factor B must have at least 2 levels".to_string(),
));
}
let mut cells: Vec<Vec<Vec<f64>>> = vec![vec![Vec::new(); levels_b]; levels_a];
for i in 0..n {
let a = factor_a[i];
let b = factor_b[i];
if a >= levels_a || b >= levels_b {
return Err(StatError::InvalidParameter(format!(
"Factor index out of bounds at position {}: a={}, b={}",
i, a, b
)));
}
cells[a][b].push(values[i]);
}
let mut cell_n: Vec<Vec<usize>> = vec![vec![0; levels_b]; levels_a];
for a in 0..levels_a {
for b in 0..levels_b {
if cells[a][b].is_empty() {
return Err(StatError::InvalidParameter(format!(
"Empty cell: no observations for factor combination (A={}, B={})",
a, b
)));
}
cell_n[a][b] = cells[a][b].len();
}
}
Ok(TwoWayData {
cells,
levels_a,
levels_b,
n_total: n,
cell_n,
})
}
fn compute_two_way_type3_ss(
data: &TwoWayData,
values: &[f64],
factor_a: &[usize],
factor_b: &[usize],
) -> (f64, f64, f64, f64) {
let n = data.n_total;
let a = data.levels_a;
let b = data.levels_b;
let mut cell_means: Vec<Vec<f64>> = vec![vec![0.0; b]; a];
for ai in 0..a {
for bi in 0..b {
cell_means[ai][bi] =
data.cells[ai][bi].iter().sum::<f64>() / data.cells[ai][bi].len() as f64;
}
}
let mut ss_error = 0.0;
for ai in 0..a {
for bi in 0..b {
let cm = cell_means[ai][bi];
for &y in &data.cells[ai][bi] {
ss_error += (y - cm).powi(2);
}
}
}
let first_n = data.cell_n[0][0];
let is_balanced = data
.cell_n
.iter()
.all(|row| row.iter().all(|&n| n == first_n));
if is_balanced {
let n_per_cell = first_n;
let mut marginal_means_a: Vec<f64> = vec![0.0; a];
for ai in 0..a {
let mut sum = 0.0;
for bi in 0..b {
sum += cell_means[ai][bi];
}
marginal_means_a[ai] = sum / b as f64;
}
let mut marginal_means_b: Vec<f64> = vec![0.0; b];
for bi in 0..b {
let mut sum = 0.0;
for ai in 0..a {
sum += cell_means[ai][bi];
}
marginal_means_b[bi] = sum / a as f64;
}
let grand_mean: f64 = cell_means.iter().flatten().sum::<f64>() / (a * b) as f64;
let ss_a: f64 = (n_per_cell * b) as f64
* marginal_means_a
.iter()
.map(|m| (m - grand_mean).powi(2))
.sum::<f64>();
let ss_b: f64 = (n_per_cell * a) as f64
* marginal_means_b
.iter()
.map(|m| (m - grand_mean).powi(2))
.sum::<f64>();
let mut ss_ab = 0.0;
for ai in 0..a {
for bi in 0..b {
let interaction_effect =
cell_means[ai][bi] - marginal_means_a[ai] - marginal_means_b[bi] + grand_mean;
ss_ab += n_per_cell as f64 * interaction_effect.powi(2);
}
}
(ss_a, ss_b, ss_ab, ss_error)
} else {
let n_params = 1 + (a - 1) + (b - 1) + (a - 1) * (b - 1);
let mut x: Vec<Vec<f64>> = vec![vec![0.0; n_params]; n];
for i in 0..n {
let ai = factor_a[i];
let bi = factor_b[i];
x[i][0] = 1.0;
for k in 0..(a - 1) {
if ai == k {
x[i][1 + k] = 1.0;
} else if ai == a - 1 {
x[i][1 + k] = -1.0;
}
}
for k in 0..(b - 1) {
if bi == k {
x[i][1 + (a - 1) + k] = 1.0;
} else if bi == b - 1 {
x[i][1 + (a - 1) + k] = -1.0;
}
}
let mut idx = 1 + (a - 1) + (b - 1);
for k_a in 0..(a - 1) {
for k_b in 0..(b - 1) {
x[i][idx] = x[i][1 + k_a] * x[i][1 + (a - 1) + k_b];
idx += 1;
}
}
}
let mut xtx = vec![vec![0.0; n_params]; n_params];
for j in 0..n_params {
for k in 0..n_params {
for i in 0..n {
xtx[j][k] += x[i][j] * x[i][k];
}
}
}
let mut xty = vec![0.0; n_params];
for j in 0..n_params {
for i in 0..n {
xty[j] += x[i][j] * values[i];
}
}
let beta = solve_linear_system(&xtx, &xty);
let xtx_inv = matrix_inverse(&xtx);
let a_start = 1;
let a_end = 1 + (a - 1);
let b_start = 1 + (a - 1);
let b_end = 1 + (a - 1) + (b - 1);
let ab_start = 1 + (a - 1) + (b - 1);
let ss_a = compute_wald_ss(&beta, &xtx_inv, a_start, a_end, n_params);
let ss_b = compute_wald_ss(&beta, &xtx_inv, b_start, b_end, n_params);
let ss_ab = compute_wald_ss(&beta, &xtx_inv, ab_start, n_params, n_params);
(ss_a, ss_b, ss_ab, ss_error)
}
}
fn compute_wald_ss(
beta: &[f64],
xtx_inv: &[Vec<f64>],
effect_start: usize,
effect_end: usize,
_n_params: usize,
) -> f64 {
let effect_size = effect_end - effect_start;
let beta_effect: Vec<f64> = beta[effect_start..effect_end].to_vec();
let mut cov_effect = vec![vec![0.0; effect_size]; effect_size];
for i in 0..effect_size {
for j in 0..effect_size {
cov_effect[i][j] = xtx_inv[effect_start + i][effect_start + j];
}
}
let cov_inv = matrix_inverse(&cov_effect);
let mut ss = 0.0;
for i in 0..effect_size {
for j in 0..effect_size {
ss += beta_effect[i] * cov_inv[i][j] * beta_effect[j];
}
}
ss
}
fn matrix_inverse(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = a.len();
let mut aug: Vec<Vec<f64>> = vec![vec![0.0; 2 * n]; n];
for i in 0..n {
for j in 0..n {
aug[i][j] = a[i][j];
}
aug[i][n + i] = 1.0;
}
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
aug.swap(col, max_row);
if aug[col][col].abs() < 1e-14 {
continue;
}
let pivot = aug[col][col];
for j in 0..(2 * n) {
aug[col][j] /= pivot;
}
for row in 0..n {
if row != col {
let factor = aug[row][col];
for j in 0..(2 * n) {
aug[row][j] -= factor * aug[col][j];
}
}
}
}
let mut inv = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
inv[i][j] = aug[i][n + j];
}
}
inv
}
#[allow(dead_code)]
fn fit_model_ss_error(x: &[Vec<f64>], y: &[f64], n_params: usize) -> f64 {
let n = x.len();
let mut xtx = vec![vec![0.0; n_params]; n_params];
for j in 0..n_params {
for k in 0..n_params {
for i in 0..n {
xtx[j][k] += x[i][j] * x[i][k];
}
}
}
let mut xty = vec![0.0; n_params];
for j in 0..n_params {
for i in 0..n {
xty[j] += x[i][j] * y[i];
}
}
let beta = solve_linear_system(&xtx, &xty);
let mut ss_error = 0.0;
for i in 0..n {
let mut y_hat_i = 0.0;
for j in 0..n_params {
y_hat_i += x[i][j] * beta[j];
}
ss_error += (y[i] - y_hat_i).powi(2);
}
ss_error
}
#[allow(dead_code)]
fn fit_reduced_model_ss_diff(
x: &[Vec<f64>],
y: &[f64],
n_params: usize,
exclude_start: usize,
exclude_end: usize,
ss_error_full: f64,
) -> f64 {
let n = x.len();
let reduced_params = n_params - (exclude_end - exclude_start);
let mut x_reduced: Vec<Vec<f64>> = vec![vec![0.0; reduced_params]; n];
for i in 0..n {
let mut j_reduced = 0;
for j in 0..n_params {
if j < exclude_start || j >= exclude_end {
x_reduced[i][j_reduced] = x[i][j];
j_reduced += 1;
}
}
}
let ss_error_reduced = fit_model_ss_error(&x_reduced, y, reduced_params);
ss_error_reduced - ss_error_full
}
fn solve_linear_system(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
let n = b.len();
let mut aug: Vec<Vec<f64>> = a.to_vec();
for i in 0..n {
aug[i].push(b[i]);
}
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
aug.swap(col, max_row);
if aug[col][col].abs() < 1e-14 {
continue;
}
for row in (col + 1)..n {
let factor = aug[row][col] / aug[col][col];
for j in col..=n {
aug[row][j] -= factor * aug[col][j];
}
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
if aug[i][i].abs() < 1e-14 {
x[i] = 0.0;
continue;
}
x[i] = aug[i][n];
for j in (i + 1)..n {
x[i] -= aug[i][j] * x[j];
}
x[i] /= aug[i][i];
}
x
}
pub fn two_way_anova(
values: &[f64],
factor_a: &[usize],
factor_b: &[usize],
) -> Result<TwoWayAnovaResult> {
let data = organize_two_way_data(values, factor_a, factor_b)?;
let a = data.levels_a;
let b = data.levels_b;
let n = data.n_total;
let grand_mean = values.iter().sum::<f64>() / n as f64;
let mut cell_means: Vec<Vec<f64>> = vec![vec![0.0; b]; a];
for ai in 0..a {
for bi in 0..b {
if !data.cells[ai][bi].is_empty() {
cell_means[ai][bi] =
data.cells[ai][bi].iter().sum::<f64>() / data.cells[ai][bi].len() as f64;
}
}
}
let mut marginal_means_a: Vec<f64> = vec![0.0; a];
for ai in 0..a {
let mut sum = 0.0;
let mut count = 0.0;
for bi in 0..b {
for &y in &data.cells[ai][bi] {
sum += y;
count += 1.0;
}
}
marginal_means_a[ai] = sum / count;
}
let mut marginal_means_b: Vec<f64> = vec![0.0; b];
for bi in 0..b {
let mut sum = 0.0;
let mut count = 0.0;
for ai in 0..a {
for &y in &data.cells[ai][bi] {
sum += y;
count += 1.0;
}
}
marginal_means_b[bi] = sum / count;
}
let (ss_a, ss_b, ss_ab, ss_error) = compute_two_way_type3_ss(&data, values, factor_a, factor_b);
let ss_total: f64 = values.iter().map(|y| (y - grand_mean).powi(2)).sum();
let df_a = (a - 1) as f64;
let df_b = (b - 1) as f64;
let df_ab = ((a - 1) * (b - 1)) as f64;
let df_error = (n - a * b) as f64;
let df_total = (n - 1) as f64;
if df_error <= 0.0 {
return Err(StatError::InsufficientData {
needed: a * b + 1,
got: n,
});
}
let ms_a = ss_a / df_a;
let ms_b = ss_b / df_b;
let ms_ab = ss_ab / df_ab;
let ms_error = ss_error / df_error;
let f_a = ms_a / ms_error;
let f_b = ms_b / ms_error;
let f_ab = ms_ab / ms_error;
let f_dist_a = FisherSnedecor::new(df_a, df_error).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_a = f_dist_a.sf(f_a);
let f_dist_b = FisherSnedecor::new(df_b, df_error).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_b = f_dist_b.sf(f_b);
let f_dist_ab = FisherSnedecor::new(df_ab, df_error).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_ab = f_dist_ab.sf(f_ab);
Ok(TwoWayAnovaResult {
factor_a: AnovaTableRow {
ss: ss_a,
df: df_a,
ms: ms_a,
f_statistic: Some(f_a),
p_value: Some(p_a),
},
factor_b: AnovaTableRow {
ss: ss_b,
df: df_b,
ms: ms_b,
f_statistic: Some(f_b),
p_value: Some(p_b),
},
interaction: AnovaTableRow {
ss: ss_ab,
df: df_ab,
ms: ms_ab,
f_statistic: Some(f_ab),
p_value: Some(p_ab),
},
residual: AnovaTableRow {
ss: ss_error,
df: df_error,
ms: ms_error,
f_statistic: None,
p_value: None,
},
total: AnovaTableRow {
ss: ss_total,
df: df_total,
ms: ss_total / df_total,
f_statistic: None,
p_value: None,
},
levels_a: a,
levels_b: b,
n,
grand_mean,
cell_means,
marginal_means_a,
marginal_means_b,
})
}
#[derive(Debug, Clone)]
pub struct SphericityResult {
pub w: f64,
pub chi_square: f64,
pub df: f64,
pub p_value: f64,
}
#[derive(Debug, Clone)]
pub struct CorrectedResult {
pub epsilon: f64,
pub df_num_corrected: f64,
pub df_den_corrected: f64,
pub f_statistic: f64,
pub p_value: f64,
}
#[derive(Debug, Clone)]
pub struct RmAnovaResult {
pub within_subjects: AnovaTableRow,
pub subjects: AnovaTableRow,
pub error: AnovaTableRow,
pub total: AnovaTableRow,
pub sphericity: Option<SphericityResult>,
pub greenhouse_geisser: Option<CorrectedResult>,
pub huynh_feldt: Option<CorrectedResult>,
pub grand_mean: f64,
pub condition_means: Vec<f64>,
pub subject_means: Vec<f64>,
}
pub fn repeated_measures_anova(data: &[&[f64]], compute_sphericity: bool) -> Result<RmAnovaResult> {
let n_subjects = data.len();
if n_subjects < 2 {
return Err(StatError::InsufficientData {
needed: 2,
got: n_subjects,
});
}
let n_conditions = data[0].len();
if n_conditions < 2 {
return Err(StatError::InvalidParameter(
"Repeated measures ANOVA requires at least 2 conditions".to_string(),
));
}
for (i, subject) in data.iter().enumerate() {
if subject.len() != n_conditions {
return Err(StatError::InvalidParameter(format!(
"Subject {} has {} conditions, expected {}",
i + 1,
subject.len(),
n_conditions
)));
}
if subject.iter().any(|x| x.is_nan()) {
return Err(StatError::InvalidParameter(format!(
"Subject {} contains NaN values",
i + 1
)));
}
}
let n = n_subjects as f64;
let k = n_conditions as f64;
let grand_sum: f64 = data.iter().flat_map(|s| s.iter()).sum();
let grand_mean = grand_sum / (n * k);
let subject_means: Vec<f64> = data.iter().map(|s| s.iter().sum::<f64>() / k).collect();
let mut condition_means: Vec<f64> = vec![0.0; n_conditions];
for j in 0..n_conditions {
let sum: f64 = data.iter().map(|s| s[j]).sum();
condition_means[j] = sum / n;
}
let ss_total: f64 = data
.iter()
.flat_map(|s| s.iter())
.map(|&y| (y - grand_mean).powi(2))
.sum();
let ss_subjects: f64 = k * subject_means
.iter()
.map(|m| (m - grand_mean).powi(2))
.sum::<f64>();
let ss_conditions: f64 = n * condition_means
.iter()
.map(|m| (m - grand_mean).powi(2))
.sum::<f64>();
let ss_error = ss_total - ss_subjects - ss_conditions;
let df_subjects = n - 1.0;
let df_conditions = k - 1.0;
let df_error = (n - 1.0) * (k - 1.0);
let df_total = n * k - 1.0;
let ms_subjects = ss_subjects / df_subjects;
let ms_conditions = ss_conditions / df_conditions;
let ms_error = ss_error / df_error;
let f_stat = ms_conditions / ms_error;
let f_dist = FisherSnedecor::new(df_conditions, df_error).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
let p_value = f_dist.sf(f_stat);
let (sphericity, greenhouse_geisser, huynh_feldt) = if compute_sphericity && n_conditions >= 3 {
compute_sphericity_corrections(data, f_stat, df_conditions, df_error)?
} else {
(None, None, None)
};
Ok(RmAnovaResult {
within_subjects: AnovaTableRow {
ss: ss_conditions,
df: df_conditions,
ms: ms_conditions,
f_statistic: Some(f_stat),
p_value: Some(p_value),
},
subjects: AnovaTableRow {
ss: ss_subjects,
df: df_subjects,
ms: ms_subjects,
f_statistic: None,
p_value: None,
},
error: AnovaTableRow {
ss: ss_error,
df: df_error,
ms: ms_error,
f_statistic: None,
p_value: None,
},
total: AnovaTableRow {
ss: ss_total,
df: df_total,
ms: ss_total / df_total,
f_statistic: None,
p_value: None,
},
sphericity,
greenhouse_geisser,
huynh_feldt,
grand_mean,
condition_means,
subject_means,
})
}
fn compute_sphericity_corrections(
data: &[&[f64]],
f_stat: f64,
df_num: f64,
df_den: f64,
) -> Result<(
Option<SphericityResult>,
Option<CorrectedResult>,
Option<CorrectedResult>,
)> {
let n = data.len();
let k = data[0].len();
let mut cov_matrix: Vec<Vec<f64>> = vec![vec![0.0; k]; k];
let mut condition_means: Vec<f64> = vec![0.0; k];
for j in 0..k {
condition_means[j] = data.iter().map(|s| s[j]).sum::<f64>() / n as f64;
}
for i in 0..k {
for j in 0..k {
let mut sum = 0.0;
for subject in data.iter() {
sum += (subject[i] - condition_means[i]) * (subject[j] - condition_means[j]);
}
cov_matrix[i][j] = sum / (n - 1) as f64;
}
}
let p = k - 1;
let mut c: Vec<Vec<f64>> = vec![vec![0.0; k]; p];
for i in 0..p {
c[i][i] = 1.0;
c[i][i + 1] = -1.0;
}
let c_orth = orthonormalize(&c);
let csc = matrix_multiply_csc(&c_orth, &cov_matrix);
let eigenvalues = compute_eigenvalues(&csc);
let sum_eigenvalues: f64 = eigenvalues.iter().sum();
let sum_eigenvalues_sq: f64 = eigenvalues.iter().map(|e| e * e).sum();
let epsilon_gg = if sum_eigenvalues_sq > 1e-15 {
(sum_eigenvalues * sum_eigenvalues) / (p as f64 * sum_eigenvalues_sq)
} else {
1.0
};
let epsilon_gg = epsilon_gg.max(1.0 / (k - 1) as f64).min(1.0);
let n_f = n as f64;
let k_f = k as f64;
let epsilon_hf = if (n_f - 1.0) * (k_f - 1.0) * epsilon_gg - 2.0 > 1e-15 {
((n_f - 1.0) * (k_f - 1.0) * epsilon_gg - 2.0)
/ ((k_f - 1.0) * (n_f - 1.0 - (k_f - 1.0) * epsilon_gg))
} else {
1.0
};
let epsilon_hf = epsilon_hf.max(epsilon_gg).min(1.0);
let det_csc = matrix_determinant(&csc);
let trace_csc: f64 = (0..p).map(|i| csc[i][i]).sum();
let mauchly_w = if (trace_csc / p as f64).abs() > 1e-15 {
det_csc / (trace_csc / p as f64).powi(p as i32)
} else {
1.0
};
let mauchly_w = mauchly_w.clamp(0.0, 1.0);
let correction = (2.0 * k_f * k_f - 3.0 * k_f + 3.0) / (6.0 * (k_f - 1.0));
let chi_sq = if mauchly_w > 1e-15 {
-(n_f - 1.0 - correction) * mauchly_w.ln()
} else {
f64::INFINITY
};
let df_chi = (p * (p + 1)) as f64 / 2.0 - 1.0;
let p_mauchly = if chi_sq.is_finite() && df_chi > 0.0 {
let chi_dist = ChiSquared::new(df_chi).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create chi-square distribution: {}", e))
})?;
chi_dist.sf(chi_sq)
} else {
0.0
};
let sphericity = SphericityResult {
w: mauchly_w,
chi_square: chi_sq,
df: df_chi,
p_value: p_mauchly,
};
let df_num_gg = df_num * epsilon_gg;
let df_den_gg = df_den * epsilon_gg;
let p_gg = if df_num_gg > 0.0 && df_den_gg > 0.0 {
let f_dist = FisherSnedecor::new(df_num_gg, df_den_gg).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
f_dist.sf(f_stat)
} else {
1.0
};
let gg = CorrectedResult {
epsilon: epsilon_gg,
df_num_corrected: df_num_gg,
df_den_corrected: df_den_gg,
f_statistic: f_stat,
p_value: p_gg,
};
let df_num_hf = df_num * epsilon_hf;
let df_den_hf = df_den * epsilon_hf;
let p_hf = if df_num_hf > 0.0 && df_den_hf > 0.0 {
let f_dist = FisherSnedecor::new(df_num_hf, df_den_hf).map_err(|e| {
StatError::InvalidParameter(format!("Failed to create F-distribution: {}", e))
})?;
f_dist.sf(f_stat)
} else {
1.0
};
let hf = CorrectedResult {
epsilon: epsilon_hf,
df_num_corrected: df_num_hf,
df_den_corrected: df_den_hf,
f_statistic: f_stat,
p_value: p_hf,
};
Ok((Some(sphericity), Some(gg), Some(hf)))
}
fn orthonormalize(c: &[Vec<f64>]) -> Vec<Vec<f64>> {
let p = c.len();
let k = c[0].len();
let mut result: Vec<Vec<f64>> = vec![vec![0.0; k]; p];
for i in 0..p {
result[i] = c[i].clone();
for j in 0..i {
let dot_product: f64 = (0..k).map(|m| result[i][m] * result[j][m]).sum();
for m in 0..k {
result[i][m] -= dot_product * result[j][m];
}
}
let norm: f64 = result[i].iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-15 {
for m in 0..k {
result[i][m] /= norm;
}
}
}
result
}
fn matrix_multiply_csc(c: &[Vec<f64>], s: &[Vec<f64>]) -> Vec<Vec<f64>> {
let p = c.len();
let k = c[0].len();
let mut cs: Vec<Vec<f64>> = vec![vec![0.0; k]; p];
for i in 0..p {
for j in 0..k {
for m in 0..k {
cs[i][j] += c[i][m] * s[m][j];
}
}
}
let mut result: Vec<Vec<f64>> = vec![vec![0.0; p]; p];
for i in 0..p {
for j in 0..p {
for m in 0..k {
result[i][j] += cs[i][m] * c[j][m];
}
}
}
result
}
fn compute_eigenvalues(a: &[Vec<f64>]) -> Vec<f64> {
let n = a.len();
if n == 0 {
return vec![];
}
let mut matrix: Vec<Vec<f64>> = a.to_vec();
let mut eigenvalues = vec![0.0; n];
for _ in 0..100 {
let (q, r) = qr_decomposition(&matrix);
matrix = matrix_multiply(&r, &q);
}
for i in 0..n {
eigenvalues[i] = matrix[i][i];
}
eigenvalues
}
fn qr_decomposition(a: &[Vec<f64>]) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
let n = a.len();
let mut q: Vec<Vec<f64>> = vec![vec![0.0; n]; n];
let mut r: Vec<Vec<f64>> = vec![vec![0.0; n]; n];
for j in 0..n {
let mut v: Vec<f64> = (0..n).map(|i| a[i][j]).collect();
for i in 0..j {
r[i][j] = (0..n).map(|k| q[k][i] * a[k][j]).sum();
for k in 0..n {
v[k] -= r[i][j] * q[k][i];
}
}
r[j][j] = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if r[j][j] > 1e-15 {
for k in 0..n {
q[k][j] = v[k] / r[j][j];
}
}
}
(q, r)
}
fn matrix_multiply(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = a.len();
let m = b[0].len();
let p = b.len();
let mut result: Vec<Vec<f64>> = vec![vec![0.0; m]; n];
for i in 0..n {
for j in 0..m {
for k in 0..p {
result[i][j] += a[i][k] * b[k][j];
}
}
}
result
}
fn matrix_determinant(a: &[Vec<f64>]) -> f64 {
let n = a.len();
if n == 0 {
return 1.0;
}
if n == 1 {
return a[0][0];
}
if n == 2 {
return a[0][0] * a[1][1] - a[0][1] * a[1][0];
}
let mut lu: Vec<Vec<f64>> = a.to_vec();
let mut sign = 1.0;
for col in 0..n {
let mut max_row = col;
let mut max_val = lu[col][col].abs();
for row in (col + 1)..n {
if lu[row][col].abs() > max_val {
max_val = lu[row][col].abs();
max_row = row;
}
}
if max_val < 1e-15 {
return 0.0;
}
if max_row != col {
lu.swap(col, max_row);
sign = -sign;
}
for row in (col + 1)..n {
let factor = lu[row][col] / lu[col][col];
for j in col..n {
lu[row][j] -= factor * lu[col][j];
}
}
}
let mut det = sign;
for i in 0..n {
det *= lu[i][i];
}
det
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_one_way_anova_fisher_basic() {
let g1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = vec![2.0, 3.0, 4.0, 5.0, 6.0];
let g3 = vec![3.0, 4.0, 5.0, 6.0, 7.0];
let result = one_way_anova(&[&g1, &g2, &g3], AnovaKind::Fisher).unwrap();
assert_eq!(result.n_groups, 3);
assert_eq!(result.group_sizes, vec![5, 5, 5]);
assert_eq!(result.df_between, 2.0);
assert_eq!(result.df_within, 12.0);
assert!(result.statistic > 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.ss_between.is_some());
assert!(result.ss_within.is_some());
}
#[test]
fn test_one_way_anova_welch_basic() {
let g1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = vec![2.0, 3.0, 4.0, 5.0, 6.0];
let g3 = vec![3.0, 4.0, 5.0, 6.0, 7.0];
let result = one_way_anova(&[&g1, &g2, &g3], AnovaKind::Welch).unwrap();
assert_eq!(result.n_groups, 3);
assert_eq!(result.df_between, 2.0);
assert!(result.df_within > 0.0);
assert!(result.statistic > 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.ss_between.is_none());
assert!(result.ss_within.is_none());
}
#[test]
fn test_one_way_anova_two_groups() {
let g1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = vec![3.0, 4.0, 5.0, 6.0, 7.0];
let result = one_way_anova(&[&g1, &g2], AnovaKind::Fisher).unwrap();
assert_eq!(result.n_groups, 2);
assert_eq!(result.df_between, 1.0);
assert_eq!(result.df_within, 8.0);
}
#[test]
fn test_one_way_anova_single_group_error() {
let g1 = vec![1.0, 2.0, 3.0];
let result = one_way_anova(&[&g1], AnovaKind::Fisher);
assert!(result.is_err());
}
#[test]
fn test_one_way_anova_empty_group_error() {
let g1 = vec![1.0, 2.0, 3.0];
let g2: Vec<f64> = vec![];
let result = one_way_anova(&[&g1, &g2[..]], AnovaKind::Fisher);
assert!(result.is_err());
}
#[test]
fn test_one_way_anova_single_observation_error() {
let g1 = vec![1.0, 2.0, 3.0];
let g2 = vec![4.0]; let result = one_way_anova(&[&g1, &g2[..]], AnovaKind::Fisher);
assert!(result.is_err());
}
#[test]
fn test_ss_total_equals_ss_between_plus_ss_within() {
let g1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let g2 = vec![2.0, 3.0, 4.0, 5.0, 6.0];
let g3 = vec![3.0, 4.0, 5.0, 6.0, 7.0];
let result = one_way_anova(&[&g1, &g2, &g3], AnovaKind::Fisher).unwrap();
let ss_total = result.ss_total.unwrap();
let ss_between = result.ss_between.unwrap();
let ss_within = result.ss_within.unwrap();
assert!((ss_total - (ss_between + ss_within)).abs() < 1e-10);
}
}