use crate::accum::moments::{checked_variance, Variance};
use crate::error::StatError;
use crate::htest::result::{EffectSize, TestResult};
use crate::special::betai;
fn f_sf(f: f64, d1: f64, d2: f64) -> f64 {
let x = d2 / (d2 + d1 * f);
betai(d2 / 2.0, d1 / 2.0, x)
}
pub(crate) fn anova_sums(groups: &[&[f64]]) -> Result<(f64, f64, f64), StatError> {
let vars: Vec<Variance> = groups.iter().map(|g| checked_variance(g)).collect::<Result<_, _>>()?;
let grand_n: f64 = vars.iter().map(|v| v.count() as f64).sum();
let grand_mean: f64 = vars.iter().map(|v| v.count() as f64 * v.mean()).sum::<f64>() / grand_n;
let ssb: f64 = vars.iter().map(|v| v.count() as f64 * (v.mean() - grand_mean).powi(2)).sum();
let ssw: f64 = vars.iter().map(|v| (v.count() as f64 - 1.0) * v.var_sample()).sum();
Ok((grand_n, ssb, ssw))
}
pub fn anova_one_way(groups: &[&[f64]]) -> Result<TestResult, StatError> {
if groups.len() < 2 { return Err(StatError::TooFewObservations { needed: 2, got: groups.len() }); }
let (grand_n, ssb, ssw) = anova_sums(groups)?;
let k = groups.len() as f64;
let (d1, d2) = (k - 1.0, grand_n - k);
let f = (ssb / d1) / (ssw / d2);
Ok(TestResult {
statistic: f, df: d1, df2: Some(d2), p_value: f_sf(f, d1, d2),
effect_size: Some(EffectSize::EtaSquared(ssb / (ssb + ssw))),
ci: None,
})
}
pub fn f_test_var(a: &[f64], b: &[f64]) -> Result<TestResult, StatError> {
let (sa, sb) = (checked_variance(a)?, checked_variance(b)?);
let (d1, d2) = (sa.count() as f64 - 1.0, sb.count() as f64 - 1.0);
let f = sa.var_sample() / sb.var_sample();
let sf = f_sf(f, d1, d2);
let p = 2.0 * sf.min(1.0 - sf);
Ok(TestResult { statistic: f, df: d1, df2: None, p_value: p, effect_size: None, ci: None })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anova_one_way_exposes_df2_within_groups() {
let g1 = [89., 88., 97., 92.];
let g2 = [84., 79., 81., 83.];
let g3 = [91., 95., 94., 88.];
let r = anova_one_way(&[&g1, &g2, &g3]).unwrap();
assert_eq!(r.df, 2.0);
assert_eq!(r.df2, Some(9.0)); }
}