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> {
484 let n = data.len();
485 if n < 8 {
486 return None;
487 }
488 if data.iter().any(|v| !v.is_finite()) {
489 return None;
490 }
491
492 let s = stats::skewness(data)?;
493 let k = stats::kurtosis(data)?;
494
495 let nf = n as f64;
496 let jb = (nf / 6.0) * (s * s + k * k / 4.0);
497 let p_value = 1.0 - special::chi_squared_cdf(jb, 2.0);
498
499 Some(TestResult {
500 statistic: jb,
501 df: 2.0,
502 p_value,
503 })
504}
505
506#[derive(Debug, Clone, Copy)]
508pub struct AndersonDarlingResult {
509 pub statistic: f64,
511 pub statistic_star: f64,
513 pub p_value: f64,
515}
516
517fn ad_upper_tail_pvalue(a2_star: f64) -> f64 {
537 const UPPER_BRANCH_VERTEX: f64 = 153.467_741_935_483_87; let p = if a2_star >= 0.6 {
542 let x = a2_star.min(UPPER_BRANCH_VERTEX);
543 (1.2937 - 5.709 * x + 0.0186 * x * x).exp()
544 } else if a2_star > 0.34 {
545 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
546 } else if a2_star > 0.2 {
547 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
548 } else {
549 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
550 };
551 p.clamp(0.0, 1.0)
552}
553
554pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
587 let n = data.len();
588 if n < 8 {
589 return None;
590 }
591 if data.iter().any(|v| !v.is_finite()) {
592 return None;
593 }
594
595 let mean = stats::mean(data)?;
596 let sd = stats::std_dev(data)?;
597
598 if sd < 1e-300 {
599 return None; }
601
602 let mut x: Vec<f64> = data.to_vec();
604 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
605
606 let nf = n as f64;
607
608 let mut s = 0.0;
610 for i in 0..n {
611 let z = (x[i] - mean) / sd;
612 let z_rev = (x[n - 1 - i] - mean) / sd;
613 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
617 let ln_sf_rev = special::standard_normal_sf(z_rev)
618 .max(f64::MIN_POSITIVE)
619 .ln();
620
621 let coeff = (2 * (i + 1) - 1) as f64;
622 s += coeff * (ln_phi + ln_sf_rev);
623 }
624
625 let a2 = -nf - s / nf;
626
627 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
629
630 let p = ad_upper_tail_pvalue(a2_star);
633
634 Some(AndersonDarlingResult {
635 statistic: a2,
636 statistic_star: a2_star,
637 p_value: p,
638 })
639}
640
641#[derive(Debug, Clone, Copy)]
647pub struct AdNormalityResult {
648 pub statistic: f64,
650 pub statistic_modified: f64,
652 pub p_value: f64,
654}
655
656pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
690 let n = data.len();
691 if n < 3 {
692 return None;
693 }
694 if data.iter().any(|v| !v.is_finite()) {
695 return None;
696 }
697
698 let mean = stats::mean(data)?;
699 let sd = stats::std_dev(data)?;
700
701 if sd < 1e-15 {
702 return None;
703 }
704
705 let mut x: Vec<f64> = data.to_vec();
706 x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
707
708 let nf = n as f64;
709
710 let mut s = 0.0;
711 for i in 0..n {
712 let z_i = (x[i] - mean) / sd;
713 let z_rev = (x[n - 1 - i] - mean) / sd;
714
715 let ln_phi_i = special::standard_normal_cdf(z_i)
716 .max(f64::MIN_POSITIVE)
717 .ln();
718 let ln_sf_rev = special::standard_normal_sf(z_rev)
719 .max(f64::MIN_POSITIVE)
720 .ln();
721
722 let coeff = (2 * i + 1) as f64;
723 s += coeff * (ln_phi_i + ln_sf_rev);
724 }
725
726 let a2 = -nf - s / nf;
727 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
728
729 let p = ad_upper_tail_pvalue(a2_star);
733
734 Some(AdNormalityResult {
735 statistic: a2,
736 statistic_modified: a2_star,
737 p_value: p,
738 })
739}
740
741#[derive(Debug, Clone, Copy)]
743pub struct ShapiroWilkResult {
744 pub w: f64,
746 pub p_value: f64,
748}
749
750pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
790 let n = data.len();
791 if !(3..=5000).contains(&n) {
792 return None;
793 }
794 if data.iter().any(|v| !v.is_finite()) {
795 return None;
796 }
797
798 let mut x: Vec<f64> = data.to_vec();
800 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
801
802 if x[n - 1] - x[0] < 1e-300 {
803 return None; }
805
806 let nn2 = n / 2;
807
808 if n == 3 {
810 return shapiro_wilk_n3(&x);
811 }
812
813 let a = sw_coefficients(n, nn2)?;
815
816 let w = sw_statistic(&x, &a, n, nn2);
818
819 if !(0.0..=1.0 + 1e-10).contains(&w) {
820 return None;
821 }
822 let w = w.min(1.0);
823
824 let p_value = sw_p_value(w, n);
826
827 Some(ShapiroWilkResult {
828 w,
829 p_value: p_value.clamp(0.0, 1.0),
830 })
831}
832
833fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
835 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
838 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
839 if ss < 1e-300 {
840 return None;
841 }
842
843 let numerator = a1 * (x[2] - x[0]);
844 let w = (numerator * numerator) / ss;
845 let w = w.clamp(0.75, 1.0);
846
847 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
849 let p = p.clamp(0.0, 1.0);
850
851 Some(ShapiroWilkResult { w, p_value: p })
852}
853
854const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
856const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
857const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
858const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
859const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
860const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
861const SW_G: [f64; 2] = [-2.273, 0.459];
862
863fn sw_poly(c: &[f64], x: f64) -> f64 {
865 let mut result = c[c.len() - 1];
866 for i in (0..c.len() - 1).rev() {
867 result = result * x + c[i];
868 }
869 result
870}
871
872fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
874 let mut a = vec![0.0; nn2];
875
876 let mut m = vec![0.0; nn2];
878 let mut summ2 = 0.0;
879 for (i, mi) in m.iter_mut().enumerate() {
880 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
882 *mi = special::inverse_normal_cdf(p);
883 summ2 += *mi * *mi;
884 }
885 summ2 *= 2.0;
886 let ssumm2 = summ2.sqrt();
887 let rsn = 1.0 / (n as f64).sqrt();
888
889 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
891
892 if n <= 5 {
893 let fac_sq = summ2 - 2.0 * m[0] * m[0];
895 let one_minus = 1.0 - 2.0 * a1 * a1;
896 if fac_sq <= 0.0 || one_minus <= 0.0 {
897 return None;
898 }
899 let fac = (fac_sq / one_minus).sqrt();
900 a[0] = a1;
901 for i in 1..nn2 {
902 a[i] = -m[i] / fac;
903 }
904 } else {
905 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
907 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
908 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
909 if fac_sq <= 0.0 || one_minus <= 0.0 {
910 return None;
911 }
912 let fac = (fac_sq / one_minus).sqrt();
913 a[0] = a1;
914 a[1] = a2;
915 for i in 2..nn2 {
916 a[i] = -m[i] / fac;
917 }
918 }
919
920 Some(a)
921}
922
923fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
925 let mut sa = 0.0;
927 for i in 0..nn2 {
928 sa += a[i] * (x[n - 1 - i] - x[i]);
929 }
930
931 let mean = x.iter().sum::<f64>() / n as f64;
933 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
934
935 if ss < 1e-300 {
936 return 1.0; }
938
939 (sa * sa) / ss
940}
941
942fn sw_p_value(w: f64, n: usize) -> f64 {
944 let nf = n as f64;
945
946 if n == 3 {
947 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
949 return p.clamp(0.0, 1.0);
950 }
951
952 let w1 = 1.0 - w;
953 if w1 <= 0.0 {
954 return 1.0; }
956
957 let y = w1.ln();
958
959 if n <= 11 {
960 let gamma = sw_poly(&SW_G, nf);
962 if y >= gamma {
963 return 0.0; }
965 let y2 = -(gamma - y).ln();
966 let m = sw_poly(&SW_C3, nf);
967 let s = sw_poly(&SW_C4, nf).exp();
968 if s < 1e-300 {
969 return 0.0;
970 }
971 let z = (y2 - m) / s;
972 special::standard_normal_sf(z)
973 } else {
974 let xx = nf.ln();
976 let m = sw_poly(&SW_C5, xx);
977 let s = sw_poly(&SW_C6, xx).exp();
978 if s < 1e-300 {
979 return 0.0;
980 }
981 let z = (y - m) / s;
982 special::standard_normal_sf(z)
983 }
984}
985
986pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
1023 let n1 = a.len();
1024 let n2 = b.len();
1025 if n1 < 2 || n2 < 2 {
1026 return None;
1027 }
1028 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1029 return None;
1030 }
1031
1032 let n = n1 + n2;
1033 let n1f = n1 as f64;
1034 let n2f = n2 as f64;
1035 let nf = n as f64;
1036
1037 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1039 for &v in a {
1040 combined.push((v, 0)); }
1042 for &v in b {
1043 combined.push((v, 1)); }
1045 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1046
1047 let ranks = average_ranks(&combined);
1049
1050 let r1: f64 = combined
1052 .iter()
1053 .zip(ranks.iter())
1054 .filter(|((_, g), _)| *g == 0)
1055 .map(|(_, &r)| r)
1056 .sum();
1057
1058 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1060
1061 let tie_correction = compute_tie_correction(&combined);
1063
1064 let mu = n1f * n2f / 2.0;
1066 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1067
1068 if sigma_sq <= 0.0 {
1069 return None;
1070 }
1071
1072 let z = (u1 - mu) / sigma_sq.sqrt();
1073 let p_value = 2.0 * special::standard_normal_sf(z.abs());
1074
1075 Some(TestResult {
1076 statistic: u1,
1077 df: 0.0, p_value,
1079 })
1080}
1081
1082pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1116 if x.len() != y.len() || x.len() < 2 {
1117 return None;
1118 }
1119 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1120 return None;
1121 }
1122
1123 let diffs: Vec<f64> = x
1125 .iter()
1126 .zip(y.iter())
1127 .map(|(&a, &b)| a - b)
1128 .filter(|&d| d.abs() > 1e-300)
1129 .collect();
1130
1131 let nr = diffs.len();
1132 if nr < 2 {
1133 return None;
1134 }
1135
1136 let nf = nr as f64;
1137
1138 let mut abs_diffs: Vec<(f64, usize)> = diffs
1140 .iter()
1141 .enumerate()
1142 .map(|(i, &d)| (d.abs(), i))
1143 .collect();
1144 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1145
1146 let ranks = average_ranks(&abs_diffs);
1148
1149 let t_plus: f64 = abs_diffs
1151 .iter()
1152 .zip(ranks.iter())
1153 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1154 .map(|(_, &r)| r)
1155 .sum();
1156
1157 let tie_correction_val = compute_tie_correction(&abs_diffs);
1159
1160 let mu = nf * (nf + 1.0) / 4.0;
1162 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1163
1164 if sigma_sq <= 0.0 {
1165 return None;
1166 }
1167
1168 let z = (t_plus - mu) / sigma_sq.sqrt();
1169 let p_value = 2.0 * special::standard_normal_sf(z.abs());
1170
1171 Some(TestResult {
1172 statistic: t_plus,
1173 df: 0.0,
1174 p_value,
1175 })
1176}
1177
1178fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1181 let n = sorted.len();
1182 let mut ranks = vec![0.0; n];
1183 let mut i = 0;
1184 while i < n {
1185 let mut j = i + 1;
1186 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1187 j += 1;
1188 }
1189 let avg_rank = (i + 1 + j) as f64 / 2.0;
1191 for rank in ranks.iter_mut().take(j).skip(i) {
1192 *rank = avg_rank;
1193 }
1194 i = j;
1195 }
1196 ranks
1197}
1198
1199fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1201 let n = sorted.len();
1202 let mut correction = 0.0;
1203 let mut i = 0;
1204 while i < n {
1205 let mut j = i + 1;
1206 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1207 j += 1;
1208 }
1209 let t = (j - i) as f64;
1210 if t > 1.0 {
1211 correction += t * (t * t - 1.0);
1212 }
1213 i = j;
1214 }
1215 correction
1216}
1217
1218pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1250 let k = groups.len();
1251 if k < 2 {
1252 return None;
1253 }
1254 for g in groups {
1255 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1256 return None;
1257 }
1258 }
1259
1260 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1261 let nf = total_n as f64;
1262
1263 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1265 for (gi, g) in groups.iter().enumerate() {
1266 for &v in *g {
1267 combined.push((v, gi));
1268 }
1269 }
1270 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1271
1272 let ranks = average_ranks(&combined);
1273
1274 let mut rank_sums = vec![0.0; k];
1276 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1277 rank_sums[*gi] += r;
1278 }
1279
1280 let mean_rank = (nf + 1.0) / 2.0;
1282 let mut h = 0.0;
1283 for (gi, g) in groups.iter().enumerate() {
1284 let ni = g.len() as f64;
1285 let mean_rank_i = rank_sums[gi] / ni;
1286 h += ni * (mean_rank_i - mean_rank).powi(2);
1287 }
1288 h *= 12.0 / (nf * (nf + 1.0));
1289
1290 let tie_corr = compute_tie_correction(&combined);
1292 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1293 if denom > 1e-15 {
1294 h /= denom;
1295 }
1296
1297 let df = (k - 1) as f64;
1298 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1299
1300 Some(TestResult {
1301 statistic: h,
1302 df,
1303 p_value,
1304 })
1305}
1306
1307pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1343 let k = groups.len();
1344 if k < 2 {
1345 return None;
1346 }
1347 for g in groups {
1348 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1349 return None;
1350 }
1351 }
1352
1353 let z_groups: Vec<Vec<f64>> = groups
1355 .iter()
1356 .map(|g| {
1357 let median = stats::median(g).unwrap_or(0.0);
1358 g.iter().map(|&x| (x - median).abs()).collect()
1359 })
1360 .collect();
1361
1362 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1364 let anova = one_way_anova(&z_refs)?;
1365
1366 Some(TestResult {
1367 statistic: anova.f_statistic,
1368 df: anova.df_between as f64,
1369 p_value: anova.p_value,
1370 })
1371}
1372
1373pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1385 if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1386 return None;
1387 }
1388 let m = p_values.len() as f64;
1389 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1390}
1391
1392pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1411 let m = p_values.len();
1412 if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1413 return None;
1414 }
1415
1416 let mut indices: Vec<usize> = (0..m).collect();
1418 indices.sort_by(|&a, &b| {
1419 p_values[a]
1420 .partial_cmp(&p_values[b])
1421 .unwrap_or(std::cmp::Ordering::Equal)
1422 });
1423
1424 let mf = m as f64;
1425 let mut adjusted = vec![0.0; m];
1426
1427 let mut cummin = f64::INFINITY;
1429 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1430 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1432 cummin = cummin.min(adj);
1433 adjusted[orig_idx] = cummin;
1434 }
1435
1436 Some(adjusted)
1437}
1438
1439pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1476 let k = groups.len();
1477 if k < 2 {
1478 return None;
1479 }
1480
1481 let mut sizes = Vec::with_capacity(k);
1482 let mut vars = Vec::with_capacity(k);
1483 let mut n_total: usize = 0;
1484
1485 for g in groups {
1486 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1487 return None;
1488 }
1489 let n = g.len();
1490 let v = stats::variance(g)?;
1491 if v <= 0.0 {
1492 return None; }
1494 sizes.push(n);
1495 vars.push(v);
1496 n_total += n;
1497 }
1498
1499 let nk = n_total - k; if nk == 0 {
1501 return None;
1502 }
1503 let nk_f = nk as f64;
1504
1505 let s2_pooled: f64 = sizes
1507 .iter()
1508 .zip(vars.iter())
1509 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1510 .sum::<f64>()
1511 / nk_f;
1512
1513 if s2_pooled <= 0.0 {
1514 return None;
1515 }
1516
1517 let num = nk_f * s2_pooled.ln()
1519 - sizes
1520 .iter()
1521 .zip(vars.iter())
1522 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1523 .sum::<f64>();
1524
1525 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1527 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1528
1529 let statistic = num / c;
1530 let df = (k - 1) as f64;
1531 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1532
1533 Some(TestResult {
1534 statistic,
1535 df,
1536 p_value,
1537 })
1538}
1539
1540pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1585 let row1 = a + b;
1586 let row2 = c + d;
1587 let col1 = a + c;
1588 let col2 = b + d;
1589 let n = a + b + c + d;
1590
1591 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1593 return None;
1594 }
1595
1596 let log_prob = |a_i: u64| -> f64 {
1598 let b_i = row1 - a_i;
1599 let c_i = col1 - a_i;
1600 let d_i = row2 - c_i;
1601 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1602 - ln_factorial(a_i)
1603 - ln_factorial(b_i)
1604 - ln_factorial(c_i)
1605 - ln_factorial(d_i)
1606 - ln_factorial(n)
1607 };
1608
1609 let a_min = col1.saturating_sub(row2);
1611 let a_max = row1.min(col1);
1612
1613 let log_p_obs = log_prob(a);
1614
1615 let mut p_value = 0.0;
1617 for a_i in a_min..=a_max {
1618 let lp = log_prob(a_i);
1619 if lp <= log_p_obs + 1e-10 {
1621 p_value += lp.exp();
1622 }
1623 }
1624
1625 let p_value = p_value.min(1.0);
1627
1628 let odds_ratio = if b > 0 && c > 0 {
1630 (a as f64 * d as f64) / (b as f64 * c as f64)
1631 } else {
1632 f64::INFINITY
1633 };
1634
1635 Some(TestResult {
1636 statistic: odds_ratio,
1637 df: 1.0,
1638 p_value,
1639 })
1640}
1641
1642#[derive(Debug, Clone, Copy)]
1648pub struct MannKendallResult {
1649 pub s_statistic: i64,
1651 pub variance: f64,
1653 pub z_statistic: f64,
1655 pub p_value: f64,
1657 pub kendall_tau: f64,
1659 pub sen_slope: f64,
1661}
1662
1663pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1701 let n = data.len();
1702 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1703 return None;
1704 }
1705
1706 let mut s: i64 = 0;
1708 for i in 0..n - 1 {
1709 for j in (i + 1)..n {
1710 let diff = data[j] - data[i];
1711 if diff > 0.0 {
1712 s += 1;
1713 } else if diff < 0.0 {
1714 s -= 1;
1715 }
1716 }
1717 }
1718
1719 let mut sorted: Vec<f64> = data.to_vec();
1721 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1722
1723 let mut tie_correction: f64 = 0.0;
1724 let mut current_count: usize = 1;
1725 for i in 1..sorted.len() {
1726 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1727 current_count += 1;
1728 } else {
1729 if current_count > 1 {
1730 let t = current_count as f64;
1731 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1732 }
1733 current_count = 1;
1734 }
1735 }
1736 if current_count > 1 {
1737 let t = current_count as f64;
1738 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1739 }
1740
1741 let nf = n as f64;
1743 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1744
1745 if variance < 1e-300 {
1746 return None; }
1748
1749 let sigma = variance.sqrt();
1751 let z_statistic = if s > 0 {
1752 (s as f64 - 1.0) / sigma
1753 } else if s < 0 {
1754 (s as f64 + 1.0) / sigma
1755 } else {
1756 0.0
1757 };
1758
1759 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1761 let p_value = p_value.clamp(0.0, 1.0);
1762
1763 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1765
1766 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1768 for i in 0..n - 1 {
1769 for j in (i + 1)..n {
1770 let dx = (j - i) as f64;
1771 slopes.push((data[j] - data[i]) / dx);
1772 }
1773 }
1774 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1775 let m = slopes.len();
1776 let sen_slope = if m % 2 == 0 {
1777 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1778 } else {
1779 slopes[m / 2]
1780 };
1781
1782 Some(MannKendallResult {
1783 s_statistic: s,
1784 variance,
1785 z_statistic,
1786 p_value,
1787 kendall_tau,
1788 sen_slope,
1789 })
1790}
1791
1792fn ln_factorial(n: u64) -> f64 {
1794 if n <= 1 {
1795 return 0.0;
1796 }
1797 special::ln_gamma(n as f64 + 1.0)
1799}
1800
1801#[derive(Debug, Clone)]
1807pub struct AdfResult {
1808 pub statistic: f64,
1810 pub n_lags: usize,
1812 pub n_obs: usize,
1814 pub critical_values: [f64; 3],
1816 pub rejected: [bool; 3],
1818}
1819
1820#[derive(Debug, Clone, Copy)]
1822pub enum AdfModel {
1823 None,
1825 Constant,
1827 ConstantTrend,
1829}
1830
1831pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1867 let n = data.len();
1868 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1869 return None;
1870 }
1871
1872 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1874
1875 let best_lag = match max_lags {
1877 Some(p) => p, None => {
1879 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1881 let p_max = schwert.min(n / 3);
1882 select_adf_lag(data, &dy, model, p_max)
1884 }
1885 };
1886
1887 adf_ols(data, &dy, model, best_lag)
1889}
1890
1891fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1893 let mut best_aic = f64::INFINITY;
1894 let mut best_p = 0;
1895
1896 for p in 0..=p_max {
1897 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1898 if aic < best_aic {
1899 best_aic = aic;
1900 best_p = p;
1901 }
1902 }
1903 }
1904
1905 best_p
1906}
1907
1908#[allow(clippy::type_complexity)]
1912fn adf_build_matrix(
1913 data: &[f64],
1914 dy: &[f64],
1915 model: AdfModel,
1916 p: usize,
1917) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1918 let start = p + 1;
1919 if start >= dy.len() || dy.len() - start < 5 {
1920 return None;
1921 }
1922 let m = dy.len() - start;
1923
1924 let y_dep: Vec<f64> = dy[start..].to_vec();
1925
1926 let has_intercept = !matches!(model, AdfModel::None);
1928 let has_trend = matches!(model, AdfModel::ConstantTrend);
1929 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1930
1931 let mut x_data = Vec::with_capacity(m * ncols);
1933 let mut gamma_col = 0;
1934
1935 for i in 0..m {
1936 let t = start + i;
1937 if has_intercept {
1938 x_data.push(1.0); gamma_col = 1;
1940 }
1941 x_data.push(data[t]); if has_trend {
1943 x_data.push((t + 1) as f64); }
1945 for lag in 1..=p {
1946 x_data.push(dy[t - lag]);
1947 }
1948 }
1949
1950 Some((x_data, y_dep, m, ncols, gamma_col))
1951}
1952
1953fn adf_ols_core(
1957 x_data: &[f64],
1958 y: &[f64],
1959 m: usize,
1960 ncols: usize,
1961 gamma_col: usize,
1962) -> Option<(f64, f64, usize)> {
1963 let mut xtx = vec![0.0_f64; ncols * ncols];
1965 for i in 0..m {
1966 let row = &x_data[i * ncols..(i + 1) * ncols];
1967 for j in 0..ncols {
1968 for k in j..ncols {
1969 xtx[j * ncols + k] += row[j] * row[k];
1970 }
1971 }
1972 }
1973 for j in 0..ncols {
1975 for k in (j + 1)..ncols {
1976 xtx[k * ncols + j] = xtx[j * ncols + k];
1977 }
1978 }
1979
1980 let mut xty = vec![0.0_f64; ncols];
1982 for i in 0..m {
1983 let row = &x_data[i * ncols..(i + 1) * ncols];
1984 for j in 0..ncols {
1985 xty[j] += row[j] * y[i];
1986 }
1987 }
1988
1989 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
1991 for i in 0..ncols {
1992 for j in 0..ncols {
1993 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
1994 }
1995 augmented[i * (ncols + 1) + ncols] = xty[i];
1996 }
1997
1998 for col in 0..ncols {
1999 let mut max_row = col;
2001 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2002 for row in (col + 1)..ncols {
2003 let val = augmented[row * (ncols + 1) + col].abs();
2004 if val > max_val {
2005 max_val = val;
2006 max_row = row;
2007 }
2008 }
2009 if max_val < 1e-15 {
2010 return None; }
2012 if max_row != col {
2013 for j in 0..=ncols {
2014 let a = col * (ncols + 1) + j;
2015 let b = max_row * (ncols + 1) + j;
2016 augmented.swap(a, b);
2017 }
2018 }
2019
2020 let pivot = augmented[col * (ncols + 1) + col];
2021 for row in (col + 1)..ncols {
2022 let factor = augmented[row * (ncols + 1) + col] / pivot;
2023 for j in col..=ncols {
2024 let above = augmented[col * (ncols + 1) + j];
2025 augmented[row * (ncols + 1) + j] -= factor * above;
2026 }
2027 }
2028 }
2029
2030 let mut beta = vec![0.0_f64; ncols];
2032 for i in (0..ncols).rev() {
2033 let mut sum = augmented[i * (ncols + 1) + ncols];
2034 for j in (i + 1)..ncols {
2035 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2036 }
2037 beta[i] = sum / augmented[i * (ncols + 1) + i];
2038 }
2039
2040 let mut rss = 0.0;
2042 for i in 0..m {
2043 let row = &x_data[i * ncols..(i + 1) * ncols];
2044 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2045 let resid = y[i] - y_hat;
2046 rss += resid * resid;
2047 }
2048
2049 let df = m - ncols;
2051 if df == 0 {
2052 return None;
2053 }
2054 let mse = rss / df as f64;
2055
2056 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2059 for j in 0..ncols {
2060 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2061 }
2062 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2063 }
2064
2065 for col in 0..ncols {
2067 let mut max_row = col;
2068 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2069 for row in (col + 1)..ncols {
2070 let val = xtx_aug[row * 2 * ncols + col].abs();
2071 if val > max_val {
2072 max_val = val;
2073 max_row = row;
2074 }
2075 }
2076 if max_val < 1e-15 {
2077 return None;
2078 }
2079 if max_row != col {
2080 for j in 0..(2 * ncols) {
2081 let a = col * 2 * ncols + j;
2082 let b = max_row * 2 * ncols + j;
2083 xtx_aug.swap(a, b);
2084 }
2085 }
2086
2087 let pivot = xtx_aug[col * 2 * ncols + col];
2088 for j in 0..(2 * ncols) {
2089 xtx_aug[col * 2 * ncols + j] /= pivot;
2090 }
2091 for row in 0..ncols {
2092 if row == col {
2093 continue;
2094 }
2095 let factor = xtx_aug[row * 2 * ncols + col];
2096 for j in 0..(2 * ncols) {
2097 let above = xtx_aug[col * 2 * ncols + j];
2098 xtx_aug[row * 2 * ncols + j] -= factor * above;
2099 }
2100 }
2101 }
2102
2103 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2105 if var_gamma <= 0.0 {
2106 return None;
2107 }
2108 let se_gamma = var_gamma.sqrt();
2109 let t_gamma = beta[gamma_col] / se_gamma;
2110
2111 Some((t_gamma, rss, ncols))
2112}
2113
2114fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2116 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2117 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2118 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2119 Some((aic, m))
2120}
2121
2122fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2124 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2125 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2126
2127 let critical_values = adf_critical_values(model, m);
2128
2129 let rejected = [
2130 gamma_t <= critical_values[0],
2131 gamma_t <= critical_values[1],
2132 gamma_t <= critical_values[2],
2133 ];
2134
2135 Some(AdfResult {
2136 statistic: gamma_t,
2137 n_lags: p,
2138 n_obs: m,
2139 critical_values,
2140 rejected,
2141 })
2142}
2143
2144fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2148 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2153 AdfModel::None => (
2154 [-2.5658, -1.9393, -1.6156],
2155 [-1.960, -0.398, -0.181],
2156 [-10.04, 0.0, 0.0],
2157 ),
2158 AdfModel::Constant => (
2159 [-3.4336, -2.8621, -2.5671],
2160 [-5.999, -2.738, -1.438],
2161 [-29.25, -8.36, -4.48],
2162 ),
2163 AdfModel::ConstantTrend => (
2164 [-3.9638, -3.4126, -3.1279],
2165 [-8.353, -4.039, -2.418],
2166 [-47.44, -17.83, -7.58],
2167 ),
2168 };
2169
2170 let nf = n as f64;
2171 let inv_n = 1.0 / nf;
2172 let inv_n2 = inv_n * inv_n;
2173
2174 [
2175 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2176 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2177 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2178 ]
2179}
2180
2181#[cfg(test)]
2182mod tests {
2183 use super::*;
2184
2185 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2191 let mut a = seed;
2192 move || {
2193 a = a.wrapping_add(0x6d2b_79f5);
2194 let mut t = a;
2195 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2196 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2197 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2198 }
2199 }
2200
2201 #[test]
2207 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2208 let mut rng = mulberry32(0x5350_4331);
2209 let full: Vec<f64> = (0..20_000)
2210 .map(|_| {
2211 let u = rng();
2212 -(1.0 - u).ln() })
2214 .collect();
2215
2216 let mut prev_a2 = 0.0_f64;
2217 let mut prev_p = f64::INFINITY;
2218 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2219 let r = anderson_darling_normality(&full[..n]).expect("computes");
2220 assert!(
2222 r.statistic_modified > prev_a2,
2223 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2224 r.statistic_modified
2225 );
2226 assert!(
2228 r.p_value < 0.5,
2229 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2230 r.statistic_modified,
2231 r.p_value
2232 );
2233 assert!(
2235 r.p_value <= prev_p + 1e-12,
2236 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2237 r.p_value
2238 );
2239 prev_a2 = r.statistic_modified;
2240 prev_p = r.p_value;
2241 }
2242 }
2243
2244 #[test]
2247 fn ad_both_functions_immune_to_overflow() {
2248 let mut rng = mulberry32(0x0bad_c0de);
2249 let data: Vec<f64> = (0..9000).map(|_| -(1.0 - rng()).ln()).collect();
2250 let a = anderson_darling_test(&data).expect("computes");
2251 let b = anderson_darling_normality(&data).expect("computes");
2252 assert!(
2253 a.statistic_star > 300.0,
2254 "expected large A*², got {}",
2255 a.statistic_star
2256 );
2257 assert!(
2258 a.p_value < 0.5,
2259 "test() overflowed toward normal: p={}",
2260 a.p_value
2261 );
2262 assert!(
2263 b.p_value < 0.5,
2264 "normality() overflowed toward normal: p={}",
2265 b.p_value
2266 );
2267 }
2268
2269 #[test]
2274 fn one_sample_null_true() {
2275 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2276 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2277 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2278 }
2279
2280 #[test]
2281 fn one_sample_null_false() {
2282 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2283 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2284 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2285 }
2286
2287 #[test]
2288 fn one_sample_edge_cases() {
2289 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());
2292 }
2293
2294 #[test]
2299 fn two_sample_same_mean() {
2300 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2301 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2302 let r = two_sample_t_test(&a, &b).expect("should compute");
2303 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2304 }
2305
2306 #[test]
2307 fn two_sample_different_means() {
2308 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2309 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2310 let r = two_sample_t_test(&a, &b).expect("should compute");
2311 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2312 }
2313
2314 #[test]
2315 fn two_sample_different_sizes() {
2316 let a = [1.0, 2.0, 3.0];
2317 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2318 let r = two_sample_t_test(&a, &b).expect("should compute");
2319 assert!(r.p_value < 0.05);
2320 }
2321
2322 #[test]
2323 fn two_sample_edge_cases() {
2324 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2325 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2326 }
2327
2328 #[test]
2333 fn paired_no_difference() {
2334 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2335 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2336 let r = paired_t_test(&x, &y).expect("should compute");
2337 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2338 }
2339
2340 #[test]
2341 fn paired_significant_difference() {
2342 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2344 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2345 let r = paired_t_test(&before, &after).expect("should compute");
2346 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2347 assert!(r.statistic < 0.0); }
2349
2350 #[test]
2351 fn paired_edge_cases() {
2352 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2355
2356 #[test]
2361 fn anova_same_means() {
2362 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2363 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2364 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2365 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2366 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2367 }
2368
2369 #[test]
2370 fn anova_different_means() {
2371 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2372 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2373 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2374 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2375 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2376 assert!(r.df_between == 2);
2377 assert!(r.df_within == 12);
2378 }
2379
2380 #[test]
2381 fn anova_ss_decomposition() {
2382 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2383 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2384 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2385 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2387 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2388 assert!(
2389 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2390 "SS decomposition: {ss_total} vs {} + {}",
2391 r.ss_between,
2392 r.ss_within
2393 );
2394 }
2395
2396 #[test]
2397 fn anova_edge_cases() {
2398 let g1 = [1.0, 2.0, 3.0];
2399 assert!(one_way_anova(&[&g1]).is_none()); }
2401
2402 #[test]
2407 fn chi2_gof_uniform() {
2408 let observed = [25.0, 25.0, 25.0, 25.0];
2410 let expected = [25.0, 25.0, 25.0, 25.0];
2411 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2412 assert!((r.statistic).abs() < 1e-15);
2413 assert!((r.p_value - 1.0).abs() < 0.01);
2414 }
2415
2416 #[test]
2417 fn chi2_gof_significant() {
2418 let observed = [90.0, 10.0];
2419 let expected = [50.0, 50.0];
2420 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2421 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2422 }
2423
2424 #[test]
2425 fn chi2_gof_edge_cases() {
2426 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()); }
2430
2431 #[test]
2436 fn chi2_independence_significant() {
2437 let table = [30.0, 10.0, 10.0, 50.0];
2439 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2440 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2441 assert!((r.df - 1.0).abs() < 1e-10);
2442 }
2443
2444 #[test]
2445 fn chi2_independence_not_significant() {
2446 let table = [25.0, 25.0, 25.0, 25.0];
2448 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2449 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2450 }
2451
2452 #[test]
2453 fn chi2_independence_3x3() {
2454 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2455 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2456 assert!((r.df - 4.0).abs() < 1e-10);
2457 assert!(r.p_value < 0.05);
2458 }
2459
2460 #[test]
2461 fn chi2_independence_edge_cases() {
2462 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()); }
2466
2467 #[test]
2472 fn jb_normal_data() {
2473 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2475 let r = jarque_bera_test(&data).expect("should compute");
2476 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2477 }
2478
2479 #[test]
2480 fn jb_skewed_data() {
2481 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2483 let r = jarque_bera_test(&data).expect("should compute");
2484 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2485 }
2486
2487 #[test]
2488 fn jb_edge_cases() {
2489 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2491
2492 #[test]
2497 fn ad_normal_data() {
2498 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2499 let r = anderson_darling_test(&data).expect("should compute");
2500 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2501 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2502 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2503 }
2504
2505 #[test]
2506 fn ad_skewed_data() {
2507 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];
2509 let r = anderson_darling_test(&data).expect("should compute");
2510 assert!(
2511 r.p_value < 0.05,
2512 "p = {} (should reject normality)",
2513 r.p_value
2514 );
2515 }
2516
2517 #[test]
2518 fn ad_bimodal_data() {
2519 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2521 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2522 let r = anderson_darling_test(&data).expect("should compute");
2523 assert!(
2524 r.p_value < 0.01,
2525 "p = {} (bimodal should reject normality)",
2526 r.p_value
2527 );
2528 }
2529
2530 #[test]
2531 fn ad_large_normal_sample() {
2532 let n = 100;
2533 let data: Vec<f64> = (1..=n)
2534 .map(|i| {
2535 let p = (i as f64 - 0.5) / n as f64;
2536 special::inverse_normal_cdf(p)
2537 })
2538 .collect();
2539 let r = anderson_darling_test(&data).expect("should compute");
2540 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2541 }
2542
2543 #[test]
2544 fn ad_edge_cases() {
2545 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());
2548 }
2549
2550 #[test]
2551 fn ad_p_value_ranges() {
2552 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2557 let r = anderson_darling_test(&near_normal).expect("should compute");
2558 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2559
2560 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];
2562 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2563 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2564 }
2565
2566 #[test]
2571 fn ad_normal_data_large_p() {
2572 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2574 let r = anderson_darling_normality(&data).unwrap();
2575 assert!(r.p_value > 0.05, "p={}", r.p_value);
2576 }
2577
2578 #[test]
2579 fn ad_exponential_data_small_p() {
2580 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2582 let r = anderson_darling_normality(&data).unwrap();
2583 assert!(r.p_value < 0.05, "p={}", r.p_value);
2584 }
2585
2586 #[test]
2587 fn ad_statistic_non_negative() {
2588 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2589 let r = anderson_darling_normality(&data).unwrap();
2590 assert!(r.statistic >= 0.0);
2591 assert!(r.statistic_modified >= r.statistic);
2592 }
2593
2594 #[test]
2595 fn ad_p_value_in_range() {
2596 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2597 let r = anderson_darling_normality(&data).unwrap();
2598 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2599 }
2600
2601 #[test]
2602 fn ad_insufficient_data() {
2603 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2605
2606 #[test]
2607 fn ad_degenerate_data() {
2608 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2610 }
2611
2612 #[test]
2613 fn ad_modified_statistic_formula() {
2614 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2616 let r = anderson_darling_normality(&data).unwrap();
2617 assert!(r.statistic_modified > r.statistic - 1e-10);
2618 }
2619
2620 #[test]
2625 fn ad_correction_factor_formula() {
2626 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2627 let n = data.len() as f64;
2628 let r = anderson_darling_normality(&data).unwrap();
2629
2630 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2632 assert!(
2633 (r.statistic_modified - expected_star).abs() < 1e-12,
2634 "A²* = {}, expected = {}",
2635 r.statistic_modified,
2636 expected_star
2637 );
2638
2639 assert!(
2641 r.statistic >= 0.0,
2642 "A² = {} must be non-negative",
2643 r.statistic
2644 );
2645 }
2646
2647 #[test]
2653 fn ad_formula_structure_symmetric_vs_skewed() {
2654 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2656 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2658
2659 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2660 let r_skew = anderson_darling_normality(&skewed).unwrap();
2661
2662 assert!(
2663 r_sym.statistic < r_skew.statistic,
2664 "Symmetric A²={} should be less than skewed A²={}",
2665 r_sym.statistic,
2666 r_skew.statistic
2667 );
2668 }
2669
2670 #[test]
2675 fn ad_pvalue_invariants() {
2676 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2678 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2679 assert!(
2680 r_normal.p_value > 0.05,
2681 "Normal data: p = {} (expected > 0.05)",
2682 r_normal.p_value
2683 );
2684
2685 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2687 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2688 assert!(
2689 r_exp.p_value < 0.05,
2690 "Exponential data: p = {} (expected < 0.05)",
2691 r_exp.p_value
2692 );
2693 }
2694
2695 #[test]
2700 fn sw_normal_data() {
2701 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2703 let r = shapiro_wilk_test(&data).expect("should compute");
2704 assert!(r.w > 0.9, "W = {}", r.w);
2705 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2706 }
2707
2708 #[test]
2709 fn sw_bimodal_data() {
2710 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2712 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2713 let r = shapiro_wilk_test(&data).expect("should compute");
2714 assert!(
2715 r.p_value < 0.01,
2716 "p = {} (bimodal should reject normality)",
2717 r.p_value
2718 );
2719 }
2720
2721 #[test]
2722 fn sw_n3() {
2723 let data = [1.0, 2.0, 3.0];
2724 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2725 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2726 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2727 }
2728
2729 #[test]
2730 fn sw_n4() {
2731 let data = [1.0, 2.0, 3.0, 4.0];
2732 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2733 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2734 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2735 }
2736
2737 #[test]
2738 fn sw_n5() {
2739 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2740 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2741 assert!(r.w > 0.9, "W = {}", r.w);
2742 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2743 }
2744
2745 #[test]
2746 fn sw_skewed_data() {
2747 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];
2749 let r = shapiro_wilk_test(&data).expect("should compute");
2750 assert!(
2751 r.p_value < 0.05,
2752 "p = {} (skewed data should reject normality)",
2753 r.p_value
2754 );
2755 }
2756
2757 #[test]
2758 fn sw_large_normal_sample() {
2759 let n = 100;
2762 let data: Vec<f64> = (1..=n)
2763 .map(|i| {
2764 let p = (i as f64 - 0.5) / n as f64;
2765 special::inverse_normal_cdf(p)
2766 })
2767 .collect();
2768 let r = shapiro_wilk_test(&data).expect("should compute");
2769 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2770 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2771 }
2772
2773 #[test]
2774 fn sw_w_bounded() {
2775 let datasets: Vec<Vec<f64>> = vec![
2777 vec![1.0, 2.0, 3.0],
2778 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2779 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2780 (0..20).map(|i| (i as f64).powi(2)).collect(),
2781 ];
2782 for (idx, data) in datasets.iter().enumerate() {
2783 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2784 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2785 assert!(
2786 r.p_value >= 0.0 && r.p_value <= 1.0,
2787 "dataset {idx}: p = {}",
2788 r.p_value
2789 );
2790 }
2791 }
2792
2793 #[test]
2794 fn sw_edge_cases() {
2795 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()); }
2801
2802 #[test]
2803 fn sw_n5001_rejected() {
2804 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2805 assert!(shapiro_wilk_test(&data).is_none()); }
2807
2808 #[test]
2813 fn mw_clearly_different() {
2814 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2815 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2816 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2817 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2818 }
2819
2820 #[test]
2821 fn mw_same_distribution() {
2822 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2823 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2824 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2825 assert!(
2826 r.p_value > 0.3,
2827 "p = {} (interleaved, same dist)",
2828 r.p_value
2829 );
2830 }
2831
2832 #[test]
2833 fn mw_with_ties() {
2834 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2835 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2836 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2837 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2838 }
2839
2840 #[test]
2841 fn mw_different_sizes() {
2842 let a = [1.0, 2.0, 3.0];
2843 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2844 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2845 assert!(r.p_value < 0.05);
2846 }
2847
2848 #[test]
2849 fn mw_edge_cases() {
2850 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());
2853 }
2854
2855 #[test]
2860 fn wilcoxon_significant_increase() {
2861 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2862 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2863 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2864 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2865 }
2866
2867 #[test]
2868 fn wilcoxon_no_difference() {
2869 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2870 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2871 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2872 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2873 }
2874
2875 #[test]
2876 fn wilcoxon_with_ties() {
2877 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2879 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2881 assert!(r.statistic > 0.0);
2883 }
2884
2885 #[test]
2886 fn wilcoxon_with_zero_diffs() {
2887 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2889 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2891 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2892 }
2893
2894 #[test]
2895 fn wilcoxon_edge_cases() {
2896 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());
2900 }
2901
2902 #[test]
2907 fn kw_clearly_different() {
2908 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2909 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2910 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2911 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2912 assert!(r.p_value < 0.01, "p = {}", r.p_value);
2913 assert!((r.df - 2.0).abs() < 1e-10);
2914 }
2915
2916 #[test]
2917 fn kw_same_distribution() {
2918 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2919 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2920 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2921 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2922 }
2923
2924 #[test]
2925 fn kw_with_ties() {
2926 let g1 = [1.0, 2.0, 2.0, 3.0];
2927 let g2 = [3.0, 4.0, 4.0, 5.0];
2928 let g3 = [5.0, 6.0, 6.0, 7.0];
2929 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2930 assert!(r.statistic > 0.0);
2931 }
2932
2933 #[test]
2934 fn kw_edge_cases() {
2935 let g1 = [1.0, 2.0, 3.0];
2936 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
2938
2939 #[test]
2944 fn levene_equal_variance() {
2945 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2946 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2947 let r = levene_test(&[&g1, &g2]).expect("should compute");
2948 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2950 }
2951
2952 #[test]
2953 fn levene_unequal_variance() {
2954 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");
2957 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2958 }
2959
2960 #[test]
2961 fn levene_three_groups() {
2962 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2963 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2964 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2965 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2966 assert!(r.df >= 2.0);
2967 }
2968
2969 #[test]
2970 fn levene_edge_cases() {
2971 let g1 = [1.0, 2.0, 3.0];
2972 assert!(levene_test(&[&g1]).is_none()); }
2974
2975 #[test]
2980 fn bonferroni_basic() {
2981 let ps = [0.01, 0.04, 0.03, 0.005];
2982 let adj = bonferroni_correction(&ps).expect("should compute");
2983 assert!((adj[0] - 0.04).abs() < 1e-10);
2984 assert!((adj[1] - 0.16).abs() < 1e-10);
2985 assert!((adj[2] - 0.12).abs() < 1e-10);
2986 assert!((adj[3] - 0.02).abs() < 1e-10);
2987 }
2988
2989 #[test]
2990 fn bonferroni_capped_at_one() {
2991 let ps = [0.5, 0.6];
2992 let adj = bonferroni_correction(&ps).expect("should compute");
2993 assert!((adj[0] - 1.0).abs() < 1e-10);
2994 assert!((adj[1] - 1.0).abs() < 1e-10);
2995 }
2996
2997 #[test]
2998 fn bh_basic() {
2999 let ps = [0.01, 0.04, 0.03, 0.005];
3000 let adj = benjamini_hochberg(&ps).expect("should compute");
3001 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3003 assert!(
3004 adjusted >= orig - 1e-15,
3005 "adj[{i}] = {adjusted} < original {orig}"
3006 );
3007 }
3008 }
3011
3012 #[test]
3013 fn bh_all_significant() {
3014 let ps = [0.001, 0.002, 0.003];
3015 let adj = benjamini_hochberg(&ps).expect("should compute");
3016 for &a in &adj {
3017 assert!(a < 0.05);
3018 }
3019 }
3020
3021 #[test]
3022 fn correction_edge_cases() {
3023 assert!(bonferroni_correction(&[]).is_none());
3024 assert!(benjamini_hochberg(&[]).is_none());
3025 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3026 }
3027
3028 #[test]
3033 fn bartlett_equal_variances() {
3034 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3035 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3036 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3037 assert!(
3038 r.p_value > 0.9,
3039 "equal variance → p high, got {}",
3040 r.p_value
3041 );
3042 }
3043
3044 #[test]
3045 fn bartlett_unequal_variances() {
3046 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");
3049 assert!(
3050 r.p_value < 0.01,
3051 "very different variances → p < 0.01, got {}",
3052 r.p_value
3053 );
3054 assert!((r.df - 1.0).abs() < 1e-10); }
3056
3057 #[test]
3058 fn bartlett_three_groups() {
3059 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3060 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3061 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3063 assert!(r.p_value < 0.05, "one group with high variance");
3064 assert!((r.df - 2.0).abs() < 1e-10); }
3066
3067 #[test]
3068 fn bartlett_edge_cases() {
3069 let g1 = [1.0, 2.0, 3.0];
3070 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3074
3075 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3077 }
3078
3079 #[test]
3084 fn fisher_tea_tasting() {
3085 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3087 assert!(
3089 (r.p_value - 0.4857).abs() < 0.01,
3090 "p ≈ 0.4857, got {}",
3091 r.p_value
3092 );
3093 }
3094
3095 #[test]
3096 fn fisher_significant() {
3097 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3099 assert!(r.p_value < 0.001, "perfect association → p very small");
3100 }
3101
3102 #[test]
3103 fn fisher_no_association() {
3104 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3106 assert!(
3107 r.p_value > 0.9,
3108 "no association → p ≈ 1.0, got {}",
3109 r.p_value
3110 );
3111 }
3112
3113 #[test]
3114 fn fisher_small_table() {
3115 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3117 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3118 }
3119
3120 #[test]
3121 fn fisher_asymmetric() {
3122 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3124 assert!(r.p_value < 0.05, "significant association");
3125 }
3126
3127 #[test]
3128 fn fisher_edge_cases() {
3129 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()); }
3134
3135 #[test]
3136 fn fisher_odds_ratio() {
3137 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3138 assert!(
3140 (r.statistic - 9.0).abs() < 1e-10,
3141 "OR = 9, got {}",
3142 r.statistic
3143 );
3144 }
3145
3146 #[test]
3151 fn mk_increasing_trend() {
3152 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3153 let r = mann_kendall_test(&data).expect("should compute");
3154 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3155 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3156 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3157 assert!(r.s_statistic > 0);
3158 }
3159
3160 #[test]
3161 fn mk_decreasing_trend() {
3162 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3163 let r = mann_kendall_test(&data).expect("should compute");
3164 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3165 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3166 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3167 assert!(r.s_statistic < 0);
3168 }
3169
3170 #[test]
3171 fn mk_no_trend() {
3172 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3174 let r = mann_kendall_test(&data).expect("should compute");
3175 assert!(
3177 r.p_value > 0.05,
3178 "p = {} (should be > 0.05 for no trend)",
3179 r.p_value
3180 );
3181 }
3182
3183 #[test]
3184 fn mk_perfect_monotone() {
3185 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3186 let r = mann_kendall_test(&data).expect("should compute");
3187 assert_eq!(r.s_statistic, 45);
3189 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3190 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3191 }
3192
3193 #[test]
3194 fn mk_with_ties() {
3195 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3196 let r = mann_kendall_test(&data).expect("should compute");
3197 assert!(r.s_statistic > 0);
3198 let n = data.len() as f64;
3200 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3201 assert!(r.variance < base_var, "ties should reduce variance");
3202 }
3203
3204 #[test]
3205 fn mk_edge_cases() {
3206 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3208 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3210 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3212 }
3213
3214 #[test]
3215 fn mk_minimum_n() {
3216 let data = [1.0, 2.0, 3.0, 4.0];
3218 let r = mann_kendall_test(&data).expect("n=4 should work");
3219 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3221 }
3222
3223 #[test]
3224 fn mk_sen_slope_robust_to_outlier() {
3225 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3227 let r = mann_kendall_test(&data).expect("should compute");
3228 assert!(
3230 (r.sen_slope - 1.0).abs() < 0.5,
3231 "sen slope = {}, expected ≈ 1.0",
3232 r.sen_slope
3233 );
3234 }
3235
3236 #[test]
3241 fn adf_stationary_mean_reverting() {
3242 let data = [
3245 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,
3246 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,
3247 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,
3248 ];
3249 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3250 assert!(
3252 r.rejected[2],
3253 "should reject at 10%: stat={}, cv={}",
3254 r.statistic, r.critical_values[2]
3255 );
3256 }
3257
3258 #[test]
3259 fn adf_nonstationary_random_walk() {
3260 let increments = [
3262 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,
3263 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,
3264 -0.13, 0.15, -0.1,
3265 ];
3266 let mut walk = Vec::with_capacity(increments.len());
3267 let mut cum = 0.0;
3268 for &inc in &increments {
3269 cum += inc;
3270 walk.push(cum);
3271 }
3272 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3273 assert!(
3275 !r.rejected[0],
3276 "should NOT reject at 1%: stat={}, cv={}",
3277 r.statistic, r.critical_values[0]
3278 );
3279 }
3280
3281 #[test]
3282 fn adf_with_fixed_lags() {
3283 let data: Vec<f64> = (0..50)
3285 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3286 .collect();
3287 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3288 assert_eq!(r.n_lags, 2);
3289 assert!(r.statistic.is_finite());
3290 }
3291
3292 #[test]
3293 fn adf_constant_trend_model() {
3294 let data: Vec<f64> = (0..30)
3297 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3298 .collect();
3299 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3300 assert!(r.statistic.is_finite());
3301 assert_eq!(r.critical_values.len(), 3);
3302 }
3303
3304 #[test]
3305 fn adf_edge_cases() {
3306 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3308 let mut data = vec![0.0; 20];
3310 data[5] = f64::NAN;
3311 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3312 }
3313
3314 #[test]
3315 fn adf_critical_values_constant() {
3316 let cv = adf_critical_values(AdfModel::Constant, 100);
3318 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3320 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3321 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3322 }
3323
3324 #[test]
3325 fn adf_critical_values_ordering() {
3326 let cv = adf_critical_values(AdfModel::Constant, 50);
3327 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3329 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3330 }
3331}
3332
3333#[cfg(test)]
3334mod proptests {
3335 use super::*;
3336 use proptest::prelude::*;
3337
3338 proptest! {
3339 #[test]
3340 fn one_sample_p_bounded(
3341 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3342 mu0 in -1e3_f64..1e3
3343 ) {
3344 if let Some(r) = one_sample_t_test(&data, mu0) {
3345 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3346 }
3347 }
3348
3349 #[test]
3350 fn two_sample_p_bounded(
3351 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3352 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3353 ) {
3354 if let Some(r) = two_sample_t_test(&a, &b) {
3355 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3356 }
3357 }
3358
3359 #[test]
3360 fn anova_p_bounded(
3361 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3362 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3363 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3364 ) {
3365 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3366 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3367 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3368 }
3369 }
3370
3371 #[test]
3372 fn bonferroni_monotone(
3373 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3374 ) {
3375 let adj = bonferroni_correction(&ps).expect("should compute");
3376 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3377 prop_assert!(adjusted >= orig - 1e-15,
3378 "adj[{i}] = {adjusted} < orig = {orig}");
3379 }
3380 }
3381
3382 #[test]
3383 fn shapiro_wilk_p_bounded(
3384 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3385 ) {
3386 if let Some(r) = shapiro_wilk_test(&data) {
3387 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3388 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3389 }
3390 }
3391
3392 #[test]
3393 fn anderson_darling_p_bounded(
3394 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3395 ) {
3396 if let Some(r) = anderson_darling_test(&data) {
3397 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3398 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3399 }
3400 }
3401
3402 #[test]
3403 fn mann_whitney_p_bounded(
3404 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3405 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3406 ) {
3407 if let Some(r) = mann_whitney_u_test(&a, &b) {
3408 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3409 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3410 }
3411 }
3412
3413 #[test]
3414 fn wilcoxon_p_bounded(
3415 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3416 ) {
3417 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3418 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3419 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3420 }
3421 }
3422
3423 #[test]
3424 fn bartlett_p_bounded(
3425 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3426 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3427 ) {
3428 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3429 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3430 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3431 }
3432 }
3433
3434 #[test]
3435 fn fisher_p_bounded(
3436 a in 0_u64..20,
3437 b in 0_u64..20,
3438 c in 0_u64..20,
3439 d in 0_u64..20,
3440 ) {
3441 if let Some(r) = fisher_exact_test(a, b, c, d) {
3442 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3443 }
3444 }
3445
3446 #[test]
3447 fn mann_kendall_p_bounded(
3448 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3449 ) {
3450 if let Some(r) = mann_kendall_test(&data) {
3451 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3452 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3453 "tau = {}", r.kendall_tau);
3454 }
3455 }
3456
3457 #[test]
3458 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3459 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3460 let r = mann_kendall_test(&data).expect("should compute");
3461 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3462 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3463 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3464 }
3465 }
3466}