1use solow_distributions::f_sf;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum UseVar {
13 Equal,
15 Unequal,
17 BrownForsythe,
19}
20
21#[derive(Debug, Clone, Copy)]
23pub struct OnewayResult {
24 pub statistic: f64,
26 pub pvalue: f64,
28 pub df_num: f64,
30 pub df_denom: f64,
32}
33
34fn mean(x: &[f64]) -> f64 {
36 x.iter().sum::<f64>() / x.len() as f64
37}
38
39fn var_unbiased(x: &[f64]) -> f64 {
41 let n = x.len() as f64;
42 let m = mean(x);
43 x.iter().map(|&v| (v - m) * (v - m)).sum::<f64>() / (n - 1.0)
44}
45
46pub fn anova_generic(
48 means: &[f64],
49 variances: &[f64],
50 nobs: &[f64],
51 use_var: UseVar,
52 welch_correction: bool,
53) -> OnewayResult {
54 let n_groups = means.len();
55 let ng = n_groups as f64;
56 let nobs_t: f64 = nobs.iter().sum();
57
58 let weights: Vec<f64> = match use_var {
60 UseVar::Unequal => nobs.iter().zip(variances).map(|(&n, &v)| n / v).collect(),
61 UseVar::Equal | UseVar::BrownForsythe => nobs.to_vec(),
62 };
63 let w_total: f64 = weights.iter().sum();
64 let w_rel: Vec<f64> = weights.iter().map(|&w| w / w_total).collect();
65 let meanw_t: f64 = w_rel.iter().zip(means).map(|(&w, &m)| w * m).sum();
66
67 let mut statistic: f64 = weights
68 .iter()
69 .zip(means)
70 .map(|(&w, &m)| w * (m - meanw_t) * (m - meanw_t))
71 .sum::<f64>()
72 / (ng - 1.0);
73 let mut df_num = ng - 1.0;
74 let df_denom;
75
76 match use_var {
77 UseVar::Unequal => {
78 let tmp: f64 = w_rel
79 .iter()
80 .zip(nobs)
81 .map(|(&wr, &n)| (1.0 - wr) * (1.0 - wr) / (n - 1.0))
82 .sum::<f64>()
83 / (ng * ng - 1.0);
84 if welch_correction {
85 statistic /= 1.0 + 2.0 * (ng - 2.0) * tmp;
86 }
87 df_denom = 1.0 / (3.0 * tmp);
88 }
89 UseVar::Equal => {
90 let tmp: f64 = nobs
91 .iter()
92 .zip(variances)
93 .map(|(&n, &v)| (n - 1.0) * v)
94 .sum::<f64>()
95 / (nobs_t - ng);
96 statistic /= tmp;
97 df_denom = nobs_t - ng;
98 }
99 UseVar::BrownForsythe => {
100 let tmp: f64 = nobs
101 .iter()
102 .zip(variances)
103 .map(|(&n, &v)| (1.0 - n / nobs_t) * v)
104 .sum();
105 statistic = nobs
106 .iter()
107 .zip(means)
108 .map(|(&n, &m)| n * (m - meanw_t) * (m - meanw_t))
109 .sum::<f64>()
110 / tmp;
111 df_denom = tmp * tmp
112 / nobs
113 .iter()
114 .zip(variances)
115 .map(|(&n, &v)| {
116 let f = 1.0 - n / nobs_t;
117 f * f * v * v / (n - 1.0)
118 })
119 .sum::<f64>();
120 let sum_v2: f64 = variances.iter().map(|&v| v * v).sum();
122 let sum_nv: f64 = nobs
123 .iter()
124 .zip(variances)
125 .map(|(&n, &v)| n / nobs_t * v)
126 .sum();
127 let sum_nv2: f64 = nobs
128 .iter()
129 .zip(variances)
130 .map(|(&n, &v)| n / nobs_t * v * v)
131 .sum();
132 df_num = tmp * tmp / (sum_v2 + sum_nv * sum_nv - 2.0 * sum_nv2);
133 }
134 }
135
136 let pvalue = f_sf(statistic, df_num, df_denom);
137 OnewayResult {
138 statistic,
139 pvalue,
140 df_num,
141 df_denom,
142 }
143}
144
145pub fn anova_oneway(groups: &[Vec<f64>], use_var: UseVar, welch_correction: bool) -> OnewayResult {
147 let means: Vec<f64> = groups.iter().map(|g| mean(g)).collect();
148 let variances: Vec<f64> = groups.iter().map(|g| var_unbiased(g)).collect();
149 let nobs: Vec<f64> = groups.iter().map(|g| g.len() as f64).collect();
150 anova_generic(&means, &variances, &nobs, use_var, welch_correction)
151}
152
153pub fn f_oneway(groups: &[Vec<f64>]) -> (f64, f64) {
158 let res = anova_oneway(groups, UseVar::Equal, true);
159 (res.statistic, res.pvalue)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn equal_variance_matches_scipy_form() {
168 let groups = vec![
169 vec![1.0, 2.0, 3.0],
170 vec![4.0, 5.0, 6.0],
171 vec![7.0, 8.0, 9.0],
172 ];
173 let (f, p) = f_oneway(&groups);
174 assert!(f > 0.0);
175 assert!((0.0..=1.0).contains(&p));
176 }
177
178 #[test]
179 fn df_num_is_k_minus_one() {
180 let groups = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
181 let res = anova_oneway(&groups, UseVar::Equal, true);
182 assert_eq!(res.df_num, 2.0);
183 }
184}