use crate::error::{Error, Result};
use crate::tests_stat::parametric::len_f64;
use crate::tests_stat::ranks::{mid_ranks, tie_correction};
use crate::tests_stat::{TestResult, chi_squared_upper_log_tail, chi_squared_upper_tail};
pub fn friedman(treatments: &[&[f64]]) -> Result<TestResult> {
let k = treatments.len();
if k < 3 {
return Err(Error::InsufficientData);
}
let n = treatments.first().map_or(0, |t| t.len());
if n == 0 {
return Err(Error::InsufficientData);
}
if treatments.iter().any(|t| t.len() != n) {
return Err(Error::InvalidInput(
"treatments differ in subject count".to_owned(),
));
}
let mut rank_sums = vec![0.0; k];
let mut tie_sum = 0.0;
for subject in 0..n {
let row: Vec<f64> = treatments
.iter()
.map(|t| t.get(subject).copied().unwrap_or(f64::NAN))
.collect();
let ranks = mid_ranks(&row);
tie_sum += tie_correction(&row);
for (j, r) in ranks.iter().enumerate() {
if let Some(slot) = rank_sums.get_mut(j) {
*slot += r;
}
}
}
let n_f = len_f64(n);
let k_f = len_f64(k);
let sum_sq: f64 = rank_sums.iter().map(|&r| r * r).sum();
let uncorrected =
(12.0 / (n_f * k_f * (k_f + 1.0))).mul_add(sum_sq, -(3.0 * n_f * (k_f + 1.0)));
let correction = 1.0 - tie_sum / (n_f * (k_f * k_f).mul_add(k_f, -k_f));
let statistic = if correction > 0.0 {
uncorrected / correction
} else {
uncorrected
};
let df_int = i64::try_from(k - 1).unwrap_or(i64::MAX);
let p_value = chi_squared_upper_tail(statistic, df_int);
let kendalls_w = statistic / (n_f * (k_f - 1.0));
Ok(TestResult {
statistic,
p_value,
log_p_value: Some(chi_squared_upper_log_tail(statistic, df_int)),
df: Some(k_f - 1.0),
effect_size: Some(kendalls_w),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_treatments_are_insufficient() {
let a = [1.0, 2.0];
let b = [3.0, 4.0];
assert!(matches!(
friedman(&[&a[..], &b[..]]),
Err(Error::InsufficientData)
));
}
#[test]
fn consistent_ranking_has_high_w() -> Result<()> {
let t1 = [1.0, 1.0, 1.0];
let t2 = [2.0, 2.0, 2.0];
let t3 = [3.0, 3.0, 3.0];
let r = friedman(&[&t1[..], &t2[..], &t3[..]])?;
assert!(
r.effect_size.is_some_and(|w| (w - 1.0).abs() < 1e-9),
"W was {:?}",
r.effect_size
);
Ok(())
}
}