1use crate::error::{check_option, check_series, TimeSeriesError};
17use u_numflow::special;
18use u_numflow::stats;
19
20#[derive(Debug, Clone, Copy)]
22pub struct TestResult {
23 pub statistic: f64,
25 pub df: f64,
27 pub p_value: f64,
29}
30
31pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
55 let n = data.len();
56 if n < 2 {
57 return None;
58 }
59 if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
60 return None;
61 }
62
63 let mean = stats::mean(data)?;
64 let sd = stats::std_dev(data)?;
65
66 if sd < 1e-300 {
67 return None; }
69
70 let t = (mean - mu0) / (sd / (n as f64).sqrt());
71 let df = (n - 1) as f64;
72 let p_value = 2.0 * special::t_distribution_sf(t.abs(), df);
73
74 Some(TestResult {
75 statistic: t,
76 df,
77 p_value,
78 })
79}
80
81pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
108 let n1 = a.len();
109 let n2 = b.len();
110 if n1 < 2 || n2 < 2 {
111 return None;
112 }
113 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
114 return None;
115 }
116
117 let mean1 = stats::mean(a)?;
118 let mean2 = stats::mean(b)?;
119 let var1 = stats::variance(a)?;
120 let var2 = stats::variance(b)?;
121
122 let n1f = n1 as f64;
123 let n2f = n2 as f64;
124
125 let se_sq = var1 / n1f + var2 / n2f;
126 if se_sq < 1e-300 {
127 return None;
128 }
129
130 let t = (mean1 - mean2) / se_sq.sqrt();
131
132 let v1 = var1 / n1f;
134 let v2 = var2 / n2f;
135 let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
136
137 let p_value = 2.0 * special::t_distribution_sf(t.abs(), df);
138
139 Some(TestResult {
140 statistic: t,
141 df,
142 p_value,
143 })
144}
145
146pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
168 if x.len() != y.len() || x.len() < 2 {
169 return None;
170 }
171
172 let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
173 one_sample_t_test(&diffs, 0.0)
174}
175
176#[derive(Debug, Clone)]
182pub struct AnovaResult {
183 pub f_statistic: f64,
185 pub df_between: usize,
187 pub df_within: usize,
189 pub p_value: f64,
191 pub ss_between: f64,
193 pub ss_within: f64,
195 pub ms_between: f64,
197 pub ms_within: f64,
199 pub group_means: Vec<f64>,
201 pub grand_mean: f64,
203}
204
205pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
234 let k = groups.len();
235 if k < 2 {
236 return None;
237 }
238
239 for g in groups {
240 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
241 return None;
242 }
243 }
244
245 let total_n: usize = groups.iter().map(|g| g.len()).sum();
246
247 let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
249 let grand_mean = grand_sum / total_n as f64;
250
251 let group_means: Vec<f64> = groups
253 .iter()
254 .map(|g| g.iter().sum::<f64>() / g.len() as f64)
255 .collect();
256
257 let ss_between: f64 = groups
259 .iter()
260 .zip(group_means.iter())
261 .map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
262 .sum();
263
264 let ss_within: f64 = groups
265 .iter()
266 .zip(group_means.iter())
267 .map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
268 .sum();
269
270 let df_between = k - 1;
271 let df_within = total_n - k;
272
273 if df_within == 0 {
274 return None;
275 }
276
277 let ms_between = ss_between / df_between as f64;
278 let ms_within = ss_within / df_within as f64;
279
280 let f_statistic = if ms_within > 1e-300 {
281 ms_between / ms_within
282 } else {
283 f64::INFINITY
284 };
285
286 let p_value = if f_statistic.is_infinite() {
287 0.0
288 } else {
289 special::f_distribution_sf(f_statistic, df_between as f64, df_within as f64)
290 };
291
292 Some(AnovaResult {
293 f_statistic,
294 df_between,
295 df_within,
296 p_value,
297 ss_between,
298 ss_within,
299 ms_between,
300 ms_within,
301 group_means,
302 grand_mean,
303 })
304}
305
306pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
332 let k = observed.len();
333 if k < 2 || k != expected.len() {
334 return None;
335 }
336
337 for &e in expected {
338 if e <= 0.0 || !e.is_finite() {
339 return None;
340 }
341 }
342 for &o in observed {
343 if o < 0.0 || !o.is_finite() {
344 return None;
345 }
346 }
347
348 let chi2: f64 = observed
349 .iter()
350 .zip(expected.iter())
351 .map(|(&o, &e)| (o - e).powi(2) / e)
352 .sum();
353
354 let df = (k - 1) as f64;
355 let p_value = special::chi_squared_sf(chi2, df);
356
357 Some(TestResult {
358 statistic: chi2,
359 df,
360 p_value,
361 })
362}
363
364pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
393 if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
394 return None;
395 }
396
397 for &v in table {
398 if v < 0.0 || !v.is_finite() {
399 return None;
400 }
401 }
402
403 let mut row_sums = vec![0.0; n_rows];
405 let mut col_sums = vec![0.0; n_cols];
406 let mut total = 0.0;
407
408 for i in 0..n_rows {
409 for j in 0..n_cols {
410 let val = table[i * n_cols + j];
411 row_sums[i] += val;
412 col_sums[j] += val;
413 total += val;
414 }
415 }
416
417 if total <= 0.0 {
418 return None;
419 }
420
421 for &r in &row_sums {
423 if r <= 0.0 {
424 return None;
425 }
426 }
427 for &c in &col_sums {
428 if c <= 0.0 {
429 return None;
430 }
431 }
432
433 let mut chi2 = 0.0;
435 for i in 0..n_rows {
436 for j in 0..n_cols {
437 let observed = table[i * n_cols + j];
438 let expected = row_sums[i] * col_sums[j] / total;
439 chi2 += (observed - expected).powi(2) / expected;
440 }
441 }
442
443 let df = ((n_rows - 1) * (n_cols - 1)) as f64;
444 let p_value = special::chi_squared_sf(chi2, df);
445
446 Some(TestResult {
447 statistic: chi2,
448 df,
449 p_value,
450 })
451}
452
453pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
498 let n = data.len();
499 if n < 8 {
500 return None;
501 }
502 if data.iter().any(|v| !v.is_finite()) {
503 return None;
504 }
505
506 let g1 = stats::skewness_moment(data)?;
507 let g2 = stats::kurtosis_moment(data)?;
508
509 let nf = n as f64;
510 let jb = (nf / 6.0) * (g1 * g1 + g2 * g2 / 4.0);
511 let p_value = (-jb / 2.0).exp();
513
514 Some(TestResult {
515 statistic: jb,
516 df: 2.0,
517 p_value,
518 })
519}
520
521#[derive(Debug, Clone, Copy)]
523pub struct AndersonDarlingResult {
524 pub statistic: f64,
526 pub statistic_star: f64,
528 pub p_value: f64,
530}
531
532fn ad_upper_tail_pvalue(a2_star: f64) -> f64 {
552 const UPPER_BRANCH_VERTEX: f64 = 153.467_741_935_483_87; let p = if a2_star >= 0.6 {
557 let x = a2_star.min(UPPER_BRANCH_VERTEX);
558 (1.2937 - 5.709 * x + 0.0186 * x * x).exp()
559 } else if a2_star > 0.34 {
560 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
561 } else if a2_star > 0.2 {
562 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
563 } else {
564 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
565 };
566 p.clamp(0.0, 1.0)
567}
568
569pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
604 let n = data.len();
605 if n < 8 {
606 return None;
607 }
608 if data.iter().any(|v| !v.is_finite()) {
609 return None;
610 }
611
612 let mean = stats::mean(data)?;
613 let sd = stats::std_dev(data)?;
614
615 if sd < 1e-300 {
616 return None; }
618
619 let mut x: Vec<f64> = data.to_vec();
621 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
622
623 let nf = n as f64;
624
625 let mut s = 0.0;
627 for i in 0..n {
628 let z = (x[i] - mean) / sd;
629 let z_rev = (x[n - 1 - i] - mean) / sd;
630 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
634 let ln_sf_rev = special::standard_normal_sf(z_rev)
635 .max(f64::MIN_POSITIVE)
636 .ln();
637
638 let coeff = (2 * (i + 1) - 1) as f64;
639 s += coeff * (ln_phi + ln_sf_rev);
640 }
641
642 let a2 = -nf - s / nf;
643
644 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
646
647 let p = ad_upper_tail_pvalue(a2_star);
650
651 Some(AndersonDarlingResult {
652 statistic: a2,
653 statistic_star: a2_star,
654 p_value: p,
655 })
656}
657
658#[derive(Debug, Clone, Copy)]
660pub struct ShapiroWilkResult {
661 pub w: f64,
663 pub p_value: f64,
665}
666
667pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
707 let n = data.len();
708 if !(3..=5000).contains(&n) {
709 return None;
710 }
711 if data.iter().any(|v| !v.is_finite()) {
712 return None;
713 }
714
715 let mut x: Vec<f64> = data.to_vec();
717 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
718
719 if x[n - 1] - x[0] < 1e-300 {
720 return None; }
722
723 let nn2 = n / 2;
724
725 if n == 3 {
727 return shapiro_wilk_n3(&x);
728 }
729
730 let a = sw_coefficients(n, nn2)?;
732
733 let w = sw_statistic(&x, &a, n, nn2);
735
736 if !(0.0..=1.0 + 1e-10).contains(&w) {
737 return None;
738 }
739 let w = w.min(1.0);
740
741 let p_value = sw_p_value(w, n);
743
744 Some(ShapiroWilkResult {
745 w,
746 p_value: p_value.clamp(0.0, 1.0),
747 })
748}
749
750fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
752 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
755 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
756 if ss < 1e-300 {
757 return None;
758 }
759
760 let numerator = a1 * (x[2] - x[0]);
761 let w = (numerator * numerator) / ss;
762 let w = w.clamp(0.75, 1.0);
763
764 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
766 let p = p.clamp(0.0, 1.0);
767
768 Some(ShapiroWilkResult { w, p_value: p })
769}
770
771const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
773const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
774const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
775const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
776const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
777const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
778const SW_G: [f64; 2] = [-2.273, 0.459];
779
780fn sw_poly(c: &[f64], x: f64) -> f64 {
782 let mut result = c[c.len() - 1];
783 for i in (0..c.len() - 1).rev() {
784 result = result * x + c[i];
785 }
786 result
787}
788
789fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
791 let mut a = vec![0.0; nn2];
792
793 let mut m = vec![0.0; nn2];
795 let mut summ2 = 0.0;
796 for (i, mi) in m.iter_mut().enumerate() {
797 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
799 *mi = special::inverse_normal_cdf(p);
800 summ2 += *mi * *mi;
801 }
802 summ2 *= 2.0;
803 let ssumm2 = summ2.sqrt();
804 let rsn = 1.0 / (n as f64).sqrt();
805
806 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
808
809 if n <= 5 {
810 let fac_sq = summ2 - 2.0 * m[0] * m[0];
812 let one_minus = 1.0 - 2.0 * a1 * a1;
813 if fac_sq <= 0.0 || one_minus <= 0.0 {
814 return None;
815 }
816 let fac = (fac_sq / one_minus).sqrt();
817 a[0] = a1;
818 for i in 1..nn2 {
819 a[i] = -m[i] / fac;
820 }
821 } else {
822 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
824 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
825 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
826 if fac_sq <= 0.0 || one_minus <= 0.0 {
827 return None;
828 }
829 let fac = (fac_sq / one_minus).sqrt();
830 a[0] = a1;
831 a[1] = a2;
832 for i in 2..nn2 {
833 a[i] = -m[i] / fac;
834 }
835 }
836
837 Some(a)
838}
839
840fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
842 let mut sa = 0.0;
844 for i in 0..nn2 {
845 sa += a[i] * (x[n - 1 - i] - x[i]);
846 }
847
848 let mean = x.iter().sum::<f64>() / n as f64;
850 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
851
852 if ss < 1e-300 {
853 return 1.0; }
855
856 (sa * sa) / ss
857}
858
859fn sw_p_value(w: f64, n: usize) -> f64 {
861 let nf = n as f64;
862
863 if n == 3 {
864 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
866 return p.clamp(0.0, 1.0);
867 }
868
869 let w1 = 1.0 - w;
870 if w1 <= 0.0 {
871 return 1.0; }
873
874 let y = w1.ln();
875
876 if n <= 11 {
877 let gamma = sw_poly(&SW_G, nf);
879 if y >= gamma {
880 return 0.0; }
882 let y2 = -(gamma - y).ln();
883 let m = sw_poly(&SW_C3, nf);
884 let s = sw_poly(&SW_C4, nf).exp();
885 if s < 1e-300 {
886 return 0.0;
887 }
888 let z = (y2 - m) / s;
889 special::standard_normal_sf(z)
890 } else {
891 let xx = nf.ln();
893 let m = sw_poly(&SW_C5, xx);
894 let s = sw_poly(&SW_C6, xx).exp();
895 if s < 1e-300 {
896 return 0.0;
897 }
898 let z = (y - m) / s;
899 special::standard_normal_sf(z)
900 }
901}
902
903pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
947 let n1 = a.len();
948 let n2 = b.len();
949 if n1 < 2 || n2 < 2 {
950 return None;
951 }
952 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
953 return None;
954 }
955
956 let n = n1 + n2;
957 let n1f = n1 as f64;
958 let n2f = n2 as f64;
959 let nf = n as f64;
960
961 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
963 for &v in a {
964 combined.push((v, 0)); }
966 for &v in b {
967 combined.push((v, 1)); }
969 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
970
971 let ranks = average_ranks(&combined);
973
974 let r1: f64 = combined
976 .iter()
977 .zip(ranks.iter())
978 .filter(|((_, g), _)| *g == 0)
979 .map(|(_, &r)| r)
980 .sum();
981
982 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
984
985 let tie_correction = compute_tie_correction(&combined);
987
988 let mu = n1f * n2f / 2.0;
990 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
991
992 if sigma_sq <= 0.0 {
993 return None;
994 }
995
996 let p_value = if tie_correction == 0.0 && n1 <= EXACT_MAX_N && n2 <= EXACT_MAX_N {
997 let u = u1.round() as usize;
1000 let tail = u.min(n1 * n2 - u);
1001 (2.0 * mann_whitney_exact_cdf(n1, n2, tail)).min(1.0)
1002 } else {
1003 continuity_corrected_p(u1, mu, sigma_sq)
1004 };
1005
1006 Some(TestResult {
1007 statistic: u1,
1008 df: 0.0, p_value,
1010 })
1011}
1012
1013const EXACT_MAX_N: usize = 50;
1017
1018fn continuity_corrected_p(statistic: f64, mu: f64, sigma_sq: f64) -> f64 {
1021 let z = ((statistic - mu).abs() - 0.5).max(0.0) / sigma_sq.sqrt();
1022 (2.0 * special::standard_normal_sf(z)).min(1.0)
1023}
1024
1025fn mann_whitney_exact_cdf(m: usize, n: usize, u: usize) -> f64 {
1034 let max_u = m * n;
1035 let mut prev: Vec<Vec<f64>> = (0..=n).map(|_| vec![1.0]).collect(); for i in 1..=m {
1038 let mut cur: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
1039 cur.push(vec![1.0]); for j in 1..=n {
1041 let mut dist = vec![0.0; i * j + 1];
1042 let wi = i as f64 / (i + j) as f64;
1043 let wj = j as f64 / (i + j) as f64;
1044 for (k, &pk) in prev[j].iter().enumerate() {
1046 dist[k + j] += wi * pk;
1047 }
1048 for (k, &pk) in cur[j - 1].iter().enumerate() {
1050 dist[k] += wj * pk;
1051 }
1052 cur.push(dist);
1053 }
1054 prev = cur;
1055 }
1056 prev[n].iter().take(u.min(max_u) + 1).sum::<f64>().min(1.0)
1057}
1058
1059fn wilcoxon_exact_cdf(n: usize, t: usize) -> f64 {
1062 let max_t = n * (n + 1) / 2;
1063 let mut dist = vec![0.0; max_t + 1];
1064 dist[0] = 1.0;
1065 let mut reach = 0;
1066 for rank in 1..=n {
1067 reach += rank;
1068 for s in (rank..=reach).rev() {
1069 dist[s] = 0.5 * dist[s] + 0.5 * dist[s - rank];
1070 }
1071 for d in dist.iter_mut().take(rank) {
1072 *d *= 0.5;
1073 }
1074 }
1075 dist.iter().take(t.min(max_t) + 1).sum::<f64>().min(1.0)
1076}
1077
1078pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1118 if x.len() != y.len() || x.len() < 2 {
1119 return None;
1120 }
1121 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1122 return None;
1123 }
1124
1125 let diffs: Vec<f64> = x
1127 .iter()
1128 .zip(y.iter())
1129 .map(|(&a, &b)| a - b)
1130 .filter(|&d| d.abs() > 1e-300)
1131 .collect();
1132
1133 let nr = diffs.len();
1134 if nr < 2 {
1135 return None;
1136 }
1137
1138 let nf = nr as f64;
1139
1140 let mut abs_diffs: Vec<(f64, usize)> = diffs
1142 .iter()
1143 .enumerate()
1144 .map(|(i, &d)| (d.abs(), i))
1145 .collect();
1146 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1147
1148 let ranks = average_ranks(&abs_diffs);
1150
1151 let t_plus: f64 = abs_diffs
1153 .iter()
1154 .zip(ranks.iter())
1155 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1156 .map(|(_, &r)| r)
1157 .sum();
1158
1159 let tie_correction_val = compute_tie_correction(&abs_diffs);
1161
1162 let mu = nf * (nf + 1.0) / 4.0;
1164 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1165
1166 if sigma_sq <= 0.0 {
1167 return None;
1168 }
1169
1170 let p_value = if tie_correction_val == 0.0 && nr <= EXACT_MAX_N {
1171 let t = t_plus.round() as usize;
1174 let tail = t.min(nr * (nr + 1) / 2 - t);
1175 (2.0 * wilcoxon_exact_cdf(nr, tail)).min(1.0)
1176 } else {
1177 continuity_corrected_p(t_plus, mu, sigma_sq)
1178 };
1179
1180 Some(TestResult {
1181 statistic: t_plus,
1182 df: 0.0,
1183 p_value,
1184 })
1185}
1186
1187fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1190 let n = sorted.len();
1191 let mut ranks = vec![0.0; n];
1192 let mut i = 0;
1193 while i < n {
1194 let mut j = i + 1;
1195 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1196 j += 1;
1197 }
1198 let avg_rank = (i + 1 + j) as f64 / 2.0;
1200 for rank in ranks.iter_mut().take(j).skip(i) {
1201 *rank = avg_rank;
1202 }
1203 i = j;
1204 }
1205 ranks
1206}
1207
1208fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1210 let n = sorted.len();
1211 let mut correction = 0.0;
1212 let mut i = 0;
1213 while i < n {
1214 let mut j = i + 1;
1215 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1216 j += 1;
1217 }
1218 let t = (j - i) as f64;
1219 if t > 1.0 {
1220 correction += t * (t * t - 1.0);
1221 }
1222 i = j;
1223 }
1224 correction
1225}
1226
1227pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1259 let k = groups.len();
1260 if k < 2 {
1261 return None;
1262 }
1263 for g in groups {
1264 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1265 return None;
1266 }
1267 }
1268
1269 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1270 let nf = total_n as f64;
1271
1272 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1274 for (gi, g) in groups.iter().enumerate() {
1275 for &v in *g {
1276 combined.push((v, gi));
1277 }
1278 }
1279 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1280
1281 let ranks = average_ranks(&combined);
1282
1283 let mut rank_sums = vec![0.0; k];
1285 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1286 rank_sums[*gi] += r;
1287 }
1288
1289 let mean_rank = (nf + 1.0) / 2.0;
1291 let mut h = 0.0;
1292 for (gi, g) in groups.iter().enumerate() {
1293 let ni = g.len() as f64;
1294 let mean_rank_i = rank_sums[gi] / ni;
1295 h += ni * (mean_rank_i - mean_rank).powi(2);
1296 }
1297 h *= 12.0 / (nf * (nf + 1.0));
1298
1299 let tie_corr = compute_tie_correction(&combined);
1301 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1302 if denom > 1e-15 {
1303 h /= denom;
1304 }
1305
1306 let df = (k - 1) as f64;
1307 let p_value = special::chi_squared_sf(h, df);
1308
1309 Some(TestResult {
1310 statistic: h,
1311 df,
1312 p_value,
1313 })
1314}
1315
1316pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1352 let k = groups.len();
1353 if k < 2 {
1354 return None;
1355 }
1356 for g in groups {
1357 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1358 return None;
1359 }
1360 }
1361
1362 let z_groups: Vec<Vec<f64>> = groups
1364 .iter()
1365 .map(|g| {
1366 let median = stats::median(g).unwrap_or(0.0);
1367 g.iter().map(|&x| (x - median).abs()).collect()
1368 })
1369 .collect();
1370
1371 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1373 let anova = one_way_anova(&z_refs)?;
1374
1375 Some(TestResult {
1376 statistic: anova.f_statistic,
1377 df: anova.df_between as f64,
1378 p_value: anova.p_value,
1379 })
1380}
1381
1382pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1395 if p_values.is_empty() || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1396 return None;
1397 }
1398 let m = p_values.len() as f64;
1399 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1400}
1401
1402pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1422 let m = p_values.len();
1423 if m == 0 || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1424 return None;
1425 }
1426
1427 let mut indices: Vec<usize> = (0..m).collect();
1429 indices.sort_by(|&a, &b| {
1430 p_values[a]
1431 .partial_cmp(&p_values[b])
1432 .unwrap_or(std::cmp::Ordering::Equal)
1433 });
1434
1435 let mf = m as f64;
1436 let mut adjusted = vec![0.0; m];
1437
1438 let mut cummin = f64::INFINITY;
1440 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1441 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1443 cummin = cummin.min(adj);
1444 adjusted[orig_idx] = cummin;
1445 }
1446
1447 Some(adjusted)
1448}
1449
1450pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1487 let k = groups.len();
1488 if k < 2 {
1489 return None;
1490 }
1491
1492 let mut sizes = Vec::with_capacity(k);
1493 let mut vars = Vec::with_capacity(k);
1494 let mut n_total: usize = 0;
1495
1496 for g in groups {
1497 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1498 return None;
1499 }
1500 let n = g.len();
1501 let v = stats::variance(g)?;
1502 if v <= 0.0 {
1503 return None; }
1505 sizes.push(n);
1506 vars.push(v);
1507 n_total += n;
1508 }
1509
1510 let nk = n_total - k; if nk == 0 {
1512 return None;
1513 }
1514 let nk_f = nk as f64;
1515
1516 let s2_pooled: f64 = sizes
1518 .iter()
1519 .zip(vars.iter())
1520 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1521 .sum::<f64>()
1522 / nk_f;
1523
1524 if s2_pooled <= 0.0 {
1525 return None;
1526 }
1527
1528 let num = nk_f * s2_pooled.ln()
1530 - sizes
1531 .iter()
1532 .zip(vars.iter())
1533 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1534 .sum::<f64>();
1535
1536 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1538 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1539
1540 let statistic = num / c;
1541 let df = (k - 1) as f64;
1542 let p_value = special::chi_squared_sf(statistic, df);
1543
1544 Some(TestResult {
1545 statistic,
1546 df,
1547 p_value,
1548 })
1549}
1550
1551pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1596 let row1 = a + b;
1597 let row2 = c + d;
1598 let col1 = a + c;
1599 let col2 = b + d;
1600 let n = a + b + c + d;
1601
1602 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1604 return None;
1605 }
1606
1607 let log_prob = |a_i: u64| -> f64 {
1609 let b_i = row1 - a_i;
1610 let c_i = col1 - a_i;
1611 let d_i = row2 - c_i;
1612 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1613 - ln_factorial(a_i)
1614 - ln_factorial(b_i)
1615 - ln_factorial(c_i)
1616 - ln_factorial(d_i)
1617 - ln_factorial(n)
1618 };
1619
1620 let a_min = col1.saturating_sub(row2);
1622 let a_max = row1.min(col1);
1623
1624 let log_p_obs = log_prob(a);
1625
1626 let mut p_value = 0.0;
1628 for a_i in a_min..=a_max {
1629 let lp = log_prob(a_i);
1630 if lp <= log_p_obs + 1e-10 {
1632 p_value += lp.exp();
1633 }
1634 }
1635
1636 let p_value = p_value.min(1.0);
1638
1639 let odds_ratio = if b > 0 && c > 0 {
1641 (a as f64 * d as f64) / (b as f64 * c as f64)
1642 } else {
1643 f64::INFINITY
1644 };
1645
1646 Some(TestResult {
1647 statistic: odds_ratio,
1648 df: 1.0,
1649 p_value,
1650 })
1651}
1652
1653#[derive(Debug, Clone, Copy)]
1659pub struct MannKendallResult {
1660 pub s_statistic: i64,
1662 pub variance: f64,
1664 pub z_statistic: f64,
1666 pub p_value: f64,
1668 pub kendall_tau: f64,
1670 pub sen_slope: f64,
1672}
1673
1674pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1712 let n = data.len();
1713 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1714 return None;
1715 }
1716
1717 let mut s: i64 = 0;
1719 for i in 0..n - 1 {
1720 for j in (i + 1)..n {
1721 let diff = data[j] - data[i];
1722 if diff > 0.0 {
1723 s += 1;
1724 } else if diff < 0.0 {
1725 s -= 1;
1726 }
1727 }
1728 }
1729
1730 let mut sorted: Vec<f64> = data.to_vec();
1732 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1733
1734 let mut tie_correction: f64 = 0.0;
1735 let mut current_count: usize = 1;
1736 for i in 1..sorted.len() {
1737 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1738 current_count += 1;
1739 } else {
1740 if current_count > 1 {
1741 let t = current_count as f64;
1742 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1743 }
1744 current_count = 1;
1745 }
1746 }
1747 if current_count > 1 {
1748 let t = current_count as f64;
1749 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1750 }
1751
1752 let nf = n as f64;
1754 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1755
1756 if variance < 1e-300 {
1757 return None; }
1759
1760 let sigma = variance.sqrt();
1762 let z_statistic = if s > 0 {
1763 (s as f64 - 1.0) / sigma
1764 } else if s < 0 {
1765 (s as f64 + 1.0) / sigma
1766 } else {
1767 0.0
1768 };
1769
1770 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1772 let p_value = p_value.clamp(0.0, 1.0);
1773
1774 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1776
1777 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1779 for i in 0..n - 1 {
1780 for j in (i + 1)..n {
1781 let dx = (j - i) as f64;
1782 slopes.push((data[j] - data[i]) / dx);
1783 }
1784 }
1785 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1786 let m = slopes.len();
1787 let sen_slope = if m % 2 == 0 {
1788 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1789 } else {
1790 slopes[m / 2]
1791 };
1792
1793 Some(MannKendallResult {
1794 s_statistic: s,
1795 variance,
1796 z_statistic,
1797 p_value,
1798 kendall_tau,
1799 sen_slope,
1800 })
1801}
1802
1803fn ln_factorial(n: u64) -> f64 {
1805 if n <= 1 {
1806 return 0.0;
1807 }
1808 special::ln_gamma(n as f64 + 1.0)
1810}
1811
1812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1818pub enum KpssModel {
1819 Level,
1821 Trend,
1823}
1824
1825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1827pub enum KpssLags {
1828 Hobijn,
1831 Schwert,
1833 Fixed(usize),
1835}
1836
1837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1839pub enum KpssPValue {
1840 Interpolated,
1842 AtMost,
1844 AtLeast,
1846}
1847
1848#[derive(Debug, Clone)]
1850pub struct KpssResult {
1851 pub statistic: f64,
1853 pub n_lags: usize,
1855 pub p_value: f64,
1858 pub p_value_bound: KpssPValue,
1860 pub critical_values: [f64; 4],
1862 pub rejected: [bool; 4],
1864}
1865
1866pub const KPSS_LEVELS: [f64; 4] = [0.10, 0.05, 0.025, 0.01];
1868
1869pub const KPSS_MIN_OBSERVATIONS: usize = 10;
1871
1872pub fn kpss_test(
1917 data: &[f64],
1918 model: KpssModel,
1919 lags: KpssLags,
1920) -> Result<KpssResult, TimeSeriesError> {
1921 check_series(data, KPSS_MIN_OBSERVATIONS)?;
1922 let n = data.len();
1923 if let KpssLags::Fixed(l) = lags {
1924 check_option("lags", l, 0, n - 1)?;
1925 }
1926 let nf = n as f64;
1927
1928 let residuals: Vec<f64> = match model {
1929 KpssModel::Level => {
1930 let mean = data.iter().sum::<f64>() / nf;
1931 data.iter().map(|x| x - mean).collect()
1932 }
1933 KpssModel::Trend => {
1934 let t_mean = (nf + 1.0) / 2.0;
1936 let y_mean = data.iter().sum::<f64>() / nf;
1937 let (mut sty, mut stt) = (0.0, 0.0);
1938 for (i, y) in data.iter().enumerate() {
1939 let dt = (i + 1) as f64 - t_mean;
1940 sty += dt * (y - y_mean);
1941 stt += dt * dt;
1942 }
1943 let slope = sty / stt;
1944 data.iter()
1945 .enumerate()
1946 .map(|(i, y)| y - y_mean - slope * ((i + 1) as f64 - t_mean))
1947 .collect()
1948 }
1949 };
1950
1951 let sum_sq: f64 = residuals.iter().map(|e| e * e).sum();
1953 let size: f64 = data.iter().map(|x| x * x).sum();
1954 if sum_sq <= 1e-20 * size {
1955 return Err(TimeSeriesError::NoVariation);
1956 }
1957
1958 let autocovariance = |i: usize| -> f64 {
1959 residuals[i..]
1960 .iter()
1961 .zip(&residuals[..n - i])
1962 .map(|(a, b)| a * b)
1963 .sum()
1964 };
1965
1966 let n_lags = match lags {
1967 KpssLags::Fixed(l) => l,
1968 KpssLags::Schwert => ((12.0 * (nf / 100.0).powf(0.25)).ceil() as usize).min(n - 1),
1969 KpssLags::Hobijn => {
1970 let cov_lags = nf.powf(2.0 / 9.0) as usize;
1972 let mut s0 = sum_sq / nf;
1973 let mut s1 = 0.0;
1974 for i in 1..=cov_lags.min(n - 1) {
1975 let g = autocovariance(i) / (nf / 2.0);
1976 s0 += g;
1977 s1 += i as f64 * g;
1978 }
1979 let s_hat = s1 / s0;
1980 let gamma = 1.1447 * (s_hat * s_hat).powf(1.0 / 3.0);
1981 ((gamma * nf.powf(1.0 / 3.0)) as usize).min(n - 1)
1982 }
1983 };
1984
1985 let mut long_run = sum_sq;
1986 for i in 1..=n_lags {
1987 long_run += 2.0 * autocovariance(i) * (1.0 - i as f64 / (n_lags as f64 + 1.0));
1988 }
1989 let long_run = long_run / nf;
1990 if long_run <= 0.0 {
1991 return Err(TimeSeriesError::NoVariation);
1992 }
1993
1994 let mut partial = 0.0;
1995 let eta = residuals
1996 .iter()
1997 .map(|e| {
1998 partial += e;
1999 partial * partial
2000 })
2001 .sum::<f64>()
2002 / (nf * nf);
2003 let statistic = eta / long_run;
2004
2005 let critical_values = match model {
2006 KpssModel::Level => [0.347, 0.463, 0.574, 0.739],
2007 KpssModel::Trend => [0.119, 0.146, 0.176, 0.216],
2008 };
2009 let (p_value, p_value_bound) = if statistic <= critical_values[0] {
2010 (KPSS_LEVELS[0], KpssPValue::AtLeast)
2011 } else if statistic >= critical_values[3] {
2012 (KPSS_LEVELS[3], KpssPValue::AtMost)
2013 } else {
2014 let k = critical_values
2015 .windows(2)
2016 .position(|w| statistic <= w[1])
2017 .expect("the statistic lies inside the table");
2018 let (c0, c1) = (critical_values[k], critical_values[k + 1]);
2019 let (p0, p1) = (KPSS_LEVELS[k], KPSS_LEVELS[k + 1]);
2020 (
2021 p0 + (statistic - c0) / (c1 - c0) * (p1 - p0),
2022 KpssPValue::Interpolated,
2023 )
2024 };
2025 let rejected = critical_values.map(|c| statistic > c);
2026
2027 Ok(KpssResult {
2028 statistic,
2029 n_lags,
2030 p_value,
2031 p_value_bound,
2032 critical_values,
2033 rejected,
2034 })
2035}
2036
2037#[cfg(test)]
2040mod kpss_reference {
2041 use super::*;
2042
2043 const AR: [f64; 60] = [
2044 0.0, 0.299, -0.154, -0.953, -0.836, -1.326, -0.471, 1.152, -0.031, -0.633, 0.237, 0.452,
2045 0.286, -0.816, -0.355, 0.553, -1.123, -0.907, -2.264, -2.196, -2.72, -1.323, -1.796,
2046 -0.447, -0.022, -0.196, -2.595, -1.577, -0.68, -0.159, -1.594, -1.115, -1.425, -1.379,
2047 0.509, -0.604, -0.275, 0.774, -0.274, -0.222, 0.021, 0.073, -1.196, -0.402, 1.198, -1.068,
2048 0.432, 0.292, -0.524, 1.79, 1.478, -0.608, -0.168, 0.51, 0.015, 0.689, 0.209, 0.75, 1.739,
2049 0.02,
2050 ];
2051 const RW: [f64; 60] = [
2052 0.001, 0.3, 0.026, -0.865, -1.32, -2.312, -2.252, -0.912, -1.404, -2.024, -1.534, -1.177,
2053 -1.072, -2.002, -2.031, -1.336, -2.68, -3.138, -5.039, -6.329, -8.171, -8.406, -9.673,
2054 -9.402, -9.245, -9.432, -11.949, -12.488, -12.537, -12.424, -13.954, -14.432, -15.411,
2055 -16.22, -15.159, -15.967, -16.0, -15.116, -15.7, -15.812, -15.702, -15.638, -16.863,
2056 -16.787, -15.428, -16.975, -16.116, -15.997, -16.638, -14.638, -13.876, -15.075, -15.0,
2057 -14.423, -14.612, -13.929, -13.996, -13.329, -11.89, -12.566,
2058 ];
2059
2060 #[test]
2061 fn statistic_lag_and_p_value_reproduce_statsmodels() {
2062 use KpssLags::{Fixed, Hobijn, Schwert};
2063 use KpssModel::{Level, Trend};
2064 type Case = (&'static [f64; 60], KpssModel, KpssLags, f64, f64, usize);
2065 let cases: [Case; 12] = [
2066 (
2067 &AR,
2068 Level,
2069 Hobijn,
2070 0.5229270606681895,
2071 0.036502914263921285,
2072 3,
2073 ),
2074 (&AR, Level, Schwert, 0.288001639308506, 0.1, 11),
2075 (
2076 &AR,
2077 Level,
2078 Fixed(4),
2079 0.46189194657609306,
2080 0.05047760923444266,
2081 4,
2082 ),
2083 (&AR, Trend, Hobijn, 0.2274846895383343, 0.01, 3),
2084 (
2085 &AR,
2086 Trend,
2087 Schwert,
2088 0.14193842821368732,
2089 0.05752142923391235,
2090 11,
2091 ),
2092 (
2093 &AR,
2094 Trend,
2095 Fixed(4),
2096 0.20574827757371436,
2097 0.013844395909857114,
2098 4,
2099 ),
2100 (&RW, Level, Hobijn, 0.9286533322383663, 0.01, 5),
2101 (
2102 &RW,
2103 Level,
2104 Schwert,
2105 0.5112288964656923,
2106 0.0391376359311504,
2107 11,
2108 ),
2109 (&RW, Level, Fixed(4), 1.09862199299003, 0.01, 4),
2110 (&RW, Trend, Hobijn, 0.26866997231516837, 0.01, 4),
2111 (
2112 &RW,
2113 Trend,
2114 Schwert,
2115 0.14468829763941088,
2116 0.05242907844553539,
2117 11,
2118 ),
2119 (&RW, Trend, Fixed(4), 0.26866997231516837, 0.01, 4),
2120 ];
2121 for (data, model, lags, statistic, p, n_lags) in cases {
2122 let r = kpss_test(data, model, lags).unwrap();
2123 let what = format!("{model:?} {lags:?}");
2124 assert_eq!(r.n_lags, n_lags, "{what}");
2125 assert!(
2126 (r.statistic - statistic).abs() < 1e-10 * statistic,
2127 "{what}: {} vs {statistic}",
2128 r.statistic
2129 );
2130 assert!(
2131 (r.p_value - p).abs() < 1e-12,
2132 "{what}: p {} vs {p}",
2133 r.p_value
2134 );
2135 }
2136 }
2137
2138 #[test]
2139 fn bounds_rejections_and_refusals() {
2140 let r = kpss_test(&RW, KpssModel::Level, KpssLags::Fixed(4)).unwrap();
2141 assert_eq!(r.p_value_bound, KpssPValue::AtMost);
2142 assert_eq!(r.rejected, [true; 4]);
2143 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Schwert).unwrap();
2144 assert_eq!(r.p_value_bound, KpssPValue::AtLeast);
2145 assert_eq!(r.rejected, [false; 4]);
2146 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Hobijn).unwrap();
2147 assert_eq!(r.p_value_bound, KpssPValue::Interpolated);
2148 assert_eq!(r.rejected, [true, true, false, false]);
2149
2150 assert_eq!(
2151 kpss_test(&AR[..9], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2152 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
2153 );
2154 assert_eq!(
2155 kpss_test(&AR, KpssModel::Level, KpssLags::Fixed(60)).unwrap_err(),
2156 TimeSeriesError::OptionOutOfRange {
2157 option: "lags",
2158 got: 60.0,
2159 min: 0.0,
2160 max: 59.0
2161 }
2162 );
2163 assert_eq!(
2164 kpss_test(&[4.0; 20], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2165 TimeSeriesError::NoVariation
2166 );
2167 let line: Vec<f64> = (0..20).map(|i| 2.0 + 0.5 * i as f64).collect();
2168 assert_eq!(
2169 kpss_test(&line, KpssModel::Trend, KpssLags::Hobijn).unwrap_err(),
2170 TimeSeriesError::NoVariation
2171 );
2172 let mut bad = AR;
2173 bad[3] = f64::INFINITY;
2174 assert_eq!(
2175 kpss_test(&bad[..5], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2176 TimeSeriesError::ValueNotFinite { index: 3 }
2177 );
2178 }
2179
2180 #[test]
2182 fn unit_invariant() {
2183 let base = kpss_test(&AR, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2184 for f in [1e-9, 1e9] {
2185 let x: Vec<f64> = AR.iter().map(|v| v * f).collect();
2186 let r = kpss_test(&x, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2187 assert_eq!(r.n_lags, base.n_lags);
2188 assert!((r.statistic - base.statistic).abs() < 1e-9 * base.statistic);
2189 }
2190 }
2191}
2192
2193#[derive(Debug, Clone)]
2199pub struct AdfResult {
2200 pub statistic: f64,
2202 pub n_lags: usize,
2204 pub n_obs: usize,
2206 pub critical_values: [f64; 3],
2208 pub rejected: [bool; 3],
2210}
2211
2212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2214pub enum AdfLags {
2215 Aic {
2220 max: Option<usize>,
2222 },
2223 Fixed(usize),
2225}
2226
2227impl Default for AdfLags {
2228 fn default() -> Self {
2229 AdfLags::Aic { max: None }
2230 }
2231}
2232
2233#[derive(Debug, Clone, Copy)]
2235pub enum AdfModel {
2236 None,
2238 Constant,
2240 ConstantTrend,
2242}
2243
2244pub fn adf_test(
2284 data: &[f64],
2285 model: AdfModel,
2286 lags: AdfLags,
2287) -> Result<AdfResult, TimeSeriesError> {
2288 check_series(data, ADF_MIN_OBSERVATIONS)?;
2289 let n = data.len();
2290 let max = adf_max_lags(n, model);
2291 match lags {
2292 AdfLags::Fixed(p) => {
2293 check_option("lags", p, 0, max)?;
2294 }
2295 AdfLags::Aic { max: Some(m) } => {
2296 check_option("max_lags", m, 0, max)?;
2297 }
2298 AdfLags::Aic { max: None } => {}
2299 }
2300
2301 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
2303
2304 let best_lag = match lags {
2305 AdfLags::Fixed(p) => p,
2306 AdfLags::Aic { max } => {
2307 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
2309 let p_max = max.unwrap_or_else(|| schwert.min(n / 3));
2310 select_adf_lag(data, &dy, model, p_max).ok_or(TimeSeriesError::DegenerateRegression)?
2311 }
2312 };
2313
2314 adf_ols(data, &dy, model, best_lag).ok_or(TimeSeriesError::DegenerateRegression)
2316}
2317
2318pub const ADF_MIN_OBSERVATIONS: usize = 10;
2320
2321const ADF_MIN_REGRESSION_ROWS: usize = 5;
2323
2324pub fn adf_max_lags(n: usize, model: AdfModel) -> usize {
2330 let fixed_columns = match model {
2331 AdfModel::None => 1,
2332 AdfModel::Constant => 2,
2333 AdfModel::ConstantTrend => 3,
2334 };
2335 (n.saturating_sub(1 + ADF_MIN_REGRESSION_ROWS)).min(n.saturating_sub(2 + fixed_columns) / 2)
2336}
2337
2338fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> Option<usize> {
2343 let start = p_max;
2344 let mut best: Option<(f64, usize)> = None;
2345 for p in 0..=p_max {
2346 if let Some(aic) = adf_ols_aic(data, dy, model, p, start) {
2347 if best.is_none_or(|(b, _)| aic < b) {
2348 best = Some((aic, p));
2349 }
2350 }
2351 }
2352 best.map(|(_, p)| p)
2353}
2354
2355#[allow(clippy::type_complexity)]
2359fn adf_build_matrix(
2360 data: &[f64],
2361 dy: &[f64],
2362 model: AdfModel,
2363 p: usize,
2364 start: usize,
2365) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2366 debug_assert!(start >= p, "the first row needs its p lagged differences");
2368 if start >= dy.len() || dy.len() - start < 5 {
2369 return None;
2370 }
2371 let m = dy.len() - start;
2372
2373 let y_dep: Vec<f64> = dy[start..].to_vec();
2374
2375 let has_intercept = !matches!(model, AdfModel::None);
2377 let has_trend = matches!(model, AdfModel::ConstantTrend);
2378 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2379
2380 let mut x_data = Vec::with_capacity(m * ncols);
2382 let mut gamma_col = 0;
2383
2384 for i in 0..m {
2385 let t = start + i;
2386 if has_intercept {
2387 x_data.push(1.0); gamma_col = 1;
2389 }
2390 x_data.push(data[t]); if has_trend {
2392 x_data.push((t + 1) as f64); }
2394 for lag in 1..=p {
2395 x_data.push(dy[t - lag]);
2396 }
2397 }
2398
2399 Some((x_data, y_dep, m, ncols, gamma_col))
2400}
2401
2402fn adf_ols_core(
2406 x_data: &[f64],
2407 y: &[f64],
2408 m: usize,
2409 ncols: usize,
2410 gamma_col: usize,
2411) -> Option<(f64, f64, usize)> {
2412 let mut xtx = vec![0.0_f64; ncols * ncols];
2414 for i in 0..m {
2415 let row = &x_data[i * ncols..(i + 1) * ncols];
2416 for j in 0..ncols {
2417 for k in j..ncols {
2418 xtx[j * ncols + k] += row[j] * row[k];
2419 }
2420 }
2421 }
2422 for j in 0..ncols {
2424 for k in (j + 1)..ncols {
2425 xtx[k * ncols + j] = xtx[j * ncols + k];
2426 }
2427 }
2428
2429 let mut xty = vec![0.0_f64; ncols];
2431 for i in 0..m {
2432 let row = &x_data[i * ncols..(i + 1) * ncols];
2433 for j in 0..ncols {
2434 xty[j] += row[j] * y[i];
2435 }
2436 }
2437
2438 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2440 for i in 0..ncols {
2441 for j in 0..ncols {
2442 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2443 }
2444 augmented[i * (ncols + 1) + ncols] = xty[i];
2445 }
2446
2447 for col in 0..ncols {
2448 let mut max_row = col;
2450 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2451 for row in (col + 1)..ncols {
2452 let val = augmented[row * (ncols + 1) + col].abs();
2453 if val > max_val {
2454 max_val = val;
2455 max_row = row;
2456 }
2457 }
2458 if max_val < 1e-15 {
2459 return None; }
2461 if max_row != col {
2462 for j in 0..=ncols {
2463 let a = col * (ncols + 1) + j;
2464 let b = max_row * (ncols + 1) + j;
2465 augmented.swap(a, b);
2466 }
2467 }
2468
2469 let pivot = augmented[col * (ncols + 1) + col];
2470 for row in (col + 1)..ncols {
2471 let factor = augmented[row * (ncols + 1) + col] / pivot;
2472 for j in col..=ncols {
2473 let above = augmented[col * (ncols + 1) + j];
2474 augmented[row * (ncols + 1) + j] -= factor * above;
2475 }
2476 }
2477 }
2478
2479 let mut beta = vec![0.0_f64; ncols];
2481 for i in (0..ncols).rev() {
2482 let mut sum = augmented[i * (ncols + 1) + ncols];
2483 for j in (i + 1)..ncols {
2484 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2485 }
2486 beta[i] = sum / augmented[i * (ncols + 1) + i];
2487 }
2488
2489 let mut rss = 0.0;
2491 for i in 0..m {
2492 let row = &x_data[i * ncols..(i + 1) * ncols];
2493 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2494 let resid = y[i] - y_hat;
2495 rss += resid * resid;
2496 }
2497
2498 let yy: f64 = y.iter().map(|v| v * v).sum();
2504 if yy == 0.0 || rss <= yy * 1e-16 {
2505 return None;
2506 }
2507
2508 let df = m - ncols;
2510 if df == 0 {
2511 return None;
2512 }
2513 let mse = rss / df as f64;
2514
2515 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2518 for j in 0..ncols {
2519 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2520 }
2521 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2522 }
2523
2524 for col in 0..ncols {
2526 let mut max_row = col;
2527 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2528 for row in (col + 1)..ncols {
2529 let val = xtx_aug[row * 2 * ncols + col].abs();
2530 if val > max_val {
2531 max_val = val;
2532 max_row = row;
2533 }
2534 }
2535 if max_val < 1e-15 {
2536 return None;
2537 }
2538 if max_row != col {
2539 for j in 0..(2 * ncols) {
2540 let a = col * 2 * ncols + j;
2541 let b = max_row * 2 * ncols + j;
2542 xtx_aug.swap(a, b);
2543 }
2544 }
2545
2546 let pivot = xtx_aug[col * 2 * ncols + col];
2547 for j in 0..(2 * ncols) {
2548 xtx_aug[col * 2 * ncols + j] /= pivot;
2549 }
2550 for row in 0..ncols {
2551 if row == col {
2552 continue;
2553 }
2554 let factor = xtx_aug[row * 2 * ncols + col];
2555 for j in 0..(2 * ncols) {
2556 let above = xtx_aug[col * 2 * ncols + j];
2557 xtx_aug[row * 2 * ncols + j] -= factor * above;
2558 }
2559 }
2560 }
2561
2562 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2564 if var_gamma <= 0.0 {
2565 return None;
2566 }
2567 let se_gamma = var_gamma.sqrt();
2568 let t_gamma = beta[gamma_col] / se_gamma;
2569
2570 Some((t_gamma, rss, ncols))
2571}
2572
2573fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize, start: usize) -> Option<f64> {
2576 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, start)?;
2577 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2578 Some(2.0 * k as f64 + m as f64 * (rss / m as f64).ln())
2579}
2580
2581fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2583 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, p)?;
2584 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2585
2586 let critical_values = adf_critical_values(model, m);
2587
2588 let rejected = [
2589 gamma_t <= critical_values[0],
2590 gamma_t <= critical_values[1],
2591 gamma_t <= critical_values[2],
2592 ];
2593
2594 Some(AdfResult {
2595 statistic: gamma_t,
2596 n_lags: p,
2597 n_obs: m,
2598 critical_values,
2599 rejected,
2600 })
2601}
2602
2603fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2607 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2612 AdfModel::None => (
2613 [-2.5658, -1.9393, -1.6156],
2614 [-1.960, -0.398, -0.181],
2615 [-10.04, 0.0, 0.0],
2616 ),
2617 AdfModel::Constant => (
2618 [-3.4336, -2.8621, -2.5671],
2619 [-5.999, -2.738, -1.438],
2620 [-29.25, -8.36, -4.48],
2621 ),
2622 AdfModel::ConstantTrend => (
2623 [-3.9638, -3.4126, -3.1279],
2624 [-8.353, -4.039, -2.418],
2625 [-47.44, -17.83, -7.58],
2626 ),
2627 };
2628
2629 let nf = n as f64;
2630 let inv_n = 1.0 / nf;
2631 let inv_n2 = inv_n * inv_n;
2632
2633 [
2634 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2635 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2636 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2637 ]
2638}
2639
2640#[cfg(test)]
2641mod tests {
2642 use super::*;
2643
2644 #[test]
2645 fn corrections_refuse_what_is_not_a_probability() {
2646 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2647 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2648 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2649 }
2650
2651 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2657 let mut a = seed;
2658 move || {
2659 a = a.wrapping_add(0x6d2b_79f5);
2660 let mut t = a;
2661 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2662 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2663 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2664 }
2665 }
2666
2667 #[test]
2673 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2674 let mut rng = mulberry32(0x5350_4331);
2675 let full: Vec<f64> = (0..20_000)
2676 .map(|_| {
2677 let u = rng();
2678 -(1.0 - u).ln() })
2680 .collect();
2681
2682 let mut prev_a2 = 0.0_f64;
2683 let mut prev_p = f64::INFINITY;
2684 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2685 let r = anderson_darling_test(&full[..n]).expect("computes");
2686 assert!(
2688 r.statistic_star > prev_a2,
2689 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2690 r.statistic_star
2691 );
2692 assert!(
2694 r.p_value < 0.5,
2695 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2696 r.statistic_star,
2697 r.p_value
2698 );
2699 assert!(
2701 r.p_value <= prev_p + 1e-12,
2702 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2703 r.p_value
2704 );
2705 prev_a2 = r.statistic_star;
2706 prev_p = r.p_value;
2707 }
2708 }
2709
2710 #[test]
2715 fn one_sample_null_true() {
2716 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2717 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2718 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2719 }
2720
2721 #[test]
2722 fn one_sample_null_false() {
2723 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2724 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2725 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2726 }
2727
2728 #[test]
2729 fn one_sample_edge_cases() {
2730 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());
2733 }
2734
2735 #[test]
2740 fn two_sample_same_mean() {
2741 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2742 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2743 let r = two_sample_t_test(&a, &b).expect("should compute");
2744 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2745 }
2746
2747 #[test]
2748 fn two_sample_different_means() {
2749 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2750 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2751 let r = two_sample_t_test(&a, &b).expect("should compute");
2752 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2753 }
2754
2755 #[test]
2756 fn two_sample_different_sizes() {
2757 let a = [1.0, 2.0, 3.0];
2758 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2759 let r = two_sample_t_test(&a, &b).expect("should compute");
2760 assert!(r.p_value < 0.05);
2761 }
2762
2763 #[test]
2764 fn two_sample_edge_cases() {
2765 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2766 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2767 }
2768
2769 #[test]
2774 fn paired_no_difference() {
2775 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2776 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2777 let r = paired_t_test(&x, &y).expect("should compute");
2778 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2779 }
2780
2781 #[test]
2782 fn paired_significant_difference() {
2783 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2785 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2786 let r = paired_t_test(&before, &after).expect("should compute");
2787 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2788 assert!(r.statistic < 0.0); }
2790
2791 #[test]
2792 fn paired_edge_cases() {
2793 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2796
2797 #[test]
2802 fn anova_same_means() {
2803 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2804 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2805 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2806 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2807 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2808 }
2809
2810 #[test]
2811 fn anova_different_means() {
2812 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2813 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2814 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2815 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2816 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2817 assert!(r.df_between == 2);
2818 assert!(r.df_within == 12);
2819 }
2820
2821 #[test]
2822 fn anova_ss_decomposition() {
2823 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2824 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2825 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2826 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2828 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2829 assert!(
2830 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2831 "SS decomposition: {ss_total} vs {} + {}",
2832 r.ss_between,
2833 r.ss_within
2834 );
2835 }
2836
2837 #[test]
2838 fn anova_edge_cases() {
2839 let g1 = [1.0, 2.0, 3.0];
2840 assert!(one_way_anova(&[&g1]).is_none()); }
2842
2843 #[test]
2848 fn chi2_gof_uniform() {
2849 let observed = [25.0, 25.0, 25.0, 25.0];
2851 let expected = [25.0, 25.0, 25.0, 25.0];
2852 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2853 assert!((r.statistic).abs() < 1e-15);
2854 assert!((r.p_value - 1.0).abs() < 0.01);
2855 }
2856
2857 #[test]
2858 fn chi2_gof_significant() {
2859 let observed = [90.0, 10.0];
2860 let expected = [50.0, 50.0];
2861 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2862 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2863 }
2864
2865 #[test]
2866 fn chi2_gof_edge_cases() {
2867 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()); }
2871
2872 #[test]
2877 fn chi2_independence_significant() {
2878 let table = [30.0, 10.0, 10.0, 50.0];
2880 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2881 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2882 assert!((r.df - 1.0).abs() < 1e-10);
2883 }
2884
2885 #[test]
2886 fn chi2_independence_not_significant() {
2887 let table = [25.0, 25.0, 25.0, 25.0];
2889 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2890 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2891 }
2892
2893 #[test]
2894 fn chi2_independence_3x3() {
2895 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2896 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2897 assert!((r.df - 4.0).abs() < 1e-10);
2898 assert!(r.p_value < 0.05);
2899 }
2900
2901 #[test]
2902 fn chi2_independence_edge_cases() {
2903 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()); }
2907
2908 #[test]
2913 fn jb_normal_data() {
2914 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2916 let r = jarque_bera_test(&data).expect("should compute");
2917 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2918 }
2919
2920 #[test]
2921 fn jb_skewed_data() {
2922 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2924 let r = jarque_bera_test(&data).expect("should compute");
2925 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2926 }
2927
2928 #[test]
2935 fn jb_is_the_moment_form_the_paper_defines() {
2936 let data = [
2937 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,
2938 3.6, 2.8, 3.2,
2939 ];
2940 let r = jarque_bera_test(&data).expect("20 finite points");
2941 assert!(
2942 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2943 "JB = {}",
2944 r.statistic
2945 );
2946
2947 let n = data.len() as f64;
2950 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2951 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2952 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2953 assert!(
2954 adjusted > r.statistic * 1.3,
2955 "the two forms differ by more than a third here: {adjusted} vs {}",
2956 r.statistic
2957 );
2958 }
2959
2960 #[test]
2964 fn jb_p_value_keeps_its_digits_in_the_tail() {
2965 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
2968 data.push(60.0);
2969 let r = jarque_bera_test(&data).expect("20 finite points");
2970 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
2971 assert!(
2972 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
2973 "this sample is only interesting because the complement underflows"
2974 );
2975 assert!(
2976 r.p_value > 0.0,
2977 "the complement returns 0 here; this must not"
2978 );
2979 assert!(
2980 (r.p_value - 6.702e-21).abs() < 1e-23,
2981 "p = {} (expected about 6.7e-21)",
2982 r.p_value
2983 );
2984 assert!(
2985 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
2986 "p = {} vs exp(-JB/2) = {}",
2987 r.p_value,
2988 (-r.statistic / 2.0).exp()
2989 );
2990 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])
2993 .expect("10 finite points");
2994 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
2995 assert!(
2996 (small.p_value - via_cdf).abs() < 1e-12,
2997 "{} vs {via_cdf}",
2998 small.p_value
2999 );
3000 }
3001
3002 #[test]
3003 fn jb_edge_cases() {
3004 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
3006
3007 #[test]
3012 fn ad_normal_data() {
3013 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3014 let r = anderson_darling_test(&data).expect("should compute");
3015 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3016 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
3017 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
3018 }
3019
3020 #[test]
3021 fn ad_skewed_data() {
3022 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];
3024 let r = anderson_darling_test(&data).expect("should compute");
3025 assert!(
3026 r.p_value < 0.05,
3027 "p = {} (should reject normality)",
3028 r.p_value
3029 );
3030 }
3031
3032 #[test]
3033 fn ad_bimodal_data() {
3034 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3036 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3037 let r = anderson_darling_test(&data).expect("should compute");
3038 assert!(
3039 r.p_value < 0.01,
3040 "p = {} (bimodal should reject normality)",
3041 r.p_value
3042 );
3043 }
3044
3045 #[test]
3046 fn ad_large_normal_sample() {
3047 let n = 100;
3048 let data: Vec<f64> = (1..=n)
3049 .map(|i| {
3050 let p = (i as f64 - 0.5) / n as f64;
3051 special::inverse_normal_cdf(p)
3052 })
3053 .collect();
3054 let r = anderson_darling_test(&data).expect("should compute");
3055 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
3056 }
3057
3058 #[test]
3059 fn ad_edge_cases() {
3060 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());
3063 }
3064
3065 #[test]
3066 fn ad_p_value_ranges() {
3067 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
3072 let r = anderson_darling_test(&near_normal).expect("should compute");
3073 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3074
3075 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];
3077 let r = anderson_darling_test(&heavy_tail).expect("should compute");
3078 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3079 }
3080
3081 #[test]
3086 fn ad_normal_data_large_p() {
3087 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3089 let r = anderson_darling_test(&data).unwrap();
3090 assert!(r.p_value > 0.05, "p={}", r.p_value);
3091 }
3092
3093 #[test]
3094 fn ad_exponential_data_small_p() {
3095 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3097 let r = anderson_darling_test(&data).unwrap();
3098 assert!(r.p_value < 0.05, "p={}", r.p_value);
3099 }
3100
3101 #[test]
3102 fn ad_statistic_non_negative() {
3103 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3104 let r = anderson_darling_test(&data).unwrap();
3105 assert!(r.statistic >= 0.0);
3106 assert!(r.statistic_star >= r.statistic);
3107 }
3108
3109 #[test]
3110 fn ad_p_value_in_range() {
3111 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
3112 let r = anderson_darling_test(&data).unwrap();
3113 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3114 }
3115
3116 #[test]
3117 fn ad_insufficient_data() {
3118 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
3120
3121 #[test]
3122 fn ad_degenerate_data() {
3123 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3125 }
3126
3127 #[test]
3128 fn ad_modified_statistic_formula() {
3129 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
3131 let r = anderson_darling_test(&data).unwrap();
3132 assert!(r.statistic_star > r.statistic - 1e-10);
3133 assert!(anderson_darling_test(&data[..7]).is_none());
3135 }
3136
3137 #[test]
3142 fn ad_correction_factor_formula() {
3143 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3144 let n = data.len() as f64;
3145 let r = anderson_darling_test(&data).unwrap();
3146
3147 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
3149 assert!(
3150 (r.statistic_star - expected_star).abs() < 1e-12,
3151 "A²* = {}, expected = {}",
3152 r.statistic_star,
3153 expected_star
3154 );
3155
3156 assert!(
3158 r.statistic >= 0.0,
3159 "A² = {} must be non-negative",
3160 r.statistic
3161 );
3162 }
3163
3164 #[test]
3170 fn ad_formula_structure_symmetric_vs_skewed() {
3171 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
3173 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
3175
3176 let r_sym = anderson_darling_test(&symmetric).unwrap();
3177 let r_skew = anderson_darling_test(&skewed).unwrap();
3178
3179 assert!(
3180 r_sym.statistic < r_skew.statistic,
3181 "Symmetric A²={} should be less than skewed A²={}",
3182 r_sym.statistic,
3183 r_skew.statistic
3184 );
3185 }
3186
3187 #[test]
3192 fn ad_pvalue_invariants() {
3193 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3195 let r_normal = anderson_darling_test(&normal_data).unwrap();
3196 assert!(
3197 r_normal.p_value > 0.05,
3198 "Normal data: p = {} (expected > 0.05)",
3199 r_normal.p_value
3200 );
3201
3202 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3204 let r_exp = anderson_darling_test(&exp_data).unwrap();
3205 assert!(
3206 r_exp.p_value < 0.05,
3207 "Exponential data: p = {} (expected < 0.05)",
3208 r_exp.p_value
3209 );
3210 }
3211
3212 #[test]
3217 fn sw_normal_data() {
3218 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3220 let r = shapiro_wilk_test(&data).expect("should compute");
3221 assert!(r.w > 0.9, "W = {}", r.w);
3222 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3223 }
3224
3225 #[test]
3226 fn sw_bimodal_data() {
3227 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3229 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3230 let r = shapiro_wilk_test(&data).expect("should compute");
3231 assert!(
3232 r.p_value < 0.01,
3233 "p = {} (bimodal should reject normality)",
3234 r.p_value
3235 );
3236 }
3237
3238 #[test]
3239 fn sw_n3() {
3240 let data = [1.0, 2.0, 3.0];
3241 let r = shapiro_wilk_test(&data).expect("n=3 should work");
3242 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3243 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3244 }
3245
3246 #[test]
3247 fn sw_n4() {
3248 let data = [1.0, 2.0, 3.0, 4.0];
3249 let r = shapiro_wilk_test(&data).expect("n=4 should work");
3250 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3251 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3252 }
3253
3254 #[test]
3255 fn sw_n5() {
3256 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
3257 let r = shapiro_wilk_test(&data).expect("n=5 should work");
3258 assert!(r.w > 0.9, "W = {}", r.w);
3259 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3260 }
3261
3262 #[test]
3263 fn sw_skewed_data() {
3264 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];
3266 let r = shapiro_wilk_test(&data).expect("should compute");
3267 assert!(
3268 r.p_value < 0.05,
3269 "p = {} (skewed data should reject normality)",
3270 r.p_value
3271 );
3272 }
3273
3274 #[test]
3275 fn sw_large_normal_sample() {
3276 let n = 100;
3279 let data: Vec<f64> = (1..=n)
3280 .map(|i| {
3281 let p = (i as f64 - 0.5) / n as f64;
3282 special::inverse_normal_cdf(p)
3283 })
3284 .collect();
3285 let r = shapiro_wilk_test(&data).expect("should compute");
3286 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
3287 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3288 }
3289
3290 #[test]
3291 fn sw_w_bounded() {
3292 let datasets: Vec<Vec<f64>> = vec![
3294 vec![1.0, 2.0, 3.0],
3295 vec![1.0, 1.0, 2.0, 3.0, 3.0],
3296 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
3297 (0..20).map(|i| (i as f64).powi(2)).collect(),
3298 ];
3299 for (idx, data) in datasets.iter().enumerate() {
3300 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
3301 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
3302 assert!(
3303 r.p_value >= 0.0 && r.p_value <= 1.0,
3304 "dataset {idx}: p = {}",
3305 r.p_value
3306 );
3307 }
3308 }
3309
3310 #[test]
3311 fn sw_edge_cases() {
3312 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()); }
3318
3319 #[test]
3320 fn sw_n5001_rejected() {
3321 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
3322 assert!(shapiro_wilk_test(&data).is_none()); }
3324
3325 #[test]
3330 fn mw_clearly_different() {
3331 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
3332 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
3333 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3334 assert!(r.p_value < 0.05, "p = {}", r.p_value);
3335 }
3336
3337 #[test]
3338 fn mw_same_distribution() {
3339 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3340 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3341 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3342 assert!(
3343 r.p_value > 0.3,
3344 "p = {} (interleaved, same dist)",
3345 r.p_value
3346 );
3347 }
3348
3349 #[test]
3350 fn mw_with_ties() {
3351 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3352 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3353 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3354 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3355 }
3356
3357 #[test]
3358 fn mw_different_sizes() {
3359 let a = [1.0, 2.0, 3.0];
3360 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3361 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3362 assert!(r.p_value < 0.05);
3363 }
3364
3365 #[test]
3366 fn mw_edge_cases() {
3367 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());
3370 }
3371
3372 #[test]
3377 fn wilcoxon_significant_increase() {
3378 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3379 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3380 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3381 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3382 }
3383
3384 #[test]
3385 fn wilcoxon_no_difference() {
3386 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3387 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3388 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3389 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3390 }
3391
3392 #[test]
3393 fn wilcoxon_with_ties() {
3394 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3396 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3398 assert!(r.statistic > 0.0);
3400 }
3401
3402 #[test]
3403 fn wilcoxon_with_zero_diffs() {
3404 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3406 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3408 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3409 }
3410
3411 #[test]
3412 fn wilcoxon_edge_cases() {
3413 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());
3417 }
3418
3419 #[test]
3424 fn mann_whitney_small_samples_use_the_exact_distribution() {
3425 let cases = [
3427 (3, 3, 0, 0.1000, 0.0495),
3428 (4, 4, 0, 0.0286, 0.0209),
3429 (4, 4, 1, 0.0571, 0.0433),
3430 (5, 5, 2, 0.0317, 0.0283),
3431 (8, 8, 13, 0.0499, 0.0460),
3432 (10, 10, 23, 0.0433, 0.0413),
3433 ];
3434 for (n1, n2, u, exact, old) in cases {
3435 let (a, b) = separated_samples(n1, n2, u);
3436 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3437 assert_eq!(r.statistic, u as f64);
3438 assert!(
3439 (r.p_value - exact).abs() < 1e-4,
3440 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3441 r.p_value
3442 );
3443 }
3444 }
3445
3446 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3450 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3451 let mut remaining = u;
3452 for k in (0..n1).rev() {
3453 let room = n2.min(remaining);
3454 ranks_a[k] += room;
3455 remaining -= room;
3456 if remaining == 0 {
3457 break;
3458 }
3459 }
3460 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3461 (
3462 ranks_a.iter().map(|&r| r as f64).collect(),
3463 ranks_b.iter().map(|&r| r as f64).collect(),
3464 )
3465 }
3466
3467 #[test]
3469 fn wilcoxon_small_samples_use_the_exact_distribution() {
3470 let cases = [
3472 (5, 0, 0.0625),
3473 (5, 1, 0.125),
3474 (6, 2, 0.09375),
3475 (8, 5, 0.078125),
3476 (10, 8, 0.048828125),
3477 (12, 14, 0.0522460938),
3478 ];
3479 for (n, t_plus, exact) in cases {
3480 let positive = ranks_summing_to(n, t_plus);
3482 let x: Vec<f64> = (1..=n)
3483 .map(|r| {
3484 if positive.contains(&r) {
3485 r as f64
3486 } else {
3487 -(r as f64)
3488 }
3489 })
3490 .collect();
3491 let y = vec![0.0; n];
3492 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3493 assert_eq!(r.statistic, t_plus as f64);
3494 assert!(
3495 (r.p_value - exact).abs() < 1e-9,
3496 "n={n} T+={t_plus}: p={} want {exact}",
3497 r.p_value
3498 );
3499 }
3500 }
3501
3502 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3504 let mut out = Vec::new();
3505 let mut left = target;
3506 for r in (1..=n).rev() {
3507 if r <= left {
3508 out.push(r);
3509 left -= r;
3510 }
3511 }
3512 assert_eq!(left, 0);
3513 out
3514 }
3515
3516 #[test]
3519 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3520 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3521 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3522 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3523 let (n1, n2) = (60.0, 60.0);
3524 let mu = n1 * n2 / 2.0;
3525 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3526 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3527 let want = 2.0 * special::standard_normal_sf(z);
3528 assert!((r.p_value - want).abs() < 1e-12);
3529
3530 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3532 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3533 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3534 let mu = 1250.0;
3535 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3536 let u = mann_whitney_u_test(&a, &b)
3537 .expect("valid samples")
3538 .statistic;
3539 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3540 assert!(
3541 (exact - approx).abs() < 1e-3,
3542 "exact {exact} vs approx {approx}"
3543 );
3544 }
3545
3546 #[test]
3551 fn kw_clearly_different() {
3552 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3553 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3554 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3555 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3556 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3557 assert!((r.df - 2.0).abs() < 1e-10);
3558 }
3559
3560 #[test]
3561 fn kw_same_distribution() {
3562 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3563 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3564 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3565 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3566 }
3567
3568 #[test]
3569 fn kw_with_ties() {
3570 let g1 = [1.0, 2.0, 2.0, 3.0];
3571 let g2 = [3.0, 4.0, 4.0, 5.0];
3572 let g3 = [5.0, 6.0, 6.0, 7.0];
3573 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3574 assert!(r.statistic > 0.0);
3575 }
3576
3577 #[test]
3578 fn kw_edge_cases() {
3579 let g1 = [1.0, 2.0, 3.0];
3580 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3582
3583 #[test]
3588 fn levene_equal_variance() {
3589 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3590 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3591 let r = levene_test(&[&g1, &g2]).expect("should compute");
3592 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3594 }
3595
3596 #[test]
3597 fn levene_unequal_variance() {
3598 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");
3601 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3602 }
3603
3604 #[test]
3605 fn levene_three_groups() {
3606 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3607 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3608 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3609 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3610 assert!(r.df >= 2.0);
3611 }
3612
3613 #[test]
3614 fn levene_edge_cases() {
3615 let g1 = [1.0, 2.0, 3.0];
3616 assert!(levene_test(&[&g1]).is_none()); }
3618
3619 #[test]
3624 fn bonferroni_basic() {
3625 let ps = [0.01, 0.04, 0.03, 0.005];
3626 let adj = bonferroni_correction(&ps).expect("should compute");
3627 assert!((adj[0] - 0.04).abs() < 1e-10);
3628 assert!((adj[1] - 0.16).abs() < 1e-10);
3629 assert!((adj[2] - 0.12).abs() < 1e-10);
3630 assert!((adj[3] - 0.02).abs() < 1e-10);
3631 }
3632
3633 #[test]
3634 fn bonferroni_capped_at_one() {
3635 let ps = [0.5, 0.6];
3636 let adj = bonferroni_correction(&ps).expect("should compute");
3637 assert!((adj[0] - 1.0).abs() < 1e-10);
3638 assert!((adj[1] - 1.0).abs() < 1e-10);
3639 }
3640
3641 #[test]
3642 fn bh_basic() {
3643 let ps = [0.01, 0.04, 0.03, 0.005];
3644 let adj = benjamini_hochberg(&ps).expect("should compute");
3645 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3647 assert!(
3648 adjusted >= orig - 1e-15,
3649 "adj[{i}] = {adjusted} < original {orig}"
3650 );
3651 }
3652 }
3655
3656 #[test]
3657 fn bh_all_significant() {
3658 let ps = [0.001, 0.002, 0.003];
3659 let adj = benjamini_hochberg(&ps).expect("should compute");
3660 for &a in &adj {
3661 assert!(a < 0.05);
3662 }
3663 }
3664
3665 #[test]
3666 fn correction_edge_cases() {
3667 assert!(bonferroni_correction(&[]).is_none());
3668 assert!(benjamini_hochberg(&[]).is_none());
3669 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3670 }
3671
3672 #[test]
3677 fn bartlett_equal_variances() {
3678 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3679 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3680 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3681 assert!(
3682 r.p_value > 0.9,
3683 "equal variance → p high, got {}",
3684 r.p_value
3685 );
3686 }
3687
3688 #[test]
3689 fn bartlett_unequal_variances() {
3690 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");
3693 assert!(
3694 r.p_value < 0.01,
3695 "very different variances → p < 0.01, got {}",
3696 r.p_value
3697 );
3698 assert!((r.df - 1.0).abs() < 1e-10); }
3700
3701 #[test]
3702 fn bartlett_three_groups() {
3703 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3704 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3705 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3707 assert!(r.p_value < 0.05, "one group with high variance");
3708 assert!((r.df - 2.0).abs() < 1e-10); }
3710
3711 #[test]
3712 fn bartlett_edge_cases() {
3713 let g1 = [1.0, 2.0, 3.0];
3714 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3718
3719 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3721 }
3722
3723 #[test]
3728 fn fisher_tea_tasting() {
3729 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3731 assert!(
3733 (r.p_value - 0.4857).abs() < 0.01,
3734 "p ≈ 0.4857, got {}",
3735 r.p_value
3736 );
3737 }
3738
3739 #[test]
3740 fn fisher_significant() {
3741 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3743 assert!(r.p_value < 0.001, "perfect association → p very small");
3744 }
3745
3746 #[test]
3747 fn fisher_no_association() {
3748 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3750 assert!(
3751 r.p_value > 0.9,
3752 "no association → p ≈ 1.0, got {}",
3753 r.p_value
3754 );
3755 }
3756
3757 #[test]
3758 fn fisher_small_table() {
3759 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3761 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3762 }
3763
3764 #[test]
3765 fn fisher_asymmetric() {
3766 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3768 assert!(r.p_value < 0.05, "significant association");
3769 }
3770
3771 #[test]
3772 fn fisher_edge_cases() {
3773 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()); }
3778
3779 #[test]
3780 fn fisher_odds_ratio() {
3781 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3782 assert!(
3784 (r.statistic - 9.0).abs() < 1e-10,
3785 "OR = 9, got {}",
3786 r.statistic
3787 );
3788 }
3789
3790 #[test]
3795 fn mk_increasing_trend() {
3796 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3797 let r = mann_kendall_test(&data).expect("should compute");
3798 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3799 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3800 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3801 assert!(r.s_statistic > 0);
3802 }
3803
3804 #[test]
3805 fn mk_decreasing_trend() {
3806 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3807 let r = mann_kendall_test(&data).expect("should compute");
3808 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3809 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3810 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3811 assert!(r.s_statistic < 0);
3812 }
3813
3814 #[test]
3815 fn mk_no_trend() {
3816 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3818 let r = mann_kendall_test(&data).expect("should compute");
3819 assert!(
3821 r.p_value > 0.05,
3822 "p = {} (should be > 0.05 for no trend)",
3823 r.p_value
3824 );
3825 }
3826
3827 #[test]
3828 fn mk_perfect_monotone() {
3829 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3830 let r = mann_kendall_test(&data).expect("should compute");
3831 assert_eq!(r.s_statistic, 45);
3833 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3834 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3835 }
3836
3837 #[test]
3838 fn mk_with_ties() {
3839 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3840 let r = mann_kendall_test(&data).expect("should compute");
3841 assert!(r.s_statistic > 0);
3842 let n = data.len() as f64;
3844 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3845 assert!(r.variance < base_var, "ties should reduce variance");
3846 }
3847
3848 #[test]
3849 fn mk_edge_cases() {
3850 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3852 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3854 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3856 }
3857
3858 #[test]
3859 fn mk_minimum_n() {
3860 let data = [1.0, 2.0, 3.0, 4.0];
3862 let r = mann_kendall_test(&data).expect("n=4 should work");
3863 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3865 }
3866
3867 #[test]
3868 fn mk_sen_slope_robust_to_outlier() {
3869 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3871 let r = mann_kendall_test(&data).expect("should compute");
3872 assert!(
3874 (r.sen_slope - 1.0).abs() < 0.5,
3875 "sen slope = {}, expected ≈ 1.0",
3876 r.sen_slope
3877 );
3878 }
3879
3880 #[test]
3885 fn adf_stationary_mean_reverting() {
3886 let data = [
3889 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,
3890 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,
3891 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,
3892 ];
3893 let r = adf_test(&data, AdfModel::Constant, AdfLags::default()).expect("should compute");
3894 assert!(
3896 r.rejected[2],
3897 "should reject at 10%: stat={}, cv={}",
3898 r.statistic, r.critical_values[2]
3899 );
3900 }
3901
3902 #[test]
3903 fn adf_nonstationary_random_walk() {
3904 let increments = [
3906 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,
3907 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,
3908 -0.13, 0.15, -0.1,
3909 ];
3910 let mut walk = Vec::with_capacity(increments.len());
3911 let mut cum = 0.0;
3912 for &inc in &increments {
3913 cum += inc;
3914 walk.push(cum);
3915 }
3916 let r = adf_test(&walk, AdfModel::Constant, AdfLags::default()).expect("should compute");
3917 assert!(
3919 !r.rejected[0],
3920 "should NOT reject at 1%: stat={}, cv={}",
3921 r.statistic, r.critical_values[0]
3922 );
3923 }
3924
3925 #[test]
3926 fn adf_with_fixed_lags() {
3927 let mut rng = mulberry32(0xadf2);
3931 let data: Vec<f64> = (0..50)
3932 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
3933 .collect();
3934 let r = adf_test(&data, AdfModel::Constant, AdfLags::Fixed(2)).expect("should compute");
3935 assert_eq!(r.n_lags, 2);
3936 assert!(r.statistic.is_finite());
3937 }
3938
3939 #[test]
3940 fn adf_constant_trend_model() {
3941 let data: Vec<f64> = (0..30)
3944 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3945 .collect();
3946 let r =
3947 adf_test(&data, AdfModel::ConstantTrend, AdfLags::default()).expect("should compute");
3948 assert!(r.statistic.is_finite());
3949 assert_eq!(r.critical_values.len(), 3);
3950 }
3951
3952 #[test]
3953 fn adf_edge_cases() {
3954 assert_eq!(
3955 adf_test(&[1.0; 9], AdfModel::Constant, AdfLags::default()).unwrap_err(),
3956 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
3957 );
3958 let mut data = vec![0.0; 20];
3959 data[5] = f64::NAN;
3960 assert_eq!(
3961 adf_test(&data, AdfModel::Constant, AdfLags::default()).unwrap_err(),
3962 TimeSeriesError::ValueNotFinite { index: 5 }
3963 );
3964 assert_eq!(adf_max_lags(20, AdfModel::Constant), 8);
3966 let walk: Vec<f64> = (0..20).map(|i| ((i * 37) % 11) as f64).collect();
3967 for lags in [AdfLags::Fixed(9), AdfLags::Aic { max: Some(9) }] {
3968 let option = if matches!(lags, AdfLags::Fixed(_)) {
3969 "lags"
3970 } else {
3971 "max_lags"
3972 };
3973 assert_eq!(
3974 adf_test(&walk, AdfModel::Constant, lags).unwrap_err(),
3975 TimeSeriesError::OptionOutOfRange {
3976 option,
3977 got: 9.0,
3978 min: 0.0,
3979 max: 8.0
3980 }
3981 );
3982 }
3983 assert_eq!(
3984 adf_test(&[3.0; 20], AdfModel::Constant, AdfLags::default()).unwrap_err(),
3985 TimeSeriesError::DegenerateRegression
3986 );
3987 }
3988
3989 #[test]
3995 fn adf_exact_fit_is_undefined_and_not_chosen() {
3996 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
3997 let mut walk = vec![0.0_f64; 60];
3998 for i in 1..walk.len() {
3999 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
4000 }
4001 assert_eq!(
4002 adf_test(&walk, AdfModel::ConstantTrend, AdfLags::Fixed(9)).unwrap_err(),
4003 TimeSeriesError::DegenerateRegression
4004 );
4005 let r = adf_test(&walk, AdfModel::ConstantTrend, AdfLags::default())
4006 .expect("a lag below 9 fits");
4007 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
4008 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
4009 }
4010
4011 #[test]
4015 fn adf_matches_statsmodels() {
4016 let mut x = vec![0.0_f64; 120];
4017 for t in 1..x.len() {
4018 let e = ((t * 7919) % 101) as f64 / 50.0 - 1.0;
4019 x[t] = 0.7 * x[t - 1] + e;
4020 }
4021 for (model, lags, stat, n_lags, n_obs) in [
4022 (
4023 AdfModel::Constant,
4024 AdfLags::Aic { max: Some(8) },
4025 -3.436_052_894_8,
4026 5,
4027 114,
4028 ),
4029 (
4030 AdfModel::Constant,
4031 AdfLags::Aic { max: None },
4032 -3.436_052_894_8,
4033 5,
4034 114,
4035 ),
4036 (
4037 AdfModel::ConstantTrend,
4038 AdfLags::Aic { max: Some(6) },
4039 -3.505_247_069_0,
4040 5,
4041 114,
4042 ),
4043 (
4044 AdfModel::None,
4045 AdfLags::Aic { max: Some(5) },
4046 -3.459_240_021_9,
4047 5,
4048 114,
4049 ),
4050 (
4051 AdfModel::Constant,
4052 AdfLags::Fixed(3),
4053 -4.652_930_484_4,
4054 3,
4055 116,
4056 ),
4057 ] {
4058 let r = adf_test(&x, model, lags).expect("fits");
4059 assert_eq!((r.n_lags, r.n_obs), (n_lags, n_obs), "{model:?} {lags:?}");
4060 assert!(
4061 (r.statistic - stat).abs() < 1e-8,
4062 "{model:?} {lags:?}: {}",
4063 r.statistic
4064 );
4065 }
4066 }
4067
4068 #[test]
4069 fn adf_critical_values_constant() {
4070 let cv = adf_critical_values(AdfModel::Constant, 100);
4072 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
4074 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
4075 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
4076 }
4077
4078 #[test]
4079 fn adf_critical_values_ordering() {
4080 let cv = adf_critical_values(AdfModel::Constant, 50);
4081 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
4083 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
4084 }
4085}
4086
4087#[cfg(test)]
4090mod scipy_reference {
4091 use super::*;
4092
4093 const A: [f64; 8] = [5.1, 4.9, 5.6, 5.8, 6.0, 5.2, 4.7, 5.5];
4094 const B: [f64; 7] = [6.3, 5.9, 6.8, 7.1, 6.0, 6.6, 7.4];
4095 const G1: [f64; 5] = [23.1, 25.4, 22.8, 26.0, 24.3];
4096 const G2: [f64; 6] = [27.2, 28.9, 26.5, 29.4, 27.8, 28.1];
4097 const G3: [f64; 4] = [22.0, 21.4, 24.9, 23.3];
4098
4099 fn close(got: f64, want: f64, rel: f64, what: &str) {
4101 let scale = want.abs().max(1.0);
4102 assert!(
4103 (got - want).abs() <= rel * scale,
4104 "{what}: got {got}, SciPy {want} (relative tolerance {rel})"
4105 );
4106 }
4107
4108 #[test]
4109 fn t_tests_reproduce_scipy() {
4110 let r = one_sample_t_test(&A, 5.0).expect("valid");
4112 close(r.statistic, 2.197950389679656, 1e-12, "one-sample t");
4113 close(r.p_value, 0.06392412293886501, 1e-9, "one-sample p");
4114 assert_eq!(r.df, 7.0);
4115 let r = two_sample_t_test(&A, &B).expect("valid");
4117 close(r.statistic, -4.67567567567568, 1e-12, "Welch t");
4118 close(r.df, 11.56763357082458, 1e-12, "Welch df");
4119 close(r.p_value, 0.0005912510074711228, 1e-9, "Welch p");
4120 let x = [12.1, 13.4, 11.8, 14.2, 12.9, 13.0];
4122 let y = [12.9, 13.9, 12.0, 15.1, 13.5, 13.2];
4123 let r = paired_t_test(&x, &y).expect("valid");
4124 close(r.statistic, -4.437601569801824, 1e-12, "paired t");
4125 close(r.p_value, 0.006779965572779817, 1e-9, "paired p");
4126 assert_eq!(r.df, 5.0);
4127 }
4128
4129 #[test]
4130 fn anova_and_equal_variance_tests_reproduce_scipy() {
4131 let groups: [&[f64]; 3] = [&G1, &G2, &G3];
4132 let r = one_way_anova(&groups).expect("valid");
4134 close(r.f_statistic, 20.526701958197258, 1e-12, "ANOVA F");
4135 close(r.p_value, 0.00013390844134272023, 1e-9, "ANOVA p");
4136 let r = levene_test(&groups).expect("valid");
4138 close(r.statistic, 0.4752743705616534, 1e-12, "Brown-Forsythe W");
4139 close(r.p_value, 0.6329340379295331, 1e-9, "Brown-Forsythe p");
4140 let r = bartlett_test(&groups).expect("valid");
4142 close(r.statistic, 0.5282298427149905, 1e-12, "Bartlett T");
4143 close(r.p_value, 0.7678852881660961, 1e-9, "Bartlett p");
4144 }
4145
4146 #[test]
4147 fn kruskal_wallis_with_ties_reproduces_scipy() {
4148 let r = kruskal_wallis_test(&[
4150 &[1.0, 2.0, 2.0, 3.0, 5.0],
4151 &[4.0, 4.0, 6.0, 7.0],
4152 &[5.0, 8.0, 8.0, 9.0, 10.0],
4153 ])
4154 .expect("valid");
4155 close(r.statistic, 9.22394678492239, 1e-12, "H (tie-corrected)");
4156 close(r.p_value, 0.009932198829456881, 1e-9, "Kruskal-Wallis p");
4157 }
4158
4159 #[test]
4160 fn chi_squared_and_fisher_tests_reproduce_scipy() {
4161 let r = chi_squared_goodness_of_fit(&[18.0, 22.0, 30.0, 30.0], &[25.0; 4]).expect("valid");
4163 close(r.statistic, 4.32, 1e-12, "goodness-of-fit chi-squared");
4164 close(r.p_value, 0.22891886433610517, 1e-9, "goodness-of-fit p");
4165 let r = chi_squared_independence(&[12.0, 7.0, 9.0, 5.0, 14.0, 11.0], 2, 3).expect("valid");
4167 close(
4168 r.statistic,
4169 5.353085901027075,
4170 1e-12,
4171 "independence chi-squared",
4172 );
4173 close(r.p_value, 0.06880059054935043, 1e-9, "independence p");
4174 assert_eq!(r.df, 2.0);
4175 let r = fisher_exact_test(8, 2, 1, 5).expect("valid");
4177 close(r.p_value, 0.034965034965034975, 1e-9, "Fisher p");
4178 }
4179
4180 #[test]
4181 fn shapiro_wilk_reproduces_scipy() {
4182 let r = shapiro_wilk_test(&[
4184 148.0, 154.0, 158.0, 160.0, 161.0, 162.0, 166.0, 170.0, 182.0, 195.0, 236.0,
4185 ])
4186 .expect("valid");
4187 close(r.w, 0.7888146948631716, 1e-9, "W, skewed");
4188 close(r.p_value, 0.006703814061898823, 1e-9, "p, skewed");
4189 let r = shapiro_wilk_test(&[
4191 2.1, 3.4, 1.9, 5.6, 4.4, 3.3, 2.8, 4.0, 3.9, 3.1, 2.6, 4.8, 3.6, 3.0, 4.2,
4192 ])
4193 .expect("valid");
4194 close(r.w, 0.9864343014896592, 1e-9, "W, near normal");
4195 close(r.p_value, 0.9958703691266937, 1e-9, "p, near normal");
4196 }
4197
4198 #[test]
4199 fn mann_kendall_tau_and_sen_slope_reproduce_scipy() {
4200 let mk = [4.2, 4.0, 4.6, 4.9, 4.7, 5.3, 5.1, 5.8, 5.6, 6.2, 6.0, 6.7];
4203 let r = mann_kendall_test(&mk).expect("valid");
4204 close(r.kendall_tau, 0.8484848484848484, 1e-12, "Kendall tau");
4205 close(r.sen_slope, 0.22500000000000003, 1e-12, "Sen's slope");
4206 }
4207
4208 #[test]
4209 fn benjamini_hochberg_reproduces_scipy() {
4210 let adjusted = benjamini_hochberg(&[0.01, 0.04, 0.03, 0.005, 0.20, 0.50]).expect("valid");
4212 for (got, want) in adjusted.iter().zip([0.03, 0.06, 0.06, 0.03, 0.24, 0.5]) {
4213 close(*got, want, 1e-12, "BH-adjusted p");
4214 }
4215 }
4216}
4217
4218#[cfg(test)]
4219mod proptests {
4220 use super::*;
4221 use proptest::prelude::*;
4222
4223 proptest! {
4224 #[test]
4225 fn one_sample_p_bounded(
4226 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
4227 mu0 in -1e3_f64..1e3
4228 ) {
4229 if let Some(r) = one_sample_t_test(&data, mu0) {
4230 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4231 }
4232 }
4233
4234 #[test]
4235 fn two_sample_p_bounded(
4236 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4237 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4238 ) {
4239 if let Some(r) = two_sample_t_test(&a, &b) {
4240 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4241 }
4242 }
4243
4244 #[test]
4245 fn anova_p_bounded(
4246 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4247 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4248 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4249 ) {
4250 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
4251 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4252 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
4253 }
4254 }
4255
4256 #[test]
4257 fn bonferroni_monotone(
4258 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
4259 ) {
4260 let adj = bonferroni_correction(&ps).expect("should compute");
4261 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
4262 prop_assert!(adjusted >= orig - 1e-15,
4263 "adj[{i}] = {adjusted} < orig = {orig}");
4264 }
4265 }
4266
4267 #[test]
4268 fn shapiro_wilk_p_bounded(
4269 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
4270 ) {
4271 if let Some(r) = shapiro_wilk_test(&data) {
4272 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
4273 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4274 }
4275 }
4276
4277 #[test]
4278 fn anderson_darling_p_bounded(
4279 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
4280 ) {
4281 if let Some(r) = anderson_darling_test(&data) {
4282 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
4283 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4284 }
4285 }
4286
4287 #[test]
4288 fn mann_whitney_p_bounded(
4289 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4290 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4291 ) {
4292 if let Some(r) = mann_whitney_u_test(&a, &b) {
4293 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4294 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
4295 }
4296 }
4297
4298 #[test]
4299 fn wilcoxon_p_bounded(
4300 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4301 ) {
4302 let zeros: Vec<f64> = vec![0.0; diffs.len()];
4303 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
4304 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4305 }
4306 }
4307
4308 #[test]
4309 fn bartlett_p_bounded(
4310 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4311 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4312 ) {
4313 if let Some(r) = bartlett_test(&[&g1, &g2]) {
4314 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4315 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
4316 }
4317 }
4318
4319 #[test]
4320 fn fisher_p_bounded(
4321 a in 0_u64..20,
4322 b in 0_u64..20,
4323 c in 0_u64..20,
4324 d in 0_u64..20,
4325 ) {
4326 if let Some(r) = fisher_exact_test(a, b, c, d) {
4327 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4328 }
4329 }
4330
4331 #[test]
4332 fn mann_kendall_p_bounded(
4333 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
4334 ) {
4335 if let Some(r) = mann_kendall_test(&data) {
4336 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4337 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
4338 "tau = {}", r.kendall_tau);
4339 }
4340 }
4341
4342 #[test]
4343 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
4344 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
4345 let r = mann_kendall_test(&data).expect("should compute");
4346 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
4347 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
4348 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
4349 }
4350 }
4351}
4352
4353#[cfg(test)]
4356mod definition_reference {
4357 use super::*;
4358
4359 #[test]
4360 fn bonferroni_is_p_times_m_capped_at_one() {
4361 let adjusted = bonferroni_correction(&[0.01, 0.02, 0.04, 0.3, 0.0004]).expect("valid");
4362 let want = [0.05, 0.1, 0.2, 1.0, 0.002];
4363 assert_eq!(adjusted.len(), want.len());
4364 for (got, want) in adjusted.iter().zip(want) {
4365 assert!((got - want).abs() <= 1e-15, "got {got}, want {want}");
4366 }
4367 }
4368}
4369
4370#[cfg(test)]
4373mod scipy_tail_reference {
4374 use super::*;
4375
4376 fn close(got: f64, want: f64, what: &str) {
4377 assert!(
4378 (got - want).abs() <= 1e-9 * want.abs(),
4379 "{what}: got {got:e}, SciPy {want:e}"
4380 );
4381 }
4382
4383 #[test]
4384 fn t_tests_keep_their_tail() {
4385 let a = [100.2, 99.8, 100.5, 100.1, 99.9, 100.3, 100.0, 99.7];
4386 let b = [120.1, 119.7, 120.4, 120.0, 119.8, 120.3];
4387 let r = one_sample_t_test(&a, 90.0).expect("valid");
4389 close(r.p_value, 1.6824197797300066e-12, "one-sample p");
4390 let r = two_sample_t_test(&a, &b).expect("valid");
4392 close(r.p_value, 8.838218005160137e-19, "Welch p");
4393 }
4394
4395 #[test]
4396 fn f_and_chi_squared_tests_keep_their_tail() {
4397 let (g1, g2, g3) = (
4399 [10.1, 10.3, 9.9, 10.0, 10.2],
4400 [12.0, 12.2, 11.9, 12.1, 12.3],
4401 [14.1, 13.9, 14.0, 14.2, 13.8],
4402 );
4403 let r = one_way_anova(&[&g1, &g2, &g3]).expect("valid");
4404 close(r.p_value, 2.297566098988527e-13, "ANOVA p");
4405 let r = chi_squared_independence(&[80.0, 20.0, 30.0, 70.0], 2, 2).expect("valid");
4407 close(r.p_value, 1.1885852395646684e-12, "independence p");
4408 let r = chi_squared_goodness_of_fit(&[90.0, 10.0, 5.0, 95.0], &[50.0; 4]).expect("valid");
4410 close(r.p_value, 3.1567404374222543e-31, "goodness-of-fit p");
4411 let low: Vec<f64> = (1..=10).map(f64::from).collect();
4413 let mid: Vec<f64> = (11..=20).map(f64::from).collect();
4414 let high: Vec<f64> = (21..=30).map(f64::from).collect();
4415 let r = kruskal_wallis_test(&[&low, &mid, &high]).expect("valid");
4416 close(r.p_value, 2.49000508218493e-06, "Kruskal-Wallis p");
4417 }
4418}