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!(a.statistic_star > 300.0, "expected large A*², got {}", a.statistic_star);
2248 assert!(a.p_value < 0.5, "test() overflowed toward normal: p={}", a.p_value);
2249 assert!(b.p_value < 0.5, "normality() overflowed toward normal: p={}", b.p_value);
2250 }
2251
2252 #[test]
2257 fn one_sample_null_true() {
2258 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2259 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2260 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2261 }
2262
2263 #[test]
2264 fn one_sample_null_false() {
2265 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2266 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2267 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2268 }
2269
2270 #[test]
2271 fn one_sample_edge_cases() {
2272 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());
2275 }
2276
2277 #[test]
2282 fn two_sample_same_mean() {
2283 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2284 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2285 let r = two_sample_t_test(&a, &b).expect("should compute");
2286 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2287 }
2288
2289 #[test]
2290 fn two_sample_different_means() {
2291 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2292 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2293 let r = two_sample_t_test(&a, &b).expect("should compute");
2294 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2295 }
2296
2297 #[test]
2298 fn two_sample_different_sizes() {
2299 let a = [1.0, 2.0, 3.0];
2300 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2301 let r = two_sample_t_test(&a, &b).expect("should compute");
2302 assert!(r.p_value < 0.05);
2303 }
2304
2305 #[test]
2306 fn two_sample_edge_cases() {
2307 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2308 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2309 }
2310
2311 #[test]
2316 fn paired_no_difference() {
2317 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2318 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2319 let r = paired_t_test(&x, &y).expect("should compute");
2320 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2321 }
2322
2323 #[test]
2324 fn paired_significant_difference() {
2325 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2327 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2328 let r = paired_t_test(&before, &after).expect("should compute");
2329 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2330 assert!(r.statistic < 0.0); }
2332
2333 #[test]
2334 fn paired_edge_cases() {
2335 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2338
2339 #[test]
2344 fn anova_same_means() {
2345 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2346 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2347 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2348 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2349 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2350 }
2351
2352 #[test]
2353 fn anova_different_means() {
2354 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2355 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2356 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2357 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2358 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2359 assert!(r.df_between == 2);
2360 assert!(r.df_within == 12);
2361 }
2362
2363 #[test]
2364 fn anova_ss_decomposition() {
2365 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2366 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2367 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2368 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2370 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2371 assert!(
2372 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2373 "SS decomposition: {ss_total} vs {} + {}",
2374 r.ss_between,
2375 r.ss_within
2376 );
2377 }
2378
2379 #[test]
2380 fn anova_edge_cases() {
2381 let g1 = [1.0, 2.0, 3.0];
2382 assert!(one_way_anova(&[&g1]).is_none()); }
2384
2385 #[test]
2390 fn chi2_gof_uniform() {
2391 let observed = [25.0, 25.0, 25.0, 25.0];
2393 let expected = [25.0, 25.0, 25.0, 25.0];
2394 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2395 assert!((r.statistic).abs() < 1e-15);
2396 assert!((r.p_value - 1.0).abs() < 0.01);
2397 }
2398
2399 #[test]
2400 fn chi2_gof_significant() {
2401 let observed = [90.0, 10.0];
2402 let expected = [50.0, 50.0];
2403 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2404 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2405 }
2406
2407 #[test]
2408 fn chi2_gof_edge_cases() {
2409 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()); }
2413
2414 #[test]
2419 fn chi2_independence_significant() {
2420 let table = [30.0, 10.0, 10.0, 50.0];
2422 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2423 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2424 assert!((r.df - 1.0).abs() < 1e-10);
2425 }
2426
2427 #[test]
2428 fn chi2_independence_not_significant() {
2429 let table = [25.0, 25.0, 25.0, 25.0];
2431 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2432 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2433 }
2434
2435 #[test]
2436 fn chi2_independence_3x3() {
2437 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2438 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2439 assert!((r.df - 4.0).abs() < 1e-10);
2440 assert!(r.p_value < 0.05);
2441 }
2442
2443 #[test]
2444 fn chi2_independence_edge_cases() {
2445 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()); }
2449
2450 #[test]
2455 fn jb_normal_data() {
2456 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2458 let r = jarque_bera_test(&data).expect("should compute");
2459 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2460 }
2461
2462 #[test]
2463 fn jb_skewed_data() {
2464 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2466 let r = jarque_bera_test(&data).expect("should compute");
2467 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2468 }
2469
2470 #[test]
2471 fn jb_edge_cases() {
2472 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2474
2475 #[test]
2480 fn ad_normal_data() {
2481 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2482 let r = anderson_darling_test(&data).expect("should compute");
2483 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2484 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2485 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2486 }
2487
2488 #[test]
2489 fn ad_skewed_data() {
2490 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];
2492 let r = anderson_darling_test(&data).expect("should compute");
2493 assert!(
2494 r.p_value < 0.05,
2495 "p = {} (should reject normality)",
2496 r.p_value
2497 );
2498 }
2499
2500 #[test]
2501 fn ad_bimodal_data() {
2502 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2504 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2505 let r = anderson_darling_test(&data).expect("should compute");
2506 assert!(
2507 r.p_value < 0.01,
2508 "p = {} (bimodal should reject normality)",
2509 r.p_value
2510 );
2511 }
2512
2513 #[test]
2514 fn ad_large_normal_sample() {
2515 let n = 100;
2516 let data: Vec<f64> = (1..=n)
2517 .map(|i| {
2518 let p = (i as f64 - 0.5) / n as f64;
2519 special::inverse_normal_cdf(p)
2520 })
2521 .collect();
2522 let r = anderson_darling_test(&data).expect("should compute");
2523 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2524 }
2525
2526 #[test]
2527 fn ad_edge_cases() {
2528 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());
2531 }
2532
2533 #[test]
2534 fn ad_p_value_ranges() {
2535 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2540 let r = anderson_darling_test(&near_normal).expect("should compute");
2541 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2542
2543 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];
2545 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2546 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2547 }
2548
2549 #[test]
2554 fn ad_normal_data_large_p() {
2555 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2557 let r = anderson_darling_normality(&data).unwrap();
2558 assert!(r.p_value > 0.05, "p={}", r.p_value);
2559 }
2560
2561 #[test]
2562 fn ad_exponential_data_small_p() {
2563 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2565 let r = anderson_darling_normality(&data).unwrap();
2566 assert!(r.p_value < 0.05, "p={}", r.p_value);
2567 }
2568
2569 #[test]
2570 fn ad_statistic_non_negative() {
2571 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2572 let r = anderson_darling_normality(&data).unwrap();
2573 assert!(r.statistic >= 0.0);
2574 assert!(r.statistic_modified >= r.statistic);
2575 }
2576
2577 #[test]
2578 fn ad_p_value_in_range() {
2579 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2580 let r = anderson_darling_normality(&data).unwrap();
2581 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2582 }
2583
2584 #[test]
2585 fn ad_insufficient_data() {
2586 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2588
2589 #[test]
2590 fn ad_degenerate_data() {
2591 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2593 }
2594
2595 #[test]
2596 fn ad_modified_statistic_formula() {
2597 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2599 let r = anderson_darling_normality(&data).unwrap();
2600 assert!(r.statistic_modified > r.statistic - 1e-10);
2601 }
2602
2603 #[test]
2608 fn ad_correction_factor_formula() {
2609 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2610 let n = data.len() as f64;
2611 let r = anderson_darling_normality(&data).unwrap();
2612
2613 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2615 assert!(
2616 (r.statistic_modified - expected_star).abs() < 1e-12,
2617 "A²* = {}, expected = {}",
2618 r.statistic_modified,
2619 expected_star
2620 );
2621
2622 assert!(
2624 r.statistic >= 0.0,
2625 "A² = {} must be non-negative",
2626 r.statistic
2627 );
2628 }
2629
2630 #[test]
2636 fn ad_formula_structure_symmetric_vs_skewed() {
2637 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2639 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2641
2642 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2643 let r_skew = anderson_darling_normality(&skewed).unwrap();
2644
2645 assert!(
2646 r_sym.statistic < r_skew.statistic,
2647 "Symmetric A²={} should be less than skewed A²={}",
2648 r_sym.statistic,
2649 r_skew.statistic
2650 );
2651 }
2652
2653 #[test]
2658 fn ad_pvalue_invariants() {
2659 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2661 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2662 assert!(
2663 r_normal.p_value > 0.05,
2664 "Normal data: p = {} (expected > 0.05)",
2665 r_normal.p_value
2666 );
2667
2668 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2670 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2671 assert!(
2672 r_exp.p_value < 0.05,
2673 "Exponential data: p = {} (expected < 0.05)",
2674 r_exp.p_value
2675 );
2676 }
2677
2678 #[test]
2683 fn sw_normal_data() {
2684 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2686 let r = shapiro_wilk_test(&data).expect("should compute");
2687 assert!(r.w > 0.9, "W = {}", r.w);
2688 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2689 }
2690
2691 #[test]
2692 fn sw_bimodal_data() {
2693 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2695 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2696 let r = shapiro_wilk_test(&data).expect("should compute");
2697 assert!(
2698 r.p_value < 0.01,
2699 "p = {} (bimodal should reject normality)",
2700 r.p_value
2701 );
2702 }
2703
2704 #[test]
2705 fn sw_n3() {
2706 let data = [1.0, 2.0, 3.0];
2707 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2708 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2709 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2710 }
2711
2712 #[test]
2713 fn sw_n4() {
2714 let data = [1.0, 2.0, 3.0, 4.0];
2715 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2716 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2717 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2718 }
2719
2720 #[test]
2721 fn sw_n5() {
2722 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2723 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2724 assert!(r.w > 0.9, "W = {}", r.w);
2725 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2726 }
2727
2728 #[test]
2729 fn sw_skewed_data() {
2730 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];
2732 let r = shapiro_wilk_test(&data).expect("should compute");
2733 assert!(
2734 r.p_value < 0.05,
2735 "p = {} (skewed data should reject normality)",
2736 r.p_value
2737 );
2738 }
2739
2740 #[test]
2741 fn sw_large_normal_sample() {
2742 let n = 100;
2745 let data: Vec<f64> = (1..=n)
2746 .map(|i| {
2747 let p = (i as f64 - 0.5) / n as f64;
2748 special::inverse_normal_cdf(p)
2749 })
2750 .collect();
2751 let r = shapiro_wilk_test(&data).expect("should compute");
2752 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2753 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2754 }
2755
2756 #[test]
2757 fn sw_w_bounded() {
2758 let datasets: Vec<Vec<f64>> = vec![
2760 vec![1.0, 2.0, 3.0],
2761 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2762 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2763 (0..20).map(|i| (i as f64).powi(2)).collect(),
2764 ];
2765 for (idx, data) in datasets.iter().enumerate() {
2766 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2767 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2768 assert!(
2769 r.p_value >= 0.0 && r.p_value <= 1.0,
2770 "dataset {idx}: p = {}",
2771 r.p_value
2772 );
2773 }
2774 }
2775
2776 #[test]
2777 fn sw_edge_cases() {
2778 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()); }
2784
2785 #[test]
2786 fn sw_n5001_rejected() {
2787 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2788 assert!(shapiro_wilk_test(&data).is_none()); }
2790
2791 #[test]
2796 fn mw_clearly_different() {
2797 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2798 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2799 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2800 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2801 }
2802
2803 #[test]
2804 fn mw_same_distribution() {
2805 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
2806 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
2807 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2808 assert!(
2809 r.p_value > 0.3,
2810 "p = {} (interleaved, same dist)",
2811 r.p_value
2812 );
2813 }
2814
2815 #[test]
2816 fn mw_with_ties() {
2817 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
2818 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
2819 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2820 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
2821 }
2822
2823 #[test]
2824 fn mw_different_sizes() {
2825 let a = [1.0, 2.0, 3.0];
2826 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2827 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2828 assert!(r.p_value < 0.05);
2829 }
2830
2831 #[test]
2832 fn mw_edge_cases() {
2833 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());
2836 }
2837
2838 #[test]
2843 fn wilcoxon_significant_increase() {
2844 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2845 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
2846 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
2847 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
2848 }
2849
2850 #[test]
2851 fn wilcoxon_no_difference() {
2852 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2853 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
2854 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2855 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
2856 }
2857
2858 #[test]
2859 fn wilcoxon_with_ties() {
2860 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2862 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2864 assert!(r.statistic > 0.0);
2866 }
2867
2868 #[test]
2869 fn wilcoxon_with_zero_diffs() {
2870 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
2872 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
2874 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2875 }
2876
2877 #[test]
2878 fn wilcoxon_edge_cases() {
2879 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());
2883 }
2884
2885 #[test]
2890 fn kw_clearly_different() {
2891 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2892 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2893 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
2894 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2895 assert!(r.p_value < 0.01, "p = {}", r.p_value);
2896 assert!((r.df - 2.0).abs() < 1e-10);
2897 }
2898
2899 #[test]
2900 fn kw_same_distribution() {
2901 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
2902 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
2903 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
2904 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
2905 }
2906
2907 #[test]
2908 fn kw_with_ties() {
2909 let g1 = [1.0, 2.0, 2.0, 3.0];
2910 let g2 = [3.0, 4.0, 4.0, 5.0];
2911 let g3 = [5.0, 6.0, 6.0, 7.0];
2912 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
2913 assert!(r.statistic > 0.0);
2914 }
2915
2916 #[test]
2917 fn kw_edge_cases() {
2918 let g1 = [1.0, 2.0, 3.0];
2919 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
2921
2922 #[test]
2927 fn levene_equal_variance() {
2928 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2929 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
2930 let r = levene_test(&[&g1, &g2]).expect("should compute");
2931 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
2933 }
2934
2935 #[test]
2936 fn levene_unequal_variance() {
2937 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");
2940 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
2941 }
2942
2943 #[test]
2944 fn levene_three_groups() {
2945 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
2946 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
2947 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
2948 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
2949 assert!(r.df >= 2.0);
2950 }
2951
2952 #[test]
2953 fn levene_edge_cases() {
2954 let g1 = [1.0, 2.0, 3.0];
2955 assert!(levene_test(&[&g1]).is_none()); }
2957
2958 #[test]
2963 fn bonferroni_basic() {
2964 let ps = [0.01, 0.04, 0.03, 0.005];
2965 let adj = bonferroni_correction(&ps).expect("should compute");
2966 assert!((adj[0] - 0.04).abs() < 1e-10);
2967 assert!((adj[1] - 0.16).abs() < 1e-10);
2968 assert!((adj[2] - 0.12).abs() < 1e-10);
2969 assert!((adj[3] - 0.02).abs() < 1e-10);
2970 }
2971
2972 #[test]
2973 fn bonferroni_capped_at_one() {
2974 let ps = [0.5, 0.6];
2975 let adj = bonferroni_correction(&ps).expect("should compute");
2976 assert!((adj[0] - 1.0).abs() < 1e-10);
2977 assert!((adj[1] - 1.0).abs() < 1e-10);
2978 }
2979
2980 #[test]
2981 fn bh_basic() {
2982 let ps = [0.01, 0.04, 0.03, 0.005];
2983 let adj = benjamini_hochberg(&ps).expect("should compute");
2984 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
2986 assert!(
2987 adjusted >= orig - 1e-15,
2988 "adj[{i}] = {adjusted} < original {orig}"
2989 );
2990 }
2991 }
2994
2995 #[test]
2996 fn bh_all_significant() {
2997 let ps = [0.001, 0.002, 0.003];
2998 let adj = benjamini_hochberg(&ps).expect("should compute");
2999 for &a in &adj {
3000 assert!(a < 0.05);
3001 }
3002 }
3003
3004 #[test]
3005 fn correction_edge_cases() {
3006 assert!(bonferroni_correction(&[]).is_none());
3007 assert!(benjamini_hochberg(&[]).is_none());
3008 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3009 }
3010
3011 #[test]
3016 fn bartlett_equal_variances() {
3017 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3018 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3019 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3020 assert!(
3021 r.p_value > 0.9,
3022 "equal variance → p high, got {}",
3023 r.p_value
3024 );
3025 }
3026
3027 #[test]
3028 fn bartlett_unequal_variances() {
3029 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");
3032 assert!(
3033 r.p_value < 0.01,
3034 "very different variances → p < 0.01, got {}",
3035 r.p_value
3036 );
3037 assert!((r.df - 1.0).abs() < 1e-10); }
3039
3040 #[test]
3041 fn bartlett_three_groups() {
3042 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3043 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3044 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3046 assert!(r.p_value < 0.05, "one group with high variance");
3047 assert!((r.df - 2.0).abs() < 1e-10); }
3049
3050 #[test]
3051 fn bartlett_edge_cases() {
3052 let g1 = [1.0, 2.0, 3.0];
3053 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3057
3058 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3060 }
3061
3062 #[test]
3067 fn fisher_tea_tasting() {
3068 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3070 assert!(
3072 (r.p_value - 0.4857).abs() < 0.01,
3073 "p ≈ 0.4857, got {}",
3074 r.p_value
3075 );
3076 }
3077
3078 #[test]
3079 fn fisher_significant() {
3080 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3082 assert!(r.p_value < 0.001, "perfect association → p very small");
3083 }
3084
3085 #[test]
3086 fn fisher_no_association() {
3087 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3089 assert!(
3090 r.p_value > 0.9,
3091 "no association → p ≈ 1.0, got {}",
3092 r.p_value
3093 );
3094 }
3095
3096 #[test]
3097 fn fisher_small_table() {
3098 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3100 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3101 }
3102
3103 #[test]
3104 fn fisher_asymmetric() {
3105 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3107 assert!(r.p_value < 0.05, "significant association");
3108 }
3109
3110 #[test]
3111 fn fisher_edge_cases() {
3112 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()); }
3117
3118 #[test]
3119 fn fisher_odds_ratio() {
3120 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3121 assert!(
3123 (r.statistic - 9.0).abs() < 1e-10,
3124 "OR = 9, got {}",
3125 r.statistic
3126 );
3127 }
3128
3129 #[test]
3134 fn mk_increasing_trend() {
3135 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3136 let r = mann_kendall_test(&data).expect("should compute");
3137 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3138 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3139 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3140 assert!(r.s_statistic > 0);
3141 }
3142
3143 #[test]
3144 fn mk_decreasing_trend() {
3145 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3146 let r = mann_kendall_test(&data).expect("should compute");
3147 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3148 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3149 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3150 assert!(r.s_statistic < 0);
3151 }
3152
3153 #[test]
3154 fn mk_no_trend() {
3155 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3157 let r = mann_kendall_test(&data).expect("should compute");
3158 assert!(
3160 r.p_value > 0.05,
3161 "p = {} (should be > 0.05 for no trend)",
3162 r.p_value
3163 );
3164 }
3165
3166 #[test]
3167 fn mk_perfect_monotone() {
3168 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3169 let r = mann_kendall_test(&data).expect("should compute");
3170 assert_eq!(r.s_statistic, 45);
3172 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3173 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3174 }
3175
3176 #[test]
3177 fn mk_with_ties() {
3178 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3179 let r = mann_kendall_test(&data).expect("should compute");
3180 assert!(r.s_statistic > 0);
3181 let n = data.len() as f64;
3183 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3184 assert!(r.variance < base_var, "ties should reduce variance");
3185 }
3186
3187 #[test]
3188 fn mk_edge_cases() {
3189 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3191 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3193 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3195 }
3196
3197 #[test]
3198 fn mk_minimum_n() {
3199 let data = [1.0, 2.0, 3.0, 4.0];
3201 let r = mann_kendall_test(&data).expect("n=4 should work");
3202 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3204 }
3205
3206 #[test]
3207 fn mk_sen_slope_robust_to_outlier() {
3208 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3210 let r = mann_kendall_test(&data).expect("should compute");
3211 assert!(
3213 (r.sen_slope - 1.0).abs() < 0.5,
3214 "sen slope = {}, expected ≈ 1.0",
3215 r.sen_slope
3216 );
3217 }
3218
3219 #[test]
3224 fn adf_stationary_mean_reverting() {
3225 let data = [
3228 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,
3229 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,
3230 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,
3231 ];
3232 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3233 assert!(
3235 r.rejected[2],
3236 "should reject at 10%: stat={}, cv={}",
3237 r.statistic, r.critical_values[2]
3238 );
3239 }
3240
3241 #[test]
3242 fn adf_nonstationary_random_walk() {
3243 let increments = [
3245 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,
3246 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,
3247 -0.13, 0.15, -0.1,
3248 ];
3249 let mut walk = Vec::with_capacity(increments.len());
3250 let mut cum = 0.0;
3251 for &inc in &increments {
3252 cum += inc;
3253 walk.push(cum);
3254 }
3255 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3256 assert!(
3258 !r.rejected[0],
3259 "should NOT reject at 1%: stat={}, cv={}",
3260 r.statistic, r.critical_values[0]
3261 );
3262 }
3263
3264 #[test]
3265 fn adf_with_fixed_lags() {
3266 let data: Vec<f64> = (0..50)
3268 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3269 .collect();
3270 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3271 assert_eq!(r.n_lags, 2);
3272 assert!(r.statistic.is_finite());
3273 }
3274
3275 #[test]
3276 fn adf_constant_trend_model() {
3277 let data: Vec<f64> = (0..30)
3280 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3281 .collect();
3282 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3283 assert!(r.statistic.is_finite());
3284 assert_eq!(r.critical_values.len(), 3);
3285 }
3286
3287 #[test]
3288 fn adf_edge_cases() {
3289 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3291 let mut data = vec![0.0; 20];
3293 data[5] = f64::NAN;
3294 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3295 }
3296
3297 #[test]
3298 fn adf_critical_values_constant() {
3299 let cv = adf_critical_values(AdfModel::Constant, 100);
3301 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3303 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3304 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3305 }
3306
3307 #[test]
3308 fn adf_critical_values_ordering() {
3309 let cv = adf_critical_values(AdfModel::Constant, 50);
3310 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3312 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3313 }
3314}
3315
3316#[cfg(test)]
3317mod proptests {
3318 use super::*;
3319 use proptest::prelude::*;
3320
3321 proptest! {
3322 #[test]
3323 fn one_sample_p_bounded(
3324 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3325 mu0 in -1e3_f64..1e3
3326 ) {
3327 if let Some(r) = one_sample_t_test(&data, mu0) {
3328 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3329 }
3330 }
3331
3332 #[test]
3333 fn two_sample_p_bounded(
3334 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3335 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3336 ) {
3337 if let Some(r) = two_sample_t_test(&a, &b) {
3338 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3339 }
3340 }
3341
3342 #[test]
3343 fn anova_p_bounded(
3344 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3345 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3346 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3347 ) {
3348 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3349 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3350 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3351 }
3352 }
3353
3354 #[test]
3355 fn bonferroni_monotone(
3356 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3357 ) {
3358 let adj = bonferroni_correction(&ps).expect("should compute");
3359 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3360 prop_assert!(adjusted >= orig - 1e-15,
3361 "adj[{i}] = {adjusted} < orig = {orig}");
3362 }
3363 }
3364
3365 #[test]
3366 fn shapiro_wilk_p_bounded(
3367 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3368 ) {
3369 if let Some(r) = shapiro_wilk_test(&data) {
3370 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3371 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3372 }
3373 }
3374
3375 #[test]
3376 fn anderson_darling_p_bounded(
3377 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3378 ) {
3379 if let Some(r) = anderson_darling_test(&data) {
3380 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3381 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3382 }
3383 }
3384
3385 #[test]
3386 fn mann_whitney_p_bounded(
3387 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3388 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3389 ) {
3390 if let Some(r) = mann_whitney_u_test(&a, &b) {
3391 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3392 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3393 }
3394 }
3395
3396 #[test]
3397 fn wilcoxon_p_bounded(
3398 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3399 ) {
3400 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3401 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3402 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3403 }
3404 }
3405
3406 #[test]
3407 fn bartlett_p_bounded(
3408 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3409 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3410 ) {
3411 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3412 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3413 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3414 }
3415 }
3416
3417 #[test]
3418 fn fisher_p_bounded(
3419 a in 0_u64..20,
3420 b in 0_u64..20,
3421 c in 0_u64..20,
3422 d in 0_u64..20,
3423 ) {
3424 if let Some(r) = fisher_exact_test(a, b, c, d) {
3425 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3426 }
3427 }
3428
3429 #[test]
3430 fn mann_kendall_p_bounded(
3431 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3432 ) {
3433 if let Some(r) = mann_kendall_test(&data) {
3434 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3435 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3436 "tau = {}", r.kendall_tau);
3437 }
3438 }
3439
3440 #[test]
3441 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3442 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3443 let r = mann_kendall_test(&data).expect("should compute");
3444 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3445 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3446 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3447 }
3448 }
3449}