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)]
1832pub enum AdfModel {
1833 None,
1835 Constant,
1837 ConstantTrend,
1839}
1840
1841pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1877 let n = data.len();
1878 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1879 return None;
1880 }
1881
1882 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1884
1885 let best_lag = match max_lags {
1887 Some(p) => p, None => {
1889 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1891 let p_max = schwert.min(n / 3);
1892 select_adf_lag(data, &dy, model, p_max)
1894 }
1895 };
1896
1897 adf_ols(data, &dy, model, best_lag)
1899}
1900
1901fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1903 let mut best_aic = f64::INFINITY;
1904 let mut best_p = 0;
1905
1906 for p in 0..=p_max {
1907 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1908 if aic < best_aic {
1909 best_aic = aic;
1910 best_p = p;
1911 }
1912 }
1913 }
1914
1915 best_p
1916}
1917
1918#[allow(clippy::type_complexity)]
1922fn adf_build_matrix(
1923 data: &[f64],
1924 dy: &[f64],
1925 model: AdfModel,
1926 p: usize,
1927) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1928 let start = p + 1;
1929 if start >= dy.len() || dy.len() - start < 5 {
1930 return None;
1931 }
1932 let m = dy.len() - start;
1933
1934 let y_dep: Vec<f64> = dy[start..].to_vec();
1935
1936 let has_intercept = !matches!(model, AdfModel::None);
1938 let has_trend = matches!(model, AdfModel::ConstantTrend);
1939 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1940
1941 let mut x_data = Vec::with_capacity(m * ncols);
1943 let mut gamma_col = 0;
1944
1945 for i in 0..m {
1946 let t = start + i;
1947 if has_intercept {
1948 x_data.push(1.0); gamma_col = 1;
1950 }
1951 x_data.push(data[t]); if has_trend {
1953 x_data.push((t + 1) as f64); }
1955 for lag in 1..=p {
1956 x_data.push(dy[t - lag]);
1957 }
1958 }
1959
1960 Some((x_data, y_dep, m, ncols, gamma_col))
1961}
1962
1963fn adf_ols_core(
1967 x_data: &[f64],
1968 y: &[f64],
1969 m: usize,
1970 ncols: usize,
1971 gamma_col: usize,
1972) -> Option<(f64, f64, usize)> {
1973 let mut xtx = vec![0.0_f64; ncols * ncols];
1975 for i in 0..m {
1976 let row = &x_data[i * ncols..(i + 1) * ncols];
1977 for j in 0..ncols {
1978 for k in j..ncols {
1979 xtx[j * ncols + k] += row[j] * row[k];
1980 }
1981 }
1982 }
1983 for j in 0..ncols {
1985 for k in (j + 1)..ncols {
1986 xtx[k * ncols + j] = xtx[j * ncols + k];
1987 }
1988 }
1989
1990 let mut xty = vec![0.0_f64; ncols];
1992 for i in 0..m {
1993 let row = &x_data[i * ncols..(i + 1) * ncols];
1994 for j in 0..ncols {
1995 xty[j] += row[j] * y[i];
1996 }
1997 }
1998
1999 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2001 for i in 0..ncols {
2002 for j in 0..ncols {
2003 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2004 }
2005 augmented[i * (ncols + 1) + ncols] = xty[i];
2006 }
2007
2008 for col in 0..ncols {
2009 let mut max_row = col;
2011 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2012 for row in (col + 1)..ncols {
2013 let val = augmented[row * (ncols + 1) + col].abs();
2014 if val > max_val {
2015 max_val = val;
2016 max_row = row;
2017 }
2018 }
2019 if max_val < 1e-15 {
2020 return None; }
2022 if max_row != col {
2023 for j in 0..=ncols {
2024 let a = col * (ncols + 1) + j;
2025 let b = max_row * (ncols + 1) + j;
2026 augmented.swap(a, b);
2027 }
2028 }
2029
2030 let pivot = augmented[col * (ncols + 1) + col];
2031 for row in (col + 1)..ncols {
2032 let factor = augmented[row * (ncols + 1) + col] / pivot;
2033 for j in col..=ncols {
2034 let above = augmented[col * (ncols + 1) + j];
2035 augmented[row * (ncols + 1) + j] -= factor * above;
2036 }
2037 }
2038 }
2039
2040 let mut beta = vec![0.0_f64; ncols];
2042 for i in (0..ncols).rev() {
2043 let mut sum = augmented[i * (ncols + 1) + ncols];
2044 for j in (i + 1)..ncols {
2045 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2046 }
2047 beta[i] = sum / augmented[i * (ncols + 1) + i];
2048 }
2049
2050 let mut rss = 0.0;
2052 for i in 0..m {
2053 let row = &x_data[i * ncols..(i + 1) * ncols];
2054 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2055 let resid = y[i] - y_hat;
2056 rss += resid * resid;
2057 }
2058
2059 let yy: f64 = y.iter().map(|v| v * v).sum();
2065 if yy == 0.0 || rss <= yy * 1e-16 {
2066 return None;
2067 }
2068
2069 let df = m - ncols;
2071 if df == 0 {
2072 return None;
2073 }
2074 let mse = rss / df as f64;
2075
2076 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2079 for j in 0..ncols {
2080 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2081 }
2082 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2083 }
2084
2085 for col in 0..ncols {
2087 let mut max_row = col;
2088 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2089 for row in (col + 1)..ncols {
2090 let val = xtx_aug[row * 2 * ncols + col].abs();
2091 if val > max_val {
2092 max_val = val;
2093 max_row = row;
2094 }
2095 }
2096 if max_val < 1e-15 {
2097 return None;
2098 }
2099 if max_row != col {
2100 for j in 0..(2 * ncols) {
2101 let a = col * 2 * ncols + j;
2102 let b = max_row * 2 * ncols + j;
2103 xtx_aug.swap(a, b);
2104 }
2105 }
2106
2107 let pivot = xtx_aug[col * 2 * ncols + col];
2108 for j in 0..(2 * ncols) {
2109 xtx_aug[col * 2 * ncols + j] /= pivot;
2110 }
2111 for row in 0..ncols {
2112 if row == col {
2113 continue;
2114 }
2115 let factor = xtx_aug[row * 2 * ncols + col];
2116 for j in 0..(2 * ncols) {
2117 let above = xtx_aug[col * 2 * ncols + j];
2118 xtx_aug[row * 2 * ncols + j] -= factor * above;
2119 }
2120 }
2121 }
2122
2123 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2125 if var_gamma <= 0.0 {
2126 return None;
2127 }
2128 let se_gamma = var_gamma.sqrt();
2129 let t_gamma = beta[gamma_col] / se_gamma;
2130
2131 Some((t_gamma, rss, ncols))
2132}
2133
2134fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2136 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2137 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2138 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2139 Some((aic, m))
2140}
2141
2142fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2144 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2145 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2146
2147 let critical_values = adf_critical_values(model, m);
2148
2149 let rejected = [
2150 gamma_t <= critical_values[0],
2151 gamma_t <= critical_values[1],
2152 gamma_t <= critical_values[2],
2153 ];
2154
2155 Some(AdfResult {
2156 statistic: gamma_t,
2157 n_lags: p,
2158 n_obs: m,
2159 critical_values,
2160 rejected,
2161 })
2162}
2163
2164fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2168 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2173 AdfModel::None => (
2174 [-2.5658, -1.9393, -1.6156],
2175 [-1.960, -0.398, -0.181],
2176 [-10.04, 0.0, 0.0],
2177 ),
2178 AdfModel::Constant => (
2179 [-3.4336, -2.8621, -2.5671],
2180 [-5.999, -2.738, -1.438],
2181 [-29.25, -8.36, -4.48],
2182 ),
2183 AdfModel::ConstantTrend => (
2184 [-3.9638, -3.4126, -3.1279],
2185 [-8.353, -4.039, -2.418],
2186 [-47.44, -17.83, -7.58],
2187 ),
2188 };
2189
2190 let nf = n as f64;
2191 let inv_n = 1.0 / nf;
2192 let inv_n2 = inv_n * inv_n;
2193
2194 [
2195 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2196 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2197 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2198 ]
2199}
2200
2201#[cfg(test)]
2202mod tests {
2203 use super::*;
2204
2205 #[test]
2206 fn corrections_refuse_what_is_not_a_probability() {
2207 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2208 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2209 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2210 }
2211
2212 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2218 let mut a = seed;
2219 move || {
2220 a = a.wrapping_add(0x6d2b_79f5);
2221 let mut t = a;
2222 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2223 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2224 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2225 }
2226 }
2227
2228 #[test]
2234 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2235 let mut rng = mulberry32(0x5350_4331);
2236 let full: Vec<f64> = (0..20_000)
2237 .map(|_| {
2238 let u = rng();
2239 -(1.0 - u).ln() })
2241 .collect();
2242
2243 let mut prev_a2 = 0.0_f64;
2244 let mut prev_p = f64::INFINITY;
2245 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2246 let r = anderson_darling_test(&full[..n]).expect("computes");
2247 assert!(
2249 r.statistic_star > prev_a2,
2250 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2251 r.statistic_star
2252 );
2253 assert!(
2255 r.p_value < 0.5,
2256 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2257 r.statistic_star,
2258 r.p_value
2259 );
2260 assert!(
2262 r.p_value <= prev_p + 1e-12,
2263 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2264 r.p_value
2265 );
2266 prev_a2 = r.statistic_star;
2267 prev_p = r.p_value;
2268 }
2269 }
2270
2271 #[test]
2276 fn one_sample_null_true() {
2277 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2278 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2279 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2280 }
2281
2282 #[test]
2283 fn one_sample_null_false() {
2284 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2285 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2286 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2287 }
2288
2289 #[test]
2290 fn one_sample_edge_cases() {
2291 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());
2294 }
2295
2296 #[test]
2301 fn two_sample_same_mean() {
2302 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2303 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2304 let r = two_sample_t_test(&a, &b).expect("should compute");
2305 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2306 }
2307
2308 #[test]
2309 fn two_sample_different_means() {
2310 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2311 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2312 let r = two_sample_t_test(&a, &b).expect("should compute");
2313 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2314 }
2315
2316 #[test]
2317 fn two_sample_different_sizes() {
2318 let a = [1.0, 2.0, 3.0];
2319 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2320 let r = two_sample_t_test(&a, &b).expect("should compute");
2321 assert!(r.p_value < 0.05);
2322 }
2323
2324 #[test]
2325 fn two_sample_edge_cases() {
2326 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2327 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2328 }
2329
2330 #[test]
2335 fn paired_no_difference() {
2336 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2337 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2338 let r = paired_t_test(&x, &y).expect("should compute");
2339 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2340 }
2341
2342 #[test]
2343 fn paired_significant_difference() {
2344 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2346 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2347 let r = paired_t_test(&before, &after).expect("should compute");
2348 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2349 assert!(r.statistic < 0.0); }
2351
2352 #[test]
2353 fn paired_edge_cases() {
2354 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2357
2358 #[test]
2363 fn anova_same_means() {
2364 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2365 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2366 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2367 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2368 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2369 }
2370
2371 #[test]
2372 fn anova_different_means() {
2373 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2374 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2375 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2376 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2377 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2378 assert!(r.df_between == 2);
2379 assert!(r.df_within == 12);
2380 }
2381
2382 #[test]
2383 fn anova_ss_decomposition() {
2384 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2385 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2386 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2387 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2389 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2390 assert!(
2391 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2392 "SS decomposition: {ss_total} vs {} + {}",
2393 r.ss_between,
2394 r.ss_within
2395 );
2396 }
2397
2398 #[test]
2399 fn anova_edge_cases() {
2400 let g1 = [1.0, 2.0, 3.0];
2401 assert!(one_way_anova(&[&g1]).is_none()); }
2403
2404 #[test]
2409 fn chi2_gof_uniform() {
2410 let observed = [25.0, 25.0, 25.0, 25.0];
2412 let expected = [25.0, 25.0, 25.0, 25.0];
2413 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2414 assert!((r.statistic).abs() < 1e-15);
2415 assert!((r.p_value - 1.0).abs() < 0.01);
2416 }
2417
2418 #[test]
2419 fn chi2_gof_significant() {
2420 let observed = [90.0, 10.0];
2421 let expected = [50.0, 50.0];
2422 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2423 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2424 }
2425
2426 #[test]
2427 fn chi2_gof_edge_cases() {
2428 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()); }
2432
2433 #[test]
2438 fn chi2_independence_significant() {
2439 let table = [30.0, 10.0, 10.0, 50.0];
2441 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2442 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2443 assert!((r.df - 1.0).abs() < 1e-10);
2444 }
2445
2446 #[test]
2447 fn chi2_independence_not_significant() {
2448 let table = [25.0, 25.0, 25.0, 25.0];
2450 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2451 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2452 }
2453
2454 #[test]
2455 fn chi2_independence_3x3() {
2456 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2457 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2458 assert!((r.df - 4.0).abs() < 1e-10);
2459 assert!(r.p_value < 0.05);
2460 }
2461
2462 #[test]
2463 fn chi2_independence_edge_cases() {
2464 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()); }
2468
2469 #[test]
2474 fn jb_normal_data() {
2475 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2477 let r = jarque_bera_test(&data).expect("should compute");
2478 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2479 }
2480
2481 #[test]
2482 fn jb_skewed_data() {
2483 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2485 let r = jarque_bera_test(&data).expect("should compute");
2486 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2487 }
2488
2489 #[test]
2496 fn jb_is_the_moment_form_the_paper_defines() {
2497 let data = [
2498 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,
2499 3.6, 2.8, 3.2,
2500 ];
2501 let r = jarque_bera_test(&data).expect("20 finite points");
2502 assert!(
2503 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2504 "JB = {}",
2505 r.statistic
2506 );
2507
2508 let n = data.len() as f64;
2511 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2512 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2513 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2514 assert!(
2515 adjusted > r.statistic * 1.3,
2516 "the two forms differ by more than a third here: {adjusted} vs {}",
2517 r.statistic
2518 );
2519 }
2520
2521 #[test]
2525 fn jb_p_value_keeps_its_digits_in_the_tail() {
2526 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2529 data.push(60.0);
2530 let r = jarque_bera_test(&data).expect("20 finite points");
2531 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2532 assert!(
2533 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2534 "this sample is only interesting because the complement underflows"
2535 );
2536 assert!(
2537 r.p_value > 0.0,
2538 "the complement returns 0 here; this must not"
2539 );
2540 assert!(
2541 (r.p_value - 6.702e-21).abs() < 1e-23,
2542 "p = {} (expected about 6.7e-21)",
2543 r.p_value
2544 );
2545 assert!(
2546 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2547 "p = {} vs exp(-JB/2) = {}",
2548 r.p_value,
2549 (-r.statistic / 2.0).exp()
2550 );
2551 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])
2554 .expect("10 finite points");
2555 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2556 assert!(
2557 (small.p_value - via_cdf).abs() < 1e-12,
2558 "{} vs {via_cdf}",
2559 small.p_value
2560 );
2561 }
2562
2563 #[test]
2564 fn jb_edge_cases() {
2565 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2567
2568 #[test]
2573 fn ad_normal_data() {
2574 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2575 let r = anderson_darling_test(&data).expect("should compute");
2576 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2577 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2578 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2579 }
2580
2581 #[test]
2582 fn ad_skewed_data() {
2583 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];
2585 let r = anderson_darling_test(&data).expect("should compute");
2586 assert!(
2587 r.p_value < 0.05,
2588 "p = {} (should reject normality)",
2589 r.p_value
2590 );
2591 }
2592
2593 #[test]
2594 fn ad_bimodal_data() {
2595 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2597 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2598 let r = anderson_darling_test(&data).expect("should compute");
2599 assert!(
2600 r.p_value < 0.01,
2601 "p = {} (bimodal should reject normality)",
2602 r.p_value
2603 );
2604 }
2605
2606 #[test]
2607 fn ad_large_normal_sample() {
2608 let n = 100;
2609 let data: Vec<f64> = (1..=n)
2610 .map(|i| {
2611 let p = (i as f64 - 0.5) / n as f64;
2612 special::inverse_normal_cdf(p)
2613 })
2614 .collect();
2615 let r = anderson_darling_test(&data).expect("should compute");
2616 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2617 }
2618
2619 #[test]
2620 fn ad_edge_cases() {
2621 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());
2624 }
2625
2626 #[test]
2627 fn ad_p_value_ranges() {
2628 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2633 let r = anderson_darling_test(&near_normal).expect("should compute");
2634 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2635
2636 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];
2638 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2639 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2640 }
2641
2642 #[test]
2647 fn ad_normal_data_large_p() {
2648 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2650 let r = anderson_darling_test(&data).unwrap();
2651 assert!(r.p_value > 0.05, "p={}", r.p_value);
2652 }
2653
2654 #[test]
2655 fn ad_exponential_data_small_p() {
2656 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2658 let r = anderson_darling_test(&data).unwrap();
2659 assert!(r.p_value < 0.05, "p={}", r.p_value);
2660 }
2661
2662 #[test]
2663 fn ad_statistic_non_negative() {
2664 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2665 let r = anderson_darling_test(&data).unwrap();
2666 assert!(r.statistic >= 0.0);
2667 assert!(r.statistic_star >= r.statistic);
2668 }
2669
2670 #[test]
2671 fn ad_p_value_in_range() {
2672 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2673 let r = anderson_darling_test(&data).unwrap();
2674 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2675 }
2676
2677 #[test]
2678 fn ad_insufficient_data() {
2679 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
2681
2682 #[test]
2683 fn ad_degenerate_data() {
2684 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
2686 }
2687
2688 #[test]
2689 fn ad_modified_statistic_formula() {
2690 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
2692 let r = anderson_darling_test(&data).unwrap();
2693 assert!(r.statistic_star > r.statistic - 1e-10);
2694 assert!(anderson_darling_test(&data[..7]).is_none());
2696 }
2697
2698 #[test]
2703 fn ad_correction_factor_formula() {
2704 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2705 let n = data.len() as f64;
2706 let r = anderson_darling_test(&data).unwrap();
2707
2708 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2710 assert!(
2711 (r.statistic_star - expected_star).abs() < 1e-12,
2712 "A²* = {}, expected = {}",
2713 r.statistic_star,
2714 expected_star
2715 );
2716
2717 assert!(
2719 r.statistic >= 0.0,
2720 "A² = {} must be non-negative",
2721 r.statistic
2722 );
2723 }
2724
2725 #[test]
2731 fn ad_formula_structure_symmetric_vs_skewed() {
2732 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2734 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2736
2737 let r_sym = anderson_darling_test(&symmetric).unwrap();
2738 let r_skew = anderson_darling_test(&skewed).unwrap();
2739
2740 assert!(
2741 r_sym.statistic < r_skew.statistic,
2742 "Symmetric A²={} should be less than skewed A²={}",
2743 r_sym.statistic,
2744 r_skew.statistic
2745 );
2746 }
2747
2748 #[test]
2753 fn ad_pvalue_invariants() {
2754 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2756 let r_normal = anderson_darling_test(&normal_data).unwrap();
2757 assert!(
2758 r_normal.p_value > 0.05,
2759 "Normal data: p = {} (expected > 0.05)",
2760 r_normal.p_value
2761 );
2762
2763 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2765 let r_exp = anderson_darling_test(&exp_data).unwrap();
2766 assert!(
2767 r_exp.p_value < 0.05,
2768 "Exponential data: p = {} (expected < 0.05)",
2769 r_exp.p_value
2770 );
2771 }
2772
2773 #[test]
2778 fn sw_normal_data() {
2779 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2781 let r = shapiro_wilk_test(&data).expect("should compute");
2782 assert!(r.w > 0.9, "W = {}", r.w);
2783 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2784 }
2785
2786 #[test]
2787 fn sw_bimodal_data() {
2788 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2790 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2791 let r = shapiro_wilk_test(&data).expect("should compute");
2792 assert!(
2793 r.p_value < 0.01,
2794 "p = {} (bimodal should reject normality)",
2795 r.p_value
2796 );
2797 }
2798
2799 #[test]
2800 fn sw_n3() {
2801 let data = [1.0, 2.0, 3.0];
2802 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2803 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2804 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2805 }
2806
2807 #[test]
2808 fn sw_n4() {
2809 let data = [1.0, 2.0, 3.0, 4.0];
2810 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2811 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2812 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2813 }
2814
2815 #[test]
2816 fn sw_n5() {
2817 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2818 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2819 assert!(r.w > 0.9, "W = {}", r.w);
2820 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2821 }
2822
2823 #[test]
2824 fn sw_skewed_data() {
2825 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];
2827 let r = shapiro_wilk_test(&data).expect("should compute");
2828 assert!(
2829 r.p_value < 0.05,
2830 "p = {} (skewed data should reject normality)",
2831 r.p_value
2832 );
2833 }
2834
2835 #[test]
2836 fn sw_large_normal_sample() {
2837 let n = 100;
2840 let data: Vec<f64> = (1..=n)
2841 .map(|i| {
2842 let p = (i as f64 - 0.5) / n as f64;
2843 special::inverse_normal_cdf(p)
2844 })
2845 .collect();
2846 let r = shapiro_wilk_test(&data).expect("should compute");
2847 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2848 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2849 }
2850
2851 #[test]
2852 fn sw_w_bounded() {
2853 let datasets: Vec<Vec<f64>> = vec![
2855 vec![1.0, 2.0, 3.0],
2856 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2857 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2858 (0..20).map(|i| (i as f64).powi(2)).collect(),
2859 ];
2860 for (idx, data) in datasets.iter().enumerate() {
2861 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2862 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2863 assert!(
2864 r.p_value >= 0.0 && r.p_value <= 1.0,
2865 "dataset {idx}: p = {}",
2866 r.p_value
2867 );
2868 }
2869 }
2870
2871 #[test]
2872 fn sw_edge_cases() {
2873 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()); }
2879
2880 #[test]
2881 fn sw_n5001_rejected() {
2882 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2883 assert!(shapiro_wilk_test(&data).is_none()); }
2885
2886 #[test]
2891 fn mw_clearly_different() {
2892 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2893 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2894 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2895 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2896 }
2897
2898 #[test]
2899 fn mw_same_distribution() {
2900 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2901 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2902 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2903 assert!(
2904 r.p_value > 0.3,
2905 "p = {} (interleaved, same dist)",
2906 r.p_value
2907 );
2908 }
2909
2910 #[test]
2911 fn mw_with_ties() {
2912 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2913 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2914 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2915 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2916 }
2917
2918 #[test]
2919 fn mw_different_sizes() {
2920 let a = [1.0, 2.0, 3.0];
2921 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2922 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2923 assert!(r.p_value < 0.05);
2924 }
2925
2926 #[test]
2927 fn mw_edge_cases() {
2928 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());
2931 }
2932
2933 #[test]
2938 fn wilcoxon_significant_increase() {
2939 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2940 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2941 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2942 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2943 }
2944
2945 #[test]
2946 fn wilcoxon_no_difference() {
2947 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2948 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2949 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2950 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2951 }
2952
2953 #[test]
2954 fn wilcoxon_with_ties() {
2955 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2957 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2959 assert!(r.statistic > 0.0);
2961 }
2962
2963 #[test]
2964 fn wilcoxon_with_zero_diffs() {
2965 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2967 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2969 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2970 }
2971
2972 #[test]
2973 fn wilcoxon_edge_cases() {
2974 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());
2978 }
2979
2980 #[test]
2985 fn mann_whitney_small_samples_use_the_exact_distribution() {
2986 let cases = [
2988 (3, 3, 0, 0.1000, 0.0495),
2989 (4, 4, 0, 0.0286, 0.0209),
2990 (4, 4, 1, 0.0571, 0.0433),
2991 (5, 5, 2, 0.0317, 0.0283),
2992 (8, 8, 13, 0.0499, 0.0460),
2993 (10, 10, 23, 0.0433, 0.0413),
2994 ];
2995 for (n1, n2, u, exact, old) in cases {
2996 let (a, b) = separated_samples(n1, n2, u);
2997 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
2998 assert_eq!(r.statistic, u as f64);
2999 assert!(
3000 (r.p_value - exact).abs() < 1e-4,
3001 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3002 r.p_value
3003 );
3004 }
3005 }
3006
3007 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3011 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3012 let mut remaining = u;
3013 for k in (0..n1).rev() {
3014 let room = n2.min(remaining);
3015 ranks_a[k] += room;
3016 remaining -= room;
3017 if remaining == 0 {
3018 break;
3019 }
3020 }
3021 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3022 (
3023 ranks_a.iter().map(|&r| r as f64).collect(),
3024 ranks_b.iter().map(|&r| r as f64).collect(),
3025 )
3026 }
3027
3028 #[test]
3030 fn wilcoxon_small_samples_use_the_exact_distribution() {
3031 let cases = [
3033 (5, 0, 0.0625),
3034 (5, 1, 0.125),
3035 (6, 2, 0.09375),
3036 (8, 5, 0.078125),
3037 (10, 8, 0.048828125),
3038 (12, 14, 0.0522460938),
3039 ];
3040 for (n, t_plus, exact) in cases {
3041 let positive = ranks_summing_to(n, t_plus);
3043 let x: Vec<f64> = (1..=n)
3044 .map(|r| {
3045 if positive.contains(&r) {
3046 r as f64
3047 } else {
3048 -(r as f64)
3049 }
3050 })
3051 .collect();
3052 let y = vec![0.0; n];
3053 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3054 assert_eq!(r.statistic, t_plus as f64);
3055 assert!(
3056 (r.p_value - exact).abs() < 1e-9,
3057 "n={n} T+={t_plus}: p={} want {exact}",
3058 r.p_value
3059 );
3060 }
3061 }
3062
3063 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3065 let mut out = Vec::new();
3066 let mut left = target;
3067 for r in (1..=n).rev() {
3068 if r <= left {
3069 out.push(r);
3070 left -= r;
3071 }
3072 }
3073 assert_eq!(left, 0);
3074 out
3075 }
3076
3077 #[test]
3080 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3081 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3082 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3083 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3084 let (n1, n2) = (60.0, 60.0);
3085 let mu = n1 * n2 / 2.0;
3086 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3087 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3088 let want = 2.0 * special::standard_normal_sf(z);
3089 assert!((r.p_value - want).abs() < 1e-12);
3090
3091 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3093 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3094 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3095 let mu = 1250.0;
3096 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3097 let u = mann_whitney_u_test(&a, &b)
3098 .expect("valid samples")
3099 .statistic;
3100 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3101 assert!(
3102 (exact - approx).abs() < 1e-3,
3103 "exact {exact} vs approx {approx}"
3104 );
3105 }
3106
3107 #[test]
3112 fn kw_clearly_different() {
3113 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3114 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3115 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3116 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3117 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3118 assert!((r.df - 2.0).abs() < 1e-10);
3119 }
3120
3121 #[test]
3122 fn kw_same_distribution() {
3123 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3124 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3125 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3126 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3127 }
3128
3129 #[test]
3130 fn kw_with_ties() {
3131 let g1 = [1.0, 2.0, 2.0, 3.0];
3132 let g2 = [3.0, 4.0, 4.0, 5.0];
3133 let g3 = [5.0, 6.0, 6.0, 7.0];
3134 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3135 assert!(r.statistic > 0.0);
3136 }
3137
3138 #[test]
3139 fn kw_edge_cases() {
3140 let g1 = [1.0, 2.0, 3.0];
3141 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3143
3144 #[test]
3149 fn levene_equal_variance() {
3150 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3151 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3152 let r = levene_test(&[&g1, &g2]).expect("should compute");
3153 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3155 }
3156
3157 #[test]
3158 fn levene_unequal_variance() {
3159 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");
3162 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3163 }
3164
3165 #[test]
3166 fn levene_three_groups() {
3167 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3168 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3169 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3170 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3171 assert!(r.df >= 2.0);
3172 }
3173
3174 #[test]
3175 fn levene_edge_cases() {
3176 let g1 = [1.0, 2.0, 3.0];
3177 assert!(levene_test(&[&g1]).is_none()); }
3179
3180 #[test]
3185 fn bonferroni_basic() {
3186 let ps = [0.01, 0.04, 0.03, 0.005];
3187 let adj = bonferroni_correction(&ps).expect("should compute");
3188 assert!((adj[0] - 0.04).abs() < 1e-10);
3189 assert!((adj[1] - 0.16).abs() < 1e-10);
3190 assert!((adj[2] - 0.12).abs() < 1e-10);
3191 assert!((adj[3] - 0.02).abs() < 1e-10);
3192 }
3193
3194 #[test]
3195 fn bonferroni_capped_at_one() {
3196 let ps = [0.5, 0.6];
3197 let adj = bonferroni_correction(&ps).expect("should compute");
3198 assert!((adj[0] - 1.0).abs() < 1e-10);
3199 assert!((adj[1] - 1.0).abs() < 1e-10);
3200 }
3201
3202 #[test]
3203 fn bh_basic() {
3204 let ps = [0.01, 0.04, 0.03, 0.005];
3205 let adj = benjamini_hochberg(&ps).expect("should compute");
3206 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3208 assert!(
3209 adjusted >= orig - 1e-15,
3210 "adj[{i}] = {adjusted} < original {orig}"
3211 );
3212 }
3213 }
3216
3217 #[test]
3218 fn bh_all_significant() {
3219 let ps = [0.001, 0.002, 0.003];
3220 let adj = benjamini_hochberg(&ps).expect("should compute");
3221 for &a in &adj {
3222 assert!(a < 0.05);
3223 }
3224 }
3225
3226 #[test]
3227 fn correction_edge_cases() {
3228 assert!(bonferroni_correction(&[]).is_none());
3229 assert!(benjamini_hochberg(&[]).is_none());
3230 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3231 }
3232
3233 #[test]
3238 fn bartlett_equal_variances() {
3239 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3240 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3241 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3242 assert!(
3243 r.p_value > 0.9,
3244 "equal variance → p high, got {}",
3245 r.p_value
3246 );
3247 }
3248
3249 #[test]
3250 fn bartlett_unequal_variances() {
3251 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");
3254 assert!(
3255 r.p_value < 0.01,
3256 "very different variances → p < 0.01, got {}",
3257 r.p_value
3258 );
3259 assert!((r.df - 1.0).abs() < 1e-10); }
3261
3262 #[test]
3263 fn bartlett_three_groups() {
3264 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3265 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3266 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3268 assert!(r.p_value < 0.05, "one group with high variance");
3269 assert!((r.df - 2.0).abs() < 1e-10); }
3271
3272 #[test]
3273 fn bartlett_edge_cases() {
3274 let g1 = [1.0, 2.0, 3.0];
3275 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3279
3280 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3282 }
3283
3284 #[test]
3289 fn fisher_tea_tasting() {
3290 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3292 assert!(
3294 (r.p_value - 0.4857).abs() < 0.01,
3295 "p ≈ 0.4857, got {}",
3296 r.p_value
3297 );
3298 }
3299
3300 #[test]
3301 fn fisher_significant() {
3302 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3304 assert!(r.p_value < 0.001, "perfect association → p very small");
3305 }
3306
3307 #[test]
3308 fn fisher_no_association() {
3309 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3311 assert!(
3312 r.p_value > 0.9,
3313 "no association → p ≈ 1.0, got {}",
3314 r.p_value
3315 );
3316 }
3317
3318 #[test]
3319 fn fisher_small_table() {
3320 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3322 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3323 }
3324
3325 #[test]
3326 fn fisher_asymmetric() {
3327 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3329 assert!(r.p_value < 0.05, "significant association");
3330 }
3331
3332 #[test]
3333 fn fisher_edge_cases() {
3334 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()); }
3339
3340 #[test]
3341 fn fisher_odds_ratio() {
3342 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3343 assert!(
3345 (r.statistic - 9.0).abs() < 1e-10,
3346 "OR = 9, got {}",
3347 r.statistic
3348 );
3349 }
3350
3351 #[test]
3356 fn mk_increasing_trend() {
3357 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3358 let r = mann_kendall_test(&data).expect("should compute");
3359 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3360 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3361 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3362 assert!(r.s_statistic > 0);
3363 }
3364
3365 #[test]
3366 fn mk_decreasing_trend() {
3367 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3368 let r = mann_kendall_test(&data).expect("should compute");
3369 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3370 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3371 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3372 assert!(r.s_statistic < 0);
3373 }
3374
3375 #[test]
3376 fn mk_no_trend() {
3377 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3379 let r = mann_kendall_test(&data).expect("should compute");
3380 assert!(
3382 r.p_value > 0.05,
3383 "p = {} (should be > 0.05 for no trend)",
3384 r.p_value
3385 );
3386 }
3387
3388 #[test]
3389 fn mk_perfect_monotone() {
3390 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3391 let r = mann_kendall_test(&data).expect("should compute");
3392 assert_eq!(r.s_statistic, 45);
3394 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3395 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3396 }
3397
3398 #[test]
3399 fn mk_with_ties() {
3400 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3401 let r = mann_kendall_test(&data).expect("should compute");
3402 assert!(r.s_statistic > 0);
3403 let n = data.len() as f64;
3405 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3406 assert!(r.variance < base_var, "ties should reduce variance");
3407 }
3408
3409 #[test]
3410 fn mk_edge_cases() {
3411 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3413 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3415 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3417 }
3418
3419 #[test]
3420 fn mk_minimum_n() {
3421 let data = [1.0, 2.0, 3.0, 4.0];
3423 let r = mann_kendall_test(&data).expect("n=4 should work");
3424 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3426 }
3427
3428 #[test]
3429 fn mk_sen_slope_robust_to_outlier() {
3430 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3432 let r = mann_kendall_test(&data).expect("should compute");
3433 assert!(
3435 (r.sen_slope - 1.0).abs() < 0.5,
3436 "sen slope = {}, expected ≈ 1.0",
3437 r.sen_slope
3438 );
3439 }
3440
3441 #[test]
3446 fn adf_stationary_mean_reverting() {
3447 let data = [
3450 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,
3451 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,
3452 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,
3453 ];
3454 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3455 assert!(
3457 r.rejected[2],
3458 "should reject at 10%: stat={}, cv={}",
3459 r.statistic, r.critical_values[2]
3460 );
3461 }
3462
3463 #[test]
3464 fn adf_nonstationary_random_walk() {
3465 let increments = [
3467 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,
3468 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,
3469 -0.13, 0.15, -0.1,
3470 ];
3471 let mut walk = Vec::with_capacity(increments.len());
3472 let mut cum = 0.0;
3473 for &inc in &increments {
3474 cum += inc;
3475 walk.push(cum);
3476 }
3477 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3478 assert!(
3480 !r.rejected[0],
3481 "should NOT reject at 1%: stat={}, cv={}",
3482 r.statistic, r.critical_values[0]
3483 );
3484 }
3485
3486 #[test]
3487 fn adf_with_fixed_lags() {
3488 let mut rng = mulberry32(0xadf2);
3492 let data: Vec<f64> = (0..50)
3493 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
3494 .collect();
3495 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3496 assert_eq!(r.n_lags, 2);
3497 assert!(r.statistic.is_finite());
3498 }
3499
3500 #[test]
3501 fn adf_constant_trend_model() {
3502 let data: Vec<f64> = (0..30)
3505 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3506 .collect();
3507 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3508 assert!(r.statistic.is_finite());
3509 assert_eq!(r.critical_values.len(), 3);
3510 }
3511
3512 #[test]
3513 fn adf_edge_cases() {
3514 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3516 let mut data = vec![0.0; 20];
3518 data[5] = f64::NAN;
3519 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3520 }
3521
3522 #[test]
3528 fn adf_exact_fit_is_undefined_and_not_chosen() {
3529 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
3530 let mut walk = vec![0.0_f64; 60];
3531 for i in 1..walk.len() {
3532 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
3533 }
3534 assert!(adf_test(&walk, AdfModel::ConstantTrend, Some(9)).is_none());
3535 let r = adf_test(&walk, AdfModel::ConstantTrend, None).expect("a lag below 9 fits");
3536 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
3537 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
3538 }
3539
3540 #[test]
3541 fn adf_critical_values_constant() {
3542 let cv = adf_critical_values(AdfModel::Constant, 100);
3544 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3546 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3547 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3548 }
3549
3550 #[test]
3551 fn adf_critical_values_ordering() {
3552 let cv = adf_critical_values(AdfModel::Constant, 50);
3553 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3555 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3556 }
3557}
3558
3559#[cfg(test)]
3560mod proptests {
3561 use super::*;
3562 use proptest::prelude::*;
3563
3564 proptest! {
3565 #[test]
3566 fn one_sample_p_bounded(
3567 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3568 mu0 in -1e3_f64..1e3
3569 ) {
3570 if let Some(r) = one_sample_t_test(&data, mu0) {
3571 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3572 }
3573 }
3574
3575 #[test]
3576 fn two_sample_p_bounded(
3577 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3578 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3579 ) {
3580 if let Some(r) = two_sample_t_test(&a, &b) {
3581 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3582 }
3583 }
3584
3585 #[test]
3586 fn anova_p_bounded(
3587 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3588 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3589 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3590 ) {
3591 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3592 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3593 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3594 }
3595 }
3596
3597 #[test]
3598 fn bonferroni_monotone(
3599 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3600 ) {
3601 let adj = bonferroni_correction(&ps).expect("should compute");
3602 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3603 prop_assert!(adjusted >= orig - 1e-15,
3604 "adj[{i}] = {adjusted} < orig = {orig}");
3605 }
3606 }
3607
3608 #[test]
3609 fn shapiro_wilk_p_bounded(
3610 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3611 ) {
3612 if let Some(r) = shapiro_wilk_test(&data) {
3613 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3614 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3615 }
3616 }
3617
3618 #[test]
3619 fn anderson_darling_p_bounded(
3620 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3621 ) {
3622 if let Some(r) = anderson_darling_test(&data) {
3623 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3624 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3625 }
3626 }
3627
3628 #[test]
3629 fn mann_whitney_p_bounded(
3630 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3631 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3632 ) {
3633 if let Some(r) = mann_whitney_u_test(&a, &b) {
3634 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3635 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3636 }
3637 }
3638
3639 #[test]
3640 fn wilcoxon_p_bounded(
3641 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3642 ) {
3643 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3644 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3645 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3646 }
3647 }
3648
3649 #[test]
3650 fn bartlett_p_bounded(
3651 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3652 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3653 ) {
3654 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3655 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3656 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3657 }
3658 }
3659
3660 #[test]
3661 fn fisher_p_bounded(
3662 a in 0_u64..20,
3663 b in 0_u64..20,
3664 c in 0_u64..20,
3665 d in 0_u64..20,
3666 ) {
3667 if let Some(r) = fisher_exact_test(a, b, c, d) {
3668 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3669 }
3670 }
3671
3672 #[test]
3673 fn mann_kendall_p_bounded(
3674 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3675 ) {
3676 if let Some(r) = mann_kendall_test(&data) {
3677 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3678 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3679 "tau = {}", r.kendall_tau);
3680 }
3681 }
3682
3683 #[test]
3684 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3685 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3686 let r = mann_kendall_test(&data).expect("should compute");
3687 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3688 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3689 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3690 }
3691 }
3692}