1use crate::error::{check_option, check_series, TimeSeriesError};
17use std::ops::RangeInclusive;
18use u_numflow::special;
19use u_numflow::stats;
20
21#[derive(Debug, Clone, Copy)]
23pub struct TestResult {
24 pub statistic: f64,
26 pub df: f64,
28 pub p_value: f64,
30}
31
32pub fn one_sample_t_test(data: &[f64], mu0: f64) -> Option<TestResult> {
56 let n = data.len();
57 if n < 2 {
58 return None;
59 }
60 if data.iter().any(|v| !v.is_finite()) || !mu0.is_finite() {
61 return None;
62 }
63
64 let mean = stats::mean(data)?;
65 let sd = stats::std_dev(data)?;
66
67 if sd < 1e-300 {
68 return None; }
70
71 let t = (mean - mu0) / (sd / (n as f64).sqrt());
72 let df = (n - 1) as f64;
73 let p_value = 2.0 * special::t_distribution_sf(t.abs(), df);
74
75 Some(TestResult {
76 statistic: t,
77 df,
78 p_value,
79 })
80}
81
82pub fn two_sample_t_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
109 let n1 = a.len();
110 let n2 = b.len();
111 if n1 < 2 || n2 < 2 {
112 return None;
113 }
114 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
115 return None;
116 }
117
118 let mean1 = stats::mean(a)?;
119 let mean2 = stats::mean(b)?;
120 let var1 = stats::variance(a)?;
121 let var2 = stats::variance(b)?;
122
123 let n1f = n1 as f64;
124 let n2f = n2 as f64;
125
126 let se_sq = var1 / n1f + var2 / n2f;
127 if se_sq < 1e-300 {
128 return None;
129 }
130
131 let t = (mean1 - mean2) / se_sq.sqrt();
132
133 let v1 = var1 / n1f;
135 let v2 = var2 / n2f;
136 let df = (v1 + v2).powi(2) / (v1 * v1 / (n1f - 1.0) + v2 * v2 / (n2f - 1.0));
137
138 let p_value = 2.0 * special::t_distribution_sf(t.abs(), df);
139
140 Some(TestResult {
141 statistic: t,
142 df,
143 p_value,
144 })
145}
146
147pub fn paired_t_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
169 if x.len() != y.len() || x.len() < 2 {
170 return None;
171 }
172
173 let diffs: Vec<f64> = x.iter().zip(y.iter()).map(|(&a, &b)| a - b).collect();
174 one_sample_t_test(&diffs, 0.0)
175}
176
177#[derive(Debug, Clone)]
183pub struct AnovaResult {
184 pub f_statistic: f64,
186 pub df_between: usize,
188 pub df_within: usize,
190 pub p_value: f64,
192 pub ss_between: f64,
194 pub ss_within: f64,
196 pub ms_between: f64,
198 pub ms_within: f64,
200 pub group_means: Vec<f64>,
202 pub grand_mean: f64,
204}
205
206pub fn one_way_anova(groups: &[&[f64]]) -> Option<AnovaResult> {
235 let k = groups.len();
236 if k < 2 {
237 return None;
238 }
239
240 for g in groups {
241 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
242 return None;
243 }
244 }
245
246 let total_n: usize = groups.iter().map(|g| g.len()).sum();
247
248 let grand_sum: f64 = groups.iter().flat_map(|g| g.iter()).sum();
250 let grand_mean = grand_sum / total_n as f64;
251
252 let group_means: Vec<f64> = groups
254 .iter()
255 .map(|g| g.iter().sum::<f64>() / g.len() as f64)
256 .collect();
257
258 let ss_between: f64 = groups
260 .iter()
261 .zip(group_means.iter())
262 .map(|(g, &gm)| g.len() as f64 * (gm - grand_mean).powi(2))
263 .sum();
264
265 let ss_within: f64 = groups
266 .iter()
267 .zip(group_means.iter())
268 .map(|(g, &gm)| g.iter().map(|&x| (x - gm).powi(2)).sum::<f64>())
269 .sum();
270
271 let df_between = k - 1;
272 let df_within = total_n - k;
273
274 if df_within == 0 {
275 return None;
276 }
277
278 let ms_between = ss_between / df_between as f64;
279 let ms_within = ss_within / df_within as f64;
280
281 let f_statistic = if ms_within > 1e-300 {
282 ms_between / ms_within
283 } else {
284 f64::INFINITY
285 };
286
287 let p_value = if f_statistic.is_infinite() {
288 0.0
289 } else {
290 special::f_distribution_sf(f_statistic, df_between as f64, df_within as f64)
291 };
292
293 Some(AnovaResult {
294 f_statistic,
295 df_between,
296 df_within,
297 p_value,
298 ss_between,
299 ss_within,
300 ms_between,
301 ms_within,
302 group_means,
303 grand_mean,
304 })
305}
306
307pub fn chi_squared_goodness_of_fit(observed: &[f64], expected: &[f64]) -> Option<TestResult> {
333 let k = observed.len();
334 if k < 2 || k != expected.len() {
335 return None;
336 }
337
338 for &e in expected {
339 if e <= 0.0 || !e.is_finite() {
340 return None;
341 }
342 }
343 for &o in observed {
344 if o < 0.0 || !o.is_finite() {
345 return None;
346 }
347 }
348
349 let chi2: f64 = observed
350 .iter()
351 .zip(expected.iter())
352 .map(|(&o, &e)| (o - e).powi(2) / e)
353 .sum();
354
355 let df = (k - 1) as f64;
356 let p_value = special::chi_squared_sf(chi2, df);
357
358 Some(TestResult {
359 statistic: chi2,
360 df,
361 p_value,
362 })
363}
364
365pub fn chi_squared_independence(table: &[f64], n_rows: usize, n_cols: usize) -> Option<TestResult> {
394 if n_rows < 2 || n_cols < 2 || table.len() != n_rows * n_cols {
395 return None;
396 }
397
398 for &v in table {
399 if v < 0.0 || !v.is_finite() {
400 return None;
401 }
402 }
403
404 let mut row_sums = vec![0.0; n_rows];
406 let mut col_sums = vec![0.0; n_cols];
407 let mut total = 0.0;
408
409 for i in 0..n_rows {
410 for j in 0..n_cols {
411 let val = table[i * n_cols + j];
412 row_sums[i] += val;
413 col_sums[j] += val;
414 total += val;
415 }
416 }
417
418 if total <= 0.0 {
419 return None;
420 }
421
422 for &r in &row_sums {
424 if r <= 0.0 {
425 return None;
426 }
427 }
428 for &c in &col_sums {
429 if c <= 0.0 {
430 return None;
431 }
432 }
433
434 let mut chi2 = 0.0;
436 for i in 0..n_rows {
437 for j in 0..n_cols {
438 let observed = table[i * n_cols + j];
439 let expected = row_sums[i] * col_sums[j] / total;
440 chi2 += (observed - expected).powi(2) / expected;
441 }
442 }
443
444 let df = ((n_rows - 1) * (n_cols - 1)) as f64;
445 let p_value = special::chi_squared_sf(chi2, df);
446
447 Some(TestResult {
448 statistic: chi2,
449 df,
450 p_value,
451 })
452}
453
454pub fn jarque_bera_test(data: &[f64]) -> Option<TestResult> {
499 let n = data.len();
500 if n < 8 {
501 return None;
502 }
503 if data.iter().any(|v| !v.is_finite()) {
504 return None;
505 }
506
507 let g1 = stats::skewness_moment(data)?;
508 let g2 = stats::kurtosis_moment(data)?;
509
510 let nf = n as f64;
511 let jb = (nf / 6.0) * (g1 * g1 + g2 * g2 / 4.0);
512 let p_value = (-jb / 2.0).exp();
514
515 Some(TestResult {
516 statistic: jb,
517 df: 2.0,
518 p_value,
519 })
520}
521
522#[derive(Debug, Clone, Copy)]
524pub struct AndersonDarlingResult {
525 pub statistic: f64,
527 pub statistic_star: f64,
529 pub p_value: f64,
531}
532
533fn ad_upper_tail_pvalue(a2_star: f64) -> f64 {
553 const UPPER_BRANCH_VERTEX: f64 = 153.467_741_935_483_87; let p = if a2_star >= 0.6 {
558 let x = a2_star.min(UPPER_BRANCH_VERTEX);
559 (1.2937 - 5.709 * x + 0.0186 * x * x).exp()
560 } else if a2_star > 0.34 {
561 (0.9177 - 4.279 * a2_star - 1.38 * a2_star * a2_star).exp()
562 } else if a2_star > 0.2 {
563 1.0 - (-8.318 + 42.796 * a2_star - 59.938 * a2_star * a2_star).exp()
564 } else {
565 1.0 - (-13.436 + 101.14 * a2_star - 223.73 * a2_star * a2_star).exp()
566 };
567 p.clamp(0.0, 1.0)
568}
569
570pub fn anderson_darling_test(data: &[f64]) -> Option<AndersonDarlingResult> {
605 let n = data.len();
606 if n < 8 {
607 return None;
608 }
609 if data.iter().any(|v| !v.is_finite()) {
610 return None;
611 }
612
613 let mean = stats::mean(data)?;
614 let sd = stats::std_dev(data)?;
615
616 if sd < 1e-300 {
617 return None; }
619
620 let mut x: Vec<f64> = data.to_vec();
622 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
623
624 let nf = n as f64;
625
626 let mut s = 0.0;
628 for i in 0..n {
629 let z = (x[i] - mean) / sd;
630 let z_rev = (x[n - 1 - i] - mean) / sd;
631 let ln_phi = special::standard_normal_cdf(z).max(f64::MIN_POSITIVE).ln();
635 let ln_sf_rev = special::standard_normal_sf(z_rev)
636 .max(f64::MIN_POSITIVE)
637 .ln();
638
639 let coeff = (2 * (i + 1) - 1) as f64;
640 s += coeff * (ln_phi + ln_sf_rev);
641 }
642
643 let a2 = -nf - s / nf;
644
645 let a2_star = a2 * (1.0 + 0.75 / nf + 2.25 / (nf * nf));
647
648 let p = ad_upper_tail_pvalue(a2_star);
651
652 Some(AndersonDarlingResult {
653 statistic: a2,
654 statistic_star: a2_star,
655 p_value: p,
656 })
657}
658
659#[derive(Debug, Clone, Copy)]
661pub struct ShapiroWilkResult {
662 pub w: f64,
664 pub p_value: f64,
666}
667
668pub fn shapiro_wilk_test(data: &[f64]) -> Option<ShapiroWilkResult> {
708 let n = data.len();
709 if !(3..=5000).contains(&n) {
710 return None;
711 }
712 if data.iter().any(|v| !v.is_finite()) {
713 return None;
714 }
715
716 let mut x: Vec<f64> = data.to_vec();
718 x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
719
720 if x[n - 1] - x[0] < 1e-300 {
721 return None; }
723
724 let nn2 = n / 2;
725
726 if n == 3 {
728 return shapiro_wilk_n3(&x);
729 }
730
731 let a = sw_coefficients(n, nn2)?;
733
734 let w = sw_statistic(&x, &a, n, nn2);
736
737 if !(0.0..=1.0 + 1e-10).contains(&w) {
738 return None;
739 }
740 let w = w.min(1.0);
741
742 let p_value = sw_p_value(w, n);
744
745 Some(ShapiroWilkResult {
746 w,
747 p_value: p_value.clamp(0.0, 1.0),
748 })
749}
750
751fn shapiro_wilk_n3(x: &[f64]) -> Option<ShapiroWilkResult> {
753 let a1 = std::f64::consts::FRAC_1_SQRT_2; let mean = (x[0] + x[1] + x[2]) / 3.0;
756 let ss = x.iter().map(|&v| (v - mean).powi(2)).sum::<f64>();
757 if ss < 1e-300 {
758 return None;
759 }
760
761 let numerator = a1 * (x[2] - x[0]);
762 let w = (numerator * numerator) / ss;
763 let w = w.clamp(0.75, 1.0);
764
765 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
767 let p = p.clamp(0.0, 1.0);
768
769 Some(ShapiroWilkResult { w, p_value: p })
770}
771
772const SW_C1: [f64; 6] = [0.0, 0.221157, -0.147981, -2.07119, 4.434685, -2.706056];
774const SW_C2: [f64; 6] = [0.0, 0.042981, -0.293762, -1.752461, 5.682633, -3.582633];
775const SW_C3: [f64; 4] = [0.544, -0.39978, 0.025054, -6.714e-4];
776const SW_C4: [f64; 4] = [1.3822, -0.77857, 0.062767, -0.0020322];
777const SW_C5: [f64; 4] = [-1.5861, -0.31082, -0.083751, 0.0038915];
778const SW_C6: [f64; 3] = [-0.4803, -0.082676, 0.0030302];
779const SW_G: [f64; 2] = [-2.273, 0.459];
780
781fn sw_poly(c: &[f64], x: f64) -> f64 {
783 let mut result = c[c.len() - 1];
784 for i in (0..c.len() - 1).rev() {
785 result = result * x + c[i];
786 }
787 result
788}
789
790fn sw_coefficients(n: usize, nn2: usize) -> Option<Vec<f64>> {
792 let mut a = vec![0.0; nn2];
793
794 let mut m = vec![0.0; nn2];
796 let mut summ2 = 0.0;
797 for (i, mi) in m.iter_mut().enumerate() {
798 let p = (i as f64 + 1.0 - 0.375) / (n as f64 + 0.25);
800 *mi = special::inverse_normal_cdf(p);
801 summ2 += *mi * *mi;
802 }
803 summ2 *= 2.0;
804 let ssumm2 = summ2.sqrt();
805 let rsn = 1.0 / (n as f64).sqrt();
806
807 let a1 = sw_poly(&SW_C1, rsn) - m[0] / ssumm2;
809
810 if n <= 5 {
811 let fac_sq = summ2 - 2.0 * m[0] * m[0];
813 let one_minus = 1.0 - 2.0 * a1 * a1;
814 if fac_sq <= 0.0 || one_minus <= 0.0 {
815 return None;
816 }
817 let fac = (fac_sq / one_minus).sqrt();
818 a[0] = a1;
819 for i in 1..nn2 {
820 a[i] = -m[i] / fac;
821 }
822 } else {
823 let a2 = -m[1] / ssumm2 + sw_poly(&SW_C2, rsn);
825 let fac_sq = summ2 - 2.0 * m[0] * m[0] - 2.0 * m[1] * m[1];
826 let one_minus = 1.0 - 2.0 * a1 * a1 - 2.0 * a2 * a2;
827 if fac_sq <= 0.0 || one_minus <= 0.0 {
828 return None;
829 }
830 let fac = (fac_sq / one_minus).sqrt();
831 a[0] = a1;
832 a[1] = a2;
833 for i in 2..nn2 {
834 a[i] = -m[i] / fac;
835 }
836 }
837
838 Some(a)
839}
840
841fn sw_statistic(x: &[f64], a: &[f64], n: usize, nn2: usize) -> f64 {
843 let mut sa = 0.0;
845 for i in 0..nn2 {
846 sa += a[i] * (x[n - 1 - i] - x[i]);
847 }
848
849 let mean = x.iter().sum::<f64>() / n as f64;
851 let ss: f64 = x.iter().map(|&v| (v - mean).powi(2)).sum();
852
853 if ss < 1e-300 {
854 return 1.0; }
856
857 (sa * sa) / ss
858}
859
860fn sw_p_value(w: f64, n: usize) -> f64 {
862 let nf = n as f64;
863
864 if n == 3 {
865 let p = 1.0 - (6.0 / std::f64::consts::PI) * w.sqrt().acos();
867 return p.clamp(0.0, 1.0);
868 }
869
870 let w1 = 1.0 - w;
871 if w1 <= 0.0 {
872 return 1.0; }
874
875 let y = w1.ln();
876
877 if n <= 11 {
878 let gamma = sw_poly(&SW_G, nf);
880 if y >= gamma {
881 return 0.0; }
883 let y2 = -(gamma - y).ln();
884 let m = sw_poly(&SW_C3, nf);
885 let s = sw_poly(&SW_C4, nf).exp();
886 if s < 1e-300 {
887 return 0.0;
888 }
889 let z = (y2 - m) / s;
890 special::standard_normal_sf(z)
891 } else {
892 let xx = nf.ln();
894 let m = sw_poly(&SW_C5, xx);
895 let s = sw_poly(&SW_C6, xx).exp();
896 if s < 1e-300 {
897 return 0.0;
898 }
899 let z = (y - m) / s;
900 special::standard_normal_sf(z)
901 }
902}
903
904pub fn mann_whitney_u_test(a: &[f64], b: &[f64]) -> Option<TestResult> {
948 let n1 = a.len();
949 let n2 = b.len();
950 if n1 < 2 || n2 < 2 {
951 return None;
952 }
953 if a.iter().any(|v| !v.is_finite()) || b.iter().any(|v| !v.is_finite()) {
954 return None;
955 }
956
957 let n = n1 + n2;
958 let n1f = n1 as f64;
959 let n2f = n2 as f64;
960 let nf = n as f64;
961
962 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(n);
964 for &v in a {
965 combined.push((v, 0)); }
967 for &v in b {
968 combined.push((v, 1)); }
970 combined.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
971
972 let ranks = average_ranks(&combined);
974
975 let r1: f64 = combined
977 .iter()
978 .zip(ranks.iter())
979 .filter(|((_, g), _)| *g == 0)
980 .map(|(_, &r)| r)
981 .sum();
982
983 let u1 = r1 - n1f * (n1f + 1.0) / 2.0;
985
986 let tie_correction = compute_tie_correction(&combined);
988
989 let mu = n1f * n2f / 2.0;
991 let sigma_sq = n1f * n2f / 12.0 * (nf + 1.0 - tie_correction / (nf * (nf - 1.0)));
992
993 if sigma_sq <= 0.0 {
994 return None;
995 }
996
997 let p_value = if tie_correction == 0.0 && n1 <= EXACT_MAX_N && n2 <= EXACT_MAX_N {
998 let u = u1.round() as usize;
1001 let tail = u.min(n1 * n2 - u);
1002 (2.0 * mann_whitney_exact_cdf(n1, n2, tail)).min(1.0)
1003 } else {
1004 continuity_corrected_p(u1, mu, sigma_sq)
1005 };
1006
1007 Some(TestResult {
1008 statistic: u1,
1009 df: 0.0, p_value,
1011 })
1012}
1013
1014const EXACT_MAX_N: usize = 50;
1018
1019fn continuity_corrected_p(statistic: f64, mu: f64, sigma_sq: f64) -> f64 {
1022 let z = ((statistic - mu).abs() - 0.5).max(0.0) / sigma_sq.sqrt();
1023 (2.0 * special::standard_normal_sf(z)).min(1.0)
1024}
1025
1026fn mann_whitney_exact_cdf(m: usize, n: usize, u: usize) -> f64 {
1035 let max_u = m * n;
1036 let mut prev: Vec<Vec<f64>> = (0..=n).map(|_| vec![1.0]).collect(); for i in 1..=m {
1039 let mut cur: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
1040 cur.push(vec![1.0]); for j in 1..=n {
1042 let mut dist = vec![0.0; i * j + 1];
1043 let wi = i as f64 / (i + j) as f64;
1044 let wj = j as f64 / (i + j) as f64;
1045 for (k, &pk) in prev[j].iter().enumerate() {
1047 dist[k + j] += wi * pk;
1048 }
1049 for (k, &pk) in cur[j - 1].iter().enumerate() {
1051 dist[k] += wj * pk;
1052 }
1053 cur.push(dist);
1054 }
1055 prev = cur;
1056 }
1057 prev[n].iter().take(u.min(max_u) + 1).sum::<f64>().min(1.0)
1058}
1059
1060fn wilcoxon_exact_cdf(n: usize, t: usize) -> f64 {
1063 let max_t = n * (n + 1) / 2;
1064 let mut dist = vec![0.0; max_t + 1];
1065 dist[0] = 1.0;
1066 let mut reach = 0;
1067 for rank in 1..=n {
1068 reach += rank;
1069 for s in (rank..=reach).rev() {
1070 dist[s] = 0.5 * dist[s] + 0.5 * dist[s - rank];
1071 }
1072 for d in dist.iter_mut().take(rank) {
1073 *d *= 0.5;
1074 }
1075 }
1076 dist.iter().take(t.min(max_t) + 1).sum::<f64>().min(1.0)
1077}
1078
1079pub fn wilcoxon_signed_rank_test(x: &[f64], y: &[f64]) -> Option<TestResult> {
1119 if x.len() != y.len() || x.len() < 2 {
1120 return None;
1121 }
1122 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
1123 return None;
1124 }
1125
1126 let diffs: Vec<f64> = x
1128 .iter()
1129 .zip(y.iter())
1130 .map(|(&a, &b)| a - b)
1131 .filter(|&d| d.abs() > 1e-300)
1132 .collect();
1133
1134 let nr = diffs.len();
1135 if nr < 2 {
1136 return None;
1137 }
1138
1139 let nf = nr as f64;
1140
1141 let mut abs_diffs: Vec<(f64, usize)> = diffs
1143 .iter()
1144 .enumerate()
1145 .map(|(i, &d)| (d.abs(), i))
1146 .collect();
1147 abs_diffs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1148
1149 let ranks = average_ranks(&abs_diffs);
1151
1152 let t_plus: f64 = abs_diffs
1154 .iter()
1155 .zip(ranks.iter())
1156 .filter(|((_, orig_idx), _)| diffs[*orig_idx] > 0.0)
1157 .map(|(_, &r)| r)
1158 .sum();
1159
1160 let tie_correction_val = compute_tie_correction(&abs_diffs);
1162
1163 let mu = nf * (nf + 1.0) / 4.0;
1165 let sigma_sq = nf * (nf + 1.0) * (2.0 * nf + 1.0) / 24.0 - tie_correction_val / 48.0;
1166
1167 if sigma_sq <= 0.0 {
1168 return None;
1169 }
1170
1171 let p_value = if tie_correction_val == 0.0 && nr <= EXACT_MAX_N {
1172 let t = t_plus.round() as usize;
1175 let tail = t.min(nr * (nr + 1) / 2 - t);
1176 (2.0 * wilcoxon_exact_cdf(nr, tail)).min(1.0)
1177 } else {
1178 continuity_corrected_p(t_plus, mu, sigma_sq)
1179 };
1180
1181 Some(TestResult {
1182 statistic: t_plus,
1183 df: 0.0,
1184 p_value,
1185 })
1186}
1187
1188fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1191 let n = sorted.len();
1192 let mut ranks = vec![0.0; n];
1193 let mut i = 0;
1194 while i < n {
1195 let mut j = i + 1;
1196 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1197 j += 1;
1198 }
1199 let avg_rank = (i + 1 + j) as f64 / 2.0;
1201 for rank in ranks.iter_mut().take(j).skip(i) {
1202 *rank = avg_rank;
1203 }
1204 i = j;
1205 }
1206 ranks
1207}
1208
1209fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1211 let n = sorted.len();
1212 let mut correction = 0.0;
1213 let mut i = 0;
1214 while i < n {
1215 let mut j = i + 1;
1216 while j < n && (sorted[j].0 - sorted[i].0).abs() < 1e-12 {
1217 j += 1;
1218 }
1219 let t = (j - i) as f64;
1220 if t > 1.0 {
1221 correction += t * (t * t - 1.0);
1222 }
1223 i = j;
1224 }
1225 correction
1226}
1227
1228pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1260 let k = groups.len();
1261 if k < 2 {
1262 return None;
1263 }
1264 for g in groups {
1265 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1266 return None;
1267 }
1268 }
1269
1270 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1271 let nf = total_n as f64;
1272
1273 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1275 for (gi, g) in groups.iter().enumerate() {
1276 for &v in *g {
1277 combined.push((v, gi));
1278 }
1279 }
1280 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1281
1282 let ranks = average_ranks(&combined);
1283
1284 let mut rank_sums = vec![0.0; k];
1286 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1287 rank_sums[*gi] += r;
1288 }
1289
1290 let mean_rank = (nf + 1.0) / 2.0;
1292 let mut h = 0.0;
1293 for (gi, g) in groups.iter().enumerate() {
1294 let ni = g.len() as f64;
1295 let mean_rank_i = rank_sums[gi] / ni;
1296 h += ni * (mean_rank_i - mean_rank).powi(2);
1297 }
1298 h *= 12.0 / (nf * (nf + 1.0));
1299
1300 let tie_corr = compute_tie_correction(&combined);
1302 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1303 if denom > 1e-15 {
1304 h /= denom;
1305 }
1306
1307 let df = (k - 1) as f64;
1308 let p_value = special::chi_squared_sf(h, df);
1309
1310 Some(TestResult {
1311 statistic: h,
1312 df,
1313 p_value,
1314 })
1315}
1316
1317pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1353 let k = groups.len();
1354 if k < 2 {
1355 return None;
1356 }
1357 for g in groups {
1358 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1359 return None;
1360 }
1361 }
1362
1363 let z_groups: Vec<Vec<f64>> = groups
1365 .iter()
1366 .map(|g| {
1367 let median = stats::median(g).unwrap_or(0.0);
1368 g.iter().map(|&x| (x - median).abs()).collect()
1369 })
1370 .collect();
1371
1372 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1374 let anova = one_way_anova(&z_refs)?;
1375
1376 Some(TestResult {
1377 statistic: anova.f_statistic,
1378 df: anova.df_between as f64,
1379 p_value: anova.p_value,
1380 })
1381}
1382
1383pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1396 if p_values.is_empty() || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1397 return None;
1398 }
1399 let m = p_values.len() as f64;
1400 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1401}
1402
1403pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1423 let m = p_values.len();
1424 if m == 0 || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1425 return None;
1426 }
1427
1428 let mut indices: Vec<usize> = (0..m).collect();
1430 indices.sort_by(|&a, &b| {
1431 p_values[a]
1432 .partial_cmp(&p_values[b])
1433 .unwrap_or(std::cmp::Ordering::Equal)
1434 });
1435
1436 let mf = m as f64;
1437 let mut adjusted = vec![0.0; m];
1438
1439 let mut cummin = f64::INFINITY;
1441 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1442 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1444 cummin = cummin.min(adj);
1445 adjusted[orig_idx] = cummin;
1446 }
1447
1448 Some(adjusted)
1449}
1450
1451pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1488 let k = groups.len();
1489 if k < 2 {
1490 return None;
1491 }
1492
1493 let mut sizes = Vec::with_capacity(k);
1494 let mut vars = Vec::with_capacity(k);
1495 let mut n_total: usize = 0;
1496
1497 for g in groups {
1498 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1499 return None;
1500 }
1501 let n = g.len();
1502 let v = stats::variance(g)?;
1503 if v <= 0.0 {
1504 return None; }
1506 sizes.push(n);
1507 vars.push(v);
1508 n_total += n;
1509 }
1510
1511 let nk = n_total - k; if nk == 0 {
1513 return None;
1514 }
1515 let nk_f = nk as f64;
1516
1517 let s2_pooled: f64 = sizes
1519 .iter()
1520 .zip(vars.iter())
1521 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1522 .sum::<f64>()
1523 / nk_f;
1524
1525 if s2_pooled <= 0.0 {
1526 return None;
1527 }
1528
1529 let num = nk_f * s2_pooled.ln()
1531 - sizes
1532 .iter()
1533 .zip(vars.iter())
1534 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1535 .sum::<f64>();
1536
1537 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1539 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1540
1541 let statistic = num / c;
1542 let df = (k - 1) as f64;
1543 let p_value = special::chi_squared_sf(statistic, df);
1544
1545 Some(TestResult {
1546 statistic,
1547 df,
1548 p_value,
1549 })
1550}
1551
1552pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1597 let row1 = a + b;
1598 let row2 = c + d;
1599 let col1 = a + c;
1600 let col2 = b + d;
1601 let n = a + b + c + d;
1602
1603 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1605 return None;
1606 }
1607
1608 let log_prob = |a_i: u64| -> f64 {
1610 let b_i = row1 - a_i;
1611 let c_i = col1 - a_i;
1612 let d_i = row2 - c_i;
1613 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1614 - ln_factorial(a_i)
1615 - ln_factorial(b_i)
1616 - ln_factorial(c_i)
1617 - ln_factorial(d_i)
1618 - ln_factorial(n)
1619 };
1620
1621 let a_min = col1.saturating_sub(row2);
1623 let a_max = row1.min(col1);
1624
1625 let log_p_obs = log_prob(a);
1626
1627 let mut p_value = 0.0;
1629 for a_i in a_min..=a_max {
1630 let lp = log_prob(a_i);
1631 if lp <= log_p_obs + 1e-10 {
1633 p_value += lp.exp();
1634 }
1635 }
1636
1637 let p_value = p_value.min(1.0);
1639
1640 let odds_ratio = if b > 0 && c > 0 {
1642 (a as f64 * d as f64) / (b as f64 * c as f64)
1643 } else {
1644 f64::INFINITY
1645 };
1646
1647 Some(TestResult {
1648 statistic: odds_ratio,
1649 df: 1.0,
1650 p_value,
1651 })
1652}
1653
1654#[derive(Debug, Clone, Copy)]
1660pub struct MannKendallResult {
1661 pub s_statistic: i64,
1663 pub variance: f64,
1665 pub z_statistic: f64,
1667 pub p_value: f64,
1669 pub kendall_tau: f64,
1671 pub sen_slope: f64,
1673}
1674
1675pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1713 let n = data.len();
1714 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1715 return None;
1716 }
1717
1718 let mut s: i64 = 0;
1720 for i in 0..n - 1 {
1721 for j in (i + 1)..n {
1722 let diff = data[j] - data[i];
1723 if diff > 0.0 {
1724 s += 1;
1725 } else if diff < 0.0 {
1726 s -= 1;
1727 }
1728 }
1729 }
1730
1731 let mut sorted: Vec<f64> = data.to_vec();
1733 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1734
1735 let mut tie_correction: f64 = 0.0;
1736 let mut current_count: usize = 1;
1737 for i in 1..sorted.len() {
1738 if (sorted[i] - sorted[i - 1]).abs() < 1e-10 {
1739 current_count += 1;
1740 } else {
1741 if current_count > 1 {
1742 let t = current_count as f64;
1743 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1744 }
1745 current_count = 1;
1746 }
1747 }
1748 if current_count > 1 {
1749 let t = current_count as f64;
1750 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1751 }
1752
1753 let nf = n as f64;
1755 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1756
1757 if variance < 1e-300 {
1758 return None; }
1760
1761 let sigma = variance.sqrt();
1763 let z_statistic = if s > 0 {
1764 (s as f64 - 1.0) / sigma
1765 } else if s < 0 {
1766 (s as f64 + 1.0) / sigma
1767 } else {
1768 0.0
1769 };
1770
1771 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1773 let p_value = p_value.clamp(0.0, 1.0);
1774
1775 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1777
1778 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1780 for i in 0..n - 1 {
1781 for j in (i + 1)..n {
1782 let dx = (j - i) as f64;
1783 slopes.push((data[j] - data[i]) / dx);
1784 }
1785 }
1786 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1787 let m = slopes.len();
1788 let sen_slope = if m % 2 == 0 {
1789 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1790 } else {
1791 slopes[m / 2]
1792 };
1793
1794 Some(MannKendallResult {
1795 s_statistic: s,
1796 variance,
1797 z_statistic,
1798 p_value,
1799 kendall_tau,
1800 sen_slope,
1801 })
1802}
1803
1804fn ln_factorial(n: u64) -> f64 {
1806 if n <= 1 {
1807 return 0.0;
1808 }
1809 special::ln_gamma(n as f64 + 1.0)
1811}
1812
1813#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1819pub enum KpssModel {
1820 Level,
1822 Trend,
1824}
1825
1826#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1828pub enum KpssLags {
1829 Hobijn,
1832 Schwert,
1834 Fixed(usize),
1836}
1837
1838#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1840pub enum KpssPValue {
1841 Interpolated,
1843 AtMost,
1845 AtLeast,
1847}
1848
1849#[derive(Debug, Clone)]
1851pub struct KpssResult {
1852 pub statistic: f64,
1854 pub n_lags: usize,
1856 pub p_value: f64,
1859 pub p_value_bound: KpssPValue,
1861 pub critical_values: [f64; 4],
1863 pub rejected: [bool; 4],
1865}
1866
1867pub const KPSS_LEVELS: [f64; 4] = [0.10, 0.05, 0.025, 0.01];
1869
1870pub fn kpss_lags_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
1878 check_series(data, KPSS_MIN_OBSERVATIONS)?;
1879 Ok(0..=data.len() - 1)
1880}
1881
1882pub const KPSS_MIN_OBSERVATIONS: usize = 10;
1884
1885pub fn kpss_test(
1930 data: &[f64],
1931 model: KpssModel,
1932 lags: KpssLags,
1933) -> Result<KpssResult, TimeSeriesError> {
1934 let domain = kpss_lags_domain(data)?;
1935 let n = data.len();
1936 if let KpssLags::Fixed(l) = lags {
1937 check_option("lags", l, &domain)?;
1938 }
1939 let nf = n as f64;
1940
1941 let residuals: Vec<f64> = match model {
1942 KpssModel::Level => {
1943 let mean = data.iter().sum::<f64>() / nf;
1944 data.iter().map(|x| x - mean).collect()
1945 }
1946 KpssModel::Trend => {
1947 let t_mean = (nf + 1.0) / 2.0;
1949 let y_mean = data.iter().sum::<f64>() / nf;
1950 let (mut sty, mut stt) = (0.0, 0.0);
1951 for (i, y) in data.iter().enumerate() {
1952 let dt = (i + 1) as f64 - t_mean;
1953 sty += dt * (y - y_mean);
1954 stt += dt * dt;
1955 }
1956 let slope = sty / stt;
1957 data.iter()
1958 .enumerate()
1959 .map(|(i, y)| y - y_mean - slope * ((i + 1) as f64 - t_mean))
1960 .collect()
1961 }
1962 };
1963
1964 let sum_sq: f64 = residuals.iter().map(|e| e * e).sum();
1966 let size: f64 = data.iter().map(|x| x * x).sum();
1967 if sum_sq <= 1e-20 * size {
1968 return Err(TimeSeriesError::NoVariation);
1969 }
1970
1971 let autocovariance = |i: usize| -> f64 {
1972 residuals[i..]
1973 .iter()
1974 .zip(&residuals[..n - i])
1975 .map(|(a, b)| a * b)
1976 .sum()
1977 };
1978
1979 let n_lags = match lags {
1980 KpssLags::Fixed(l) => l,
1981 KpssLags::Schwert => ((12.0 * (nf / 100.0).powf(0.25)).ceil() as usize).min(n - 1),
1982 KpssLags::Hobijn => {
1983 let cov_lags = nf.powf(2.0 / 9.0) as usize;
1985 let mut s0 = sum_sq / nf;
1986 let mut s1 = 0.0;
1987 for i in 1..=cov_lags.min(n - 1) {
1988 let g = autocovariance(i) / (nf / 2.0);
1989 s0 += g;
1990 s1 += i as f64 * g;
1991 }
1992 let s_hat = s1 / s0;
1993 let gamma = 1.1447 * (s_hat * s_hat).powf(1.0 / 3.0);
1994 ((gamma * nf.powf(1.0 / 3.0)) as usize).min(n - 1)
1995 }
1996 };
1997
1998 let mut long_run = sum_sq;
1999 for i in 1..=n_lags {
2000 long_run += 2.0 * autocovariance(i) * (1.0 - i as f64 / (n_lags as f64 + 1.0));
2001 }
2002 let long_run = long_run / nf;
2003 if long_run <= 0.0 {
2004 return Err(TimeSeriesError::NoVariation);
2005 }
2006
2007 let mut partial = 0.0;
2008 let eta = residuals
2009 .iter()
2010 .map(|e| {
2011 partial += e;
2012 partial * partial
2013 })
2014 .sum::<f64>()
2015 / (nf * nf);
2016 let statistic = eta / long_run;
2017
2018 let critical_values = match model {
2019 KpssModel::Level => [0.347, 0.463, 0.574, 0.739],
2020 KpssModel::Trend => [0.119, 0.146, 0.176, 0.216],
2021 };
2022 let (p_value, p_value_bound) = if statistic <= critical_values[0] {
2023 (KPSS_LEVELS[0], KpssPValue::AtLeast)
2024 } else if statistic >= critical_values[3] {
2025 (KPSS_LEVELS[3], KpssPValue::AtMost)
2026 } else {
2027 let k = critical_values
2028 .windows(2)
2029 .position(|w| statistic <= w[1])
2030 .expect("the statistic lies inside the table");
2031 let (c0, c1) = (critical_values[k], critical_values[k + 1]);
2032 let (p0, p1) = (KPSS_LEVELS[k], KPSS_LEVELS[k + 1]);
2033 (
2034 p0 + (statistic - c0) / (c1 - c0) * (p1 - p0),
2035 KpssPValue::Interpolated,
2036 )
2037 };
2038 let rejected = critical_values.map(|c| statistic > c);
2039
2040 Ok(KpssResult {
2041 statistic,
2042 n_lags,
2043 p_value,
2044 p_value_bound,
2045 critical_values,
2046 rejected,
2047 })
2048}
2049
2050#[cfg(test)]
2053mod kpss_reference {
2054 use super::*;
2055
2056 const AR: [f64; 60] = [
2057 0.0, 0.299, -0.154, -0.953, -0.836, -1.326, -0.471, 1.152, -0.031, -0.633, 0.237, 0.452,
2058 0.286, -0.816, -0.355, 0.553, -1.123, -0.907, -2.264, -2.196, -2.72, -1.323, -1.796,
2059 -0.447, -0.022, -0.196, -2.595, -1.577, -0.68, -0.159, -1.594, -1.115, -1.425, -1.379,
2060 0.509, -0.604, -0.275, 0.774, -0.274, -0.222, 0.021, 0.073, -1.196, -0.402, 1.198, -1.068,
2061 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,
2062 0.02,
2063 ];
2064 const RW: [f64; 60] = [
2065 0.001, 0.3, 0.026, -0.865, -1.32, -2.312, -2.252, -0.912, -1.404, -2.024, -1.534, -1.177,
2066 -1.072, -2.002, -2.031, -1.336, -2.68, -3.138, -5.039, -6.329, -8.171, -8.406, -9.673,
2067 -9.402, -9.245, -9.432, -11.949, -12.488, -12.537, -12.424, -13.954, -14.432, -15.411,
2068 -16.22, -15.159, -15.967, -16.0, -15.116, -15.7, -15.812, -15.702, -15.638, -16.863,
2069 -16.787, -15.428, -16.975, -16.116, -15.997, -16.638, -14.638, -13.876, -15.075, -15.0,
2070 -14.423, -14.612, -13.929, -13.996, -13.329, -11.89, -12.566,
2071 ];
2072
2073 #[test]
2074 fn statistic_lag_and_p_value_reproduce_statsmodels() {
2075 use KpssLags::{Fixed, Hobijn, Schwert};
2076 use KpssModel::{Level, Trend};
2077 type Case = (&'static [f64; 60], KpssModel, KpssLags, f64, f64, usize);
2078 let cases: [Case; 12] = [
2079 (
2080 &AR,
2081 Level,
2082 Hobijn,
2083 0.5229270606681895,
2084 0.036502914263921285,
2085 3,
2086 ),
2087 (&AR, Level, Schwert, 0.288001639308506, 0.1, 11),
2088 (
2089 &AR,
2090 Level,
2091 Fixed(4),
2092 0.46189194657609306,
2093 0.05047760923444266,
2094 4,
2095 ),
2096 (&AR, Trend, Hobijn, 0.2274846895383343, 0.01, 3),
2097 (
2098 &AR,
2099 Trend,
2100 Schwert,
2101 0.14193842821368732,
2102 0.05752142923391235,
2103 11,
2104 ),
2105 (
2106 &AR,
2107 Trend,
2108 Fixed(4),
2109 0.20574827757371436,
2110 0.013844395909857114,
2111 4,
2112 ),
2113 (&RW, Level, Hobijn, 0.9286533322383663, 0.01, 5),
2114 (
2115 &RW,
2116 Level,
2117 Schwert,
2118 0.5112288964656923,
2119 0.0391376359311504,
2120 11,
2121 ),
2122 (&RW, Level, Fixed(4), 1.09862199299003, 0.01, 4),
2123 (&RW, Trend, Hobijn, 0.26866997231516837, 0.01, 4),
2124 (
2125 &RW,
2126 Trend,
2127 Schwert,
2128 0.14468829763941088,
2129 0.05242907844553539,
2130 11,
2131 ),
2132 (&RW, Trend, Fixed(4), 0.26866997231516837, 0.01, 4),
2133 ];
2134 for (data, model, lags, statistic, p, n_lags) in cases {
2135 let r = kpss_test(data, model, lags).unwrap();
2136 let what = format!("{model:?} {lags:?}");
2137 assert_eq!(r.n_lags, n_lags, "{what}");
2138 assert!(
2139 (r.statistic - statistic).abs() < 1e-10 * statistic,
2140 "{what}: {} vs {statistic}",
2141 r.statistic
2142 );
2143 assert!(
2144 (r.p_value - p).abs() < 1e-12,
2145 "{what}: p {} vs {p}",
2146 r.p_value
2147 );
2148 }
2149 }
2150
2151 #[test]
2152 fn bounds_rejections_and_refusals() {
2153 let r = kpss_test(&RW, KpssModel::Level, KpssLags::Fixed(4)).unwrap();
2154 assert_eq!(r.p_value_bound, KpssPValue::AtMost);
2155 assert_eq!(r.rejected, [true; 4]);
2156 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Schwert).unwrap();
2157 assert_eq!(r.p_value_bound, KpssPValue::AtLeast);
2158 assert_eq!(r.rejected, [false; 4]);
2159 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Hobijn).unwrap();
2160 assert_eq!(r.p_value_bound, KpssPValue::Interpolated);
2161 assert_eq!(r.rejected, [true, true, false, false]);
2162
2163 assert_eq!(
2164 kpss_test(&AR[..9], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2165 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
2166 );
2167 assert_eq!(
2168 kpss_test(&AR, KpssModel::Level, KpssLags::Fixed(60)).unwrap_err(),
2169 TimeSeriesError::OptionOutOfRange {
2170 option: "lags",
2171 got: 60.0,
2172 min: 0.0,
2173 max: 59.0
2174 }
2175 );
2176 assert_eq!(
2177 kpss_test(&[4.0; 20], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2178 TimeSeriesError::NoVariation
2179 );
2180 let line: Vec<f64> = (0..20).map(|i| 2.0 + 0.5 * i as f64).collect();
2181 assert_eq!(
2182 kpss_test(&line, KpssModel::Trend, KpssLags::Hobijn).unwrap_err(),
2183 TimeSeriesError::NoVariation
2184 );
2185 let mut bad = AR;
2186 bad[3] = f64::INFINITY;
2187 assert_eq!(
2188 kpss_test(&bad[..5], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2189 TimeSeriesError::ValueNotFinite { index: 3 }
2190 );
2191 }
2192
2193 #[test]
2195 fn unit_invariant() {
2196 let base = kpss_test(&AR, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2197 for f in [1e-9, 1e9] {
2198 let x: Vec<f64> = AR.iter().map(|v| v * f).collect();
2199 let r = kpss_test(&x, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2200 assert_eq!(r.n_lags, base.n_lags);
2201 assert!((r.statistic - base.statistic).abs() < 1e-9 * base.statistic);
2202 }
2203 }
2204}
2205
2206#[derive(Debug, Clone)]
2212pub struct AdfResult {
2213 pub statistic: f64,
2215 pub n_lags: usize,
2217 pub n_obs: usize,
2219 pub critical_values: [f64; 3],
2221 pub rejected: [bool; 3],
2223}
2224
2225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2227pub enum AdfLags {
2228 Aic {
2233 max: Option<usize>,
2235 },
2236 Fixed(usize),
2238}
2239
2240impl Default for AdfLags {
2241 fn default() -> Self {
2242 AdfLags::Aic { max: None }
2243 }
2244}
2245
2246#[derive(Debug, Clone, Copy)]
2248pub enum AdfModel {
2249 None,
2251 Constant,
2253 ConstantTrend,
2255}
2256
2257pub fn adf_test(
2297 data: &[f64],
2298 model: AdfModel,
2299 lags: AdfLags,
2300) -> Result<AdfResult, TimeSeriesError> {
2301 let domain = adf_lags_domain(data, model)?;
2302 let n = data.len();
2303 match lags {
2304 AdfLags::Fixed(p) => {
2305 check_option("lags", p, &domain)?;
2306 }
2307 AdfLags::Aic { max: Some(m) } => {
2308 check_option("max_lags", m, &domain)?;
2309 }
2310 AdfLags::Aic { max: None } => {}
2311 }
2312
2313 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
2315
2316 let best_lag = match lags {
2317 AdfLags::Fixed(p) => p,
2318 AdfLags::Aic { max } => {
2319 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
2321 let p_max = max.unwrap_or_else(|| schwert.min(n / 3));
2322 select_adf_lag(data, &dy, model, p_max).ok_or(TimeSeriesError::DegenerateRegression)?
2323 }
2324 };
2325
2326 adf_ols(data, &dy, model, best_lag).ok_or(TimeSeriesError::DegenerateRegression)
2328}
2329
2330pub const ADF_MIN_OBSERVATIONS: usize = 10;
2332
2333const ADF_MIN_REGRESSION_ROWS: usize = 5;
2335
2336pub fn adf_lags_domain(
2357 data: &[f64],
2358 model: AdfModel,
2359) -> Result<RangeInclusive<usize>, TimeSeriesError> {
2360 check_series(data, ADF_MIN_OBSERVATIONS)?;
2361 let n = data.len();
2362 let fixed_columns = match model {
2363 AdfModel::None => 1,
2364 AdfModel::Constant => 2,
2365 AdfModel::ConstantTrend => 3,
2366 };
2367 Ok(0..=(n - 1 - ADF_MIN_REGRESSION_ROWS).min((n - 2 - fixed_columns) / 2))
2368}
2369
2370fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> Option<usize> {
2375 let start = p_max;
2376 let mut best: Option<(f64, usize)> = None;
2377 for p in 0..=p_max {
2378 if let Some(aic) = adf_ols_aic(data, dy, model, p, start) {
2379 if best.is_none_or(|(b, _)| aic < b) {
2380 best = Some((aic, p));
2381 }
2382 }
2383 }
2384 best.map(|(_, p)| p)
2385}
2386
2387#[allow(clippy::type_complexity)]
2391fn adf_build_matrix(
2392 data: &[f64],
2393 dy: &[f64],
2394 model: AdfModel,
2395 p: usize,
2396 start: usize,
2397) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2398 debug_assert!(start >= p, "the first row needs its p lagged differences");
2400 if start >= dy.len() || dy.len() - start < 5 {
2401 return None;
2402 }
2403 let m = dy.len() - start;
2404
2405 let y_dep: Vec<f64> = dy[start..].to_vec();
2406
2407 let has_intercept = !matches!(model, AdfModel::None);
2409 let has_trend = matches!(model, AdfModel::ConstantTrend);
2410 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2411
2412 let mut x_data = Vec::with_capacity(m * ncols);
2414 let mut gamma_col = 0;
2415
2416 for i in 0..m {
2417 let t = start + i;
2418 if has_intercept {
2419 x_data.push(1.0); gamma_col = 1;
2421 }
2422 x_data.push(data[t]); if has_trend {
2424 x_data.push((t + 1) as f64); }
2426 for lag in 1..=p {
2427 x_data.push(dy[t - lag]);
2428 }
2429 }
2430
2431 Some((x_data, y_dep, m, ncols, gamma_col))
2432}
2433
2434fn adf_ols_core(
2438 x_data: &[f64],
2439 y: &[f64],
2440 m: usize,
2441 ncols: usize,
2442 gamma_col: usize,
2443) -> Option<(f64, f64, usize)> {
2444 let mut xtx = vec![0.0_f64; ncols * ncols];
2446 for i in 0..m {
2447 let row = &x_data[i * ncols..(i + 1) * ncols];
2448 for j in 0..ncols {
2449 for k in j..ncols {
2450 xtx[j * ncols + k] += row[j] * row[k];
2451 }
2452 }
2453 }
2454 for j in 0..ncols {
2456 for k in (j + 1)..ncols {
2457 xtx[k * ncols + j] = xtx[j * ncols + k];
2458 }
2459 }
2460
2461 let mut xty = vec![0.0_f64; ncols];
2463 for i in 0..m {
2464 let row = &x_data[i * ncols..(i + 1) * ncols];
2465 for j in 0..ncols {
2466 xty[j] += row[j] * y[i];
2467 }
2468 }
2469
2470 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2472 for i in 0..ncols {
2473 for j in 0..ncols {
2474 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2475 }
2476 augmented[i * (ncols + 1) + ncols] = xty[i];
2477 }
2478
2479 for col in 0..ncols {
2480 let mut max_row = col;
2482 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2483 for row in (col + 1)..ncols {
2484 let val = augmented[row * (ncols + 1) + col].abs();
2485 if val > max_val {
2486 max_val = val;
2487 max_row = row;
2488 }
2489 }
2490 if max_val < 1e-15 {
2491 return None; }
2493 if max_row != col {
2494 for j in 0..=ncols {
2495 let a = col * (ncols + 1) + j;
2496 let b = max_row * (ncols + 1) + j;
2497 augmented.swap(a, b);
2498 }
2499 }
2500
2501 let pivot = augmented[col * (ncols + 1) + col];
2502 for row in (col + 1)..ncols {
2503 let factor = augmented[row * (ncols + 1) + col] / pivot;
2504 for j in col..=ncols {
2505 let above = augmented[col * (ncols + 1) + j];
2506 augmented[row * (ncols + 1) + j] -= factor * above;
2507 }
2508 }
2509 }
2510
2511 let mut beta = vec![0.0_f64; ncols];
2513 for i in (0..ncols).rev() {
2514 let mut sum = augmented[i * (ncols + 1) + ncols];
2515 for j in (i + 1)..ncols {
2516 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2517 }
2518 beta[i] = sum / augmented[i * (ncols + 1) + i];
2519 }
2520
2521 let mut rss = 0.0;
2523 for i in 0..m {
2524 let row = &x_data[i * ncols..(i + 1) * ncols];
2525 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2526 let resid = y[i] - y_hat;
2527 rss += resid * resid;
2528 }
2529
2530 let yy: f64 = y.iter().map(|v| v * v).sum();
2536 if yy == 0.0 || rss <= yy * 1e-16 {
2537 return None;
2538 }
2539
2540 let df = m - ncols;
2542 if df == 0 {
2543 return None;
2544 }
2545 let mse = rss / df as f64;
2546
2547 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2550 for j in 0..ncols {
2551 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2552 }
2553 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2554 }
2555
2556 for col in 0..ncols {
2558 let mut max_row = col;
2559 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2560 for row in (col + 1)..ncols {
2561 let val = xtx_aug[row * 2 * ncols + col].abs();
2562 if val > max_val {
2563 max_val = val;
2564 max_row = row;
2565 }
2566 }
2567 if max_val < 1e-15 {
2568 return None;
2569 }
2570 if max_row != col {
2571 for j in 0..(2 * ncols) {
2572 let a = col * 2 * ncols + j;
2573 let b = max_row * 2 * ncols + j;
2574 xtx_aug.swap(a, b);
2575 }
2576 }
2577
2578 let pivot = xtx_aug[col * 2 * ncols + col];
2579 for j in 0..(2 * ncols) {
2580 xtx_aug[col * 2 * ncols + j] /= pivot;
2581 }
2582 for row in 0..ncols {
2583 if row == col {
2584 continue;
2585 }
2586 let factor = xtx_aug[row * 2 * ncols + col];
2587 for j in 0..(2 * ncols) {
2588 let above = xtx_aug[col * 2 * ncols + j];
2589 xtx_aug[row * 2 * ncols + j] -= factor * above;
2590 }
2591 }
2592 }
2593
2594 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2596 if var_gamma <= 0.0 {
2597 return None;
2598 }
2599 let se_gamma = var_gamma.sqrt();
2600 let t_gamma = beta[gamma_col] / se_gamma;
2601
2602 Some((t_gamma, rss, ncols))
2603}
2604
2605fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize, start: usize) -> Option<f64> {
2608 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, start)?;
2609 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2610 Some(2.0 * k as f64 + m as f64 * (rss / m as f64).ln())
2611}
2612
2613fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2615 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, p)?;
2616 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2617
2618 let critical_values = adf_critical_values(model, m);
2619
2620 let rejected = [
2621 gamma_t <= critical_values[0],
2622 gamma_t <= critical_values[1],
2623 gamma_t <= critical_values[2],
2624 ];
2625
2626 Some(AdfResult {
2627 statistic: gamma_t,
2628 n_lags: p,
2629 n_obs: m,
2630 critical_values,
2631 rejected,
2632 })
2633}
2634
2635fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2639 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2644 AdfModel::None => (
2645 [-2.5658, -1.9393, -1.6156],
2646 [-1.960, -0.398, -0.181],
2647 [-10.04, 0.0, 0.0],
2648 ),
2649 AdfModel::Constant => (
2650 [-3.4336, -2.8621, -2.5671],
2651 [-5.999, -2.738, -1.438],
2652 [-29.25, -8.36, -4.48],
2653 ),
2654 AdfModel::ConstantTrend => (
2655 [-3.9638, -3.4126, -3.1279],
2656 [-8.353, -4.039, -2.418],
2657 [-47.44, -17.83, -7.58],
2658 ),
2659 };
2660
2661 let nf = n as f64;
2662 let inv_n = 1.0 / nf;
2663 let inv_n2 = inv_n * inv_n;
2664
2665 [
2666 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2667 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2668 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2669 ]
2670}
2671
2672#[cfg(test)]
2673mod tests {
2674 use super::*;
2675
2676 #[test]
2677 fn corrections_refuse_what_is_not_a_probability() {
2678 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2679 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2680 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2681 }
2682
2683 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2689 let mut a = seed;
2690 move || {
2691 a = a.wrapping_add(0x6d2b_79f5);
2692 let mut t = a;
2693 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2694 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2695 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2696 }
2697 }
2698
2699 #[test]
2705 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2706 let mut rng = mulberry32(0x5350_4331);
2707 let full: Vec<f64> = (0..20_000)
2708 .map(|_| {
2709 let u = rng();
2710 -(1.0 - u).ln() })
2712 .collect();
2713
2714 let mut prev_a2 = 0.0_f64;
2715 let mut prev_p = f64::INFINITY;
2716 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2717 let r = anderson_darling_test(&full[..n]).expect("computes");
2718 assert!(
2720 r.statistic_star > prev_a2,
2721 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2722 r.statistic_star
2723 );
2724 assert!(
2726 r.p_value < 0.5,
2727 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2728 r.statistic_star,
2729 r.p_value
2730 );
2731 assert!(
2733 r.p_value <= prev_p + 1e-12,
2734 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2735 r.p_value
2736 );
2737 prev_a2 = r.statistic_star;
2738 prev_p = r.p_value;
2739 }
2740 }
2741
2742 #[test]
2747 fn one_sample_null_true() {
2748 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2749 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2750 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2751 }
2752
2753 #[test]
2754 fn one_sample_null_false() {
2755 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2756 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2757 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2758 }
2759
2760 #[test]
2761 fn one_sample_edge_cases() {
2762 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());
2765 }
2766
2767 #[test]
2772 fn two_sample_same_mean() {
2773 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2774 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2775 let r = two_sample_t_test(&a, &b).expect("should compute");
2776 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2777 }
2778
2779 #[test]
2780 fn two_sample_different_means() {
2781 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2782 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2783 let r = two_sample_t_test(&a, &b).expect("should compute");
2784 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2785 }
2786
2787 #[test]
2788 fn two_sample_different_sizes() {
2789 let a = [1.0, 2.0, 3.0];
2790 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2791 let r = two_sample_t_test(&a, &b).expect("should compute");
2792 assert!(r.p_value < 0.05);
2793 }
2794
2795 #[test]
2796 fn two_sample_edge_cases() {
2797 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2798 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2799 }
2800
2801 #[test]
2806 fn paired_no_difference() {
2807 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2808 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2809 let r = paired_t_test(&x, &y).expect("should compute");
2810 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2811 }
2812
2813 #[test]
2814 fn paired_significant_difference() {
2815 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2817 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2818 let r = paired_t_test(&before, &after).expect("should compute");
2819 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2820 assert!(r.statistic < 0.0); }
2822
2823 #[test]
2824 fn paired_edge_cases() {
2825 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2828
2829 #[test]
2834 fn anova_same_means() {
2835 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2836 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2837 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2838 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2839 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2840 }
2841
2842 #[test]
2843 fn anova_different_means() {
2844 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2845 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2846 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2847 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2848 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2849 assert!(r.df_between == 2);
2850 assert!(r.df_within == 12);
2851 }
2852
2853 #[test]
2854 fn anova_ss_decomposition() {
2855 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2856 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2857 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2858 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2860 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2861 assert!(
2862 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2863 "SS decomposition: {ss_total} vs {} + {}",
2864 r.ss_between,
2865 r.ss_within
2866 );
2867 }
2868
2869 #[test]
2870 fn anova_edge_cases() {
2871 let g1 = [1.0, 2.0, 3.0];
2872 assert!(one_way_anova(&[&g1]).is_none()); }
2874
2875 #[test]
2880 fn chi2_gof_uniform() {
2881 let observed = [25.0, 25.0, 25.0, 25.0];
2883 let expected = [25.0, 25.0, 25.0, 25.0];
2884 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2885 assert!((r.statistic).abs() < 1e-15);
2886 assert!((r.p_value - 1.0).abs() < 0.01);
2887 }
2888
2889 #[test]
2890 fn chi2_gof_significant() {
2891 let observed = [90.0, 10.0];
2892 let expected = [50.0, 50.0];
2893 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2894 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2895 }
2896
2897 #[test]
2898 fn chi2_gof_edge_cases() {
2899 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()); }
2903
2904 #[test]
2909 fn chi2_independence_significant() {
2910 let table = [30.0, 10.0, 10.0, 50.0];
2912 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2913 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2914 assert!((r.df - 1.0).abs() < 1e-10);
2915 }
2916
2917 #[test]
2918 fn chi2_independence_not_significant() {
2919 let table = [25.0, 25.0, 25.0, 25.0];
2921 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2922 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2923 }
2924
2925 #[test]
2926 fn chi2_independence_3x3() {
2927 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2928 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2929 assert!((r.df - 4.0).abs() < 1e-10);
2930 assert!(r.p_value < 0.05);
2931 }
2932
2933 #[test]
2934 fn chi2_independence_edge_cases() {
2935 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()); }
2939
2940 #[test]
2945 fn jb_normal_data() {
2946 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2948 let r = jarque_bera_test(&data).expect("should compute");
2949 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2950 }
2951
2952 #[test]
2953 fn jb_skewed_data() {
2954 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2956 let r = jarque_bera_test(&data).expect("should compute");
2957 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2958 }
2959
2960 #[test]
2967 fn jb_is_the_moment_form_the_paper_defines() {
2968 let data = [
2969 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,
2970 3.6, 2.8, 3.2,
2971 ];
2972 let r = jarque_bera_test(&data).expect("20 finite points");
2973 assert!(
2974 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2975 "JB = {}",
2976 r.statistic
2977 );
2978
2979 let n = data.len() as f64;
2982 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
2983 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
2984 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
2985 assert!(
2986 adjusted > r.statistic * 1.3,
2987 "the two forms differ by more than a third here: {adjusted} vs {}",
2988 r.statistic
2989 );
2990 }
2991
2992 #[test]
2996 fn jb_p_value_keeps_its_digits_in_the_tail() {
2997 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
3000 data.push(60.0);
3001 let r = jarque_bera_test(&data).expect("20 finite points");
3002 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
3003 assert!(
3004 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
3005 "this sample is only interesting because the complement underflows"
3006 );
3007 assert!(
3008 r.p_value > 0.0,
3009 "the complement returns 0 here; this must not"
3010 );
3011 assert!(
3012 (r.p_value - 6.702e-21).abs() < 1e-23,
3013 "p = {} (expected about 6.7e-21)",
3014 r.p_value
3015 );
3016 assert!(
3017 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
3018 "p = {} vs exp(-JB/2) = {}",
3019 r.p_value,
3020 (-r.statistic / 2.0).exp()
3021 );
3022 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])
3025 .expect("10 finite points");
3026 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
3027 assert!(
3028 (small.p_value - via_cdf).abs() < 1e-12,
3029 "{} vs {via_cdf}",
3030 small.p_value
3031 );
3032 }
3033
3034 #[test]
3035 fn jb_edge_cases() {
3036 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
3038
3039 #[test]
3044 fn ad_normal_data() {
3045 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3046 let r = anderson_darling_test(&data).expect("should compute");
3047 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3048 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
3049 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
3050 }
3051
3052 #[test]
3053 fn ad_skewed_data() {
3054 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];
3056 let r = anderson_darling_test(&data).expect("should compute");
3057 assert!(
3058 r.p_value < 0.05,
3059 "p = {} (should reject normality)",
3060 r.p_value
3061 );
3062 }
3063
3064 #[test]
3065 fn ad_bimodal_data() {
3066 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3068 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3069 let r = anderson_darling_test(&data).expect("should compute");
3070 assert!(
3071 r.p_value < 0.01,
3072 "p = {} (bimodal should reject normality)",
3073 r.p_value
3074 );
3075 }
3076
3077 #[test]
3078 fn ad_large_normal_sample() {
3079 let n = 100;
3080 let data: Vec<f64> = (1..=n)
3081 .map(|i| {
3082 let p = (i as f64 - 0.5) / n as f64;
3083 special::inverse_normal_cdf(p)
3084 })
3085 .collect();
3086 let r = anderson_darling_test(&data).expect("should compute");
3087 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
3088 }
3089
3090 #[test]
3091 fn ad_edge_cases() {
3092 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());
3095 }
3096
3097 #[test]
3098 fn ad_p_value_ranges() {
3099 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
3104 let r = anderson_darling_test(&near_normal).expect("should compute");
3105 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3106
3107 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];
3109 let r = anderson_darling_test(&heavy_tail).expect("should compute");
3110 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3111 }
3112
3113 #[test]
3118 fn ad_normal_data_large_p() {
3119 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3121 let r = anderson_darling_test(&data).unwrap();
3122 assert!(r.p_value > 0.05, "p={}", r.p_value);
3123 }
3124
3125 #[test]
3126 fn ad_exponential_data_small_p() {
3127 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3129 let r = anderson_darling_test(&data).unwrap();
3130 assert!(r.p_value < 0.05, "p={}", r.p_value);
3131 }
3132
3133 #[test]
3134 fn ad_statistic_non_negative() {
3135 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3136 let r = anderson_darling_test(&data).unwrap();
3137 assert!(r.statistic >= 0.0);
3138 assert!(r.statistic_star >= r.statistic);
3139 }
3140
3141 #[test]
3142 fn ad_p_value_in_range() {
3143 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
3144 let r = anderson_darling_test(&data).unwrap();
3145 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3146 }
3147
3148 #[test]
3149 fn ad_insufficient_data() {
3150 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
3152
3153 #[test]
3154 fn ad_degenerate_data() {
3155 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3157 }
3158
3159 #[test]
3160 fn ad_modified_statistic_formula() {
3161 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
3163 let r = anderson_darling_test(&data).unwrap();
3164 assert!(r.statistic_star > r.statistic - 1e-10);
3165 assert!(anderson_darling_test(&data[..7]).is_none());
3167 }
3168
3169 #[test]
3174 fn ad_correction_factor_formula() {
3175 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3176 let n = data.len() as f64;
3177 let r = anderson_darling_test(&data).unwrap();
3178
3179 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
3181 assert!(
3182 (r.statistic_star - expected_star).abs() < 1e-12,
3183 "A²* = {}, expected = {}",
3184 r.statistic_star,
3185 expected_star
3186 );
3187
3188 assert!(
3190 r.statistic >= 0.0,
3191 "A² = {} must be non-negative",
3192 r.statistic
3193 );
3194 }
3195
3196 #[test]
3202 fn ad_formula_structure_symmetric_vs_skewed() {
3203 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
3205 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
3207
3208 let r_sym = anderson_darling_test(&symmetric).unwrap();
3209 let r_skew = anderson_darling_test(&skewed).unwrap();
3210
3211 assert!(
3212 r_sym.statistic < r_skew.statistic,
3213 "Symmetric A²={} should be less than skewed A²={}",
3214 r_sym.statistic,
3215 r_skew.statistic
3216 );
3217 }
3218
3219 #[test]
3224 fn ad_pvalue_invariants() {
3225 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3227 let r_normal = anderson_darling_test(&normal_data).unwrap();
3228 assert!(
3229 r_normal.p_value > 0.05,
3230 "Normal data: p = {} (expected > 0.05)",
3231 r_normal.p_value
3232 );
3233
3234 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3236 let r_exp = anderson_darling_test(&exp_data).unwrap();
3237 assert!(
3238 r_exp.p_value < 0.05,
3239 "Exponential data: p = {} (expected < 0.05)",
3240 r_exp.p_value
3241 );
3242 }
3243
3244 #[test]
3249 fn sw_normal_data() {
3250 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3252 let r = shapiro_wilk_test(&data).expect("should compute");
3253 assert!(r.w > 0.9, "W = {}", r.w);
3254 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3255 }
3256
3257 #[test]
3258 fn sw_bimodal_data() {
3259 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3261 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3262 let r = shapiro_wilk_test(&data).expect("should compute");
3263 assert!(
3264 r.p_value < 0.01,
3265 "p = {} (bimodal should reject normality)",
3266 r.p_value
3267 );
3268 }
3269
3270 #[test]
3271 fn sw_n3() {
3272 let data = [1.0, 2.0, 3.0];
3273 let r = shapiro_wilk_test(&data).expect("n=3 should work");
3274 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3275 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3276 }
3277
3278 #[test]
3279 fn sw_n4() {
3280 let data = [1.0, 2.0, 3.0, 4.0];
3281 let r = shapiro_wilk_test(&data).expect("n=4 should work");
3282 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3283 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3284 }
3285
3286 #[test]
3287 fn sw_n5() {
3288 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
3289 let r = shapiro_wilk_test(&data).expect("n=5 should work");
3290 assert!(r.w > 0.9, "W = {}", r.w);
3291 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3292 }
3293
3294 #[test]
3295 fn sw_skewed_data() {
3296 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];
3298 let r = shapiro_wilk_test(&data).expect("should compute");
3299 assert!(
3300 r.p_value < 0.05,
3301 "p = {} (skewed data should reject normality)",
3302 r.p_value
3303 );
3304 }
3305
3306 #[test]
3307 fn sw_large_normal_sample() {
3308 let n = 100;
3311 let data: Vec<f64> = (1..=n)
3312 .map(|i| {
3313 let p = (i as f64 - 0.5) / n as f64;
3314 special::inverse_normal_cdf(p)
3315 })
3316 .collect();
3317 let r = shapiro_wilk_test(&data).expect("should compute");
3318 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
3319 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3320 }
3321
3322 #[test]
3323 fn sw_w_bounded() {
3324 let datasets: Vec<Vec<f64>> = vec![
3326 vec![1.0, 2.0, 3.0],
3327 vec![1.0, 1.0, 2.0, 3.0, 3.0],
3328 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
3329 (0..20).map(|i| (i as f64).powi(2)).collect(),
3330 ];
3331 for (idx, data) in datasets.iter().enumerate() {
3332 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
3333 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
3334 assert!(
3335 r.p_value >= 0.0 && r.p_value <= 1.0,
3336 "dataset {idx}: p = {}",
3337 r.p_value
3338 );
3339 }
3340 }
3341
3342 #[test]
3343 fn sw_edge_cases() {
3344 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()); }
3350
3351 #[test]
3352 fn sw_n5001_rejected() {
3353 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
3354 assert!(shapiro_wilk_test(&data).is_none()); }
3356
3357 #[test]
3362 fn mw_clearly_different() {
3363 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
3364 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
3365 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3366 assert!(r.p_value < 0.05, "p = {}", r.p_value);
3367 }
3368
3369 #[test]
3370 fn mw_same_distribution() {
3371 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3372 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3373 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3374 assert!(
3375 r.p_value > 0.3,
3376 "p = {} (interleaved, same dist)",
3377 r.p_value
3378 );
3379 }
3380
3381 #[test]
3382 fn mw_with_ties() {
3383 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3384 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3385 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3386 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3387 }
3388
3389 #[test]
3390 fn mw_different_sizes() {
3391 let a = [1.0, 2.0, 3.0];
3392 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3393 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3394 assert!(r.p_value < 0.05);
3395 }
3396
3397 #[test]
3398 fn mw_edge_cases() {
3399 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());
3402 }
3403
3404 #[test]
3409 fn wilcoxon_significant_increase() {
3410 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3411 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3412 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3413 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3414 }
3415
3416 #[test]
3417 fn wilcoxon_no_difference() {
3418 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3419 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3420 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3421 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3422 }
3423
3424 #[test]
3425 fn wilcoxon_with_ties() {
3426 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3428 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3430 assert!(r.statistic > 0.0);
3432 }
3433
3434 #[test]
3435 fn wilcoxon_with_zero_diffs() {
3436 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3438 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3440 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3441 }
3442
3443 #[test]
3444 fn wilcoxon_edge_cases() {
3445 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());
3449 }
3450
3451 #[test]
3456 fn mann_whitney_small_samples_use_the_exact_distribution() {
3457 let cases = [
3459 (3, 3, 0, 0.1000, 0.0495),
3460 (4, 4, 0, 0.0286, 0.0209),
3461 (4, 4, 1, 0.0571, 0.0433),
3462 (5, 5, 2, 0.0317, 0.0283),
3463 (8, 8, 13, 0.0499, 0.0460),
3464 (10, 10, 23, 0.0433, 0.0413),
3465 ];
3466 for (n1, n2, u, exact, old) in cases {
3467 let (a, b) = separated_samples(n1, n2, u);
3468 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3469 assert_eq!(r.statistic, u as f64);
3470 assert!(
3471 (r.p_value - exact).abs() < 1e-4,
3472 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3473 r.p_value
3474 );
3475 }
3476 }
3477
3478 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3482 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3483 let mut remaining = u;
3484 for k in (0..n1).rev() {
3485 let room = n2.min(remaining);
3486 ranks_a[k] += room;
3487 remaining -= room;
3488 if remaining == 0 {
3489 break;
3490 }
3491 }
3492 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3493 (
3494 ranks_a.iter().map(|&r| r as f64).collect(),
3495 ranks_b.iter().map(|&r| r as f64).collect(),
3496 )
3497 }
3498
3499 #[test]
3501 fn wilcoxon_small_samples_use_the_exact_distribution() {
3502 let cases = [
3504 (5, 0, 0.0625),
3505 (5, 1, 0.125),
3506 (6, 2, 0.09375),
3507 (8, 5, 0.078125),
3508 (10, 8, 0.048828125),
3509 (12, 14, 0.0522460938),
3510 ];
3511 for (n, t_plus, exact) in cases {
3512 let positive = ranks_summing_to(n, t_plus);
3514 let x: Vec<f64> = (1..=n)
3515 .map(|r| {
3516 if positive.contains(&r) {
3517 r as f64
3518 } else {
3519 -(r as f64)
3520 }
3521 })
3522 .collect();
3523 let y = vec![0.0; n];
3524 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3525 assert_eq!(r.statistic, t_plus as f64);
3526 assert!(
3527 (r.p_value - exact).abs() < 1e-9,
3528 "n={n} T+={t_plus}: p={} want {exact}",
3529 r.p_value
3530 );
3531 }
3532 }
3533
3534 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3536 let mut out = Vec::new();
3537 let mut left = target;
3538 for r in (1..=n).rev() {
3539 if r <= left {
3540 out.push(r);
3541 left -= r;
3542 }
3543 }
3544 assert_eq!(left, 0);
3545 out
3546 }
3547
3548 #[test]
3551 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3552 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3553 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3554 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3555 let (n1, n2) = (60.0, 60.0);
3556 let mu = n1 * n2 / 2.0;
3557 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3558 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3559 let want = 2.0 * special::standard_normal_sf(z);
3560 assert!((r.p_value - want).abs() < 1e-12);
3561
3562 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3564 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3565 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3566 let mu = 1250.0;
3567 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3568 let u = mann_whitney_u_test(&a, &b)
3569 .expect("valid samples")
3570 .statistic;
3571 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3572 assert!(
3573 (exact - approx).abs() < 1e-3,
3574 "exact {exact} vs approx {approx}"
3575 );
3576 }
3577
3578 #[test]
3583 fn kw_clearly_different() {
3584 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3585 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3586 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3587 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3588 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3589 assert!((r.df - 2.0).abs() < 1e-10);
3590 }
3591
3592 #[test]
3593 fn kw_same_distribution() {
3594 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3595 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3596 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3597 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3598 }
3599
3600 #[test]
3601 fn kw_with_ties() {
3602 let g1 = [1.0, 2.0, 2.0, 3.0];
3603 let g2 = [3.0, 4.0, 4.0, 5.0];
3604 let g3 = [5.0, 6.0, 6.0, 7.0];
3605 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3606 assert!(r.statistic > 0.0);
3607 }
3608
3609 #[test]
3610 fn kw_edge_cases() {
3611 let g1 = [1.0, 2.0, 3.0];
3612 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3614
3615 #[test]
3620 fn levene_equal_variance() {
3621 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3622 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3623 let r = levene_test(&[&g1, &g2]).expect("should compute");
3624 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3626 }
3627
3628 #[test]
3629 fn levene_unequal_variance() {
3630 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");
3633 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3634 }
3635
3636 #[test]
3637 fn levene_three_groups() {
3638 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3639 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3640 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3641 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3642 assert!(r.df >= 2.0);
3643 }
3644
3645 #[test]
3646 fn levene_edge_cases() {
3647 let g1 = [1.0, 2.0, 3.0];
3648 assert!(levene_test(&[&g1]).is_none()); }
3650
3651 #[test]
3656 fn bonferroni_basic() {
3657 let ps = [0.01, 0.04, 0.03, 0.005];
3658 let adj = bonferroni_correction(&ps).expect("should compute");
3659 assert!((adj[0] - 0.04).abs() < 1e-10);
3660 assert!((adj[1] - 0.16).abs() < 1e-10);
3661 assert!((adj[2] - 0.12).abs() < 1e-10);
3662 assert!((adj[3] - 0.02).abs() < 1e-10);
3663 }
3664
3665 #[test]
3666 fn bonferroni_capped_at_one() {
3667 let ps = [0.5, 0.6];
3668 let adj = bonferroni_correction(&ps).expect("should compute");
3669 assert!((adj[0] - 1.0).abs() < 1e-10);
3670 assert!((adj[1] - 1.0).abs() < 1e-10);
3671 }
3672
3673 #[test]
3674 fn bh_basic() {
3675 let ps = [0.01, 0.04, 0.03, 0.005];
3676 let adj = benjamini_hochberg(&ps).expect("should compute");
3677 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3679 assert!(
3680 adjusted >= orig - 1e-15,
3681 "adj[{i}] = {adjusted} < original {orig}"
3682 );
3683 }
3684 }
3687
3688 #[test]
3689 fn bh_all_significant() {
3690 let ps = [0.001, 0.002, 0.003];
3691 let adj = benjamini_hochberg(&ps).expect("should compute");
3692 for &a in &adj {
3693 assert!(a < 0.05);
3694 }
3695 }
3696
3697 #[test]
3698 fn correction_edge_cases() {
3699 assert!(bonferroni_correction(&[]).is_none());
3700 assert!(benjamini_hochberg(&[]).is_none());
3701 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3702 }
3703
3704 #[test]
3709 fn bartlett_equal_variances() {
3710 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3711 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3712 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3713 assert!(
3714 r.p_value > 0.9,
3715 "equal variance → p high, got {}",
3716 r.p_value
3717 );
3718 }
3719
3720 #[test]
3721 fn bartlett_unequal_variances() {
3722 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");
3725 assert!(
3726 r.p_value < 0.01,
3727 "very different variances → p < 0.01, got {}",
3728 r.p_value
3729 );
3730 assert!((r.df - 1.0).abs() < 1e-10); }
3732
3733 #[test]
3734 fn bartlett_three_groups() {
3735 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3736 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3737 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3739 assert!(r.p_value < 0.05, "one group with high variance");
3740 assert!((r.df - 2.0).abs() < 1e-10); }
3742
3743 #[test]
3744 fn bartlett_edge_cases() {
3745 let g1 = [1.0, 2.0, 3.0];
3746 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3750
3751 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3753 }
3754
3755 #[test]
3760 fn fisher_tea_tasting() {
3761 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3763 assert!(
3765 (r.p_value - 0.4857).abs() < 0.01,
3766 "p ≈ 0.4857, got {}",
3767 r.p_value
3768 );
3769 }
3770
3771 #[test]
3772 fn fisher_significant() {
3773 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3775 assert!(r.p_value < 0.001, "perfect association → p very small");
3776 }
3777
3778 #[test]
3779 fn fisher_no_association() {
3780 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3782 assert!(
3783 r.p_value > 0.9,
3784 "no association → p ≈ 1.0, got {}",
3785 r.p_value
3786 );
3787 }
3788
3789 #[test]
3790 fn fisher_small_table() {
3791 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3793 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3794 }
3795
3796 #[test]
3797 fn fisher_asymmetric() {
3798 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3800 assert!(r.p_value < 0.05, "significant association");
3801 }
3802
3803 #[test]
3804 fn fisher_edge_cases() {
3805 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()); }
3810
3811 #[test]
3812 fn fisher_odds_ratio() {
3813 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3814 assert!(
3816 (r.statistic - 9.0).abs() < 1e-10,
3817 "OR = 9, got {}",
3818 r.statistic
3819 );
3820 }
3821
3822 #[test]
3827 fn mk_increasing_trend() {
3828 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3829 let r = mann_kendall_test(&data).expect("should compute");
3830 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3831 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3832 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3833 assert!(r.s_statistic > 0);
3834 }
3835
3836 #[test]
3837 fn mk_decreasing_trend() {
3838 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3839 let r = mann_kendall_test(&data).expect("should compute");
3840 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3841 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3842 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3843 assert!(r.s_statistic < 0);
3844 }
3845
3846 #[test]
3847 fn mk_no_trend() {
3848 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3850 let r = mann_kendall_test(&data).expect("should compute");
3851 assert!(
3853 r.p_value > 0.05,
3854 "p = {} (should be > 0.05 for no trend)",
3855 r.p_value
3856 );
3857 }
3858
3859 #[test]
3860 fn mk_perfect_monotone() {
3861 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3862 let r = mann_kendall_test(&data).expect("should compute");
3863 assert_eq!(r.s_statistic, 45);
3865 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3866 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3867 }
3868
3869 #[test]
3870 fn mk_with_ties() {
3871 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3872 let r = mann_kendall_test(&data).expect("should compute");
3873 assert!(r.s_statistic > 0);
3874 let n = data.len() as f64;
3876 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3877 assert!(r.variance < base_var, "ties should reduce variance");
3878 }
3879
3880 #[test]
3881 fn mk_edge_cases() {
3882 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3884 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3886 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3888 }
3889
3890 #[test]
3891 fn mk_minimum_n() {
3892 let data = [1.0, 2.0, 3.0, 4.0];
3894 let r = mann_kendall_test(&data).expect("n=4 should work");
3895 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3897 }
3898
3899 #[test]
3900 fn mk_sen_slope_robust_to_outlier() {
3901 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3903 let r = mann_kendall_test(&data).expect("should compute");
3904 assert!(
3906 (r.sen_slope - 1.0).abs() < 0.5,
3907 "sen slope = {}, expected ≈ 1.0",
3908 r.sen_slope
3909 );
3910 }
3911
3912 #[test]
3917 fn adf_stationary_mean_reverting() {
3918 let data = [
3921 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,
3922 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,
3923 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,
3924 ];
3925 let r = adf_test(&data, AdfModel::Constant, AdfLags::default()).expect("should compute");
3926 assert!(
3928 r.rejected[2],
3929 "should reject at 10%: stat={}, cv={}",
3930 r.statistic, r.critical_values[2]
3931 );
3932 }
3933
3934 #[test]
3935 fn adf_nonstationary_random_walk() {
3936 let increments = [
3938 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,
3939 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,
3940 -0.13, 0.15, -0.1,
3941 ];
3942 let mut walk = Vec::with_capacity(increments.len());
3943 let mut cum = 0.0;
3944 for &inc in &increments {
3945 cum += inc;
3946 walk.push(cum);
3947 }
3948 let r = adf_test(&walk, AdfModel::Constant, AdfLags::default()).expect("should compute");
3949 assert!(
3951 !r.rejected[0],
3952 "should NOT reject at 1%: stat={}, cv={}",
3953 r.statistic, r.critical_values[0]
3954 );
3955 }
3956
3957 #[test]
3958 fn adf_with_fixed_lags() {
3959 let mut rng = mulberry32(0xadf2);
3963 let data: Vec<f64> = (0..50)
3964 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
3965 .collect();
3966 let r = adf_test(&data, AdfModel::Constant, AdfLags::Fixed(2)).expect("should compute");
3967 assert_eq!(r.n_lags, 2);
3968 assert!(r.statistic.is_finite());
3969 }
3970
3971 #[test]
3972 fn adf_constant_trend_model() {
3973 let data: Vec<f64> = (0..30)
3976 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
3977 .collect();
3978 let r =
3979 adf_test(&data, AdfModel::ConstantTrend, AdfLags::default()).expect("should compute");
3980 assert!(r.statistic.is_finite());
3981 assert_eq!(r.critical_values.len(), 3);
3982 }
3983
3984 #[test]
3985 fn adf_edge_cases() {
3986 assert_eq!(
3987 adf_test(&[1.0; 9], AdfModel::Constant, AdfLags::default()).unwrap_err(),
3988 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
3989 );
3990 let mut data = vec![0.0; 20];
3991 data[5] = f64::NAN;
3992 assert_eq!(
3993 adf_test(&data, AdfModel::Constant, AdfLags::default()).unwrap_err(),
3994 TimeSeriesError::ValueNotFinite { index: 5 }
3995 );
3996 let walk: Vec<f64> = (0..20).map(|i| ((i * 37) % 11) as f64).collect();
3998 assert_eq!(adf_lags_domain(&walk, AdfModel::Constant).unwrap(), 0..=8);
3999 assert_eq!(
4000 adf_lags_domain(&walk, AdfModel::ConstantTrend).unwrap(),
4001 0..=7
4002 );
4003 assert_eq!(
4004 adf_lags_domain(&walk[..9], AdfModel::Constant).unwrap_err(),
4005 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
4006 );
4007 for lags in [AdfLags::Fixed(9), AdfLags::Aic { max: Some(9) }] {
4008 let option = if matches!(lags, AdfLags::Fixed(_)) {
4009 "lags"
4010 } else {
4011 "max_lags"
4012 };
4013 assert_eq!(
4014 adf_test(&walk, AdfModel::Constant, lags).unwrap_err(),
4015 TimeSeriesError::OptionOutOfRange {
4016 option,
4017 got: 9.0,
4018 min: 0.0,
4019 max: 8.0
4020 }
4021 );
4022 }
4023 assert_eq!(
4024 adf_test(&[3.0; 20], AdfModel::Constant, AdfLags::default()).unwrap_err(),
4025 TimeSeriesError::DegenerateRegression
4026 );
4027 }
4028
4029 #[test]
4035 fn adf_exact_fit_is_undefined_and_not_chosen() {
4036 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
4037 let mut walk = vec![0.0_f64; 60];
4038 for i in 1..walk.len() {
4039 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
4040 }
4041 assert_eq!(
4042 adf_test(&walk, AdfModel::ConstantTrend, AdfLags::Fixed(9)).unwrap_err(),
4043 TimeSeriesError::DegenerateRegression
4044 );
4045 let r = adf_test(&walk, AdfModel::ConstantTrend, AdfLags::default())
4046 .expect("a lag below 9 fits");
4047 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
4048 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
4049 }
4050
4051 #[test]
4055 fn adf_matches_statsmodels() {
4056 let mut x = vec![0.0_f64; 120];
4057 for t in 1..x.len() {
4058 let e = ((t * 7919) % 101) as f64 / 50.0 - 1.0;
4059 x[t] = 0.7 * x[t - 1] + e;
4060 }
4061 for (model, lags, stat, n_lags, n_obs) in [
4062 (
4063 AdfModel::Constant,
4064 AdfLags::Aic { max: Some(8) },
4065 -3.436_052_894_8,
4066 5,
4067 114,
4068 ),
4069 (
4070 AdfModel::Constant,
4071 AdfLags::Aic { max: None },
4072 -3.436_052_894_8,
4073 5,
4074 114,
4075 ),
4076 (
4077 AdfModel::ConstantTrend,
4078 AdfLags::Aic { max: Some(6) },
4079 -3.505_247_069_0,
4080 5,
4081 114,
4082 ),
4083 (
4084 AdfModel::None,
4085 AdfLags::Aic { max: Some(5) },
4086 -3.459_240_021_9,
4087 5,
4088 114,
4089 ),
4090 (
4091 AdfModel::Constant,
4092 AdfLags::Fixed(3),
4093 -4.652_930_484_4,
4094 3,
4095 116,
4096 ),
4097 ] {
4098 let r = adf_test(&x, model, lags).expect("fits");
4099 assert_eq!((r.n_lags, r.n_obs), (n_lags, n_obs), "{model:?} {lags:?}");
4100 assert!(
4101 (r.statistic - stat).abs() < 1e-8,
4102 "{model:?} {lags:?}: {}",
4103 r.statistic
4104 );
4105 }
4106 }
4107
4108 #[test]
4109 fn adf_critical_values_constant() {
4110 let cv = adf_critical_values(AdfModel::Constant, 100);
4112 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
4114 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
4115 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
4116 }
4117
4118 #[test]
4119 fn adf_critical_values_ordering() {
4120 let cv = adf_critical_values(AdfModel::Constant, 50);
4121 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
4123 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
4124 }
4125}
4126
4127#[cfg(test)]
4130mod scipy_reference {
4131 use super::*;
4132
4133 const A: [f64; 8] = [5.1, 4.9, 5.6, 5.8, 6.0, 5.2, 4.7, 5.5];
4134 const B: [f64; 7] = [6.3, 5.9, 6.8, 7.1, 6.0, 6.6, 7.4];
4135 const G1: [f64; 5] = [23.1, 25.4, 22.8, 26.0, 24.3];
4136 const G2: [f64; 6] = [27.2, 28.9, 26.5, 29.4, 27.8, 28.1];
4137 const G3: [f64; 4] = [22.0, 21.4, 24.9, 23.3];
4138
4139 fn close(got: f64, want: f64, rel: f64, what: &str) {
4141 let scale = want.abs().max(1.0);
4142 assert!(
4143 (got - want).abs() <= rel * scale,
4144 "{what}: got {got}, SciPy {want} (relative tolerance {rel})"
4145 );
4146 }
4147
4148 #[test]
4149 fn t_tests_reproduce_scipy() {
4150 let r = one_sample_t_test(&A, 5.0).expect("valid");
4152 close(r.statistic, 2.197950389679656, 1e-12, "one-sample t");
4153 close(r.p_value, 0.06392412293886501, 1e-9, "one-sample p");
4154 assert_eq!(r.df, 7.0);
4155 let r = two_sample_t_test(&A, &B).expect("valid");
4157 close(r.statistic, -4.67567567567568, 1e-12, "Welch t");
4158 close(r.df, 11.56763357082458, 1e-12, "Welch df");
4159 close(r.p_value, 0.0005912510074711228, 1e-9, "Welch p");
4160 let x = [12.1, 13.4, 11.8, 14.2, 12.9, 13.0];
4162 let y = [12.9, 13.9, 12.0, 15.1, 13.5, 13.2];
4163 let r = paired_t_test(&x, &y).expect("valid");
4164 close(r.statistic, -4.437601569801824, 1e-12, "paired t");
4165 close(r.p_value, 0.006779965572779817, 1e-9, "paired p");
4166 assert_eq!(r.df, 5.0);
4167 }
4168
4169 #[test]
4170 fn anova_and_equal_variance_tests_reproduce_scipy() {
4171 let groups: [&[f64]; 3] = [&G1, &G2, &G3];
4172 let r = one_way_anova(&groups).expect("valid");
4174 close(r.f_statistic, 20.526701958197258, 1e-12, "ANOVA F");
4175 close(r.p_value, 0.00013390844134272023, 1e-9, "ANOVA p");
4176 let r = levene_test(&groups).expect("valid");
4178 close(r.statistic, 0.4752743705616534, 1e-12, "Brown-Forsythe W");
4179 close(r.p_value, 0.6329340379295331, 1e-9, "Brown-Forsythe p");
4180 let r = bartlett_test(&groups).expect("valid");
4182 close(r.statistic, 0.5282298427149905, 1e-12, "Bartlett T");
4183 close(r.p_value, 0.7678852881660961, 1e-9, "Bartlett p");
4184 }
4185
4186 #[test]
4187 fn kruskal_wallis_with_ties_reproduces_scipy() {
4188 let r = kruskal_wallis_test(&[
4190 &[1.0, 2.0, 2.0, 3.0, 5.0],
4191 &[4.0, 4.0, 6.0, 7.0],
4192 &[5.0, 8.0, 8.0, 9.0, 10.0],
4193 ])
4194 .expect("valid");
4195 close(r.statistic, 9.22394678492239, 1e-12, "H (tie-corrected)");
4196 close(r.p_value, 0.009932198829456881, 1e-9, "Kruskal-Wallis p");
4197 }
4198
4199 #[test]
4200 fn chi_squared_and_fisher_tests_reproduce_scipy() {
4201 let r = chi_squared_goodness_of_fit(&[18.0, 22.0, 30.0, 30.0], &[25.0; 4]).expect("valid");
4203 close(r.statistic, 4.32, 1e-12, "goodness-of-fit chi-squared");
4204 close(r.p_value, 0.22891886433610517, 1e-9, "goodness-of-fit p");
4205 let r = chi_squared_independence(&[12.0, 7.0, 9.0, 5.0, 14.0, 11.0], 2, 3).expect("valid");
4207 close(
4208 r.statistic,
4209 5.353085901027075,
4210 1e-12,
4211 "independence chi-squared",
4212 );
4213 close(r.p_value, 0.06880059054935043, 1e-9, "independence p");
4214 assert_eq!(r.df, 2.0);
4215 let r = fisher_exact_test(8, 2, 1, 5).expect("valid");
4217 close(r.p_value, 0.034965034965034975, 1e-9, "Fisher p");
4218 }
4219
4220 #[test]
4221 fn shapiro_wilk_reproduces_scipy() {
4222 let r = shapiro_wilk_test(&[
4224 148.0, 154.0, 158.0, 160.0, 161.0, 162.0, 166.0, 170.0, 182.0, 195.0, 236.0,
4225 ])
4226 .expect("valid");
4227 close(r.w, 0.7888146948631716, 1e-9, "W, skewed");
4228 close(r.p_value, 0.006703814061898823, 1e-9, "p, skewed");
4229 let r = shapiro_wilk_test(&[
4231 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,
4232 ])
4233 .expect("valid");
4234 close(r.w, 0.9864343014896592, 1e-9, "W, near normal");
4235 close(r.p_value, 0.9958703691266937, 1e-9, "p, near normal");
4236 }
4237
4238 #[test]
4239 fn mann_kendall_tau_and_sen_slope_reproduce_scipy() {
4240 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];
4243 let r = mann_kendall_test(&mk).expect("valid");
4244 close(r.kendall_tau, 0.8484848484848484, 1e-12, "Kendall tau");
4245 close(r.sen_slope, 0.22500000000000003, 1e-12, "Sen's slope");
4246 }
4247
4248 #[test]
4249 fn benjamini_hochberg_reproduces_scipy() {
4250 let adjusted = benjamini_hochberg(&[0.01, 0.04, 0.03, 0.005, 0.20, 0.50]).expect("valid");
4252 for (got, want) in adjusted.iter().zip([0.03, 0.06, 0.06, 0.03, 0.24, 0.5]) {
4253 close(*got, want, 1e-12, "BH-adjusted p");
4254 }
4255 }
4256}
4257
4258#[cfg(test)]
4259mod proptests {
4260 use super::*;
4261 use proptest::prelude::*;
4262
4263 proptest! {
4264 #[test]
4265 fn one_sample_p_bounded(
4266 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
4267 mu0 in -1e3_f64..1e3
4268 ) {
4269 if let Some(r) = one_sample_t_test(&data, mu0) {
4270 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4271 }
4272 }
4273
4274 #[test]
4275 fn two_sample_p_bounded(
4276 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4277 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4278 ) {
4279 if let Some(r) = two_sample_t_test(&a, &b) {
4280 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4281 }
4282 }
4283
4284 #[test]
4285 fn anova_p_bounded(
4286 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4287 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4288 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4289 ) {
4290 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
4291 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4292 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
4293 }
4294 }
4295
4296 #[test]
4297 fn bonferroni_monotone(
4298 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
4299 ) {
4300 let adj = bonferroni_correction(&ps).expect("should compute");
4301 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
4302 prop_assert!(adjusted >= orig - 1e-15,
4303 "adj[{i}] = {adjusted} < orig = {orig}");
4304 }
4305 }
4306
4307 #[test]
4308 fn shapiro_wilk_p_bounded(
4309 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
4310 ) {
4311 if let Some(r) = shapiro_wilk_test(&data) {
4312 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
4313 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4314 }
4315 }
4316
4317 #[test]
4318 fn anderson_darling_p_bounded(
4319 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
4320 ) {
4321 if let Some(r) = anderson_darling_test(&data) {
4322 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
4323 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4324 }
4325 }
4326
4327 #[test]
4328 fn mann_whitney_p_bounded(
4329 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4330 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4331 ) {
4332 if let Some(r) = mann_whitney_u_test(&a, &b) {
4333 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4334 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
4335 }
4336 }
4337
4338 #[test]
4339 fn wilcoxon_p_bounded(
4340 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4341 ) {
4342 let zeros: Vec<f64> = vec![0.0; diffs.len()];
4343 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
4344 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4345 }
4346 }
4347
4348 #[test]
4349 fn bartlett_p_bounded(
4350 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4351 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4352 ) {
4353 if let Some(r) = bartlett_test(&[&g1, &g2]) {
4354 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4355 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
4356 }
4357 }
4358
4359 #[test]
4360 fn fisher_p_bounded(
4361 a in 0_u64..20,
4362 b in 0_u64..20,
4363 c in 0_u64..20,
4364 d in 0_u64..20,
4365 ) {
4366 if let Some(r) = fisher_exact_test(a, b, c, d) {
4367 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4368 }
4369 }
4370
4371 #[test]
4372 fn mann_kendall_p_bounded(
4373 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
4374 ) {
4375 if let Some(r) = mann_kendall_test(&data) {
4376 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4377 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
4378 "tau = {}", r.kendall_tau);
4379 }
4380 }
4381
4382 #[test]
4383 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
4384 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
4385 let r = mann_kendall_test(&data).expect("should compute");
4386 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
4387 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
4388 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
4389 }
4390 }
4391}
4392
4393#[cfg(test)]
4396mod definition_reference {
4397 use super::*;
4398
4399 #[test]
4400 fn bonferroni_is_p_times_m_capped_at_one() {
4401 let adjusted = bonferroni_correction(&[0.01, 0.02, 0.04, 0.3, 0.0004]).expect("valid");
4402 let want = [0.05, 0.1, 0.2, 1.0, 0.002];
4403 assert_eq!(adjusted.len(), want.len());
4404 for (got, want) in adjusted.iter().zip(want) {
4405 assert!((got - want).abs() <= 1e-15, "got {got}, want {want}");
4406 }
4407 }
4408}
4409
4410#[cfg(test)]
4413mod scipy_tail_reference {
4414 use super::*;
4415
4416 fn close(got: f64, want: f64, what: &str) {
4417 assert!(
4418 (got - want).abs() <= 1e-9 * want.abs(),
4419 "{what}: got {got:e}, SciPy {want:e}"
4420 );
4421 }
4422
4423 #[test]
4424 fn t_tests_keep_their_tail() {
4425 let a = [100.2, 99.8, 100.5, 100.1, 99.9, 100.3, 100.0, 99.7];
4426 let b = [120.1, 119.7, 120.4, 120.0, 119.8, 120.3];
4427 let r = one_sample_t_test(&a, 90.0).expect("valid");
4429 close(r.p_value, 1.6824197797300066e-12, "one-sample p");
4430 let r = two_sample_t_test(&a, &b).expect("valid");
4432 close(r.p_value, 8.838218005160137e-19, "Welch p");
4433 }
4434
4435 #[test]
4436 fn f_and_chi_squared_tests_keep_their_tail() {
4437 let (g1, g2, g3) = (
4439 [10.1, 10.3, 9.9, 10.0, 10.2],
4440 [12.0, 12.2, 11.9, 12.1, 12.3],
4441 [14.1, 13.9, 14.0, 14.2, 13.8],
4442 );
4443 let r = one_way_anova(&[&g1, &g2, &g3]).expect("valid");
4444 close(r.p_value, 2.297566098988527e-13, "ANOVA p");
4445 let r = chi_squared_independence(&[80.0, 20.0, 30.0, 70.0], 2, 2).expect("valid");
4447 close(r.p_value, 1.1885852395646684e-12, "independence p");
4448 let r = chi_squared_goodness_of_fit(&[90.0, 10.0, 5.0, 95.0], &[50.0; 4]).expect("valid");
4450 close(r.p_value, 3.1567404374222543e-31, "goodness-of-fit p");
4451 let low: Vec<f64> = (1..=10).map(f64::from).collect();
4453 let mid: Vec<f64> = (11..=20).map(f64::from).collect();
4454 let high: Vec<f64> = (21..=30).map(f64::from).collect();
4455 let r = kruskal_wallis_test(&[&low, &mid, &high]).expect("valid");
4456 close(r.p_value, 2.49000508218493e-06, "Kruskal-Wallis p");
4457 }
4458}