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> {
1037 let n1 = a.len();
1038 let n2 = b.len();
1039 if n1 < 2 || n2 < 2 {
1040 return None;
1041 }
1042 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1043 return None;
1044 }
1045
1046 let n = n1 + n2;
1047 let n1f = n1 as f64;
1048 let n2f = n2 as f64;
1049 let nf = n as f64;
1050
1051 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1053 for &v in a {
1054 combined.push((v, 0)); }
1056 for &v in b {
1057 combined.push((v, 1)); }
1059 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1060
1061 let ranks = average_ranks(&combined);
1063
1064 let r1: f64 = combined
1066 .iter()
1067 .zip(ranks.iter())
1068 .filter(|((_, g), _)| *g == 0)
1069 .map(|(_, &r)| r)
1070 .sum();
1071
1072 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1074
1075 let tie_correction = compute_tie_correction(&combined);
1077
1078 let mu = n1f * n2f / 2.0;
1080 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1081
1082 if sigma_sq <= 0.0 {
1083 return None;
1084 }
1085
1086 let z = (u1 - mu) / sigma_sq.sqrt();
1087 let p_value = 2.0 * special::standard_normal_sf(z.abs());
1088
1089 Some(TestResult {
1090 statistic: u1,
1091 df: 0.0, p_value,
1093 })
1094}
1095
1096pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1130 if x.len() != y.len() || x.len() < 2 {
1131 return None;
1132 }
1133 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1134 return None;
1135 }
1136
1137 let diffs: Vec<f64> = x
1139 .iter()
1140 .zip(y.iter())
1141 .map(|(&a, &b)| a - b)
1142 .filter(|&d| d.abs() > 1e-300)
1143 .collect();
1144
1145 let nr = diffs.len();
1146 if nr < 2 {
1147 return None;
1148 }
1149
1150 let nf = nr as f64;
1151
1152 let mut abs_diffs: Vec<(f64, usize)> = diffs
1154 .iter()
1155 .enumerate()
1156 .map(|(i, &d)| (d.abs(), i))
1157 .collect();
1158 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1159
1160 let ranks = average_ranks(&abs_diffs);
1162
1163 let t_plus: f64 = abs_diffs
1165 .iter()
1166 .zip(ranks.iter())
1167 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1168 .map(|(_, &r)| r)
1169 .sum();
1170
1171 let tie_correction_val = compute_tie_correction(&abs_diffs);
1173
1174 let mu = nf * (nf + 1.0) / 4.0;
1176 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1177
1178 if sigma_sq <= 0.0 {
1179 return None;
1180 }
1181
1182 let z = (t_plus - mu) / sigma_sq.sqrt();
1183 let p_value = 2.0 * special::standard_normal_sf(z.abs());
1184
1185 Some(TestResult {
1186 statistic: t_plus,
1187 df: 0.0,
1188 p_value,
1189 })
1190}
1191
1192fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1195 let n = sorted.len();
1196 let mut ranks = vec![0.0; n];
1197 let mut i = 0;
1198 while i < n {
1199 let mut j = i + 1;
1200 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1201 j += 1;
1202 }
1203 let avg_rank = (i + 1 + j) as f64 / 2.0;
1205 for rank in ranks.iter_mut().take(j).skip(i) {
1206 *rank = avg_rank;
1207 }
1208 i = j;
1209 }
1210 ranks
1211}
1212
1213fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1215 let n = sorted.len();
1216 let mut correction = 0.0;
1217 let mut i = 0;
1218 while i < n {
1219 let mut j = i + 1;
1220 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1221 j += 1;
1222 }
1223 let t = (j - i) as f64;
1224 if t > 1.0 {
1225 correction += t * (t * t - 1.0);
1226 }
1227 i = j;
1228 }
1229 correction
1230}
1231
1232pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1264 let k = groups.len();
1265 if k < 2 {
1266 return None;
1267 }
1268 for g in groups {
1269 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1270 return None;
1271 }
1272 }
1273
1274 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1275 let nf = total_n as f64;
1276
1277 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1279 for (gi, g) in groups.iter().enumerate() {
1280 for &v in *g {
1281 combined.push((v, gi));
1282 }
1283 }
1284 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1285
1286 let ranks = average_ranks(&combined);
1287
1288 let mut rank_sums = vec![0.0; k];
1290 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1291 rank_sums[*gi] += r;
1292 }
1293
1294 let mean_rank = (nf + 1.0) / 2.0;
1296 let mut h = 0.0;
1297 for (gi, g) in groups.iter().enumerate() {
1298 let ni = g.len() as f64;
1299 let mean_rank_i = rank_sums[gi] / ni;
1300 h += ni * (mean_rank_i - mean_rank).powi(2);
1301 }
1302 h *= 12.0 / (nf * (nf + 1.0));
1303
1304 let tie_corr = compute_tie_correction(&combined);
1306 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1307 if denom > 1e-15 {
1308 h /= denom;
1309 }
1310
1311 let df = (k - 1) as f64;
1312 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1313
1314 Some(TestResult {
1315 statistic: h,
1316 df,
1317 p_value,
1318 })
1319}
1320
1321pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1357 let k = groups.len();
1358 if k < 2 {
1359 return None;
1360 }
1361 for g in groups {
1362 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1363 return None;
1364 }
1365 }
1366
1367 let z_groups: Vec<Vec<f64>> = groups
1369 .iter()
1370 .map(|g| {
1371 let median = stats::median(g).unwrap_or(0.0);
1372 g.iter().map(|&x| (x - median).abs()).collect()
1373 })
1374 .collect();
1375
1376 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1378 let anova = one_way_anova(&z_refs)?;
1379
1380 Some(TestResult {
1381 statistic: anova.f_statistic,
1382 df: anova.df_between as f64,
1383 p_value: anova.p_value,
1384 })
1385}
1386
1387pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1399 if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1400 return None;
1401 }
1402 let m = p_values.len() as f64;
1403 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1404}
1405
1406pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1425 let m = p_values.len();
1426 if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1427 return None;
1428 }
1429
1430 let mut indices: Vec<usize> = (0..m).collect();
1432 indices.sort_by(|&a, &b| {
1433 p_values[a]
1434 .partial_cmp(&p_values[b])
1435 .unwrap_or(std::cmp::Ordering::Equal)
1436 });
1437
1438 let mf = m as f64;
1439 let mut adjusted = vec![0.0; m];
1440
1441 let mut cummin = f64::INFINITY;
1443 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1444 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1446 cummin = cummin.min(adj);
1447 adjusted[orig_idx] = cummin;
1448 }
1449
1450 Some(adjusted)
1451}
1452
1453pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1490 let k = groups.len();
1491 if k < 2 {
1492 return None;
1493 }
1494
1495 let mut sizes = Vec::with_capacity(k);
1496 let mut vars = Vec::with_capacity(k);
1497 let mut n_total: usize = 0;
1498
1499 for g in groups {
1500 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1501 return None;
1502 }
1503 let n = g.len();
1504 let v = stats::variance(g)?;
1505 if v <= 0.0 {
1506 return None; }
1508 sizes.push(n);
1509 vars.push(v);
1510 n_total += n;
1511 }
1512
1513 let nk = n_total - k; if nk == 0 {
1515 return None;
1516 }
1517 let nk_f = nk as f64;
1518
1519 let s2_pooled: f64 = sizes
1521 .iter()
1522 .zip(vars.iter())
1523 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1524 .sum::<f64>()
1525 / nk_f;
1526
1527 if s2_pooled <= 0.0 {
1528 return None;
1529 }
1530
1531 let num = nk_f * s2_pooled.ln()
1533 - sizes
1534 .iter()
1535 .zip(vars.iter())
1536 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1537 .sum::<f64>();
1538
1539 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1541 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1542
1543 let statistic = num / c;
1544 let df = (k - 1) as f64;
1545 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1546
1547 Some(TestResult {
1548 statistic,
1549 df,
1550 p_value,
1551 })
1552}
1553
1554pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1599 let row1 = a + b;
1600 let row2 = c + d;
1601 let col1 = a + c;
1602 let col2 = b + d;
1603 let n = a + b + c + d;
1604
1605 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1607 return None;
1608 }
1609
1610 let log_prob = |a_i: u64| -> f64 {
1612 let b_i = row1 - a_i;
1613 let c_i = col1 - a_i;
1614 let d_i = row2 - c_i;
1615 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1616 - ln_factorial(a_i)
1617 - ln_factorial(b_i)
1618 - ln_factorial(c_i)
1619 - ln_factorial(d_i)
1620 - ln_factorial(n)
1621 };
1622
1623 let a_min = col1.saturating_sub(row2);
1625 let a_max = row1.min(col1);
1626
1627 let log_p_obs = log_prob(a);
1628
1629 let mut p_value = 0.0;
1631 for a_i in a_min..=a_max {
1632 let lp = log_prob(a_i);
1633 if lp <= log_p_obs + 1e-10 {
1635 p_value += lp.exp();
1636 }
1637 }
1638
1639 let p_value = p_value.min(1.0);
1641
1642 let odds_ratio = if b > 0 && c > 0 {
1644 (a as f64 * d as f64) / (b as f64 * c as f64)
1645 } else {
1646 f64::INFINITY
1647 };
1648
1649 Some(TestResult {
1650 statistic: odds_ratio,
1651 df: 1.0,
1652 p_value,
1653 })
1654}
1655
1656#[derive(Debug, Clone, Copy)]
1662pub struct MannKendallResult {
1663 pub s_statistic: i64,
1665 pub variance: f64,
1667 pub z_statistic: f64,
1669 pub p_value: f64,
1671 pub kendall_tau: f64,
1673 pub sen_slope: f64,
1675}
1676
1677pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1715 let n = data.len();
1716 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1717 return None;
1718 }
1719
1720 let mut s: i64 = 0;
1722 for i in 0..n - 1 {
1723 for j in (i + 1)..n {
1724 let diff = data[j] - data[i];
1725 if diff > 0.0 {
1726 s += 1;
1727 } else if diff < 0.0 {
1728 s -= 1;
1729 }
1730 }
1731 }
1732
1733 let mut sorted: Vec<f64> = data.to_vec();
1735 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1736
1737 let mut tie_correction: f64 = 0.0;
1738 let mut current_count: usize = 1;
1739 for i in 1..sorted.len() {
1740 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1741 current_count += 1;
1742 } else {
1743 if current_count > 1 {
1744 let t = current_count as f64;
1745 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1746 }
1747 current_count = 1;
1748 }
1749 }
1750 if current_count > 1 {
1751 let t = current_count as f64;
1752 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1753 }
1754
1755 let nf = n as f64;
1757 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1758
1759 if variance < 1e-300 {
1760 return None; }
1762
1763 let sigma = variance.sqrt();
1765 let z_statistic = if s > 0 {
1766 (s as f64 - 1.0) / sigma
1767 } else if s < 0 {
1768 (s as f64 + 1.0) / sigma
1769 } else {
1770 0.0
1771 };
1772
1773 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1775 let p_value = p_value.clamp(0.0, 1.0);
1776
1777 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1779
1780 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1782 for i in 0..n - 1 {
1783 for j in (i + 1)..n {
1784 let dx = (j - i) as f64;
1785 slopes.push((data[j] - data[i]) / dx);
1786 }
1787 }
1788 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1789 let m = slopes.len();
1790 let sen_slope = if m % 2 == 0 {
1791 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1792 } else {
1793 slopes[m / 2]
1794 };
1795
1796 Some(MannKendallResult {
1797 s_statistic: s,
1798 variance,
1799 z_statistic,
1800 p_value,
1801 kendall_tau,
1802 sen_slope,
1803 })
1804}
1805
1806fn ln_factorial(n: u64) -> f64 {
1808 if n <= 1 {
1809 return 0.0;
1810 }
1811 special::ln_gamma(n as f64 + 1.0)
1813}
1814
1815#[derive(Debug, Clone)]
1821pub struct AdfResult {
1822 pub statistic: f64,
1824 pub n_lags: usize,
1826 pub n_obs: usize,
1828 pub critical_values: [f64; 3],
1830 pub rejected: [bool; 3],
1832}
1833
1834#[derive(Debug, Clone, Copy)]
1836pub enum AdfModel {
1837 None,
1839 Constant,
1841 ConstantTrend,
1843}
1844
1845pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1881 let n = data.len();
1882 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1883 return None;
1884 }
1885
1886 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1888
1889 let best_lag = match max_lags {
1891 Some(p) => p, None => {
1893 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1895 let p_max = schwert.min(n / 3);
1896 select_adf_lag(data, &dy, model, p_max)
1898 }
1899 };
1900
1901 adf_ols(data, &dy, model, best_lag)
1903}
1904
1905fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1907 let mut best_aic = f64::INFINITY;
1908 let mut best_p = 0;
1909
1910 for p in 0..=p_max {
1911 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1912 if aic < best_aic {
1913 best_aic = aic;
1914 best_p = p;
1915 }
1916 }
1917 }
1918
1919 best_p
1920}
1921
1922#[allow(clippy::type_complexity)]
1926fn adf_build_matrix(
1927 data: &[f64],
1928 dy: &[f64],
1929 model: AdfModel,
1930 p: usize,
1931) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1932 let start = p + 1;
1933 if start >= dy.len() || dy.len() - start < 5 {
1934 return None;
1935 }
1936 let m = dy.len() - start;
1937
1938 let y_dep: Vec<f64> = dy[start..].to_vec();
1939
1940 let has_intercept = !matches!(model, AdfModel::None);
1942 let has_trend = matches!(model, AdfModel::ConstantTrend);
1943 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1944
1945 let mut x_data = Vec::with_capacity(m * ncols);
1947 let mut gamma_col = 0;
1948
1949 for i in 0..m {
1950 let t = start + i;
1951 if has_intercept {
1952 x_data.push(1.0); gamma_col = 1;
1954 }
1955 x_data.push(data[t]); if has_trend {
1957 x_data.push((t + 1) as f64); }
1959 for lag in 1..=p {
1960 x_data.push(dy[t - lag]);
1961 }
1962 }
1963
1964 Some((x_data, y_dep, m, ncols, gamma_col))
1965}
1966
1967fn adf_ols_core(
1971 x_data: &[f64],
1972 y: &[f64],
1973 m: usize,
1974 ncols: usize,
1975 gamma_col: usize,
1976) -> Option<(f64, f64, usize)> {
1977 let mut xtx = vec![0.0_f64; ncols * ncols];
1979 for i in 0..m {
1980 let row = &x_data[i * ncols..(i + 1) * ncols];
1981 for j in 0..ncols {
1982 for k in j..ncols {
1983 xtx[j * ncols + k] += row[j] * row[k];
1984 }
1985 }
1986 }
1987 for j in 0..ncols {
1989 for k in (j + 1)..ncols {
1990 xtx[k * ncols + j] = xtx[j * ncols + k];
1991 }
1992 }
1993
1994 let mut xty = vec![0.0_f64; ncols];
1996 for i in 0..m {
1997 let row = &x_data[i * ncols..(i + 1) * ncols];
1998 for j in 0..ncols {
1999 xty[j] += row[j] * y[i];
2000 }
2001 }
2002
2003 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2005 for i in 0..ncols {
2006 for j in 0..ncols {
2007 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2008 }
2009 augmented[i * (ncols + 1) + ncols] = xty[i];
2010 }
2011
2012 for col in 0..ncols {
2013 let mut max_row = col;
2015 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2016 for row in (col + 1)..ncols {
2017 let val = augmented[row * (ncols + 1) + col].abs();
2018 if val > max_val {
2019 max_val = val;
2020 max_row = row;
2021 }
2022 }
2023 if max_val < 1e-15 {
2024 return None; }
2026 if max_row != col {
2027 for j in 0..=ncols {
2028 let a = col * (ncols + 1) + j;
2029 let b = max_row * (ncols + 1) + j;
2030 augmented.swap(a, b);
2031 }
2032 }
2033
2034 let pivot = augmented[col * (ncols + 1) + col];
2035 for row in (col + 1)..ncols {
2036 let factor = augmented[row * (ncols + 1) + col] / pivot;
2037 for j in col..=ncols {
2038 let above = augmented[col * (ncols + 1) + j];
2039 augmented[row * (ncols + 1) + j] -= factor * above;
2040 }
2041 }
2042 }
2043
2044 let mut beta = vec![0.0_f64; ncols];
2046 for i in (0..ncols).rev() {
2047 let mut sum = augmented[i * (ncols + 1) + ncols];
2048 for j in (i + 1)..ncols {
2049 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2050 }
2051 beta[i] = sum / augmented[i * (ncols + 1) + i];
2052 }
2053
2054 let mut rss = 0.0;
2056 for i in 0..m {
2057 let row = &x_data[i * ncols..(i + 1) * ncols];
2058 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2059 let resid = y[i] - y_hat;
2060 rss += resid * resid;
2061 }
2062
2063 let df = m - ncols;
2065 if df == 0 {
2066 return None;
2067 }
2068 let mse = rss / df as f64;
2069
2070 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2073 for j in 0..ncols {
2074 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2075 }
2076 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2077 }
2078
2079 for col in 0..ncols {
2081 let mut max_row = col;
2082 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2083 for row in (col + 1)..ncols {
2084 let val = xtx_aug[row * 2 * ncols + col].abs();
2085 if val > max_val {
2086 max_val = val;
2087 max_row = row;
2088 }
2089 }
2090 if max_val < 1e-15 {
2091 return None;
2092 }
2093 if max_row != col {
2094 for j in 0..(2 * ncols) {
2095 let a = col * 2 * ncols + j;
2096 let b = max_row * 2 * ncols + j;
2097 xtx_aug.swap(a, b);
2098 }
2099 }
2100
2101 let pivot = xtx_aug[col * 2 * ncols + col];
2102 for j in 0..(2 * ncols) {
2103 xtx_aug[col * 2 * ncols + j] /= pivot;
2104 }
2105 for row in 0..ncols {
2106 if row == col {
2107 continue;
2108 }
2109 let factor = xtx_aug[row * 2 * ncols + col];
2110 for j in 0..(2 * ncols) {
2111 let above = xtx_aug[col * 2 * ncols + j];
2112 xtx_aug[row * 2 * ncols + j] -= factor * above;
2113 }
2114 }
2115 }
2116
2117 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2119 if var_gamma <= 0.0 {
2120 return None;
2121 }
2122 let se_gamma = var_gamma.sqrt();
2123 let t_gamma = beta[gamma_col] / se_gamma;
2124
2125 Some((t_gamma, rss, ncols))
2126}
2127
2128fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2130 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2131 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2132 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2133 Some((aic, m))
2134}
2135
2136fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2138 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2139 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2140
2141 let critical_values = adf_critical_values(model, m);
2142
2143 let rejected = [
2144 gamma_t <= critical_values[0],
2145 gamma_t <= critical_values[1],
2146 gamma_t <= critical_values[2],
2147 ];
2148
2149 Some(AdfResult {
2150 statistic: gamma_t,
2151 n_lags: p,
2152 n_obs: m,
2153 critical_values,
2154 rejected,
2155 })
2156}
2157
2158fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2162 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2167 AdfModel::None => (
2168 [-2.5658, -1.9393, -1.6156],
2169 [-1.960, -0.398, -0.181],
2170 [-10.04, 0.0, 0.0],
2171 ),
2172 AdfModel::Constant => (
2173 [-3.4336, -2.8621, -2.5671],
2174 [-5.999, -2.738, -1.438],
2175 [-29.25, -8.36, -4.48],
2176 ),
2177 AdfModel::ConstantTrend => (
2178 [-3.9638, -3.4126, -3.1279],
2179 [-8.353, -4.039, -2.418],
2180 [-47.44, -17.83, -7.58],
2181 ),
2182 };
2183
2184 let nf = n as f64;
2185 let inv_n = 1.0 / nf;
2186 let inv_n2 = inv_n * inv_n;
2187
2188 [
2189 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2190 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2191 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2192 ]
2193}
2194
2195#[cfg(test)]
2196mod tests {
2197 use super::*;
2198
2199 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2205 let mut a = seed;
2206 move || {
2207 a = a.wrapping_add(0x6d2b_79f5);
2208 let mut t = a;
2209 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2210 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2211 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2212 }
2213 }
2214
2215 #[test]
2221 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2222 let mut rng = mulberry32(0x5350_4331);
2223 let full: Vec<f64> = (0..20_000)
2224 .map(|_| {
2225 let u = rng();
2226 -(1.0 - u).ln() })
2228 .collect();
2229
2230 let mut prev_a2 = 0.0_f64;
2231 let mut prev_p = f64::INFINITY;
2232 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2233 let r = anderson_darling_normality(&full[..n]).expect("computes");
2234 assert!(
2236 r.statistic_modified > prev_a2,
2237 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2238 r.statistic_modified
2239 );
2240 assert!(
2242 r.p_value < 0.5,
2243 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2244 r.statistic_modified,
2245 r.p_value
2246 );
2247 assert!(
2249 r.p_value <= prev_p + 1e-12,
2250 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2251 r.p_value
2252 );
2253 prev_a2 = r.statistic_modified;
2254 prev_p = r.p_value;
2255 }
2256 }
2257
2258 #[test]
2261 fn ad_both_functions_immune_to_overflow() {
2262 let mut rng = mulberry32(0x0bad_c0de);
2263 let data: Vec<f64> = (0..9000).map(|_| -(1.0 - rng()).ln()).collect();
2264 let a = anderson_darling_test(&data).expect("computes");
2265 let b = anderson_darling_normality(&data).expect("computes");
2266 assert!(
2267 a.statistic_star > 300.0,
2268 "expected large A*², got {}",
2269 a.statistic_star
2270 );
2271 assert!(
2272 a.p_value < 0.5,
2273 "test() overflowed toward normal: p={}",
2274 a.p_value
2275 );
2276 assert!(
2277 b.p_value < 0.5,
2278 "normality() overflowed toward normal: p={}",
2279 b.p_value
2280 );
2281 }
2282
2283 #[test]
2288 fn one_sample_null_true() {
2289 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2290 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2291 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2292 }
2293
2294 #[test]
2295 fn one_sample_null_false() {
2296 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2297 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2298 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2299 }
2300
2301 #[test]
2302 fn one_sample_edge_cases() {
2303 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());
2306 }
2307
2308 #[test]
2313 fn two_sample_same_mean() {
2314 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2315 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2316 let r = two_sample_t_test(&a, &b).expect("should compute");
2317 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2318 }
2319
2320 #[test]
2321 fn two_sample_different_means() {
2322 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2323 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2324 let r = two_sample_t_test(&a, &b).expect("should compute");
2325 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2326 }
2327
2328 #[test]
2329 fn two_sample_different_sizes() {
2330 let a = [1.0, 2.0, 3.0];
2331 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2332 let r = two_sample_t_test(&a, &b).expect("should compute");
2333 assert!(r.p_value < 0.05);
2334 }
2335
2336 #[test]
2337 fn two_sample_edge_cases() {
2338 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2339 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2340 }
2341
2342 #[test]
2347 fn paired_no_difference() {
2348 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2349 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2350 let r = paired_t_test(&x, &y).expect("should compute");
2351 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2352 }
2353
2354 #[test]
2355 fn paired_significant_difference() {
2356 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2358 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2359 let r = paired_t_test(&before, &after).expect("should compute");
2360 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2361 assert!(r.statistic < 0.0); }
2363
2364 #[test]
2365 fn paired_edge_cases() {
2366 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2369
2370 #[test]
2375 fn anova_same_means() {
2376 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2377 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2378 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2379 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2380 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2381 }
2382
2383 #[test]
2384 fn anova_different_means() {
2385 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2386 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2387 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2388 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2389 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2390 assert!(r.df_between == 2);
2391 assert!(r.df_within == 12);
2392 }
2393
2394 #[test]
2395 fn anova_ss_decomposition() {
2396 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2397 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2398 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2399 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2401 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2402 assert!(
2403 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2404 "SS decomposition: {ss_total} vs {} + {}",
2405 r.ss_between,
2406 r.ss_within
2407 );
2408 }
2409
2410 #[test]
2411 fn anova_edge_cases() {
2412 let g1 = [1.0, 2.0, 3.0];
2413 assert!(one_way_anova(&[&g1]).is_none()); }
2415
2416 #[test]
2421 fn chi2_gof_uniform() {
2422 let observed = [25.0, 25.0, 25.0, 25.0];
2424 let expected = [25.0, 25.0, 25.0, 25.0];
2425 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2426 assert!((r.statistic).abs() < 1e-15);
2427 assert!((r.p_value - 1.0).abs() < 0.01);
2428 }
2429
2430 #[test]
2431 fn chi2_gof_significant() {
2432 let observed = [90.0, 10.0];
2433 let expected = [50.0, 50.0];
2434 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2435 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2436 }
2437
2438 #[test]
2439 fn chi2_gof_edge_cases() {
2440 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()); }
2444
2445 #[test]
2450 fn chi2_independence_significant() {
2451 let table = [30.0, 10.0, 10.0, 50.0];
2453 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2454 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2455 assert!((r.df - 1.0).abs() < 1e-10);
2456 }
2457
2458 #[test]
2459 fn chi2_independence_not_significant() {
2460 let table = [25.0, 25.0, 25.0, 25.0];
2462 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2463 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2464 }
2465
2466 #[test]
2467 fn chi2_independence_3x3() {
2468 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2469 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2470 assert!((r.df - 4.0).abs() < 1e-10);
2471 assert!(r.p_value < 0.05);
2472 }
2473
2474 #[test]
2475 fn chi2_independence_edge_cases() {
2476 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()); }
2480
2481 #[test]
2486 fn jb_normal_data() {
2487 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2489 let r = jarque_bera_test(&data).expect("should compute");
2490 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2491 }
2492
2493 #[test]
2494 fn jb_skewed_data() {
2495 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2497 let r = jarque_bera_test(&data).expect("should compute");
2498 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2499 }
2500
2501 #[test]
2508 fn jb_is_the_moment_form_the_paper_defines() {
2509 let data = [
2510 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,
2511 3.6, 2.8, 3.2,
2512 ];
2513 let r = jarque_bera_test(&data).expect("20 finite points");
2514 assert!(
2515 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2516 "JB = {}",
2517 r.statistic
2518 );
2519
2520 let n = data.len() as f64;
2523 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2524 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2525 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2526 assert!(
2527 adjusted > r.statistic * 1.3,
2528 "the two forms differ by more than a third here: {adjusted} vs {}",
2529 r.statistic
2530 );
2531 }
2532
2533 #[test]
2537 fn jb_p_value_keeps_its_digits_in_the_tail() {
2538 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2541 data.push(60.0);
2542 let r = jarque_bera_test(&data).expect("20 finite points");
2543 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2544 assert!(
2545 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2546 "this sample is only interesting because the complement underflows"
2547 );
2548 assert!(
2549 r.p_value > 0.0,
2550 "the complement returns 0 here; this must not"
2551 );
2552 assert!(
2553 (r.p_value - 6.702e-21).abs() < 1e-23,
2554 "p = {} (expected about 6.7e-21)",
2555 r.p_value
2556 );
2557 assert!(
2558 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2559 "p = {} vs exp(-JB/2) = {}",
2560 r.p_value,
2561 (-r.statistic / 2.0).exp()
2562 );
2563 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])
2566 .expect("10 finite points");
2567 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2568 assert!(
2569 (small.p_value - via_cdf).abs() < 1e-12,
2570 "{} vs {via_cdf}",
2571 small.p_value
2572 );
2573 }
2574
2575 #[test]
2576 fn jb_edge_cases() {
2577 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2579
2580 #[test]
2585 fn ad_normal_data() {
2586 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2587 let r = anderson_darling_test(&data).expect("should compute");
2588 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2589 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2590 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2591 }
2592
2593 #[test]
2594 fn ad_skewed_data() {
2595 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];
2597 let r = anderson_darling_test(&data).expect("should compute");
2598 assert!(
2599 r.p_value < 0.05,
2600 "p = {} (should reject normality)",
2601 r.p_value
2602 );
2603 }
2604
2605 #[test]
2606 fn ad_bimodal_data() {
2607 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2609 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2610 let r = anderson_darling_test(&data).expect("should compute");
2611 assert!(
2612 r.p_value < 0.01,
2613 "p = {} (bimodal should reject normality)",
2614 r.p_value
2615 );
2616 }
2617
2618 #[test]
2619 fn ad_large_normal_sample() {
2620 let n = 100;
2621 let data: Vec<f64> = (1..=n)
2622 .map(|i| {
2623 let p = (i as f64 - 0.5) / n as f64;
2624 special::inverse_normal_cdf(p)
2625 })
2626 .collect();
2627 let r = anderson_darling_test(&data).expect("should compute");
2628 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2629 }
2630
2631 #[test]
2632 fn ad_edge_cases() {
2633 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());
2636 }
2637
2638 #[test]
2639 fn ad_p_value_ranges() {
2640 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2645 let r = anderson_darling_test(&near_normal).expect("should compute");
2646 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2647
2648 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];
2650 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2651 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2652 }
2653
2654 #[test]
2659 fn ad_normal_data_large_p() {
2660 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2662 let r = anderson_darling_normality(&data).unwrap();
2663 assert!(r.p_value > 0.05, "p={}", r.p_value);
2664 }
2665
2666 #[test]
2667 fn ad_exponential_data_small_p() {
2668 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2670 let r = anderson_darling_normality(&data).unwrap();
2671 assert!(r.p_value < 0.05, "p={}", r.p_value);
2672 }
2673
2674 #[test]
2675 fn ad_statistic_non_negative() {
2676 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2677 let r = anderson_darling_normality(&data).unwrap();
2678 assert!(r.statistic >= 0.0);
2679 assert!(r.statistic_modified >= r.statistic);
2680 }
2681
2682 #[test]
2683 fn ad_p_value_in_range() {
2684 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2685 let r = anderson_darling_normality(&data).unwrap();
2686 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2687 }
2688
2689 #[test]
2690 fn ad_insufficient_data() {
2691 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2693
2694 #[test]
2695 fn ad_degenerate_data() {
2696 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2698 }
2699
2700 #[test]
2701 fn ad_modified_statistic_formula() {
2702 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2704 let r = anderson_darling_normality(&data).unwrap();
2705 assert!(r.statistic_modified > r.statistic - 1e-10);
2706 }
2707
2708 #[test]
2713 fn ad_correction_factor_formula() {
2714 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2715 let n = data.len() as f64;
2716 let r = anderson_darling_normality(&data).unwrap();
2717
2718 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2720 assert!(
2721 (r.statistic_modified - expected_star).abs() < 1e-12,
2722 "A²* = {}, expected = {}",
2723 r.statistic_modified,
2724 expected_star
2725 );
2726
2727 assert!(
2729 r.statistic >= 0.0,
2730 "A² = {} must be non-negative",
2731 r.statistic
2732 );
2733 }
2734
2735 #[test]
2741 fn ad_formula_structure_symmetric_vs_skewed() {
2742 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2744 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2746
2747 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2748 let r_skew = anderson_darling_normality(&skewed).unwrap();
2749
2750 assert!(
2751 r_sym.statistic < r_skew.statistic,
2752 "Symmetric A²={} should be less than skewed A²={}",
2753 r_sym.statistic,
2754 r_skew.statistic
2755 );
2756 }
2757
2758 #[test]
2763 fn ad_pvalue_invariants() {
2764 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2766 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2767 assert!(
2768 r_normal.p_value > 0.05,
2769 "Normal data: p = {} (expected > 0.05)",
2770 r_normal.p_value
2771 );
2772
2773 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2775 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2776 assert!(
2777 r_exp.p_value < 0.05,
2778 "Exponential data: p = {} (expected < 0.05)",
2779 r_exp.p_value
2780 );
2781 }
2782
2783 #[test]
2788 fn sw_normal_data() {
2789 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2791 let r = shapiro_wilk_test(&data).expect("should compute");
2792 assert!(r.w > 0.9, "W = {}", r.w);
2793 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2794 }
2795
2796 #[test]
2797 fn sw_bimodal_data() {
2798 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2800 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2801 let r = shapiro_wilk_test(&data).expect("should compute");
2802 assert!(
2803 r.p_value < 0.01,
2804 "p = {} (bimodal should reject normality)",
2805 r.p_value
2806 );
2807 }
2808
2809 #[test]
2810 fn sw_n3() {
2811 let data = [1.0, 2.0, 3.0];
2812 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2813 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2814 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2815 }
2816
2817 #[test]
2818 fn sw_n4() {
2819 let data = [1.0, 2.0, 3.0, 4.0];
2820 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2821 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2822 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2823 }
2824
2825 #[test]
2826 fn sw_n5() {
2827 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2828 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2829 assert!(r.w > 0.9, "W = {}", r.w);
2830 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2831 }
2832
2833 #[test]
2834 fn sw_skewed_data() {
2835 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];
2837 let r = shapiro_wilk_test(&data).expect("should compute");
2838 assert!(
2839 r.p_value < 0.05,
2840 "p = {} (skewed data should reject normality)",
2841 r.p_value
2842 );
2843 }
2844
2845 #[test]
2846 fn sw_large_normal_sample() {
2847 let n = 100;
2850 let data: Vec<f64> = (1..=n)
2851 .map(|i| {
2852 let p = (i as f64 - 0.5) / n as f64;
2853 special::inverse_normal_cdf(p)
2854 })
2855 .collect();
2856 let r = shapiro_wilk_test(&data).expect("should compute");
2857 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2858 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2859 }
2860
2861 #[test]
2862 fn sw_w_bounded() {
2863 let datasets: Vec<Vec<f64>> = vec![
2865 vec![1.0, 2.0, 3.0],
2866 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2867 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2868 (0..20).map(|i| (i as f64).powi(2)).collect(),
2869 ];
2870 for (idx, data) in datasets.iter().enumerate() {
2871 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2872 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2873 assert!(
2874 r.p_value >= 0.0 && r.p_value <= 1.0,
2875 "dataset {idx}: p = {}",
2876 r.p_value
2877 );
2878 }
2879 }
2880
2881 #[test]
2882 fn sw_edge_cases() {
2883 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()); }
2889
2890 #[test]
2891 fn sw_n5001_rejected() {
2892 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2893 assert!(shapiro_wilk_test(&data).is_none()); }
2895
2896 #[test]
2901 fn mw_clearly_different() {
2902 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2903 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2904 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2905 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2906 }
2907
2908 #[test]
2909 fn mw_same_distribution() {
2910 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2911 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2912 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2913 assert!(
2914 r.p_value > 0.3,
2915 "p = {} (interleaved, same dist)",
2916 r.p_value
2917 );
2918 }
2919
2920 #[test]
2921 fn mw_with_ties() {
2922 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2923 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2924 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2925 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2926 }
2927
2928 #[test]
2929 fn mw_different_sizes() {
2930 let a = [1.0, 2.0, 3.0];
2931 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2932 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2933 assert!(r.p_value < 0.05);
2934 }
2935
2936 #[test]
2937 fn mw_edge_cases() {
2938 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());
2941 }
2942
2943 #[test]
2948 fn wilcoxon_significant_increase() {
2949 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2950 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2951 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2952 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2953 }
2954
2955 #[test]
2956 fn wilcoxon_no_difference() {
2957 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2958 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2959 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2960 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2961 }
2962
2963 #[test]
2964 fn wilcoxon_with_ties() {
2965 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2967 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2969 assert!(r.statistic > 0.0);
2971 }
2972
2973 #[test]
2974 fn wilcoxon_with_zero_diffs() {
2975 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2977 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2979 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2980 }
2981
2982 #[test]
2983 fn wilcoxon_edge_cases() {
2984 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());
2988 }
2989
2990 #[test]
2995 fn kw_clearly_different() {
2996 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2997 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2998 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2999 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3000 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3001 assert!((r.df - 2.0).abs() < 1e-10);
3002 }
3003
3004 #[test]
3005 fn kw_same_distribution() {
3006 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3007 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3008 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3009 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3010 }
3011
3012 #[test]
3013 fn kw_with_ties() {
3014 let g1 = [1.0, 2.0, 2.0, 3.0];
3015 let g2 = [3.0, 4.0, 4.0, 5.0];
3016 let g3 = [5.0, 6.0, 6.0, 7.0];
3017 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3018 assert!(r.statistic > 0.0);
3019 }
3020
3021 #[test]
3022 fn kw_edge_cases() {
3023 let g1 = [1.0, 2.0, 3.0];
3024 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3026
3027 #[test]
3032 fn levene_equal_variance() {
3033 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3034 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3035 let r = levene_test(&[&g1, &g2]).expect("should compute");
3036 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3038 }
3039
3040 #[test]
3041 fn levene_unequal_variance() {
3042 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");
3045 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3046 }
3047
3048 #[test]
3049 fn levene_three_groups() {
3050 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3051 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3052 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3053 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3054 assert!(r.df >= 2.0);
3055 }
3056
3057 #[test]
3058 fn levene_edge_cases() {
3059 let g1 = [1.0, 2.0, 3.0];
3060 assert!(levene_test(&[&g1]).is_none()); }
3062
3063 #[test]
3068 fn bonferroni_basic() {
3069 let ps = [0.01, 0.04, 0.03, 0.005];
3070 let adj = bonferroni_correction(&ps).expect("should compute");
3071 assert!((adj[0] - 0.04).abs() < 1e-10);
3072 assert!((adj[1] - 0.16).abs() < 1e-10);
3073 assert!((adj[2] - 0.12).abs() < 1e-10);
3074 assert!((adj[3] - 0.02).abs() < 1e-10);
3075 }
3076
3077 #[test]
3078 fn bonferroni_capped_at_one() {
3079 let ps = [0.5, 0.6];
3080 let adj = bonferroni_correction(&ps).expect("should compute");
3081 assert!((adj[0] - 1.0).abs() < 1e-10);
3082 assert!((adj[1] - 1.0).abs() < 1e-10);
3083 }
3084
3085 #[test]
3086 fn bh_basic() {
3087 let ps = [0.01, 0.04, 0.03, 0.005];
3088 let adj = benjamini_hochberg(&ps).expect("should compute");
3089 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3091 assert!(
3092 adjusted >= orig - 1e-15,
3093 "adj[{i}] = {adjusted} < original {orig}"
3094 );
3095 }
3096 }
3099
3100 #[test]
3101 fn bh_all_significant() {
3102 let ps = [0.001, 0.002, 0.003];
3103 let adj = benjamini_hochberg(&ps).expect("should compute");
3104 for &a in &adj {
3105 assert!(a < 0.05);
3106 }
3107 }
3108
3109 #[test]
3110 fn correction_edge_cases() {
3111 assert!(bonferroni_correction(&[]).is_none());
3112 assert!(benjamini_hochberg(&[]).is_none());
3113 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3114 }
3115
3116 #[test]
3121 fn bartlett_equal_variances() {
3122 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3123 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3124 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3125 assert!(
3126 r.p_value > 0.9,
3127 "equal variance → p high, got {}",
3128 r.p_value
3129 );
3130 }
3131
3132 #[test]
3133 fn bartlett_unequal_variances() {
3134 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");
3137 assert!(
3138 r.p_value < 0.01,
3139 "very different variances → p < 0.01, got {}",
3140 r.p_value
3141 );
3142 assert!((r.df - 1.0).abs() < 1e-10); }
3144
3145 #[test]
3146 fn bartlett_three_groups() {
3147 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3148 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3149 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3151 assert!(r.p_value < 0.05, "one group with high variance");
3152 assert!((r.df - 2.0).abs() < 1e-10); }
3154
3155 #[test]
3156 fn bartlett_edge_cases() {
3157 let g1 = [1.0, 2.0, 3.0];
3158 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3162
3163 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3165 }
3166
3167 #[test]
3172 fn fisher_tea_tasting() {
3173 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3175 assert!(
3177 (r.p_value - 0.4857).abs() < 0.01,
3178 "p ≈ 0.4857, got {}",
3179 r.p_value
3180 );
3181 }
3182
3183 #[test]
3184 fn fisher_significant() {
3185 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3187 assert!(r.p_value < 0.001, "perfect association → p very small");
3188 }
3189
3190 #[test]
3191 fn fisher_no_association() {
3192 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3194 assert!(
3195 r.p_value > 0.9,
3196 "no association → p ≈ 1.0, got {}",
3197 r.p_value
3198 );
3199 }
3200
3201 #[test]
3202 fn fisher_small_table() {
3203 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3205 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3206 }
3207
3208 #[test]
3209 fn fisher_asymmetric() {
3210 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3212 assert!(r.p_value < 0.05, "significant association");
3213 }
3214
3215 #[test]
3216 fn fisher_edge_cases() {
3217 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()); }
3222
3223 #[test]
3224 fn fisher_odds_ratio() {
3225 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3226 assert!(
3228 (r.statistic - 9.0).abs() < 1e-10,
3229 "OR = 9, got {}",
3230 r.statistic
3231 );
3232 }
3233
3234 #[test]
3239 fn mk_increasing_trend() {
3240 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3241 let r = mann_kendall_test(&data).expect("should compute");
3242 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3243 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3244 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3245 assert!(r.s_statistic > 0);
3246 }
3247
3248 #[test]
3249 fn mk_decreasing_trend() {
3250 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3251 let r = mann_kendall_test(&data).expect("should compute");
3252 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3253 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3254 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3255 assert!(r.s_statistic < 0);
3256 }
3257
3258 #[test]
3259 fn mk_no_trend() {
3260 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3262 let r = mann_kendall_test(&data).expect("should compute");
3263 assert!(
3265 r.p_value > 0.05,
3266 "p = {} (should be > 0.05 for no trend)",
3267 r.p_value
3268 );
3269 }
3270
3271 #[test]
3272 fn mk_perfect_monotone() {
3273 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3274 let r = mann_kendall_test(&data).expect("should compute");
3275 assert_eq!(r.s_statistic, 45);
3277 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3278 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3279 }
3280
3281 #[test]
3282 fn mk_with_ties() {
3283 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3284 let r = mann_kendall_test(&data).expect("should compute");
3285 assert!(r.s_statistic > 0);
3286 let n = data.len() as f64;
3288 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3289 assert!(r.variance < base_var, "ties should reduce variance");
3290 }
3291
3292 #[test]
3293 fn mk_edge_cases() {
3294 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3296 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3298 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3300 }
3301
3302 #[test]
3303 fn mk_minimum_n() {
3304 let data = [1.0, 2.0, 3.0, 4.0];
3306 let r = mann_kendall_test(&data).expect("n=4 should work");
3307 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3309 }
3310
3311 #[test]
3312 fn mk_sen_slope_robust_to_outlier() {
3313 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3315 let r = mann_kendall_test(&data).expect("should compute");
3316 assert!(
3318 (r.sen_slope - 1.0).abs() < 0.5,
3319 "sen slope = {}, expected ≈ 1.0",
3320 r.sen_slope
3321 );
3322 }
3323
3324 #[test]
3329 fn adf_stationary_mean_reverting() {
3330 let data = [
3333 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,
3334 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,
3335 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,
3336 ];
3337 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3338 assert!(
3340 r.rejected[2],
3341 "should reject at 10%: stat={}, cv={}",
3342 r.statistic, r.critical_values[2]
3343 );
3344 }
3345
3346 #[test]
3347 fn adf_nonstationary_random_walk() {
3348 let increments = [
3350 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,
3351 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,
3352 -0.13, 0.15, -0.1,
3353 ];
3354 let mut walk = Vec::with_capacity(increments.len());
3355 let mut cum = 0.0;
3356 for &inc in &increments {
3357 cum += inc;
3358 walk.push(cum);
3359 }
3360 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3361 assert!(
3363 !r.rejected[0],
3364 "should NOT reject at 1%: stat={}, cv={}",
3365 r.statistic, r.critical_values[0]
3366 );
3367 }
3368
3369 #[test]
3370 fn adf_with_fixed_lags() {
3371 let data: Vec<f64> = (0..50)
3373 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3374 .collect();
3375 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3376 assert_eq!(r.n_lags, 2);
3377 assert!(r.statistic.is_finite());
3378 }
3379
3380 #[test]
3381 fn adf_constant_trend_model() {
3382 let data: Vec<f64> = (0..30)
3385 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3386 .collect();
3387 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3388 assert!(r.statistic.is_finite());
3389 assert_eq!(r.critical_values.len(), 3);
3390 }
3391
3392 #[test]
3393 fn adf_edge_cases() {
3394 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3396 let mut data = vec![0.0; 20];
3398 data[5] = f64::NAN;
3399 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3400 }
3401
3402 #[test]
3403 fn adf_critical_values_constant() {
3404 let cv = adf_critical_values(AdfModel::Constant, 100);
3406 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3408 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3409 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3410 }
3411
3412 #[test]
3413 fn adf_critical_values_ordering() {
3414 let cv = adf_critical_values(AdfModel::Constant, 50);
3415 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3417 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3418 }
3419}
3420
3421#[cfg(test)]
3422mod proptests {
3423 use super::*;
3424 use proptest::prelude::*;
3425
3426 proptest! {
3427 #[test]
3428 fn one_sample_p_bounded(
3429 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3430 mu0 in -1e3_f64..1e3
3431 ) {
3432 if let Some(r) = one_sample_t_test(&data, mu0) {
3433 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3434 }
3435 }
3436
3437 #[test]
3438 fn two_sample_p_bounded(
3439 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3440 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3441 ) {
3442 if let Some(r) = two_sample_t_test(&a, &b) {
3443 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3444 }
3445 }
3446
3447 #[test]
3448 fn anova_p_bounded(
3449 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3450 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3451 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3452 ) {
3453 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3454 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3455 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3456 }
3457 }
3458
3459 #[test]
3460 fn bonferroni_monotone(
3461 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3462 ) {
3463 let adj = bonferroni_correction(&ps).expect("should compute");
3464 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3465 prop_assert!(adjusted >= orig - 1e-15,
3466 "adj[{i}] = {adjusted} < orig = {orig}");
3467 }
3468 }
3469
3470 #[test]
3471 fn shapiro_wilk_p_bounded(
3472 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3473 ) {
3474 if let Some(r) = shapiro_wilk_test(&data) {
3475 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3476 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3477 }
3478 }
3479
3480 #[test]
3481 fn anderson_darling_p_bounded(
3482 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3483 ) {
3484 if let Some(r) = anderson_darling_test(&data) {
3485 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3486 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3487 }
3488 }
3489
3490 #[test]
3491 fn mann_whitney_p_bounded(
3492 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3493 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3494 ) {
3495 if let Some(r) = mann_whitney_u_test(&a, &b) {
3496 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3497 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3498 }
3499 }
3500
3501 #[test]
3502 fn wilcoxon_p_bounded(
3503 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3504 ) {
3505 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3506 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3507 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3508 }
3509 }
3510
3511 #[test]
3512 fn bartlett_p_bounded(
3513 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3514 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3515 ) {
3516 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3517 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3518 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3519 }
3520 }
3521
3522 #[test]
3523 fn fisher_p_bounded(
3524 a in 0_u64..20,
3525 b in 0_u64..20,
3526 c in 0_u64..20,
3527 d in 0_u64..20,
3528 ) {
3529 if let Some(r) = fisher_exact_test(a, b, c, d) {
3530 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3531 }
3532 }
3533
3534 #[test]
3535 fn mann_kendall_p_bounded(
3536 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3537 ) {
3538 if let Some(r) = mann_kendall_test(&data) {
3539 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3540 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3541 "tau = {}", r.kendall_tau);
3542 }
3543 }
3544
3545 #[test]
3546 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3547 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3548 let r = mann_kendall_test(&data).expect("should compute");
3549 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3550 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3551 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3552 }
3553 }
3554}