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> {
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 = 1.0 - special::chi_squared_cdf(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 = 1.0 - special::chi_squared_cdf(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)]
1817pub struct AdfResult {
1818 pub statistic: f64,
1820 pub n_lags: usize,
1822 pub n_obs: usize,
1824 pub critical_values: [f64; 3],
1826 pub rejected: [bool; 3],
1828}
1829
1830#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1832pub enum AdfLags {
1833 Aic {
1838 max: Option<usize>,
1840 },
1841 Fixed(usize),
1843}
1844
1845impl Default for AdfLags {
1846 fn default() -> Self {
1847 AdfLags::Aic { max: None }
1848 }
1849}
1850
1851#[derive(Debug, Clone, Copy)]
1853pub enum AdfModel {
1854 None,
1856 Constant,
1858 ConstantTrend,
1860}
1861
1862pub fn adf_test(data: &[f64], model: AdfModel, lags: AdfLags) -> Option<AdfResult> {
1899 let n = data.len();
1900 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1901 return None;
1902 }
1903
1904 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1906
1907 let best_lag = match lags {
1908 AdfLags::Fixed(p) => p,
1909 AdfLags::Aic { max } => {
1910 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1912 let p_max = max.unwrap_or_else(|| schwert.min(n / 3));
1913 select_adf_lag(data, &dy, model, p_max)?
1914 }
1915 };
1916
1917 adf_ols(data, &dy, model, best_lag)
1919}
1920
1921fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> Option<usize> {
1926 let start = p_max;
1927 let mut best: Option<(f64, usize)> = None;
1928 for p in 0..=p_max {
1929 if let Some(aic) = adf_ols_aic(data, dy, model, p, start) {
1930 if best.is_none_or(|(b, _)| aic < b) {
1931 best = Some((aic, p));
1932 }
1933 }
1934 }
1935 best.map(|(_, p)| p)
1936}
1937
1938#[allow(clippy::type_complexity)]
1942fn adf_build_matrix(
1943 data: &[f64],
1944 dy: &[f64],
1945 model: AdfModel,
1946 p: usize,
1947 start: usize,
1948) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1949 debug_assert!(start >= p, "the first row needs its p lagged differences");
1951 if start >= dy.len() || dy.len() - start < 5 {
1952 return None;
1953 }
1954 let m = dy.len() - start;
1955
1956 let y_dep: Vec<f64> = dy[start..].to_vec();
1957
1958 let has_intercept = !matches!(model, AdfModel::None);
1960 let has_trend = matches!(model, AdfModel::ConstantTrend);
1961 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1962
1963 let mut x_data = Vec::with_capacity(m * ncols);
1965 let mut gamma_col = 0;
1966
1967 for i in 0..m {
1968 let t = start + i;
1969 if has_intercept {
1970 x_data.push(1.0); gamma_col = 1;
1972 }
1973 x_data.push(data[t]); if has_trend {
1975 x_data.push((t + 1) as f64); }
1977 for lag in 1..=p {
1978 x_data.push(dy[t - lag]);
1979 }
1980 }
1981
1982 Some((x_data, y_dep, m, ncols, gamma_col))
1983}
1984
1985fn adf_ols_core(
1989 x_data: &[f64],
1990 y: &[f64],
1991 m: usize,
1992 ncols: usize,
1993 gamma_col: usize,
1994) -> Option<(f64, f64, usize)> {
1995 let mut xtx = vec![0.0_f64; ncols * ncols];
1997 for i in 0..m {
1998 let row = &x_data[i * ncols..(i + 1) * ncols];
1999 for j in 0..ncols {
2000 for k in j..ncols {
2001 xtx[j * ncols + k] += row[j] * row[k];
2002 }
2003 }
2004 }
2005 for j in 0..ncols {
2007 for k in (j + 1)..ncols {
2008 xtx[k * ncols + j] = xtx[j * ncols + k];
2009 }
2010 }
2011
2012 let mut xty = vec![0.0_f64; ncols];
2014 for i in 0..m {
2015 let row = &x_data[i * ncols..(i + 1) * ncols];
2016 for j in 0..ncols {
2017 xty[j] += row[j] * y[i];
2018 }
2019 }
2020
2021 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2023 for i in 0..ncols {
2024 for j in 0..ncols {
2025 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2026 }
2027 augmented[i * (ncols + 1) + ncols] = xty[i];
2028 }
2029
2030 for col in 0..ncols {
2031 let mut max_row = col;
2033 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2034 for row in (col + 1)..ncols {
2035 let val = augmented[row * (ncols + 1) + col].abs();
2036 if val > max_val {
2037 max_val = val;
2038 max_row = row;
2039 }
2040 }
2041 if max_val < 1e-15 {
2042 return None; }
2044 if max_row != col {
2045 for j in 0..=ncols {
2046 let a = col * (ncols + 1) + j;
2047 let b = max_row * (ncols + 1) + j;
2048 augmented.swap(a, b);
2049 }
2050 }
2051
2052 let pivot = augmented[col * (ncols + 1) + col];
2053 for row in (col + 1)..ncols {
2054 let factor = augmented[row * (ncols + 1) + col] / pivot;
2055 for j in col..=ncols {
2056 let above = augmented[col * (ncols + 1) + j];
2057 augmented[row * (ncols + 1) + j] -= factor * above;
2058 }
2059 }
2060 }
2061
2062 let mut beta = vec![0.0_f64; ncols];
2064 for i in (0..ncols).rev() {
2065 let mut sum = augmented[i * (ncols + 1) + ncols];
2066 for j in (i + 1)..ncols {
2067 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2068 }
2069 beta[i] = sum / augmented[i * (ncols + 1) + i];
2070 }
2071
2072 let mut rss = 0.0;
2074 for i in 0..m {
2075 let row = &x_data[i * ncols..(i + 1) * ncols];
2076 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2077 let resid = y[i] - y_hat;
2078 rss += resid * resid;
2079 }
2080
2081 let yy: f64 = y.iter().map(|v| v * v).sum();
2087 if yy == 0.0 || rss <= yy * 1e-16 {
2088 return None;
2089 }
2090
2091 let df = m - ncols;
2093 if df == 0 {
2094 return None;
2095 }
2096 let mse = rss / df as f64;
2097
2098 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2101 for j in 0..ncols {
2102 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2103 }
2104 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2105 }
2106
2107 for col in 0..ncols {
2109 let mut max_row = col;
2110 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2111 for row in (col + 1)..ncols {
2112 let val = xtx_aug[row * 2 * ncols + col].abs();
2113 if val > max_val {
2114 max_val = val;
2115 max_row = row;
2116 }
2117 }
2118 if max_val < 1e-15 {
2119 return None;
2120 }
2121 if max_row != col {
2122 for j in 0..(2 * ncols) {
2123 let a = col * 2 * ncols + j;
2124 let b = max_row * 2 * ncols + j;
2125 xtx_aug.swap(a, b);
2126 }
2127 }
2128
2129 let pivot = xtx_aug[col * 2 * ncols + col];
2130 for j in 0..(2 * ncols) {
2131 xtx_aug[col * 2 * ncols + j] /= pivot;
2132 }
2133 for row in 0..ncols {
2134 if row == col {
2135 continue;
2136 }
2137 let factor = xtx_aug[row * 2 * ncols + col];
2138 for j in 0..(2 * ncols) {
2139 let above = xtx_aug[col * 2 * ncols + j];
2140 xtx_aug[row * 2 * ncols + j] -= factor * above;
2141 }
2142 }
2143 }
2144
2145 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2147 if var_gamma <= 0.0 {
2148 return None;
2149 }
2150 let se_gamma = var_gamma.sqrt();
2151 let t_gamma = beta[gamma_col] / se_gamma;
2152
2153 Some((t_gamma, rss, ncols))
2154}
2155
2156fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize, start: usize) -> Option<f64> {
2159 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, start)?;
2160 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2161 Some(2.0 * k as f64 + m as f64 * (rss / m as f64).ln())
2162}
2163
2164fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2166 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, p)?;
2167 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2168
2169 let critical_values = adf_critical_values(model, m);
2170
2171 let rejected = [
2172 gamma_t <= critical_values[0],
2173 gamma_t <= critical_values[1],
2174 gamma_t <= critical_values[2],
2175 ];
2176
2177 Some(AdfResult {
2178 statistic: gamma_t,
2179 n_lags: p,
2180 n_obs: m,
2181 critical_values,
2182 rejected,
2183 })
2184}
2185
2186fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2190 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2195 AdfModel::None => (
2196 [-2.5658, -1.9393, -1.6156],
2197 [-1.960, -0.398, -0.181],
2198 [-10.04, 0.0, 0.0],
2199 ),
2200 AdfModel::Constant => (
2201 [-3.4336, -2.8621, -2.5671],
2202 [-5.999, -2.738, -1.438],
2203 [-29.25, -8.36, -4.48],
2204 ),
2205 AdfModel::ConstantTrend => (
2206 [-3.9638, -3.4126, -3.1279],
2207 [-8.353, -4.039, -2.418],
2208 [-47.44, -17.83, -7.58],
2209 ),
2210 };
2211
2212 let nf = n as f64;
2213 let inv_n = 1.0 / nf;
2214 let inv_n2 = inv_n * inv_n;
2215
2216 [
2217 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2218 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2219 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2220 ]
2221}
2222
2223#[cfg(test)]
2224mod tests {
2225 use super::*;
2226
2227 #[test]
2228 fn corrections_refuse_what_is_not_a_probability() {
2229 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2230 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2231 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2232 }
2233
2234 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2240 let mut a = seed;
2241 move || {
2242 a = a.wrapping_add(0x6d2b_79f5);
2243 let mut t = a;
2244 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2245 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2246 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2247 }
2248 }
2249
2250 #[test]
2256 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2257 let mut rng = mulberry32(0x5350_4331);
2258 let full: Vec<f64> = (0..20_000)
2259 .map(|_| {
2260 let u = rng();
2261 -(1.0 - u).ln() })
2263 .collect();
2264
2265 let mut prev_a2 = 0.0_f64;
2266 let mut prev_p = f64::INFINITY;
2267 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2268 let r = anderson_darling_test(&full[..n]).expect("computes");
2269 assert!(
2271 r.statistic_star > prev_a2,
2272 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2273 r.statistic_star
2274 );
2275 assert!(
2277 r.p_value < 0.5,
2278 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2279 r.statistic_star,
2280 r.p_value
2281 );
2282 assert!(
2284 r.p_value <= prev_p + 1e-12,
2285 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2286 r.p_value
2287 );
2288 prev_a2 = r.statistic_star;
2289 prev_p = r.p_value;
2290 }
2291 }
2292
2293 #[test]
2298 fn one_sample_null_true() {
2299 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2300 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2301 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2302 }
2303
2304 #[test]
2305 fn one_sample_null_false() {
2306 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2307 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2308 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2309 }
2310
2311 #[test]
2312 fn one_sample_edge_cases() {
2313 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());
2316 }
2317
2318 #[test]
2323 fn two_sample_same_mean() {
2324 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2325 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2326 let r = two_sample_t_test(&a, &b).expect("should compute");
2327 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2328 }
2329
2330 #[test]
2331 fn two_sample_different_means() {
2332 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2333 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2334 let r = two_sample_t_test(&a, &b).expect("should compute");
2335 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2336 }
2337
2338 #[test]
2339 fn two_sample_different_sizes() {
2340 let a = [1.0, 2.0, 3.0];
2341 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2342 let r = two_sample_t_test(&a, &b).expect("should compute");
2343 assert!(r.p_value < 0.05);
2344 }
2345
2346 #[test]
2347 fn two_sample_edge_cases() {
2348 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2349 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2350 }
2351
2352 #[test]
2357 fn paired_no_difference() {
2358 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2359 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2360 let r = paired_t_test(&x, &y).expect("should compute");
2361 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2362 }
2363
2364 #[test]
2365 fn paired_significant_difference() {
2366 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2368 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2369 let r = paired_t_test(&before, &after).expect("should compute");
2370 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2371 assert!(r.statistic < 0.0); }
2373
2374 #[test]
2375 fn paired_edge_cases() {
2376 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2379
2380 #[test]
2385 fn anova_same_means() {
2386 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2387 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2388 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2389 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2390 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2391 }
2392
2393 #[test]
2394 fn anova_different_means() {
2395 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2396 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2397 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2398 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2399 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2400 assert!(r.df_between == 2);
2401 assert!(r.df_within == 12);
2402 }
2403
2404 #[test]
2405 fn anova_ss_decomposition() {
2406 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2407 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2408 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2409 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2411 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2412 assert!(
2413 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2414 "SS decomposition: {ss_total} vs {} + {}",
2415 r.ss_between,
2416 r.ss_within
2417 );
2418 }
2419
2420 #[test]
2421 fn anova_edge_cases() {
2422 let g1 = [1.0, 2.0, 3.0];
2423 assert!(one_way_anova(&[&g1]).is_none()); }
2425
2426 #[test]
2431 fn chi2_gof_uniform() {
2432 let observed = [25.0, 25.0, 25.0, 25.0];
2434 let expected = [25.0, 25.0, 25.0, 25.0];
2435 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2436 assert!((r.statistic).abs() < 1e-15);
2437 assert!((r.p_value - 1.0).abs() < 0.01);
2438 }
2439
2440 #[test]
2441 fn chi2_gof_significant() {
2442 let observed = [90.0, 10.0];
2443 let expected = [50.0, 50.0];
2444 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2445 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2446 }
2447
2448 #[test]
2449 fn chi2_gof_edge_cases() {
2450 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()); }
2454
2455 #[test]
2460 fn chi2_independence_significant() {
2461 let table = [30.0, 10.0, 10.0, 50.0];
2463 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2464 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2465 assert!((r.df - 1.0).abs() < 1e-10);
2466 }
2467
2468 #[test]
2469 fn chi2_independence_not_significant() {
2470 let table = [25.0, 25.0, 25.0, 25.0];
2472 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2473 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2474 }
2475
2476 #[test]
2477 fn chi2_independence_3x3() {
2478 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2479 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2480 assert!((r.df - 4.0).abs() < 1e-10);
2481 assert!(r.p_value < 0.05);
2482 }
2483
2484 #[test]
2485 fn chi2_independence_edge_cases() {
2486 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()); }
2490
2491 #[test]
2496 fn jb_normal_data() {
2497 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2499 let r = jarque_bera_test(&data).expect("should compute");
2500 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2501 }
2502
2503 #[test]
2504 fn jb_skewed_data() {
2505 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2507 let r = jarque_bera_test(&data).expect("should compute");
2508 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2509 }
2510
2511 #[test]
2518 fn jb_is_the_moment_form_the_paper_defines() {
2519 let data = [
2520 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,
2521 3.6, 2.8, 3.2,
2522 ];
2523 let r = jarque_bera_test(&data).expect("20 finite points");
2524 assert!(
2525 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2526 "JB = {}",
2527 r.statistic
2528 );
2529
2530 let n = data.len() as f64;
2533 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2534 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2535 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2536 assert!(
2537 adjusted > r.statistic * 1.3,
2538 "the two forms differ by more than a third here: {adjusted} vs {}",
2539 r.statistic
2540 );
2541 }
2542
2543 #[test]
2547 fn jb_p_value_keeps_its_digits_in_the_tail() {
2548 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2551 data.push(60.0);
2552 let r = jarque_bera_test(&data).expect("20 finite points");
2553 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2554 assert!(
2555 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2556 "this sample is only interesting because the complement underflows"
2557 );
2558 assert!(
2559 r.p_value > 0.0,
2560 "the complement returns 0 here; this must not"
2561 );
2562 assert!(
2563 (r.p_value - 6.702e-21).abs() < 1e-23,
2564 "p = {} (expected about 6.7e-21)",
2565 r.p_value
2566 );
2567 assert!(
2568 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2569 "p = {} vs exp(-JB/2) = {}",
2570 r.p_value,
2571 (-r.statistic / 2.0).exp()
2572 );
2573 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])
2576 .expect("10 finite points");
2577 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2578 assert!(
2579 (small.p_value - via_cdf).abs() < 1e-12,
2580 "{} vs {via_cdf}",
2581 small.p_value
2582 );
2583 }
2584
2585 #[test]
2586 fn jb_edge_cases() {
2587 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2589
2590 #[test]
2595 fn ad_normal_data() {
2596 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2597 let r = anderson_darling_test(&data).expect("should compute");
2598 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2599 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2600 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2601 }
2602
2603 #[test]
2604 fn ad_skewed_data() {
2605 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];
2607 let r = anderson_darling_test(&data).expect("should compute");
2608 assert!(
2609 r.p_value < 0.05,
2610 "p = {} (should reject normality)",
2611 r.p_value
2612 );
2613 }
2614
2615 #[test]
2616 fn ad_bimodal_data() {
2617 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2619 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2620 let r = anderson_darling_test(&data).expect("should compute");
2621 assert!(
2622 r.p_value < 0.01,
2623 "p = {} (bimodal should reject normality)",
2624 r.p_value
2625 );
2626 }
2627
2628 #[test]
2629 fn ad_large_normal_sample() {
2630 let n = 100;
2631 let data: Vec<f64> = (1..=n)
2632 .map(|i| {
2633 let p = (i as f64 - 0.5) / n as f64;
2634 special::inverse_normal_cdf(p)
2635 })
2636 .collect();
2637 let r = anderson_darling_test(&data).expect("should compute");
2638 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2639 }
2640
2641 #[test]
2642 fn ad_edge_cases() {
2643 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());
2646 }
2647
2648 #[test]
2649 fn ad_p_value_ranges() {
2650 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2655 let r = anderson_darling_test(&near_normal).expect("should compute");
2656 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2657
2658 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];
2660 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2661 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2662 }
2663
2664 #[test]
2669 fn ad_normal_data_large_p() {
2670 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2672 let r = anderson_darling_test(&data).unwrap();
2673 assert!(r.p_value > 0.05, "p={}", r.p_value);
2674 }
2675
2676 #[test]
2677 fn ad_exponential_data_small_p() {
2678 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2680 let r = anderson_darling_test(&data).unwrap();
2681 assert!(r.p_value < 0.05, "p={}", r.p_value);
2682 }
2683
2684 #[test]
2685 fn ad_statistic_non_negative() {
2686 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2687 let r = anderson_darling_test(&data).unwrap();
2688 assert!(r.statistic >= 0.0);
2689 assert!(r.statistic_star >= r.statistic);
2690 }
2691
2692 #[test]
2693 fn ad_p_value_in_range() {
2694 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2695 let r = anderson_darling_test(&data).unwrap();
2696 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2697 }
2698
2699 #[test]
2700 fn ad_insufficient_data() {
2701 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
2703
2704 #[test]
2705 fn ad_degenerate_data() {
2706 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
2708 }
2709
2710 #[test]
2711 fn ad_modified_statistic_formula() {
2712 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
2714 let r = anderson_darling_test(&data).unwrap();
2715 assert!(r.statistic_star > r.statistic - 1e-10);
2716 assert!(anderson_darling_test(&data[..7]).is_none());
2718 }
2719
2720 #[test]
2725 fn ad_correction_factor_formula() {
2726 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2727 let n = data.len() as f64;
2728 let r = anderson_darling_test(&data).unwrap();
2729
2730 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2732 assert!(
2733 (r.statistic_star - expected_star).abs() < 1e-12,
2734 "A²* = {}, expected = {}",
2735 r.statistic_star,
2736 expected_star
2737 );
2738
2739 assert!(
2741 r.statistic >= 0.0,
2742 "A² = {} must be non-negative",
2743 r.statistic
2744 );
2745 }
2746
2747 #[test]
2753 fn ad_formula_structure_symmetric_vs_skewed() {
2754 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2756 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2758
2759 let r_sym = anderson_darling_test(&symmetric).unwrap();
2760 let r_skew = anderson_darling_test(&skewed).unwrap();
2761
2762 assert!(
2763 r_sym.statistic < r_skew.statistic,
2764 "Symmetric A²={} should be less than skewed A²={}",
2765 r_sym.statistic,
2766 r_skew.statistic
2767 );
2768 }
2769
2770 #[test]
2775 fn ad_pvalue_invariants() {
2776 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2778 let r_normal = anderson_darling_test(&normal_data).unwrap();
2779 assert!(
2780 r_normal.p_value > 0.05,
2781 "Normal data: p = {} (expected > 0.05)",
2782 r_normal.p_value
2783 );
2784
2785 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2787 let r_exp = anderson_darling_test(&exp_data).unwrap();
2788 assert!(
2789 r_exp.p_value < 0.05,
2790 "Exponential data: p = {} (expected < 0.05)",
2791 r_exp.p_value
2792 );
2793 }
2794
2795 #[test]
2800 fn sw_normal_data() {
2801 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2803 let r = shapiro_wilk_test(&data).expect("should compute");
2804 assert!(r.w > 0.9, "W = {}", r.w);
2805 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2806 }
2807
2808 #[test]
2809 fn sw_bimodal_data() {
2810 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2812 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2813 let r = shapiro_wilk_test(&data).expect("should compute");
2814 assert!(
2815 r.p_value < 0.01,
2816 "p = {} (bimodal should reject normality)",
2817 r.p_value
2818 );
2819 }
2820
2821 #[test]
2822 fn sw_n3() {
2823 let data = [1.0, 2.0, 3.0];
2824 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2825 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2826 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2827 }
2828
2829 #[test]
2830 fn sw_n4() {
2831 let data = [1.0, 2.0, 3.0, 4.0];
2832 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2833 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2834 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2835 }
2836
2837 #[test]
2838 fn sw_n5() {
2839 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2840 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2841 assert!(r.w > 0.9, "W = {}", r.w);
2842 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2843 }
2844
2845 #[test]
2846 fn sw_skewed_data() {
2847 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];
2849 let r = shapiro_wilk_test(&data).expect("should compute");
2850 assert!(
2851 r.p_value < 0.05,
2852 "p = {} (skewed data should reject normality)",
2853 r.p_value
2854 );
2855 }
2856
2857 #[test]
2858 fn sw_large_normal_sample() {
2859 let n = 100;
2862 let data: Vec<f64> = (1..=n)
2863 .map(|i| {
2864 let p = (i as f64 - 0.5) / n as f64;
2865 special::inverse_normal_cdf(p)
2866 })
2867 .collect();
2868 let r = shapiro_wilk_test(&data).expect("should compute");
2869 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2870 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2871 }
2872
2873 #[test]
2874 fn sw_w_bounded() {
2875 let datasets: Vec<Vec<f64>> = vec![
2877 vec![1.0, 2.0, 3.0],
2878 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2879 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2880 (0..20).map(|i| (i as f64).powi(2)).collect(),
2881 ];
2882 for (idx, data) in datasets.iter().enumerate() {
2883 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2884 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2885 assert!(
2886 r.p_value >= 0.0 && r.p_value <= 1.0,
2887 "dataset {idx}: p = {}",
2888 r.p_value
2889 );
2890 }
2891 }
2892
2893 #[test]
2894 fn sw_edge_cases() {
2895 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()); }
2901
2902 #[test]
2903 fn sw_n5001_rejected() {
2904 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2905 assert!(shapiro_wilk_test(&data).is_none()); }
2907
2908 #[test]
2913 fn mw_clearly_different() {
2914 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2915 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2916 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2917 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2918 }
2919
2920 #[test]
2921 fn mw_same_distribution() {
2922 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2923 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2924 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2925 assert!(
2926 r.p_value > 0.3,
2927 "p = {} (interleaved, same dist)",
2928 r.p_value
2929 );
2930 }
2931
2932 #[test]
2933 fn mw_with_ties() {
2934 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2935 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2936 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2937 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2938 }
2939
2940 #[test]
2941 fn mw_different_sizes() {
2942 let a = [1.0, 2.0, 3.0];
2943 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2944 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2945 assert!(r.p_value < 0.05);
2946 }
2947
2948 #[test]
2949 fn mw_edge_cases() {
2950 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());
2953 }
2954
2955 #[test]
2960 fn wilcoxon_significant_increase() {
2961 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2962 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2963 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2964 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2965 }
2966
2967 #[test]
2968 fn wilcoxon_no_difference() {
2969 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2970 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2971 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2972 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2973 }
2974
2975 #[test]
2976 fn wilcoxon_with_ties() {
2977 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2979 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2981 assert!(r.statistic > 0.0);
2983 }
2984
2985 #[test]
2986 fn wilcoxon_with_zero_diffs() {
2987 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2989 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2991 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2992 }
2993
2994 #[test]
2995 fn wilcoxon_edge_cases() {
2996 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());
3000 }
3001
3002 #[test]
3007 fn mann_whitney_small_samples_use_the_exact_distribution() {
3008 let cases = [
3010 (3, 3, 0, 0.1000, 0.0495),
3011 (4, 4, 0, 0.0286, 0.0209),
3012 (4, 4, 1, 0.0571, 0.0433),
3013 (5, 5, 2, 0.0317, 0.0283),
3014 (8, 8, 13, 0.0499, 0.0460),
3015 (10, 10, 23, 0.0433, 0.0413),
3016 ];
3017 for (n1, n2, u, exact, old) in cases {
3018 let (a, b) = separated_samples(n1, n2, u);
3019 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3020 assert_eq!(r.statistic, u as f64);
3021 assert!(
3022 (r.p_value - exact).abs() < 1e-4,
3023 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3024 r.p_value
3025 );
3026 }
3027 }
3028
3029 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3033 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3034 let mut remaining = u;
3035 for k in (0..n1).rev() {
3036 let room = n2.min(remaining);
3037 ranks_a[k] += room;
3038 remaining -= room;
3039 if remaining == 0 {
3040 break;
3041 }
3042 }
3043 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3044 (
3045 ranks_a.iter().map(|&r| r as f64).collect(),
3046 ranks_b.iter().map(|&r| r as f64).collect(),
3047 )
3048 }
3049
3050 #[test]
3052 fn wilcoxon_small_samples_use_the_exact_distribution() {
3053 let cases = [
3055 (5, 0, 0.0625),
3056 (5, 1, 0.125),
3057 (6, 2, 0.09375),
3058 (8, 5, 0.078125),
3059 (10, 8, 0.048828125),
3060 (12, 14, 0.0522460938),
3061 ];
3062 for (n, t_plus, exact) in cases {
3063 let positive = ranks_summing_to(n, t_plus);
3065 let x: Vec<f64> = (1..=n)
3066 .map(|r| {
3067 if positive.contains(&r) {
3068 r as f64
3069 } else {
3070 -(r as f64)
3071 }
3072 })
3073 .collect();
3074 let y = vec![0.0; n];
3075 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3076 assert_eq!(r.statistic, t_plus as f64);
3077 assert!(
3078 (r.p_value - exact).abs() < 1e-9,
3079 "n={n} T+={t_plus}: p={} want {exact}",
3080 r.p_value
3081 );
3082 }
3083 }
3084
3085 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3087 let mut out = Vec::new();
3088 let mut left = target;
3089 for r in (1..=n).rev() {
3090 if r <= left {
3091 out.push(r);
3092 left -= r;
3093 }
3094 }
3095 assert_eq!(left, 0);
3096 out
3097 }
3098
3099 #[test]
3102 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3103 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3104 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3105 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3106 let (n1, n2) = (60.0, 60.0);
3107 let mu = n1 * n2 / 2.0;
3108 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3109 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3110 let want = 2.0 * special::standard_normal_sf(z);
3111 assert!((r.p_value - want).abs() < 1e-12);
3112
3113 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3115 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3116 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3117 let mu = 1250.0;
3118 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3119 let u = mann_whitney_u_test(&a, &b)
3120 .expect("valid samples")
3121 .statistic;
3122 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3123 assert!(
3124 (exact - approx).abs() < 1e-3,
3125 "exact {exact} vs approx {approx}"
3126 );
3127 }
3128
3129 #[test]
3134 fn kw_clearly_different() {
3135 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3136 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3137 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3138 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3139 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3140 assert!((r.df - 2.0).abs() < 1e-10);
3141 }
3142
3143 #[test]
3144 fn kw_same_distribution() {
3145 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3146 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3147 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3148 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3149 }
3150
3151 #[test]
3152 fn kw_with_ties() {
3153 let g1 = [1.0, 2.0, 2.0, 3.0];
3154 let g2 = [3.0, 4.0, 4.0, 5.0];
3155 let g3 = [5.0, 6.0, 6.0, 7.0];
3156 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3157 assert!(r.statistic > 0.0);
3158 }
3159
3160 #[test]
3161 fn kw_edge_cases() {
3162 let g1 = [1.0, 2.0, 3.0];
3163 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3165
3166 #[test]
3171 fn levene_equal_variance() {
3172 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3173 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3174 let r = levene_test(&[&g1, &g2]).expect("should compute");
3175 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3177 }
3178
3179 #[test]
3180 fn levene_unequal_variance() {
3181 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");
3184 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3185 }
3186
3187 #[test]
3188 fn levene_three_groups() {
3189 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3190 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3191 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3192 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3193 assert!(r.df >= 2.0);
3194 }
3195
3196 #[test]
3197 fn levene_edge_cases() {
3198 let g1 = [1.0, 2.0, 3.0];
3199 assert!(levene_test(&[&g1]).is_none()); }
3201
3202 #[test]
3207 fn bonferroni_basic() {
3208 let ps = [0.01, 0.04, 0.03, 0.005];
3209 let adj = bonferroni_correction(&ps).expect("should compute");
3210 assert!((adj[0] - 0.04).abs() < 1e-10);
3211 assert!((adj[1] - 0.16).abs() < 1e-10);
3212 assert!((adj[2] - 0.12).abs() < 1e-10);
3213 assert!((adj[3] - 0.02).abs() < 1e-10);
3214 }
3215
3216 #[test]
3217 fn bonferroni_capped_at_one() {
3218 let ps = [0.5, 0.6];
3219 let adj = bonferroni_correction(&ps).expect("should compute");
3220 assert!((adj[0] - 1.0).abs() < 1e-10);
3221 assert!((adj[1] - 1.0).abs() < 1e-10);
3222 }
3223
3224 #[test]
3225 fn bh_basic() {
3226 let ps = [0.01, 0.04, 0.03, 0.005];
3227 let adj = benjamini_hochberg(&ps).expect("should compute");
3228 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3230 assert!(
3231 adjusted >= orig - 1e-15,
3232 "adj[{i}] = {adjusted} < original {orig}"
3233 );
3234 }
3235 }
3238
3239 #[test]
3240 fn bh_all_significant() {
3241 let ps = [0.001, 0.002, 0.003];
3242 let adj = benjamini_hochberg(&ps).expect("should compute");
3243 for &a in &adj {
3244 assert!(a < 0.05);
3245 }
3246 }
3247
3248 #[test]
3249 fn correction_edge_cases() {
3250 assert!(bonferroni_correction(&[]).is_none());
3251 assert!(benjamini_hochberg(&[]).is_none());
3252 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3253 }
3254
3255 #[test]
3260 fn bartlett_equal_variances() {
3261 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3262 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3263 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3264 assert!(
3265 r.p_value > 0.9,
3266 "equal variance → p high, got {}",
3267 r.p_value
3268 );
3269 }
3270
3271 #[test]
3272 fn bartlett_unequal_variances() {
3273 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");
3276 assert!(
3277 r.p_value < 0.01,
3278 "very different variances → p < 0.01, got {}",
3279 r.p_value
3280 );
3281 assert!((r.df - 1.0).abs() < 1e-10); }
3283
3284 #[test]
3285 fn bartlett_three_groups() {
3286 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3287 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3288 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3290 assert!(r.p_value < 0.05, "one group with high variance");
3291 assert!((r.df - 2.0).abs() < 1e-10); }
3293
3294 #[test]
3295 fn bartlett_edge_cases() {
3296 let g1 = [1.0, 2.0, 3.0];
3297 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3301
3302 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3304 }
3305
3306 #[test]
3311 fn fisher_tea_tasting() {
3312 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3314 assert!(
3316 (r.p_value - 0.4857).abs() < 0.01,
3317 "p ≈ 0.4857, got {}",
3318 r.p_value
3319 );
3320 }
3321
3322 #[test]
3323 fn fisher_significant() {
3324 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3326 assert!(r.p_value < 0.001, "perfect association → p very small");
3327 }
3328
3329 #[test]
3330 fn fisher_no_association() {
3331 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3333 assert!(
3334 r.p_value > 0.9,
3335 "no association → p ≈ 1.0, got {}",
3336 r.p_value
3337 );
3338 }
3339
3340 #[test]
3341 fn fisher_small_table() {
3342 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3344 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3345 }
3346
3347 #[test]
3348 fn fisher_asymmetric() {
3349 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3351 assert!(r.p_value < 0.05, "significant association");
3352 }
3353
3354 #[test]
3355 fn fisher_edge_cases() {
3356 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()); }
3361
3362 #[test]
3363 fn fisher_odds_ratio() {
3364 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3365 assert!(
3367 (r.statistic - 9.0).abs() < 1e-10,
3368 "OR = 9, got {}",
3369 r.statistic
3370 );
3371 }
3372
3373 #[test]
3378 fn mk_increasing_trend() {
3379 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3380 let r = mann_kendall_test(&data).expect("should compute");
3381 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3382 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3383 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3384 assert!(r.s_statistic > 0);
3385 }
3386
3387 #[test]
3388 fn mk_decreasing_trend() {
3389 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3390 let r = mann_kendall_test(&data).expect("should compute");
3391 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3392 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3393 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3394 assert!(r.s_statistic < 0);
3395 }
3396
3397 #[test]
3398 fn mk_no_trend() {
3399 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3401 let r = mann_kendall_test(&data).expect("should compute");
3402 assert!(
3404 r.p_value > 0.05,
3405 "p = {} (should be > 0.05 for no trend)",
3406 r.p_value
3407 );
3408 }
3409
3410 #[test]
3411 fn mk_perfect_monotone() {
3412 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3413 let r = mann_kendall_test(&data).expect("should compute");
3414 assert_eq!(r.s_statistic, 45);
3416 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3417 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3418 }
3419
3420 #[test]
3421 fn mk_with_ties() {
3422 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3423 let r = mann_kendall_test(&data).expect("should compute");
3424 assert!(r.s_statistic > 0);
3425 let n = data.len() as f64;
3427 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3428 assert!(r.variance < base_var, "ties should reduce variance");
3429 }
3430
3431 #[test]
3432 fn mk_edge_cases() {
3433 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3435 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3437 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3439 }
3440
3441 #[test]
3442 fn mk_minimum_n() {
3443 let data = [1.0, 2.0, 3.0, 4.0];
3445 let r = mann_kendall_test(&data).expect("n=4 should work");
3446 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3448 }
3449
3450 #[test]
3451 fn mk_sen_slope_robust_to_outlier() {
3452 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3454 let r = mann_kendall_test(&data).expect("should compute");
3455 assert!(
3457 (r.sen_slope - 1.0).abs() < 0.5,
3458 "sen slope = {}, expected ≈ 1.0",
3459 r.sen_slope
3460 );
3461 }
3462
3463 #[test]
3468 fn adf_stationary_mean_reverting() {
3469 let data = [
3472 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,
3473 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,
3474 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,
3475 ];
3476 let r = adf_test(&data, AdfModel::Constant, AdfLags::default()).expect("should compute");
3477 assert!(
3479 r.rejected[2],
3480 "should reject at 10%: stat={}, cv={}",
3481 r.statistic, r.critical_values[2]
3482 );
3483 }
3484
3485 #[test]
3486 fn adf_nonstationary_random_walk() {
3487 let increments = [
3489 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,
3490 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,
3491 -0.13, 0.15, -0.1,
3492 ];
3493 let mut walk = Vec::with_capacity(increments.len());
3494 let mut cum = 0.0;
3495 for &inc in &increments {
3496 cum += inc;
3497 walk.push(cum);
3498 }
3499 let r = adf_test(&walk, AdfModel::Constant, AdfLags::default()).expect("should compute");
3500 assert!(
3502 !r.rejected[0],
3503 "should NOT reject at 1%: stat={}, cv={}",
3504 r.statistic, r.critical_values[0]
3505 );
3506 }
3507
3508 #[test]
3509 fn adf_with_fixed_lags() {
3510 let mut rng = mulberry32(0xadf2);
3514 let data: Vec<f64> = (0..50)
3515 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
3516 .collect();
3517 let r = adf_test(&data, AdfModel::Constant, AdfLags::Fixed(2)).expect("should compute");
3518 assert_eq!(r.n_lags, 2);
3519 assert!(r.statistic.is_finite());
3520 }
3521
3522 #[test]
3523 fn adf_constant_trend_model() {
3524 let data: Vec<f64> = (0..30)
3527 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3528 .collect();
3529 let r =
3530 adf_test(&data, AdfModel::ConstantTrend, AdfLags::default()).expect("should compute");
3531 assert!(r.statistic.is_finite());
3532 assert_eq!(r.critical_values.len(), 3);
3533 }
3534
3535 #[test]
3536 fn adf_edge_cases() {
3537 assert!(adf_test(&[1.0; 9], AdfModel::Constant, AdfLags::default()).is_none());
3539 let mut data = vec![0.0; 20];
3541 data[5] = f64::NAN;
3542 assert!(adf_test(&data, AdfModel::Constant, AdfLags::default()).is_none());
3543 }
3544
3545 #[test]
3551 fn adf_exact_fit_is_undefined_and_not_chosen() {
3552 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
3553 let mut walk = vec![0.0_f64; 60];
3554 for i in 1..walk.len() {
3555 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
3556 }
3557 assert!(adf_test(&walk, AdfModel::ConstantTrend, AdfLags::Fixed(9)).is_none());
3558 let r = adf_test(&walk, AdfModel::ConstantTrend, AdfLags::default())
3559 .expect("a lag below 9 fits");
3560 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
3561 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
3562 }
3563
3564 #[test]
3568 fn adf_matches_statsmodels() {
3569 let mut x = vec![0.0_f64; 120];
3570 for t in 1..x.len() {
3571 let e = ((t * 7919) % 101) as f64 / 50.0 - 1.0;
3572 x[t] = 0.7 * x[t - 1] + e;
3573 }
3574 for (model, lags, stat, n_lags, n_obs) in [
3575 (
3576 AdfModel::Constant,
3577 AdfLags::Aic { max: Some(8) },
3578 -3.436_052_894_8,
3579 5,
3580 114,
3581 ),
3582 (
3583 AdfModel::Constant,
3584 AdfLags::Aic { max: None },
3585 -3.436_052_894_8,
3586 5,
3587 114,
3588 ),
3589 (
3590 AdfModel::ConstantTrend,
3591 AdfLags::Aic { max: Some(6) },
3592 -3.505_247_069_0,
3593 5,
3594 114,
3595 ),
3596 (
3597 AdfModel::None,
3598 AdfLags::Aic { max: Some(5) },
3599 -3.459_240_021_9,
3600 5,
3601 114,
3602 ),
3603 (
3604 AdfModel::Constant,
3605 AdfLags::Fixed(3),
3606 -4.652_930_484_4,
3607 3,
3608 116,
3609 ),
3610 ] {
3611 let r = adf_test(&x, model, lags).expect("fits");
3612 assert_eq!((r.n_lags, r.n_obs), (n_lags, n_obs), "{model:?} {lags:?}");
3613 assert!(
3614 (r.statistic - stat).abs() < 1e-8,
3615 "{model:?} {lags:?}: {}",
3616 r.statistic
3617 );
3618 }
3619 }
3620
3621 #[test]
3622 fn adf_critical_values_constant() {
3623 let cv = adf_critical_values(AdfModel::Constant, 100);
3625 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3627 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3628 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3629 }
3630
3631 #[test]
3632 fn adf_critical_values_ordering() {
3633 let cv = adf_critical_values(AdfModel::Constant, 50);
3634 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3636 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3637 }
3638}
3639
3640#[cfg(test)]
3641mod proptests {
3642 use super::*;
3643 use proptest::prelude::*;
3644
3645 proptest! {
3646 #[test]
3647 fn one_sample_p_bounded(
3648 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3649 mu0 in -1e3_f64..1e3
3650 ) {
3651 if let Some(r) = one_sample_t_test(&data, mu0) {
3652 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3653 }
3654 }
3655
3656 #[test]
3657 fn two_sample_p_bounded(
3658 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3659 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3660 ) {
3661 if let Some(r) = two_sample_t_test(&a, &b) {
3662 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3663 }
3664 }
3665
3666 #[test]
3667 fn anova_p_bounded(
3668 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3669 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3670 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3671 ) {
3672 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3673 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3674 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3675 }
3676 }
3677
3678 #[test]
3679 fn bonferroni_monotone(
3680 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3681 ) {
3682 let adj = bonferroni_correction(&ps).expect("should compute");
3683 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3684 prop_assert!(adjusted >= orig - 1e-15,
3685 "adj[{i}] = {adjusted} < orig = {orig}");
3686 }
3687 }
3688
3689 #[test]
3690 fn shapiro_wilk_p_bounded(
3691 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3692 ) {
3693 if let Some(r) = shapiro_wilk_test(&data) {
3694 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3695 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3696 }
3697 }
3698
3699 #[test]
3700 fn anderson_darling_p_bounded(
3701 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3702 ) {
3703 if let Some(r) = anderson_darling_test(&data) {
3704 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3705 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3706 }
3707 }
3708
3709 #[test]
3710 fn mann_whitney_p_bounded(
3711 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3712 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3713 ) {
3714 if let Some(r) = mann_whitney_u_test(&a, &b) {
3715 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3716 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3717 }
3718 }
3719
3720 #[test]
3721 fn wilcoxon_p_bounded(
3722 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3723 ) {
3724 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3725 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3726 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3727 }
3728 }
3729
3730 #[test]
3731 fn bartlett_p_bounded(
3732 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3733 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3734 ) {
3735 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3736 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3737 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3738 }
3739 }
3740
3741 #[test]
3742 fn fisher_p_bounded(
3743 a in 0_u64..20,
3744 b in 0_u64..20,
3745 c in 0_u64..20,
3746 d in 0_u64..20,
3747 ) {
3748 if let Some(r) = fisher_exact_test(a, b, c, d) {
3749 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3750 }
3751 }
3752
3753 #[test]
3754 fn mann_kendall_p_bounded(
3755 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3756 ) {
3757 if let Some(r) = mann_kendall_test(&data) {
3758 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3759 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3760 "tau = {}", r.kendall_tau);
3761 }
3762 }
3763
3764 #[test]
3765 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3766 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3767 let r = mann_kendall_test(&data).expect("should compute");
3768 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3769 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3770 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3771 }
3772 }
3773}