use super::{f_upper_log_tail, f_upper_tail, len_f64, mean};
use crate::error::{Error, Result};
use crate::tests_stat::TestResult;
pub fn one_way_anova(groups: &[&[f64]]) -> Result<TestResult> {
let k = groups.len();
if k < 2 {
return Err(Error::InsufficientData);
}
let total_n: usize = groups.iter().map(|g| g.len()).sum();
if total_n < k + 1 || groups.iter().any(|g| g.is_empty()) {
return Err(Error::InsufficientData);
}
let grand_mean = {
let sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
sum / len_f64(total_n)
};
let mut ss_between = 0.0;
let mut ss_within = 0.0;
for group in groups {
let gm = mean(group);
let diff = gm - grand_mean;
ss_between = (len_f64(group.len()) * diff).mul_add(diff, ss_between);
ss_within += group.iter().map(|&x| (x - gm) * (x - gm)).sum::<f64>();
}
let ss_total = ss_between + ss_within;
let df_between = k - 1;
let df_within = total_n - k;
if ss_within <= 0.0 {
return Err(Error::DegenerateInput(
"zero within-group variation".to_owned(),
));
}
let ms_between = ss_between / len_f64(df_between);
let ms_within = ss_within / len_f64(df_within);
let f = ms_between / ms_within;
let df_num = i64::try_from(df_between).unwrap_or(i64::MAX);
let df_den = i64::try_from(df_within).unwrap_or(i64::MAX);
let p_value = f_upper_tail(f, df_num, df_den);
let eta_squared = if ss_total > 0.0 {
ss_between / ss_total
} else {
0.0
};
Ok(TestResult {
statistic: f,
p_value,
log_p_value: Some(f_upper_log_tail(f, df_num, df_den)),
df: Some(len_f64(df_between)),
effect_size: Some(eta_squared),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_groups_have_zero_f() -> Result<()> {
let a = [1.0, 2.0, 3.0];
let b = [1.0, 2.0, 3.0];
let r = one_way_anova(&[&a[..], &b[..]])?;
assert!(r.statistic.abs() < 1e-12, "F was {}", r.statistic);
assert!(
r.effect_size.is_some_and(|e| e.abs() < 1e-12),
"eta^2 was {:?}",
r.effect_size
);
Ok(())
}
#[test]
fn single_group_is_insufficient() {
let a = [1.0, 2.0];
assert!(matches!(
one_way_anova(&[&a[..]]),
Err(Error::InsufficientData)
));
}
}