use crate::srange::{srange_ppf, srange_sf};
#[derive(Debug, Clone)]
pub struct TukeyHsdResult {
pub pairs: Vec<(usize, usize)>,
pub meandiffs: Vec<f64>,
pub std_pairs: Vec<f64>,
pub confint: Vec<(f64, f64)>,
pub pvalues: Vec<f64>,
pub reject: Vec<bool>,
pub q_crit: f64,
pub df_total: f64,
pub variance: f64,
}
pub fn pairwise_tukeyhsd(data: &[f64], groups: &[usize], alpha: f64) -> TukeyHsdResult {
assert_eq!(data.len(), groups.len(), "data and groups length mismatch");
let mut labels: Vec<usize> = groups.to_vec();
labels.sort_unstable();
labels.dedup();
let k = labels.len();
let mut nobs = vec![0.0_f64; k];
let mut sums = vec![0.0_f64; k];
let label_idx = |g: usize| labels.iter().position(|&l| l == g).unwrap();
for (&v, &g) in data.iter().zip(groups) {
let gi = label_idx(g);
nobs[gi] += 1.0;
sums[gi] += v;
}
let means: Vec<f64> = sums.iter().zip(&nobs).map(|(&s, &n)| s / n).collect();
let total_n: f64 = nobs.iter().sum();
let mut ss_within = 0.0;
for (&v, &g) in data.iter().zip(groups) {
let gi = label_idx(g);
let d = v - means[gi];
ss_within += d * d;
}
let df_total = total_n - k as f64;
let variance = ss_within / df_total;
let mut pairs = Vec::new();
let mut meandiffs = Vec::new();
let mut std_pairs = Vec::new();
for i in 0..k {
for j in (i + 1)..k {
pairs.push((i, j));
meandiffs.push(means[j] - means[i]);
let var_pair = variance * (1.0 / nobs[i] + 1.0 / nobs[j]) / 2.0;
std_pairs.push(var_pair.sqrt());
}
}
let kf = k as f64;
let q_crit = srange_ppf(1.0 - alpha, kf, df_total);
let mut pvalues = Vec::with_capacity(pairs.len());
let mut reject = Vec::with_capacity(pairs.len());
let mut confint = Vec::with_capacity(pairs.len());
for idx in 0..pairs.len() {
let st_range = meandiffs[idx].abs() / std_pairs[idx];
let pval = srange_sf(st_range, kf, df_total);
pvalues.push(pval);
reject.push(st_range > q_crit);
let crit_int = std_pairs[idx] * q_crit;
confint.push((meandiffs[idx] - crit_int, meandiffs[idx] + crit_int));
}
TukeyHsdResult {
pairs,
meandiffs,
std_pairs,
confint,
pvalues,
reject,
q_crit,
df_total,
variance,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn three_groups_pairs() {
let data = [1.0, 2.0, 3.0, 5.0, 6.0, 4.0, 8.0, 9.0, 7.0, 10.0];
let groups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 2];
let res = pairwise_tukeyhsd(&data, &groups, 0.05);
assert_eq!(res.pairs.len(), 3);
assert_eq!(res.df_total, 7.0);
assert!(res.reject.iter().all(|&r| r));
}
}