1use u_numflow::special;
17use u_numflow::stats;
18
19#[derive(Debug, Clone, Copy)]
21pub struct TestResult {
22 pub statistic: f64,
24 pub df: f64,
26 pub p_value: f64,
28}
29
30pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
54 let n = data.len();
55 if n < 2 {
56 return None;
57 }
58 if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
59 return None;
60 }
61
62 let mean = stats::mean(data)?;
63 let sd = stats::std_dev(data)?;
64
65 if sd < 1e-300 {
66 return None; }
68
69 let t = (mean - mu0) / (sd / (n as f64).sqrt());
70 let df = (n - 1) as f64;
71 let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
72
73 Some(TestResult {
74 statistic: t,
75 df,
76 p_value,
77 })
78}
79
80pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
107 let n1 = a.len();
108 let n2 = b.len();
109 if n1 < 2 || n2 < 2 {
110 return None;
111 }
112 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
113 return None;
114 }
115
116 let mean1 = stats::mean(a)?;
117 let mean2 = stats::mean(b)?;
118 let var1 = stats::variance(a)?;
119 let var2 = stats::variance(b)?;
120
121 let n1f = n1 as f64;
122 let n2f = n2 as f64;
123
124 let se_sq = var1 / n1f + var2 / n2f;
125 if se_sq < 1e-300 {
126 return None;
127 }
128
129 let t = (mean1 - mean2) / se_sq.sqrt();
130
131 let v1 = var1 / n1f;
133 let v2 = var2 / n2f;
134 let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
135
136 let p_value = 2.0 * (1.0 - special::t_distribution_cdf(t.abs(), df));
137
138 Some(TestResult {
139 statistic: t,
140 df,
141 p_value,
142 })
143}
144
145pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
167 if x.len() != y.len() || x.len() < 2 {
168 return None;
169 }
170
171 let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
172 one_sample_t_test(&diffs, 0.0)
173}
174
175#[derive(Debug, Clone)]
181pub struct AnovaResult {
182 pub f_statistic: f64,
184 pub df_between: usize,
186 pub df_within: usize,
188 pub p_value: f64,
190 pub ss_between: f64,
192 pub ss_within: f64,
194 pub ms_between: f64,
196 pub ms_within: f64,
198 pub group_means: Vec<f64>,
200 pub grand_mean: f64,
202}
203
204pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
233 let k = groups.len();
234 if k < 2 {
235 return None;
236 }
237
238 for g in groups {
239 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
240 return None;
241 }
242 }
243
244 let total_n: usize = groups.iter().map(|g| g.len()).sum();
245
246 let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
248 let grand_mean = grand_sum / total_n as f64;
249
250 let group_means: Vec<f64> = groups
252 .iter()
253 .map(|g| g.iter().sum::<f64>() / g.len() as f64)
254 .collect();
255
256 let ss_between: f64 = groups
258 .iter()
259 .zip(group_means.iter())
260 .map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
261 .sum();
262
263 let ss_within: f64 = groups
264 .iter()
265 .zip(group_means.iter())
266 .map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
267 .sum();
268
269 let df_between = k - 1;
270 let df_within = total_n - k;
271
272 if df_within == 0 {
273 return None;
274 }
275
276 let ms_between = ss_between / df_between as f64;
277 let ms_within = ss_within / df_within as f64;
278
279 let f_statistic = if ms_within > 1e-300 {
280 ms_between / ms_within
281 } else {
282 f64::INFINITY
283 };
284
285 let p_value = if f_statistic.is_infinite() {
286 0.0
287 } else {
288 1.0 - special::f_distribution_cdf(f_statistic, df_between as f64, df_within as f64)
289 };
290
291 Some(AnovaResult {
292 f_statistic,
293 df_between,
294 df_within,
295 p_value,
296 ss_between,
297 ss_within,
298 ms_between,
299 ms_within,
300 group_means,
301 grand_mean,
302 })
303}
304
305pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
331 let k = observed.len();
332 if k < 2 || k != expected.len() {
333 return None;
334 }
335
336 for &e in expected {
337 if e <= 0.0 || !e.is_finite() {
338 return None;
339 }
340 }
341 for &o in observed {
342 if o < 0.0 || !o.is_finite() {
343 return None;
344 }
345 }
346
347 let chi2: f64 = observed
348 .iter()
349 .zip(expected.iter())
350 .map(|(&o, &e)| (o - e).powi(2) / e)
351 .sum();
352
353 let df = (k - 1) as f64;
354 let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
355
356 Some(TestResult {
357 statistic: chi2,
358 df,
359 p_value,
360 })
361}
362
363pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
392 if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
393 return None;
394 }
395
396 for &v in table {
397 if v < 0.0 || !v.is_finite() {
398 return None;
399 }
400 }
401
402 let mut row_sums = vec![0.0; n_rows];
404 let mut col_sums = vec![0.0; n_cols];
405 let mut total = 0.0;
406
407 for i in 0..n_rows {
408 for j in 0..n_cols {
409 let val = table[i * n_cols + j];
410 row_sums[i] += val;
411 col_sums[j] += val;
412 total += val;
413 }
414 }
415
416 if total <= 0.0 {
417 return None;
418 }
419
420 for &r in &row_sums {
422 if r <= 0.0 {
423 return None;
424 }
425 }
426 for &c in &col_sums {
427 if c <= 0.0 {
428 return None;
429 }
430 }
431
432 let mut chi2 = 0.0;
434 for i in 0..n_rows {
435 for j in 0..n_cols {
436 let observed = table[i * n_cols + j];
437 let expected = row_sums[i] * col_sums[j] / total;
438 chi2 += (observed - expected).powi(2) / expected;
439 }
440 }
441
442 let df = ((n_rows - 1) * (n_cols - 1)) as f64;
443 let p_value = 1.0 - special::chi_squared_cdf(chi2, df);
444
445 Some(TestResult {
446 statistic: chi2,
447 df,
448 p_value,
449 })
450}
451
452pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
497 let n = data.len();
498 if n < 8 {
499 return None;
500 }
501 if data.iter().any(|v| !v.is_finite()) {
502 return None;
503 }
504
505 let g1 = stats::skewness_moment(data)?;
506 let g2 = stats::kurtosis_moment(data)?;
507
508 let nf = n as f64;
509 let jb = (nf / 6.0) * (g1 * g1 + g2 * g2 / 4.0);
510 let p_value = (-jb / 2.0).exp();
512
513 Some(TestResult {
514 statistic: jb,
515 df: 2.0,
516 p_value,
517 })
518}
519
520#[derive(Debug, Clone, Copy)]
522pub struct AndersonDarlingResult {
523 pub statistic: f64,
525 pub statistic_star: f64,
527 pub p_value: f64,
529}
530
531fn ad_upper_tail_pvalue(a2_star: f64) -> f64 {
551 const UPPER_BRANCH_VERTEX: f64 = 153.467_741_935_483_87; let p = if a2_star >= 0.6 {
556 let x = a2_star.min(UPPER_BRANCH_VERTEX);
557 (1.2937 - 5.709 * x + 0.0186 * x * x).exp()
558 } else if a2_star > 0.34 {
559 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
560 } else if a2_star > 0.2 {
561 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
562 } else {
563 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
564 };
565 p.clamp(0.0, 1.0)
566}
567
568pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
601 let n = data.len();
602 if n < 8 {
603 return None;
604 }
605 if data.iter().any(|v| !v.is_finite()) {
606 return None;
607 }
608
609 let mean = stats::mean(data)?;
610 let sd = stats::std_dev(data)?;
611
612 if sd < 1e-300 {
613 return None; }
615
616 let mut x: Vec<f64> = data.to_vec();
618 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
619
620 let nf = n as f64;
621
622 let mut s = 0.0;
624 for i in 0..n {
625 let z = (x[i] - mean) / sd;
626 let z_rev = (x[n - 1 - i] - mean) / sd;
627 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
631 let ln_sf_rev = special::standard_normal_sf(z_rev)
632 .max(f64::MIN_POSITIVE)
633 .ln();
634
635 let coeff = (2 * (i + 1) - 1) as f64;
636 s += coeff * (ln_phi + ln_sf_rev);
637 }
638
639 let a2 = -nf - s / nf;
640
641 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
643
644 let p = ad_upper_tail_pvalue(a2_star);
647
648 Some(AndersonDarlingResult {
649 statistic: a2,
650 statistic_star: a2_star,
651 p_value: p,
652 })
653}
654
655#[derive(Debug, Clone, Copy)]
661pub struct AdNormalityResult {
662 pub statistic: f64,
664 pub statistic_modified: f64,
666 pub p_value: f64,
668}
669
670pub fn anderson_darling_normality(data: &[f64]) -> Option<AdNormalityResult> {
704 let n = data.len();
705 if n < 3 {
706 return None;
707 }
708 if data.iter().any(|v| !v.is_finite()) {
709 return None;
710 }
711
712 let mean = stats::mean(data)?;
713 let sd = stats::std_dev(data)?;
714
715 if sd < 1e-15 {
716 return None;
717 }
718
719 let mut x: Vec<f64> = data.to_vec();
720 x.sort_by(|a, b| a.partial_cmp(b).expect("values are finite"));
721
722 let nf = n as f64;
723
724 let mut s = 0.0;
725 for i in 0..n {
726 let z_i = (x[i] - mean) / sd;
727 let z_rev = (x[n - 1 - i] - mean) / sd;
728
729 let ln_phi_i = special::standard_normal_cdf(z_i)
730 .max(f64::MIN_POSITIVE)
731 .ln();
732 let ln_sf_rev = special::standard_normal_sf(z_rev)
733 .max(f64::MIN_POSITIVE)
734 .ln();
735
736 let coeff = (2 * i + 1) as f64;
737 s += coeff * (ln_phi_i + ln_sf_rev);
738 }
739
740 let a2 = -nf - s / nf;
741 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
742
743 let p = ad_upper_tail_pvalue(a2_star);
747
748 Some(AdNormalityResult {
749 statistic: a2,
750 statistic_modified: a2_star,
751 p_value: p,
752 })
753}
754
755#[derive(Debug, Clone, Copy)]
757pub struct ShapiroWilkResult {
758 pub w: f64,
760 pub p_value: f64,
762}
763
764pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
804 let n = data.len();
805 if !(3..=5000).contains(&n) {
806 return None;
807 }
808 if data.iter().any(|v| !v.is_finite()) {
809 return None;
810 }
811
812 let mut x: Vec<f64> = data.to_vec();
814 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
815
816 if x[n - 1] - x[0] < 1e-300 {
817 return None; }
819
820 let nn2 = n / 2;
821
822 if n == 3 {
824 return shapiro_wilk_n3(&x);
825 }
826
827 let a = sw_coefficients(n, nn2)?;
829
830 let w = sw_statistic(&x, &a, n, nn2);
832
833 if !(0.0..=1.0 + 1e-10).contains(&w) {
834 return None;
835 }
836 let w = w.min(1.0);
837
838 let p_value = sw_p_value(w, n);
840
841 Some(ShapiroWilkResult {
842 w,
843 p_value: p_value.clamp(0.0, 1.0),
844 })
845}
846
847fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
849 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
852 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
853 if ss < 1e-300 {
854 return None;
855 }
856
857 let numerator = a1 * (x[2] - x[0]);
858 let w = (numerator * numerator) / ss;
859 let w = w.clamp(0.75, 1.0);
860
861 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
863 let p = p.clamp(0.0, 1.0);
864
865 Some(ShapiroWilkResult { w, p_value: p })
866}
867
868const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
870const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
871const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
872const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
873const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
874const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
875const SW_G: [f64; 2] = [-2.273, 0.459];
876
877fn sw_poly(c: &[f64], x: f64) -> f64 {
879 let mut result = c[c.len() - 1];
880 for i in (0..c.len() - 1).rev() {
881 result = result * x + c[i];
882 }
883 result
884}
885
886fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
888 let mut a = vec![0.0; nn2];
889
890 let mut m = vec![0.0; nn2];
892 let mut summ2 = 0.0;
893 for (i, mi) in m.iter_mut().enumerate() {
894 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
896 *mi = special::inverse_normal_cdf(p);
897 summ2 += *mi * *mi;
898 }
899 summ2 *= 2.0;
900 let ssumm2 = summ2.sqrt();
901 let rsn = 1.0 / (n as f64).sqrt();
902
903 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
905
906 if n <= 5 {
907 let fac_sq = summ2 - 2.0 * m[0] * m[0];
909 let one_minus = 1.0 - 2.0 * a1 * a1;
910 if fac_sq <= 0.0 || one_minus <= 0.0 {
911 return None;
912 }
913 let fac = (fac_sq / one_minus).sqrt();
914 a[0] = a1;
915 for i in 1..nn2 {
916 a[i] = -m[i] / fac;
917 }
918 } else {
919 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
921 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
922 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
923 if fac_sq <= 0.0 || one_minus <= 0.0 {
924 return None;
925 }
926 let fac = (fac_sq / one_minus).sqrt();
927 a[0] = a1;
928 a[1] = a2;
929 for i in 2..nn2 {
930 a[i] = -m[i] / fac;
931 }
932 }
933
934 Some(a)
935}
936
937fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
939 let mut sa = 0.0;
941 for i in 0..nn2 {
942 sa += a[i] * (x[n - 1 - i] - x[i]);
943 }
944
945 let mean = x.iter().sum::<f64>() / n as f64;
947 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
948
949 if ss < 1e-300 {
950 return 1.0; }
952
953 (sa * sa) / ss
954}
955
956fn sw_p_value(w: f64, n: usize) -> f64 {
958 let nf = n as f64;
959
960 if n == 3 {
961 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
963 return p.clamp(0.0, 1.0);
964 }
965
966 let w1 = 1.0 - w;
967 if w1 <= 0.0 {
968 return 1.0; }
970
971 let y = w1.ln();
972
973 if n <= 11 {
974 let gamma = sw_poly(&SW_G, nf);
976 if y >= gamma {
977 return 0.0; }
979 let y2 = -(gamma - y).ln();
980 let m = sw_poly(&SW_C3, nf);
981 let s = sw_poly(&SW_C4, nf).exp();
982 if s < 1e-300 {
983 return 0.0;
984 }
985 let z = (y2 - m) / s;
986 special::standard_normal_sf(z)
987 } else {
988 let xx = nf.ln();
990 let m = sw_poly(&SW_C5, xx);
991 let s = sw_poly(&SW_C6, xx).exp();
992 if s < 1e-300 {
993 return 0.0;
994 }
995 let z = (y - m) / s;
996 special::standard_normal_sf(z)
997 }
998}
999
1000pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
1044 let n1 = a.len();
1045 let n2 = b.len();
1046 if n1 < 2 || n2 < 2 {
1047 return None;
1048 }
1049 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
1050 return None;
1051 }
1052
1053 let n = n1 + n2;
1054 let n1f = n1 as f64;
1055 let n2f = n2 as f64;
1056 let nf = n as f64;
1057
1058 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
1060 for &v in a {
1061 combined.push((v, 0)); }
1063 for &v in b {
1064 combined.push((v, 1)); }
1066 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
1067
1068 let ranks = average_ranks(&combined);
1070
1071 let r1: f64 = combined
1073 .iter()
1074 .zip(ranks.iter())
1075 .filter(|((_, g), _)| *g == 0)
1076 .map(|(_, &r)| r)
1077 .sum();
1078
1079 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
1081
1082 let tie_correction = compute_tie_correction(&combined);
1084
1085 let mu = n1f * n2f / 2.0;
1087 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
1088
1089 if sigma_sq <= 0.0 {
1090 return None;
1091 }
1092
1093 let p_value = if tie_correction == 0.0 && n1 <= EXACT_MAX_N && n2 <= EXACT_MAX_N {
1094 let u = u1.round() as usize;
1097 let tail = u.min(n1 * n2 - u);
1098 (2.0 * mann_whitney_exact_cdf(n1, n2, tail)).min(1.0)
1099 } else {
1100 continuity_corrected_p(u1, mu, sigma_sq)
1101 };
1102
1103 Some(TestResult {
1104 statistic: u1,
1105 df: 0.0, p_value,
1107 })
1108}
1109
1110const EXACT_MAX_N: usize = 50;
1114
1115fn continuity_corrected_p(statistic: f64, mu: f64, sigma_sq: f64) -> f64 {
1118 let z = ((statistic - mu).abs() - 0.5).max(0.0) / sigma_sq.sqrt();
1119 (2.0 * special::standard_normal_sf(z)).min(1.0)
1120}
1121
1122fn mann_whitney_exact_cdf(m: usize, n: usize, u: usize) -> f64 {
1131 let max_u = m * n;
1132 let mut prev: Vec<Vec<f64>> = (0..=n).map(|_| vec![1.0]).collect(); for i in 1..=m {
1135 let mut cur: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
1136 cur.push(vec![1.0]); for j in 1..=n {
1138 let mut dist = vec![0.0; i * j + 1];
1139 let wi = i as f64 / (i + j) as f64;
1140 let wj = j as f64 / (i + j) as f64;
1141 for (k, &pk) in prev[j].iter().enumerate() {
1143 dist[k + j] += wi * pk;
1144 }
1145 for (k, &pk) in cur[j - 1].iter().enumerate() {
1147 dist[k] += wj * pk;
1148 }
1149 cur.push(dist);
1150 }
1151 prev = cur;
1152 }
1153 prev[n].iter().take(u.min(max_u) + 1).sum::<f64>().min(1.0)
1154}
1155
1156fn wilcoxon_exact_cdf(n: usize, t: usize) -> f64 {
1159 let max_t = n * (n + 1) / 2;
1160 let mut dist = vec![0.0; max_t + 1];
1161 dist[0] = 1.0;
1162 let mut reach = 0;
1163 for rank in 1..=n {
1164 reach += rank;
1165 for s in (rank..=reach).rev() {
1166 dist[s] = 0.5 * dist[s] + 0.5 * dist[s - rank];
1167 }
1168 for d in dist.iter_mut().take(rank) {
1169 *d *= 0.5;
1170 }
1171 }
1172 dist.iter().take(t.min(max_t) + 1).sum::<f64>().min(1.0)
1173}
1174
1175pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1215 if x.len() != y.len() || x.len() < 2 {
1216 return None;
1217 }
1218 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1219 return None;
1220 }
1221
1222 let diffs: Vec<f64> = x
1224 .iter()
1225 .zip(y.iter())
1226 .map(|(&a, &b)| a - b)
1227 .filter(|&d| d.abs() > 1e-300)
1228 .collect();
1229
1230 let nr = diffs.len();
1231 if nr < 2 {
1232 return None;
1233 }
1234
1235 let nf = nr as f64;
1236
1237 let mut abs_diffs: Vec<(f64, usize)> = diffs
1239 .iter()
1240 .enumerate()
1241 .map(|(i, &d)| (d.abs(), i))
1242 .collect();
1243 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1244
1245 let ranks = average_ranks(&abs_diffs);
1247
1248 let t_plus: f64 = abs_diffs
1250 .iter()
1251 .zip(ranks.iter())
1252 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1253 .map(|(_, &r)| r)
1254 .sum();
1255
1256 let tie_correction_val = compute_tie_correction(&abs_diffs);
1258
1259 let mu = nf * (nf + 1.0) / 4.0;
1261 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1262
1263 if sigma_sq <= 0.0 {
1264 return None;
1265 }
1266
1267 let p_value = if tie_correction_val == 0.0 && nr <= EXACT_MAX_N {
1268 let t = t_plus.round() as usize;
1271 let tail = t.min(nr * (nr + 1) / 2 - t);
1272 (2.0 * wilcoxon_exact_cdf(nr, tail)).min(1.0)
1273 } else {
1274 continuity_corrected_p(t_plus, mu, sigma_sq)
1275 };
1276
1277 Some(TestResult {
1278 statistic: t_plus,
1279 df: 0.0,
1280 p_value,
1281 })
1282}
1283
1284fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1287 let n = sorted.len();
1288 let mut ranks = vec![0.0; n];
1289 let mut i = 0;
1290 while i < n {
1291 let mut j = i + 1;
1292 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1293 j += 1;
1294 }
1295 let avg_rank = (i + 1 + j) as f64 / 2.0;
1297 for rank in ranks.iter_mut().take(j).skip(i) {
1298 *rank = avg_rank;
1299 }
1300 i = j;
1301 }
1302 ranks
1303}
1304
1305fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1307 let n = sorted.len();
1308 let mut correction = 0.0;
1309 let mut i = 0;
1310 while i < n {
1311 let mut j = i + 1;
1312 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1313 j += 1;
1314 }
1315 let t = (j - i) as f64;
1316 if t > 1.0 {
1317 correction += t * (t * t - 1.0);
1318 }
1319 i = j;
1320 }
1321 correction
1322}
1323
1324pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1356 let k = groups.len();
1357 if k < 2 {
1358 return None;
1359 }
1360 for g in groups {
1361 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1362 return None;
1363 }
1364 }
1365
1366 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1367 let nf = total_n as f64;
1368
1369 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1371 for (gi, g) in groups.iter().enumerate() {
1372 for &v in *g {
1373 combined.push((v, gi));
1374 }
1375 }
1376 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1377
1378 let ranks = average_ranks(&combined);
1379
1380 let mut rank_sums = vec![0.0; k];
1382 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1383 rank_sums[*gi] += r;
1384 }
1385
1386 let mean_rank = (nf + 1.0) / 2.0;
1388 let mut h = 0.0;
1389 for (gi, g) in groups.iter().enumerate() {
1390 let ni = g.len() as f64;
1391 let mean_rank_i = rank_sums[gi] / ni;
1392 h += ni * (mean_rank_i - mean_rank).powi(2);
1393 }
1394 h *= 12.0 / (nf * (nf + 1.0));
1395
1396 let tie_corr = compute_tie_correction(&combined);
1398 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1399 if denom > 1e-15 {
1400 h /= denom;
1401 }
1402
1403 let df = (k - 1) as f64;
1404 let p_value = 1.0 - special::chi_squared_cdf(h, df);
1405
1406 Some(TestResult {
1407 statistic: h,
1408 df,
1409 p_value,
1410 })
1411}
1412
1413pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1449 let k = groups.len();
1450 if k < 2 {
1451 return None;
1452 }
1453 for g in groups {
1454 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1455 return None;
1456 }
1457 }
1458
1459 let z_groups: Vec<Vec<f64>> = groups
1461 .iter()
1462 .map(|g| {
1463 let median = stats::median(g).unwrap_or(0.0);
1464 g.iter().map(|&x| (x - median).abs()).collect()
1465 })
1466 .collect();
1467
1468 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1470 let anova = one_way_anova(&z_refs)?;
1471
1472 Some(TestResult {
1473 statistic: anova.f_statistic,
1474 df: anova.df_between as f64,
1475 p_value: anova.p_value,
1476 })
1477}
1478
1479pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1491 if p_values.is_empty() || p_values.iter().any(|v| !v.is_finite()) {
1492 return None;
1493 }
1494 let m = p_values.len() as f64;
1495 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1496}
1497
1498pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1517 let m = p_values.len();
1518 if m == 0 || p_values.iter().any(|v| !v.is_finite()) {
1519 return None;
1520 }
1521
1522 let mut indices: Vec<usize> = (0..m).collect();
1524 indices.sort_by(|&a, &b| {
1525 p_values[a]
1526 .partial_cmp(&p_values[b])
1527 .unwrap_or(std::cmp::Ordering::Equal)
1528 });
1529
1530 let mf = m as f64;
1531 let mut adjusted = vec![0.0; m];
1532
1533 let mut cummin = f64::INFINITY;
1535 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1536 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1538 cummin = cummin.min(adj);
1539 adjusted[orig_idx] = cummin;
1540 }
1541
1542 Some(adjusted)
1543}
1544
1545pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1582 let k = groups.len();
1583 if k < 2 {
1584 return None;
1585 }
1586
1587 let mut sizes = Vec::with_capacity(k);
1588 let mut vars = Vec::with_capacity(k);
1589 let mut n_total: usize = 0;
1590
1591 for g in groups {
1592 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1593 return None;
1594 }
1595 let n = g.len();
1596 let v = stats::variance(g)?;
1597 if v <= 0.0 {
1598 return None; }
1600 sizes.push(n);
1601 vars.push(v);
1602 n_total += n;
1603 }
1604
1605 let nk = n_total - k; if nk == 0 {
1607 return None;
1608 }
1609 let nk_f = nk as f64;
1610
1611 let s2_pooled: f64 = sizes
1613 .iter()
1614 .zip(vars.iter())
1615 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1616 .sum::<f64>()
1617 / nk_f;
1618
1619 if s2_pooled <= 0.0 {
1620 return None;
1621 }
1622
1623 let num = nk_f * s2_pooled.ln()
1625 - sizes
1626 .iter()
1627 .zip(vars.iter())
1628 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1629 .sum::<f64>();
1630
1631 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1633 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1634
1635 let statistic = num / c;
1636 let df = (k - 1) as f64;
1637 let p_value = 1.0 - special::chi_squared_cdf(statistic, df);
1638
1639 Some(TestResult {
1640 statistic,
1641 df,
1642 p_value,
1643 })
1644}
1645
1646pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1691 let row1 = a + b;
1692 let row2 = c + d;
1693 let col1 = a + c;
1694 let col2 = b + d;
1695 let n = a + b + c + d;
1696
1697 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1699 return None;
1700 }
1701
1702 let log_prob = |a_i: u64| -> f64 {
1704 let b_i = row1 - a_i;
1705 let c_i = col1 - a_i;
1706 let d_i = row2 - c_i;
1707 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1708 - ln_factorial(a_i)
1709 - ln_factorial(b_i)
1710 - ln_factorial(c_i)
1711 - ln_factorial(d_i)
1712 - ln_factorial(n)
1713 };
1714
1715 let a_min = col1.saturating_sub(row2);
1717 let a_max = row1.min(col1);
1718
1719 let log_p_obs = log_prob(a);
1720
1721 let mut p_value = 0.0;
1723 for a_i in a_min..=a_max {
1724 let lp = log_prob(a_i);
1725 if lp <= log_p_obs + 1e-10 {
1727 p_value += lp.exp();
1728 }
1729 }
1730
1731 let p_value = p_value.min(1.0);
1733
1734 let odds_ratio = if b > 0 && c > 0 {
1736 (a as f64 * d as f64) / (b as f64 * c as f64)
1737 } else {
1738 f64::INFINITY
1739 };
1740
1741 Some(TestResult {
1742 statistic: odds_ratio,
1743 df: 1.0,
1744 p_value,
1745 })
1746}
1747
1748#[derive(Debug, Clone, Copy)]
1754pub struct MannKendallResult {
1755 pub s_statistic: i64,
1757 pub variance: f64,
1759 pub z_statistic: f64,
1761 pub p_value: f64,
1763 pub kendall_tau: f64,
1765 pub sen_slope: f64,
1767}
1768
1769pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1807 let n = data.len();
1808 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1809 return None;
1810 }
1811
1812 let mut s: i64 = 0;
1814 for i in 0..n - 1 {
1815 for j in (i + 1)..n {
1816 let diff = data[j] - data[i];
1817 if diff > 0.0 {
1818 s += 1;
1819 } else if diff < 0.0 {
1820 s -= 1;
1821 }
1822 }
1823 }
1824
1825 let mut sorted: Vec<f64> = data.to_vec();
1827 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1828
1829 let mut tie_correction: f64 = 0.0;
1830 let mut current_count: usize = 1;
1831 for i in 1..sorted.len() {
1832 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1833 current_count += 1;
1834 } else {
1835 if current_count > 1 {
1836 let t = current_count as f64;
1837 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1838 }
1839 current_count = 1;
1840 }
1841 }
1842 if current_count > 1 {
1843 let t = current_count as f64;
1844 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1845 }
1846
1847 let nf = n as f64;
1849 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1850
1851 if variance < 1e-300 {
1852 return None; }
1854
1855 let sigma = variance.sqrt();
1857 let z_statistic = if s > 0 {
1858 (s as f64 - 1.0) / sigma
1859 } else if s < 0 {
1860 (s as f64 + 1.0) / sigma
1861 } else {
1862 0.0
1863 };
1864
1865 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1867 let p_value = p_value.clamp(0.0, 1.0);
1868
1869 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1871
1872 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1874 for i in 0..n - 1 {
1875 for j in (i + 1)..n {
1876 let dx = (j - i) as f64;
1877 slopes.push((data[j] - data[i]) / dx);
1878 }
1879 }
1880 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1881 let m = slopes.len();
1882 let sen_slope = if m % 2 == 0 {
1883 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1884 } else {
1885 slopes[m / 2]
1886 };
1887
1888 Some(MannKendallResult {
1889 s_statistic: s,
1890 variance,
1891 z_statistic,
1892 p_value,
1893 kendall_tau,
1894 sen_slope,
1895 })
1896}
1897
1898fn ln_factorial(n: u64) -> f64 {
1900 if n <= 1 {
1901 return 0.0;
1902 }
1903 special::ln_gamma(n as f64 + 1.0)
1905}
1906
1907#[derive(Debug, Clone)]
1913pub struct AdfResult {
1914 pub statistic: f64,
1916 pub n_lags: usize,
1918 pub n_obs: usize,
1920 pub critical_values: [f64; 3],
1922 pub rejected: [bool; 3],
1924}
1925
1926#[derive(Debug, Clone, Copy)]
1928pub enum AdfModel {
1929 None,
1931 Constant,
1933 ConstantTrend,
1935}
1936
1937pub fn adf_test(data: &[f64], model: AdfModel, max_lags: Option<usize>) -> Option<AdfResult> {
1973 let n = data.len();
1974 if n < 10 || data.iter().any(|v| !v.is_finite()) {
1975 return None;
1976 }
1977
1978 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
1980
1981 let best_lag = match max_lags {
1983 Some(p) => p, None => {
1985 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
1987 let p_max = schwert.min(n / 3);
1988 select_adf_lag(data, &dy, model, p_max)
1990 }
1991 };
1992
1993 adf_ols(data, &dy, model, best_lag)
1995}
1996
1997fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> usize {
1999 let mut best_aic = f64::INFINITY;
2000 let mut best_p = 0;
2001
2002 for p in 0..=p_max {
2003 if let Some((aic, _)) = adf_ols_aic(data, dy, model, p) {
2004 if aic < best_aic {
2005 best_aic = aic;
2006 best_p = p;
2007 }
2008 }
2009 }
2010
2011 best_p
2012}
2013
2014#[allow(clippy::type_complexity)]
2018fn adf_build_matrix(
2019 data: &[f64],
2020 dy: &[f64],
2021 model: AdfModel,
2022 p: usize,
2023) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2024 let start = p + 1;
2025 if start >= dy.len() || dy.len() - start < 5 {
2026 return None;
2027 }
2028 let m = dy.len() - start;
2029
2030 let y_dep: Vec<f64> = dy[start..].to_vec();
2031
2032 let has_intercept = !matches!(model, AdfModel::None);
2034 let has_trend = matches!(model, AdfModel::ConstantTrend);
2035 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2036
2037 let mut x_data = Vec::with_capacity(m * ncols);
2039 let mut gamma_col = 0;
2040
2041 for i in 0..m {
2042 let t = start + i;
2043 if has_intercept {
2044 x_data.push(1.0); gamma_col = 1;
2046 }
2047 x_data.push(data[t]); if has_trend {
2049 x_data.push((t + 1) as f64); }
2051 for lag in 1..=p {
2052 x_data.push(dy[t - lag]);
2053 }
2054 }
2055
2056 Some((x_data, y_dep, m, ncols, gamma_col))
2057}
2058
2059fn adf_ols_core(
2063 x_data: &[f64],
2064 y: &[f64],
2065 m: usize,
2066 ncols: usize,
2067 gamma_col: usize,
2068) -> Option<(f64, f64, usize)> {
2069 let mut xtx = vec![0.0_f64; ncols * ncols];
2071 for i in 0..m {
2072 let row = &x_data[i * ncols..(i + 1) * ncols];
2073 for j in 0..ncols {
2074 for k in j..ncols {
2075 xtx[j * ncols + k] += row[j] * row[k];
2076 }
2077 }
2078 }
2079 for j in 0..ncols {
2081 for k in (j + 1)..ncols {
2082 xtx[k * ncols + j] = xtx[j * ncols + k];
2083 }
2084 }
2085
2086 let mut xty = vec![0.0_f64; ncols];
2088 for i in 0..m {
2089 let row = &x_data[i * ncols..(i + 1) * ncols];
2090 for j in 0..ncols {
2091 xty[j] += row[j] * y[i];
2092 }
2093 }
2094
2095 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2097 for i in 0..ncols {
2098 for j in 0..ncols {
2099 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2100 }
2101 augmented[i * (ncols + 1) + ncols] = xty[i];
2102 }
2103
2104 for col in 0..ncols {
2105 let mut max_row = col;
2107 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2108 for row in (col + 1)..ncols {
2109 let val = augmented[row * (ncols + 1) + col].abs();
2110 if val > max_val {
2111 max_val = val;
2112 max_row = row;
2113 }
2114 }
2115 if max_val < 1e-15 {
2116 return None; }
2118 if max_row != col {
2119 for j in 0..=ncols {
2120 let a = col * (ncols + 1) + j;
2121 let b = max_row * (ncols + 1) + j;
2122 augmented.swap(a, b);
2123 }
2124 }
2125
2126 let pivot = augmented[col * (ncols + 1) + col];
2127 for row in (col + 1)..ncols {
2128 let factor = augmented[row * (ncols + 1) + col] / pivot;
2129 for j in col..=ncols {
2130 let above = augmented[col * (ncols + 1) + j];
2131 augmented[row * (ncols + 1) + j] -= factor * above;
2132 }
2133 }
2134 }
2135
2136 let mut beta = vec![0.0_f64; ncols];
2138 for i in (0..ncols).rev() {
2139 let mut sum = augmented[i * (ncols + 1) + ncols];
2140 for j in (i + 1)..ncols {
2141 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2142 }
2143 beta[i] = sum / augmented[i * (ncols + 1) + i];
2144 }
2145
2146 let mut rss = 0.0;
2148 for i in 0..m {
2149 let row = &x_data[i * ncols..(i + 1) * ncols];
2150 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2151 let resid = y[i] - y_hat;
2152 rss += resid * resid;
2153 }
2154
2155 let df = m - ncols;
2157 if df == 0 {
2158 return None;
2159 }
2160 let mse = rss / df as f64;
2161
2162 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2165 for j in 0..ncols {
2166 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2167 }
2168 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2169 }
2170
2171 for col in 0..ncols {
2173 let mut max_row = col;
2174 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2175 for row in (col + 1)..ncols {
2176 let val = xtx_aug[row * 2 * ncols + col].abs();
2177 if val > max_val {
2178 max_val = val;
2179 max_row = row;
2180 }
2181 }
2182 if max_val < 1e-15 {
2183 return None;
2184 }
2185 if max_row != col {
2186 for j in 0..(2 * ncols) {
2187 let a = col * 2 * ncols + j;
2188 let b = max_row * 2 * ncols + j;
2189 xtx_aug.swap(a, b);
2190 }
2191 }
2192
2193 let pivot = xtx_aug[col * 2 * ncols + col];
2194 for j in 0..(2 * ncols) {
2195 xtx_aug[col * 2 * ncols + j] /= pivot;
2196 }
2197 for row in 0..ncols {
2198 if row == col {
2199 continue;
2200 }
2201 let factor = xtx_aug[row * 2 * ncols + col];
2202 for j in 0..(2 * ncols) {
2203 let above = xtx_aug[col * 2 * ncols + j];
2204 xtx_aug[row * 2 * ncols + j] -= factor * above;
2205 }
2206 }
2207 }
2208
2209 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2211 if var_gamma <= 0.0 {
2212 return None;
2213 }
2214 let se_gamma = var_gamma.sqrt();
2215 let t_gamma = beta[gamma_col] / se_gamma;
2216
2217 Some((t_gamma, rss, ncols))
2218}
2219
2220fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<(f64, usize)> {
2222 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2223 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2224 let aic = 2.0 * k as f64 + m as f64 * (rss / m as f64).ln();
2225 Some((aic, m))
2226}
2227
2228fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2230 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p)?;
2231 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2232
2233 let critical_values = adf_critical_values(model, m);
2234
2235 let rejected = [
2236 gamma_t <= critical_values[0],
2237 gamma_t <= critical_values[1],
2238 gamma_t <= critical_values[2],
2239 ];
2240
2241 Some(AdfResult {
2242 statistic: gamma_t,
2243 n_lags: p,
2244 n_obs: m,
2245 critical_values,
2246 rejected,
2247 })
2248}
2249
2250fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2254 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2259 AdfModel::None => (
2260 [-2.5658, -1.9393, -1.6156],
2261 [-1.960, -0.398, -0.181],
2262 [-10.04, 0.0, 0.0],
2263 ),
2264 AdfModel::Constant => (
2265 [-3.4336, -2.8621, -2.5671],
2266 [-5.999, -2.738, -1.438],
2267 [-29.25, -8.36, -4.48],
2268 ),
2269 AdfModel::ConstantTrend => (
2270 [-3.9638, -3.4126, -3.1279],
2271 [-8.353, -4.039, -2.418],
2272 [-47.44, -17.83, -7.58],
2273 ),
2274 };
2275
2276 let nf = n as f64;
2277 let inv_n = 1.0 / nf;
2278 let inv_n2 = inv_n * inv_n;
2279
2280 [
2281 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2282 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2283 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2284 ]
2285}
2286
2287#[cfg(test)]
2288mod tests {
2289 use super::*;
2290
2291 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2297 let mut a = seed;
2298 move || {
2299 a = a.wrapping_add(0x6d2b_79f5);
2300 let mut t = a;
2301 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2302 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2303 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2304 }
2305 }
2306
2307 #[test]
2313 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2314 let mut rng = mulberry32(0x5350_4331);
2315 let full: Vec<f64> = (0..20_000)
2316 .map(|_| {
2317 let u = rng();
2318 -(1.0 - u).ln() })
2320 .collect();
2321
2322 let mut prev_a2 = 0.0_f64;
2323 let mut prev_p = f64::INFINITY;
2324 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2325 let r = anderson_darling_normality(&full[..n]).expect("computes");
2326 assert!(
2328 r.statistic_modified > prev_a2,
2329 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2330 r.statistic_modified
2331 );
2332 assert!(
2334 r.p_value < 0.5,
2335 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2336 r.statistic_modified,
2337 r.p_value
2338 );
2339 assert!(
2341 r.p_value <= prev_p + 1e-12,
2342 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2343 r.p_value
2344 );
2345 prev_a2 = r.statistic_modified;
2346 prev_p = r.p_value;
2347 }
2348 }
2349
2350 #[test]
2353 fn ad_both_functions_immune_to_overflow() {
2354 let mut rng = mulberry32(0x0bad_c0de);
2355 let data: Vec<f64> = (0..9000).map(|_| -(1.0 - rng()).ln()).collect();
2356 let a = anderson_darling_test(&data).expect("computes");
2357 let b = anderson_darling_normality(&data).expect("computes");
2358 assert!(
2359 a.statistic_star > 300.0,
2360 "expected large A*², got {}",
2361 a.statistic_star
2362 );
2363 assert!(
2364 a.p_value < 0.5,
2365 "test() overflowed toward normal: p={}",
2366 a.p_value
2367 );
2368 assert!(
2369 b.p_value < 0.5,
2370 "normality() overflowed toward normal: p={}",
2371 b.p_value
2372 );
2373 }
2374
2375 #[test]
2380 fn one_sample_null_true() {
2381 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2382 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2383 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2384 }
2385
2386 #[test]
2387 fn one_sample_null_false() {
2388 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2389 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2390 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2391 }
2392
2393 #[test]
2394 fn one_sample_edge_cases() {
2395 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());
2398 }
2399
2400 #[test]
2405 fn two_sample_same_mean() {
2406 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2407 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2408 let r = two_sample_t_test(&a, &b).expect("should compute");
2409 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2410 }
2411
2412 #[test]
2413 fn two_sample_different_means() {
2414 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2415 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2416 let r = two_sample_t_test(&a, &b).expect("should compute");
2417 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2418 }
2419
2420 #[test]
2421 fn two_sample_different_sizes() {
2422 let a = [1.0, 2.0, 3.0];
2423 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2424 let r = two_sample_t_test(&a, &b).expect("should compute");
2425 assert!(r.p_value < 0.05);
2426 }
2427
2428 #[test]
2429 fn two_sample_edge_cases() {
2430 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2431 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2432 }
2433
2434 #[test]
2439 fn paired_no_difference() {
2440 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2441 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2442 let r = paired_t_test(&x, &y).expect("should compute");
2443 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2444 }
2445
2446 #[test]
2447 fn paired_significant_difference() {
2448 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2450 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2451 let r = paired_t_test(&before, &after).expect("should compute");
2452 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2453 assert!(r.statistic < 0.0); }
2455
2456 #[test]
2457 fn paired_edge_cases() {
2458 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2461
2462 #[test]
2467 fn anova_same_means() {
2468 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2469 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2470 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2471 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2472 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2473 }
2474
2475 #[test]
2476 fn anova_different_means() {
2477 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2478 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2479 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2480 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2481 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2482 assert!(r.df_between == 2);
2483 assert!(r.df_within == 12);
2484 }
2485
2486 #[test]
2487 fn anova_ss_decomposition() {
2488 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2489 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2490 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2491 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2493 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2494 assert!(
2495 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2496 "SS decomposition: {ss_total} vs {} + {}",
2497 r.ss_between,
2498 r.ss_within
2499 );
2500 }
2501
2502 #[test]
2503 fn anova_edge_cases() {
2504 let g1 = [1.0, 2.0, 3.0];
2505 assert!(one_way_anova(&[&g1]).is_none()); }
2507
2508 #[test]
2513 fn chi2_gof_uniform() {
2514 let observed = [25.0, 25.0, 25.0, 25.0];
2516 let expected = [25.0, 25.0, 25.0, 25.0];
2517 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2518 assert!((r.statistic).abs() < 1e-15);
2519 assert!((r.p_value - 1.0).abs() < 0.01);
2520 }
2521
2522 #[test]
2523 fn chi2_gof_significant() {
2524 let observed = [90.0, 10.0];
2525 let expected = [50.0, 50.0];
2526 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2527 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2528 }
2529
2530 #[test]
2531 fn chi2_gof_edge_cases() {
2532 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()); }
2536
2537 #[test]
2542 fn chi2_independence_significant() {
2543 let table = [30.0, 10.0, 10.0, 50.0];
2545 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2546 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2547 assert!((r.df - 1.0).abs() < 1e-10);
2548 }
2549
2550 #[test]
2551 fn chi2_independence_not_significant() {
2552 let table = [25.0, 25.0, 25.0, 25.0];
2554 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2555 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2556 }
2557
2558 #[test]
2559 fn chi2_independence_3x3() {
2560 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2561 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2562 assert!((r.df - 4.0).abs() < 1e-10);
2563 assert!(r.p_value < 0.05);
2564 }
2565
2566 #[test]
2567 fn chi2_independence_edge_cases() {
2568 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()); }
2572
2573 #[test]
2578 fn jb_normal_data() {
2579 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2581 let r = jarque_bera_test(&data).expect("should compute");
2582 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2583 }
2584
2585 #[test]
2586 fn jb_skewed_data() {
2587 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2589 let r = jarque_bera_test(&data).expect("should compute");
2590 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2591 }
2592
2593 #[test]
2600 fn jb_is_the_moment_form_the_paper_defines() {
2601 let data = [
2602 2.1, 3.4, 1.9, 5.6, 2.2, 3.1, 4.8, 2.9, 3.3, 7.2, 2.5, 3.0, 4.1, 2.7, 3.8, 2.4, 6.1,
2603 3.6, 2.8, 3.2,
2604 ];
2605 let r = jarque_bera_test(&data).expect("20 finite points");
2606 assert!(
2607 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2608 "JB = {}",
2609 r.statistic
2610 );
2611
2612 let n = data.len() as f64;
2615 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2616 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2617 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2618 assert!(
2619 adjusted > r.statistic * 1.3,
2620 "the two forms differ by more than a third here: {adjusted} vs {}",
2621 r.statistic
2622 );
2623 }
2624
2625 #[test]
2629 fn jb_p_value_keeps_its_digits_in_the_tail() {
2630 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2633 data.push(60.0);
2634 let r = jarque_bera_test(&data).expect("20 finite points");
2635 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2636 assert!(
2637 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2638 "this sample is only interesting because the complement underflows"
2639 );
2640 assert!(
2641 r.p_value > 0.0,
2642 "the complement returns 0 here; this must not"
2643 );
2644 assert!(
2645 (r.p_value - 6.702e-21).abs() < 1e-23,
2646 "p = {} (expected about 6.7e-21)",
2647 r.p_value
2648 );
2649 assert!(
2650 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2651 "p = {} vs exp(-JB/2) = {}",
2652 r.p_value,
2653 (-r.statistic / 2.0).exp()
2654 );
2655 let small = jarque_bera_test(&[-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0])
2658 .expect("10 finite points");
2659 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2660 assert!(
2661 (small.p_value - via_cdf).abs() < 1e-12,
2662 "{} vs {via_cdf}",
2663 small.p_value
2664 );
2665 }
2666
2667 #[test]
2668 fn jb_edge_cases() {
2669 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
2671
2672 #[test]
2677 fn ad_normal_data() {
2678 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2679 let r = anderson_darling_test(&data).expect("should compute");
2680 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2681 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
2682 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
2683 }
2684
2685 #[test]
2686 fn ad_skewed_data() {
2687 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];
2689 let r = anderson_darling_test(&data).expect("should compute");
2690 assert!(
2691 r.p_value < 0.05,
2692 "p = {} (should reject normality)",
2693 r.p_value
2694 );
2695 }
2696
2697 #[test]
2698 fn ad_bimodal_data() {
2699 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2701 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2702 let r = anderson_darling_test(&data).expect("should compute");
2703 assert!(
2704 r.p_value < 0.01,
2705 "p = {} (bimodal should reject normality)",
2706 r.p_value
2707 );
2708 }
2709
2710 #[test]
2711 fn ad_large_normal_sample() {
2712 let n = 100;
2713 let data: Vec<f64> = (1..=n)
2714 .map(|i| {
2715 let p = (i as f64 - 0.5) / n as f64;
2716 special::inverse_normal_cdf(p)
2717 })
2718 .collect();
2719 let r = anderson_darling_test(&data).expect("should compute");
2720 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
2721 }
2722
2723 #[test]
2724 fn ad_edge_cases() {
2725 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());
2728 }
2729
2730 #[test]
2731 fn ad_p_value_ranges() {
2732 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
2737 let r = anderson_darling_test(&near_normal).expect("should compute");
2738 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2739
2740 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];
2742 let r = anderson_darling_test(&heavy_tail).expect("should compute");
2743 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2744 }
2745
2746 #[test]
2751 fn ad_normal_data_large_p() {
2752 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2754 let r = anderson_darling_normality(&data).unwrap();
2755 assert!(r.p_value > 0.05, "p={}", r.p_value);
2756 }
2757
2758 #[test]
2759 fn ad_exponential_data_small_p() {
2760 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2762 let r = anderson_darling_normality(&data).unwrap();
2763 assert!(r.p_value < 0.05, "p={}", r.p_value);
2764 }
2765
2766 #[test]
2767 fn ad_statistic_non_negative() {
2768 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2769 let r = anderson_darling_normality(&data).unwrap();
2770 assert!(r.statistic >= 0.0);
2771 assert!(r.statistic_modified >= r.statistic);
2772 }
2773
2774 #[test]
2775 fn ad_p_value_in_range() {
2776 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
2777 let r = anderson_darling_normality(&data).unwrap();
2778 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
2779 }
2780
2781 #[test]
2782 fn ad_insufficient_data() {
2783 assert!(anderson_darling_normality(&[1.0, 2.0]).is_none()); }
2785
2786 #[test]
2787 fn ad_degenerate_data() {
2788 assert!(anderson_darling_normality(&[5.0, 5.0, 5.0, 5.0]).is_none());
2790 }
2791
2792 #[test]
2793 fn ad_modified_statistic_formula() {
2794 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
2796 let r = anderson_darling_normality(&data).unwrap();
2797 assert!(r.statistic_modified > r.statistic - 1e-10);
2798 }
2799
2800 #[test]
2805 fn ad_correction_factor_formula() {
2806 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2807 let n = data.len() as f64;
2808 let r = anderson_darling_normality(&data).unwrap();
2809
2810 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
2812 assert!(
2813 (r.statistic_modified - expected_star).abs() < 1e-12,
2814 "A²* = {}, expected = {}",
2815 r.statistic_modified,
2816 expected_star
2817 );
2818
2819 assert!(
2821 r.statistic >= 0.0,
2822 "A² = {} must be non-negative",
2823 r.statistic
2824 );
2825 }
2826
2827 #[test]
2833 fn ad_formula_structure_symmetric_vs_skewed() {
2834 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
2836 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
2838
2839 let r_sym = anderson_darling_normality(&symmetric).unwrap();
2840 let r_skew = anderson_darling_normality(&skewed).unwrap();
2841
2842 assert!(
2843 r_sym.statistic < r_skew.statistic,
2844 "Symmetric A²={} should be less than skewed A²={}",
2845 r_sym.statistic,
2846 r_skew.statistic
2847 );
2848 }
2849
2850 #[test]
2855 fn ad_pvalue_invariants() {
2856 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
2858 let r_normal = anderson_darling_normality(&normal_data).unwrap();
2859 assert!(
2860 r_normal.p_value > 0.05,
2861 "Normal data: p = {} (expected > 0.05)",
2862 r_normal.p_value
2863 );
2864
2865 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
2867 let r_exp = anderson_darling_normality(&exp_data).unwrap();
2868 assert!(
2869 r_exp.p_value < 0.05,
2870 "Exponential data: p = {} (expected < 0.05)",
2871 r_exp.p_value
2872 );
2873 }
2874
2875 #[test]
2880 fn sw_normal_data() {
2881 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2883 let r = shapiro_wilk_test(&data).expect("should compute");
2884 assert!(r.w > 0.9, "W = {}", r.w);
2885 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2886 }
2887
2888 #[test]
2889 fn sw_bimodal_data() {
2890 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
2892 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
2893 let r = shapiro_wilk_test(&data).expect("should compute");
2894 assert!(
2895 r.p_value < 0.01,
2896 "p = {} (bimodal should reject normality)",
2897 r.p_value
2898 );
2899 }
2900
2901 #[test]
2902 fn sw_n3() {
2903 let data = [1.0, 2.0, 3.0];
2904 let r = shapiro_wilk_test(&data).expect("n=3 should work");
2905 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2906 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2907 }
2908
2909 #[test]
2910 fn sw_n4() {
2911 let data = [1.0, 2.0, 3.0, 4.0];
2912 let r = shapiro_wilk_test(&data).expect("n=4 should work");
2913 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
2914 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
2915 }
2916
2917 #[test]
2918 fn sw_n5() {
2919 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
2920 let r = shapiro_wilk_test(&data).expect("n=5 should work");
2921 assert!(r.w > 0.9, "W = {}", r.w);
2922 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2923 }
2924
2925 #[test]
2926 fn sw_skewed_data() {
2927 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];
2929 let r = shapiro_wilk_test(&data).expect("should compute");
2930 assert!(
2931 r.p_value < 0.05,
2932 "p = {} (skewed data should reject normality)",
2933 r.p_value
2934 );
2935 }
2936
2937 #[test]
2938 fn sw_large_normal_sample() {
2939 let n = 100;
2942 let data: Vec<f64> = (1..=n)
2943 .map(|i| {
2944 let p = (i as f64 - 0.5) / n as f64;
2945 special::inverse_normal_cdf(p)
2946 })
2947 .collect();
2948 let r = shapiro_wilk_test(&data).expect("should compute");
2949 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
2950 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2951 }
2952
2953 #[test]
2954 fn sw_w_bounded() {
2955 let datasets: Vec<Vec<f64>> = vec![
2957 vec![1.0, 2.0, 3.0],
2958 vec![1.0, 1.0, 2.0, 3.0, 3.0],
2959 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
2960 (0..20).map(|i| (i as f64).powi(2)).collect(),
2961 ];
2962 for (idx, data) in datasets.iter().enumerate() {
2963 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
2964 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
2965 assert!(
2966 r.p_value >= 0.0 && r.p_value <= 1.0,
2967 "dataset {idx}: p = {}",
2968 r.p_value
2969 );
2970 }
2971 }
2972
2973 #[test]
2974 fn sw_edge_cases() {
2975 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()); }
2981
2982 #[test]
2983 fn sw_n5001_rejected() {
2984 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
2985 assert!(shapiro_wilk_test(&data).is_none()); }
2987
2988 #[test]
2993 fn mw_clearly_different() {
2994 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
2995 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
2996 let r = mann_whitney_u_test(&a, &b).expect("should compute");
2997 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2998 }
2999
3000 #[test]
3001 fn mw_same_distribution() {
3002 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3003 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3004 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3005 assert!(
3006 r.p_value > 0.3,
3007 "p = {} (interleaved, same dist)",
3008 r.p_value
3009 );
3010 }
3011
3012 #[test]
3013 fn mw_with_ties() {
3014 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3015 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3016 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3017 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3018 }
3019
3020 #[test]
3021 fn mw_different_sizes() {
3022 let a = [1.0, 2.0, 3.0];
3023 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3024 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3025 assert!(r.p_value < 0.05);
3026 }
3027
3028 #[test]
3029 fn mw_edge_cases() {
3030 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());
3033 }
3034
3035 #[test]
3040 fn wilcoxon_significant_increase() {
3041 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3042 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3043 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3044 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3045 }
3046
3047 #[test]
3048 fn wilcoxon_no_difference() {
3049 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3050 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3051 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3052 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3053 }
3054
3055 #[test]
3056 fn wilcoxon_with_ties() {
3057 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3059 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3061 assert!(r.statistic > 0.0);
3063 }
3064
3065 #[test]
3066 fn wilcoxon_with_zero_diffs() {
3067 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3069 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3071 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3072 }
3073
3074 #[test]
3075 fn wilcoxon_edge_cases() {
3076 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());
3080 }
3081
3082 #[test]
3087 fn mann_whitney_small_samples_use_the_exact_distribution() {
3088 let cases = [
3090 (3, 3, 0, 0.1000, 0.0495),
3091 (4, 4, 0, 0.0286, 0.0209),
3092 (4, 4, 1, 0.0571, 0.0433),
3093 (5, 5, 2, 0.0317, 0.0283),
3094 (8, 8, 13, 0.0499, 0.0460),
3095 (10, 10, 23, 0.0433, 0.0413),
3096 ];
3097 for (n1, n2, u, exact, old) in cases {
3098 let (a, b) = separated_samples(n1, n2, u);
3099 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3100 assert_eq!(r.statistic, u as f64);
3101 assert!(
3102 (r.p_value - exact).abs() < 1e-4,
3103 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3104 r.p_value
3105 );
3106 }
3107 }
3108
3109 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3113 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3114 let mut remaining = u;
3115 for k in (0..n1).rev() {
3116 let room = n2.min(remaining);
3117 ranks_a[k] += room;
3118 remaining -= room;
3119 if remaining == 0 {
3120 break;
3121 }
3122 }
3123 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3124 (
3125 ranks_a.iter().map(|&r| r as f64).collect(),
3126 ranks_b.iter().map(|&r| r as f64).collect(),
3127 )
3128 }
3129
3130 #[test]
3132 fn wilcoxon_small_samples_use_the_exact_distribution() {
3133 let cases = [
3135 (5, 0, 0.0625),
3136 (5, 1, 0.125),
3137 (6, 2, 0.09375),
3138 (8, 5, 0.078125),
3139 (10, 8, 0.048828125),
3140 (12, 14, 0.0522460938),
3141 ];
3142 for (n, t_plus, exact) in cases {
3143 let positive = ranks_summing_to(n, t_plus);
3145 let x: Vec<f64> = (1..=n)
3146 .map(|r| {
3147 if positive.contains(&r) {
3148 r as f64
3149 } else {
3150 -(r as f64)
3151 }
3152 })
3153 .collect();
3154 let y = vec![0.0; n];
3155 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3156 assert_eq!(r.statistic, t_plus as f64);
3157 assert!(
3158 (r.p_value - exact).abs() < 1e-9,
3159 "n={n} T+={t_plus}: p={} want {exact}",
3160 r.p_value
3161 );
3162 }
3163 }
3164
3165 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3167 let mut out = Vec::new();
3168 let mut left = target;
3169 for r in (1..=n).rev() {
3170 if r <= left {
3171 out.push(r);
3172 left -= r;
3173 }
3174 }
3175 assert_eq!(left, 0);
3176 out
3177 }
3178
3179 #[test]
3182 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3183 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3184 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3185 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3186 let (n1, n2) = (60.0, 60.0);
3187 let mu = n1 * n2 / 2.0;
3188 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3189 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3190 let want = 2.0 * special::standard_normal_sf(z);
3191 assert!((r.p_value - want).abs() < 1e-12);
3192
3193 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3195 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3196 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3197 let mu = 1250.0;
3198 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3199 let u = mann_whitney_u_test(&a, &b)
3200 .expect("valid samples")
3201 .statistic;
3202 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3203 assert!(
3204 (exact - approx).abs() < 1e-3,
3205 "exact {exact} vs approx {approx}"
3206 );
3207 }
3208
3209 #[test]
3214 fn kw_clearly_different() {
3215 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3216 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3217 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3218 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3219 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3220 assert!((r.df - 2.0).abs() < 1e-10);
3221 }
3222
3223 #[test]
3224 fn kw_same_distribution() {
3225 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3226 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3227 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3228 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3229 }
3230
3231 #[test]
3232 fn kw_with_ties() {
3233 let g1 = [1.0, 2.0, 2.0, 3.0];
3234 let g2 = [3.0, 4.0, 4.0, 5.0];
3235 let g3 = [5.0, 6.0, 6.0, 7.0];
3236 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3237 assert!(r.statistic > 0.0);
3238 }
3239
3240 #[test]
3241 fn kw_edge_cases() {
3242 let g1 = [1.0, 2.0, 3.0];
3243 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3245
3246 #[test]
3251 fn levene_equal_variance() {
3252 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3253 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3254 let r = levene_test(&[&g1, &g2]).expect("should compute");
3255 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3257 }
3258
3259 #[test]
3260 fn levene_unequal_variance() {
3261 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");
3264 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3265 }
3266
3267 #[test]
3268 fn levene_three_groups() {
3269 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3270 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3271 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3272 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3273 assert!(r.df >= 2.0);
3274 }
3275
3276 #[test]
3277 fn levene_edge_cases() {
3278 let g1 = [1.0, 2.0, 3.0];
3279 assert!(levene_test(&[&g1]).is_none()); }
3281
3282 #[test]
3287 fn bonferroni_basic() {
3288 let ps = [0.01, 0.04, 0.03, 0.005];
3289 let adj = bonferroni_correction(&ps).expect("should compute");
3290 assert!((adj[0] - 0.04).abs() < 1e-10);
3291 assert!((adj[1] - 0.16).abs() < 1e-10);
3292 assert!((adj[2] - 0.12).abs() < 1e-10);
3293 assert!((adj[3] - 0.02).abs() < 1e-10);
3294 }
3295
3296 #[test]
3297 fn bonferroni_capped_at_one() {
3298 let ps = [0.5, 0.6];
3299 let adj = bonferroni_correction(&ps).expect("should compute");
3300 assert!((adj[0] - 1.0).abs() < 1e-10);
3301 assert!((adj[1] - 1.0).abs() < 1e-10);
3302 }
3303
3304 #[test]
3305 fn bh_basic() {
3306 let ps = [0.01, 0.04, 0.03, 0.005];
3307 let adj = benjamini_hochberg(&ps).expect("should compute");
3308 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3310 assert!(
3311 adjusted >= orig - 1e-15,
3312 "adj[{i}] = {adjusted} < original {orig}"
3313 );
3314 }
3315 }
3318
3319 #[test]
3320 fn bh_all_significant() {
3321 let ps = [0.001, 0.002, 0.003];
3322 let adj = benjamini_hochberg(&ps).expect("should compute");
3323 for &a in &adj {
3324 assert!(a < 0.05);
3325 }
3326 }
3327
3328 #[test]
3329 fn correction_edge_cases() {
3330 assert!(bonferroni_correction(&[]).is_none());
3331 assert!(benjamini_hochberg(&[]).is_none());
3332 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3333 }
3334
3335 #[test]
3340 fn bartlett_equal_variances() {
3341 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3342 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3343 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3344 assert!(
3345 r.p_value > 0.9,
3346 "equal variance → p high, got {}",
3347 r.p_value
3348 );
3349 }
3350
3351 #[test]
3352 fn bartlett_unequal_variances() {
3353 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");
3356 assert!(
3357 r.p_value < 0.01,
3358 "very different variances → p < 0.01, got {}",
3359 r.p_value
3360 );
3361 assert!((r.df - 1.0).abs() < 1e-10); }
3363
3364 #[test]
3365 fn bartlett_three_groups() {
3366 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3367 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3368 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3370 assert!(r.p_value < 0.05, "one group with high variance");
3371 assert!((r.df - 2.0).abs() < 1e-10); }
3373
3374 #[test]
3375 fn bartlett_edge_cases() {
3376 let g1 = [1.0, 2.0, 3.0];
3377 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3381
3382 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3384 }
3385
3386 #[test]
3391 fn fisher_tea_tasting() {
3392 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3394 assert!(
3396 (r.p_value - 0.4857).abs() < 0.01,
3397 "p ≈ 0.4857, got {}",
3398 r.p_value
3399 );
3400 }
3401
3402 #[test]
3403 fn fisher_significant() {
3404 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3406 assert!(r.p_value < 0.001, "perfect association → p very small");
3407 }
3408
3409 #[test]
3410 fn fisher_no_association() {
3411 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3413 assert!(
3414 r.p_value > 0.9,
3415 "no association → p ≈ 1.0, got {}",
3416 r.p_value
3417 );
3418 }
3419
3420 #[test]
3421 fn fisher_small_table() {
3422 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3424 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3425 }
3426
3427 #[test]
3428 fn fisher_asymmetric() {
3429 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3431 assert!(r.p_value < 0.05, "significant association");
3432 }
3433
3434 #[test]
3435 fn fisher_edge_cases() {
3436 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()); }
3441
3442 #[test]
3443 fn fisher_odds_ratio() {
3444 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3445 assert!(
3447 (r.statistic - 9.0).abs() < 1e-10,
3448 "OR = 9, got {}",
3449 r.statistic
3450 );
3451 }
3452
3453 #[test]
3458 fn mk_increasing_trend() {
3459 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3460 let r = mann_kendall_test(&data).expect("should compute");
3461 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3462 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3463 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3464 assert!(r.s_statistic > 0);
3465 }
3466
3467 #[test]
3468 fn mk_decreasing_trend() {
3469 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3470 let r = mann_kendall_test(&data).expect("should compute");
3471 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3472 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3473 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3474 assert!(r.s_statistic < 0);
3475 }
3476
3477 #[test]
3478 fn mk_no_trend() {
3479 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3481 let r = mann_kendall_test(&data).expect("should compute");
3482 assert!(
3484 r.p_value > 0.05,
3485 "p = {} (should be > 0.05 for no trend)",
3486 r.p_value
3487 );
3488 }
3489
3490 #[test]
3491 fn mk_perfect_monotone() {
3492 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3493 let r = mann_kendall_test(&data).expect("should compute");
3494 assert_eq!(r.s_statistic, 45);
3496 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3497 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3498 }
3499
3500 #[test]
3501 fn mk_with_ties() {
3502 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3503 let r = mann_kendall_test(&data).expect("should compute");
3504 assert!(r.s_statistic > 0);
3505 let n = data.len() as f64;
3507 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3508 assert!(r.variance < base_var, "ties should reduce variance");
3509 }
3510
3511 #[test]
3512 fn mk_edge_cases() {
3513 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3515 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3517 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3519 }
3520
3521 #[test]
3522 fn mk_minimum_n() {
3523 let data = [1.0, 2.0, 3.0, 4.0];
3525 let r = mann_kendall_test(&data).expect("n=4 should work");
3526 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3528 }
3529
3530 #[test]
3531 fn mk_sen_slope_robust_to_outlier() {
3532 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3534 let r = mann_kendall_test(&data).expect("should compute");
3535 assert!(
3537 (r.sen_slope - 1.0).abs() < 0.5,
3538 "sen slope = {}, expected ≈ 1.0",
3539 r.sen_slope
3540 );
3541 }
3542
3543 #[test]
3548 fn adf_stationary_mean_reverting() {
3549 let data = [
3552 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,
3553 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,
3554 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,
3555 ];
3556 let r = adf_test(&data, AdfModel::Constant, None).expect("should compute");
3557 assert!(
3559 r.rejected[2],
3560 "should reject at 10%: stat={}, cv={}",
3561 r.statistic, r.critical_values[2]
3562 );
3563 }
3564
3565 #[test]
3566 fn adf_nonstationary_random_walk() {
3567 let increments = [
3569 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,
3570 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,
3571 -0.13, 0.15, -0.1,
3572 ];
3573 let mut walk = Vec::with_capacity(increments.len());
3574 let mut cum = 0.0;
3575 for &inc in &increments {
3576 cum += inc;
3577 walk.push(cum);
3578 }
3579 let r = adf_test(&walk, AdfModel::Constant, None).expect("should compute");
3580 assert!(
3582 !r.rejected[0],
3583 "should NOT reject at 1%: stat={}, cv={}",
3584 r.statistic, r.critical_values[0]
3585 );
3586 }
3587
3588 #[test]
3589 fn adf_with_fixed_lags() {
3590 let data: Vec<f64> = (0..50)
3592 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64)
3593 .collect();
3594 let r = adf_test(&data, AdfModel::Constant, Some(2)).expect("should compute");
3595 assert_eq!(r.n_lags, 2);
3596 assert!(r.statistic.is_finite());
3597 }
3598
3599 #[test]
3600 fn adf_constant_trend_model() {
3601 let data: Vec<f64> = (0..30)
3604 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3605 .collect();
3606 let r = adf_test(&data, AdfModel::ConstantTrend, None).expect("should compute");
3607 assert!(r.statistic.is_finite());
3608 assert_eq!(r.critical_values.len(), 3);
3609 }
3610
3611 #[test]
3612 fn adf_edge_cases() {
3613 assert!(adf_test(&[1.0; 9], AdfModel::Constant, None).is_none());
3615 let mut data = vec![0.0; 20];
3617 data[5] = f64::NAN;
3618 assert!(adf_test(&data, AdfModel::Constant, None).is_none());
3619 }
3620
3621 #[test]
3622 fn adf_critical_values_constant() {
3623 let cv = adf_critical_values(AdfModel::Constant, 100);
3625 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
3627 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
3628 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
3629 }
3630
3631 #[test]
3632 fn adf_critical_values_ordering() {
3633 let cv = adf_critical_values(AdfModel::Constant, 50);
3634 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
3636 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
3637 }
3638}
3639
3640#[cfg(test)]
3641mod proptests {
3642 use super::*;
3643 use proptest::prelude::*;
3644
3645 proptest! {
3646 #[test]
3647 fn one_sample_p_bounded(
3648 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
3649 mu0 in -1e3_f64..1e3
3650 ) {
3651 if let Some(r) = one_sample_t_test(&data, mu0) {
3652 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3653 }
3654 }
3655
3656 #[test]
3657 fn two_sample_p_bounded(
3658 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3659 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3660 ) {
3661 if let Some(r) = two_sample_t_test(&a, &b) {
3662 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3663 }
3664 }
3665
3666 #[test]
3667 fn anova_p_bounded(
3668 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3669 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3670 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
3671 ) {
3672 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
3673 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3674 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
3675 }
3676 }
3677
3678 #[test]
3679 fn bonferroni_monotone(
3680 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
3681 ) {
3682 let adj = bonferroni_correction(&ps).expect("should compute");
3683 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3684 prop_assert!(adjusted >= orig - 1e-15,
3685 "adj[{i}] = {adjusted} < orig = {orig}");
3686 }
3687 }
3688
3689 #[test]
3690 fn shapiro_wilk_p_bounded(
3691 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
3692 ) {
3693 if let Some(r) = shapiro_wilk_test(&data) {
3694 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3695 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3696 }
3697 }
3698
3699 #[test]
3700 fn anderson_darling_p_bounded(
3701 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
3702 ) {
3703 if let Some(r) = anderson_darling_test(&data) {
3704 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
3705 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3706 }
3707 }
3708
3709 #[test]
3710 fn mann_whitney_p_bounded(
3711 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3712 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3713 ) {
3714 if let Some(r) = mann_whitney_u_test(&a, &b) {
3715 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3716 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
3717 }
3718 }
3719
3720 #[test]
3721 fn wilcoxon_p_bounded(
3722 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
3723 ) {
3724 let zeros: Vec<f64> = vec![0.0; diffs.len()];
3725 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
3726 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3727 }
3728 }
3729
3730 #[test]
3731 fn bartlett_p_bounded(
3732 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3733 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
3734 ) {
3735 if let Some(r) = bartlett_test(&[&g1, &g2]) {
3736 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3737 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
3738 }
3739 }
3740
3741 #[test]
3742 fn fisher_p_bounded(
3743 a in 0_u64..20,
3744 b in 0_u64..20,
3745 c in 0_u64..20,
3746 d in 0_u64..20,
3747 ) {
3748 if let Some(r) = fisher_exact_test(a, b, c, d) {
3749 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3750 }
3751 }
3752
3753 #[test]
3754 fn mann_kendall_p_bounded(
3755 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
3756 ) {
3757 if let Some(r) = mann_kendall_test(&data) {
3758 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3759 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
3760 "tau = {}", r.kendall_tau);
3761 }
3762 }
3763
3764 #[test]
3765 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
3766 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
3767 let r = mann_kendall_test(&data).expect("should compute");
3768 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
3769 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
3770 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
3771 }
3772 }
3773}