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 phi = special::standard_normal_cdf(z);
613 let phi = phi.clamp(1e-15, 1.0 - 1e-15);
615
616 let z_rev = (x[n - 1 - i] - mean) / sd;
617 let phi_rev = special::standard_normal_cdf(z_rev);
618 let phi_rev = phi_rev.clamp(1e-15, 1.0 - 1e-15);
619
620 let coeff = (2 * (i + 1) - 1) as f64;
621 s += coeff * (phi.ln() + (1.0 - phi_rev).ln());
622 }
623
624 let a2 = -nf - s / nf;
625
626 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
628
629 let p = ad_upper_tail_pvalue(a2_star);
632
633 Some(AndersonDarlingResult {
634 statistic: a2,
635 statistic_star: a2_star,
636 p_value: p,
637 })
638}
639
640#[derive(Debug, Clone, Copy)]
646pub struct AdNormalityResult {
647 pub statistic: f64,
649 pub statistic_modified: f64,
651 pub p_value: f64,
653}
654
655pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
689 let n = data.len();
690 if n < 3 {
691 return None;
692 }
693 if data.iter().any(|v| !v.is_finite()) {
694 return None;
695 }
696
697 let mean = stats::mean(data)?;
698 let sd = stats::std_dev(data)?;
699
700 if sd < 1e-15 {
701 return None;
702 }
703
704 let mut x: Vec<f64> = data.to_vec();
705 x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
706
707 let nf = n as f64;
708
709 let mut s = 0.0;
710 for i in 0..n {
711 let z_i = (x[i] - mean) / sd;
712 let z_rev = (x[n - 1 - i] - mean) / sd;
713
714 let phi_i = special::standard_normal_cdf(z_i).clamp(1e-15, 1.0 - 1e-15);
715 let phi_rev = special::standard_normal_cdf(z_rev).clamp(1e-15, 1.0 - 1e-15);
716
717 let coeff = (2 * i + 1) as f64;
718 s += coeff * (phi_i.ln() + (1.0 - phi_rev).ln());
719 }
720
721 let a2 = -nf - s / nf;
722 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
723
724 let p = ad_upper_tail_pvalue(a2_star);
728
729 Some(AdNormalityResult {
730 statistic: a2,
731 statistic_modified: a2_star,
732 p_value: p,
733 })
734}
735
736#[derive(Debug, Clone, Copy)]
738pub struct ShapiroWilkResult {
739 pub w: f64,
741 pub p_value: f64,
743}
744
745pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
785 let n = data.len();
786 if !(3..=5000).contains(&n) {
787 return None;
788 }
789 if data.iter().any(|v| !v.is_finite()) {
790 return None;
791 }
792
793 let mut x: Vec<f64> = data.to_vec();
795 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
796
797 if x[n - 1] - x[0] < 1e-300 {
798 return None; }
800
801 let nn2 = n / 2;
802
803 if n == 3 {
805 return shapiro_wilk_n3(&x);
806 }
807
808 let a = sw_coefficients(n, nn2)?;
810
811 let w = sw_statistic(&x, &a, n, nn2);
813
814 if !(0.0..=1.0 + 1e-10).contains(&w) {
815 return None;
816 }
817 let w = w.min(1.0);
818
819 let p_value = sw_p_value(w, n);
821
822 Some(ShapiroWilkResult {
823 w,
824 p_value: p_value.clamp(0.0, 1.0),
825 })
826}
827
828fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
830 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
833 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
834 if ss < 1e-300 {
835 return None;
836 }
837
838 let numerator = a1 * (x[2] - x[0]);
839 let w = (numerator * numerator) / ss;
840 let w = w.clamp(0.75, 1.0);
841
842 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
844 let p = p.clamp(0.0, 1.0);
845
846 Some(ShapiroWilkResult { w, p_value: p })
847}
848
849const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
851const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
852const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
853const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
854const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
855const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
856const SW_G: [f64; 2] = [-2.273, 0.459];
857
858fn sw_poly(c: &[f64], x: f64) -> f64 {
860 let mut result = c[c.len() - 1];
861 for i in (0..c.len() - 1).rev() {
862 result = result * x + c[i];
863 }
864 result
865}
866
867fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
869 let mut a = vec![0.0; nn2];
870
871 let mut m = vec![0.0; nn2];
873 let mut summ2 = 0.0;
874 for (i, mi) in m.iter_mut().enumerate() {
875 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
877 *mi = special::inverse_normal_cdf(p);
878 summ2 += *mi * *mi;
879 }
880 summ2 *= 2.0;
881 let ssumm2 = summ2.sqrt();
882 let rsn = 1.0 / (n as f64).sqrt();
883
884 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
886
887 if n <= 5 {
888 let fac_sq = summ2 - 2.0 * m[0] * m[0];
890 let one_minus = 1.0 - 2.0 * a1 * a1;
891 if fac_sq <= 0.0 || one_minus <= 0.0 {
892 return None;
893 }
894 let fac = (fac_sq / one_minus).sqrt();
895 a[0] = a1;
896 for i in 1..nn2 {
897 a[i] = -m[i] / fac;
898 }
899 } else {
900 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
902 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
903 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
904 if fac_sq <= 0.0 || one_minus <= 0.0 {
905 return None;
906 }
907 let fac = (fac_sq / one_minus).sqrt();
908 a[0] = a1;
909 a[1] = a2;
910 for i in 2..nn2 {
911 a[i] = -m[i] / fac;
912 }
913 }
914
915 Some(a)
916}
917
918fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
920 let mut sa = 0.0;
922 for i in 0..nn2 {
923 sa += a[i] * (x[n - 1 - i] - x[i]);
924 }
925
926 let mean = x.iter().sum::<f64>() / n as f64;
928 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
929
930 if ss < 1e-300 {
931 return 1.0; }
933
934 (sa * sa) / ss
935}
936
937fn sw_p_value(w: f64, n: usize) -> f64 {
939 let nf = n as f64;
940
941 if n == 3 {
942 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
944 return p.clamp(0.0, 1.0);
945 }
946
947 let w1 = 1.0 - w;
948 if w1 <= 0.0 {
949 return 1.0; }
951
952 let y = w1.ln();
953
954 if n <= 11 {
955 let gamma = sw_poly(&SW_G, nf);
957 if y >= gamma {
958 return 0.0; }
960 let y2 = -(gamma - y).ln();
961 let m = sw_poly(&SW_C3, nf);
962 let s = sw_poly(&SW_C4, nf).exp();
963 if s < 1e-300 {
964 return 0.0;
965 }
966 let z = (y2 - m) / s;
967 1.0 - special::standard_normal_cdf(z)
968 } else {
969 let xx = nf.ln();
971 let m = sw_poly(&SW_C5, xx);
972 let s = sw_poly(&SW_C6, xx).exp();
973 if s < 1e-300 {
974 return 0.0;
975 }
976 let z = (y - m) / s;
977 1.0 - special::standard_normal_cdf(z)
978 }
979}
980
981pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
1018 let n1 = a.len();
1019 let n2 = b.len();
1020 if n1 < 2 || n2 < 2 {
1021 return None;
1022 }
1023 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1024 return None;
1025 }
1026
1027 let n = n1 + n2;
1028 let n1f = n1 as f64;
1029 let n2f = n2 as f64;
1030 let nf = n as f64;
1031
1032 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1034 for &v in a {
1035 combined.push((v, 0)); }
1037 for &v in b {
1038 combined.push((v, 1)); }
1040 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1041
1042 let ranks = average_ranks(&combined);
1044
1045 let r1: f64 = combined
1047 .iter()
1048 .zip(ranks.iter())
1049 .filter(|((_, g), _)| *g == 0)
1050 .map(|(_, &r)| r)
1051 .sum();
1052
1053 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1055
1056 let tie_correction = compute_tie_correction(&combined);
1058
1059 let mu = n1f * n2f / 2.0;
1061 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1062
1063 if sigma_sq <= 0.0 {
1064 return None;
1065 }
1066
1067 let z = (u1 - mu) / sigma_sq.sqrt();
1068 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1069
1070 Some(TestResult {
1071 statistic: u1,
1072 df: 0.0, p_value,
1074 })
1075}
1076
1077pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1111 if x.len() != y.len() || x.len() < 2 {
1112 return None;
1113 }
1114 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1115 return None;
1116 }
1117
1118 let diffs: Vec<f64> = x
1120 .iter()
1121 .zip(y.iter())
1122 .map(|(&a, &b)| a - b)
1123 .filter(|&d| d.abs() > 1e-300)
1124 .collect();
1125
1126 let nr = diffs.len();
1127 if nr < 2 {
1128 return None;
1129 }
1130
1131 let nf = nr as f64;
1132
1133 let mut abs_diffs: Vec<(f64, usize)> = diffs
1135 .iter()
1136 .enumerate()
1137 .map(|(i, &d)| (d.abs(), i))
1138 .collect();
1139 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1140
1141 let ranks = average_ranks(&abs_diffs);
1143
1144 let t_plus: f64 = abs_diffs
1146 .iter()
1147 .zip(ranks.iter())
1148 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1149 .map(|(_, &r)| r)
1150 .sum();
1151
1152 let tie_correction_val = compute_tie_correction(&abs_diffs);
1154
1155 let mu = nf * (nf + 1.0) / 4.0;
1157 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1158
1159 if sigma_sq <= 0.0 {
1160 return None;
1161 }
1162
1163 let z = (t_plus - mu) / sigma_sq.sqrt();
1164 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z.abs()));
1165
1166 Some(TestResult {
1167 statistic: t_plus,
1168 df: 0.0,
1169 p_value,
1170 })
1171}
1172
1173fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1176 let n = sorted.len();
1177 let mut ranks = vec![0.0; n];
1178 let mut i = 0;
1179 while i < n {
1180 let mut j = i + 1;
1181 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1182 j += 1;
1183 }
1184 let avg_rank = (i + 1 + j) as f64 / 2.0;
1186 for rank in ranks.iter_mut().take(j).skip(i) {
1187 *rank = avg_rank;
1188 }
1189 i = j;
1190 }
1191 ranks
1192}
1193
1194fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1196 let n = sorted.len();
1197 let mut correction = 0.0;
1198 let mut i = 0;
1199 while i < n {
1200 let mut j = i + 1;
1201 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1202 j += 1;
1203 }
1204 let t = (j - i) as f64;
1205 if t > 1.0 {
1206 correction += t * (t * t - 1.0);
1207 }
1208 i = j;
1209 }
1210 correction
1211}
1212
1213pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1245 let k = groups.len();
1246 if k < 2 {
1247 return None;
1248 }
1249 for g in groups {
1250 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1251 return None;
1252 }
1253 }
1254
1255 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1256 let nf = total_n as f64;
1257
1258 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1260 for (gi, g) in groups.iter().enumerate() {
1261 for &v in *g {
1262 combined.push((v, gi));
1263 }
1264 }
1265 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1266
1267 let ranks = average_ranks(&combined);
1268
1269 let mut rank_sums = vec![0.0; k];
1271 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1272 rank_sums[*gi] += r;
1273 }
1274
1275 let mean_rank = (nf + 1.0) / 2.0;
1277 let mut h = 0.0;
1278 for (gi, g) in groups.iter().enumerate() {
1279 let ni = g.len() as f64;
1280 let mean_rank_i = rank_sums[gi] / ni;
1281 h += ni * (mean_rank_i - mean_rank).powi(2);
1282 }
1283 h *= 12.0 / (nf * (nf + 1.0));
1284
1285 let tie_corr = compute_tie_correction(&combined);
1287 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1288 if denom > 1e-15 {
1289 h /= denom;
1290 }
1291
1292 let df = (k - 1) as f64;
1293 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1294
1295 Some(TestResult {
1296 statistic: h,
1297 df,
1298 p_value,
1299 })
1300}
1301
1302pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1338 let k = groups.len();
1339 if k < 2 {
1340 return None;
1341 }
1342 for g in groups {
1343 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1344 return None;
1345 }
1346 }
1347
1348 let z_groups: Vec<Vec<f64>> = groups
1350 .iter()
1351 .map(|g| {
1352 let median = stats::median(g).unwrap_or(0.0);
1353 g.iter().map(|&x| (x - median).abs()).collect()
1354 })
1355 .collect();
1356
1357 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1359 let anova = one_way_anova(&z_refs)?;
1360
1361 Some(TestResult {
1362 statistic: anova.f_statistic,
1363 df: anova.df_between as f64,
1364 p_value: anova.p_value,
1365 })
1366}
1367
1368pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1380 if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1381 return None;
1382 }
1383 let m = p_values.len() as f64;
1384 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1385}
1386
1387pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1406 let m = p_values.len();
1407 if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1408 return None;
1409 }
1410
1411 let mut indices: Vec<usize> = (0..m).collect();
1413 indices.sort_by(|&a, &b| {
1414 p_values[a]
1415 .partial_cmp(&p_values[b])
1416 .unwrap_or(std::cmp::Ordering::Equal)
1417 });
1418
1419 let mf = m as f64;
1420 let mut adjusted = vec![0.0; m];
1421
1422 let mut cummin = f64::INFINITY;
1424 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1425 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1427 cummin = cummin.min(adj);
1428 adjusted[orig_idx] = cummin;
1429 }
1430
1431 Some(adjusted)
1432}
1433
1434pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1471 let k = groups.len();
1472 if k < 2 {
1473 return None;
1474 }
1475
1476 let mut sizes = Vec::with_capacity(k);
1477 let mut vars = Vec::with_capacity(k);
1478 let mut n_total: usize = 0;
1479
1480 for g in groups {
1481 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1482 return None;
1483 }
1484 let n = g.len();
1485 let v = stats::variance(g)?;
1486 if v <= 0.0 {
1487 return None; }
1489 sizes.push(n);
1490 vars.push(v);
1491 n_total += n;
1492 }
1493
1494 let nk = n_total - k; if nk == 0 {
1496 return None;
1497 }
1498 let nk_f = nk as f64;
1499
1500 let s2_pooled: f64 = sizes
1502 .iter()
1503 .zip(vars.iter())
1504 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1505 .sum::<f64>()
1506 / nk_f;
1507
1508 if s2_pooled <= 0.0 {
1509 return None;
1510 }
1511
1512 let num = nk_f * s2_pooled.ln()
1514 - sizes
1515 .iter()
1516 .zip(vars.iter())
1517 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1518 .sum::<f64>();
1519
1520 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1522 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1523
1524 let statistic = num / c;
1525 let df = (k - 1) as f64;
1526 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1527
1528 Some(TestResult {
1529 statistic,
1530 df,
1531 p_value,
1532 })
1533}
1534
1535pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1580 let row1 = a + b;
1581 let row2 = c + d;
1582 let col1 = a + c;
1583 let col2 = b + d;
1584 let n = a + b + c + d;
1585
1586 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1588 return None;
1589 }
1590
1591 let log_prob = |a_i: u64| -> f64 {
1593 let b_i = row1 - a_i;
1594 let c_i = col1 - a_i;
1595 let d_i = row2 - c_i;
1596 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1597 - ln_factorial(a_i)
1598 - ln_factorial(b_i)
1599 - ln_factorial(c_i)
1600 - ln_factorial(d_i)
1601 - ln_factorial(n)
1602 };
1603
1604 let a_min = col1.saturating_sub(row2);
1606 let a_max = row1.min(col1);
1607
1608 let log_p_obs = log_prob(a);
1609
1610 let mut p_value = 0.0;
1612 for a_i in a_min..=a_max {
1613 let lp = log_prob(a_i);
1614 if lp <= log_p_obs + 1e-10 {
1616 p_value += lp.exp();
1617 }
1618 }
1619
1620 let p_value = p_value.min(1.0);
1622
1623 let odds_ratio = if b > 0 && c > 0 {
1625 (a as f64 * d as f64) / (b as f64 * c as f64)
1626 } else {
1627 f64::INFINITY
1628 };
1629
1630 Some(TestResult {
1631 statistic: odds_ratio,
1632 df: 1.0,
1633 p_value,
1634 })
1635}
1636
1637#[derive(Debug, Clone, Copy)]
1643pub struct MannKendallResult {
1644 pub s_statistic: i64,
1646 pub variance: f64,
1648 pub z_statistic: f64,
1650 pub p_value: f64,
1652 pub kendall_tau: f64,
1654 pub sen_slope: f64,
1656}
1657
1658pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1696 let n = data.len();
1697 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1698 return None;
1699 }
1700
1701 let mut s: i64 = 0;
1703 for i in 0..n - 1 {
1704 for j in (i + 1)..n {
1705 let diff = data[j] - data[i];
1706 if diff > 0.0 {
1707 s += 1;
1708 } else if diff < 0.0 {
1709 s -= 1;
1710 }
1711 }
1712 }
1713
1714 let mut sorted: Vec<f64> = data.to_vec();
1716 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1717
1718 let mut tie_correction: f64 = 0.0;
1719 let mut current_count: usize = 1;
1720 for i in 1..sorted.len() {
1721 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1722 current_count += 1;
1723 } else {
1724 if current_count > 1 {
1725 let t = current_count as f64;
1726 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1727 }
1728 current_count = 1;
1729 }
1730 }
1731 if current_count > 1 {
1732 let t = current_count as f64;
1733 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1734 }
1735
1736 let nf = n as f64;
1738 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1739
1740 if variance < 1e-300 {
1741 return None; }
1743
1744 let sigma = variance.sqrt();
1746 let z_statistic = if s > 0 {
1747 (s as f64 - 1.0) / sigma
1748 } else if s < 0 {
1749 (s as f64 + 1.0) / sigma
1750 } else {
1751 0.0
1752 };
1753
1754 let p_value = 2.0 * (1.0 - special::standard_normal_cdf(z_statistic.abs()));
1756 let p_value = p_value.clamp(0.0, 1.0);
1757
1758 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1760
1761 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1763 for i in 0..n - 1 {
1764 for j in (i + 1)..n {
1765 let dx = (j - i) as f64;
1766 slopes.push((data[j] - data[i]) / dx);
1767 }
1768 }
1769 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1770 let m = slopes.len();
1771 let sen_slope = if m % 2 == 0 {
1772 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1773 } else {
1774 slopes[m / 2]
1775 };
1776
1777 Some(MannKendallResult {
1778 s_statistic: s,
1779 variance,
1780 z_statistic,
1781 p_value,
1782 kendall_tau,
1783 sen_slope,
1784 })
1785}
1786
1787fn ln_factorial(n: u64) -> f64 {
1789 if n <= 1 {
1790 return 0.0;
1791 }
1792 special::ln_gamma(n as f64 + 1.0)
1794}
1795
1796#[derive(Debug, Clone)]
1802pub struct AdfResult {
1803 pub statistic: f64,
1805 pub n_lags: usize,
1807 pub n_obs: usize,
1809 pub critical_values: [f64; 3],
1811 pub rejected: [bool; 3],
1813}
1814
1815#[derive(Debug, Clone, Copy)]
1817pub enum AdfModel {
1818 None,
1820 Constant,
1822 ConstantTrend,
1824}
1825
1826pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1862 let n = data.len();
1863 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1864 return None;
1865 }
1866
1867 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1869
1870 let best_lag = match max_lags {
1872 Some(p) => p, None => {
1874 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1876 let p_max = schwert.min(n / 3);
1877 select_adf_lag(data, &dy, model, p_max)
1879 }
1880 };
1881
1882 adf_ols(data, &dy, model, best_lag)
1884}
1885
1886fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1888 let mut best_aic = f64::INFINITY;
1889 let mut best_p = 0;
1890
1891 for p in 0..=p_max {
1892 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
1893 if aic < best_aic {
1894 best_aic = aic;
1895 best_p = p;
1896 }
1897 }
1898 }
1899
1900 best_p
1901}
1902
1903#[allow(clippy::type_complexity)]
1907fn adf_build_matrix(
1908 data: &[f64],
1909 dy: &[f64],
1910 model: AdfModel,
1911 p: usize,
1912) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
1913 let start = p + 1;
1914 if start >= dy.len() || dy.len() - start < 5 {
1915 return None;
1916 }
1917 let m = dy.len() - start;
1918
1919 let y_dep: Vec<f64> = dy[start..].to_vec();
1920
1921 let has_intercept = !matches!(model, AdfModel::None);
1923 let has_trend = matches!(model, AdfModel::ConstantTrend);
1924 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
1925
1926 let mut x_data = Vec::with_capacity(m * ncols);
1928 let mut gamma_col = 0;
1929
1930 for i in 0..m {
1931 let t = start + i;
1932 if has_intercept {
1933 x_data.push(1.0); gamma_col = 1;
1935 }
1936 x_data.push(data[t]); if has_trend {
1938 x_data.push((t + 1) as f64); }
1940 for lag in 1..=p {
1941 x_data.push(dy[t - lag]);
1942 }
1943 }
1944
1945 Some((x_data, y_dep, m, ncols, gamma_col))
1946}
1947
1948fn adf_ols_core(
1952 x_data: &[f64],
1953 y: &[f64],
1954 m: usize,
1955 ncols: usize,
1956 gamma_col: usize,
1957) -> Option<(f64, f64, usize)> {
1958 let mut xtx = vec![0.0_f64; ncols * ncols];
1960 for i in 0..m {
1961 let row = &x_data[i * ncols..(i + 1) * ncols];
1962 for j in 0..ncols {
1963 for k in j..ncols {
1964 xtx[j * ncols + k] += row[j] * row[k];
1965 }
1966 }
1967 }
1968 for j in 0..ncols {
1970 for k in (j + 1)..ncols {
1971 xtx[k * ncols + j] = xtx[j * ncols + k];
1972 }
1973 }
1974
1975 let mut xty = vec![0.0_f64; ncols];
1977 for i in 0..m {
1978 let row = &x_data[i * ncols..(i + 1) * ncols];
1979 for j in 0..ncols {
1980 xty[j] += row[j] * y[i];
1981 }
1982 }
1983
1984 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
1986 for i in 0..ncols {
1987 for j in 0..ncols {
1988 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
1989 }
1990 augmented[i * (ncols + 1) + ncols] = xty[i];
1991 }
1992
1993 for col in 0..ncols {
1994 let mut max_row = col;
1996 let mut max_val = augmented[col * (ncols + 1) + col].abs();
1997 for row in (col + 1)..ncols {
1998 let val = augmented[row * (ncols + 1) + col].abs();
1999 if val > max_val {
2000 max_val = val;
2001 max_row = row;
2002 }
2003 }
2004 if max_val < 1e-15 {
2005 return None; }
2007 if max_row != col {
2008 for j in 0..=ncols {
2009 let a = col * (ncols + 1) + j;
2010 let b = max_row * (ncols + 1) + j;
2011 augmented.swap(a, b);
2012 }
2013 }
2014
2015 let pivot = augmented[col * (ncols + 1) + col];
2016 for row in (col + 1)..ncols {
2017 let factor = augmented[row * (ncols + 1) + col] / pivot;
2018 for j in col..=ncols {
2019 let above = augmented[col * (ncols + 1) + j];
2020 augmented[row * (ncols + 1) + j] -= factor * above;
2021 }
2022 }
2023 }
2024
2025 let mut beta = vec![0.0_f64; ncols];
2027 for i in (0..ncols).rev() {
2028 let mut sum = augmented[i * (ncols + 1) + ncols];
2029 for j in (i + 1)..ncols {
2030 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2031 }
2032 beta[i] = sum / augmented[i * (ncols + 1) + i];
2033 }
2034
2035 let mut rss = 0.0;
2037 for i in 0..m {
2038 let row = &x_data[i * ncols..(i + 1) * ncols];
2039 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2040 let resid = y[i] - y_hat;
2041 rss += resid * resid;
2042 }
2043
2044 let df = m - ncols;
2046 if df == 0 {
2047 return None;
2048 }
2049 let mse = rss / df as f64;
2050
2051 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2054 for j in 0..ncols {
2055 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2056 }
2057 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2058 }
2059
2060 for col in 0..ncols {
2062 let mut max_row = col;
2063 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2064 for row in (col + 1)..ncols {
2065 let val = xtx_aug[row * 2 * ncols + col].abs();
2066 if val > max_val {
2067 max_val = val;
2068 max_row = row;
2069 }
2070 }
2071 if max_val < 1e-15 {
2072 return None;
2073 }
2074 if max_row != col {
2075 for j in 0..(2 * ncols) {
2076 let a = col * 2 * ncols + j;
2077 let b = max_row * 2 * ncols + j;
2078 xtx_aug.swap(a, b);
2079 }
2080 }
2081
2082 let pivot = xtx_aug[col * 2 * ncols + col];
2083 for j in 0..(2 * ncols) {
2084 xtx_aug[col * 2 * ncols + j] /= pivot;
2085 }
2086 for row in 0..ncols {
2087 if row == col {
2088 continue;
2089 }
2090 let factor = xtx_aug[row * 2 * ncols + col];
2091 for j in 0..(2 * ncols) {
2092 let above = xtx_aug[col * 2 * ncols + j];
2093 xtx_aug[row * 2 * ncols + j] -= factor * above;
2094 }
2095 }
2096 }
2097
2098 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2100 if var_gamma <= 0.0 {
2101 return None;
2102 }
2103 let se_gamma = var_gamma.sqrt();
2104 let t_gamma = beta[gamma_col] / se_gamma;
2105
2106 Some((t_gamma, rss, ncols))
2107}
2108
2109fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2111 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2112 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2113 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2114 Some((aic, m))
2115}
2116
2117fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2119 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2120 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2121
2122 let critical_values = adf_critical_values(model, m);
2123
2124 let rejected = [
2125 gamma_t <= critical_values[0],
2126 gamma_t <= critical_values[1],
2127 gamma_t <= critical_values[2],
2128 ];
2129
2130 Some(AdfResult {
2131 statistic: gamma_t,
2132 n_lags: p,
2133 n_obs: m,
2134 critical_values,
2135 rejected,
2136 })
2137}
2138
2139fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2143 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2148 AdfModel::None => (
2149 [-2.5658, -1.9393, -1.6156],
2150 [-1.960, -0.398, -0.181],
2151 [-10.04, 0.0, 0.0],
2152 ),
2153 AdfModel::Constant => (
2154 [-3.4336, -2.8621, -2.5671],
2155 [-5.999, -2.738, -1.438],
2156 [-29.25, -8.36, -4.48],
2157 ),
2158 AdfModel::ConstantTrend => (
2159 [-3.9638, -3.4126, -3.1279],
2160 [-8.353, -4.039, -2.418],
2161 [-47.44, -17.83, -7.58],
2162 ),
2163 };
2164
2165 let nf = n as f64;
2166 let inv_n = 1.0 / nf;
2167 let inv_n2 = inv_n * inv_n;
2168
2169 [
2170 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2171 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2172 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2173 ]
2174}
2175
2176#[cfg(test)]
2177mod tests {
2178 use super::*;
2179
2180 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2186 let mut a = seed;
2187 move || {
2188 a = a.wrapping_add(0x6d2b_79f5);
2189 let mut t = a;
2190 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2191 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2192 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2193 }
2194 }
2195
2196 #[test]
2202 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2203 let mut rng = mulberry32(0x5350_4331);
2204 let full: Vec<f64> = (0..20_000)
2205 .map(|_| {
2206 let u = rng();
2207 -(1.0 - u).ln() })
2209 .collect();
2210
2211 let mut prev_a2 = 0.0_f64;
2212 let mut prev_p = f64::INFINITY;
2213 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2214 let r = anderson_darling_normality(&full[..n]).expect("computes");
2215 assert!(
2217 r.statistic_modified > prev_a2,
2218 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2219 r.statistic_modified
2220 );
2221 assert!(
2223 r.p_value < 0.5,
2224 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2225 r.statistic_modified,
2226 r.p_value
2227 );
2228 assert!(
2230 r.p_value <= prev_p + 1e-12,
2231 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2232 r.p_value
2233 );
2234 prev_a2 = r.statistic_modified;
2235 prev_p = r.p_value;
2236 }
2237 }
2238
2239 #[test]
2242 fn ad_both_functions_immune_to_overflow() {
2243 let mut rng = mulberry32(0x0bad_c0de);
2244 let data: Vec<f64> = (0..9000).map(|_| -(1.0 - rng()).ln()).collect();
2245 let a = anderson_darling_test(&data).expect("computes");
2246 let b = anderson_darling_normality(&data).expect("computes");
2247 assert!(
2248 a.statistic_star > 300.0,
2249 "expected large A*², got {}",
2250 a.statistic_star
2251 );
2252 assert!(
2253 a.p_value < 0.5,
2254 "test() overflowed toward normal: p={}",
2255 a.p_value
2256 );
2257 assert!(
2258 b.p_value < 0.5,
2259 "normality() overflowed toward normal: p={}",
2260 b.p_value
2261 );
2262 }
2263
2264 #[test]
2269 fn one_sample_null_true() {
2270 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2271 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2272 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2273 }
2274
2275 #[test]
2276 fn one_sample_null_false() {
2277 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2278 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2279 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2280 }
2281
2282 #[test]
2283 fn one_sample_edge_cases() {
2284 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());
2287 }
2288
2289 #[test]
2294 fn two_sample_same_mean() {
2295 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2296 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2297 let r = two_sample_t_test(&a, &b).expect("should compute");
2298 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2299 }
2300
2301 #[test]
2302 fn two_sample_different_means() {
2303 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2304 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2305 let r = two_sample_t_test(&a, &b).expect("should compute");
2306 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2307 }
2308
2309 #[test]
2310 fn two_sample_different_sizes() {
2311 let a = [1.0, 2.0, 3.0];
2312 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2313 let r = two_sample_t_test(&a, &b).expect("should compute");
2314 assert!(r.p_value < 0.05);
2315 }
2316
2317 #[test]
2318 fn two_sample_edge_cases() {
2319 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2320 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2321 }
2322
2323 #[test]
2328 fn paired_no_difference() {
2329 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2330 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2331 let r = paired_t_test(&x, &y).expect("should compute");
2332 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2333 }
2334
2335 #[test]
2336 fn paired_significant_difference() {
2337 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2339 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2340 let r = paired_t_test(&before, &after).expect("should compute");
2341 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2342 assert!(r.statistic < 0.0); }
2344
2345 #[test]
2346 fn paired_edge_cases() {
2347 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2350
2351 #[test]
2356 fn anova_same_means() {
2357 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2358 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2359 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2360 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2361 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2362 }
2363
2364 #[test]
2365 fn anova_different_means() {
2366 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2367 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2368 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2369 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2370 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2371 assert!(r.df_between == 2);
2372 assert!(r.df_within == 12);
2373 }
2374
2375 #[test]
2376 fn anova_ss_decomposition() {
2377 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2378 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2379 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2380 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2382 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2383 assert!(
2384 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2385 "SS decomposition: {ss_total} vs {} + {}",
2386 r.ss_between,
2387 r.ss_within
2388 );
2389 }
2390
2391 #[test]
2392 fn anova_edge_cases() {
2393 let g1 = [1.0, 2.0, 3.0];
2394 assert!(one_way_anova(&[&g1]).is_none()); }
2396
2397 #[test]
2402 fn chi2_gof_uniform() {
2403 let observed = [25.0, 25.0, 25.0, 25.0];
2405 let expected = [25.0, 25.0, 25.0, 25.0];
2406 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2407 assert!((r.statistic).abs() < 1e-15);
2408 assert!((r.p_value - 1.0).abs() < 0.01);
2409 }
2410
2411 #[test]
2412 fn chi2_gof_significant() {
2413 let observed = [90.0, 10.0];
2414 let expected = [50.0, 50.0];
2415 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2416 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2417 }
2418
2419 #[test]
2420 fn chi2_gof_edge_cases() {
2421 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()); }
2425
2426 #[test]
2431 fn chi2_independence_significant() {
2432 let table = [30.0, 10.0, 10.0, 50.0];
2434 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2435 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2436 assert!((r.df - 1.0).abs() < 1e-10);
2437 }
2438
2439 #[test]
2440 fn chi2_independence_not_significant() {
2441 let table = [25.0, 25.0, 25.0, 25.0];
2443 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2444 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2445 }
2446
2447 #[test]
2448 fn chi2_independence_3x3() {
2449 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2450 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2451 assert!((r.df - 4.0).abs() < 1e-10);
2452 assert!(r.p_value < 0.05);
2453 }
2454
2455 #[test]
2456 fn chi2_independence_edge_cases() {
2457 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()); }
2461
2462 #[test]
2467 fn jb_normal_data() {
2468 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2470 let r = jarque_bera_test(&data).expect("should compute");
2471 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2472 }
2473
2474 #[test]
2475 fn jb_skewed_data() {
2476 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2478 let r = jarque_bera_test(&data).expect("should compute");
2479 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2480 }
2481
2482 #[test]
2483 fn jb_edge_cases() {
2484 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2486
2487 #[test]
2492 fn ad_normal_data() {
2493 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2494 let r = anderson_darling_test(&data).expect("should compute");
2495 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2496 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2497 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2498 }
2499
2500 #[test]
2501 fn ad_skewed_data() {
2502 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];
2504 let r = anderson_darling_test(&data).expect("should compute");
2505 assert!(
2506 r.p_value < 0.05,
2507 "p = {} (should reject normality)",
2508 r.p_value
2509 );
2510 }
2511
2512 #[test]
2513 fn ad_bimodal_data() {
2514 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2516 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2517 let r = anderson_darling_test(&data).expect("should compute");
2518 assert!(
2519 r.p_value < 0.01,
2520 "p = {} (bimodal should reject normality)",
2521 r.p_value
2522 );
2523 }
2524
2525 #[test]
2526 fn ad_large_normal_sample() {
2527 let n = 100;
2528 let data: Vec<f64> = (1..=n)
2529 .map(|i| {
2530 let p = (i as f64 - 0.5) / n as f64;
2531 special::inverse_normal_cdf(p)
2532 })
2533 .collect();
2534 let r = anderson_darling_test(&data).expect("should compute");
2535 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2536 }
2537
2538 #[test]
2539 fn ad_edge_cases() {
2540 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());
2543 }
2544
2545 #[test]
2546 fn ad_p_value_ranges() {
2547 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2552 let r = anderson_darling_test(&near_normal).expect("should compute");
2553 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2554
2555 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];
2557 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2558 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2559 }
2560
2561 #[test]
2566 fn ad_normal_data_large_p() {
2567 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2569 let r = anderson_darling_normality(&data).unwrap();
2570 assert!(r.p_value > 0.05, "p={}", r.p_value);
2571 }
2572
2573 #[test]
2574 fn ad_exponential_data_small_p() {
2575 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2577 let r = anderson_darling_normality(&data).unwrap();
2578 assert!(r.p_value < 0.05, "p={}", r.p_value);
2579 }
2580
2581 #[test]
2582 fn ad_statistic_non_negative() {
2583 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2584 let r = anderson_darling_normality(&data).unwrap();
2585 assert!(r.statistic >= 0.0);
2586 assert!(r.statistic_modified >= r.statistic);
2587 }
2588
2589 #[test]
2590 fn ad_p_value_in_range() {
2591 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2592 let r = anderson_darling_normality(&data).unwrap();
2593 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2594 }
2595
2596 #[test]
2597 fn ad_insufficient_data() {
2598 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2600
2601 #[test]
2602 fn ad_degenerate_data() {
2603 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2605 }
2606
2607 #[test]
2608 fn ad_modified_statistic_formula() {
2609 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2611 let r = anderson_darling_normality(&data).unwrap();
2612 assert!(r.statistic_modified > r.statistic - 1e-10);
2613 }
2614
2615 #[test]
2620 fn ad_correction_factor_formula() {
2621 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2622 let n = data.len() as f64;
2623 let r = anderson_darling_normality(&data).unwrap();
2624
2625 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2627 assert!(
2628 (r.statistic_modified - expected_star).abs() < 1e-12,
2629 "A²* = {}, expected = {}",
2630 r.statistic_modified,
2631 expected_star
2632 );
2633
2634 assert!(
2636 r.statistic >= 0.0,
2637 "A² = {} must be non-negative",
2638 r.statistic
2639 );
2640 }
2641
2642 #[test]
2648 fn ad_formula_structure_symmetric_vs_skewed() {
2649 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2651 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2653
2654 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2655 let r_skew = anderson_darling_normality(&skewed).unwrap();
2656
2657 assert!(
2658 r_sym.statistic < r_skew.statistic,
2659 "Symmetric A²={} should be less than skewed A²={}",
2660 r_sym.statistic,
2661 r_skew.statistic
2662 );
2663 }
2664
2665 #[test]
2670 fn ad_pvalue_invariants() {
2671 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2673 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2674 assert!(
2675 r_normal.p_value > 0.05,
2676 "Normal data: p = {} (expected > 0.05)",
2677 r_normal.p_value
2678 );
2679
2680 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2682 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2683 assert!(
2684 r_exp.p_value < 0.05,
2685 "Exponential data: p = {} (expected < 0.05)",
2686 r_exp.p_value
2687 );
2688 }
2689
2690 #[test]
2695 fn sw_normal_data() {
2696 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2698 let r = shapiro_wilk_test(&data).expect("should compute");
2699 assert!(r.w > 0.9, "W = {}", r.w);
2700 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2701 }
2702
2703 #[test]
2704 fn sw_bimodal_data() {
2705 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2707 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2708 let r = shapiro_wilk_test(&data).expect("should compute");
2709 assert!(
2710 r.p_value < 0.01,
2711 "p = {} (bimodal should reject normality)",
2712 r.p_value
2713 );
2714 }
2715
2716 #[test]
2717 fn sw_n3() {
2718 let data = [1.0, 2.0, 3.0];
2719 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2720 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2721 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2722 }
2723
2724 #[test]
2725 fn sw_n4() {
2726 let data = [1.0, 2.0, 3.0, 4.0];
2727 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2728 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2729 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2730 }
2731
2732 #[test]
2733 fn sw_n5() {
2734 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2735 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2736 assert!(r.w > 0.9, "W = {}", r.w);
2737 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2738 }
2739
2740 #[test]
2741 fn sw_skewed_data() {
2742 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];
2744 let r = shapiro_wilk_test(&data).expect("should compute");
2745 assert!(
2746 r.p_value < 0.05,
2747 "p = {} (skewed data should reject normality)",
2748 r.p_value
2749 );
2750 }
2751
2752 #[test]
2753 fn sw_large_normal_sample() {
2754 let n = 100;
2757 let data: Vec<f64> = (1..=n)
2758 .map(|i| {
2759 let p = (i as f64 - 0.5) / n as f64;
2760 special::inverse_normal_cdf(p)
2761 })
2762 .collect();
2763 let r = shapiro_wilk_test(&data).expect("should compute");
2764 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2765 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2766 }
2767
2768 #[test]
2769 fn sw_w_bounded() {
2770 let datasets: Vec<Vec<f64>> = vec![
2772 vec![1.0, 2.0, 3.0],
2773 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2774 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2775 (0..20).map(|i| (i as f64).powi(2)).collect(),
2776 ];
2777 for (idx, data) in datasets.iter().enumerate() {
2778 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2779 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2780 assert!(
2781 r.p_value >= 0.0 && r.p_value <= 1.0,
2782 "dataset {idx}: p = {}",
2783 r.p_value
2784 );
2785 }
2786 }
2787
2788 #[test]
2789 fn sw_edge_cases() {
2790 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()); }
2796
2797 #[test]
2798 fn sw_n5001_rejected() {
2799 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2800 assert!(shapiro_wilk_test(&data).is_none()); }
2802
2803 #[test]
2808 fn mw_clearly_different() {
2809 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2810 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2811 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2812 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2813 }
2814
2815 #[test]
2816 fn mw_same_distribution() {
2817 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2818 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2819 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2820 assert!(
2821 r.p_value > 0.3,
2822 "p = {} (interleaved, same dist)",
2823 r.p_value
2824 );
2825 }
2826
2827 #[test]
2828 fn mw_with_ties() {
2829 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2830 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2831 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2832 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2833 }
2834
2835 #[test]
2836 fn mw_different_sizes() {
2837 let a = [1.0, 2.0, 3.0];
2838 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2839 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2840 assert!(r.p_value < 0.05);
2841 }
2842
2843 #[test]
2844 fn mw_edge_cases() {
2845 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());
2848 }
2849
2850 #[test]
2855 fn wilcoxon_significant_increase() {
2856 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2857 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2858 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2859 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2860 }
2861
2862 #[test]
2863 fn wilcoxon_no_difference() {
2864 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2865 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2866 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2867 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2868 }
2869
2870 #[test]
2871 fn wilcoxon_with_ties() {
2872 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2874 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2876 assert!(r.statistic > 0.0);
2878 }
2879
2880 #[test]
2881 fn wilcoxon_with_zero_diffs() {
2882 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2884 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2886 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2887 }
2888
2889 #[test]
2890 fn wilcoxon_edge_cases() {
2891 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());
2895 }
2896
2897 #[test]
2902 fn kw_clearly_different() {
2903 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2904 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2905 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2906 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2907 assert!(r.p_value < 0.01, "p = {}", r.p_value);
2908 assert!((r.df - 2.0).abs() < 1e-10);
2909 }
2910
2911 #[test]
2912 fn kw_same_distribution() {
2913 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2914 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2915 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2916 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2917 }
2918
2919 #[test]
2920 fn kw_with_ties() {
2921 let g1 = [1.0, 2.0, 2.0, 3.0];
2922 let g2 = [3.0, 4.0, 4.0, 5.0];
2923 let g3 = [5.0, 6.0, 6.0, 7.0];
2924 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2925 assert!(r.statistic > 0.0);
2926 }
2927
2928 #[test]
2929 fn kw_edge_cases() {
2930 let g1 = [1.0, 2.0, 3.0];
2931 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
2933
2934 #[test]
2939 fn levene_equal_variance() {
2940 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2941 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2942 let r = levene_test(&[&g1, &g2]).expect("should compute");
2943 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2945 }
2946
2947 #[test]
2948 fn levene_unequal_variance() {
2949 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");
2952 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2953 }
2954
2955 #[test]
2956 fn levene_three_groups() {
2957 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2958 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2959 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2960 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2961 assert!(r.df >= 2.0);
2962 }
2963
2964 #[test]
2965 fn levene_edge_cases() {
2966 let g1 = [1.0, 2.0, 3.0];
2967 assert!(levene_test(&[&g1]).is_none()); }
2969
2970 #[test]
2975 fn bonferroni_basic() {
2976 let ps = [0.01, 0.04, 0.03, 0.005];
2977 let adj = bonferroni_correction(&ps).expect("should compute");
2978 assert!((adj[0] - 0.04).abs() < 1e-10);
2979 assert!((adj[1] - 0.16).abs() < 1e-10);
2980 assert!((adj[2] - 0.12).abs() < 1e-10);
2981 assert!((adj[3] - 0.02).abs() < 1e-10);
2982 }
2983
2984 #[test]
2985 fn bonferroni_capped_at_one() {
2986 let ps = [0.5, 0.6];
2987 let adj = bonferroni_correction(&ps).expect("should compute");
2988 assert!((adj[0] - 1.0).abs() < 1e-10);
2989 assert!((adj[1] - 1.0).abs() < 1e-10);
2990 }
2991
2992 #[test]
2993 fn bh_basic() {
2994 let ps = [0.01, 0.04, 0.03, 0.005];
2995 let adj = benjamini_hochberg(&ps).expect("should compute");
2996 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
2998 assert!(
2999 adjusted >= orig - 1e-15,
3000 "adj[{i}] = {adjusted} < original {orig}"
3001 );
3002 }
3003 }
3006
3007 #[test]
3008 fn bh_all_significant() {
3009 let ps = [0.001, 0.002, 0.003];
3010 let adj = benjamini_hochberg(&ps).expect("should compute");
3011 for &a in &adj {
3012 assert!(a < 0.05);
3013 }
3014 }
3015
3016 #[test]
3017 fn correction_edge_cases() {
3018 assert!(bonferroni_correction(&[]).is_none());
3019 assert!(benjamini_hochberg(&[]).is_none());
3020 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3021 }
3022
3023 #[test]
3028 fn bartlett_equal_variances() {
3029 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3030 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3031 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3032 assert!(
3033 r.p_value > 0.9,
3034 "equal variance → p high, got {}",
3035 r.p_value
3036 );
3037 }
3038
3039 #[test]
3040 fn bartlett_unequal_variances() {
3041 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");
3044 assert!(
3045 r.p_value < 0.01,
3046 "very different variances → p < 0.01, got {}",
3047 r.p_value
3048 );
3049 assert!((r.df - 1.0).abs() < 1e-10); }
3051
3052 #[test]
3053 fn bartlett_three_groups() {
3054 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3055 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3056 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3058 assert!(r.p_value < 0.05, "one group with high variance");
3059 assert!((r.df - 2.0).abs() < 1e-10); }
3061
3062 #[test]
3063 fn bartlett_edge_cases() {
3064 let g1 = [1.0, 2.0, 3.0];
3065 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3069
3070 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3072 }
3073
3074 #[test]
3079 fn fisher_tea_tasting() {
3080 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3082 assert!(
3084 (r.p_value - 0.4857).abs() < 0.01,
3085 "p ≈ 0.4857, got {}",
3086 r.p_value
3087 );
3088 }
3089
3090 #[test]
3091 fn fisher_significant() {
3092 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3094 assert!(r.p_value < 0.001, "perfect association → p very small");
3095 }
3096
3097 #[test]
3098 fn fisher_no_association() {
3099 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3101 assert!(
3102 r.p_value > 0.9,
3103 "no association → p ≈ 1.0, got {}",
3104 r.p_value
3105 );
3106 }
3107
3108 #[test]
3109 fn fisher_small_table() {
3110 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3112 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3113 }
3114
3115 #[test]
3116 fn fisher_asymmetric() {
3117 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3119 assert!(r.p_value < 0.05, "significant association");
3120 }
3121
3122 #[test]
3123 fn fisher_edge_cases() {
3124 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()); }
3129
3130 #[test]
3131 fn fisher_odds_ratio() {
3132 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3133 assert!(
3135 (r.statistic - 9.0).abs() < 1e-10,
3136 "OR = 9, got {}",
3137 r.statistic
3138 );
3139 }
3140
3141 #[test]
3146 fn mk_increasing_trend() {
3147 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3148 let r = mann_kendall_test(&data).expect("should compute");
3149 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3150 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3151 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3152 assert!(r.s_statistic > 0);
3153 }
3154
3155 #[test]
3156 fn mk_decreasing_trend() {
3157 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3158 let r = mann_kendall_test(&data).expect("should compute");
3159 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3160 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3161 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3162 assert!(r.s_statistic < 0);
3163 }
3164
3165 #[test]
3166 fn mk_no_trend() {
3167 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3169 let r = mann_kendall_test(&data).expect("should compute");
3170 assert!(
3172 r.p_value > 0.05,
3173 "p = {} (should be > 0.05 for no trend)",
3174 r.p_value
3175 );
3176 }
3177
3178 #[test]
3179 fn mk_perfect_monotone() {
3180 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3181 let r = mann_kendall_test(&data).expect("should compute");
3182 assert_eq!(r.s_statistic, 45);
3184 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3185 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3186 }
3187
3188 #[test]
3189 fn mk_with_ties() {
3190 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3191 let r = mann_kendall_test(&data).expect("should compute");
3192 assert!(r.s_statistic > 0);
3193 let n = data.len() as f64;
3195 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3196 assert!(r.variance < base_var, "ties should reduce variance");
3197 }
3198
3199 #[test]
3200 fn mk_edge_cases() {
3201 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3203 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3205 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3207 }
3208
3209 #[test]
3210 fn mk_minimum_n() {
3211 let data = [1.0, 2.0, 3.0, 4.0];
3213 let r = mann_kendall_test(&data).expect("n=4 should work");
3214 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3216 }
3217
3218 #[test]
3219 fn mk_sen_slope_robust_to_outlier() {
3220 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3222 let r = mann_kendall_test(&data).expect("should compute");
3223 assert!(
3225 (r.sen_slope - 1.0).abs() < 0.5,
3226 "sen slope = {}, expected ≈ 1.0",
3227 r.sen_slope
3228 );
3229 }
3230
3231 #[test]
3236 fn adf_stationary_mean_reverting() {
3237 let data = [
3240 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,
3241 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,
3242 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,
3243 ];
3244 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3245 assert!(
3247 r.rejected[2],
3248 "should reject at 10%: stat={}, cv={}",
3249 r.statistic, r.critical_values[2]
3250 );
3251 }
3252
3253 #[test]
3254 fn adf_nonstationary_random_walk() {
3255 let increments = [
3257 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,
3258 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,
3259 -0.13, 0.15, -0.1,
3260 ];
3261 let mut walk = Vec::with_capacity(increments.len());
3262 let mut cum = 0.0;
3263 for &inc in &increments {
3264 cum += inc;
3265 walk.push(cum);
3266 }
3267 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3268 assert!(
3270 !r.rejected[0],
3271 "should NOT reject at 1%: stat={}, cv={}",
3272 r.statistic, r.critical_values[0]
3273 );
3274 }
3275
3276 #[test]
3277 fn adf_with_fixed_lags() {
3278 let data: Vec<f64> = (0..50)
3280 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3281 .collect();
3282 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3283 assert_eq!(r.n_lags, 2);
3284 assert!(r.statistic.is_finite());
3285 }
3286
3287 #[test]
3288 fn adf_constant_trend_model() {
3289 let data: Vec<f64> = (0..30)
3292 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3293 .collect();
3294 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3295 assert!(r.statistic.is_finite());
3296 assert_eq!(r.critical_values.len(), 3);
3297 }
3298
3299 #[test]
3300 fn adf_edge_cases() {
3301 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3303 let mut data = vec![0.0; 20];
3305 data[5] = f64::NAN;
3306 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3307 }
3308
3309 #[test]
3310 fn adf_critical_values_constant() {
3311 let cv = adf_critical_values(AdfModel::Constant, 100);
3313 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3315 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3316 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3317 }
3318
3319 #[test]
3320 fn adf_critical_values_ordering() {
3321 let cv = adf_critical_values(AdfModel::Constant, 50);
3322 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3324 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3325 }
3326}
3327
3328#[cfg(test)]
3329mod proptests {
3330 use super::*;
3331 use proptest::prelude::*;
3332
3333 proptest! {
3334 #[test]
3335 fn one_sample_p_bounded(
3336 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3337 mu0 in -1e3_f64..1e3
3338 ) {
3339 if let Some(r) = one_sample_t_test(&data, mu0) {
3340 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3341 }
3342 }
3343
3344 #[test]
3345 fn two_sample_p_bounded(
3346 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3347 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3348 ) {
3349 if let Some(r) = two_sample_t_test(&a, &b) {
3350 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3351 }
3352 }
3353
3354 #[test]
3355 fn anova_p_bounded(
3356 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3357 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3358 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3359 ) {
3360 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3361 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3362 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3363 }
3364 }
3365
3366 #[test]
3367 fn bonferroni_monotone(
3368 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3369 ) {
3370 let adj = bonferroni_correction(&ps).expect("should compute");
3371 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3372 prop_assert!(adjusted >= orig - 1e-15,
3373 "adj[{i}] = {adjusted} < orig = {orig}");
3374 }
3375 }
3376
3377 #[test]
3378 fn shapiro_wilk_p_bounded(
3379 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3380 ) {
3381 if let Some(r) = shapiro_wilk_test(&data) {
3382 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3383 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3384 }
3385 }
3386
3387 #[test]
3388 fn anderson_darling_p_bounded(
3389 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3390 ) {
3391 if let Some(r) = anderson_darling_test(&data) {
3392 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3393 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3394 }
3395 }
3396
3397 #[test]
3398 fn mann_whitney_p_bounded(
3399 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3400 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3401 ) {
3402 if let Some(r) = mann_whitney_u_test(&a, &b) {
3403 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3404 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3405 }
3406 }
3407
3408 #[test]
3409 fn wilcoxon_p_bounded(
3410 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3411 ) {
3412 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3413 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3414 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3415 }
3416 }
3417
3418 #[test]
3419 fn bartlett_p_bounded(
3420 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3421 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3422 ) {
3423 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3424 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3425 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3426 }
3427 }
3428
3429 #[test]
3430 fn fisher_p_bounded(
3431 a in 0_u64..20,
3432 b in 0_u64..20,
3433 c in 0_u64..20,
3434 d in 0_u64..20,
3435 ) {
3436 if let Some(r) = fisher_exact_test(a, b, c, d) {
3437 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3438 }
3439 }
3440
3441 #[test]
3442 fn mann_kendall_p_bounded(
3443 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3444 ) {
3445 if let Some(r) = mann_kendall_test(&data) {
3446 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3447 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3448 "tau = {}", r.kendall_tau);
3449 }
3450 }
3451
3452 #[test]
3453 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3454 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3455 let r = mann_kendall_test(&data).expect("should compute");
3456 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3457 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3458 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3459 }
3460 }
3461}