1use u_numflow::special;
17use u_numflow::stats;
18
19#[derive(Debug, Clone, Copy)]
21pub struct TestResult {
22 pub statistic: f64,
24 pub df: f64,
26 pub p_value: f64,
28}
29
30pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
54 let n = data.len();
55 if n < 2 {
56 return None;
57 }
58 if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
59 return None;
60 }
61
62 let mean = stats::mean(data)?;
63 let sd = stats::std_dev(data)?;
64
65 if sd < 1e-300 {
66 return None; }
68
69 let t = (mean - mu0) / (sd / (n as f64).sqrt());
70 let df = (n - 1) as f64;
71 let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
72
73 Some(TestResult {
74 statistic: t,
75 df,
76 p_value,
77 })
78}
79
80pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
107 let n1 = a.len();
108 let n2 = b.len();
109 if n1 < 2 || n2 < 2 {
110 return None;
111 }
112 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
113 return None;
114 }
115
116 let mean1 = stats::mean(a)?;
117 let mean2 = stats::mean(b)?;
118 let var1 = stats::variance(a)?;
119 let var2 = stats::variance(b)?;
120
121 let n1f = n1 as f64;
122 let n2f = n2 as f64;
123
124 let se_sq = var1 / n1f + var2 / n2f;
125 if se_sq < 1e-300 {
126 return None;
127 }
128
129 let t = (mean1 - mean2) / se_sq.sqrt();
130
131 let v1 = var1 / n1f;
133 let v2 = var2 / n2f;
134 let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
135
136 let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
137
138 Some(TestResult {
139 statistic: t,
140 df,
141 p_value,
142 })
143}
144
145pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
167 if x.len() != y.len() || x.len() < 2 {
168 return None;
169 }
170
171 let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
172 one_sample_t_test(&diffs, 0.0)
173}
174
175#[derive(Debug, Clone)]
181pub struct AnovaResult {
182 pub f_statistic: f64,
184 pub df_between: usize,
186 pub df_within: usize,
188 pub p_value: f64,
190 pub ss_between: f64,
192 pub ss_within: f64,
194 pub ms_between: f64,
196 pub ms_within: f64,
198 pub group_means: Vec<f64>,
200 pub grand_mean: f64,
202}
203
204pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
233 let k = groups.len();
234 if k < 2 {
235 return None;
236 }
237
238 for g in groups {
239 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
240 return None;
241 }
242 }
243
244 let total_n: usize = groups.iter().map(|g| g.len()).sum();
245
246 let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
248 let grand_mean = grand_sum / total_n as f64;
249
250 let group_means: Vec<f64> = groups
252 .iter()
253 .map(|g| g.iter().sum::<f64>() / g.len() as f64)
254 .collect();
255
256 let ss_between: f64 = groups
258 .iter()
259 .zip(group_means.iter())
260 .map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
261 .sum();
262
263 let ss_within: f64 = groups
264 .iter()
265 .zip(group_means.iter())
266 .map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
267 .sum();
268
269 let df_between = k - 1;
270 let df_within = total_n - k;
271
272 if df_within == 0 {
273 return None;
274 }
275
276 let ms_between = ss_between / df_between as f64;
277 let ms_within = ss_within / df_within as f64;
278
279 let f_statistic = if ms_within > 1e-300 {
280 ms_between / ms_within
281 } else {
282 f64::INFINITY
283 };
284
285 let p_value = if f_statistic.is_infinite() {
286 0.0
287 } else {
288 1.0 - special::f_distribution_cdf(f_statistic, df_between as f64, df_within as f64)
289 };
290
291 Some(AnovaResult {
292 f_statistic,
293 df_between,
294 df_within,
295 p_value,
296 ss_between,
297 ss_within,
298 ms_between,
299 ms_within,
300 group_means,
301 grand_mean,
302 })
303}
304
305pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
331 let k = observed.len();
332 if k < 2 || k != expected.len() {
333 return None;
334 }
335
336 for &e in expected {
337 if e <= 0.0 || !e.is_finite() {
338 return None;
339 }
340 }
341 for &o in observed {
342 if o < 0.0 || !o.is_finite() {
343 return None;
344 }
345 }
346
347 let chi2: f64 = observed
348 .iter()
349 .zip(expected.iter())
350 .map(|(&o, &e)| (o - e).powi(2) / e)
351 .sum();
352
353 let df = (k - 1) as f64;
354 let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
355
356 Some(TestResult {
357 statistic: chi2,
358 df,
359 p_value,
360 })
361}
362
363pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
392 if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
393 return None;
394 }
395
396 for &v in table {
397 if v < 0.0 || !v.is_finite() {
398 return None;
399 }
400 }
401
402 let mut row_sums = vec![0.0; n_rows];
404 let mut col_sums = vec![0.0; n_cols];
405 let mut total = 0.0;
406
407 for i in 0..n_rows {
408 for j in 0..n_cols {
409 let val = table[i * n_cols + j];
410 row_sums[i] += val;
411 col_sums[j] += val;
412 total += val;
413 }
414 }
415
416 if total <= 0.0 {
417 return None;
418 }
419
420 for &r in &row_sums {
422 if r <= 0.0 {
423 return None;
424 }
425 }
426 for &c in &col_sums {
427 if c <= 0.0 {
428 return None;
429 }
430 }
431
432 let mut chi2 = 0.0;
434 for i in 0..n_rows {
435 for j in 0..n_cols {
436 let observed = table[i * n_cols + j];
437 let expected = row_sums[i] * col_sums[j] / total;
438 chi2 += (observed - expected).powi(2) / expected;
439 }
440 }
441
442 let df = ((n_rows - 1) * (n_cols - 1)) as f64;
443 let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
444
445 Some(TestResult {
446 statistic: chi2,
447 df,
448 p_value,
449 })
450}
451
452pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
484 let n = data.len();
485 if n < 8 {
486 return None;
487 }
488 if data.iter().any(|v| !v.is_finite()) {
489 return None;
490 }
491
492 let s = stats::skewness(data)?;
493 let k = stats::kurtosis(data)?;
494
495 let nf = n as f64;
496 let jb = (nf / 6.0) * (s * s + k * k / 4.0);
497 let p_value = 1.0 - special::chi_squared_cdf(jb, 2.0);
498
499 Some(TestResult {
500 statistic: jb,
501 df: 2.0,
502 p_value,
503 })
504}
505
506#[derive(Debug, Clone, Copy)]
508pub struct AndersonDarlingResult {
509 pub statistic: f64,
511 pub statistic_star: f64,
513 pub p_value: f64,
515}
516
517pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
550 let n = data.len();
551 if n < 8 {
552 return None;
553 }
554 if data.iter().any(|v| !v.is_finite()) {
555 return None;
556 }
557
558 let mean = stats::mean(data)?;
559 let sd = stats::std_dev(data)?;
560
561 if sd < 1e-300 {
562 return None; }
564
565 let mut x: Vec<f64> = data.to_vec();
567 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
568
569 let nf = n as f64;
570
571 let mut s = 0.0;
573 for i in 0..n {
574 let z = (x[i] - mean) / sd;
575 let phi = special::standard_normal_cdf(z);
576 let phi = phi.clamp(1e-15, 1.0 - 1e-15);
578
579 let z_rev = (x[n - 1 - i] - mean) / sd;
580 let phi_rev = special::standard_normal_cdf(z_rev);
581 let phi_rev = phi_rev.clamp(1e-15, 1.0 - 1e-15);
582
583 let coeff = (2 * (i + 1) - 1) as f64;
584 s += coeff * (phi.ln() + (1.0 - phi_rev).ln());
585 }
586
587 let a2 = -nf - s / nf;
588
589 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
591
592 let p = if a2_star >= 0.6 {
594 (1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
595 } else if a2_star > 0.34 {
596 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
597 } else if a2_star > 0.2 {
598 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
599 } else {
600 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
601 };
602
603 Some(AndersonDarlingResult {
604 statistic: a2,
605 statistic_star: a2_star,
606 p_value: p.clamp(0.0, 1.0),
607 })
608}
609
610#[derive(Debug, Clone, Copy)]
616pub struct AdNormalityResult {
617 pub statistic: f64,
619 pub statistic_modified: f64,
621 pub p_value: f64,
623}
624
625pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
659 let n = data.len();
660 if n < 3 {
661 return None;
662 }
663 if data.iter().any(|v| !v.is_finite()) {
664 return None;
665 }
666
667 let mean = stats::mean(data)?;
668 let sd = stats::std_dev(data)?;
669
670 if sd < 1e-15 {
671 return None;
672 }
673
674 let mut x: Vec<f64> = data.to_vec();
675 x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
676
677 let nf = n as f64;
678
679 let mut s = 0.0;
680 for i in 0..n {
681 let z_i = (x[i] - mean) / sd;
682 let z_rev = (x[n - 1 - i] - mean) / sd;
683
684 let phi_i = special::standard_normal_cdf(z_i).clamp(1e-15, 1.0 - 1e-15);
685 let phi_rev = special::standard_normal_cdf(z_rev).clamp(1e-15, 1.0 - 1e-15);
686
687 let coeff = (2 * i + 1) as f64;
688 s += coeff * (phi_i.ln() + (1.0 - phi_rev).ln());
689 }
690
691 let a2 = -nf - s / nf;
692 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
693
694 let p = if a2_star < 0.200 {
696 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
697 } else if a2_star < 0.340 {
698 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
699 } else if a2_star < 0.600 {
700 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
701 } else {
702 (1.2937 - 5.709 * a2_star + 0.0186 * a2_star * a2_star).exp()
703 };
704
705 Some(AdNormalityResult {
706 statistic: a2,
707 statistic_modified: a2_star,
708 p_value: p.clamp(0.0, 1.0),
709 })
710}
711
712#[derive(Debug, Clone, Copy)]
714pub struct ShapiroWilkResult {
715 pub w: f64,
717 pub p_value: f64,
719}
720
721pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
761 let n = data.len();
762 if !(3..=5000).contains(&n) {
763 return None;
764 }
765 if data.iter().any(|v| !v.is_finite()) {
766 return None;
767 }
768
769 let mut x: Vec<f64> = data.to_vec();
771 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
772
773 if x[n - 1] - x[0] < 1e-300 {
774 return None; }
776
777 let nn2 = n / 2;
778
779 if n == 3 {
781 return shapiro_wilk_n3(&x);
782 }
783
784 let a = sw_coefficients(n, nn2)?;
786
787 let w = sw_statistic(&x, &a, n, nn2);
789
790 if !(0.0..=1.0 + 1e-10).contains(&w) {
791 return None;
792 }
793 let w = w.min(1.0);
794
795 let p_value = sw_p_value(w, n);
797
798 Some(ShapiroWilkResult {
799 w,
800 p_value: p_value.clamp(0.0, 1.0),
801 })
802}
803
804fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
806 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
809 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
810 if ss < 1e-300 {
811 return None;
812 }
813
814 let numerator = a1 * (x[2] - x[0]);
815 let w = (numerator * numerator) / ss;
816 let w = w.clamp(0.75, 1.0);
817
818 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
820 let p = p.clamp(0.0, 1.0);
821
822 Some(ShapiroWilkResult { w, p_value: p })
823}
824
825const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
827const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
828const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
829const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
830const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
831const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
832const SW_G: [f64; 2] = [-2.273, 0.459];
833
834fn sw_poly(c: &[f64], x: f64) -> f64 {
836 let mut result = c[c.len() - 1];
837 for i in (0..c.len() - 1).rev() {
838 result = result * x + c[i];
839 }
840 result
841}
842
843fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
845 let mut a = vec![0.0; nn2];
846
847 let mut m = vec![0.0; nn2];
849 let mut summ2 = 0.0;
850 for (i, mi) in m.iter_mut().enumerate() {
851 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
853 *mi = special::inverse_normal_cdf(p);
854 summ2 += *mi * *mi;
855 }
856 summ2 *= 2.0;
857 let ssumm2 = summ2.sqrt();
858 let rsn = 1.0 / (n as f64).sqrt();
859
860 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
862
863 if n <= 5 {
864 let fac_sq = summ2 - 2.0 * m[0] * m[0];
866 let one_minus = 1.0 - 2.0 * a1 * a1;
867 if fac_sq <= 0.0 || one_minus <= 0.0 {
868 return None;
869 }
870 let fac = (fac_sq / one_minus).sqrt();
871 a[0] = a1;
872 for i in 1..nn2 {
873 a[i] = -m[i] / fac;
874 }
875 } else {
876 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
878 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
879 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
880 if fac_sq <= 0.0 || one_minus <= 0.0 {
881 return None;
882 }
883 let fac = (fac_sq / one_minus).sqrt();
884 a[0] = a1;
885 a[1] = a2;
886 for i in 2..nn2 {
887 a[i] = -m[i] / fac;
888 }
889 }
890
891 Some(a)
892}
893
894fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
896 let mut sa = 0.0;
898 for i in 0..nn2 {
899 sa += a[i] * (x[n - 1 - i] - x[i]);
900 }
901
902 let mean = x.iter().sum::<f64>() / n as f64;
904 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
905
906 if ss < 1e-300 {
907 return 1.0; }
909
910 (sa * sa) / ss
911}
912
913fn sw_p_value(w: f64, n: usize) -> f64 {
915 let nf = n as f64;
916
917 if n == 3 {
918 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
920 return p.clamp(0.0, 1.0);
921 }
922
923 let w1 = 1.0 - w;
924 if w1 <= 0.0 {
925 return 1.0; }
927
928 let y = w1.ln();
929
930 if n <= 11 {
931 let gamma = sw_poly(&SW_G, nf);
933 if y >= gamma {
934 return 0.0; }
936 let y2 = -(gamma - y).ln();
937 let m = sw_poly(&SW_C3, nf);
938 let s = sw_poly(&SW_C4, nf).exp();
939 if s < 1e-300 {
940 return 0.0;
941 }
942 let z = (y2 - m) / s;
943 1.0 - special::standard_normal_cdf(z)
944 } else {
945 let xx = nf.ln();
947 let m = sw_poly(&SW_C5, xx);
948 let s = sw_poly(&SW_C6, xx).exp();
949 if s < 1e-300 {
950 return 0.0;
951 }
952 let z = (y - m) / s;
953 1.0 - special::standard_normal_cdf(z)
954 }
955}
956
957pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
994 let n1 = a.len();
995 let n2 = b.len();
996 if n1 < 2 || n2 < 2 {
997 return None;
998 }
999 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1000 return None;
1001 }
1002
1003 let n = n1 + n2;
1004 let n1f = n1 as f64;
1005 let n2f = n2 as f64;
1006 let nf = n as f64;
1007
1008 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1010 for &v in a {
1011 combined.push((v, 0)); }
1013 for &v in b {
1014 combined.push((v, 1)); }
1016 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1017
1018 let ranks = average_ranks(&combined);
1020
1021 let r1: f64 = combined
1023 .iter()
1024 .zip(ranks.iter())
1025 .filter(|((_, g), _)| *g == 0)
1026 .map(|(_, &r)| r)
1027 .sum();
1028
1029 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1031
1032 let tie_correction = compute_tie_correction(&combined);
1034
1035 let mu = n1f * n2f / 2.0;
1037 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1038
1039 if sigma_sq <= 0.0 {
1040 return None;
1041 }
1042
1043 let z = (u1 - mu) / sigma_sq.sqrt();
1044 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1045
1046 Some(TestResult {
1047 statistic: u1,
1048 df: 0.0, p_value,
1050 })
1051}
1052
1053pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1087 if x.len() != y.len() || x.len() < 2 {
1088 return None;
1089 }
1090 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1091 return None;
1092 }
1093
1094 let diffs: Vec<f64> = x
1096 .iter()
1097 .zip(y.iter())
1098 .map(|(&a, &b)| a - b)
1099 .filter(|&d| d.abs() > 1e-300)
1100 .collect();
1101
1102 let nr = diffs.len();
1103 if nr < 2 {
1104 return None;
1105 }
1106
1107 let nf = nr as f64;
1108
1109 let mut abs_diffs: Vec<(f64, usize)> = diffs
1111 .iter()
1112 .enumerate()
1113 .map(|(i, &d)| (d.abs(), i))
1114 .collect();
1115 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1116
1117 let ranks = average_ranks(&abs_diffs);
1119
1120 let t_plus: f64 = abs_diffs
1122 .iter()
1123 .zip(ranks.iter())
1124 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1125 .map(|(_, &r)| r)
1126 .sum();
1127
1128 let tie_correction_val = compute_tie_correction(&abs_diffs);
1130
1131 let mu = nf * (nf + 1.0) / 4.0;
1133 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1134
1135 if sigma_sq <= 0.0 {
1136 return None;
1137 }
1138
1139 let z = (t_plus - mu) / sigma_sq.sqrt();
1140 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1141
1142 Some(TestResult {
1143 statistic: t_plus,
1144 df: 0.0,
1145 p_value,
1146 })
1147}
1148
1149fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1152 let n = sorted.len();
1153 let mut ranks = vec![0.0; n];
1154 let mut i = 0;
1155 while i < n {
1156 let mut j = i + 1;
1157 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1158 j += 1;
1159 }
1160 let avg_rank = (i + 1 + j) as f64 / 2.0;
1162 for rank in ranks.iter_mut().take(j).skip(i) {
1163 *rank = avg_rank;
1164 }
1165 i = j;
1166 }
1167 ranks
1168}
1169
1170fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1172 let n = sorted.len();
1173 let mut correction = 0.0;
1174 let mut i = 0;
1175 while i < n {
1176 let mut j = i + 1;
1177 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1178 j += 1;
1179 }
1180 let t = (j - i) as f64;
1181 if t > 1.0 {
1182 correction += t * (t * t - 1.0);
1183 }
1184 i = j;
1185 }
1186 correction
1187}
1188
1189pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1221 let k = groups.len();
1222 if k < 2 {
1223 return None;
1224 }
1225 for g in groups {
1226 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1227 return None;
1228 }
1229 }
1230
1231 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1232 let nf = total_n as f64;
1233
1234 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1236 for (gi, g) in groups.iter().enumerate() {
1237 for &v in *g {
1238 combined.push((v, gi));
1239 }
1240 }
1241 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1242
1243 let ranks = average_ranks(&combined);
1244
1245 let mut rank_sums = vec![0.0; k];
1247 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1248 rank_sums[*gi] += r;
1249 }
1250
1251 let mean_rank = (nf + 1.0) / 2.0;
1253 let mut h = 0.0;
1254 for (gi, g) in groups.iter().enumerate() {
1255 let ni = g.len() as f64;
1256 let mean_rank_i = rank_sums[gi] / ni;
1257 h += ni * (mean_rank_i - mean_rank).powi(2);
1258 }
1259 h *= 12.0 / (nf * (nf + 1.0));
1260
1261 let tie_corr = compute_tie_correction(&combined);
1263 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1264 if denom > 1e-15 {
1265 h /= denom;
1266 }
1267
1268 let df = (k - 1) as f64;
1269 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1270
1271 Some(TestResult {
1272 statistic: h,
1273 df,
1274 p_value,
1275 })
1276}
1277
1278pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1314 let k = groups.len();
1315 if k < 2 {
1316 return None;
1317 }
1318 for g in groups {
1319 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1320 return None;
1321 }
1322 }
1323
1324 let z_groups: Vec<Vec<f64>> = groups
1326 .iter()
1327 .map(|g| {
1328 let median = stats::median(g).unwrap_or(0.0);
1329 g.iter().map(|&x| (x - median).abs()).collect()
1330 })
1331 .collect();
1332
1333 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1335 let anova = one_way_anova(&z_refs)?;
1336
1337 Some(TestResult {
1338 statistic: anova.f_statistic,
1339 df: anova.df_between as f64,
1340 p_value: anova.p_value,
1341 })
1342}
1343
1344pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1356 if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1357 return None;
1358 }
1359 let m = p_values.len() as f64;
1360 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1361}
1362
1363pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1382 let m = p_values.len();
1383 if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1384 return None;
1385 }
1386
1387 let mut indices: Vec<usize> = (0..m).collect();
1389 indices.sort_by(|&a, &b| {
1390 p_values[a]
1391 .partial_cmp(&p_values[b])
1392 .unwrap_or(std::cmp::Ordering::Equal)
1393 });
1394
1395 let mf = m as f64;
1396 let mut adjusted = vec![0.0; m];
1397
1398 let mut cummin = f64::INFINITY;
1400 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1401 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1403 cummin = cummin.min(adj);
1404 adjusted[orig_idx] = cummin;
1405 }
1406
1407 Some(adjusted)
1408}
1409
1410pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1447 let k = groups.len();
1448 if k < 2 {
1449 return None;
1450 }
1451
1452 let mut sizes = Vec::with_capacity(k);
1453 let mut vars = Vec::with_capacity(k);
1454 let mut n_total: usize = 0;
1455
1456 for g in groups {
1457 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1458 return None;
1459 }
1460 let n = g.len();
1461 let v = stats::variance(g)?;
1462 if v <= 0.0 {
1463 return None; }
1465 sizes.push(n);
1466 vars.push(v);
1467 n_total += n;
1468 }
1469
1470 let nk = n_total - k; if nk == 0 {
1472 return None;
1473 }
1474 let nk_f = nk as f64;
1475
1476 let s2_pooled: f64 = sizes
1478 .iter()
1479 .zip(vars.iter())
1480 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1481 .sum::<f64>()
1482 / nk_f;
1483
1484 if s2_pooled <= 0.0 {
1485 return None;
1486 }
1487
1488 let num = nk_f * s2_pooled.ln()
1490 - sizes
1491 .iter()
1492 .zip(vars.iter())
1493 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1494 .sum::<f64>();
1495
1496 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1498 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1499
1500 let statistic = num / c;
1501 let df = (k - 1) as f64;
1502 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1503
1504 Some(TestResult {
1505 statistic,
1506 df,
1507 p_value,
1508 })
1509}
1510
1511pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1556 let row1 = a + b;
1557 let row2 = c + d;
1558 let col1 = a + c;
1559 let col2 = b + d;
1560 let n = a + b + c + d;
1561
1562 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1564 return None;
1565 }
1566
1567 let log_prob = |a_i: u64| -> f64 {
1569 let b_i = row1 - a_i;
1570 let c_i = col1 - a_i;
1571 let d_i = row2 - c_i;
1572 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1573 - ln_factorial(a_i)
1574 - ln_factorial(b_i)
1575 - ln_factorial(c_i)
1576 - ln_factorial(d_i)
1577 - ln_factorial(n)
1578 };
1579
1580 let a_min = col1.saturating_sub(row2);
1582 let a_max = row1.min(col1);
1583
1584 let log_p_obs = log_prob(a);
1585
1586 let mut p_value = 0.0;
1588 for a_i in a_min..=a_max {
1589 let lp = log_prob(a_i);
1590 if lp <= log_p_obs + 1e-10 {
1592 p_value += lp.exp();
1593 }
1594 }
1595
1596 let p_value = p_value.min(1.0);
1598
1599 let odds_ratio = if b > 0 && c > 0 {
1601 (a as f64 * d as f64) / (b as f64 * c as f64)
1602 } else {
1603 f64::INFINITY
1604 };
1605
1606 Some(TestResult {
1607 statistic: odds_ratio,
1608 df: 1.0,
1609 p_value,
1610 })
1611}
1612
1613#[derive(Debug, Clone, Copy)]
1619pub struct MannKendallResult {
1620 pub s_statistic: i64,
1622 pub variance: f64,
1624 pub z_statistic: f64,
1626 pub p_value: f64,
1628 pub kendall_tau: f64,
1630 pub sen_slope: f64,
1632}
1633
1634pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1672 let n = data.len();
1673 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1674 return None;
1675 }
1676
1677 let mut s: i64 = 0;
1679 for i in 0..n - 1 {
1680 for j in (i + 1)..n {
1681 let diff = data[j] - data[i];
1682 if diff > 0.0 {
1683 s += 1;
1684 } else if diff < 0.0 {
1685 s -= 1;
1686 }
1687 }
1688 }
1689
1690 let mut sorted: Vec<f64> = data.to_vec();
1692 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1693
1694 let mut tie_correction: f64 = 0.0;
1695 let mut current_count: usize = 1;
1696 for i in 1..sorted.len() {
1697 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1698 current_count += 1;
1699 } else {
1700 if current_count > 1 {
1701 let t = current_count as f64;
1702 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1703 }
1704 current_count = 1;
1705 }
1706 }
1707 if current_count > 1 {
1708 let t = current_count as f64;
1709 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1710 }
1711
1712 let nf = n as f64;
1714 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1715
1716 if variance < 1e-300 {
1717 return None; }
1719
1720 let sigma = variance.sqrt();
1722 let z_statistic = if s > 0 {
1723 (s as f64 - 1.0) / sigma
1724 } else if s < 0 {
1725 (s as f64 + 1.0) / sigma
1726 } else {
1727 0.0
1728 };
1729
1730 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z_statistic.abs()));
1732 let p_value = p_value.clamp(0.0, 1.0);
1733
1734 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1736
1737 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1739 for i in 0..n - 1 {
1740 for j in (i + 1)..n {
1741 let dx = (j - i) as f64;
1742 slopes.push((data[j] - data[i]) / dx);
1743 }
1744 }
1745 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1746 let m = slopes.len();
1747 let sen_slope = if m % 2 == 0 {
1748 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1749 } else {
1750 slopes[m / 2]
1751 };
1752
1753 Some(MannKendallResult {
1754 s_statistic: s,
1755 variance,
1756 z_statistic,
1757 p_value,
1758 kendall_tau,
1759 sen_slope,
1760 })
1761}
1762
1763fn ln_factorial(n: u64) -> f64 {
1765 if n <= 1 {
1766 return 0.0;
1767 }
1768 special::ln_gamma(n as f64 + 1.0)
1770}
1771
1772#[derive(Debug, Clone)]
1778pub struct AdfResult {
1779 pub statistic: f64,
1781 pub n_lags: usize,
1783 pub n_obs: usize,
1785 pub critical_values: [f64; 3],
1787 pub rejected: [bool; 3],
1789}
1790
1791#[derive(Debug, Clone, Copy)]
1793pub enum AdfModel {
1794 None,
1796 Constant,
1798 ConstantTrend,
1800}
1801
1802pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1838 let n = data.len();
1839 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1840 return None;
1841 }
1842
1843 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1845
1846 let best_lag = match max_lags {
1848 Some(p) => p, None => {
1850 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1852 let p_max = schwert.min(n / 3);
1853 select_adf_lag(data, &dy, model, p_max)
1855 }
1856 };
1857
1858 adf_ols(data, &dy, model, best_lag)
1860}
1861
1862fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1864 let mut best_aic = f64::INFINITY;
1865 let mut best_p = 0;
1866
1867 for p in 0..=p_max {
1868 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1869 if aic < best_aic {
1870 best_aic = aic;
1871 best_p = p;
1872 }
1873 }
1874 }
1875
1876 best_p
1877}
1878
1879#[allow(clippy::type_complexity)]
1883fn adf_build_matrix(
1884 data: &[f64],
1885 dy: &[f64],
1886 model: AdfModel,
1887 p: usize,
1888) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1889 let start = p + 1;
1890 if start >= dy.len() || dy.len() - start < 5 {
1891 return None;
1892 }
1893 let m = dy.len() - start;
1894
1895 let y_dep: Vec<f64> = dy[start..].to_vec();
1896
1897 let has_intercept = !matches!(model, AdfModel::None);
1899 let has_trend = matches!(model, AdfModel::ConstantTrend);
1900 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1901
1902 let mut x_data = Vec::with_capacity(m * ncols);
1904 let mut gamma_col = 0;
1905
1906 for i in 0..m {
1907 let t = start + i;
1908 if has_intercept {
1909 x_data.push(1.0); gamma_col = 1;
1911 }
1912 x_data.push(data[t]); if has_trend {
1914 x_data.push((t + 1) as f64); }
1916 for lag in 1..=p {
1917 x_data.push(dy[t - lag]);
1918 }
1919 }
1920
1921 Some((x_data, y_dep, m, ncols, gamma_col))
1922}
1923
1924fn adf_ols_core(
1928 x_data: &[f64],
1929 y: &[f64],
1930 m: usize,
1931 ncols: usize,
1932 gamma_col: usize,
1933) -> Option<(f64, f64, usize)> {
1934 let mut xtx = vec![0.0_f64; ncols * ncols];
1936 for i in 0..m {
1937 let row = &x_data[i * ncols..(i + 1) * ncols];
1938 for j in 0..ncols {
1939 for k in j..ncols {
1940 xtx[j * ncols + k] += row[j] * row[k];
1941 }
1942 }
1943 }
1944 for j in 0..ncols {
1946 for k in (j + 1)..ncols {
1947 xtx[k * ncols + j] = xtx[j * ncols + k];
1948 }
1949 }
1950
1951 let mut xty = vec![0.0_f64; ncols];
1953 for i in 0..m {
1954 let row = &x_data[i * ncols..(i + 1) * ncols];
1955 for j in 0..ncols {
1956 xty[j] += row[j] * y[i];
1957 }
1958 }
1959
1960 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
1962 for i in 0..ncols {
1963 for j in 0..ncols {
1964 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
1965 }
1966 augmented[i * (ncols + 1) + ncols] = xty[i];
1967 }
1968
1969 for col in 0..ncols {
1970 let mut max_row = col;
1972 let mut max_val = augmented[col * (ncols + 1) + col].abs();
1973 for row in (col + 1)..ncols {
1974 let val = augmented[row * (ncols + 1) + col].abs();
1975 if val > max_val {
1976 max_val = val;
1977 max_row = row;
1978 }
1979 }
1980 if max_val < 1e-15 {
1981 return None; }
1983 if max_row != col {
1984 for j in 0..=ncols {
1985 let a = col * (ncols + 1) + j;
1986 let b = max_row * (ncols + 1) + j;
1987 augmented.swap(a, b);
1988 }
1989 }
1990
1991 let pivot = augmented[col * (ncols + 1) + col];
1992 for row in (col + 1)..ncols {
1993 let factor = augmented[row * (ncols + 1) + col] / pivot;
1994 for j in col..=ncols {
1995 let above = augmented[col * (ncols + 1) + j];
1996 augmented[row * (ncols + 1) + j] -= factor * above;
1997 }
1998 }
1999 }
2000
2001 let mut beta = vec![0.0_f64; ncols];
2003 for i in (0..ncols).rev() {
2004 let mut sum = augmented[i * (ncols + 1) + ncols];
2005 for j in (i + 1)..ncols {
2006 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2007 }
2008 beta[i] = sum / augmented[i * (ncols + 1) + i];
2009 }
2010
2011 let mut rss = 0.0;
2013 for i in 0..m {
2014 let row = &x_data[i * ncols..(i + 1) * ncols];
2015 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2016 let resid = y[i] - y_hat;
2017 rss += resid * resid;
2018 }
2019
2020 let df = m - ncols;
2022 if df == 0 {
2023 return None;
2024 }
2025 let mse = rss / df as f64;
2026
2027 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2030 for j in 0..ncols {
2031 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2032 }
2033 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2034 }
2035
2036 for col in 0..ncols {
2038 let mut max_row = col;
2039 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2040 for row in (col + 1)..ncols {
2041 let val = xtx_aug[row * 2 * ncols + col].abs();
2042 if val > max_val {
2043 max_val = val;
2044 max_row = row;
2045 }
2046 }
2047 if max_val < 1e-15 {
2048 return None;
2049 }
2050 if max_row != col {
2051 for j in 0..(2 * ncols) {
2052 let a = col * 2 * ncols + j;
2053 let b = max_row * 2 * ncols + j;
2054 xtx_aug.swap(a, b);
2055 }
2056 }
2057
2058 let pivot = xtx_aug[col * 2 * ncols + col];
2059 for j in 0..(2 * ncols) {
2060 xtx_aug[col * 2 * ncols + j] /= pivot;
2061 }
2062 for row in 0..ncols {
2063 if row == col {
2064 continue;
2065 }
2066 let factor = xtx_aug[row * 2 * ncols + col];
2067 for j in 0..(2 * ncols) {
2068 let above = xtx_aug[col * 2 * ncols + j];
2069 xtx_aug[row * 2 * ncols + j] -= factor * above;
2070 }
2071 }
2072 }
2073
2074 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2076 if var_gamma <= 0.0 {
2077 return None;
2078 }
2079 let se_gamma = var_gamma.sqrt();
2080 let t_gamma = beta[gamma_col] / se_gamma;
2081
2082 Some((t_gamma, rss, ncols))
2083}
2084
2085fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2087 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2088 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2089 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2090 Some((aic, m))
2091}
2092
2093fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2095 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2096 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2097
2098 let critical_values = adf_critical_values(model, m);
2099
2100 let rejected = [
2101 gamma_t <= critical_values[0],
2102 gamma_t <= critical_values[1],
2103 gamma_t <= critical_values[2],
2104 ];
2105
2106 Some(AdfResult {
2107 statistic: gamma_t,
2108 n_lags: p,
2109 n_obs: m,
2110 critical_values,
2111 rejected,
2112 })
2113}
2114
2115fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2119 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2124 AdfModel::None => (
2125 [-2.5658, -1.9393, -1.6156],
2126 [-1.960, -0.398, -0.181],
2127 [-10.04, 0.0, 0.0],
2128 ),
2129 AdfModel::Constant => (
2130 [-3.4336, -2.8621, -2.5671],
2131 [-5.999, -2.738, -1.438],
2132 [-29.25, -8.36, -4.48],
2133 ),
2134 AdfModel::ConstantTrend => (
2135 [-3.9638, -3.4126, -3.1279],
2136 [-8.353, -4.039, -2.418],
2137 [-47.44, -17.83, -7.58],
2138 ),
2139 };
2140
2141 let nf = n as f64;
2142 let inv_n = 1.0 / nf;
2143 let inv_n2 = inv_n * inv_n;
2144
2145 [
2146 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2147 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2148 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2149 ]
2150}
2151
2152#[cfg(test)]
2153mod tests {
2154 use super::*;
2155
2156 #[test]
2161 fn one_sample_null_true() {
2162 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2163 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2164 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2165 }
2166
2167 #[test]
2168 fn one_sample_null_false() {
2169 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2170 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2171 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2172 }
2173
2174 #[test]
2175 fn one_sample_edge_cases() {
2176 assert!(one_sample_t_test(&[1.0], 0.0).is_none()); assert!(one_sample_t_test(&[5.0, 5.0, 5.0], 5.0).is_none()); assert!(one_sample_t_test(&[1.0, f64::NAN, 3.0], 2.0).is_none());
2179 }
2180
2181 #[test]
2186 fn two_sample_same_mean() {
2187 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2188 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2189 let r = two_sample_t_test(&a, &b).expect("should compute");
2190 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2191 }
2192
2193 #[test]
2194 fn two_sample_different_means() {
2195 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2196 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2197 let r = two_sample_t_test(&a, &b).expect("should compute");
2198 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2199 }
2200
2201 #[test]
2202 fn two_sample_different_sizes() {
2203 let a = [1.0, 2.0, 3.0];
2204 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2205 let r = two_sample_t_test(&a, &b).expect("should compute");
2206 assert!(r.p_value < 0.05);
2207 }
2208
2209 #[test]
2210 fn two_sample_edge_cases() {
2211 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2212 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2213 }
2214
2215 #[test]
2220 fn paired_no_difference() {
2221 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2222 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2223 let r = paired_t_test(&x, &y).expect("should compute");
2224 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2225 }
2226
2227 #[test]
2228 fn paired_significant_difference() {
2229 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2231 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2232 let r = paired_t_test(&before, &after).expect("should compute");
2233 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2234 assert!(r.statistic < 0.0); }
2236
2237 #[test]
2238 fn paired_edge_cases() {
2239 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2242
2243 #[test]
2248 fn anova_same_means() {
2249 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2250 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2251 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2252 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2253 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2254 }
2255
2256 #[test]
2257 fn anova_different_means() {
2258 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2259 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2260 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2261 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2262 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2263 assert!(r.df_between == 2);
2264 assert!(r.df_within == 12);
2265 }
2266
2267 #[test]
2268 fn anova_ss_decomposition() {
2269 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2270 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2271 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2272 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2274 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2275 assert!(
2276 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2277 "SS decomposition: {ss_total} vs {} + {}",
2278 r.ss_between,
2279 r.ss_within
2280 );
2281 }
2282
2283 #[test]
2284 fn anova_edge_cases() {
2285 let g1 = [1.0, 2.0, 3.0];
2286 assert!(one_way_anova(&[&g1]).is_none()); }
2288
2289 #[test]
2294 fn chi2_gof_uniform() {
2295 let observed = [25.0, 25.0, 25.0, 25.0];
2297 let expected = [25.0, 25.0, 25.0, 25.0];
2298 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2299 assert!((r.statistic).abs() < 1e-15);
2300 assert!((r.p_value - 1.0).abs() < 0.01);
2301 }
2302
2303 #[test]
2304 fn chi2_gof_significant() {
2305 let observed = [90.0, 10.0];
2306 let expected = [50.0, 50.0];
2307 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2308 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2309 }
2310
2311 #[test]
2312 fn chi2_gof_edge_cases() {
2313 assert!(chi_squared_goodness_of_fit(&[10.0], &[10.0]).is_none()); assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0, 0.0]).is_none()); assert!(chi_squared_goodness_of_fit(&[10.0, 20.0], &[10.0]).is_none()); }
2317
2318 #[test]
2323 fn chi2_independence_significant() {
2324 let table = [30.0, 10.0, 10.0, 50.0];
2326 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2327 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2328 assert!((r.df - 1.0).abs() < 1e-10);
2329 }
2330
2331 #[test]
2332 fn chi2_independence_not_significant() {
2333 let table = [25.0, 25.0, 25.0, 25.0];
2335 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2336 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2337 }
2338
2339 #[test]
2340 fn chi2_independence_3x3() {
2341 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2342 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2343 assert!((r.df - 4.0).abs() < 1e-10);
2344 assert!(r.p_value < 0.05);
2345 }
2346
2347 #[test]
2348 fn chi2_independence_edge_cases() {
2349 assert!(chi_squared_independence(&[10.0, 20.0], 1, 2).is_none()); assert!(chi_squared_independence(&[10.0, 20.0], 2, 1).is_none()); assert!(chi_squared_independence(&[10.0], 2, 2).is_none()); }
2353
2354 #[test]
2359 fn jb_normal_data() {
2360 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2362 let r = jarque_bera_test(&data).expect("should compute");
2363 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2364 }
2365
2366 #[test]
2367 fn jb_skewed_data() {
2368 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2370 let r = jarque_bera_test(&data).expect("should compute");
2371 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2372 }
2373
2374 #[test]
2375 fn jb_edge_cases() {
2376 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2378
2379 #[test]
2384 fn ad_normal_data() {
2385 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2386 let r = anderson_darling_test(&data).expect("should compute");
2387 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2388 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2389 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2390 }
2391
2392 #[test]
2393 fn ad_skewed_data() {
2394 let data = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2396 let r = anderson_darling_test(&data).expect("should compute");
2397 assert!(
2398 r.p_value < 0.05,
2399 "p = {} (should reject normality)",
2400 r.p_value
2401 );
2402 }
2403
2404 #[test]
2405 fn ad_bimodal_data() {
2406 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2408 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2409 let r = anderson_darling_test(&data).expect("should compute");
2410 assert!(
2411 r.p_value < 0.01,
2412 "p = {} (bimodal should reject normality)",
2413 r.p_value
2414 );
2415 }
2416
2417 #[test]
2418 fn ad_large_normal_sample() {
2419 let n = 100;
2420 let data: Vec<f64> = (1..=n)
2421 .map(|i| {
2422 let p = (i as f64 - 0.5) / n as f64;
2423 special::inverse_normal_cdf(p)
2424 })
2425 .collect();
2426 let r = anderson_darling_test(&data).expect("should compute");
2427 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2428 }
2429
2430 #[test]
2431 fn ad_edge_cases() {
2432 assert!(anderson_darling_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); assert!(anderson_darling_test(&[5.0; 10]).is_none()); assert!(anderson_darling_test(&[1.0, f64::NAN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).is_none());
2435 }
2436
2437 #[test]
2438 fn ad_p_value_ranges() {
2439 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2444 let r = anderson_darling_test(&near_normal).expect("should compute");
2445 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2446
2447 let heavy_tail = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2449 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2450 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2451 }
2452
2453 #[test]
2458 fn ad_normal_data_large_p() {
2459 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2461 let r = anderson_darling_normality(&data).unwrap();
2462 assert!(r.p_value > 0.05, "p={}", r.p_value);
2463 }
2464
2465 #[test]
2466 fn ad_exponential_data_small_p() {
2467 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2469 let r = anderson_darling_normality(&data).unwrap();
2470 assert!(r.p_value < 0.05, "p={}", r.p_value);
2471 }
2472
2473 #[test]
2474 fn ad_statistic_non_negative() {
2475 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2476 let r = anderson_darling_normality(&data).unwrap();
2477 assert!(r.statistic >= 0.0);
2478 assert!(r.statistic_modified >= r.statistic);
2479 }
2480
2481 #[test]
2482 fn ad_p_value_in_range() {
2483 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2484 let r = anderson_darling_normality(&data).unwrap();
2485 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2486 }
2487
2488 #[test]
2489 fn ad_insufficient_data() {
2490 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2492
2493 #[test]
2494 fn ad_degenerate_data() {
2495 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2497 }
2498
2499 #[test]
2500 fn ad_modified_statistic_formula() {
2501 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2503 let r = anderson_darling_normality(&data).unwrap();
2504 assert!(r.statistic_modified > r.statistic - 1e-10);
2505 }
2506
2507 #[test]
2512 fn ad_correction_factor_formula() {
2513 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2514 let n = data.len() as f64;
2515 let r = anderson_darling_normality(&data).unwrap();
2516
2517 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2519 assert!(
2520 (r.statistic_modified - expected_star).abs() < 1e-12,
2521 "A²* = {}, expected = {}",
2522 r.statistic_modified,
2523 expected_star
2524 );
2525
2526 assert!(
2528 r.statistic >= 0.0,
2529 "A² = {} must be non-negative",
2530 r.statistic
2531 );
2532 }
2533
2534 #[test]
2540 fn ad_formula_structure_symmetric_vs_skewed() {
2541 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2543 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2545
2546 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2547 let r_skew = anderson_darling_normality(&skewed).unwrap();
2548
2549 assert!(
2550 r_sym.statistic < r_skew.statistic,
2551 "Symmetric A²={} should be less than skewed A²={}",
2552 r_sym.statistic,
2553 r_skew.statistic
2554 );
2555 }
2556
2557 #[test]
2562 fn ad_pvalue_invariants() {
2563 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2565 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2566 assert!(
2567 r_normal.p_value > 0.05,
2568 "Normal data: p = {} (expected > 0.05)",
2569 r_normal.p_value
2570 );
2571
2572 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2574 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2575 assert!(
2576 r_exp.p_value < 0.05,
2577 "Exponential data: p = {} (expected < 0.05)",
2578 r_exp.p_value
2579 );
2580 }
2581
2582 #[test]
2587 fn sw_normal_data() {
2588 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2590 let r = shapiro_wilk_test(&data).expect("should compute");
2591 assert!(r.w > 0.9, "W = {}", r.w);
2592 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2593 }
2594
2595 #[test]
2596 fn sw_bimodal_data() {
2597 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2599 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2600 let r = shapiro_wilk_test(&data).expect("should compute");
2601 assert!(
2602 r.p_value < 0.01,
2603 "p = {} (bimodal should reject normality)",
2604 r.p_value
2605 );
2606 }
2607
2608 #[test]
2609 fn sw_n3() {
2610 let data = [1.0, 2.0, 3.0];
2611 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2612 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2613 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2614 }
2615
2616 #[test]
2617 fn sw_n4() {
2618 let data = [1.0, 2.0, 3.0, 4.0];
2619 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2620 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2621 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2622 }
2623
2624 #[test]
2625 fn sw_n5() {
2626 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2627 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2628 assert!(r.w > 0.9, "W = {}", r.w);
2629 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2630 }
2631
2632 #[test]
2633 fn sw_skewed_data() {
2634 let data = [0.1, 0.2, 0.3, 0.5, 0.8, 1.3, 2.1, 3.4, 5.5, 8.9, 14.4, 23.3];
2636 let r = shapiro_wilk_test(&data).expect("should compute");
2637 assert!(
2638 r.p_value < 0.05,
2639 "p = {} (skewed data should reject normality)",
2640 r.p_value
2641 );
2642 }
2643
2644 #[test]
2645 fn sw_large_normal_sample() {
2646 let n = 100;
2649 let data: Vec<f64> = (1..=n)
2650 .map(|i| {
2651 let p = (i as f64 - 0.5) / n as f64;
2652 special::inverse_normal_cdf(p)
2653 })
2654 .collect();
2655 let r = shapiro_wilk_test(&data).expect("should compute");
2656 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2657 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2658 }
2659
2660 #[test]
2661 fn sw_w_bounded() {
2662 let datasets: Vec<Vec<f64>> = vec![
2664 vec![1.0, 2.0, 3.0],
2665 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2666 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2667 (0..20).map(|i| (i as f64).powi(2)).collect(),
2668 ];
2669 for (idx, data) in datasets.iter().enumerate() {
2670 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2671 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2672 assert!(
2673 r.p_value >= 0.0 && r.p_value <= 1.0,
2674 "dataset {idx}: p = {}",
2675 r.p_value
2676 );
2677 }
2678 }
2679
2680 #[test]
2681 fn sw_edge_cases() {
2682 assert!(shapiro_wilk_test(&[1.0, 2.0]).is_none()); assert!(shapiro_wilk_test(&[]).is_none()); assert!(shapiro_wilk_test(&[5.0, 5.0, 5.0]).is_none()); assert!(shapiro_wilk_test(&[1.0, f64::NAN, 3.0]).is_none()); assert!(shapiro_wilk_test(&[1.0, f64::INFINITY, 3.0]).is_none()); }
2688
2689 #[test]
2690 fn sw_n5001_rejected() {
2691 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2692 assert!(shapiro_wilk_test(&data).is_none()); }
2694
2695 #[test]
2700 fn mw_clearly_different() {
2701 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2702 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2703 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2704 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2705 }
2706
2707 #[test]
2708 fn mw_same_distribution() {
2709 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2710 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2711 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2712 assert!(
2713 r.p_value > 0.3,
2714 "p = {} (interleaved, same dist)",
2715 r.p_value
2716 );
2717 }
2718
2719 #[test]
2720 fn mw_with_ties() {
2721 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2722 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2723 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2724 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2725 }
2726
2727 #[test]
2728 fn mw_different_sizes() {
2729 let a = [1.0, 2.0, 3.0];
2730 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2731 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2732 assert!(r.p_value < 0.05);
2733 }
2734
2735 #[test]
2736 fn mw_edge_cases() {
2737 assert!(mann_whitney_u_test(&[1.0], &[2.0, 3.0]).is_none()); assert!(mann_whitney_u_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(mann_whitney_u_test(&[1.0, f64::NAN], &[2.0, 3.0]).is_none());
2740 }
2741
2742 #[test]
2747 fn wilcoxon_significant_increase() {
2748 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2749 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2750 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2751 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2752 }
2753
2754 #[test]
2755 fn wilcoxon_no_difference() {
2756 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2757 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2758 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2759 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2760 }
2761
2762 #[test]
2763 fn wilcoxon_with_ties() {
2764 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2766 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2768 assert!(r.statistic > 0.0);
2770 }
2771
2772 #[test]
2773 fn wilcoxon_with_zero_diffs() {
2774 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2776 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2778 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2779 }
2780
2781 #[test]
2782 fn wilcoxon_edge_cases() {
2783 assert!(wilcoxon_signed_rank_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(wilcoxon_signed_rank_test(&[1.0], &[2.0]).is_none()); assert!(wilcoxon_signed_rank_test(&[5.0, 5.0], &[5.0, 5.0]).is_none());
2787 }
2788
2789 #[test]
2794 fn kw_clearly_different() {
2795 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2796 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2797 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2798 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2799 assert!(r.p_value < 0.01, "p = {}", r.p_value);
2800 assert!((r.df - 2.0).abs() < 1e-10);
2801 }
2802
2803 #[test]
2804 fn kw_same_distribution() {
2805 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2806 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2807 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2808 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2809 }
2810
2811 #[test]
2812 fn kw_with_ties() {
2813 let g1 = [1.0, 2.0, 2.0, 3.0];
2814 let g2 = [3.0, 4.0, 4.0, 5.0];
2815 let g3 = [5.0, 6.0, 6.0, 7.0];
2816 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2817 assert!(r.statistic > 0.0);
2818 }
2819
2820 #[test]
2821 fn kw_edge_cases() {
2822 let g1 = [1.0, 2.0, 3.0];
2823 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
2825
2826 #[test]
2831 fn levene_equal_variance() {
2832 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2833 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2834 let r = levene_test(&[&g1, &g2]).expect("should compute");
2835 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2837 }
2838
2839 #[test]
2840 fn levene_unequal_variance() {
2841 let g1 = [4.5, 4.8, 5.0, 5.2, 5.5]; let g2 = [0.0, 2.0, 5.0, 8.0, 10.0]; let r = levene_test(&[&g1, &g2]).expect("should compute");
2844 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2845 }
2846
2847 #[test]
2848 fn levene_three_groups() {
2849 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2850 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2851 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2852 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2853 assert!(r.df >= 2.0);
2854 }
2855
2856 #[test]
2857 fn levene_edge_cases() {
2858 let g1 = [1.0, 2.0, 3.0];
2859 assert!(levene_test(&[&g1]).is_none()); }
2861
2862 #[test]
2867 fn bonferroni_basic() {
2868 let ps = [0.01, 0.04, 0.03, 0.005];
2869 let adj = bonferroni_correction(&ps).expect("should compute");
2870 assert!((adj[0] - 0.04).abs() < 1e-10);
2871 assert!((adj[1] - 0.16).abs() < 1e-10);
2872 assert!((adj[2] - 0.12).abs() < 1e-10);
2873 assert!((adj[3] - 0.02).abs() < 1e-10);
2874 }
2875
2876 #[test]
2877 fn bonferroni_capped_at_one() {
2878 let ps = [0.5, 0.6];
2879 let adj = bonferroni_correction(&ps).expect("should compute");
2880 assert!((adj[0] - 1.0).abs() < 1e-10);
2881 assert!((adj[1] - 1.0).abs() < 1e-10);
2882 }
2883
2884 #[test]
2885 fn bh_basic() {
2886 let ps = [0.01, 0.04, 0.03, 0.005];
2887 let adj = benjamini_hochberg(&ps).expect("should compute");
2888 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
2890 assert!(
2891 adjusted >= orig - 1e-15,
2892 "adj[{i}] = {adjusted} < original {orig}"
2893 );
2894 }
2895 }
2898
2899 #[test]
2900 fn bh_all_significant() {
2901 let ps = [0.001, 0.002, 0.003];
2902 let adj = benjamini_hochberg(&ps).expect("should compute");
2903 for &a in &adj {
2904 assert!(a < 0.05);
2905 }
2906 }
2907
2908 #[test]
2909 fn correction_edge_cases() {
2910 assert!(bonferroni_correction(&[]).is_none());
2911 assert!(benjamini_hochberg(&[]).is_none());
2912 assert!(bonferroni_correction(&[f64::NAN]).is_none());
2913 }
2914
2915 #[test]
2920 fn bartlett_equal_variances() {
2921 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
2922 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
2923 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
2924 assert!(
2925 r.p_value > 0.9,
2926 "equal variance → p high, got {}",
2927 r.p_value
2928 );
2929 }
2930
2931 #[test]
2932 fn bartlett_unequal_variances() {
2933 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0]; let g2 = [10.0, 20.0, 30.0, 40.0, 50.0]; let r = bartlett_test(&[&g1, &g2]).expect("should compute");
2936 assert!(
2937 r.p_value < 0.01,
2938 "very different variances → p < 0.01, got {}",
2939 r.p_value
2940 );
2941 assert!((r.df - 1.0).abs() < 1e-10); }
2943
2944 #[test]
2945 fn bartlett_three_groups() {
2946 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2947 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
2948 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
2950 assert!(r.p_value < 0.05, "one group with high variance");
2951 assert!((r.df - 2.0).abs() < 1e-10); }
2953
2954 #[test]
2955 fn bartlett_edge_cases() {
2956 let g1 = [1.0, 2.0, 3.0];
2957 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
2961
2962 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
2964 }
2965
2966 #[test]
2971 fn fisher_tea_tasting() {
2972 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
2974 assert!(
2976 (r.p_value - 0.4857).abs() < 0.01,
2977 "p ≈ 0.4857, got {}",
2978 r.p_value
2979 );
2980 }
2981
2982 #[test]
2983 fn fisher_significant() {
2984 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
2986 assert!(r.p_value < 0.001, "perfect association → p very small");
2987 }
2988
2989 #[test]
2990 fn fisher_no_association() {
2991 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
2993 assert!(
2994 r.p_value > 0.9,
2995 "no association → p ≈ 1.0, got {}",
2996 r.p_value
2997 );
2998 }
2999
3000 #[test]
3001 fn fisher_small_table() {
3002 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3004 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3005 }
3006
3007 #[test]
3008 fn fisher_asymmetric() {
3009 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3011 assert!(r.p_value < 0.05, "significant association");
3012 }
3013
3014 #[test]
3015 fn fisher_edge_cases() {
3016 assert!(fisher_exact_test(0, 0, 1, 2).is_none()); assert!(fisher_exact_test(1, 2, 0, 0).is_none()); assert!(fisher_exact_test(0, 1, 0, 2).is_none()); }
3021
3022 #[test]
3023 fn fisher_odds_ratio() {
3024 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3025 assert!(
3027 (r.statistic - 9.0).abs() < 1e-10,
3028 "OR = 9, got {}",
3029 r.statistic
3030 );
3031 }
3032
3033 #[test]
3038 fn mk_increasing_trend() {
3039 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3040 let r = mann_kendall_test(&data).expect("should compute");
3041 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3042 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3043 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3044 assert!(r.s_statistic > 0);
3045 }
3046
3047 #[test]
3048 fn mk_decreasing_trend() {
3049 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3050 let r = mann_kendall_test(&data).expect("should compute");
3051 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3052 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3053 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3054 assert!(r.s_statistic < 0);
3055 }
3056
3057 #[test]
3058 fn mk_no_trend() {
3059 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3061 let r = mann_kendall_test(&data).expect("should compute");
3062 assert!(
3064 r.p_value > 0.05,
3065 "p = {} (should be > 0.05 for no trend)",
3066 r.p_value
3067 );
3068 }
3069
3070 #[test]
3071 fn mk_perfect_monotone() {
3072 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3073 let r = mann_kendall_test(&data).expect("should compute");
3074 assert_eq!(r.s_statistic, 45);
3076 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3077 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3078 }
3079
3080 #[test]
3081 fn mk_with_ties() {
3082 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3083 let r = mann_kendall_test(&data).expect("should compute");
3084 assert!(r.s_statistic > 0);
3085 let n = data.len() as f64;
3087 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3088 assert!(r.variance < base_var, "ties should reduce variance");
3089 }
3090
3091 #[test]
3092 fn mk_edge_cases() {
3093 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3095 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3097 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3099 }
3100
3101 #[test]
3102 fn mk_minimum_n() {
3103 let data = [1.0, 2.0, 3.0, 4.0];
3105 let r = mann_kendall_test(&data).expect("n=4 should work");
3106 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3108 }
3109
3110 #[test]
3111 fn mk_sen_slope_robust_to_outlier() {
3112 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3114 let r = mann_kendall_test(&data).expect("should compute");
3115 assert!(
3117 (r.sen_slope - 1.0).abs() < 0.5,
3118 "sen slope = {}, expected ≈ 1.0",
3119 r.sen_slope
3120 );
3121 }
3122
3123 #[test]
3128 fn adf_stationary_mean_reverting() {
3129 let data = [
3132 0.5, 0.45, -0.2, 0.14, 0.54, -0.04, 0.39, -0.18, 0.35, 0.01, -0.3, 0.21, 0.47, -0.06,
3133 0.38, -0.25, 0.12, 0.44, -0.13, 0.36, -0.09, 0.27, 0.51, -0.15, 0.33, -0.22, 0.18,
3134 0.42, -0.08, 0.31, -0.19, 0.25, 0.48, -0.11, 0.37, -0.24, 0.15, 0.43, -0.07, 0.34,
3135 ];
3136 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3137 assert!(
3139 r.rejected[2],
3140 "should reject at 10%: stat={}, cv={}",
3141 r.statistic, r.critical_values[2]
3142 );
3143 }
3144
3145 #[test]
3146 fn adf_nonstationary_random_walk() {
3147 let increments = [
3149 0.1, -0.2, 0.15, -0.05, 0.3, -0.1, 0.2, -0.15, 0.25, -0.08, 0.12, -0.18, 0.22, -0.07,
3150 0.16, -0.11, 0.19, -0.14, 0.21, -0.09, 0.13, -0.17, 0.24, -0.06, 0.18, -0.12, 0.2,
3151 -0.13, 0.15, -0.1,
3152 ];
3153 let mut walk = Vec::with_capacity(increments.len());
3154 let mut cum = 0.0;
3155 for &inc in &increments {
3156 cum += inc;
3157 walk.push(cum);
3158 }
3159 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3160 assert!(
3162 !r.rejected[0],
3163 "should NOT reject at 1%: stat={}, cv={}",
3164 r.statistic, r.critical_values[0]
3165 );
3166 }
3167
3168 #[test]
3169 fn adf_with_fixed_lags() {
3170 let data: Vec<f64> = (0..50)
3172 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3173 .collect();
3174 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3175 assert_eq!(r.n_lags, 2);
3176 assert!(r.statistic.is_finite());
3177 }
3178
3179 #[test]
3180 fn adf_constant_trend_model() {
3181 let data: Vec<f64> = (0..30)
3184 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3185 .collect();
3186 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3187 assert!(r.statistic.is_finite());
3188 assert_eq!(r.critical_values.len(), 3);
3189 }
3190
3191 #[test]
3192 fn adf_edge_cases() {
3193 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3195 let mut data = vec![0.0; 20];
3197 data[5] = f64::NAN;
3198 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3199 }
3200
3201 #[test]
3202 fn adf_critical_values_constant() {
3203 let cv = adf_critical_values(AdfModel::Constant, 100);
3205 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3207 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3208 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3209 }
3210
3211 #[test]
3212 fn adf_critical_values_ordering() {
3213 let cv = adf_critical_values(AdfModel::Constant, 50);
3214 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3216 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3217 }
3218}
3219
3220#[cfg(test)]
3221mod proptests {
3222 use super::*;
3223 use proptest::prelude::*;
3224
3225 proptest! {
3226 #[test]
3227 fn one_sample_p_bounded(
3228 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3229 mu0 in -1e3_f64..1e3
3230 ) {
3231 if let Some(r) = one_sample_t_test(&data, mu0) {
3232 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3233 }
3234 }
3235
3236 #[test]
3237 fn two_sample_p_bounded(
3238 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3239 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3240 ) {
3241 if let Some(r) = two_sample_t_test(&a, &b) {
3242 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3243 }
3244 }
3245
3246 #[test]
3247 fn anova_p_bounded(
3248 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3249 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3250 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3251 ) {
3252 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3253 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3254 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3255 }
3256 }
3257
3258 #[test]
3259 fn bonferroni_monotone(
3260 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3261 ) {
3262 let adj = bonferroni_correction(&ps).expect("should compute");
3263 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3264 prop_assert!(adjusted >= orig - 1e-15,
3265 "adj[{i}] = {adjusted} < orig = {orig}");
3266 }
3267 }
3268
3269 #[test]
3270 fn shapiro_wilk_p_bounded(
3271 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3272 ) {
3273 if let Some(r) = shapiro_wilk_test(&data) {
3274 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3275 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3276 }
3277 }
3278
3279 #[test]
3280 fn anderson_darling_p_bounded(
3281 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3282 ) {
3283 if let Some(r) = anderson_darling_test(&data) {
3284 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3285 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3286 }
3287 }
3288
3289 #[test]
3290 fn mann_whitney_p_bounded(
3291 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3292 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3293 ) {
3294 if let Some(r) = mann_whitney_u_test(&a, &b) {
3295 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3296 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3297 }
3298 }
3299
3300 #[test]
3301 fn wilcoxon_p_bounded(
3302 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3303 ) {
3304 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3305 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3306 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3307 }
3308 }
3309
3310 #[test]
3311 fn bartlett_p_bounded(
3312 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3313 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3314 ) {
3315 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3316 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3317 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3318 }
3319 }
3320
3321 #[test]
3322 fn fisher_p_bounded(
3323 a in 0_u64..20,
3324 b in 0_u64..20,
3325 c in 0_u64..20,
3326 d in 0_u64..20,
3327 ) {
3328 if let Some(r) = fisher_exact_test(a, b, c, d) {
3329 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3330 }
3331 }
3332
3333 #[test]
3334 fn mann_kendall_p_bounded(
3335 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3336 ) {
3337 if let Some(r) = mann_kendall_test(&data) {
3338 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3339 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3340 "tau = {}", r.kendall_tau);
3341 }
3342 }
3343
3344 #[test]
3345 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3346 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3347 let r = mann_kendall_test(&data).expect("should compute");
3348 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3349 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3350 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3351 }
3352 }
3353}