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> {
601 let n = data.len();
602 if n < 8 {
603 return None;
604 }
605 if data.iter().any(|v| !v.is_finite()) {
606 return None;
607 }
608
609 let mean = stats::mean(data)?;
610 let sd = stats::std_dev(data)?;
611
612 if sd < 1e-300 {
613 return None; }
615
616 let mut x: Vec<f64> = data.to_vec();
618 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
619
620 let nf = n as f64;
621
622 let mut s = 0.0;
624 for i in 0..n {
625 let z = (x[i] - mean) / sd;
626 let z_rev = (x[n - 1 - i] - mean) / sd;
627 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
631 let ln_sf_rev = special::standard_normal_sf(z_rev)
632 .max(f64::MIN_POSITIVE)
633 .ln();
634
635 let coeff = (2 * (i + 1) - 1) as f64;
636 s += coeff * (ln_phi + ln_sf_rev);
637 }
638
639 let a2 = -nf - s / nf;
640
641 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
643
644 let p = ad_upper_tail_pvalue(a2_star);
647
648 Some(AndersonDarlingResult {
649 statistic: a2,
650 statistic_star: a2_star,
651 p_value: p,
652 })
653}
654
655#[derive(Debug, Clone, Copy)]
661pub struct AdNormalityResult {
662 pub statistic: f64,
664 pub statistic_modified: f64,
666 pub p_value: f64,
668}
669
670pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
704 let n = data.len();
705 if n < 3 {
706 return None;
707 }
708 if data.iter().any(|v| !v.is_finite()) {
709 return None;
710 }
711
712 let mean = stats::mean(data)?;
713 let sd = stats::std_dev(data)?;
714
715 if sd < 1e-15 {
716 return None;
717 }
718
719 let mut x: Vec<f64> = data.to_vec();
720 x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
721
722 let nf = n as f64;
723
724 let mut s = 0.0;
725 for i in 0..n {
726 let z_i = (x[i] - mean) / sd;
727 let z_rev = (x[n - 1 - i] - mean) / sd;
728
729 let ln_phi_i = special::standard_normal_cdf(z_i)
730 .max(f64::MIN_POSITIVE)
731 .ln();
732 let ln_sf_rev = special::standard_normal_sf(z_rev)
733 .max(f64::MIN_POSITIVE)
734 .ln();
735
736 let coeff = (2 * i + 1) as f64;
737 s += coeff * (ln_phi_i + ln_sf_rev);
738 }
739
740 let a2 = -nf - s / nf;
741 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
742
743 let p = ad_upper_tail_pvalue(a2_star);
747
748 Some(AdNormalityResult {
749 statistic: a2,
750 statistic_modified: a2_star,
751 p_value: p,
752 })
753}
754
755#[derive(Debug, Clone, Copy)]
757pub struct ShapiroWilkResult {
758 pub w: f64,
760 pub p_value: f64,
762}
763
764pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
804 let n = data.len();
805 if !(3..=5000).contains(&n) {
806 return None;
807 }
808 if data.iter().any(|v| !v.is_finite()) {
809 return None;
810 }
811
812 let mut x: Vec<f64> = data.to_vec();
814 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
815
816 if x[n - 1] - x[0] < 1e-300 {
817 return None; }
819
820 let nn2 = n / 2;
821
822 if n == 3 {
824 return shapiro_wilk_n3(&x);
825 }
826
827 let a = sw_coefficients(n, nn2)?;
829
830 let w = sw_statistic(&x, &a, n, nn2);
832
833 if !(0.0..=1.0 + 1e-10).contains(&w) {
834 return None;
835 }
836 let w = w.min(1.0);
837
838 let p_value = sw_p_value(w, n);
840
841 Some(ShapiroWilkResult {
842 w,
843 p_value: p_value.clamp(0.0, 1.0),
844 })
845}
846
847fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
849 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
852 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
853 if ss < 1e-300 {
854 return None;
855 }
856
857 let numerator = a1 * (x[2] - x[0]);
858 let w = (numerator * numerator) / ss;
859 let w = w.clamp(0.75, 1.0);
860
861 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
863 let p = p.clamp(0.0, 1.0);
864
865 Some(ShapiroWilkResult { w, p_value: p })
866}
867
868const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
870const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
871const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
872const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
873const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
874const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
875const SW_G: [f64; 2] = [-2.273, 0.459];
876
877fn sw_poly(c: &[f64], x: f64) -> f64 {
879 let mut result = c[c.len() - 1];
880 for i in (0..c.len() - 1).rev() {
881 result = result * x + c[i];
882 }
883 result
884}
885
886fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
888 let mut a = vec![0.0; nn2];
889
890 let mut m = vec![0.0; nn2];
892 let mut summ2 = 0.0;
893 for (i, mi) in m.iter_mut().enumerate() {
894 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
896 *mi = special::inverse_normal_cdf(p);
897 summ2 += *mi * *mi;
898 }
899 summ2 *= 2.0;
900 let ssumm2 = summ2.sqrt();
901 let rsn = 1.0 / (n as f64).sqrt();
902
903 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
905
906 if n <= 5 {
907 let fac_sq = summ2 - 2.0 * m[0] * m[0];
909 let one_minus = 1.0 - 2.0 * a1 * a1;
910 if fac_sq <= 0.0 || one_minus <= 0.0 {
911 return None;
912 }
913 let fac = (fac_sq / one_minus).sqrt();
914 a[0] = a1;
915 for i in 1..nn2 {
916 a[i] = -m[i] / fac;
917 }
918 } else {
919 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
921 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
922 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
923 if fac_sq <= 0.0 || one_minus <= 0.0 {
924 return None;
925 }
926 let fac = (fac_sq / one_minus).sqrt();
927 a[0] = a1;
928 a[1] = a2;
929 for i in 2..nn2 {
930 a[i] = -m[i] / fac;
931 }
932 }
933
934 Some(a)
935}
936
937fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
939 let mut sa = 0.0;
941 for i in 0..nn2 {
942 sa += a[i] * (x[n - 1 - i] - x[i]);
943 }
944
945 let mean = x.iter().sum::<f64>() / n as f64;
947 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
948
949 if ss < 1e-300 {
950 return 1.0; }
952
953 (sa * sa) / ss
954}
955
956fn sw_p_value(w: f64, n: usize) -> f64 {
958 let nf = n as f64;
959
960 if n == 3 {
961 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
963 return p.clamp(0.0, 1.0);
964 }
965
966 let w1 = 1.0 - w;
967 if w1 <= 0.0 {
968 return 1.0; }
970
971 let y = w1.ln();
972
973 if n <= 11 {
974 let gamma = sw_poly(&SW_G, nf);
976 if y >= gamma {
977 return 0.0; }
979 let y2 = -(gamma - y).ln();
980 let m = sw_poly(&SW_C3, nf);
981 let s = sw_poly(&SW_C4, nf).exp();
982 if s < 1e-300 {
983 return 0.0;
984 }
985 let z = (y2 - m) / s;
986 special::standard_normal_sf(z)
987 } else {
988 let xx = nf.ln();
990 let m = sw_poly(&SW_C5, xx);
991 let s = sw_poly(&SW_C6, xx).exp();
992 if s < 1e-300 {
993 return 0.0;
994 }
995 let z = (y - m) / s;
996 special::standard_normal_sf(z)
997 }
998}
999
1000pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
1044 let n1 = a.len();
1045 let n2 = b.len();
1046 if n1 < 2 || n2 < 2 {
1047 return None;
1048 }
1049 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1050 return None;
1051 }
1052
1053 let n = n1 + n2;
1054 let n1f = n1 as f64;
1055 let n2f = n2 as f64;
1056 let nf = n as f64;
1057
1058 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1060 for &v in a {
1061 combined.push((v, 0)); }
1063 for &v in b {
1064 combined.push((v, 1)); }
1066 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1067
1068 let ranks = average_ranks(&combined);
1070
1071 let r1: f64 = combined
1073 .iter()
1074 .zip(ranks.iter())
1075 .filter(|((_, g), _)| *g == 0)
1076 .map(|(_, &r)| r)
1077 .sum();
1078
1079 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1081
1082 let tie_correction = compute_tie_correction(&combined);
1084
1085 let mu = n1f * n2f / 2.0;
1087 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1088
1089 if sigma_sq <= 0.0 {
1090 return None;
1091 }
1092
1093 let p_value = if tie_correction == 0.0 && n1 <= EXACT_MAX_N && n2 <= EXACT_MAX_N {
1094 let u = u1.round() as usize;
1097 let tail = u.min(n1 * n2 - u);
1098 (2.0 * mann_whitney_exact_cdf(n1, n2, tail)).min(1.0)
1099 } else {
1100 continuity_corrected_p(u1, mu, sigma_sq)
1101 };
1102
1103 Some(TestResult {
1104 statistic: u1,
1105 df: 0.0, p_value,
1107 })
1108}
1109
1110const EXACT_MAX_N: usize = 50;
1114
1115fn continuity_corrected_p(statistic: f64, mu: f64, sigma_sq: f64) -> f64 {
1118 let z = ((statistic - mu).abs() - 0.5).max(0.0) / sigma_sq.sqrt();
1119 (2.0 * special::standard_normal_sf(z)).min(1.0)
1120}
1121
1122fn mann_whitney_exact_cdf(m: usize, n: usize, u: usize) -> f64 {
1131 let max_u = m * n;
1132 let mut prev: Vec<Vec<f64>> = (0..=n).map(|_| vec![1.0]).collect(); for i in 1..=m {
1135 let mut cur: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
1136 cur.push(vec![1.0]); for j in 1..=n {
1138 let mut dist = vec![0.0; i * j + 1];
1139 let wi = i as f64 / (i + j) as f64;
1140 let wj = j as f64 / (i + j) as f64;
1141 for (k, &pk) in prev[j].iter().enumerate() {
1143 dist[k + j] += wi * pk;
1144 }
1145 for (k, &pk) in cur[j - 1].iter().enumerate() {
1147 dist[k] += wj * pk;
1148 }
1149 cur.push(dist);
1150 }
1151 prev = cur;
1152 }
1153 prev[n].iter().take(u.min(max_u) + 1).sum::<f64>().min(1.0)
1154}
1155
1156fn wilcoxon_exact_cdf(n: usize, t: usize) -> f64 {
1159 let max_t = n * (n + 1) / 2;
1160 let mut dist = vec![0.0; max_t + 1];
1161 dist[0] = 1.0;
1162 let mut reach = 0;
1163 for rank in 1..=n {
1164 reach += rank;
1165 for s in (rank..=reach).rev() {
1166 dist[s] = 0.5 * dist[s] + 0.5 * dist[s - rank];
1167 }
1168 for d in dist.iter_mut().take(rank) {
1169 *d *= 0.5;
1170 }
1171 }
1172 dist.iter().take(t.min(max_t) + 1).sum::<f64>().min(1.0)
1173}
1174
1175pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1215 if x.len() != y.len() || x.len() < 2 {
1216 return None;
1217 }
1218 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1219 return None;
1220 }
1221
1222 let diffs: Vec<f64> = x
1224 .iter()
1225 .zip(y.iter())
1226 .map(|(&a, &b)| a - b)
1227 .filter(|&d| d.abs() > 1e-300)
1228 .collect();
1229
1230 let nr = diffs.len();
1231 if nr < 2 {
1232 return None;
1233 }
1234
1235 let nf = nr as f64;
1236
1237 let mut abs_diffs: Vec<(f64, usize)> = diffs
1239 .iter()
1240 .enumerate()
1241 .map(|(i, &d)| (d.abs(), i))
1242 .collect();
1243 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1244
1245 let ranks = average_ranks(&abs_diffs);
1247
1248 let t_plus: f64 = abs_diffs
1250 .iter()
1251 .zip(ranks.iter())
1252 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1253 .map(|(_, &r)| r)
1254 .sum();
1255
1256 let tie_correction_val = compute_tie_correction(&abs_diffs);
1258
1259 let mu = nf * (nf + 1.0) / 4.0;
1261 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1262
1263 if sigma_sq <= 0.0 {
1264 return None;
1265 }
1266
1267 let p_value = if tie_correction_val == 0.0 && nr <= EXACT_MAX_N {
1268 let t = t_plus.round() as usize;
1271 let tail = t.min(nr * (nr + 1) / 2 - t);
1272 (2.0 * wilcoxon_exact_cdf(nr, tail)).min(1.0)
1273 } else {
1274 continuity_corrected_p(t_plus, mu, sigma_sq)
1275 };
1276
1277 Some(TestResult {
1278 statistic: t_plus,
1279 df: 0.0,
1280 p_value,
1281 })
1282}
1283
1284fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1287 let n = sorted.len();
1288 let mut ranks = vec![0.0; n];
1289 let mut i = 0;
1290 while i < n {
1291 let mut j = i + 1;
1292 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1293 j += 1;
1294 }
1295 let avg_rank = (i + 1 + j) as f64 / 2.0;
1297 for rank in ranks.iter_mut().take(j).skip(i) {
1298 *rank = avg_rank;
1299 }
1300 i = j;
1301 }
1302 ranks
1303}
1304
1305fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1307 let n = sorted.len();
1308 let mut correction = 0.0;
1309 let mut i = 0;
1310 while i < n {
1311 let mut j = i + 1;
1312 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1313 j += 1;
1314 }
1315 let t = (j - i) as f64;
1316 if t > 1.0 {
1317 correction += t * (t * t - 1.0);
1318 }
1319 i = j;
1320 }
1321 correction
1322}
1323
1324pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1356 let k = groups.len();
1357 if k < 2 {
1358 return None;
1359 }
1360 for g in groups {
1361 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1362 return None;
1363 }
1364 }
1365
1366 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1367 let nf = total_n as f64;
1368
1369 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1371 for (gi, g) in groups.iter().enumerate() {
1372 for &v in *g {
1373 combined.push((v, gi));
1374 }
1375 }
1376 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1377
1378 let ranks = average_ranks(&combined);
1379
1380 let mut rank_sums = vec![0.0; k];
1382 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1383 rank_sums[*gi] += r;
1384 }
1385
1386 let mean_rank = (nf + 1.0) / 2.0;
1388 let mut h = 0.0;
1389 for (gi, g) in groups.iter().enumerate() {
1390 let ni = g.len() as f64;
1391 let mean_rank_i = rank_sums[gi] / ni;
1392 h += ni * (mean_rank_i - mean_rank).powi(2);
1393 }
1394 h *= 12.0 / (nf * (nf + 1.0));
1395
1396 let tie_corr = compute_tie_correction(&combined);
1398 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1399 if denom > 1e-15 {
1400 h /= denom;
1401 }
1402
1403 let df = (k - 1) as f64;
1404 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1405
1406 Some(TestResult {
1407 statistic: h,
1408 df,
1409 p_value,
1410 })
1411}
1412
1413pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1449 let k = groups.len();
1450 if k < 2 {
1451 return None;
1452 }
1453 for g in groups {
1454 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1455 return None;
1456 }
1457 }
1458
1459 let z_groups: Vec<Vec<f64>> = groups
1461 .iter()
1462 .map(|g| {
1463 let median = stats::median(g).unwrap_or(0.0);
1464 g.iter().map(|&x| (x - median).abs()).collect()
1465 })
1466 .collect();
1467
1468 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1470 let anova = one_way_anova(&z_refs)?;
1471
1472 Some(TestResult {
1473 statistic: anova.f_statistic,
1474 df: anova.df_between as f64,
1475 p_value: anova.p_value,
1476 })
1477}
1478
1479pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1492 if p_values.is_empty() || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1493 return None;
1494 }
1495 let m = p_values.len() as f64;
1496 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1497}
1498
1499pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1519 let m = p_values.len();
1520 if m == 0 || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1521 return None;
1522 }
1523
1524 let mut indices: Vec<usize> = (0..m).collect();
1526 indices.sort_by(|&a, &b| {
1527 p_values[a]
1528 .partial_cmp(&p_values[b])
1529 .unwrap_or(std::cmp::Ordering::Equal)
1530 });
1531
1532 let mf = m as f64;
1533 let mut adjusted = vec![0.0; m];
1534
1535 let mut cummin = f64::INFINITY;
1537 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1538 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1540 cummin = cummin.min(adj);
1541 adjusted[orig_idx] = cummin;
1542 }
1543
1544 Some(adjusted)
1545}
1546
1547pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1584 let k = groups.len();
1585 if k < 2 {
1586 return None;
1587 }
1588
1589 let mut sizes = Vec::with_capacity(k);
1590 let mut vars = Vec::with_capacity(k);
1591 let mut n_total: usize = 0;
1592
1593 for g in groups {
1594 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1595 return None;
1596 }
1597 let n = g.len();
1598 let v = stats::variance(g)?;
1599 if v <= 0.0 {
1600 return None; }
1602 sizes.push(n);
1603 vars.push(v);
1604 n_total += n;
1605 }
1606
1607 let nk = n_total - k; if nk == 0 {
1609 return None;
1610 }
1611 let nk_f = nk as f64;
1612
1613 let s2_pooled: f64 = sizes
1615 .iter()
1616 .zip(vars.iter())
1617 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1618 .sum::<f64>()
1619 / nk_f;
1620
1621 if s2_pooled <= 0.0 {
1622 return None;
1623 }
1624
1625 let num = nk_f * s2_pooled.ln()
1627 - sizes
1628 .iter()
1629 .zip(vars.iter())
1630 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1631 .sum::<f64>();
1632
1633 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1635 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1636
1637 let statistic = num / c;
1638 let df = (k - 1) as f64;
1639 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1640
1641 Some(TestResult {
1642 statistic,
1643 df,
1644 p_value,
1645 })
1646}
1647
1648pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1693 let row1 = a + b;
1694 let row2 = c + d;
1695 let col1 = a + c;
1696 let col2 = b + d;
1697 let n = a + b + c + d;
1698
1699 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1701 return None;
1702 }
1703
1704 let log_prob = |a_i: u64| -> f64 {
1706 let b_i = row1 - a_i;
1707 let c_i = col1 - a_i;
1708 let d_i = row2 - c_i;
1709 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1710 - ln_factorial(a_i)
1711 - ln_factorial(b_i)
1712 - ln_factorial(c_i)
1713 - ln_factorial(d_i)
1714 - ln_factorial(n)
1715 };
1716
1717 let a_min = col1.saturating_sub(row2);
1719 let a_max = row1.min(col1);
1720
1721 let log_p_obs = log_prob(a);
1722
1723 let mut p_value = 0.0;
1725 for a_i in a_min..=a_max {
1726 let lp = log_prob(a_i);
1727 if lp <= log_p_obs + 1e-10 {
1729 p_value += lp.exp();
1730 }
1731 }
1732
1733 let p_value = p_value.min(1.0);
1735
1736 let odds_ratio = if b > 0 && c > 0 {
1738 (a as f64 * d as f64) / (b as f64 * c as f64)
1739 } else {
1740 f64::INFINITY
1741 };
1742
1743 Some(TestResult {
1744 statistic: odds_ratio,
1745 df: 1.0,
1746 p_value,
1747 })
1748}
1749
1750#[derive(Debug, Clone, Copy)]
1756pub struct MannKendallResult {
1757 pub s_statistic: i64,
1759 pub variance: f64,
1761 pub z_statistic: f64,
1763 pub p_value: f64,
1765 pub kendall_tau: f64,
1767 pub sen_slope: f64,
1769}
1770
1771pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1809 let n = data.len();
1810 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1811 return None;
1812 }
1813
1814 let mut s: i64 = 0;
1816 for i in 0..n - 1 {
1817 for j in (i + 1)..n {
1818 let diff = data[j] - data[i];
1819 if diff > 0.0 {
1820 s += 1;
1821 } else if diff < 0.0 {
1822 s -= 1;
1823 }
1824 }
1825 }
1826
1827 let mut sorted: Vec<f64> = data.to_vec();
1829 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1830
1831 let mut tie_correction: f64 = 0.0;
1832 let mut current_count: usize = 1;
1833 for i in 1..sorted.len() {
1834 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1835 current_count += 1;
1836 } else {
1837 if current_count > 1 {
1838 let t = current_count as f64;
1839 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1840 }
1841 current_count = 1;
1842 }
1843 }
1844 if current_count > 1 {
1845 let t = current_count as f64;
1846 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1847 }
1848
1849 let nf = n as f64;
1851 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1852
1853 if variance < 1e-300 {
1854 return None; }
1856
1857 let sigma = variance.sqrt();
1859 let z_statistic = if s > 0 {
1860 (s as f64 - 1.0) / sigma
1861 } else if s < 0 {
1862 (s as f64 + 1.0) / sigma
1863 } else {
1864 0.0
1865 };
1866
1867 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1869 let p_value = p_value.clamp(0.0, 1.0);
1870
1871 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1873
1874 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1876 for i in 0..n - 1 {
1877 for j in (i + 1)..n {
1878 let dx = (j - i) as f64;
1879 slopes.push((data[j] - data[i]) / dx);
1880 }
1881 }
1882 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1883 let m = slopes.len();
1884 let sen_slope = if m % 2 == 0 {
1885 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1886 } else {
1887 slopes[m / 2]
1888 };
1889
1890 Some(MannKendallResult {
1891 s_statistic: s,
1892 variance,
1893 z_statistic,
1894 p_value,
1895 kendall_tau,
1896 sen_slope,
1897 })
1898}
1899
1900fn ln_factorial(n: u64) -> f64 {
1902 if n <= 1 {
1903 return 0.0;
1904 }
1905 special::ln_gamma(n as f64 + 1.0)
1907}
1908
1909#[derive(Debug, Clone)]
1915pub struct AdfResult {
1916 pub statistic: f64,
1918 pub n_lags: usize,
1920 pub n_obs: usize,
1922 pub critical_values: [f64; 3],
1924 pub rejected: [bool; 3],
1926}
1927
1928#[derive(Debug, Clone, Copy)]
1930pub enum AdfModel {
1931 None,
1933 Constant,
1935 ConstantTrend,
1937}
1938
1939pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1975 let n = data.len();
1976 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1977 return None;
1978 }
1979
1980 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1982
1983 let best_lag = match max_lags {
1985 Some(p) => p, None => {
1987 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1989 let p_max = schwert.min(n / 3);
1990 select_adf_lag(data, &dy, model, p_max)
1992 }
1993 };
1994
1995 adf_ols(data, &dy, model, best_lag)
1997}
1998
1999fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
2001 let mut best_aic = f64::INFINITY;
2002 let mut best_p = 0;
2003
2004 for p in 0..=p_max {
2005 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
2006 if aic < best_aic {
2007 best_aic = aic;
2008 best_p = p;
2009 }
2010 }
2011 }
2012
2013 best_p
2014}
2015
2016#[allow(clippy::type_complexity)]
2020fn adf_build_matrix(
2021 data: &[f64],
2022 dy: &[f64],
2023 model: AdfModel,
2024 p: usize,
2025) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2026 let start = p + 1;
2027 if start >= dy.len() || dy.len() - start < 5 {
2028 return None;
2029 }
2030 let m = dy.len() - start;
2031
2032 let y_dep: Vec<f64> = dy[start..].to_vec();
2033
2034 let has_intercept = !matches!(model, AdfModel::None);
2036 let has_trend = matches!(model, AdfModel::ConstantTrend);
2037 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2038
2039 let mut x_data = Vec::with_capacity(m * ncols);
2041 let mut gamma_col = 0;
2042
2043 for i in 0..m {
2044 let t = start + i;
2045 if has_intercept {
2046 x_data.push(1.0); gamma_col = 1;
2048 }
2049 x_data.push(data[t]); if has_trend {
2051 x_data.push((t + 1) as f64); }
2053 for lag in 1..=p {
2054 x_data.push(dy[t - lag]);
2055 }
2056 }
2057
2058 Some((x_data, y_dep, m, ncols, gamma_col))
2059}
2060
2061fn adf_ols_core(
2065 x_data: &[f64],
2066 y: &[f64],
2067 m: usize,
2068 ncols: usize,
2069 gamma_col: usize,
2070) -> Option<(f64, f64, usize)> {
2071 let mut xtx = vec![0.0_f64; ncols * ncols];
2073 for i in 0..m {
2074 let row = &x_data[i * ncols..(i + 1) * ncols];
2075 for j in 0..ncols {
2076 for k in j..ncols {
2077 xtx[j * ncols + k] += row[j] * row[k];
2078 }
2079 }
2080 }
2081 for j in 0..ncols {
2083 for k in (j + 1)..ncols {
2084 xtx[k * ncols + j] = xtx[j * ncols + k];
2085 }
2086 }
2087
2088 let mut xty = vec![0.0_f64; ncols];
2090 for i in 0..m {
2091 let row = &x_data[i * ncols..(i + 1) * ncols];
2092 for j in 0..ncols {
2093 xty[j] += row[j] * y[i];
2094 }
2095 }
2096
2097 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2099 for i in 0..ncols {
2100 for j in 0..ncols {
2101 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2102 }
2103 augmented[i * (ncols + 1) + ncols] = xty[i];
2104 }
2105
2106 for col in 0..ncols {
2107 let mut max_row = col;
2109 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2110 for row in (col + 1)..ncols {
2111 let val = augmented[row * (ncols + 1) + col].abs();
2112 if val > max_val {
2113 max_val = val;
2114 max_row = row;
2115 }
2116 }
2117 if max_val < 1e-15 {
2118 return None; }
2120 if max_row != col {
2121 for j in 0..=ncols {
2122 let a = col * (ncols + 1) + j;
2123 let b = max_row * (ncols + 1) + j;
2124 augmented.swap(a, b);
2125 }
2126 }
2127
2128 let pivot = augmented[col * (ncols + 1) + col];
2129 for row in (col + 1)..ncols {
2130 let factor = augmented[row * (ncols + 1) + col] / pivot;
2131 for j in col..=ncols {
2132 let above = augmented[col * (ncols + 1) + j];
2133 augmented[row * (ncols + 1) + j] -= factor * above;
2134 }
2135 }
2136 }
2137
2138 let mut beta = vec![0.0_f64; ncols];
2140 for i in (0..ncols).rev() {
2141 let mut sum = augmented[i * (ncols + 1) + ncols];
2142 for j in (i + 1)..ncols {
2143 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2144 }
2145 beta[i] = sum / augmented[i * (ncols + 1) + i];
2146 }
2147
2148 let mut rss = 0.0;
2150 for i in 0..m {
2151 let row = &x_data[i * ncols..(i + 1) * ncols];
2152 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2153 let resid = y[i] - y_hat;
2154 rss += resid * resid;
2155 }
2156
2157 let df = m - ncols;
2159 if df == 0 {
2160 return None;
2161 }
2162 let mse = rss / df as f64;
2163
2164 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2167 for j in 0..ncols {
2168 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2169 }
2170 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2171 }
2172
2173 for col in 0..ncols {
2175 let mut max_row = col;
2176 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2177 for row in (col + 1)..ncols {
2178 let val = xtx_aug[row * 2 * ncols + col].abs();
2179 if val > max_val {
2180 max_val = val;
2181 max_row = row;
2182 }
2183 }
2184 if max_val < 1e-15 {
2185 return None;
2186 }
2187 if max_row != col {
2188 for j in 0..(2 * ncols) {
2189 let a = col * 2 * ncols + j;
2190 let b = max_row * 2 * ncols + j;
2191 xtx_aug.swap(a, b);
2192 }
2193 }
2194
2195 let pivot = xtx_aug[col * 2 * ncols + col];
2196 for j in 0..(2 * ncols) {
2197 xtx_aug[col * 2 * ncols + j] /= pivot;
2198 }
2199 for row in 0..ncols {
2200 if row == col {
2201 continue;
2202 }
2203 let factor = xtx_aug[row * 2 * ncols + col];
2204 for j in 0..(2 * ncols) {
2205 let above = xtx_aug[col * 2 * ncols + j];
2206 xtx_aug[row * 2 * ncols + j] -= factor * above;
2207 }
2208 }
2209 }
2210
2211 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2213 if var_gamma <= 0.0 {
2214 return None;
2215 }
2216 let se_gamma = var_gamma.sqrt();
2217 let t_gamma = beta[gamma_col] / se_gamma;
2218
2219 Some((t_gamma, rss, ncols))
2220}
2221
2222fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2224 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2225 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2226 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2227 Some((aic, m))
2228}
2229
2230fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2232 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2233 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2234
2235 let critical_values = adf_critical_values(model, m);
2236
2237 let rejected = [
2238 gamma_t <= critical_values[0],
2239 gamma_t <= critical_values[1],
2240 gamma_t <= critical_values[2],
2241 ];
2242
2243 Some(AdfResult {
2244 statistic: gamma_t,
2245 n_lags: p,
2246 n_obs: m,
2247 critical_values,
2248 rejected,
2249 })
2250}
2251
2252fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2256 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2261 AdfModel::None => (
2262 [-2.5658, -1.9393, -1.6156],
2263 [-1.960, -0.398, -0.181],
2264 [-10.04, 0.0, 0.0],
2265 ),
2266 AdfModel::Constant => (
2267 [-3.4336, -2.8621, -2.5671],
2268 [-5.999, -2.738, -1.438],
2269 [-29.25, -8.36, -4.48],
2270 ),
2271 AdfModel::ConstantTrend => (
2272 [-3.9638, -3.4126, -3.1279],
2273 [-8.353, -4.039, -2.418],
2274 [-47.44, -17.83, -7.58],
2275 ),
2276 };
2277
2278 let nf = n as f64;
2279 let inv_n = 1.0 / nf;
2280 let inv_n2 = inv_n * inv_n;
2281
2282 [
2283 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2284 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2285 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2286 ]
2287}
2288
2289#[cfg(test)]
2290mod tests {
2291 use super::*;
2292
2293 #[test]
2294 fn corrections_refuse_what_is_not_a_probability() {
2295 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2296 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2297 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2298 }
2299
2300 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2306 let mut a = seed;
2307 move || {
2308 a = a.wrapping_add(0x6d2b_79f5);
2309 let mut t = a;
2310 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2311 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2312 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2313 }
2314 }
2315
2316 #[test]
2322 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2323 let mut rng = mulberry32(0x5350_4331);
2324 let full: Vec<f64> = (0..20_000)
2325 .map(|_| {
2326 let u = rng();
2327 -(1.0 - u).ln() })
2329 .collect();
2330
2331 let mut prev_a2 = 0.0_f64;
2332 let mut prev_p = f64::INFINITY;
2333 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2334 let r = anderson_darling_normality(&full[..n]).expect("computes");
2335 assert!(
2337 r.statistic_modified > prev_a2,
2338 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2339 r.statistic_modified
2340 );
2341 assert!(
2343 r.p_value < 0.5,
2344 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2345 r.statistic_modified,
2346 r.p_value
2347 );
2348 assert!(
2350 r.p_value <= prev_p + 1e-12,
2351 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2352 r.p_value
2353 );
2354 prev_a2 = r.statistic_modified;
2355 prev_p = r.p_value;
2356 }
2357 }
2358
2359 #[test]
2362 fn ad_both_functions_immune_to_overflow() {
2363 let mut rng = mulberry32(0x0bad_c0de);
2364 let data: Vec<f64> = (0..9000).map(|_| -(1.0 - rng()).ln()).collect();
2365 let a = anderson_darling_test(&data).expect("computes");
2366 let b = anderson_darling_normality(&data).expect("computes");
2367 assert!(
2368 a.statistic_star > 300.0,
2369 "expected large A*², got {}",
2370 a.statistic_star
2371 );
2372 assert!(
2373 a.p_value < 0.5,
2374 "test() overflowed toward normal: p={}",
2375 a.p_value
2376 );
2377 assert!(
2378 b.p_value < 0.5,
2379 "normality() overflowed toward normal: p={}",
2380 b.p_value
2381 );
2382 }
2383
2384 #[test]
2389 fn one_sample_null_true() {
2390 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2391 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2392 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2393 }
2394
2395 #[test]
2396 fn one_sample_null_false() {
2397 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2398 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2399 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2400 }
2401
2402 #[test]
2403 fn one_sample_edge_cases() {
2404 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());
2407 }
2408
2409 #[test]
2414 fn two_sample_same_mean() {
2415 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2416 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2417 let r = two_sample_t_test(&a, &b).expect("should compute");
2418 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2419 }
2420
2421 #[test]
2422 fn two_sample_different_means() {
2423 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2424 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2425 let r = two_sample_t_test(&a, &b).expect("should compute");
2426 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2427 }
2428
2429 #[test]
2430 fn two_sample_different_sizes() {
2431 let a = [1.0, 2.0, 3.0];
2432 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2433 let r = two_sample_t_test(&a, &b).expect("should compute");
2434 assert!(r.p_value < 0.05);
2435 }
2436
2437 #[test]
2438 fn two_sample_edge_cases() {
2439 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2440 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2441 }
2442
2443 #[test]
2448 fn paired_no_difference() {
2449 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2450 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2451 let r = paired_t_test(&x, &y).expect("should compute");
2452 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2453 }
2454
2455 #[test]
2456 fn paired_significant_difference() {
2457 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2459 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2460 let r = paired_t_test(&before, &after).expect("should compute");
2461 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2462 assert!(r.statistic < 0.0); }
2464
2465 #[test]
2466 fn paired_edge_cases() {
2467 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2470
2471 #[test]
2476 fn anova_same_means() {
2477 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2478 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2479 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2480 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2481 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2482 }
2483
2484 #[test]
2485 fn anova_different_means() {
2486 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2487 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2488 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2489 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2490 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2491 assert!(r.df_between == 2);
2492 assert!(r.df_within == 12);
2493 }
2494
2495 #[test]
2496 fn anova_ss_decomposition() {
2497 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2498 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2499 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2500 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2502 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2503 assert!(
2504 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2505 "SS decomposition: {ss_total} vs {} + {}",
2506 r.ss_between,
2507 r.ss_within
2508 );
2509 }
2510
2511 #[test]
2512 fn anova_edge_cases() {
2513 let g1 = [1.0, 2.0, 3.0];
2514 assert!(one_way_anova(&[&g1]).is_none()); }
2516
2517 #[test]
2522 fn chi2_gof_uniform() {
2523 let observed = [25.0, 25.0, 25.0, 25.0];
2525 let expected = [25.0, 25.0, 25.0, 25.0];
2526 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2527 assert!((r.statistic).abs() < 1e-15);
2528 assert!((r.p_value - 1.0).abs() < 0.01);
2529 }
2530
2531 #[test]
2532 fn chi2_gof_significant() {
2533 let observed = [90.0, 10.0];
2534 let expected = [50.0, 50.0];
2535 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2536 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2537 }
2538
2539 #[test]
2540 fn chi2_gof_edge_cases() {
2541 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()); }
2545
2546 #[test]
2551 fn chi2_independence_significant() {
2552 let table = [30.0, 10.0, 10.0, 50.0];
2554 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2555 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2556 assert!((r.df - 1.0).abs() < 1e-10);
2557 }
2558
2559 #[test]
2560 fn chi2_independence_not_significant() {
2561 let table = [25.0, 25.0, 25.0, 25.0];
2563 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2564 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2565 }
2566
2567 #[test]
2568 fn chi2_independence_3x3() {
2569 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2570 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2571 assert!((r.df - 4.0).abs() < 1e-10);
2572 assert!(r.p_value < 0.05);
2573 }
2574
2575 #[test]
2576 fn chi2_independence_edge_cases() {
2577 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()); }
2581
2582 #[test]
2587 fn jb_normal_data() {
2588 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2590 let r = jarque_bera_test(&data).expect("should compute");
2591 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2592 }
2593
2594 #[test]
2595 fn jb_skewed_data() {
2596 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2598 let r = jarque_bera_test(&data).expect("should compute");
2599 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2600 }
2601
2602 #[test]
2609 fn jb_is_the_moment_form_the_paper_defines() {
2610 let data = [
2611 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,
2612 3.6, 2.8, 3.2,
2613 ];
2614 let r = jarque_bera_test(&data).expect("20 finite points");
2615 assert!(
2616 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2617 "JB = {}",
2618 r.statistic
2619 );
2620
2621 let n = data.len() as f64;
2624 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2625 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2626 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2627 assert!(
2628 adjusted > r.statistic * 1.3,
2629 "the two forms differ by more than a third here: {adjusted} vs {}",
2630 r.statistic
2631 );
2632 }
2633
2634 #[test]
2638 fn jb_p_value_keeps_its_digits_in_the_tail() {
2639 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2642 data.push(60.0);
2643 let r = jarque_bera_test(&data).expect("20 finite points");
2644 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2645 assert!(
2646 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2647 "this sample is only interesting because the complement underflows"
2648 );
2649 assert!(
2650 r.p_value > 0.0,
2651 "the complement returns 0 here; this must not"
2652 );
2653 assert!(
2654 (r.p_value - 6.702e-21).abs() < 1e-23,
2655 "p = {} (expected about 6.7e-21)",
2656 r.p_value
2657 );
2658 assert!(
2659 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2660 "p = {} vs exp(-JB/2) = {}",
2661 r.p_value,
2662 (-r.statistic / 2.0).exp()
2663 );
2664 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])
2667 .expect("10 finite points");
2668 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2669 assert!(
2670 (small.p_value - via_cdf).abs() < 1e-12,
2671 "{} vs {via_cdf}",
2672 small.p_value
2673 );
2674 }
2675
2676 #[test]
2677 fn jb_edge_cases() {
2678 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2680
2681 #[test]
2686 fn ad_normal_data() {
2687 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2688 let r = anderson_darling_test(&data).expect("should compute");
2689 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2690 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2691 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2692 }
2693
2694 #[test]
2695 fn ad_skewed_data() {
2696 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];
2698 let r = anderson_darling_test(&data).expect("should compute");
2699 assert!(
2700 r.p_value < 0.05,
2701 "p = {} (should reject normality)",
2702 r.p_value
2703 );
2704 }
2705
2706 #[test]
2707 fn ad_bimodal_data() {
2708 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2710 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2711 let r = anderson_darling_test(&data).expect("should compute");
2712 assert!(
2713 r.p_value < 0.01,
2714 "p = {} (bimodal should reject normality)",
2715 r.p_value
2716 );
2717 }
2718
2719 #[test]
2720 fn ad_large_normal_sample() {
2721 let n = 100;
2722 let data: Vec<f64> = (1..=n)
2723 .map(|i| {
2724 let p = (i as f64 - 0.5) / n as f64;
2725 special::inverse_normal_cdf(p)
2726 })
2727 .collect();
2728 let r = anderson_darling_test(&data).expect("should compute");
2729 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2730 }
2731
2732 #[test]
2733 fn ad_edge_cases() {
2734 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());
2737 }
2738
2739 #[test]
2740 fn ad_p_value_ranges() {
2741 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2746 let r = anderson_darling_test(&near_normal).expect("should compute");
2747 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2748
2749 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];
2751 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2752 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2753 }
2754
2755 #[test]
2760 fn ad_normal_data_large_p() {
2761 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2763 let r = anderson_darling_normality(&data).unwrap();
2764 assert!(r.p_value > 0.05, "p={}", r.p_value);
2765 }
2766
2767 #[test]
2768 fn ad_exponential_data_small_p() {
2769 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2771 let r = anderson_darling_normality(&data).unwrap();
2772 assert!(r.p_value < 0.05, "p={}", r.p_value);
2773 }
2774
2775 #[test]
2776 fn ad_statistic_non_negative() {
2777 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2778 let r = anderson_darling_normality(&data).unwrap();
2779 assert!(r.statistic >= 0.0);
2780 assert!(r.statistic_modified >= r.statistic);
2781 }
2782
2783 #[test]
2784 fn ad_p_value_in_range() {
2785 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2786 let r = anderson_darling_normality(&data).unwrap();
2787 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2788 }
2789
2790 #[test]
2791 fn ad_insufficient_data() {
2792 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2794
2795 #[test]
2796 fn ad_degenerate_data() {
2797 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2799 }
2800
2801 #[test]
2802 fn ad_modified_statistic_formula() {
2803 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2805 let r = anderson_darling_normality(&data).unwrap();
2806 assert!(r.statistic_modified > r.statistic - 1e-10);
2807 }
2808
2809 #[test]
2814 fn ad_correction_factor_formula() {
2815 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2816 let n = data.len() as f64;
2817 let r = anderson_darling_normality(&data).unwrap();
2818
2819 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2821 assert!(
2822 (r.statistic_modified - expected_star).abs() < 1e-12,
2823 "A²* = {}, expected = {}",
2824 r.statistic_modified,
2825 expected_star
2826 );
2827
2828 assert!(
2830 r.statistic >= 0.0,
2831 "A² = {} must be non-negative",
2832 r.statistic
2833 );
2834 }
2835
2836 #[test]
2842 fn ad_formula_structure_symmetric_vs_skewed() {
2843 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2845 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2847
2848 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2849 let r_skew = anderson_darling_normality(&skewed).unwrap();
2850
2851 assert!(
2852 r_sym.statistic < r_skew.statistic,
2853 "Symmetric A²={} should be less than skewed A²={}",
2854 r_sym.statistic,
2855 r_skew.statistic
2856 );
2857 }
2858
2859 #[test]
2864 fn ad_pvalue_invariants() {
2865 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2867 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2868 assert!(
2869 r_normal.p_value > 0.05,
2870 "Normal data: p = {} (expected > 0.05)",
2871 r_normal.p_value
2872 );
2873
2874 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2876 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2877 assert!(
2878 r_exp.p_value < 0.05,
2879 "Exponential data: p = {} (expected < 0.05)",
2880 r_exp.p_value
2881 );
2882 }
2883
2884 #[test]
2889 fn sw_normal_data() {
2890 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2892 let r = shapiro_wilk_test(&data).expect("should compute");
2893 assert!(r.w > 0.9, "W = {}", r.w);
2894 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2895 }
2896
2897 #[test]
2898 fn sw_bimodal_data() {
2899 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2901 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2902 let r = shapiro_wilk_test(&data).expect("should compute");
2903 assert!(
2904 r.p_value < 0.01,
2905 "p = {} (bimodal should reject normality)",
2906 r.p_value
2907 );
2908 }
2909
2910 #[test]
2911 fn sw_n3() {
2912 let data = [1.0, 2.0, 3.0];
2913 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2914 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2915 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2916 }
2917
2918 #[test]
2919 fn sw_n4() {
2920 let data = [1.0, 2.0, 3.0, 4.0];
2921 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2922 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2923 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2924 }
2925
2926 #[test]
2927 fn sw_n5() {
2928 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2929 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2930 assert!(r.w > 0.9, "W = {}", r.w);
2931 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2932 }
2933
2934 #[test]
2935 fn sw_skewed_data() {
2936 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];
2938 let r = shapiro_wilk_test(&data).expect("should compute");
2939 assert!(
2940 r.p_value < 0.05,
2941 "p = {} (skewed data should reject normality)",
2942 r.p_value
2943 );
2944 }
2945
2946 #[test]
2947 fn sw_large_normal_sample() {
2948 let n = 100;
2951 let data: Vec<f64> = (1..=n)
2952 .map(|i| {
2953 let p = (i as f64 - 0.5) / n as f64;
2954 special::inverse_normal_cdf(p)
2955 })
2956 .collect();
2957 let r = shapiro_wilk_test(&data).expect("should compute");
2958 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2959 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2960 }
2961
2962 #[test]
2963 fn sw_w_bounded() {
2964 let datasets: Vec<Vec<f64>> = vec![
2966 vec![1.0, 2.0, 3.0],
2967 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2968 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2969 (0..20).map(|i| (i as f64).powi(2)).collect(),
2970 ];
2971 for (idx, data) in datasets.iter().enumerate() {
2972 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2973 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2974 assert!(
2975 r.p_value >= 0.0 && r.p_value <= 1.0,
2976 "dataset {idx}: p = {}",
2977 r.p_value
2978 );
2979 }
2980 }
2981
2982 #[test]
2983 fn sw_edge_cases() {
2984 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()); }
2990
2991 #[test]
2992 fn sw_n5001_rejected() {
2993 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2994 assert!(shapiro_wilk_test(&data).is_none()); }
2996
2997 #[test]
3002 fn mw_clearly_different() {
3003 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
3004 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
3005 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3006 assert!(r.p_value < 0.05, "p = {}", r.p_value);
3007 }
3008
3009 #[test]
3010 fn mw_same_distribution() {
3011 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3012 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3013 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3014 assert!(
3015 r.p_value > 0.3,
3016 "p = {} (interleaved, same dist)",
3017 r.p_value
3018 );
3019 }
3020
3021 #[test]
3022 fn mw_with_ties() {
3023 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3024 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3025 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3026 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3027 }
3028
3029 #[test]
3030 fn mw_different_sizes() {
3031 let a = [1.0, 2.0, 3.0];
3032 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3033 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3034 assert!(r.p_value < 0.05);
3035 }
3036
3037 #[test]
3038 fn mw_edge_cases() {
3039 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());
3042 }
3043
3044 #[test]
3049 fn wilcoxon_significant_increase() {
3050 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3051 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3052 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3053 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3054 }
3055
3056 #[test]
3057 fn wilcoxon_no_difference() {
3058 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3059 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3060 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3061 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3062 }
3063
3064 #[test]
3065 fn wilcoxon_with_ties() {
3066 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3068 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3070 assert!(r.statistic > 0.0);
3072 }
3073
3074 #[test]
3075 fn wilcoxon_with_zero_diffs() {
3076 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3078 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3080 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3081 }
3082
3083 #[test]
3084 fn wilcoxon_edge_cases() {
3085 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());
3089 }
3090
3091 #[test]
3096 fn mann_whitney_small_samples_use_the_exact_distribution() {
3097 let cases = [
3099 (3, 3, 0, 0.1000, 0.0495),
3100 (4, 4, 0, 0.0286, 0.0209),
3101 (4, 4, 1, 0.0571, 0.0433),
3102 (5, 5, 2, 0.0317, 0.0283),
3103 (8, 8, 13, 0.0499, 0.0460),
3104 (10, 10, 23, 0.0433, 0.0413),
3105 ];
3106 for (n1, n2, u, exact, old) in cases {
3107 let (a, b) = separated_samples(n1, n2, u);
3108 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3109 assert_eq!(r.statistic, u as f64);
3110 assert!(
3111 (r.p_value - exact).abs() < 1e-4,
3112 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3113 r.p_value
3114 );
3115 }
3116 }
3117
3118 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3122 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3123 let mut remaining = u;
3124 for k in (0..n1).rev() {
3125 let room = n2.min(remaining);
3126 ranks_a[k] += room;
3127 remaining -= room;
3128 if remaining == 0 {
3129 break;
3130 }
3131 }
3132 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3133 (
3134 ranks_a.iter().map(|&r| r as f64).collect(),
3135 ranks_b.iter().map(|&r| r as f64).collect(),
3136 )
3137 }
3138
3139 #[test]
3141 fn wilcoxon_small_samples_use_the_exact_distribution() {
3142 let cases = [
3144 (5, 0, 0.0625),
3145 (5, 1, 0.125),
3146 (6, 2, 0.09375),
3147 (8, 5, 0.078125),
3148 (10, 8, 0.048828125),
3149 (12, 14, 0.0522460938),
3150 ];
3151 for (n, t_plus, exact) in cases {
3152 let positive = ranks_summing_to(n, t_plus);
3154 let x: Vec<f64> = (1..=n)
3155 .map(|r| {
3156 if positive.contains(&r) {
3157 r as f64
3158 } else {
3159 -(r as f64)
3160 }
3161 })
3162 .collect();
3163 let y = vec![0.0; n];
3164 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3165 assert_eq!(r.statistic, t_plus as f64);
3166 assert!(
3167 (r.p_value - exact).abs() < 1e-9,
3168 "n={n} T+={t_plus}: p={} want {exact}",
3169 r.p_value
3170 );
3171 }
3172 }
3173
3174 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3176 let mut out = Vec::new();
3177 let mut left = target;
3178 for r in (1..=n).rev() {
3179 if r <= left {
3180 out.push(r);
3181 left -= r;
3182 }
3183 }
3184 assert_eq!(left, 0);
3185 out
3186 }
3187
3188 #[test]
3191 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3192 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3193 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3194 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3195 let (n1, n2) = (60.0, 60.0);
3196 let mu = n1 * n2 / 2.0;
3197 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3198 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3199 let want = 2.0 * special::standard_normal_sf(z);
3200 assert!((r.p_value - want).abs() < 1e-12);
3201
3202 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3204 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3205 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3206 let mu = 1250.0;
3207 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3208 let u = mann_whitney_u_test(&a, &b)
3209 .expect("valid samples")
3210 .statistic;
3211 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3212 assert!(
3213 (exact - approx).abs() < 1e-3,
3214 "exact {exact} vs approx {approx}"
3215 );
3216 }
3217
3218 #[test]
3223 fn kw_clearly_different() {
3224 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3225 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3226 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3227 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3228 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3229 assert!((r.df - 2.0).abs() < 1e-10);
3230 }
3231
3232 #[test]
3233 fn kw_same_distribution() {
3234 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3235 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3236 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3237 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3238 }
3239
3240 #[test]
3241 fn kw_with_ties() {
3242 let g1 = [1.0, 2.0, 2.0, 3.0];
3243 let g2 = [3.0, 4.0, 4.0, 5.0];
3244 let g3 = [5.0, 6.0, 6.0, 7.0];
3245 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3246 assert!(r.statistic > 0.0);
3247 }
3248
3249 #[test]
3250 fn kw_edge_cases() {
3251 let g1 = [1.0, 2.0, 3.0];
3252 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3254
3255 #[test]
3260 fn levene_equal_variance() {
3261 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3262 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3263 let r = levene_test(&[&g1, &g2]).expect("should compute");
3264 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3266 }
3267
3268 #[test]
3269 fn levene_unequal_variance() {
3270 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");
3273 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3274 }
3275
3276 #[test]
3277 fn levene_three_groups() {
3278 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3279 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3280 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3281 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3282 assert!(r.df >= 2.0);
3283 }
3284
3285 #[test]
3286 fn levene_edge_cases() {
3287 let g1 = [1.0, 2.0, 3.0];
3288 assert!(levene_test(&[&g1]).is_none()); }
3290
3291 #[test]
3296 fn bonferroni_basic() {
3297 let ps = [0.01, 0.04, 0.03, 0.005];
3298 let adj = bonferroni_correction(&ps).expect("should compute");
3299 assert!((adj[0] - 0.04).abs() < 1e-10);
3300 assert!((adj[1] - 0.16).abs() < 1e-10);
3301 assert!((adj[2] - 0.12).abs() < 1e-10);
3302 assert!((adj[3] - 0.02).abs() < 1e-10);
3303 }
3304
3305 #[test]
3306 fn bonferroni_capped_at_one() {
3307 let ps = [0.5, 0.6];
3308 let adj = bonferroni_correction(&ps).expect("should compute");
3309 assert!((adj[0] - 1.0).abs() < 1e-10);
3310 assert!((adj[1] - 1.0).abs() < 1e-10);
3311 }
3312
3313 #[test]
3314 fn bh_basic() {
3315 let ps = [0.01, 0.04, 0.03, 0.005];
3316 let adj = benjamini_hochberg(&ps).expect("should compute");
3317 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3319 assert!(
3320 adjusted >= orig - 1e-15,
3321 "adj[{i}] = {adjusted} < original {orig}"
3322 );
3323 }
3324 }
3327
3328 #[test]
3329 fn bh_all_significant() {
3330 let ps = [0.001, 0.002, 0.003];
3331 let adj = benjamini_hochberg(&ps).expect("should compute");
3332 for &a in &adj {
3333 assert!(a < 0.05);
3334 }
3335 }
3336
3337 #[test]
3338 fn correction_edge_cases() {
3339 assert!(bonferroni_correction(&[]).is_none());
3340 assert!(benjamini_hochberg(&[]).is_none());
3341 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3342 }
3343
3344 #[test]
3349 fn bartlett_equal_variances() {
3350 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3351 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3352 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3353 assert!(
3354 r.p_value > 0.9,
3355 "equal variance → p high, got {}",
3356 r.p_value
3357 );
3358 }
3359
3360 #[test]
3361 fn bartlett_unequal_variances() {
3362 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");
3365 assert!(
3366 r.p_value < 0.01,
3367 "very different variances → p < 0.01, got {}",
3368 r.p_value
3369 );
3370 assert!((r.df - 1.0).abs() < 1e-10); }
3372
3373 #[test]
3374 fn bartlett_three_groups() {
3375 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3376 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3377 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3379 assert!(r.p_value < 0.05, "one group with high variance");
3380 assert!((r.df - 2.0).abs() < 1e-10); }
3382
3383 #[test]
3384 fn bartlett_edge_cases() {
3385 let g1 = [1.0, 2.0, 3.0];
3386 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3390
3391 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3393 }
3394
3395 #[test]
3400 fn fisher_tea_tasting() {
3401 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3403 assert!(
3405 (r.p_value - 0.4857).abs() < 0.01,
3406 "p ≈ 0.4857, got {}",
3407 r.p_value
3408 );
3409 }
3410
3411 #[test]
3412 fn fisher_significant() {
3413 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3415 assert!(r.p_value < 0.001, "perfect association → p very small");
3416 }
3417
3418 #[test]
3419 fn fisher_no_association() {
3420 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3422 assert!(
3423 r.p_value > 0.9,
3424 "no association → p ≈ 1.0, got {}",
3425 r.p_value
3426 );
3427 }
3428
3429 #[test]
3430 fn fisher_small_table() {
3431 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3433 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3434 }
3435
3436 #[test]
3437 fn fisher_asymmetric() {
3438 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3440 assert!(r.p_value < 0.05, "significant association");
3441 }
3442
3443 #[test]
3444 fn fisher_edge_cases() {
3445 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()); }
3450
3451 #[test]
3452 fn fisher_odds_ratio() {
3453 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3454 assert!(
3456 (r.statistic - 9.0).abs() < 1e-10,
3457 "OR = 9, got {}",
3458 r.statistic
3459 );
3460 }
3461
3462 #[test]
3467 fn mk_increasing_trend() {
3468 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3469 let r = mann_kendall_test(&data).expect("should compute");
3470 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3471 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3472 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3473 assert!(r.s_statistic > 0);
3474 }
3475
3476 #[test]
3477 fn mk_decreasing_trend() {
3478 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3479 let r = mann_kendall_test(&data).expect("should compute");
3480 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3481 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3482 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3483 assert!(r.s_statistic < 0);
3484 }
3485
3486 #[test]
3487 fn mk_no_trend() {
3488 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3490 let r = mann_kendall_test(&data).expect("should compute");
3491 assert!(
3493 r.p_value > 0.05,
3494 "p = {} (should be > 0.05 for no trend)",
3495 r.p_value
3496 );
3497 }
3498
3499 #[test]
3500 fn mk_perfect_monotone() {
3501 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3502 let r = mann_kendall_test(&data).expect("should compute");
3503 assert_eq!(r.s_statistic, 45);
3505 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3506 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3507 }
3508
3509 #[test]
3510 fn mk_with_ties() {
3511 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3512 let r = mann_kendall_test(&data).expect("should compute");
3513 assert!(r.s_statistic > 0);
3514 let n = data.len() as f64;
3516 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3517 assert!(r.variance < base_var, "ties should reduce variance");
3518 }
3519
3520 #[test]
3521 fn mk_edge_cases() {
3522 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3524 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3526 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3528 }
3529
3530 #[test]
3531 fn mk_minimum_n() {
3532 let data = [1.0, 2.0, 3.0, 4.0];
3534 let r = mann_kendall_test(&data).expect("n=4 should work");
3535 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3537 }
3538
3539 #[test]
3540 fn mk_sen_slope_robust_to_outlier() {
3541 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3543 let r = mann_kendall_test(&data).expect("should compute");
3544 assert!(
3546 (r.sen_slope - 1.0).abs() < 0.5,
3547 "sen slope = {}, expected ≈ 1.0",
3548 r.sen_slope
3549 );
3550 }
3551
3552 #[test]
3557 fn adf_stationary_mean_reverting() {
3558 let data = [
3561 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,
3562 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,
3563 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,
3564 ];
3565 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3566 assert!(
3568 r.rejected[2],
3569 "should reject at 10%: stat={}, cv={}",
3570 r.statistic, r.critical_values[2]
3571 );
3572 }
3573
3574 #[test]
3575 fn adf_nonstationary_random_walk() {
3576 let increments = [
3578 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,
3579 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,
3580 -0.13, 0.15, -0.1,
3581 ];
3582 let mut walk = Vec::with_capacity(increments.len());
3583 let mut cum = 0.0;
3584 for &inc in &increments {
3585 cum += inc;
3586 walk.push(cum);
3587 }
3588 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3589 assert!(
3591 !r.rejected[0],
3592 "should NOT reject at 1%: stat={}, cv={}",
3593 r.statistic, r.critical_values[0]
3594 );
3595 }
3596
3597 #[test]
3598 fn adf_with_fixed_lags() {
3599 let data: Vec<f64> = (0..50)
3601 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3602 .collect();
3603 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3604 assert_eq!(r.n_lags, 2);
3605 assert!(r.statistic.is_finite());
3606 }
3607
3608 #[test]
3609 fn adf_constant_trend_model() {
3610 let data: Vec<f64> = (0..30)
3613 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3614 .collect();
3615 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3616 assert!(r.statistic.is_finite());
3617 assert_eq!(r.critical_values.len(), 3);
3618 }
3619
3620 #[test]
3621 fn adf_edge_cases() {
3622 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3624 let mut data = vec![0.0; 20];
3626 data[5] = f64::NAN;
3627 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3628 }
3629
3630 #[test]
3631 fn adf_critical_values_constant() {
3632 let cv = adf_critical_values(AdfModel::Constant, 100);
3634 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3636 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3637 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3638 }
3639
3640 #[test]
3641 fn adf_critical_values_ordering() {
3642 let cv = adf_critical_values(AdfModel::Constant, 50);
3643 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3645 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3646 }
3647}
3648
3649#[cfg(test)]
3650mod proptests {
3651 use super::*;
3652 use proptest::prelude::*;
3653
3654 proptest! {
3655 #[test]
3656 fn one_sample_p_bounded(
3657 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3658 mu0 in -1e3_f64..1e3
3659 ) {
3660 if let Some(r) = one_sample_t_test(&data, mu0) {
3661 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3662 }
3663 }
3664
3665 #[test]
3666 fn two_sample_p_bounded(
3667 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3668 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3669 ) {
3670 if let Some(r) = two_sample_t_test(&a, &b) {
3671 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3672 }
3673 }
3674
3675 #[test]
3676 fn anova_p_bounded(
3677 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3678 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3679 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3680 ) {
3681 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3682 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3683 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3684 }
3685 }
3686
3687 #[test]
3688 fn bonferroni_monotone(
3689 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3690 ) {
3691 let adj = bonferroni_correction(&ps).expect("should compute");
3692 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3693 prop_assert!(adjusted >= orig - 1e-15,
3694 "adj[{i}] = {adjusted} < orig = {orig}");
3695 }
3696 }
3697
3698 #[test]
3699 fn shapiro_wilk_p_bounded(
3700 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3701 ) {
3702 if let Some(r) = shapiro_wilk_test(&data) {
3703 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3704 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3705 }
3706 }
3707
3708 #[test]
3709 fn anderson_darling_p_bounded(
3710 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3711 ) {
3712 if let Some(r) = anderson_darling_test(&data) {
3713 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3714 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3715 }
3716 }
3717
3718 #[test]
3719 fn mann_whitney_p_bounded(
3720 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3721 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3722 ) {
3723 if let Some(r) = mann_whitney_u_test(&a, &b) {
3724 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3725 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3726 }
3727 }
3728
3729 #[test]
3730 fn wilcoxon_p_bounded(
3731 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3732 ) {
3733 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3734 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3735 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3736 }
3737 }
3738
3739 #[test]
3740 fn bartlett_p_bounded(
3741 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3742 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3743 ) {
3744 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3745 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3746 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3747 }
3748 }
3749
3750 #[test]
3751 fn fisher_p_bounded(
3752 a in 0_u64..20,
3753 b in 0_u64..20,
3754 c in 0_u64..20,
3755 d in 0_u64..20,
3756 ) {
3757 if let Some(r) = fisher_exact_test(a, b, c, d) {
3758 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3759 }
3760 }
3761
3762 #[test]
3763 fn mann_kendall_p_bounded(
3764 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3765 ) {
3766 if let Some(r) = mann_kendall_test(&data) {
3767 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3768 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3769 "tau = {}", r.kendall_tau);
3770 }
3771 }
3772
3773 #[test]
3774 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3775 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3776 let r = mann_kendall_test(&data).expect("should compute");
3777 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3778 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3779 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3780 }
3781 }
3782}