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 * special::t_distribution_sf(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 * special::t_distribution_sf(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 special::f_distribution_sf(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 = special::chi_squared_sf(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 = special::chi_squared_sf(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> {
497 let n = data.len();
498 if n < 8 {
499 return None;
500 }
501 if data.iter().any(|v| !v.is_finite()) {
502 return None;
503 }
504
505 let g1 = stats::skewness_moment(data)?;
506 let g2 = stats::kurtosis_moment(data)?;
507
508 let nf = n as f64;
509 let jb = (nf / 6.0) * (g1 * g1 + g2 * g2 / 4.0);
510 let p_value = (-jb / 2.0).exp();
512
513 Some(TestResult {
514 statistic: jb,
515 df: 2.0,
516 p_value,
517 })
518}
519
520#[derive(Debug, Clone, Copy)]
522pub struct AndersonDarlingResult {
523 pub statistic: f64,
525 pub statistic_star: f64,
527 pub p_value: f64,
529}
530
531fn ad_upper_tail_pvalue(a2_star: f64) -> f64 {
551 const UPPER_BRANCH_VERTEX: f64 = 153.467_741_935_483_87; let p = if a2_star >= 0.6 {
556 let x = a2_star.min(UPPER_BRANCH_VERTEX);
557 (1.2937 - 5.709 * x + 0.0186 * x * x).exp()
558 } else if a2_star > 0.34 {
559 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
560 } else if a2_star > 0.2 {
561 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
562 } else {
563 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
564 };
565 p.clamp(0.0, 1.0)
566}
567
568pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
603 let n = data.len();
604 if n < 8 {
605 return None;
606 }
607 if data.iter().any(|v| !v.is_finite()) {
608 return None;
609 }
610
611 let mean = stats::mean(data)?;
612 let sd = stats::std_dev(data)?;
613
614 if sd < 1e-300 {
615 return None; }
617
618 let mut x: Vec<f64> = data.to_vec();
620 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
621
622 let nf = n as f64;
623
624 let mut s = 0.0;
626 for i in 0..n {
627 let z = (x[i] - mean) / sd;
628 let z_rev = (x[n - 1 - i] - mean) / sd;
629 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
633 let ln_sf_rev = special::standard_normal_sf(z_rev)
634 .max(f64::MIN_POSITIVE)
635 .ln();
636
637 let coeff = (2 * (i + 1) - 1) as f64;
638 s += coeff * (ln_phi + ln_sf_rev);
639 }
640
641 let a2 = -nf - s / nf;
642
643 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
645
646 let p = ad_upper_tail_pvalue(a2_star);
649
650 Some(AndersonDarlingResult {
651 statistic: a2,
652 statistic_star: a2_star,
653 p_value: p,
654 })
655}
656
657#[derive(Debug, Clone, Copy)]
659pub struct ShapiroWilkResult {
660 pub w: f64,
662 pub p_value: f64,
664}
665
666pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
706 let n = data.len();
707 if !(3..=5000).contains(&n) {
708 return None;
709 }
710 if data.iter().any(|v| !v.is_finite()) {
711 return None;
712 }
713
714 let mut x: Vec<f64> = data.to_vec();
716 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
717
718 if x[n - 1] - x[0] < 1e-300 {
719 return None; }
721
722 let nn2 = n / 2;
723
724 if n == 3 {
726 return shapiro_wilk_n3(&x);
727 }
728
729 let a = sw_coefficients(n, nn2)?;
731
732 let w = sw_statistic(&x, &a, n, nn2);
734
735 if !(0.0..=1.0 + 1e-10).contains(&w) {
736 return None;
737 }
738 let w = w.min(1.0);
739
740 let p_value = sw_p_value(w, n);
742
743 Some(ShapiroWilkResult {
744 w,
745 p_value: p_value.clamp(0.0, 1.0),
746 })
747}
748
749fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
751 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
754 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
755 if ss < 1e-300 {
756 return None;
757 }
758
759 let numerator = a1 * (x[2] - x[0]);
760 let w = (numerator * numerator) / ss;
761 let w = w.clamp(0.75, 1.0);
762
763 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
765 let p = p.clamp(0.0, 1.0);
766
767 Some(ShapiroWilkResult { w, p_value: p })
768}
769
770const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
772const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
773const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
774const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
775const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
776const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
777const SW_G: [f64; 2] = [-2.273, 0.459];
778
779fn sw_poly(c: &[f64], x: f64) -> f64 {
781 let mut result = c[c.len() - 1];
782 for i in (0..c.len() - 1).rev() {
783 result = result * x + c[i];
784 }
785 result
786}
787
788fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
790 let mut a = vec![0.0; nn2];
791
792 let mut m = vec![0.0; nn2];
794 let mut summ2 = 0.0;
795 for (i, mi) in m.iter_mut().enumerate() {
796 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
798 *mi = special::inverse_normal_cdf(p);
799 summ2 += *mi * *mi;
800 }
801 summ2 *= 2.0;
802 let ssumm2 = summ2.sqrt();
803 let rsn = 1.0 / (n as f64).sqrt();
804
805 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
807
808 if n <= 5 {
809 let fac_sq = summ2 - 2.0 * m[0] * m[0];
811 let one_minus = 1.0 - 2.0 * a1 * a1;
812 if fac_sq <= 0.0 || one_minus <= 0.0 {
813 return None;
814 }
815 let fac = (fac_sq / one_minus).sqrt();
816 a[0] = a1;
817 for i in 1..nn2 {
818 a[i] = -m[i] / fac;
819 }
820 } else {
821 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
823 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
824 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
825 if fac_sq <= 0.0 || one_minus <= 0.0 {
826 return None;
827 }
828 let fac = (fac_sq / one_minus).sqrt();
829 a[0] = a1;
830 a[1] = a2;
831 for i in 2..nn2 {
832 a[i] = -m[i] / fac;
833 }
834 }
835
836 Some(a)
837}
838
839fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
841 let mut sa = 0.0;
843 for i in 0..nn2 {
844 sa += a[i] * (x[n - 1 - i] - x[i]);
845 }
846
847 let mean = x.iter().sum::<f64>() / n as f64;
849 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
850
851 if ss < 1e-300 {
852 return 1.0; }
854
855 (sa * sa) / ss
856}
857
858fn sw_p_value(w: f64, n: usize) -> f64 {
860 let nf = n as f64;
861
862 if n == 3 {
863 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
865 return p.clamp(0.0, 1.0);
866 }
867
868 let w1 = 1.0 - w;
869 if w1 <= 0.0 {
870 return 1.0; }
872
873 let y = w1.ln();
874
875 if n <= 11 {
876 let gamma = sw_poly(&SW_G, nf);
878 if y >= gamma {
879 return 0.0; }
881 let y2 = -(gamma - y).ln();
882 let m = sw_poly(&SW_C3, nf);
883 let s = sw_poly(&SW_C4, nf).exp();
884 if s < 1e-300 {
885 return 0.0;
886 }
887 let z = (y2 - m) / s;
888 special::standard_normal_sf(z)
889 } else {
890 let xx = nf.ln();
892 let m = sw_poly(&SW_C5, xx);
893 let s = sw_poly(&SW_C6, xx).exp();
894 if s < 1e-300 {
895 return 0.0;
896 }
897 let z = (y - m) / s;
898 special::standard_normal_sf(z)
899 }
900}
901
902pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
946 let n1 = a.len();
947 let n2 = b.len();
948 if n1 < 2 || n2 < 2 {
949 return None;
950 }
951 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
952 return None;
953 }
954
955 let n = n1 + n2;
956 let n1f = n1 as f64;
957 let n2f = n2 as f64;
958 let nf = n as f64;
959
960 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
962 for &v in a {
963 combined.push((v, 0)); }
965 for &v in b {
966 combined.push((v, 1)); }
968 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
969
970 let ranks = average_ranks(&combined);
972
973 let r1: f64 = combined
975 .iter()
976 .zip(ranks.iter())
977 .filter(|((_, g), _)| *g == 0)
978 .map(|(_, &r)| r)
979 .sum();
980
981 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
983
984 let tie_correction = compute_tie_correction(&combined);
986
987 let mu = n1f * n2f / 2.0;
989 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
990
991 if sigma_sq <= 0.0 {
992 return None;
993 }
994
995 let p_value = if tie_correction == 0.0 && n1 <= EXACT_MAX_N && n2 <= EXACT_MAX_N {
996 let u = u1.round() as usize;
999 let tail = u.min(n1 * n2 - u);
1000 (2.0 * mann_whitney_exact_cdf(n1, n2, tail)).min(1.0)
1001 } else {
1002 continuity_corrected_p(u1, mu, sigma_sq)
1003 };
1004
1005 Some(TestResult {
1006 statistic: u1,
1007 df: 0.0, p_value,
1009 })
1010}
1011
1012const EXACT_MAX_N: usize = 50;
1016
1017fn continuity_corrected_p(statistic: f64, mu: f64, sigma_sq: f64) -> f64 {
1020 let z = ((statistic - mu).abs() - 0.5).max(0.0) / sigma_sq.sqrt();
1021 (2.0 * special::standard_normal_sf(z)).min(1.0)
1022}
1023
1024fn mann_whitney_exact_cdf(m: usize, n: usize, u: usize) -> f64 {
1033 let max_u = m * n;
1034 let mut prev: Vec<Vec<f64>> = (0..=n).map(|_| vec![1.0]).collect(); for i in 1..=m {
1037 let mut cur: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
1038 cur.push(vec![1.0]); for j in 1..=n {
1040 let mut dist = vec![0.0; i * j + 1];
1041 let wi = i as f64 / (i + j) as f64;
1042 let wj = j as f64 / (i + j) as f64;
1043 for (k, &pk) in prev[j].iter().enumerate() {
1045 dist[k + j] += wi * pk;
1046 }
1047 for (k, &pk) in cur[j - 1].iter().enumerate() {
1049 dist[k] += wj * pk;
1050 }
1051 cur.push(dist);
1052 }
1053 prev = cur;
1054 }
1055 prev[n].iter().take(u.min(max_u) + 1).sum::<f64>().min(1.0)
1056}
1057
1058fn wilcoxon_exact_cdf(n: usize, t: usize) -> f64 {
1061 let max_t = n * (n + 1) / 2;
1062 let mut dist = vec![0.0; max_t + 1];
1063 dist[0] = 1.0;
1064 let mut reach = 0;
1065 for rank in 1..=n {
1066 reach += rank;
1067 for s in (rank..=reach).rev() {
1068 dist[s] = 0.5 * dist[s] + 0.5 * dist[s - rank];
1069 }
1070 for d in dist.iter_mut().take(rank) {
1071 *d *= 0.5;
1072 }
1073 }
1074 dist.iter().take(t.min(max_t) + 1).sum::<f64>().min(1.0)
1075}
1076
1077pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1117 if x.len() != y.len() || x.len() < 2 {
1118 return None;
1119 }
1120 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1121 return None;
1122 }
1123
1124 let diffs: Vec<f64> = x
1126 .iter()
1127 .zip(y.iter())
1128 .map(|(&a, &b)| a - b)
1129 .filter(|&d| d.abs() > 1e-300)
1130 .collect();
1131
1132 let nr = diffs.len();
1133 if nr < 2 {
1134 return None;
1135 }
1136
1137 let nf = nr as f64;
1138
1139 let mut abs_diffs: Vec<(f64, usize)> = diffs
1141 .iter()
1142 .enumerate()
1143 .map(|(i, &d)| (d.abs(), i))
1144 .collect();
1145 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1146
1147 let ranks = average_ranks(&abs_diffs);
1149
1150 let t_plus: f64 = abs_diffs
1152 .iter()
1153 .zip(ranks.iter())
1154 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1155 .map(|(_, &r)| r)
1156 .sum();
1157
1158 let tie_correction_val = compute_tie_correction(&abs_diffs);
1160
1161 let mu = nf * (nf + 1.0) / 4.0;
1163 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1164
1165 if sigma_sq <= 0.0 {
1166 return None;
1167 }
1168
1169 let p_value = if tie_correction_val == 0.0 && nr <= EXACT_MAX_N {
1170 let t = t_plus.round() as usize;
1173 let tail = t.min(nr * (nr + 1) / 2 - t);
1174 (2.0 * wilcoxon_exact_cdf(nr, tail)).min(1.0)
1175 } else {
1176 continuity_corrected_p(t_plus, mu, sigma_sq)
1177 };
1178
1179 Some(TestResult {
1180 statistic: t_plus,
1181 df: 0.0,
1182 p_value,
1183 })
1184}
1185
1186fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1189 let n = sorted.len();
1190 let mut ranks = vec![0.0; n];
1191 let mut i = 0;
1192 while i < n {
1193 let mut j = i + 1;
1194 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1195 j += 1;
1196 }
1197 let avg_rank = (i + 1 + j) as f64 / 2.0;
1199 for rank in ranks.iter_mut().take(j).skip(i) {
1200 *rank = avg_rank;
1201 }
1202 i = j;
1203 }
1204 ranks
1205}
1206
1207fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1209 let n = sorted.len();
1210 let mut correction = 0.0;
1211 let mut i = 0;
1212 while i < n {
1213 let mut j = i + 1;
1214 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1215 j += 1;
1216 }
1217 let t = (j - i) as f64;
1218 if t > 1.0 {
1219 correction += t * (t * t - 1.0);
1220 }
1221 i = j;
1222 }
1223 correction
1224}
1225
1226pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1258 let k = groups.len();
1259 if k < 2 {
1260 return None;
1261 }
1262 for g in groups {
1263 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1264 return None;
1265 }
1266 }
1267
1268 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1269 let nf = total_n as f64;
1270
1271 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1273 for (gi, g) in groups.iter().enumerate() {
1274 for &v in *g {
1275 combined.push((v, gi));
1276 }
1277 }
1278 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1279
1280 let ranks = average_ranks(&combined);
1281
1282 let mut rank_sums = vec![0.0; k];
1284 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1285 rank_sums[*gi] += r;
1286 }
1287
1288 let mean_rank = (nf + 1.0) / 2.0;
1290 let mut h = 0.0;
1291 for (gi, g) in groups.iter().enumerate() {
1292 let ni = g.len() as f64;
1293 let mean_rank_i = rank_sums[gi] / ni;
1294 h += ni * (mean_rank_i - mean_rank).powi(2);
1295 }
1296 h *= 12.0 / (nf * (nf + 1.0));
1297
1298 let tie_corr = compute_tie_correction(&combined);
1300 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1301 if denom > 1e-15 {
1302 h /= denom;
1303 }
1304
1305 let df = (k - 1) as f64;
1306 let p_value = special::chi_squared_sf(h, df);
1307
1308 Some(TestResult {
1309 statistic: h,
1310 df,
1311 p_value,
1312 })
1313}
1314
1315pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1351 let k = groups.len();
1352 if k < 2 {
1353 return None;
1354 }
1355 for g in groups {
1356 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1357 return None;
1358 }
1359 }
1360
1361 let z_groups: Vec<Vec<f64>> = groups
1363 .iter()
1364 .map(|g| {
1365 let median = stats::median(g).unwrap_or(0.0);
1366 g.iter().map(|&x| (x - median).abs()).collect()
1367 })
1368 .collect();
1369
1370 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1372 let anova = one_way_anova(&z_refs)?;
1373
1374 Some(TestResult {
1375 statistic: anova.f_statistic,
1376 df: anova.df_between as f64,
1377 p_value: anova.p_value,
1378 })
1379}
1380
1381pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1394 if p_values.is_empty() || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1395 return None;
1396 }
1397 let m = p_values.len() as f64;
1398 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1399}
1400
1401pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1421 let m = p_values.len();
1422 if m == 0 || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1423 return None;
1424 }
1425
1426 let mut indices: Vec<usize> = (0..m).collect();
1428 indices.sort_by(|&a, &b| {
1429 p_values[a]
1430 .partial_cmp(&p_values[b])
1431 .unwrap_or(std::cmp::Ordering::Equal)
1432 });
1433
1434 let mf = m as f64;
1435 let mut adjusted = vec![0.0; m];
1436
1437 let mut cummin = f64::INFINITY;
1439 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1440 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1442 cummin = cummin.min(adj);
1443 adjusted[orig_idx] = cummin;
1444 }
1445
1446 Some(adjusted)
1447}
1448
1449pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1486 let k = groups.len();
1487 if k < 2 {
1488 return None;
1489 }
1490
1491 let mut sizes = Vec::with_capacity(k);
1492 let mut vars = Vec::with_capacity(k);
1493 let mut n_total: usize = 0;
1494
1495 for g in groups {
1496 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1497 return None;
1498 }
1499 let n = g.len();
1500 let v = stats::variance(g)?;
1501 if v <= 0.0 {
1502 return None; }
1504 sizes.push(n);
1505 vars.push(v);
1506 n_total += n;
1507 }
1508
1509 let nk = n_total - k; if nk == 0 {
1511 return None;
1512 }
1513 let nk_f = nk as f64;
1514
1515 let s2_pooled: f64 = sizes
1517 .iter()
1518 .zip(vars.iter())
1519 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1520 .sum::<f64>()
1521 / nk_f;
1522
1523 if s2_pooled <= 0.0 {
1524 return None;
1525 }
1526
1527 let num = nk_f * s2_pooled.ln()
1529 - sizes
1530 .iter()
1531 .zip(vars.iter())
1532 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1533 .sum::<f64>();
1534
1535 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1537 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1538
1539 let statistic = num / c;
1540 let df = (k - 1) as f64;
1541 let p_value = special::chi_squared_sf(statistic, df);
1542
1543 Some(TestResult {
1544 statistic,
1545 df,
1546 p_value,
1547 })
1548}
1549
1550pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1595 let row1 = a + b;
1596 let row2 = c + d;
1597 let col1 = a + c;
1598 let col2 = b + d;
1599 let n = a + b + c + d;
1600
1601 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1603 return None;
1604 }
1605
1606 let log_prob = |a_i: u64| -> f64 {
1608 let b_i = row1 - a_i;
1609 let c_i = col1 - a_i;
1610 let d_i = row2 - c_i;
1611 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1612 - ln_factorial(a_i)
1613 - ln_factorial(b_i)
1614 - ln_factorial(c_i)
1615 - ln_factorial(d_i)
1616 - ln_factorial(n)
1617 };
1618
1619 let a_min = col1.saturating_sub(row2);
1621 let a_max = row1.min(col1);
1622
1623 let log_p_obs = log_prob(a);
1624
1625 let mut p_value = 0.0;
1627 for a_i in a_min..=a_max {
1628 let lp = log_prob(a_i);
1629 if lp <= log_p_obs + 1e-10 {
1631 p_value += lp.exp();
1632 }
1633 }
1634
1635 let p_value = p_value.min(1.0);
1637
1638 let odds_ratio = if b > 0 && c > 0 {
1640 (a as f64 * d as f64) / (b as f64 * c as f64)
1641 } else {
1642 f64::INFINITY
1643 };
1644
1645 Some(TestResult {
1646 statistic: odds_ratio,
1647 df: 1.0,
1648 p_value,
1649 })
1650}
1651
1652#[derive(Debug, Clone, Copy)]
1658pub struct MannKendallResult {
1659 pub s_statistic: i64,
1661 pub variance: f64,
1663 pub z_statistic: f64,
1665 pub p_value: f64,
1667 pub kendall_tau: f64,
1669 pub sen_slope: f64,
1671}
1672
1673pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1711 let n = data.len();
1712 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1713 return None;
1714 }
1715
1716 let mut s: i64 = 0;
1718 for i in 0..n - 1 {
1719 for j in (i + 1)..n {
1720 let diff = data[j] - data[i];
1721 if diff > 0.0 {
1722 s += 1;
1723 } else if diff < 0.0 {
1724 s -= 1;
1725 }
1726 }
1727 }
1728
1729 let mut sorted: Vec<f64> = data.to_vec();
1731 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1732
1733 let mut tie_correction: f64 = 0.0;
1734 let mut current_count: usize = 1;
1735 for i in 1..sorted.len() {
1736 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1737 current_count += 1;
1738 } else {
1739 if current_count > 1 {
1740 let t = current_count as f64;
1741 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1742 }
1743 current_count = 1;
1744 }
1745 }
1746 if current_count > 1 {
1747 let t = current_count as f64;
1748 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1749 }
1750
1751 let nf = n as f64;
1753 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1754
1755 if variance < 1e-300 {
1756 return None; }
1758
1759 let sigma = variance.sqrt();
1761 let z_statistic = if s > 0 {
1762 (s as f64 - 1.0) / sigma
1763 } else if s < 0 {
1764 (s as f64 + 1.0) / sigma
1765 } else {
1766 0.0
1767 };
1768
1769 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1771 let p_value = p_value.clamp(0.0, 1.0);
1772
1773 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1775
1776 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1778 for i in 0..n - 1 {
1779 for j in (i + 1)..n {
1780 let dx = (j - i) as f64;
1781 slopes.push((data[j] - data[i]) / dx);
1782 }
1783 }
1784 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1785 let m = slopes.len();
1786 let sen_slope = if m % 2 == 0 {
1787 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1788 } else {
1789 slopes[m / 2]
1790 };
1791
1792 Some(MannKendallResult {
1793 s_statistic: s,
1794 variance,
1795 z_statistic,
1796 p_value,
1797 kendall_tau,
1798 sen_slope,
1799 })
1800}
1801
1802fn ln_factorial(n: u64) -> f64 {
1804 if n <= 1 {
1805 return 0.0;
1806 }
1807 special::ln_gamma(n as f64 + 1.0)
1809}
1810
1811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1817pub enum KpssModel {
1818 Level,
1820 Trend,
1822}
1823
1824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1826pub enum KpssLags {
1827 Hobijn,
1830 Schwert,
1832 Fixed(usize),
1834}
1835
1836#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1838pub enum KpssPValue {
1839 Interpolated,
1841 AtMost,
1843 AtLeast,
1845}
1846
1847#[derive(Debug, Clone)]
1849pub struct KpssResult {
1850 pub statistic: f64,
1852 pub n_lags: usize,
1854 pub p_value: f64,
1857 pub p_value_bound: KpssPValue,
1859 pub critical_values: [f64; 4],
1861 pub rejected: [bool; 4],
1863}
1864
1865pub const KPSS_LEVELS: [f64; 4] = [0.10, 0.05, 0.025, 0.01];
1867
1868pub fn kpss_test(data: &[f64], model: KpssModel, lags: KpssLags) -> Option<KpssResult> {
1912 let n = data.len();
1913 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1914 return None;
1915 }
1916 let nf = n as f64;
1917
1918 let residuals: Vec<f64> = match model {
1919 KpssModel::Level => {
1920 let mean = data.iter().sum::<f64>() / nf;
1921 data.iter().map(|x| x - mean).collect()
1922 }
1923 KpssModel::Trend => {
1924 let t_mean = (nf + 1.0) / 2.0;
1926 let y_mean = data.iter().sum::<f64>() / nf;
1927 let (mut sty, mut stt) = (0.0, 0.0);
1928 for (i, y) in data.iter().enumerate() {
1929 let dt = (i + 1) as f64 - t_mean;
1930 sty += dt * (y - y_mean);
1931 stt += dt * dt;
1932 }
1933 let slope = sty / stt;
1934 data.iter()
1935 .enumerate()
1936 .map(|(i, y)| y - y_mean - slope * ((i + 1) as f64 - t_mean))
1937 .collect()
1938 }
1939 };
1940
1941 let sum_sq: f64 = residuals.iter().map(|e| e * e).sum();
1943 let size: f64 = data.iter().map(|x| x * x).sum();
1944 if sum_sq <= 1e-20 * size {
1945 return None;
1946 }
1947
1948 let autocovariance = |i: usize| -> f64 {
1949 residuals[i..]
1950 .iter()
1951 .zip(&residuals[..n - i])
1952 .map(|(a, b)| a * b)
1953 .sum()
1954 };
1955
1956 let n_lags = match lags {
1957 KpssLags::Fixed(l) if l >= n => return None,
1958 KpssLags::Fixed(l) => l,
1959 KpssLags::Schwert => ((12.0 * (nf / 100.0).powf(0.25)).ceil() as usize).min(n - 1),
1960 KpssLags::Hobijn => {
1961 let cov_lags = nf.powf(2.0 / 9.0) as usize;
1963 let mut s0 = sum_sq / nf;
1964 let mut s1 = 0.0;
1965 for i in 1..=cov_lags.min(n - 1) {
1966 let g = autocovariance(i) / (nf / 2.0);
1967 s0 += g;
1968 s1 += i as f64 * g;
1969 }
1970 let s_hat = s1 / s0;
1971 let gamma = 1.1447 * (s_hat * s_hat).powf(1.0 / 3.0);
1972 ((gamma * nf.powf(1.0 / 3.0)) as usize).min(n - 1)
1973 }
1974 };
1975
1976 let mut long_run = sum_sq;
1977 for i in 1..=n_lags {
1978 long_run += 2.0 * autocovariance(i) * (1.0 - i as f64 / (n_lags as f64 + 1.0));
1979 }
1980 let long_run = long_run / nf;
1981 if long_run <= 0.0 {
1982 return None;
1983 }
1984
1985 let mut partial = 0.0;
1986 let eta = residuals
1987 .iter()
1988 .map(|e| {
1989 partial += e;
1990 partial * partial
1991 })
1992 .sum::<f64>()
1993 / (nf * nf);
1994 let statistic = eta / long_run;
1995
1996 let critical_values = match model {
1997 KpssModel::Level => [0.347, 0.463, 0.574, 0.739],
1998 KpssModel::Trend => [0.119, 0.146, 0.176, 0.216],
1999 };
2000 let (p_value, p_value_bound) = if statistic <= critical_values[0] {
2001 (KPSS_LEVELS[0], KpssPValue::AtLeast)
2002 } else if statistic >= critical_values[3] {
2003 (KPSS_LEVELS[3], KpssPValue::AtMost)
2004 } else {
2005 let k = critical_values
2006 .windows(2)
2007 .position(|w| statistic <= w[1])
2008 .expect("the statistic lies inside the table");
2009 let (c0, c1) = (critical_values[k], critical_values[k + 1]);
2010 let (p0, p1) = (KPSS_LEVELS[k], KPSS_LEVELS[k + 1]);
2011 (
2012 p0 + (statistic - c0) / (c1 - c0) * (p1 - p0),
2013 KpssPValue::Interpolated,
2014 )
2015 };
2016 let rejected = critical_values.map(|c| statistic > c);
2017
2018 Some(KpssResult {
2019 statistic,
2020 n_lags,
2021 p_value,
2022 p_value_bound,
2023 critical_values,
2024 rejected,
2025 })
2026}
2027
2028#[cfg(test)]
2031mod kpss_reference {
2032 use super::*;
2033
2034 const AR: [f64; 60] = [
2035 0.0, 0.299, -0.154, -0.953, -0.836, -1.326, -0.471, 1.152, -0.031, -0.633, 0.237, 0.452,
2036 0.286, -0.816, -0.355, 0.553, -1.123, -0.907, -2.264, -2.196, -2.72, -1.323, -1.796,
2037 -0.447, -0.022, -0.196, -2.595, -1.577, -0.68, -0.159, -1.594, -1.115, -1.425, -1.379,
2038 0.509, -0.604, -0.275, 0.774, -0.274, -0.222, 0.021, 0.073, -1.196, -0.402, 1.198, -1.068,
2039 0.432, 0.292, -0.524, 1.79, 1.478, -0.608, -0.168, 0.51, 0.015, 0.689, 0.209, 0.75, 1.739,
2040 0.02,
2041 ];
2042 const RW: [f64; 60] = [
2043 0.001, 0.3, 0.026, -0.865, -1.32, -2.312, -2.252, -0.912, -1.404, -2.024, -1.534, -1.177,
2044 -1.072, -2.002, -2.031, -1.336, -2.68, -3.138, -5.039, -6.329, -8.171, -8.406, -9.673,
2045 -9.402, -9.245, -9.432, -11.949, -12.488, -12.537, -12.424, -13.954, -14.432, -15.411,
2046 -16.22, -15.159, -15.967, -16.0, -15.116, -15.7, -15.812, -15.702, -15.638, -16.863,
2047 -16.787, -15.428, -16.975, -16.116, -15.997, -16.638, -14.638, -13.876, -15.075, -15.0,
2048 -14.423, -14.612, -13.929, -13.996, -13.329, -11.89, -12.566,
2049 ];
2050
2051 #[test]
2052 fn statistic_lag_and_p_value_reproduce_statsmodels() {
2053 use KpssLags::{Fixed, Hobijn, Schwert};
2054 use KpssModel::{Level, Trend};
2055 type Case = (&'static [f64; 60], KpssModel, KpssLags, f64, f64, usize);
2056 let cases: [Case; 12] = [
2057 (
2058 &AR,
2059 Level,
2060 Hobijn,
2061 0.5229270606681895,
2062 0.036502914263921285,
2063 3,
2064 ),
2065 (&AR, Level, Schwert, 0.288001639308506, 0.1, 11),
2066 (
2067 &AR,
2068 Level,
2069 Fixed(4),
2070 0.46189194657609306,
2071 0.05047760923444266,
2072 4,
2073 ),
2074 (&AR, Trend, Hobijn, 0.2274846895383343, 0.01, 3),
2075 (
2076 &AR,
2077 Trend,
2078 Schwert,
2079 0.14193842821368732,
2080 0.05752142923391235,
2081 11,
2082 ),
2083 (
2084 &AR,
2085 Trend,
2086 Fixed(4),
2087 0.20574827757371436,
2088 0.013844395909857114,
2089 4,
2090 ),
2091 (&RW, Level, Hobijn, 0.9286533322383663, 0.01, 5),
2092 (
2093 &RW,
2094 Level,
2095 Schwert,
2096 0.5112288964656923,
2097 0.0391376359311504,
2098 11,
2099 ),
2100 (&RW, Level, Fixed(4), 1.09862199299003, 0.01, 4),
2101 (&RW, Trend, Hobijn, 0.26866997231516837, 0.01, 4),
2102 (
2103 &RW,
2104 Trend,
2105 Schwert,
2106 0.14468829763941088,
2107 0.05242907844553539,
2108 11,
2109 ),
2110 (&RW, Trend, Fixed(4), 0.26866997231516837, 0.01, 4),
2111 ];
2112 for (data, model, lags, statistic, p, n_lags) in cases {
2113 let r = kpss_test(data, model, lags).unwrap();
2114 let what = format!("{model:?} {lags:?}");
2115 assert_eq!(r.n_lags, n_lags, "{what}");
2116 assert!(
2117 (r.statistic - statistic).abs() < 1e-10 * statistic,
2118 "{what}: {} vs {statistic}",
2119 r.statistic
2120 );
2121 assert!(
2122 (r.p_value - p).abs() < 1e-12,
2123 "{what}: p {} vs {p}",
2124 r.p_value
2125 );
2126 }
2127 }
2128
2129 #[test]
2130 fn bounds_rejections_and_refusals() {
2131 let r = kpss_test(&RW, KpssModel::Level, KpssLags::Fixed(4)).unwrap();
2132 assert_eq!(r.p_value_bound, KpssPValue::AtMost);
2133 assert_eq!(r.rejected, [true; 4]);
2134 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Schwert).unwrap();
2135 assert_eq!(r.p_value_bound, KpssPValue::AtLeast);
2136 assert_eq!(r.rejected, [false; 4]);
2137 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Hobijn).unwrap();
2138 assert_eq!(r.p_value_bound, KpssPValue::Interpolated);
2139 assert_eq!(r.rejected, [true, true, false, false]);
2140
2141 assert!(kpss_test(&AR[..9], KpssModel::Level, KpssLags::Hobijn).is_none());
2142 assert!(kpss_test(&AR, KpssModel::Level, KpssLags::Fixed(60)).is_none());
2143 assert!(kpss_test(&[4.0; 20], KpssModel::Level, KpssLags::Hobijn).is_none());
2144 let line: Vec<f64> = (0..20).map(|i| 2.0 + 0.5 * i as f64).collect();
2145 assert!(kpss_test(&line, KpssModel::Trend, KpssLags::Hobijn).is_none());
2146 }
2147
2148 #[test]
2150 fn unit_invariant() {
2151 let base = kpss_test(&AR, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2152 for f in [1e-9, 1e9] {
2153 let x: Vec<f64> = AR.iter().map(|v| v * f).collect();
2154 let r = kpss_test(&x, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2155 assert_eq!(r.n_lags, base.n_lags);
2156 assert!((r.statistic - base.statistic).abs() < 1e-9 * base.statistic);
2157 }
2158 }
2159}
2160
2161#[derive(Debug, Clone)]
2167pub struct AdfResult {
2168 pub statistic: f64,
2170 pub n_lags: usize,
2172 pub n_obs: usize,
2174 pub critical_values: [f64; 3],
2176 pub rejected: [bool; 3],
2178}
2179
2180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2182pub enum AdfLags {
2183 Aic {
2188 max: Option<usize>,
2190 },
2191 Fixed(usize),
2193}
2194
2195impl Default for AdfLags {
2196 fn default() -> Self {
2197 AdfLags::Aic { max: None }
2198 }
2199}
2200
2201#[derive(Debug, Clone, Copy)]
2203pub enum AdfModel {
2204 None,
2206 Constant,
2208 ConstantTrend,
2210}
2211
2212pub fn adf_test(data: &[f64], model: AdfModel, lags: AdfLags) -> Option<AdfResult> {
2249 let n = data.len();
2250 if n < 10 || data.iter().any(|v| !v.is_finite()) {
2251 return None;
2252 }
2253
2254 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
2256
2257 let best_lag = match lags {
2258 AdfLags::Fixed(p) => p,
2259 AdfLags::Aic { max } => {
2260 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
2262 let p_max = max.unwrap_or_else(|| schwert.min(n / 3));
2263 select_adf_lag(data, &dy, model, p_max)?
2264 }
2265 };
2266
2267 adf_ols(data, &dy, model, best_lag)
2269}
2270
2271fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> Option<usize> {
2276 let start = p_max;
2277 let mut best: Option<(f64, usize)> = None;
2278 for p in 0..=p_max {
2279 if let Some(aic) = adf_ols_aic(data, dy, model, p, start) {
2280 if best.is_none_or(|(b, _)| aic < b) {
2281 best = Some((aic, p));
2282 }
2283 }
2284 }
2285 best.map(|(_, p)| p)
2286}
2287
2288#[allow(clippy::type_complexity)]
2292fn adf_build_matrix(
2293 data: &[f64],
2294 dy: &[f64],
2295 model: AdfModel,
2296 p: usize,
2297 start: usize,
2298) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2299 debug_assert!(start >= p, "the first row needs its p lagged differences");
2301 if start >= dy.len() || dy.len() - start < 5 {
2302 return None;
2303 }
2304 let m = dy.len() - start;
2305
2306 let y_dep: Vec<f64> = dy[start..].to_vec();
2307
2308 let has_intercept = !matches!(model, AdfModel::None);
2310 let has_trend = matches!(model, AdfModel::ConstantTrend);
2311 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2312
2313 let mut x_data = Vec::with_capacity(m * ncols);
2315 let mut gamma_col = 0;
2316
2317 for i in 0..m {
2318 let t = start + i;
2319 if has_intercept {
2320 x_data.push(1.0); gamma_col = 1;
2322 }
2323 x_data.push(data[t]); if has_trend {
2325 x_data.push((t + 1) as f64); }
2327 for lag in 1..=p {
2328 x_data.push(dy[t - lag]);
2329 }
2330 }
2331
2332 Some((x_data, y_dep, m, ncols, gamma_col))
2333}
2334
2335fn adf_ols_core(
2339 x_data: &[f64],
2340 y: &[f64],
2341 m: usize,
2342 ncols: usize,
2343 gamma_col: usize,
2344) -> Option<(f64, f64, usize)> {
2345 let mut xtx = vec![0.0_f64; ncols * ncols];
2347 for i in 0..m {
2348 let row = &x_data[i * ncols..(i + 1) * ncols];
2349 for j in 0..ncols {
2350 for k in j..ncols {
2351 xtx[j * ncols + k] += row[j] * row[k];
2352 }
2353 }
2354 }
2355 for j in 0..ncols {
2357 for k in (j + 1)..ncols {
2358 xtx[k * ncols + j] = xtx[j * ncols + k];
2359 }
2360 }
2361
2362 let mut xty = vec![0.0_f64; ncols];
2364 for i in 0..m {
2365 let row = &x_data[i * ncols..(i + 1) * ncols];
2366 for j in 0..ncols {
2367 xty[j] += row[j] * y[i];
2368 }
2369 }
2370
2371 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2373 for i in 0..ncols {
2374 for j in 0..ncols {
2375 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2376 }
2377 augmented[i * (ncols + 1) + ncols] = xty[i];
2378 }
2379
2380 for col in 0..ncols {
2381 let mut max_row = col;
2383 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2384 for row in (col + 1)..ncols {
2385 let val = augmented[row * (ncols + 1) + col].abs();
2386 if val > max_val {
2387 max_val = val;
2388 max_row = row;
2389 }
2390 }
2391 if max_val < 1e-15 {
2392 return None; }
2394 if max_row != col {
2395 for j in 0..=ncols {
2396 let a = col * (ncols + 1) + j;
2397 let b = max_row * (ncols + 1) + j;
2398 augmented.swap(a, b);
2399 }
2400 }
2401
2402 let pivot = augmented[col * (ncols + 1) + col];
2403 for row in (col + 1)..ncols {
2404 let factor = augmented[row * (ncols + 1) + col] / pivot;
2405 for j in col..=ncols {
2406 let above = augmented[col * (ncols + 1) + j];
2407 augmented[row * (ncols + 1) + j] -= factor * above;
2408 }
2409 }
2410 }
2411
2412 let mut beta = vec![0.0_f64; ncols];
2414 for i in (0..ncols).rev() {
2415 let mut sum = augmented[i * (ncols + 1) + ncols];
2416 for j in (i + 1)..ncols {
2417 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2418 }
2419 beta[i] = sum / augmented[i * (ncols + 1) + i];
2420 }
2421
2422 let mut rss = 0.0;
2424 for i in 0..m {
2425 let row = &x_data[i * ncols..(i + 1) * ncols];
2426 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2427 let resid = y[i] - y_hat;
2428 rss += resid * resid;
2429 }
2430
2431 let yy: f64 = y.iter().map(|v| v * v).sum();
2437 if yy == 0.0 || rss <= yy * 1e-16 {
2438 return None;
2439 }
2440
2441 let df = m - ncols;
2443 if df == 0 {
2444 return None;
2445 }
2446 let mse = rss / df as f64;
2447
2448 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2451 for j in 0..ncols {
2452 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2453 }
2454 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2455 }
2456
2457 for col in 0..ncols {
2459 let mut max_row = col;
2460 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2461 for row in (col + 1)..ncols {
2462 let val = xtx_aug[row * 2 * ncols + col].abs();
2463 if val > max_val {
2464 max_val = val;
2465 max_row = row;
2466 }
2467 }
2468 if max_val < 1e-15 {
2469 return None;
2470 }
2471 if max_row != col {
2472 for j in 0..(2 * ncols) {
2473 let a = col * 2 * ncols + j;
2474 let b = max_row * 2 * ncols + j;
2475 xtx_aug.swap(a, b);
2476 }
2477 }
2478
2479 let pivot = xtx_aug[col * 2 * ncols + col];
2480 for j in 0..(2 * ncols) {
2481 xtx_aug[col * 2 * ncols + j] /= pivot;
2482 }
2483 for row in 0..ncols {
2484 if row == col {
2485 continue;
2486 }
2487 let factor = xtx_aug[row * 2 * ncols + col];
2488 for j in 0..(2 * ncols) {
2489 let above = xtx_aug[col * 2 * ncols + j];
2490 xtx_aug[row * 2 * ncols + j] -= factor * above;
2491 }
2492 }
2493 }
2494
2495 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2497 if var_gamma <= 0.0 {
2498 return None;
2499 }
2500 let se_gamma = var_gamma.sqrt();
2501 let t_gamma = beta[gamma_col] / se_gamma;
2502
2503 Some((t_gamma, rss, ncols))
2504}
2505
2506fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize, start: usize) -> Option<f64> {
2509 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, start)?;
2510 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2511 Some(2.0 * k as f64 + m as f64 * (rss / m as f64).ln())
2512}
2513
2514fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2516 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, p)?;
2517 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2518
2519 let critical_values = adf_critical_values(model, m);
2520
2521 let rejected = [
2522 gamma_t <= critical_values[0],
2523 gamma_t <= critical_values[1],
2524 gamma_t <= critical_values[2],
2525 ];
2526
2527 Some(AdfResult {
2528 statistic: gamma_t,
2529 n_lags: p,
2530 n_obs: m,
2531 critical_values,
2532 rejected,
2533 })
2534}
2535
2536fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2540 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2545 AdfModel::None => (
2546 [-2.5658, -1.9393, -1.6156],
2547 [-1.960, -0.398, -0.181],
2548 [-10.04, 0.0, 0.0],
2549 ),
2550 AdfModel::Constant => (
2551 [-3.4336, -2.8621, -2.5671],
2552 [-5.999, -2.738, -1.438],
2553 [-29.25, -8.36, -4.48],
2554 ),
2555 AdfModel::ConstantTrend => (
2556 [-3.9638, -3.4126, -3.1279],
2557 [-8.353, -4.039, -2.418],
2558 [-47.44, -17.83, -7.58],
2559 ),
2560 };
2561
2562 let nf = n as f64;
2563 let inv_n = 1.0 / nf;
2564 let inv_n2 = inv_n * inv_n;
2565
2566 [
2567 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2568 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2569 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2570 ]
2571}
2572
2573#[cfg(test)]
2574mod tests {
2575 use super::*;
2576
2577 #[test]
2578 fn corrections_refuse_what_is_not_a_probability() {
2579 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2580 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2581 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2582 }
2583
2584 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2590 let mut a = seed;
2591 move || {
2592 a = a.wrapping_add(0x6d2b_79f5);
2593 let mut t = a;
2594 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2595 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2596 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2597 }
2598 }
2599
2600 #[test]
2606 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2607 let mut rng = mulberry32(0x5350_4331);
2608 let full: Vec<f64> = (0..20_000)
2609 .map(|_| {
2610 let u = rng();
2611 -(1.0 - u).ln() })
2613 .collect();
2614
2615 let mut prev_a2 = 0.0_f64;
2616 let mut prev_p = f64::INFINITY;
2617 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2618 let r = anderson_darling_test(&full[..n]).expect("computes");
2619 assert!(
2621 r.statistic_star > prev_a2,
2622 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2623 r.statistic_star
2624 );
2625 assert!(
2627 r.p_value < 0.5,
2628 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2629 r.statistic_star,
2630 r.p_value
2631 );
2632 assert!(
2634 r.p_value <= prev_p + 1e-12,
2635 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2636 r.p_value
2637 );
2638 prev_a2 = r.statistic_star;
2639 prev_p = r.p_value;
2640 }
2641 }
2642
2643 #[test]
2648 fn one_sample_null_true() {
2649 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2650 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2651 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2652 }
2653
2654 #[test]
2655 fn one_sample_null_false() {
2656 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2657 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2658 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2659 }
2660
2661 #[test]
2662 fn one_sample_edge_cases() {
2663 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());
2666 }
2667
2668 #[test]
2673 fn two_sample_same_mean() {
2674 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2675 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2676 let r = two_sample_t_test(&a, &b).expect("should compute");
2677 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2678 }
2679
2680 #[test]
2681 fn two_sample_different_means() {
2682 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2683 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2684 let r = two_sample_t_test(&a, &b).expect("should compute");
2685 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2686 }
2687
2688 #[test]
2689 fn two_sample_different_sizes() {
2690 let a = [1.0, 2.0, 3.0];
2691 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2692 let r = two_sample_t_test(&a, &b).expect("should compute");
2693 assert!(r.p_value < 0.05);
2694 }
2695
2696 #[test]
2697 fn two_sample_edge_cases() {
2698 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2699 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2700 }
2701
2702 #[test]
2707 fn paired_no_difference() {
2708 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2709 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2710 let r = paired_t_test(&x, &y).expect("should compute");
2711 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2712 }
2713
2714 #[test]
2715 fn paired_significant_difference() {
2716 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2718 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2719 let r = paired_t_test(&before, &after).expect("should compute");
2720 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2721 assert!(r.statistic < 0.0); }
2723
2724 #[test]
2725 fn paired_edge_cases() {
2726 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2729
2730 #[test]
2735 fn anova_same_means() {
2736 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2737 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2738 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2739 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2740 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2741 }
2742
2743 #[test]
2744 fn anova_different_means() {
2745 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2746 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2747 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2748 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2749 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2750 assert!(r.df_between == 2);
2751 assert!(r.df_within == 12);
2752 }
2753
2754 #[test]
2755 fn anova_ss_decomposition() {
2756 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2757 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2758 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2759 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2761 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2762 assert!(
2763 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2764 "SS decomposition: {ss_total} vs {} + {}",
2765 r.ss_between,
2766 r.ss_within
2767 );
2768 }
2769
2770 #[test]
2771 fn anova_edge_cases() {
2772 let g1 = [1.0, 2.0, 3.0];
2773 assert!(one_way_anova(&[&g1]).is_none()); }
2775
2776 #[test]
2781 fn chi2_gof_uniform() {
2782 let observed = [25.0, 25.0, 25.0, 25.0];
2784 let expected = [25.0, 25.0, 25.0, 25.0];
2785 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2786 assert!((r.statistic).abs() < 1e-15);
2787 assert!((r.p_value - 1.0).abs() < 0.01);
2788 }
2789
2790 #[test]
2791 fn chi2_gof_significant() {
2792 let observed = [90.0, 10.0];
2793 let expected = [50.0, 50.0];
2794 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2795 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2796 }
2797
2798 #[test]
2799 fn chi2_gof_edge_cases() {
2800 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()); }
2804
2805 #[test]
2810 fn chi2_independence_significant() {
2811 let table = [30.0, 10.0, 10.0, 50.0];
2813 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2814 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2815 assert!((r.df - 1.0).abs() < 1e-10);
2816 }
2817
2818 #[test]
2819 fn chi2_independence_not_significant() {
2820 let table = [25.0, 25.0, 25.0, 25.0];
2822 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2823 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2824 }
2825
2826 #[test]
2827 fn chi2_independence_3x3() {
2828 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2829 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2830 assert!((r.df - 4.0).abs() < 1e-10);
2831 assert!(r.p_value < 0.05);
2832 }
2833
2834 #[test]
2835 fn chi2_independence_edge_cases() {
2836 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()); }
2840
2841 #[test]
2846 fn jb_normal_data() {
2847 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2849 let r = jarque_bera_test(&data).expect("should compute");
2850 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2851 }
2852
2853 #[test]
2854 fn jb_skewed_data() {
2855 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2857 let r = jarque_bera_test(&data).expect("should compute");
2858 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2859 }
2860
2861 #[test]
2868 fn jb_is_the_moment_form_the_paper_defines() {
2869 let data = [
2870 2.1, 3.4, 1.9, 5.6, 2.2, 3.1, 4.8, 2.9, 3.3, 7.2, 2.5, 3.0, 4.1, 2.7, 3.8, 2.4, 6.1,
2871 3.6, 2.8, 3.2,
2872 ];
2873 let r = jarque_bera_test(&data).expect("20 finite points");
2874 assert!(
2875 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2876 "JB = {}",
2877 r.statistic
2878 );
2879
2880 let n = data.len() as f64;
2883 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2884 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2885 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2886 assert!(
2887 adjusted > r.statistic * 1.3,
2888 "the two forms differ by more than a third here: {adjusted} vs {}",
2889 r.statistic
2890 );
2891 }
2892
2893 #[test]
2897 fn jb_p_value_keeps_its_digits_in_the_tail() {
2898 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2901 data.push(60.0);
2902 let r = jarque_bera_test(&data).expect("20 finite points");
2903 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2904 assert!(
2905 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2906 "this sample is only interesting because the complement underflows"
2907 );
2908 assert!(
2909 r.p_value > 0.0,
2910 "the complement returns 0 here; this must not"
2911 );
2912 assert!(
2913 (r.p_value - 6.702e-21).abs() < 1e-23,
2914 "p = {} (expected about 6.7e-21)",
2915 r.p_value
2916 );
2917 assert!(
2918 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2919 "p = {} vs exp(-JB/2) = {}",
2920 r.p_value,
2921 (-r.statistic / 2.0).exp()
2922 );
2923 let small = jarque_bera_test(&[-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0])
2926 .expect("10 finite points");
2927 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2928 assert!(
2929 (small.p_value - via_cdf).abs() < 1e-12,
2930 "{} vs {via_cdf}",
2931 small.p_value
2932 );
2933 }
2934
2935 #[test]
2936 fn jb_edge_cases() {
2937 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2939
2940 #[test]
2945 fn ad_normal_data() {
2946 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2947 let r = anderson_darling_test(&data).expect("should compute");
2948 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2949 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2950 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2951 }
2952
2953 #[test]
2954 fn ad_skewed_data() {
2955 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];
2957 let r = anderson_darling_test(&data).expect("should compute");
2958 assert!(
2959 r.p_value < 0.05,
2960 "p = {} (should reject normality)",
2961 r.p_value
2962 );
2963 }
2964
2965 #[test]
2966 fn ad_bimodal_data() {
2967 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2969 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2970 let r = anderson_darling_test(&data).expect("should compute");
2971 assert!(
2972 r.p_value < 0.01,
2973 "p = {} (bimodal should reject normality)",
2974 r.p_value
2975 );
2976 }
2977
2978 #[test]
2979 fn ad_large_normal_sample() {
2980 let n = 100;
2981 let data: Vec<f64> = (1..=n)
2982 .map(|i| {
2983 let p = (i as f64 - 0.5) / n as f64;
2984 special::inverse_normal_cdf(p)
2985 })
2986 .collect();
2987 let r = anderson_darling_test(&data).expect("should compute");
2988 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2989 }
2990
2991 #[test]
2992 fn ad_edge_cases() {
2993 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());
2996 }
2997
2998 #[test]
2999 fn ad_p_value_ranges() {
3000 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
3005 let r = anderson_darling_test(&near_normal).expect("should compute");
3006 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3007
3008 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];
3010 let r = anderson_darling_test(&heavy_tail).expect("should compute");
3011 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3012 }
3013
3014 #[test]
3019 fn ad_normal_data_large_p() {
3020 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3022 let r = anderson_darling_test(&data).unwrap();
3023 assert!(r.p_value > 0.05, "p={}", r.p_value);
3024 }
3025
3026 #[test]
3027 fn ad_exponential_data_small_p() {
3028 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3030 let r = anderson_darling_test(&data).unwrap();
3031 assert!(r.p_value < 0.05, "p={}", r.p_value);
3032 }
3033
3034 #[test]
3035 fn ad_statistic_non_negative() {
3036 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3037 let r = anderson_darling_test(&data).unwrap();
3038 assert!(r.statistic >= 0.0);
3039 assert!(r.statistic_star >= r.statistic);
3040 }
3041
3042 #[test]
3043 fn ad_p_value_in_range() {
3044 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
3045 let r = anderson_darling_test(&data).unwrap();
3046 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3047 }
3048
3049 #[test]
3050 fn ad_insufficient_data() {
3051 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
3053
3054 #[test]
3055 fn ad_degenerate_data() {
3056 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3058 }
3059
3060 #[test]
3061 fn ad_modified_statistic_formula() {
3062 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
3064 let r = anderson_darling_test(&data).unwrap();
3065 assert!(r.statistic_star > r.statistic - 1e-10);
3066 assert!(anderson_darling_test(&data[..7]).is_none());
3068 }
3069
3070 #[test]
3075 fn ad_correction_factor_formula() {
3076 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3077 let n = data.len() as f64;
3078 let r = anderson_darling_test(&data).unwrap();
3079
3080 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
3082 assert!(
3083 (r.statistic_star - expected_star).abs() < 1e-12,
3084 "A²* = {}, expected = {}",
3085 r.statistic_star,
3086 expected_star
3087 );
3088
3089 assert!(
3091 r.statistic >= 0.0,
3092 "A² = {} must be non-negative",
3093 r.statistic
3094 );
3095 }
3096
3097 #[test]
3103 fn ad_formula_structure_symmetric_vs_skewed() {
3104 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
3106 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
3108
3109 let r_sym = anderson_darling_test(&symmetric).unwrap();
3110 let r_skew = anderson_darling_test(&skewed).unwrap();
3111
3112 assert!(
3113 r_sym.statistic < r_skew.statistic,
3114 "Symmetric A²={} should be less than skewed A²={}",
3115 r_sym.statistic,
3116 r_skew.statistic
3117 );
3118 }
3119
3120 #[test]
3125 fn ad_pvalue_invariants() {
3126 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3128 let r_normal = anderson_darling_test(&normal_data).unwrap();
3129 assert!(
3130 r_normal.p_value > 0.05,
3131 "Normal data: p = {} (expected > 0.05)",
3132 r_normal.p_value
3133 );
3134
3135 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3137 let r_exp = anderson_darling_test(&exp_data).unwrap();
3138 assert!(
3139 r_exp.p_value < 0.05,
3140 "Exponential data: p = {} (expected < 0.05)",
3141 r_exp.p_value
3142 );
3143 }
3144
3145 #[test]
3150 fn sw_normal_data() {
3151 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3153 let r = shapiro_wilk_test(&data).expect("should compute");
3154 assert!(r.w > 0.9, "W = {}", r.w);
3155 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3156 }
3157
3158 #[test]
3159 fn sw_bimodal_data() {
3160 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3162 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3163 let r = shapiro_wilk_test(&data).expect("should compute");
3164 assert!(
3165 r.p_value < 0.01,
3166 "p = {} (bimodal should reject normality)",
3167 r.p_value
3168 );
3169 }
3170
3171 #[test]
3172 fn sw_n3() {
3173 let data = [1.0, 2.0, 3.0];
3174 let r = shapiro_wilk_test(&data).expect("n=3 should work");
3175 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3176 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3177 }
3178
3179 #[test]
3180 fn sw_n4() {
3181 let data = [1.0, 2.0, 3.0, 4.0];
3182 let r = shapiro_wilk_test(&data).expect("n=4 should work");
3183 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3184 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3185 }
3186
3187 #[test]
3188 fn sw_n5() {
3189 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
3190 let r = shapiro_wilk_test(&data).expect("n=5 should work");
3191 assert!(r.w > 0.9, "W = {}", r.w);
3192 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3193 }
3194
3195 #[test]
3196 fn sw_skewed_data() {
3197 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];
3199 let r = shapiro_wilk_test(&data).expect("should compute");
3200 assert!(
3201 r.p_value < 0.05,
3202 "p = {} (skewed data should reject normality)",
3203 r.p_value
3204 );
3205 }
3206
3207 #[test]
3208 fn sw_large_normal_sample() {
3209 let n = 100;
3212 let data: Vec<f64> = (1..=n)
3213 .map(|i| {
3214 let p = (i as f64 - 0.5) / n as f64;
3215 special::inverse_normal_cdf(p)
3216 })
3217 .collect();
3218 let r = shapiro_wilk_test(&data).expect("should compute");
3219 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
3220 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3221 }
3222
3223 #[test]
3224 fn sw_w_bounded() {
3225 let datasets: Vec<Vec<f64>> = vec![
3227 vec![1.0, 2.0, 3.0],
3228 vec![1.0, 1.0, 2.0, 3.0, 3.0],
3229 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
3230 (0..20).map(|i| (i as f64).powi(2)).collect(),
3231 ];
3232 for (idx, data) in datasets.iter().enumerate() {
3233 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
3234 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
3235 assert!(
3236 r.p_value >= 0.0 && r.p_value <= 1.0,
3237 "dataset {idx}: p = {}",
3238 r.p_value
3239 );
3240 }
3241 }
3242
3243 #[test]
3244 fn sw_edge_cases() {
3245 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()); }
3251
3252 #[test]
3253 fn sw_n5001_rejected() {
3254 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
3255 assert!(shapiro_wilk_test(&data).is_none()); }
3257
3258 #[test]
3263 fn mw_clearly_different() {
3264 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
3265 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
3266 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3267 assert!(r.p_value < 0.05, "p = {}", r.p_value);
3268 }
3269
3270 #[test]
3271 fn mw_same_distribution() {
3272 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3273 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3274 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3275 assert!(
3276 r.p_value > 0.3,
3277 "p = {} (interleaved, same dist)",
3278 r.p_value
3279 );
3280 }
3281
3282 #[test]
3283 fn mw_with_ties() {
3284 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3285 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3286 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3287 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3288 }
3289
3290 #[test]
3291 fn mw_different_sizes() {
3292 let a = [1.0, 2.0, 3.0];
3293 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3294 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3295 assert!(r.p_value < 0.05);
3296 }
3297
3298 #[test]
3299 fn mw_edge_cases() {
3300 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());
3303 }
3304
3305 #[test]
3310 fn wilcoxon_significant_increase() {
3311 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3312 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3313 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3314 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3315 }
3316
3317 #[test]
3318 fn wilcoxon_no_difference() {
3319 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3320 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3321 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3322 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3323 }
3324
3325 #[test]
3326 fn wilcoxon_with_ties() {
3327 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3329 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3331 assert!(r.statistic > 0.0);
3333 }
3334
3335 #[test]
3336 fn wilcoxon_with_zero_diffs() {
3337 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3339 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3341 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3342 }
3343
3344 #[test]
3345 fn wilcoxon_edge_cases() {
3346 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());
3350 }
3351
3352 #[test]
3357 fn mann_whitney_small_samples_use_the_exact_distribution() {
3358 let cases = [
3360 (3, 3, 0, 0.1000, 0.0495),
3361 (4, 4, 0, 0.0286, 0.0209),
3362 (4, 4, 1, 0.0571, 0.0433),
3363 (5, 5, 2, 0.0317, 0.0283),
3364 (8, 8, 13, 0.0499, 0.0460),
3365 (10, 10, 23, 0.0433, 0.0413),
3366 ];
3367 for (n1, n2, u, exact, old) in cases {
3368 let (a, b) = separated_samples(n1, n2, u);
3369 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3370 assert_eq!(r.statistic, u as f64);
3371 assert!(
3372 (r.p_value - exact).abs() < 1e-4,
3373 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3374 r.p_value
3375 );
3376 }
3377 }
3378
3379 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3383 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3384 let mut remaining = u;
3385 for k in (0..n1).rev() {
3386 let room = n2.min(remaining);
3387 ranks_a[k] += room;
3388 remaining -= room;
3389 if remaining == 0 {
3390 break;
3391 }
3392 }
3393 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3394 (
3395 ranks_a.iter().map(|&r| r as f64).collect(),
3396 ranks_b.iter().map(|&r| r as f64).collect(),
3397 )
3398 }
3399
3400 #[test]
3402 fn wilcoxon_small_samples_use_the_exact_distribution() {
3403 let cases = [
3405 (5, 0, 0.0625),
3406 (5, 1, 0.125),
3407 (6, 2, 0.09375),
3408 (8, 5, 0.078125),
3409 (10, 8, 0.048828125),
3410 (12, 14, 0.0522460938),
3411 ];
3412 for (n, t_plus, exact) in cases {
3413 let positive = ranks_summing_to(n, t_plus);
3415 let x: Vec<f64> = (1..=n)
3416 .map(|r| {
3417 if positive.contains(&r) {
3418 r as f64
3419 } else {
3420 -(r as f64)
3421 }
3422 })
3423 .collect();
3424 let y = vec![0.0; n];
3425 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3426 assert_eq!(r.statistic, t_plus as f64);
3427 assert!(
3428 (r.p_value - exact).abs() < 1e-9,
3429 "n={n} T+={t_plus}: p={} want {exact}",
3430 r.p_value
3431 );
3432 }
3433 }
3434
3435 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3437 let mut out = Vec::new();
3438 let mut left = target;
3439 for r in (1..=n).rev() {
3440 if r <= left {
3441 out.push(r);
3442 left -= r;
3443 }
3444 }
3445 assert_eq!(left, 0);
3446 out
3447 }
3448
3449 #[test]
3452 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3453 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3454 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3455 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3456 let (n1, n2) = (60.0, 60.0);
3457 let mu = n1 * n2 / 2.0;
3458 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3459 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3460 let want = 2.0 * special::standard_normal_sf(z);
3461 assert!((r.p_value - want).abs() < 1e-12);
3462
3463 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3465 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3466 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3467 let mu = 1250.0;
3468 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3469 let u = mann_whitney_u_test(&a, &b)
3470 .expect("valid samples")
3471 .statistic;
3472 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3473 assert!(
3474 (exact - approx).abs() < 1e-3,
3475 "exact {exact} vs approx {approx}"
3476 );
3477 }
3478
3479 #[test]
3484 fn kw_clearly_different() {
3485 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3486 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3487 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3488 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3489 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3490 assert!((r.df - 2.0).abs() < 1e-10);
3491 }
3492
3493 #[test]
3494 fn kw_same_distribution() {
3495 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3496 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3497 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3498 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3499 }
3500
3501 #[test]
3502 fn kw_with_ties() {
3503 let g1 = [1.0, 2.0, 2.0, 3.0];
3504 let g2 = [3.0, 4.0, 4.0, 5.0];
3505 let g3 = [5.0, 6.0, 6.0, 7.0];
3506 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3507 assert!(r.statistic > 0.0);
3508 }
3509
3510 #[test]
3511 fn kw_edge_cases() {
3512 let g1 = [1.0, 2.0, 3.0];
3513 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3515
3516 #[test]
3521 fn levene_equal_variance() {
3522 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3523 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3524 let r = levene_test(&[&g1, &g2]).expect("should compute");
3525 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3527 }
3528
3529 #[test]
3530 fn levene_unequal_variance() {
3531 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");
3534 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3535 }
3536
3537 #[test]
3538 fn levene_three_groups() {
3539 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3540 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3541 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3542 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3543 assert!(r.df >= 2.0);
3544 }
3545
3546 #[test]
3547 fn levene_edge_cases() {
3548 let g1 = [1.0, 2.0, 3.0];
3549 assert!(levene_test(&[&g1]).is_none()); }
3551
3552 #[test]
3557 fn bonferroni_basic() {
3558 let ps = [0.01, 0.04, 0.03, 0.005];
3559 let adj = bonferroni_correction(&ps).expect("should compute");
3560 assert!((adj[0] - 0.04).abs() < 1e-10);
3561 assert!((adj[1] - 0.16).abs() < 1e-10);
3562 assert!((adj[2] - 0.12).abs() < 1e-10);
3563 assert!((adj[3] - 0.02).abs() < 1e-10);
3564 }
3565
3566 #[test]
3567 fn bonferroni_capped_at_one() {
3568 let ps = [0.5, 0.6];
3569 let adj = bonferroni_correction(&ps).expect("should compute");
3570 assert!((adj[0] - 1.0).abs() < 1e-10);
3571 assert!((adj[1] - 1.0).abs() < 1e-10);
3572 }
3573
3574 #[test]
3575 fn bh_basic() {
3576 let ps = [0.01, 0.04, 0.03, 0.005];
3577 let adj = benjamini_hochberg(&ps).expect("should compute");
3578 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3580 assert!(
3581 adjusted >= orig - 1e-15,
3582 "adj[{i}] = {adjusted} < original {orig}"
3583 );
3584 }
3585 }
3588
3589 #[test]
3590 fn bh_all_significant() {
3591 let ps = [0.001, 0.002, 0.003];
3592 let adj = benjamini_hochberg(&ps).expect("should compute");
3593 for &a in &adj {
3594 assert!(a < 0.05);
3595 }
3596 }
3597
3598 #[test]
3599 fn correction_edge_cases() {
3600 assert!(bonferroni_correction(&[]).is_none());
3601 assert!(benjamini_hochberg(&[]).is_none());
3602 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3603 }
3604
3605 #[test]
3610 fn bartlett_equal_variances() {
3611 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3612 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3613 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3614 assert!(
3615 r.p_value > 0.9,
3616 "equal variance → p high, got {}",
3617 r.p_value
3618 );
3619 }
3620
3621 #[test]
3622 fn bartlett_unequal_variances() {
3623 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");
3626 assert!(
3627 r.p_value < 0.01,
3628 "very different variances → p < 0.01, got {}",
3629 r.p_value
3630 );
3631 assert!((r.df - 1.0).abs() < 1e-10); }
3633
3634 #[test]
3635 fn bartlett_three_groups() {
3636 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3637 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3638 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3640 assert!(r.p_value < 0.05, "one group with high variance");
3641 assert!((r.df - 2.0).abs() < 1e-10); }
3643
3644 #[test]
3645 fn bartlett_edge_cases() {
3646 let g1 = [1.0, 2.0, 3.0];
3647 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3651
3652 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3654 }
3655
3656 #[test]
3661 fn fisher_tea_tasting() {
3662 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3664 assert!(
3666 (r.p_value - 0.4857).abs() < 0.01,
3667 "p ≈ 0.4857, got {}",
3668 r.p_value
3669 );
3670 }
3671
3672 #[test]
3673 fn fisher_significant() {
3674 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3676 assert!(r.p_value < 0.001, "perfect association → p very small");
3677 }
3678
3679 #[test]
3680 fn fisher_no_association() {
3681 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3683 assert!(
3684 r.p_value > 0.9,
3685 "no association → p ≈ 1.0, got {}",
3686 r.p_value
3687 );
3688 }
3689
3690 #[test]
3691 fn fisher_small_table() {
3692 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3694 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3695 }
3696
3697 #[test]
3698 fn fisher_asymmetric() {
3699 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3701 assert!(r.p_value < 0.05, "significant association");
3702 }
3703
3704 #[test]
3705 fn fisher_edge_cases() {
3706 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()); }
3711
3712 #[test]
3713 fn fisher_odds_ratio() {
3714 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3715 assert!(
3717 (r.statistic - 9.0).abs() < 1e-10,
3718 "OR = 9, got {}",
3719 r.statistic
3720 );
3721 }
3722
3723 #[test]
3728 fn mk_increasing_trend() {
3729 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3730 let r = mann_kendall_test(&data).expect("should compute");
3731 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3732 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3733 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3734 assert!(r.s_statistic > 0);
3735 }
3736
3737 #[test]
3738 fn mk_decreasing_trend() {
3739 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3740 let r = mann_kendall_test(&data).expect("should compute");
3741 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3742 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3743 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3744 assert!(r.s_statistic < 0);
3745 }
3746
3747 #[test]
3748 fn mk_no_trend() {
3749 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3751 let r = mann_kendall_test(&data).expect("should compute");
3752 assert!(
3754 r.p_value > 0.05,
3755 "p = {} (should be > 0.05 for no trend)",
3756 r.p_value
3757 );
3758 }
3759
3760 #[test]
3761 fn mk_perfect_monotone() {
3762 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3763 let r = mann_kendall_test(&data).expect("should compute");
3764 assert_eq!(r.s_statistic, 45);
3766 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3767 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3768 }
3769
3770 #[test]
3771 fn mk_with_ties() {
3772 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3773 let r = mann_kendall_test(&data).expect("should compute");
3774 assert!(r.s_statistic > 0);
3775 let n = data.len() as f64;
3777 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3778 assert!(r.variance < base_var, "ties should reduce variance");
3779 }
3780
3781 #[test]
3782 fn mk_edge_cases() {
3783 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3785 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3787 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3789 }
3790
3791 #[test]
3792 fn mk_minimum_n() {
3793 let data = [1.0, 2.0, 3.0, 4.0];
3795 let r = mann_kendall_test(&data).expect("n=4 should work");
3796 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3798 }
3799
3800 #[test]
3801 fn mk_sen_slope_robust_to_outlier() {
3802 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3804 let r = mann_kendall_test(&data).expect("should compute");
3805 assert!(
3807 (r.sen_slope - 1.0).abs() < 0.5,
3808 "sen slope = {}, expected ≈ 1.0",
3809 r.sen_slope
3810 );
3811 }
3812
3813 #[test]
3818 fn adf_stationary_mean_reverting() {
3819 let data = [
3822 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,
3823 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,
3824 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,
3825 ];
3826 let r = adf_test(&data, AdfModel::Constant, AdfLags::default()).expect("should compute");
3827 assert!(
3829 r.rejected[2],
3830 "should reject at 10%: stat={}, cv={}",
3831 r.statistic, r.critical_values[2]
3832 );
3833 }
3834
3835 #[test]
3836 fn adf_nonstationary_random_walk() {
3837 let increments = [
3839 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,
3840 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,
3841 -0.13, 0.15, -0.1,
3842 ];
3843 let mut walk = Vec::with_capacity(increments.len());
3844 let mut cum = 0.0;
3845 for &inc in &increments {
3846 cum += inc;
3847 walk.push(cum);
3848 }
3849 let r = adf_test(&walk, AdfModel::Constant, AdfLags::default()).expect("should compute");
3850 assert!(
3852 !r.rejected[0],
3853 "should NOT reject at 1%: stat={}, cv={}",
3854 r.statistic, r.critical_values[0]
3855 );
3856 }
3857
3858 #[test]
3859 fn adf_with_fixed_lags() {
3860 let mut rng = mulberry32(0xadf2);
3864 let data: Vec<f64> = (0..50)
3865 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
3866 .collect();
3867 let r = adf_test(&data, AdfModel::Constant, AdfLags::Fixed(2)).expect("should compute");
3868 assert_eq!(r.n_lags, 2);
3869 assert!(r.statistic.is_finite());
3870 }
3871
3872 #[test]
3873 fn adf_constant_trend_model() {
3874 let data: Vec<f64> = (0..30)
3877 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3878 .collect();
3879 let r =
3880 adf_test(&data, AdfModel::ConstantTrend, AdfLags::default()).expect("should compute");
3881 assert!(r.statistic.is_finite());
3882 assert_eq!(r.critical_values.len(), 3);
3883 }
3884
3885 #[test]
3886 fn adf_edge_cases() {
3887 assert!(adf_test(&[1.0; 9], AdfModel::Constant, AdfLags::default()).is_none());
3889 let mut data = vec![0.0; 20];
3891 data[5] = f64::NAN;
3892 assert!(adf_test(&data, AdfModel::Constant, AdfLags::default()).is_none());
3893 }
3894
3895 #[test]
3901 fn adf_exact_fit_is_undefined_and_not_chosen() {
3902 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
3903 let mut walk = vec![0.0_f64; 60];
3904 for i in 1..walk.len() {
3905 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
3906 }
3907 assert!(adf_test(&walk, AdfModel::ConstantTrend, AdfLags::Fixed(9)).is_none());
3908 let r = adf_test(&walk, AdfModel::ConstantTrend, AdfLags::default())
3909 .expect("a lag below 9 fits");
3910 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
3911 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
3912 }
3913
3914 #[test]
3918 fn adf_matches_statsmodels() {
3919 let mut x = vec![0.0_f64; 120];
3920 for t in 1..x.len() {
3921 let e = ((t * 7919) % 101) as f64 / 50.0 - 1.0;
3922 x[t] = 0.7 * x[t - 1] + e;
3923 }
3924 for (model, lags, stat, n_lags, n_obs) in [
3925 (
3926 AdfModel::Constant,
3927 AdfLags::Aic { max: Some(8) },
3928 -3.436_052_894_8,
3929 5,
3930 114,
3931 ),
3932 (
3933 AdfModel::Constant,
3934 AdfLags::Aic { max: None },
3935 -3.436_052_894_8,
3936 5,
3937 114,
3938 ),
3939 (
3940 AdfModel::ConstantTrend,
3941 AdfLags::Aic { max: Some(6) },
3942 -3.505_247_069_0,
3943 5,
3944 114,
3945 ),
3946 (
3947 AdfModel::None,
3948 AdfLags::Aic { max: Some(5) },
3949 -3.459_240_021_9,
3950 5,
3951 114,
3952 ),
3953 (
3954 AdfModel::Constant,
3955 AdfLags::Fixed(3),
3956 -4.652_930_484_4,
3957 3,
3958 116,
3959 ),
3960 ] {
3961 let r = adf_test(&x, model, lags).expect("fits");
3962 assert_eq!((r.n_lags, r.n_obs), (n_lags, n_obs), "{model:?} {lags:?}");
3963 assert!(
3964 (r.statistic - stat).abs() < 1e-8,
3965 "{model:?} {lags:?}: {}",
3966 r.statistic
3967 );
3968 }
3969 }
3970
3971 #[test]
3972 fn adf_critical_values_constant() {
3973 let cv = adf_critical_values(AdfModel::Constant, 100);
3975 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3977 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3978 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3979 }
3980
3981 #[test]
3982 fn adf_critical_values_ordering() {
3983 let cv = adf_critical_values(AdfModel::Constant, 50);
3984 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3986 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3987 }
3988}
3989
3990#[cfg(test)]
3993mod scipy_reference {
3994 use super::*;
3995
3996 const A: [f64; 8] = [5.1, 4.9, 5.6, 5.8, 6.0, 5.2, 4.7, 5.5];
3997 const B: [f64; 7] = [6.3, 5.9, 6.8, 7.1, 6.0, 6.6, 7.4];
3998 const G1: [f64; 5] = [23.1, 25.4, 22.8, 26.0, 24.3];
3999 const G2: [f64; 6] = [27.2, 28.9, 26.5, 29.4, 27.8, 28.1];
4000 const G3: [f64; 4] = [22.0, 21.4, 24.9, 23.3];
4001
4002 fn close(got: f64, want: f64, rel: f64, what: &str) {
4004 let scale = want.abs().max(1.0);
4005 assert!(
4006 (got - want).abs() <= rel * scale,
4007 "{what}: got {got}, SciPy {want} (relative tolerance {rel})"
4008 );
4009 }
4010
4011 #[test]
4012 fn t_tests_reproduce_scipy() {
4013 let r = one_sample_t_test(&A, 5.0).expect("valid");
4015 close(r.statistic, 2.197950389679656, 1e-12, "one-sample t");
4016 close(r.p_value, 0.06392412293886501, 1e-9, "one-sample p");
4017 assert_eq!(r.df, 7.0);
4018 let r = two_sample_t_test(&A, &B).expect("valid");
4020 close(r.statistic, -4.67567567567568, 1e-12, "Welch t");
4021 close(r.df, 11.56763357082458, 1e-12, "Welch df");
4022 close(r.p_value, 0.0005912510074711228, 1e-9, "Welch p");
4023 let x = [12.1, 13.4, 11.8, 14.2, 12.9, 13.0];
4025 let y = [12.9, 13.9, 12.0, 15.1, 13.5, 13.2];
4026 let r = paired_t_test(&x, &y).expect("valid");
4027 close(r.statistic, -4.437601569801824, 1e-12, "paired t");
4028 close(r.p_value, 0.006779965572779817, 1e-9, "paired p");
4029 assert_eq!(r.df, 5.0);
4030 }
4031
4032 #[test]
4033 fn anova_and_equal_variance_tests_reproduce_scipy() {
4034 let groups: [&[f64]; 3] = [&G1, &G2, &G3];
4035 let r = one_way_anova(&groups).expect("valid");
4037 close(r.f_statistic, 20.526701958197258, 1e-12, "ANOVA F");
4038 close(r.p_value, 0.00013390844134272023, 1e-9, "ANOVA p");
4039 let r = levene_test(&groups).expect("valid");
4041 close(r.statistic, 0.4752743705616534, 1e-12, "Brown-Forsythe W");
4042 close(r.p_value, 0.6329340379295331, 1e-9, "Brown-Forsythe p");
4043 let r = bartlett_test(&groups).expect("valid");
4045 close(r.statistic, 0.5282298427149905, 1e-12, "Bartlett T");
4046 close(r.p_value, 0.7678852881660961, 1e-9, "Bartlett p");
4047 }
4048
4049 #[test]
4050 fn kruskal_wallis_with_ties_reproduces_scipy() {
4051 let r = kruskal_wallis_test(&[
4053 &[1.0, 2.0, 2.0, 3.0, 5.0],
4054 &[4.0, 4.0, 6.0, 7.0],
4055 &[5.0, 8.0, 8.0, 9.0, 10.0],
4056 ])
4057 .expect("valid");
4058 close(r.statistic, 9.22394678492239, 1e-12, "H (tie-corrected)");
4059 close(r.p_value, 0.009932198829456881, 1e-9, "Kruskal-Wallis p");
4060 }
4061
4062 #[test]
4063 fn chi_squared_and_fisher_tests_reproduce_scipy() {
4064 let r = chi_squared_goodness_of_fit(&[18.0, 22.0, 30.0, 30.0], &[25.0; 4]).expect("valid");
4066 close(r.statistic, 4.32, 1e-12, "goodness-of-fit chi-squared");
4067 close(r.p_value, 0.22891886433610517, 1e-9, "goodness-of-fit p");
4068 let r = chi_squared_independence(&[12.0, 7.0, 9.0, 5.0, 14.0, 11.0], 2, 3).expect("valid");
4070 close(
4071 r.statistic,
4072 5.353085901027075,
4073 1e-12,
4074 "independence chi-squared",
4075 );
4076 close(r.p_value, 0.06880059054935043, 1e-9, "independence p");
4077 assert_eq!(r.df, 2.0);
4078 let r = fisher_exact_test(8, 2, 1, 5).expect("valid");
4080 close(r.p_value, 0.034965034965034975, 1e-9, "Fisher p");
4081 }
4082
4083 #[test]
4084 fn shapiro_wilk_reproduces_scipy() {
4085 let r = shapiro_wilk_test(&[
4087 148.0, 154.0, 158.0, 160.0, 161.0, 162.0, 166.0, 170.0, 182.0, 195.0, 236.0,
4088 ])
4089 .expect("valid");
4090 close(r.w, 0.7888146948631716, 1e-9, "W, skewed");
4091 close(r.p_value, 0.006703814061898823, 1e-9, "p, skewed");
4092 let r = shapiro_wilk_test(&[
4094 2.1, 3.4, 1.9, 5.6, 4.4, 3.3, 2.8, 4.0, 3.9, 3.1, 2.6, 4.8, 3.6, 3.0, 4.2,
4095 ])
4096 .expect("valid");
4097 close(r.w, 0.9864343014896592, 1e-9, "W, near normal");
4098 close(r.p_value, 0.9958703691266937, 1e-9, "p, near normal");
4099 }
4100
4101 #[test]
4102 fn mann_kendall_tau_and_sen_slope_reproduce_scipy() {
4103 let mk = [4.2, 4.0, 4.6, 4.9, 4.7, 5.3, 5.1, 5.8, 5.6, 6.2, 6.0, 6.7];
4106 let r = mann_kendall_test(&mk).expect("valid");
4107 close(r.kendall_tau, 0.8484848484848484, 1e-12, "Kendall tau");
4108 close(r.sen_slope, 0.22500000000000003, 1e-12, "Sen's slope");
4109 }
4110
4111 #[test]
4112 fn benjamini_hochberg_reproduces_scipy() {
4113 let adjusted = benjamini_hochberg(&[0.01, 0.04, 0.03, 0.005, 0.20, 0.50]).expect("valid");
4115 for (got, want) in adjusted.iter().zip([0.03, 0.06, 0.06, 0.03, 0.24, 0.5]) {
4116 close(*got, want, 1e-12, "BH-adjusted p");
4117 }
4118 }
4119}
4120
4121#[cfg(test)]
4122mod proptests {
4123 use super::*;
4124 use proptest::prelude::*;
4125
4126 proptest! {
4127 #[test]
4128 fn one_sample_p_bounded(
4129 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
4130 mu0 in -1e3_f64..1e3
4131 ) {
4132 if let Some(r) = one_sample_t_test(&data, mu0) {
4133 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4134 }
4135 }
4136
4137 #[test]
4138 fn two_sample_p_bounded(
4139 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4140 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4141 ) {
4142 if let Some(r) = two_sample_t_test(&a, &b) {
4143 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4144 }
4145 }
4146
4147 #[test]
4148 fn anova_p_bounded(
4149 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4150 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4151 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4152 ) {
4153 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
4154 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4155 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
4156 }
4157 }
4158
4159 #[test]
4160 fn bonferroni_monotone(
4161 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
4162 ) {
4163 let adj = bonferroni_correction(&ps).expect("should compute");
4164 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
4165 prop_assert!(adjusted >= orig - 1e-15,
4166 "adj[{i}] = {adjusted} < orig = {orig}");
4167 }
4168 }
4169
4170 #[test]
4171 fn shapiro_wilk_p_bounded(
4172 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
4173 ) {
4174 if let Some(r) = shapiro_wilk_test(&data) {
4175 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
4176 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4177 }
4178 }
4179
4180 #[test]
4181 fn anderson_darling_p_bounded(
4182 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
4183 ) {
4184 if let Some(r) = anderson_darling_test(&data) {
4185 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
4186 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4187 }
4188 }
4189
4190 #[test]
4191 fn mann_whitney_p_bounded(
4192 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4193 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4194 ) {
4195 if let Some(r) = mann_whitney_u_test(&a, &b) {
4196 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4197 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
4198 }
4199 }
4200
4201 #[test]
4202 fn wilcoxon_p_bounded(
4203 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4204 ) {
4205 let zeros: Vec<f64> = vec![0.0; diffs.len()];
4206 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
4207 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4208 }
4209 }
4210
4211 #[test]
4212 fn bartlett_p_bounded(
4213 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4214 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4215 ) {
4216 if let Some(r) = bartlett_test(&[&g1, &g2]) {
4217 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4218 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
4219 }
4220 }
4221
4222 #[test]
4223 fn fisher_p_bounded(
4224 a in 0_u64..20,
4225 b in 0_u64..20,
4226 c in 0_u64..20,
4227 d in 0_u64..20,
4228 ) {
4229 if let Some(r) = fisher_exact_test(a, b, c, d) {
4230 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4231 }
4232 }
4233
4234 #[test]
4235 fn mann_kendall_p_bounded(
4236 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
4237 ) {
4238 if let Some(r) = mann_kendall_test(&data) {
4239 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4240 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
4241 "tau = {}", r.kendall_tau);
4242 }
4243 }
4244
4245 #[test]
4246 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
4247 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
4248 let r = mann_kendall_test(&data).expect("should compute");
4249 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
4250 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
4251 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
4252 }
4253 }
4254}
4255
4256#[cfg(test)]
4259mod definition_reference {
4260 use super::*;
4261
4262 #[test]
4263 fn bonferroni_is_p_times_m_capped_at_one() {
4264 let adjusted = bonferroni_correction(&[0.01, 0.02, 0.04, 0.3, 0.0004]).expect("valid");
4265 let want = [0.05, 0.1, 0.2, 1.0, 0.002];
4266 assert_eq!(adjusted.len(), want.len());
4267 for (got, want) in adjusted.iter().zip(want) {
4268 assert!((got - want).abs() <= 1e-15, "got {got}, want {want}");
4269 }
4270 }
4271}
4272
4273#[cfg(test)]
4276mod scipy_tail_reference {
4277 use super::*;
4278
4279 fn close(got: f64, want: f64, what: &str) {
4280 assert!(
4281 (got - want).abs() <= 1e-9 * want.abs(),
4282 "{what}: got {got:e}, SciPy {want:e}"
4283 );
4284 }
4285
4286 #[test]
4287 fn t_tests_keep_their_tail() {
4288 let a = [100.2, 99.8, 100.5, 100.1, 99.9, 100.3, 100.0, 99.7];
4289 let b = [120.1, 119.7, 120.4, 120.0, 119.8, 120.3];
4290 let r = one_sample_t_test(&a, 90.0).expect("valid");
4292 close(r.p_value, 1.6824197797300066e-12, "one-sample p");
4293 let r = two_sample_t_test(&a, &b).expect("valid");
4295 close(r.p_value, 8.838218005160137e-19, "Welch p");
4296 }
4297
4298 #[test]
4299 fn f_and_chi_squared_tests_keep_their_tail() {
4300 let (g1, g2, g3) = (
4302 [10.1, 10.3, 9.9, 10.0, 10.2],
4303 [12.0, 12.2, 11.9, 12.1, 12.3],
4304 [14.1, 13.9, 14.0, 14.2, 13.8],
4305 );
4306 let r = one_way_anova(&[&g1, &g2, &g3]).expect("valid");
4307 close(r.p_value, 2.297566098988527e-13, "ANOVA p");
4308 let r = chi_squared_independence(&[80.0, 20.0, 30.0, 70.0], 2, 2).expect("valid");
4310 close(r.p_value, 1.1885852395646684e-12, "independence p");
4311 let r = chi_squared_goodness_of_fit(&[90.0, 10.0, 5.0, 95.0], &[50.0; 4]).expect("valid");
4313 close(r.p_value, 3.1567404374222543e-31, "goodness-of-fit p");
4314 let low: Vec<f64> = (1..=10).map(f64::from).collect();
4316 let mid: Vec<f64> = (11..=20).map(f64::from).collect();
4317 let high: Vec<f64> = (21..=30).map(f64::from).collect();
4318 let r = kruskal_wallis_test(&[&low, &mid, &high]).expect("valid");
4319 close(r.p_value, 2.49000508218493e-06, "Kruskal-Wallis p");
4320 }
4321}