use crate::error::{Error, Result};
use crate::tests_stat::parametric::len_f64;
use crate::tests_stat::{TestResult, chi_squared_upper_log_tail, chi_squared_upper_tail};
pub fn cochrans_q(rows: &[&[f64]]) -> Result<TestResult> {
let n = rows.len();
if n < 2 {
return Err(Error::InsufficientData);
}
let k = rows.first().map_or(0, |r| r.len());
if k < 2 {
return Err(Error::InsufficientData);
}
if rows.iter().any(|r| r.len() != k) {
return Err(Error::InvalidInput(
"subjects differ in treatment count".to_owned(),
));
}
let mut col_sums = vec![0.0; k];
let mut sum_row_sq = 0.0;
let mut grand = 0.0;
for row in rows {
let row_sum: f64 = row.iter().sum();
sum_row_sq = row_sum.mul_add(row_sum, sum_row_sq);
grand += row_sum;
for (j, &v) in row.iter().enumerate() {
if let Some(slot) = col_sums.get_mut(j) {
*slot += v;
}
}
}
let sum_col_sq: f64 = col_sums.iter().map(|&g| g * g).sum();
let k_f = len_f64(k);
let denom = k_f.mul_add(grand, -sum_row_sq);
let statistic = if denom > 0.0 {
(k_f - 1.0) * k_f.mul_add(sum_col_sq, -(grand * grand)) / denom
} else {
0.0
};
let df_int = i64::try_from(k - 1).unwrap_or(i64::MAX);
let p_value = chi_squared_upper_tail(statistic, df_int);
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: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_subject_is_insufficient() {
let row = [1.0, 0.0, 1.0];
assert!(matches!(
cochrans_q(&[&row[..]]),
Err(Error::InsufficientData)
));
}
#[test]
fn identical_responses_are_zero_q() -> Result<()> {
let r1 = [1.0, 1.0, 1.0];
let r2 = [0.0, 0.0, 0.0];
let r = cochrans_q(&[&r1[..], &r2[..]])?;
assert!(r.statistic.abs() < 1e-12, "Q was {}", r.statistic);
Ok(())
}
}