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