1use crate::srange::{srange_ppf, srange_sf};
10
11#[derive(Debug, Clone)]
13pub struct TukeyHsdResult {
14 pub pairs: Vec<(usize, usize)>,
16 pub meandiffs: Vec<f64>,
18 pub std_pairs: Vec<f64>,
20 pub confint: Vec<(f64, f64)>,
22 pub pvalues: Vec<f64>,
24 pub reject: Vec<bool>,
26 pub q_crit: f64,
28 pub df_total: f64,
30 pub variance: f64,
32}
33
34pub fn pairwise_tukeyhsd(data: &[f64], groups: &[usize], alpha: f64) -> TukeyHsdResult {
40 assert_eq!(data.len(), groups.len(), "data and groups length mismatch");
41
42 let mut labels: Vec<usize> = groups.to_vec();
44 labels.sort_unstable();
45 labels.dedup();
46 let k = labels.len();
47
48 let mut nobs = vec![0.0_f64; k];
50 let mut sums = vec![0.0_f64; k];
51 let label_idx = |g: usize| labels.iter().position(|&l| l == g).unwrap();
52 for (&v, &g) in data.iter().zip(groups) {
53 let gi = label_idx(g);
54 nobs[gi] += 1.0;
55 sums[gi] += v;
56 }
57 let means: Vec<f64> = sums.iter().zip(&nobs).map(|(&s, &n)| s / n).collect();
58
59 let total_n: f64 = nobs.iter().sum();
61 let mut ss_within = 0.0;
62 for (&v, &g) in data.iter().zip(groups) {
63 let gi = label_idx(g);
64 let d = v - means[gi];
65 ss_within += d * d;
66 }
67 let df_total = total_n - k as f64;
68 let variance = ss_within / df_total;
69
70 let mut pairs = Vec::new();
72 let mut meandiffs = Vec::new();
73 let mut std_pairs = Vec::new();
74 for i in 0..k {
75 for j in (i + 1)..k {
76 pairs.push((i, j));
77 meandiffs.push(means[j] - means[i]);
79 let var_pair = variance * (1.0 / nobs[i] + 1.0 / nobs[j]) / 2.0;
81 std_pairs.push(var_pair.sqrt());
82 }
83 }
84
85 let kf = k as f64;
86 let q_crit = srange_ppf(1.0 - alpha, kf, df_total);
87
88 let mut pvalues = Vec::with_capacity(pairs.len());
89 let mut reject = Vec::with_capacity(pairs.len());
90 let mut confint = Vec::with_capacity(pairs.len());
91 for idx in 0..pairs.len() {
92 let st_range = meandiffs[idx].abs() / std_pairs[idx];
93 let pval = srange_sf(st_range, kf, df_total);
94 pvalues.push(pval);
95 reject.push(st_range > q_crit);
96 let crit_int = std_pairs[idx] * q_crit;
97 confint.push((meandiffs[idx] - crit_int, meandiffs[idx] + crit_int));
98 }
99
100 TukeyHsdResult {
101 pairs,
102 meandiffs,
103 std_pairs,
104 confint,
105 pvalues,
106 reject,
107 q_crit,
108 df_total,
109 variance,
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn three_groups_pairs() {
119 let data = [1.0, 2.0, 3.0, 5.0, 6.0, 4.0, 8.0, 9.0, 7.0, 10.0];
120 let groups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 2];
121 let res = pairwise_tukeyhsd(&data, &groups, 0.05);
122 assert_eq!(res.pairs.len(), 3);
123 assert_eq!(res.df_total, 7.0);
124 assert!(res.reject.iter().all(|&r| r));
126 }
127}