1use solow_core::{Error, Result};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct VarianceTestResult {
13 pub statistic: f64,
15 pub pvalue: f64,
17 pub df: (f64, f64),
19}
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum LeveneCenter {
24 Mean,
26 Median,
28}
29
30pub fn levene(groups: &[Vec<f64>], center: LeveneCenter) -> Result<VarianceTestResult> {
32 if groups.len() < 2 {
33 return Err(Error::Value("levene: need ≥ 2 groups".into()));
34 }
35 let k = groups.len();
36 let n_total: usize = groups.iter().map(|g| g.len()).sum();
37 if n_total < k + 1 {
38 return Err(Error::Value("levene: too few samples".into()));
39 }
40 let mut group_z: Vec<Vec<f64>> = Vec::with_capacity(k);
41 for g in groups {
42 let c = match center {
43 LeveneCenter::Mean => g.iter().sum::<f64>() / g.len() as f64,
44 LeveneCenter::Median => median(g),
45 };
46 group_z.push(g.iter().map(|v| (v - c).abs()).collect());
47 }
48 let grand: f64 = group_z.iter().flatten().sum::<f64>() / n_total as f64;
50 let mut ss_between = 0.0_f64;
51 let mut ss_within = 0.0_f64;
52 for g in &group_z {
53 let mg: f64 = g.iter().sum::<f64>() / g.len() as f64;
54 ss_between += g.len() as f64 * (mg - grand).powi(2);
55 for &v in g {
56 ss_within += (v - mg).powi(2);
57 }
58 }
59 let dfn = (k - 1) as f64;
60 let dfd = (n_total - k) as f64;
61 let f = (ss_between / dfn) / (ss_within / dfd).max(1e-300);
62 let pvalue = f_survival(f, dfn, dfd);
63 Ok(VarianceTestResult { statistic: f, pvalue, df: (dfn, dfd) })
64}
65
66pub fn bartlett(groups: &[Vec<f64>]) -> Result<VarianceTestResult> {
69 if groups.len() < 2 {
70 return Err(Error::Value("bartlett: need ≥ 2 groups".into()));
71 }
72 let k = groups.len() as f64;
73 let mut ni = Vec::with_capacity(groups.len());
74 let mut si2 = Vec::with_capacity(groups.len());
75 for g in groups {
76 if g.len() < 2 {
77 return Err(Error::Value("bartlett: each group must have ≥ 2 samples".into()));
78 }
79 let n = g.len() as f64;
80 let m = g.iter().sum::<f64>() / n;
81 let v = g.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (n - 1.0);
82 ni.push(n);
83 si2.push(v);
84 }
85 let n_total: f64 = ni.iter().sum();
86 let sp2: f64 =
87 ni.iter().zip(si2.iter()).map(|(n, v)| (n - 1.0) * v).sum::<f64>() / (n_total - k);
88 let numer = (n_total - k) * sp2.ln()
89 - ni.iter().zip(si2.iter()).map(|(n, v)| (n - 1.0) * v.ln()).sum::<f64>();
90 let one_over_n_minus_1: f64 = ni.iter().map(|n| 1.0 / (n - 1.0)).sum();
91 let one_over_total: f64 = 1.0 / (n_total - k);
92 let c = 1.0 + 1.0 / (3.0 * (k - 1.0)) * (one_over_n_minus_1 - one_over_total);
93 let chi2 = numer / c;
94 let dfn = k - 1.0;
95 let pvalue = chi2_survival(chi2, dfn);
96 Ok(VarianceTestResult { statistic: chi2, pvalue, df: (dfn, 0.0) })
97}
98
99pub fn fligner(groups: &[Vec<f64>]) -> Result<VarianceTestResult> {
101 if groups.len() < 2 {
102 return Err(Error::Value("fligner: need ≥ 2 groups".into()));
103 }
104 let n_total: usize = groups.iter().map(|g| g.len()).sum();
105 let mut deviations: Vec<(f64, usize)> = Vec::with_capacity(n_total);
107 for (i, g) in groups.iter().enumerate() {
108 let m = median(g);
109 for &v in g {
110 deviations.push(((v - m).abs(), i));
111 }
112 }
113 deviations.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
114 let mut a_scores = vec![0.0_f64; n_total];
116 for i in 0..n_total {
117 let rank = (i + 1) as f64;
118 let quantile = 0.5 * (rank / (n_total as f64 + 1.0) + 1.0);
119 a_scores[i] = inv_normal_cdf(quantile);
120 }
121 let mean_a: f64 = a_scores.iter().sum::<f64>() / n_total as f64;
122 let var_a: f64 = a_scores.iter().map(|a| (a - mean_a).powi(2)).sum::<f64>() / n_total as f64;
123 let mut group_sum = vec![0.0_f64; groups.len()];
125 let mut group_n = vec![0.0_f64; groups.len()];
126 for (i, (_, gi)) in deviations.iter().enumerate() {
127 group_sum[*gi] += a_scores[i];
128 group_n[*gi] += 1.0;
129 }
130 let mut chi2 = 0.0_f64;
131 for j in 0..groups.len() {
132 let mj = group_sum[j] / group_n[j];
133 chi2 += group_n[j] * (mj - mean_a).powi(2);
134 }
135 chi2 /= var_a.max(1e-300);
136 let dfn = (groups.len() - 1) as f64;
137 let pvalue = chi2_survival(chi2, dfn);
138 Ok(VarianceTestResult { statistic: chi2, pvalue, df: (dfn, 0.0) })
139}
140
141fn median(x: &[f64]) -> f64 {
142 let mut v: Vec<f64> = x.to_vec();
143 v.sort_by(|a, b| a.partial_cmp(b).unwrap());
144 let n = v.len();
145 if n % 2 == 0 {
146 0.5 * (v[n / 2 - 1] + v[n / 2])
147 } else {
148 v[n / 2]
149 }
150}
151
152fn inv_normal_cdf(p: f64) -> f64 {
153 let a = [
155 -3.969_683_028_665_376e1,
156 2.209_460_984_245_205e2,
157 -2.759_285_104_469_687e2,
158 1.383_577_518_672_69e2,
159 -3.066_479_806_614_716e1,
160 2.506_628_277_459_239,
161 ];
162 let b = [
163 -5.447_609_879_822_406e1,
164 1.615_858_368_580_409e2,
165 -1.556_989_798_598_866e2,
166 6.680_131_188_771_972e1,
167 -1.328_068_155_288_572e1,
168 ];
169 let c = [
170 -7.784_894_002_430_293e-3,
171 -3.223_964_580_411_365e-1,
172 -2.400_758_277_161_838,
173 -2.549_732_539_343_734,
174 4.374_664_141_464_968,
175 2.938_163_982_698_783,
176 ];
177 let d = [
178 7.784_695_709_041_462e-3,
179 3.224_671_290_700_398e-1,
180 2.445_134_137_142_996,
181 3.754_408_661_907_416,
182 ];
183 let p_low = 0.02425;
184 let p_high = 1.0 - p_low;
185 if p < p_low {
186 let q = (-2.0 * p.ln()).sqrt();
187 return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
188 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0);
189 }
190 if p <= p_high {
191 let q = p - 0.5;
192 let r = q * q;
193 return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
194 / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0);
195 }
196 let q = (-2.0 * (1.0 - p).ln()).sqrt();
197 -((((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
198 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0))
199}
200
201fn f_survival(f: f64, d1: f64, d2: f64) -> f64 {
202 if f <= 0.0 {
203 return 1.0;
204 }
205 let x = d2 / (d2 + d1 * f);
206 regularised_incomplete_beta(x, d2 / 2.0, d1 / 2.0)
207}
208
209fn chi2_survival(x: f64, df: f64) -> f64 {
210 if x <= 0.0 {
211 return 1.0;
212 }
213 1.0 - lower_regularised_gamma(df / 2.0, x / 2.0)
214}
215
216fn lower_regularised_gamma(s: f64, x: f64) -> f64 {
217 if x < 0.0 || s <= 0.0 {
218 return 0.0;
219 }
220 if x < s + 1.0 {
221 gamma_series(s, x)
222 } else {
223 1.0 - gamma_continued_fraction(s, x)
224 }
225}
226
227fn gamma_series(s: f64, x: f64) -> f64 {
228 let mut sum = 1.0 / s;
229 let mut term = sum;
230 for n in 1..200 {
231 term *= x / (s + n as f64);
232 sum += term;
233 if term.abs() < sum.abs() * 3e-15 {
234 break;
235 }
236 }
237 sum * (-x + s * x.ln() - ln_gamma(s)).exp()
238}
239
240fn gamma_continued_fraction(s: f64, x: f64) -> f64 {
241 let mut b = x + 1.0 - s;
242 let mut c = 1.0 / 1e-300;
243 let mut d = 1.0 / b;
244 let mut h = d;
245 for i in 1..200 {
246 let an = -(i as f64) * (i as f64 - s);
247 b += 2.0;
248 d = an * d + b;
249 if d.abs() < 1e-300 {
250 d = 1e-300;
251 }
252 c = b + an / c;
253 if c.abs() < 1e-300 {
254 c = 1e-300;
255 }
256 d = 1.0 / d;
257 let delta = d * c;
258 h *= delta;
259 if (delta - 1.0).abs() < 3e-15 {
260 break;
261 }
262 }
263 (-x + s * x.ln() - ln_gamma(s)).exp() * h
264}
265
266fn regularised_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
267 if x <= 0.0 {
268 return 0.0;
269 }
270 if x >= 1.0 {
271 return 1.0;
272 }
273 let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
274 let front = ((a * x.ln() + b * (1.0 - x).ln()) - ln_beta).exp() / a;
275 if x < (a + 1.0) / (a + b + 2.0) {
276 front * betacf(x, a, b)
277 } else {
278 1.0 - front * betacf(1.0 - x, b, a)
279 }
280}
281
282fn betacf(x: f64, a: f64, b: f64) -> f64 {
283 let mut c = 1.0_f64;
284 let qab = a + b;
285 let qap = a + 1.0;
286 let qam = a - 1.0;
287 let mut d = 1.0 - qab * x / qap;
288 if d.abs() < 1e-300 {
289 d = 1e-300;
290 }
291 d = 1.0 / d;
292 let mut h = d;
293 for m in 1..200 {
294 let mf = m as f64;
295 let two_m = 2.0 * mf;
296 let mut aa = mf * (b - mf) * x / ((qam + two_m) * (a + two_m));
297 d = 1.0 + aa * d;
298 if d.abs() < 1e-300 {
299 d = 1e-300;
300 }
301 c = 1.0 + aa / c;
302 if c.abs() < 1e-300 {
303 c = 1e-300;
304 }
305 d = 1.0 / d;
306 h *= d * c;
307 aa = -(a + mf) * (qab + mf) * x / ((a + two_m) * (qap + two_m));
308 d = 1.0 + aa * d;
309 if d.abs() < 1e-300 {
310 d = 1e-300;
311 }
312 c = 1.0 + aa / c;
313 if c.abs() < 1e-300 {
314 c = 1e-300;
315 }
316 d = 1.0 / d;
317 let delta = d * c;
318 h *= delta;
319 if (delta - 1.0).abs() < 3e-15 {
320 break;
321 }
322 }
323 h
324}
325
326fn ln_gamma(x: f64) -> f64 {
327 let g = 7.0;
328 let cof = [
329 0.999_999_999_999_809_93,
330 676.520_368_121_885_1,
331 -1_259.139_216_722_402_8,
332 771.323_428_777_653_13,
333 -176.615_029_162_140_59,
334 12.507_343_278_686_905,
335 -0.138_571_095_265_720_12,
336 9.984_369_578_019_571_5e-6,
337 1.505_632_735_149_311_6e-7,
338 ];
339 if x < 0.5 {
340 std::f64::consts::PI.ln() - (std::f64::consts::PI * x).sin().ln() - ln_gamma(1.0 - x)
341 } else {
342 let x = x - 1.0;
343 let mut a = cof[0];
344 let t = x + g + 0.5;
345 for (i, &c) in cof.iter().enumerate().skip(1) {
346 a += c / (x + i as f64);
347 }
348 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn levene_detects_variance_difference_between_two_groups() {
358 let a = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
359 let b = vec![10.0_f64, 100.0, 200.0, 300.0, 400.0];
360 let r = levene(&[a, b], LeveneCenter::Median).unwrap();
361 assert!(r.pvalue < 0.1);
362 }
363
364 #[test]
365 fn bartlett_detects_variance_difference() {
366 let a = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
367 let b = vec![10.0_f64, 100.0, 200.0, 300.0, 400.0];
368 let r = bartlett(&[a, b]).unwrap();
369 assert!(r.pvalue < 0.1);
370 }
371
372 #[test]
373 fn fligner_detects_variance_difference() {
374 let a = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0];
375 let b = vec![10.0_f64, 100.0, 200.0, 300.0, 400.0, 500.0];
376 let r = fligner(&[a, b]).unwrap();
377 assert!(r.pvalue < 0.1);
378 }
379}