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 tied(a: f64, b: f64) -> bool {
1193 a == b || (a - b).abs() <= 1e-12 * a.abs().max(b.abs())
1194}
1195
1196fn average_ranks(sorted: &[(f64, usize)]) -> Vec<f64> {
1199 let n = sorted.len();
1200 let mut ranks = vec![0.0; n];
1201 let mut i = 0;
1202 while i < n {
1203 let mut j = i + 1;
1204 while j < n && tied(sorted[j].0, sorted[i].0) {
1205 j += 1;
1206 }
1207 let avg_rank = (i + 1 + j) as f64 / 2.0;
1209 for rank in ranks.iter_mut().take(j).skip(i) {
1210 *rank = avg_rank;
1211 }
1212 i = j;
1213 }
1214 ranks
1215}
1216
1217fn compute_tie_correction(sorted: &[(f64, usize)]) -> f64 {
1219 let n = sorted.len();
1220 let mut correction = 0.0;
1221 let mut i = 0;
1222 while i < n {
1223 let mut j = i + 1;
1224 while j < n && tied(sorted[j].0, sorted[i].0) {
1225 j += 1;
1226 }
1227 let t = (j - i) as f64;
1228 if t > 1.0 {
1229 correction += t * (t * t - 1.0);
1230 }
1231 i = j;
1232 }
1233 correction
1234}
1235
1236pub fn kruskal_wallis_test(groups: &[&[f64]]) -> Option<TestResult> {
1268 let k = groups.len();
1269 if k < 2 {
1270 return None;
1271 }
1272 for g in groups {
1273 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1274 return None;
1275 }
1276 }
1277
1278 let total_n: usize = groups.iter().map(|g| g.len()).sum();
1279 let nf = total_n as f64;
1280
1281 let mut combined: Vec<(f64, usize)> = Vec::with_capacity(total_n);
1283 for (gi, g) in groups.iter().enumerate() {
1284 for &v in *g {
1285 combined.push((v, gi));
1286 }
1287 }
1288 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1289
1290 let ranks = average_ranks(&combined);
1291
1292 let mut rank_sums = vec![0.0; k];
1294 for ((_, gi), &r) in combined.iter().zip(ranks.iter()) {
1295 rank_sums[*gi] += r;
1296 }
1297
1298 let mean_rank = (nf + 1.0) / 2.0;
1300 let mut h = 0.0;
1301 for (gi, g) in groups.iter().enumerate() {
1302 let ni = g.len() as f64;
1303 let mean_rank_i = rank_sums[gi] / ni;
1304 h += ni * (mean_rank_i - mean_rank).powi(2);
1305 }
1306 h *= 12.0 / (nf * (nf + 1.0));
1307
1308 let tie_corr = compute_tie_correction(&combined);
1310 let denom = 1.0 - tie_corr / (nf * nf * nf - nf);
1311 if denom > 1e-15 {
1312 h /= denom;
1313 }
1314
1315 let df = (k - 1) as f64;
1316 let p_value = special::chi_squared_sf(h, df);
1317
1318 Some(TestResult {
1319 statistic: h,
1320 df,
1321 p_value,
1322 })
1323}
1324
1325pub fn levene_test(groups: &[&[f64]]) -> Option<TestResult> {
1361 let k = groups.len();
1362 if k < 2 {
1363 return None;
1364 }
1365 for g in groups {
1366 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1367 return None;
1368 }
1369 }
1370
1371 let z_groups: Vec<Vec<f64>> = groups
1373 .iter()
1374 .map(|g| {
1375 let median = stats::median(g).unwrap_or(0.0);
1376 g.iter().map(|&x| (x - median).abs()).collect()
1377 })
1378 .collect();
1379
1380 let z_refs: Vec<&[f64]> = z_groups.iter().map(|v| v.as_slice()).collect();
1382 let anova = one_way_anova(&z_refs)?;
1383
1384 Some(TestResult {
1385 statistic: anova.f_statistic,
1386 df: anova.df_between as f64,
1387 p_value: anova.p_value,
1388 })
1389}
1390
1391pub fn bonferroni_correction(p_values: &[f64]) -> Option<Vec<f64>> {
1404 if p_values.is_empty() || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1405 return None;
1406 }
1407 let m = p_values.len() as f64;
1408 Some(p_values.iter().map(|&p| (p * m).min(1.0)).collect())
1409}
1410
1411pub fn benjamini_hochberg(p_values: &[f64]) -> Option<Vec<f64>> {
1431 let m = p_values.len();
1432 if m == 0 || p_values.iter().any(|v| !(0.0..=1.0).contains(v)) {
1433 return None;
1434 }
1435
1436 let mut indices: Vec<usize> = (0..m).collect();
1438 indices.sort_by(|&a, &b| {
1439 p_values[a]
1440 .partial_cmp(&p_values[b])
1441 .unwrap_or(std::cmp::Ordering::Equal)
1442 });
1443
1444 let mf = m as f64;
1445 let mut adjusted = vec![0.0; m];
1446
1447 let mut cummin = f64::INFINITY;
1449 for (rank_rev, &orig_idx) in indices.iter().enumerate().rev() {
1450 let rank = rank_rev + 1; let adj = (p_values[orig_idx] * mf / rank as f64).min(1.0);
1452 cummin = cummin.min(adj);
1453 adjusted[orig_idx] = cummin;
1454 }
1455
1456 Some(adjusted)
1457}
1458
1459pub fn bartlett_test(groups: &[&[f64]]) -> Option<TestResult> {
1496 let k = groups.len();
1497 if k < 2 {
1498 return None;
1499 }
1500
1501 let mut sizes = Vec::with_capacity(k);
1502 let mut vars = Vec::with_capacity(k);
1503 let mut n_total: usize = 0;
1504
1505 for g in groups {
1506 if g.len() < 2 || g.iter().any(|v| !v.is_finite()) {
1507 return None;
1508 }
1509 let n = g.len();
1510 let v = stats::variance(g)?;
1511 if v <= 0.0 {
1512 return None; }
1514 sizes.push(n);
1515 vars.push(v);
1516 n_total += n;
1517 }
1518
1519 let nk = n_total - k; if nk == 0 {
1521 return None;
1522 }
1523 let nk_f = nk as f64;
1524
1525 let s2_pooled: f64 = sizes
1527 .iter()
1528 .zip(vars.iter())
1529 .map(|(&n, &v)| (n as f64 - 1.0) * v)
1530 .sum::<f64>()
1531 / nk_f;
1532
1533 if s2_pooled <= 0.0 {
1534 return None;
1535 }
1536
1537 let num = nk_f * s2_pooled.ln()
1539 - sizes
1540 .iter()
1541 .zip(vars.iter())
1542 .map(|(&n, &v)| (n as f64 - 1.0) * v.ln())
1543 .sum::<f64>();
1544
1545 let sum_recip: f64 = sizes.iter().map(|&n| 1.0 / (n as f64 - 1.0)).sum();
1547 let c = 1.0 + (sum_recip - 1.0 / nk_f) / (3.0 * (k as f64 - 1.0));
1548
1549 let statistic = num / c;
1550 let df = (k - 1) as f64;
1551 let p_value = special::chi_squared_sf(statistic, df);
1552
1553 Some(TestResult {
1554 statistic,
1555 df,
1556 p_value,
1557 })
1558}
1559
1560pub fn fisher_exact_test(a: u64, b: u64, c: u64, d: u64) -> Option<TestResult> {
1605 let row1 = a + b;
1606 let row2 = c + d;
1607 let col1 = a + c;
1608 let col2 = b + d;
1609 let n = a + b + c + d;
1610
1611 if row1 == 0 || row2 == 0 || col1 == 0 || col2 == 0 {
1613 return None;
1614 }
1615
1616 let log_prob = |a_i: u64| -> f64 {
1618 let b_i = row1 - a_i;
1619 let c_i = col1 - a_i;
1620 let d_i = row2 - c_i;
1621 ln_factorial(row1) + ln_factorial(row2) + ln_factorial(col1) + ln_factorial(col2)
1622 - ln_factorial(a_i)
1623 - ln_factorial(b_i)
1624 - ln_factorial(c_i)
1625 - ln_factorial(d_i)
1626 - ln_factorial(n)
1627 };
1628
1629 let a_min = col1.saturating_sub(row2);
1631 let a_max = row1.min(col1);
1632
1633 let log_p_obs = log_prob(a);
1634
1635 let mut p_value = 0.0;
1637 for a_i in a_min..=a_max {
1638 let lp = log_prob(a_i);
1639 if lp <= log_p_obs + 1e-10 {
1641 p_value += lp.exp();
1642 }
1643 }
1644
1645 let p_value = p_value.min(1.0);
1647
1648 let odds_ratio = if b > 0 && c > 0 {
1650 (a as f64 * d as f64) / (b as f64 * c as f64)
1651 } else {
1652 f64::INFINITY
1653 };
1654
1655 Some(TestResult {
1656 statistic: odds_ratio,
1657 df: 1.0,
1658 p_value,
1659 })
1660}
1661
1662#[derive(Debug, Clone, Copy)]
1668pub struct MannKendallResult {
1669 pub s_statistic: i64,
1671 pub variance: f64,
1673 pub z_statistic: f64,
1675 pub p_value: f64,
1677 pub kendall_tau: f64,
1679 pub sen_slope: f64,
1681}
1682
1683pub fn mann_kendall_test(data: &[f64]) -> Option<MannKendallResult> {
1721 let n = data.len();
1722 if n < 4 || data.iter().any(|v| !v.is_finite()) {
1723 return None;
1724 }
1725
1726 let mut s: i64 = 0;
1728 for i in 0..n - 1 {
1729 for j in (i + 1)..n {
1730 let diff = data[j] - data[i];
1731 if diff > 0.0 {
1732 s += 1;
1733 } else if diff < 0.0 {
1734 s -= 1;
1735 }
1736 }
1737 }
1738
1739 let mut sorted: Vec<f64> = data.to_vec();
1741 sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1742
1743 let mut tie_correction: f64 = 0.0;
1747 let mut current_count: usize = 1;
1748 for i in 1..sorted.len() {
1749 if sorted[i] == sorted[i - 1] {
1750 current_count += 1;
1751 } else {
1752 if current_count > 1 {
1753 let t = current_count as f64;
1754 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1755 }
1756 current_count = 1;
1757 }
1758 }
1759 if current_count > 1 {
1760 let t = current_count as f64;
1761 tie_correction += t * (t - 1.0) * (2.0 * t + 5.0);
1762 }
1763
1764 let nf = n as f64;
1766 let variance = (nf * (nf - 1.0) * (2.0 * nf + 5.0) - tie_correction) / 18.0;
1767
1768 if variance < 1e-300 {
1769 return None; }
1771
1772 let sigma = variance.sqrt();
1774 let z_statistic = if s > 0 {
1775 (s as f64 - 1.0) / sigma
1776 } else if s < 0 {
1777 (s as f64 + 1.0) / sigma
1778 } else {
1779 0.0
1780 };
1781
1782 let p_value = 2.0 * special::standard_normal_sf(z_statistic.abs());
1784 let p_value = p_value.clamp(0.0, 1.0);
1785
1786 let kendall_tau = (2 * s) as f64 / (nf * (nf - 1.0));
1788
1789 let mut slopes = Vec::with_capacity(n * (n - 1) / 2);
1791 for i in 0..n - 1 {
1792 for j in (i + 1)..n {
1793 let dx = (j - i) as f64;
1794 slopes.push((data[j] - data[i]) / dx);
1795 }
1796 }
1797 slopes.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
1798 let m = slopes.len();
1799 let sen_slope = if m % 2 == 0 {
1800 (slopes[m / 2 - 1] + slopes[m / 2]) / 2.0
1801 } else {
1802 slopes[m / 2]
1803 };
1804
1805 Some(MannKendallResult {
1806 s_statistic: s,
1807 variance,
1808 z_statistic,
1809 p_value,
1810 kendall_tau,
1811 sen_slope,
1812 })
1813}
1814
1815fn ln_factorial(n: u64) -> f64 {
1817 if n <= 1 {
1818 return 0.0;
1819 }
1820 special::ln_gamma(n as f64 + 1.0)
1822}
1823
1824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1830pub enum KpssModel {
1831 Level,
1833 Trend,
1835}
1836
1837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1839pub enum KpssLags {
1840 Hobijn,
1843 Schwert,
1845 Fixed(usize),
1847}
1848
1849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1851pub enum KpssPValue {
1852 Interpolated,
1854 AtMost,
1856 AtLeast,
1858}
1859
1860#[derive(Debug, Clone)]
1862pub struct KpssResult {
1863 pub statistic: f64,
1865 pub n_lags: usize,
1867 pub p_value: f64,
1870 pub p_value_bound: KpssPValue,
1872 pub critical_values: [f64; 4],
1874 pub rejected: [bool; 4],
1876}
1877
1878pub const KPSS_LEVELS: [f64; 4] = [0.10, 0.05, 0.025, 0.01];
1880
1881pub fn kpss_lags_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
1889 check_series(data, KPSS_MIN_OBSERVATIONS)?;
1890 Ok(0..=data.len() - 1)
1891}
1892
1893pub const KPSS_MIN_OBSERVATIONS: usize = 10;
1895
1896pub fn kpss_test(
1941 data: &[f64],
1942 model: KpssModel,
1943 lags: KpssLags,
1944) -> Result<KpssResult, TimeSeriesError> {
1945 let domain = kpss_lags_domain(data)?;
1946 let n = data.len();
1947 if let KpssLags::Fixed(l) = lags {
1948 check_option("lags", l, &domain)?;
1949 }
1950 let nf = n as f64;
1951
1952 let residuals: Vec<f64> = match model {
1953 KpssModel::Level => {
1954 let mean = data.iter().sum::<f64>() / nf;
1955 data.iter().map(|x| x - mean).collect()
1956 }
1957 KpssModel::Trend => {
1958 let t_mean = (nf + 1.0) / 2.0;
1960 let y_mean = data.iter().sum::<f64>() / nf;
1961 let (mut sty, mut stt) = (0.0, 0.0);
1962 for (i, y) in data.iter().enumerate() {
1963 let dt = (i + 1) as f64 - t_mean;
1964 sty += dt * (y - y_mean);
1965 stt += dt * dt;
1966 }
1967 let slope = sty / stt;
1968 data.iter()
1969 .enumerate()
1970 .map(|(i, y)| y - y_mean - slope * ((i + 1) as f64 - t_mean))
1971 .collect()
1972 }
1973 };
1974
1975 let sum_sq: f64 = residuals.iter().map(|e| e * e).sum();
1977 let size: f64 = data.iter().map(|x| x * x).sum();
1978 if sum_sq <= 1e-20 * size {
1979 return Err(TimeSeriesError::NoVariation);
1980 }
1981
1982 let autocovariance = |i: usize| -> f64 {
1983 residuals[i..]
1984 .iter()
1985 .zip(&residuals[..n - i])
1986 .map(|(a, b)| a * b)
1987 .sum()
1988 };
1989
1990 let n_lags = match lags {
1991 KpssLags::Fixed(l) => l,
1992 KpssLags::Schwert => ((12.0 * (nf / 100.0).powf(0.25)).ceil() as usize).min(n - 1),
1993 KpssLags::Hobijn => {
1994 let cov_lags = nf.powf(2.0 / 9.0) as usize;
1996 let mut s0 = sum_sq / nf;
1997 let mut s1 = 0.0;
1998 for i in 1..=cov_lags.min(n - 1) {
1999 let g = autocovariance(i) / (nf / 2.0);
2000 s0 += g;
2001 s1 += i as f64 * g;
2002 }
2003 let s_hat = s1 / s0;
2004 let gamma = 1.1447 * (s_hat * s_hat).powf(1.0 / 3.0);
2005 ((gamma * nf.powf(1.0 / 3.0)) as usize).min(n - 1)
2006 }
2007 };
2008
2009 let mut long_run = sum_sq;
2010 for i in 1..=n_lags {
2011 long_run += 2.0 * autocovariance(i) * (1.0 - i as f64 / (n_lags as f64 + 1.0));
2012 }
2013 let long_run = long_run / nf;
2014 if long_run <= 0.0 {
2015 return Err(TimeSeriesError::NoVariation);
2016 }
2017
2018 let mut partial = 0.0;
2019 let eta = residuals
2020 .iter()
2021 .map(|e| {
2022 partial += e;
2023 partial * partial
2024 })
2025 .sum::<f64>()
2026 / (nf * nf);
2027 let statistic = eta / long_run;
2028
2029 let critical_values = match model {
2030 KpssModel::Level => [0.347, 0.463, 0.574, 0.739],
2031 KpssModel::Trend => [0.119, 0.146, 0.176, 0.216],
2032 };
2033 let (p_value, p_value_bound) = if statistic <= critical_values[0] {
2034 (KPSS_LEVELS[0], KpssPValue::AtLeast)
2035 } else if statistic >= critical_values[3] {
2036 (KPSS_LEVELS[3], KpssPValue::AtMost)
2037 } else {
2038 let k = critical_values
2039 .windows(2)
2040 .position(|w| statistic <= w[1])
2041 .expect("the statistic lies inside the table");
2042 let (c0, c1) = (critical_values[k], critical_values[k + 1]);
2043 let (p0, p1) = (KPSS_LEVELS[k], KPSS_LEVELS[k + 1]);
2044 (
2045 p0 + (statistic - c0) / (c1 - c0) * (p1 - p0),
2046 KpssPValue::Interpolated,
2047 )
2048 };
2049 let rejected = critical_values.map(|c| statistic > c);
2050
2051 Ok(KpssResult {
2052 statistic,
2053 n_lags,
2054 p_value,
2055 p_value_bound,
2056 critical_values,
2057 rejected,
2058 })
2059}
2060
2061#[cfg(test)]
2064mod kpss_reference {
2065 use super::*;
2066
2067 const AR: [f64; 60] = [
2068 0.0, 0.299, -0.154, -0.953, -0.836, -1.326, -0.471, 1.152, -0.031, -0.633, 0.237, 0.452,
2069 0.286, -0.816, -0.355, 0.553, -1.123, -0.907, -2.264, -2.196, -2.72, -1.323, -1.796,
2070 -0.447, -0.022, -0.196, -2.595, -1.577, -0.68, -0.159, -1.594, -1.115, -1.425, -1.379,
2071 0.509, -0.604, -0.275, 0.774, -0.274, -0.222, 0.021, 0.073, -1.196, -0.402, 1.198, -1.068,
2072 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,
2073 0.02,
2074 ];
2075 const RW: [f64; 60] = [
2076 0.001, 0.3, 0.026, -0.865, -1.32, -2.312, -2.252, -0.912, -1.404, -2.024, -1.534, -1.177,
2077 -1.072, -2.002, -2.031, -1.336, -2.68, -3.138, -5.039, -6.329, -8.171, -8.406, -9.673,
2078 -9.402, -9.245, -9.432, -11.949, -12.488, -12.537, -12.424, -13.954, -14.432, -15.411,
2079 -16.22, -15.159, -15.967, -16.0, -15.116, -15.7, -15.812, -15.702, -15.638, -16.863,
2080 -16.787, -15.428, -16.975, -16.116, -15.997, -16.638, -14.638, -13.876, -15.075, -15.0,
2081 -14.423, -14.612, -13.929, -13.996, -13.329, -11.89, -12.566,
2082 ];
2083
2084 #[test]
2085 fn statistic_lag_and_p_value_reproduce_statsmodels() {
2086 use KpssLags::{Fixed, Hobijn, Schwert};
2087 use KpssModel::{Level, Trend};
2088 type Case = (&'static [f64; 60], KpssModel, KpssLags, f64, f64, usize);
2089 let cases: [Case; 12] = [
2090 (
2091 &AR,
2092 Level,
2093 Hobijn,
2094 0.5229270606681895,
2095 0.036502914263921285,
2096 3,
2097 ),
2098 (&AR, Level, Schwert, 0.288001639308506, 0.1, 11),
2099 (
2100 &AR,
2101 Level,
2102 Fixed(4),
2103 0.46189194657609306,
2104 0.05047760923444266,
2105 4,
2106 ),
2107 (&AR, Trend, Hobijn, 0.2274846895383343, 0.01, 3),
2108 (
2109 &AR,
2110 Trend,
2111 Schwert,
2112 0.14193842821368732,
2113 0.05752142923391235,
2114 11,
2115 ),
2116 (
2117 &AR,
2118 Trend,
2119 Fixed(4),
2120 0.20574827757371436,
2121 0.013844395909857114,
2122 4,
2123 ),
2124 (&RW, Level, Hobijn, 0.9286533322383663, 0.01, 5),
2125 (
2126 &RW,
2127 Level,
2128 Schwert,
2129 0.5112288964656923,
2130 0.0391376359311504,
2131 11,
2132 ),
2133 (&RW, Level, Fixed(4), 1.09862199299003, 0.01, 4),
2134 (&RW, Trend, Hobijn, 0.26866997231516837, 0.01, 4),
2135 (
2136 &RW,
2137 Trend,
2138 Schwert,
2139 0.14468829763941088,
2140 0.05242907844553539,
2141 11,
2142 ),
2143 (&RW, Trend, Fixed(4), 0.26866997231516837, 0.01, 4),
2144 ];
2145 for (data, model, lags, statistic, p, n_lags) in cases {
2146 let r = kpss_test(data, model, lags).unwrap();
2147 let what = format!("{model:?} {lags:?}");
2148 assert_eq!(r.n_lags, n_lags, "{what}");
2149 assert!(
2150 (r.statistic - statistic).abs() < 1e-10 * statistic,
2151 "{what}: {} vs {statistic}",
2152 r.statistic
2153 );
2154 assert!(
2155 (r.p_value - p).abs() < 1e-12,
2156 "{what}: p {} vs {p}",
2157 r.p_value
2158 );
2159 }
2160 }
2161
2162 #[test]
2163 fn bounds_rejections_and_refusals() {
2164 let r = kpss_test(&RW, KpssModel::Level, KpssLags::Fixed(4)).unwrap();
2165 assert_eq!(r.p_value_bound, KpssPValue::AtMost);
2166 assert_eq!(r.rejected, [true; 4]);
2167 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Schwert).unwrap();
2168 assert_eq!(r.p_value_bound, KpssPValue::AtLeast);
2169 assert_eq!(r.rejected, [false; 4]);
2170 let r = kpss_test(&AR, KpssModel::Level, KpssLags::Hobijn).unwrap();
2171 assert_eq!(r.p_value_bound, KpssPValue::Interpolated);
2172 assert_eq!(r.rejected, [true, true, false, false]);
2173
2174 assert_eq!(
2175 kpss_test(&AR[..9], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2176 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
2177 );
2178 assert_eq!(
2179 kpss_test(&AR, KpssModel::Level, KpssLags::Fixed(60)).unwrap_err(),
2180 TimeSeriesError::OptionOutOfRange {
2181 option: "lags",
2182 got: 60.0,
2183 min: 0.0,
2184 max: 59.0
2185 }
2186 );
2187 assert_eq!(
2188 kpss_test(&[4.0; 20], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2189 TimeSeriesError::NoVariation
2190 );
2191 let line: Vec<f64> = (0..20).map(|i| 2.0 + 0.5 * i as f64).collect();
2192 assert_eq!(
2193 kpss_test(&line, KpssModel::Trend, KpssLags::Hobijn).unwrap_err(),
2194 TimeSeriesError::NoVariation
2195 );
2196 let mut bad = AR;
2197 bad[3] = f64::INFINITY;
2198 assert_eq!(
2199 kpss_test(&bad[..5], KpssModel::Level, KpssLags::Hobijn).unwrap_err(),
2200 TimeSeriesError::ValueNotFinite { index: 3 }
2201 );
2202 }
2203
2204 #[test]
2206 fn unit_invariant() {
2207 let base = kpss_test(&AR, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2208 for f in [1e-9, 1e9] {
2209 let x: Vec<f64> = AR.iter().map(|v| v * f).collect();
2210 let r = kpss_test(&x, KpssModel::Trend, KpssLags::Hobijn).unwrap();
2211 assert_eq!(r.n_lags, base.n_lags);
2212 assert!((r.statistic - base.statistic).abs() < 1e-9 * base.statistic);
2213 }
2214 }
2215}
2216
2217#[derive(Debug, Clone)]
2223pub struct AdfResult {
2224 pub statistic: f64,
2226 pub n_lags: usize,
2228 pub n_obs: usize,
2230 pub critical_values: [f64; 3],
2232 pub rejected: [bool; 3],
2234}
2235
2236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2238pub enum AdfLags {
2239 Aic {
2244 max: Option<usize>,
2246 },
2247 Fixed(usize),
2249}
2250
2251impl Default for AdfLags {
2252 fn default() -> Self {
2253 AdfLags::Aic { max: None }
2254 }
2255}
2256
2257#[derive(Debug, Clone, Copy)]
2259pub enum AdfModel {
2260 None,
2262 Constant,
2264 ConstantTrend,
2266}
2267
2268pub fn adf_test(
2308 data: &[f64],
2309 model: AdfModel,
2310 lags: AdfLags,
2311) -> Result<AdfResult, TimeSeriesError> {
2312 let domain = adf_lags_domain(data, model)?;
2313 let n = data.len();
2314 match lags {
2315 AdfLags::Fixed(p) => {
2316 check_option("lags", p, &domain)?;
2317 }
2318 AdfLags::Aic { max: Some(m) } => {
2319 check_option("max_lags", m, &domain)?;
2320 }
2321 AdfLags::Aic { max: None } => {}
2322 }
2323
2324 let dy: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
2326
2327 let best_lag = match lags {
2328 AdfLags::Fixed(p) => p,
2329 AdfLags::Aic { max } => {
2330 let schwert = (12.0 * (n as f64 / 100.0).powf(0.25)).floor() as usize;
2332 let p_max = max.unwrap_or_else(|| schwert.min(n / 3));
2333 select_adf_lag(data, &dy, model, p_max).ok_or(TimeSeriesError::DegenerateRegression)?
2334 }
2335 };
2336
2337 adf_ols(data, &dy, model, best_lag).ok_or(TimeSeriesError::DegenerateRegression)
2339}
2340
2341pub const ADF_MIN_OBSERVATIONS: usize = 10;
2343
2344const ADF_MIN_REGRESSION_ROWS: usize = 5;
2346
2347pub fn adf_lags_domain(
2368 data: &[f64],
2369 model: AdfModel,
2370) -> Result<RangeInclusive<usize>, TimeSeriesError> {
2371 check_series(data, ADF_MIN_OBSERVATIONS)?;
2372 let n = data.len();
2373 let fixed_columns = match model {
2374 AdfModel::None => 1,
2375 AdfModel::Constant => 2,
2376 AdfModel::ConstantTrend => 3,
2377 };
2378 Ok(0..=(n - 1 - ADF_MIN_REGRESSION_ROWS).min((n - 2 - fixed_columns) / 2))
2379}
2380
2381fn select_adf_lag(data: &[f64], dy: &[f64], model: AdfModel, p_max: usize) -> Option<usize> {
2386 let start = p_max;
2387 let mut best: Option<(f64, usize)> = None;
2388 for p in 0..=p_max {
2389 if let Some(aic) = adf_ols_aic(data, dy, model, p, start) {
2390 if best.is_none_or(|(b, _)| aic < b) {
2391 best = Some((aic, p));
2392 }
2393 }
2394 }
2395 best.map(|(_, p)| p)
2396}
2397
2398#[allow(clippy::type_complexity)]
2402fn adf_build_matrix(
2403 data: &[f64],
2404 dy: &[f64],
2405 model: AdfModel,
2406 p: usize,
2407 start: usize,
2408) -> Option<(Vec<f64>, Vec<f64>, usize, usize, usize)> {
2409 debug_assert!(start >= p, "the first row needs its p lagged differences");
2411 if start >= dy.len() || dy.len() - start < 5 {
2412 return None;
2413 }
2414 let m = dy.len() - start;
2415
2416 let y_dep: Vec<f64> = dy[start..].to_vec();
2417
2418 let has_intercept = !matches!(model, AdfModel::None);
2420 let has_trend = matches!(model, AdfModel::ConstantTrend);
2421 let ncols = has_intercept as usize + 1 + has_trend as usize + p;
2422
2423 let mut x_data = Vec::with_capacity(m * ncols);
2425 let mut gamma_col = 0;
2426
2427 for i in 0..m {
2428 let t = start + i;
2429 if has_intercept {
2430 x_data.push(1.0); gamma_col = 1;
2432 }
2433 x_data.push(data[t]); if has_trend {
2435 x_data.push((t + 1) as f64); }
2437 for lag in 1..=p {
2438 x_data.push(dy[t - lag]);
2439 }
2440 }
2441
2442 Some((x_data, y_dep, m, ncols, gamma_col))
2443}
2444
2445fn adf_ols_core(
2449 x_data: &[f64],
2450 y: &[f64],
2451 m: usize,
2452 ncols: usize,
2453 gamma_col: usize,
2454) -> Option<(f64, f64, usize)> {
2455 let mut xtx = vec![0.0_f64; ncols * ncols];
2457 for i in 0..m {
2458 let row = &x_data[i * ncols..(i + 1) * ncols];
2459 for j in 0..ncols {
2460 for k in j..ncols {
2461 xtx[j * ncols + k] += row[j] * row[k];
2462 }
2463 }
2464 }
2465 for j in 0..ncols {
2467 for k in (j + 1)..ncols {
2468 xtx[k * ncols + j] = xtx[j * ncols + k];
2469 }
2470 }
2471
2472 let mut xty = vec![0.0_f64; ncols];
2474 for i in 0..m {
2475 let row = &x_data[i * ncols..(i + 1) * ncols];
2476 for j in 0..ncols {
2477 xty[j] += row[j] * y[i];
2478 }
2479 }
2480
2481 let own_size: Vec<f64> = (0..ncols).map(|i| xtx[i * ncols + i]).collect();
2485 let negligible = |pivot: f64, col: usize, row: usize| {
2486 pivot <= 1e-15 * (own_size[col] * own_size[row]).sqrt()
2487 };
2488
2489 let mut augmented = vec![0.0_f64; ncols * (ncols + 1)];
2491 for i in 0..ncols {
2492 for j in 0..ncols {
2493 augmented[i * (ncols + 1) + j] = xtx[i * ncols + j];
2494 }
2495 augmented[i * (ncols + 1) + ncols] = xty[i];
2496 }
2497
2498 for col in 0..ncols {
2499 let mut max_row = col;
2501 let mut max_val = augmented[col * (ncols + 1) + col].abs();
2502 for row in (col + 1)..ncols {
2503 let val = augmented[row * (ncols + 1) + col].abs();
2504 if val > max_val {
2505 max_val = val;
2506 max_row = row;
2507 }
2508 }
2509 if negligible(max_val, col, max_row) {
2510 return None; }
2512 if max_row != col {
2513 for j in 0..=ncols {
2514 let a = col * (ncols + 1) + j;
2515 let b = max_row * (ncols + 1) + j;
2516 augmented.swap(a, b);
2517 }
2518 }
2519
2520 let pivot = augmented[col * (ncols + 1) + col];
2521 for row in (col + 1)..ncols {
2522 let factor = augmented[row * (ncols + 1) + col] / pivot;
2523 for j in col..=ncols {
2524 let above = augmented[col * (ncols + 1) + j];
2525 augmented[row * (ncols + 1) + j] -= factor * above;
2526 }
2527 }
2528 }
2529
2530 let mut beta = vec![0.0_f64; ncols];
2532 for i in (0..ncols).rev() {
2533 let mut sum = augmented[i * (ncols + 1) + ncols];
2534 for j in (i + 1)..ncols {
2535 sum -= augmented[i * (ncols + 1) + j] * beta[j];
2536 }
2537 beta[i] = sum / augmented[i * (ncols + 1) + i];
2538 }
2539
2540 let mut rss = 0.0;
2542 for i in 0..m {
2543 let row = &x_data[i * ncols..(i + 1) * ncols];
2544 let y_hat: f64 = row.iter().zip(beta.iter()).map(|(&x, &b)| x * b).sum();
2545 let resid = y[i] - y_hat;
2546 rss += resid * resid;
2547 }
2548
2549 let yy: f64 = y.iter().map(|v| v * v).sum();
2555 if yy == 0.0 || rss <= yy * 1e-16 {
2556 return None;
2557 }
2558
2559 let df = m - ncols;
2561 if df == 0 {
2562 return None;
2563 }
2564 let mse = rss / df as f64;
2565
2566 let mut xtx_aug = vec![0.0_f64; ncols * ncols * 2]; for i in 0..ncols {
2569 for j in 0..ncols {
2570 xtx_aug[i * 2 * ncols + j] = xtx[i * ncols + j];
2571 }
2572 xtx_aug[i * 2 * ncols + ncols + i] = 1.0;
2573 }
2574
2575 for col in 0..ncols {
2577 let mut max_row = col;
2578 let mut max_val = xtx_aug[col * 2 * ncols + col].abs();
2579 for row in (col + 1)..ncols {
2580 let val = xtx_aug[row * 2 * ncols + col].abs();
2581 if val > max_val {
2582 max_val = val;
2583 max_row = row;
2584 }
2585 }
2586 if negligible(max_val, col, max_row) {
2587 return None;
2588 }
2589 if max_row != col {
2590 for j in 0..(2 * ncols) {
2591 let a = col * 2 * ncols + j;
2592 let b = max_row * 2 * ncols + j;
2593 xtx_aug.swap(a, b);
2594 }
2595 }
2596
2597 let pivot = xtx_aug[col * 2 * ncols + col];
2598 for j in 0..(2 * ncols) {
2599 xtx_aug[col * 2 * ncols + j] /= pivot;
2600 }
2601 for row in 0..ncols {
2602 if row == col {
2603 continue;
2604 }
2605 let factor = xtx_aug[row * 2 * ncols + col];
2606 for j in 0..(2 * ncols) {
2607 let above = xtx_aug[col * 2 * ncols + j];
2608 xtx_aug[row * 2 * ncols + j] -= factor * above;
2609 }
2610 }
2611 }
2612
2613 let var_gamma = mse * xtx_aug[gamma_col * 2 * ncols + ncols + gamma_col];
2615 if var_gamma <= 0.0 {
2616 return None;
2617 }
2618 let se_gamma = var_gamma.sqrt();
2619 let t_gamma = beta[gamma_col] / se_gamma;
2620
2621 Some((t_gamma, rss, ncols))
2622}
2623
2624fn adf_ols_aic(data: &[f64], dy: &[f64], model: AdfModel, p: usize, start: usize) -> Option<f64> {
2627 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, start)?;
2628 let (_t_stat, rss, k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2629 Some(2.0 * k as f64 + m as f64 * (rss / m as f64).ln())
2630}
2631
2632fn adf_ols(data: &[f64], dy: &[f64], model: AdfModel, p: usize) -> Option<AdfResult> {
2634 let (x_data, y_dep, m, ncols, gamma_col) = adf_build_matrix(data, dy, model, p, p)?;
2635 let (gamma_t, _rss, _k) = adf_ols_core(&x_data, &y_dep, m, ncols, gamma_col)?;
2636
2637 let critical_values = adf_critical_values(model, m);
2638
2639 let rejected = [
2640 gamma_t <= critical_values[0],
2641 gamma_t <= critical_values[1],
2642 gamma_t <= critical_values[2],
2643 ];
2644
2645 Some(AdfResult {
2646 statistic: gamma_t,
2647 n_lags: p,
2648 n_obs: m,
2649 critical_values,
2650 rejected,
2651 })
2652}
2653
2654fn adf_critical_values(model: AdfModel, n: usize) -> [f64; 3] {
2658 let (tau_inf, tau1, tau2): ([f64; 3], [f64; 3], [f64; 3]) = match model {
2663 AdfModel::None => (
2664 [-2.5658, -1.9393, -1.6156],
2665 [-1.960, -0.398, -0.181],
2666 [-10.04, 0.0, 0.0],
2667 ),
2668 AdfModel::Constant => (
2669 [-3.4336, -2.8621, -2.5671],
2670 [-5.999, -2.738, -1.438],
2671 [-29.25, -8.36, -4.48],
2672 ),
2673 AdfModel::ConstantTrend => (
2674 [-3.9638, -3.4126, -3.1279],
2675 [-8.353, -4.039, -2.418],
2676 [-47.44, -17.83, -7.58],
2677 ),
2678 };
2679
2680 let nf = n as f64;
2681 let inv_n = 1.0 / nf;
2682 let inv_n2 = inv_n * inv_n;
2683
2684 [
2685 tau_inf[0] + tau1[0] * inv_n + tau2[0] * inv_n2,
2686 tau_inf[1] + tau1[1] * inv_n + tau2[1] * inv_n2,
2687 tau_inf[2] + tau1[2] * inv_n + tau2[2] * inv_n2,
2688 ]
2689}
2690
2691#[cfg(test)]
2692mod tests {
2693 use super::*;
2694
2695 #[test]
2696 fn corrections_refuse_what_is_not_a_probability() {
2697 assert!(bonferroni_correction(&[0.01, 1.5]).is_none());
2698 assert!(benjamini_hochberg(&[-0.1, 0.2]).is_none());
2699 assert_eq!(bonferroni_correction(&[0.01, 0.2]), Some(vec![0.02, 0.4]));
2700 }
2701
2702 fn mulberry32(seed: u32) -> impl FnMut() -> f64 {
2708 let mut a = seed;
2709 move || {
2710 a = a.wrapping_add(0x6d2b_79f5);
2711 let mut t = a;
2712 t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2713 t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2714 ((t ^ (t >> 14)) as f64) / 4_294_967_296.0
2715 }
2716 }
2717
2718 #[test]
2724 fn ad_pvalue_no_overflow_large_nonnormal_n() {
2725 let mut rng = mulberry32(0x5350_4331);
2726 let full: Vec<f64> = (0..20_000)
2727 .map(|_| {
2728 let u = rng();
2729 -(1.0 - u).ln() })
2731 .collect();
2732
2733 let mut prev_a2 = 0.0_f64;
2734 let mut prev_p = f64::INFINITY;
2735 for &n in &[3000usize, 5000, 6000, 7000, 8000, 10000, 16000, 20000] {
2736 let r = anderson_darling_test(&full[..n]).expect("computes");
2737 assert!(
2739 r.statistic_star > prev_a2,
2740 "A*² must grow with n; n={n} A*²={} prev={prev_a2}",
2741 r.statistic_star
2742 );
2743 assert!(
2745 r.p_value < 0.5,
2746 "clearly non-normal data (n={n}, A*²={}) must not report p={} ≈ normal",
2747 r.statistic_star,
2748 r.p_value
2749 );
2750 assert!(
2752 r.p_value <= prev_p + 1e-12,
2753 "p must be non-increasing as A*² grows; n={n} p={} prev={prev_p}",
2754 r.p_value
2755 );
2756 prev_a2 = r.statistic_star;
2757 prev_p = r.p_value;
2758 }
2759 }
2760
2761 #[test]
2766 fn one_sample_null_true() {
2767 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2768 let r = one_sample_t_test(&data, 5.0).expect("should compute");
2769 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2770 }
2771
2772 #[test]
2773 fn one_sample_null_false() {
2774 let data = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2775 let r = one_sample_t_test(&data, 10.0).expect("should compute");
2776 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2777 }
2778
2779 #[test]
2780 fn one_sample_edge_cases() {
2781 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());
2784 }
2785
2786 #[test]
2791 fn two_sample_same_mean() {
2792 let a = [5.0, 5.1, 4.9, 5.0, 5.1, 4.9, 5.0, 5.0];
2793 let b = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.1, 4.9];
2794 let r = two_sample_t_test(&a, &b).expect("should compute");
2795 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2796 }
2797
2798 #[test]
2799 fn two_sample_different_means() {
2800 let a = [1.0, 2.0, 3.0, 2.0, 1.5, 2.5];
2801 let b = [10.0, 11.0, 12.0, 10.5, 11.5, 10.5];
2802 let r = two_sample_t_test(&a, &b).expect("should compute");
2803 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2804 }
2805
2806 #[test]
2807 fn two_sample_different_sizes() {
2808 let a = [1.0, 2.0, 3.0];
2809 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
2810 let r = two_sample_t_test(&a, &b).expect("should compute");
2811 assert!(r.p_value < 0.05);
2812 }
2813
2814 #[test]
2815 fn two_sample_edge_cases() {
2816 assert!(two_sample_t_test(&[1.0], &[2.0, 3.0]).is_none());
2817 assert!(two_sample_t_test(&[1.0, 2.0], &[3.0]).is_none());
2818 }
2819
2820 #[test]
2825 fn paired_no_difference() {
2826 let x = [5.0, 6.0, 7.0, 8.0, 9.0];
2827 let y = [5.1, 5.9, 7.1, 7.9, 9.1];
2828 let r = paired_t_test(&x, &y).expect("should compute");
2829 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2830 }
2831
2832 #[test]
2833 fn paired_significant_difference() {
2834 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
2836 let after = [6.2, 7.1, 8.3, 9.0, 10.4, 11.1, 12.2, 13.3];
2837 let r = paired_t_test(&before, &after).expect("should compute");
2838 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2839 assert!(r.statistic < 0.0); }
2841
2842 #[test]
2843 fn paired_edge_cases() {
2844 assert!(paired_t_test(&[1.0, 2.0], &[3.0]).is_none()); assert!(paired_t_test(&[1.0], &[2.0]).is_none()); }
2847
2848 #[test]
2853 fn anova_same_means() {
2854 let g1 = [5.0, 5.1, 4.9, 5.0, 5.1];
2855 let g2 = [5.0, 5.2, 4.8, 5.1, 4.9];
2856 let g3 = [5.1, 4.9, 5.0, 5.0, 5.1];
2857 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2858 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2859 }
2860
2861 #[test]
2862 fn anova_different_means() {
2863 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2864 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2865 let g3 = [10.0, 11.0, 12.0, 11.0, 10.5];
2866 let r = one_way_anova(&[&g1, &g2, &g3]).expect("should compute");
2867 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2868 assert!(r.df_between == 2);
2869 assert!(r.df_within == 12);
2870 }
2871
2872 #[test]
2873 fn anova_ss_decomposition() {
2874 let g1 = [1.0, 2.0, 3.0, 2.0, 1.5];
2875 let g2 = [5.0, 6.0, 7.0, 6.0, 5.5];
2876 let r = one_way_anova(&[&g1, &g2]).expect("should compute");
2877 let all_data: Vec<f64> = g1.iter().chain(g2.iter()).copied().collect();
2879 let ss_total: f64 = all_data.iter().map(|&x| (x - r.grand_mean).powi(2)).sum();
2880 assert!(
2881 (ss_total - (r.ss_between + r.ss_within)).abs() < 1e-10,
2882 "SS decomposition: {ss_total} vs {} + {}",
2883 r.ss_between,
2884 r.ss_within
2885 );
2886 }
2887
2888 #[test]
2889 fn anova_edge_cases() {
2890 let g1 = [1.0, 2.0, 3.0];
2891 assert!(one_way_anova(&[&g1]).is_none()); }
2893
2894 #[test]
2899 fn chi2_gof_uniform() {
2900 let observed = [25.0, 25.0, 25.0, 25.0];
2902 let expected = [25.0, 25.0, 25.0, 25.0];
2903 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2904 assert!((r.statistic).abs() < 1e-15);
2905 assert!((r.p_value - 1.0).abs() < 0.01);
2906 }
2907
2908 #[test]
2909 fn chi2_gof_significant() {
2910 let observed = [90.0, 10.0];
2911 let expected = [50.0, 50.0];
2912 let r = chi_squared_goodness_of_fit(&observed, &expected).expect("should compute");
2913 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2914 }
2915
2916 #[test]
2917 fn chi2_gof_edge_cases() {
2918 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()); }
2922
2923 #[test]
2928 fn chi2_independence_significant() {
2929 let table = [30.0, 10.0, 10.0, 50.0];
2931 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2932 assert!(r.p_value < 0.001, "p = {}", r.p_value);
2933 assert!((r.df - 1.0).abs() < 1e-10);
2934 }
2935
2936 #[test]
2937 fn chi2_independence_not_significant() {
2938 let table = [25.0, 25.0, 25.0, 25.0];
2940 let r = chi_squared_independence(&table, 2, 2).expect("should compute");
2941 assert!(r.p_value > 0.3, "p = {}", r.p_value);
2942 }
2943
2944 #[test]
2945 fn chi2_independence_3x3() {
2946 let table = [10.0, 20.0, 30.0, 40.0, 30.0, 20.0, 20.0, 25.0, 25.0];
2947 let r = chi_squared_independence(&table, 3, 3).expect("should compute");
2948 assert!((r.df - 4.0).abs() < 1e-10);
2949 assert!(r.p_value < 0.05);
2950 }
2951
2952 #[test]
2953 fn chi2_independence_edge_cases() {
2954 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()); }
2958
2959 #[test]
2964 fn jb_normal_data() {
2965 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
2967 let r = jarque_bera_test(&data).expect("should compute");
2968 assert!(r.p_value > 0.05, "p = {}", r.p_value);
2969 }
2970
2971 #[test]
2972 fn jb_skewed_data() {
2973 let data = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 20.0, 50.0];
2975 let r = jarque_bera_test(&data).expect("should compute");
2976 assert!(r.p_value < 0.05, "p = {}", r.p_value);
2977 }
2978
2979 #[test]
2986 fn jb_is_the_moment_form_the_paper_defines() {
2987 let data = [
2988 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,
2989 3.6, 2.8, 3.2,
2990 ];
2991 let r = jarque_bera_test(&data).expect("20 finite points");
2992 assert!(
2993 (r.statistic - 5.563_714_337_9).abs() < 1e-8,
2994 "JB = {}",
2995 r.statistic
2996 );
2997
2998 let n = data.len() as f64;
3001 let big_g1 = u_numflow::stats::skewness(&data).expect("same data");
3002 let big_g2 = u_numflow::stats::kurtosis(&data).expect("same data");
3003 let adjusted = (n / 6.0) * (big_g1 * big_g1 + big_g2 * big_g2 / 4.0);
3004 assert!(
3005 adjusted > r.statistic * 1.3,
3006 "the two forms differ by more than a third here: {adjusted} vs {}",
3007 r.statistic
3008 );
3009 }
3010
3011 #[test]
3015 fn jb_p_value_keeps_its_digits_in_the_tail() {
3016 let mut data: Vec<f64> = (1..20).map(f64::from).collect();
3019 data.push(60.0);
3020 let r = jarque_bera_test(&data).expect("20 finite points");
3021 assert!(r.statistic > 90.0, "JB = {}", r.statistic);
3022 assert!(
3023 1.0 - special::chi_squared_cdf(r.statistic, 2.0) == 0.0,
3024 "this sample is only interesting because the complement underflows"
3025 );
3026 assert!(
3027 r.p_value > 0.0,
3028 "the complement returns 0 here; this must not"
3029 );
3030 assert!(
3031 (r.p_value - 6.702e-21).abs() < 1e-23,
3032 "p = {} (expected about 6.7e-21)",
3033 r.p_value
3034 );
3035 assert!(
3036 (r.p_value - (-r.statistic / 2.0).exp()).abs() <= f64::EPSILON * r.p_value,
3037 "p = {} vs exp(-JB/2) = {}",
3038 r.p_value,
3039 (-r.statistic / 2.0).exp()
3040 );
3041 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])
3044 .expect("10 finite points");
3045 let via_cdf = 1.0 - special::chi_squared_cdf(small.statistic, 2.0);
3046 assert!(
3047 (small.p_value - via_cdf).abs() < 1e-12,
3048 "{} vs {via_cdf}",
3049 small.p_value
3050 );
3051 }
3052
3053 #[test]
3054 fn jb_edge_cases() {
3055 assert!(jarque_bera_test(&[1.0, 2.0, 3.0, 4.0]).is_none()); }
3057
3058 #[test]
3063 fn ad_normal_data() {
3064 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3065 let r = anderson_darling_test(&data).expect("should compute");
3066 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3067 assert!(r.statistic > 0.0, "A2 = {}", r.statistic);
3068 assert!(r.statistic_star > r.statistic, "A*2 should be > A2");
3069 }
3070
3071 #[test]
3072 fn ad_skewed_data() {
3073 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];
3075 let r = anderson_darling_test(&data).expect("should compute");
3076 assert!(
3077 r.p_value < 0.05,
3078 "p = {} (should reject normality)",
3079 r.p_value
3080 );
3081 }
3082
3083 #[test]
3084 fn ad_bimodal_data() {
3085 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3087 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3088 let r = anderson_darling_test(&data).expect("should compute");
3089 assert!(
3090 r.p_value < 0.01,
3091 "p = {} (bimodal should reject normality)",
3092 r.p_value
3093 );
3094 }
3095
3096 #[test]
3097 fn ad_large_normal_sample() {
3098 let n = 100;
3099 let data: Vec<f64> = (1..=n)
3100 .map(|i| {
3101 let p = (i as f64 - 0.5) / n as f64;
3102 special::inverse_normal_cdf(p)
3103 })
3104 .collect();
3105 let r = anderson_darling_test(&data).expect("should compute");
3106 assert!(r.p_value > 0.05, "p = {} for normal quantiles", r.p_value);
3107 }
3108
3109 #[test]
3110 fn ad_edge_cases() {
3111 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());
3114 }
3115
3116 #[test]
3117 fn ad_p_value_ranges() {
3118 let near_normal = [-1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5];
3123 let r = anderson_darling_test(&near_normal).expect("should compute");
3124 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3125
3126 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];
3128 let r = anderson_darling_test(&heavy_tail).expect("should compute");
3129 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3130 }
3131
3132 #[test]
3137 fn ad_normal_data_large_p() {
3138 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3140 let r = anderson_darling_test(&data).unwrap();
3141 assert!(r.p_value > 0.05, "p={}", r.p_value);
3142 }
3143
3144 #[test]
3145 fn ad_exponential_data_small_p() {
3146 let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3148 let r = anderson_darling_test(&data).unwrap();
3149 assert!(r.p_value < 0.05, "p={}", r.p_value);
3150 }
3151
3152 #[test]
3153 fn ad_statistic_non_negative() {
3154 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3155 let r = anderson_darling_test(&data).unwrap();
3156 assert!(r.statistic >= 0.0);
3157 assert!(r.statistic_star >= r.statistic);
3158 }
3159
3160 #[test]
3161 fn ad_p_value_in_range() {
3162 let data = [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.0];
3163 let r = anderson_darling_test(&data).unwrap();
3164 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3165 }
3166
3167 #[test]
3168 fn ad_insufficient_data() {
3169 assert!(anderson_darling_test(&[1.0, 2.0]).is_none()); }
3171
3172 #[test]
3173 fn ad_degenerate_data() {
3174 assert!(anderson_darling_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3176 }
3177
3178 #[test]
3179 fn ad_modified_statistic_formula() {
3180 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
3182 let r = anderson_darling_test(&data).unwrap();
3183 assert!(r.statistic_star > r.statistic - 1e-10);
3184 assert!(anderson_darling_test(&data[..7]).is_none());
3186 }
3187
3188 #[test]
3193 fn ad_correction_factor_formula() {
3194 let data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3195 let n = data.len() as f64;
3196 let r = anderson_darling_test(&data).unwrap();
3197
3198 let expected_star = r.statistic * (1.0 + 0.75 / n + 2.25 / (n * n));
3200 assert!(
3201 (r.statistic_star - expected_star).abs() < 1e-12,
3202 "A²* = {}, expected = {}",
3203 r.statistic_star,
3204 expected_star
3205 );
3206
3207 assert!(
3209 r.statistic >= 0.0,
3210 "A² = {} must be non-negative",
3211 r.statistic
3212 );
3213 }
3214
3215 #[test]
3221 fn ad_formula_structure_symmetric_vs_skewed() {
3222 let symmetric = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 0.0];
3224 let skewed: Vec<f64> = (1..=10).map(|i| (i as f64 * 0.5).exp()).collect();
3226
3227 let r_sym = anderson_darling_test(&symmetric).unwrap();
3228 let r_skew = anderson_darling_test(&skewed).unwrap();
3229
3230 assert!(
3231 r_sym.statistic < r_skew.statistic,
3232 "Symmetric A²={} should be less than skewed A²={}",
3233 r_sym.statistic,
3234 r_skew.statistic
3235 );
3236 }
3237
3238 #[test]
3243 fn ad_pvalue_invariants() {
3244 let normal_data = [2.1, 1.9, 2.0, 2.05, 1.95, 2.02, 1.98, 2.01, 2.03, 1.97];
3246 let r_normal = anderson_darling_test(&normal_data).unwrap();
3247 assert!(
3248 r_normal.p_value > 0.05,
3249 "Normal data: p = {} (expected > 0.05)",
3250 r_normal.p_value
3251 );
3252
3253 let exp_data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.3).exp()).collect();
3255 let r_exp = anderson_darling_test(&exp_data).unwrap();
3256 assert!(
3257 r_exp.p_value < 0.05,
3258 "Exponential data: p = {} (expected < 0.05)",
3259 r_exp.p_value
3260 );
3261 }
3262
3263 #[test]
3268 fn sw_normal_data() {
3269 let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0];
3271 let r = shapiro_wilk_test(&data).expect("should compute");
3272 assert!(r.w > 0.9, "W = {}", r.w);
3273 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3274 }
3275
3276 #[test]
3277 fn sw_bimodal_data() {
3278 let mut data = vec![0.0, 0.1, 0.2, 0.3, 0.4, 0.5];
3280 data.extend_from_slice(&[9.5, 9.6, 9.7, 9.8, 9.9, 10.0]);
3281 let r = shapiro_wilk_test(&data).expect("should compute");
3282 assert!(
3283 r.p_value < 0.01,
3284 "p = {} (bimodal should reject normality)",
3285 r.p_value
3286 );
3287 }
3288
3289 #[test]
3290 fn sw_n3() {
3291 let data = [1.0, 2.0, 3.0];
3292 let r = shapiro_wilk_test(&data).expect("n=3 should work");
3293 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3294 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3295 }
3296
3297 #[test]
3298 fn sw_n4() {
3299 let data = [1.0, 2.0, 3.0, 4.0];
3300 let r = shapiro_wilk_test(&data).expect("n=4 should work");
3301 assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
3302 assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
3303 }
3304
3305 #[test]
3306 fn sw_n5() {
3307 let data = [-1.0, -0.5, 0.0, 0.5, 1.0];
3308 let r = shapiro_wilk_test(&data).expect("n=5 should work");
3309 assert!(r.w > 0.9, "W = {}", r.w);
3310 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3311 }
3312
3313 #[test]
3314 fn sw_skewed_data() {
3315 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];
3317 let r = shapiro_wilk_test(&data).expect("should compute");
3318 assert!(
3319 r.p_value < 0.05,
3320 "p = {} (skewed data should reject normality)",
3321 r.p_value
3322 );
3323 }
3324
3325 #[test]
3326 fn sw_large_normal_sample() {
3327 let n = 100;
3330 let data: Vec<f64> = (1..=n)
3331 .map(|i| {
3332 let p = (i as f64 - 0.5) / n as f64;
3333 special::inverse_normal_cdf(p)
3334 })
3335 .collect();
3336 let r = shapiro_wilk_test(&data).expect("should compute");
3337 assert!(r.w > 0.99, "W = {} for normal quantiles", r.w);
3338 assert!(r.p_value > 0.05, "p = {}", r.p_value);
3339 }
3340
3341 #[test]
3342 fn sw_w_bounded() {
3343 let datasets: Vec<Vec<f64>> = vec![
3345 vec![1.0, 2.0, 3.0],
3346 vec![1.0, 1.0, 2.0, 3.0, 3.0],
3347 vec![0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0],
3348 (0..20).map(|i| (i as f64).powi(2)).collect(),
3349 ];
3350 for (idx, data) in datasets.iter().enumerate() {
3351 let r = shapiro_wilk_test(data).unwrap_or_else(|| panic!("dataset {idx} should work"));
3352 assert!(r.w > 0.0 && r.w <= 1.0, "dataset {idx}: W = {}", r.w);
3353 assert!(
3354 r.p_value >= 0.0 && r.p_value <= 1.0,
3355 "dataset {idx}: p = {}",
3356 r.p_value
3357 );
3358 }
3359 }
3360
3361 #[test]
3362 fn sw_edge_cases() {
3363 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()); }
3369
3370 #[test]
3371 fn sw_n5001_rejected() {
3372 let data: Vec<f64> = (0..5001).map(|i| i as f64).collect();
3373 assert!(shapiro_wilk_test(&data).is_none()); }
3375
3376 #[test]
3381 fn mw_clearly_different() {
3382 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
3383 let b = [6.0, 7.0, 8.0, 9.0, 10.0];
3384 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3385 assert!(r.p_value < 0.05, "p = {}", r.p_value);
3386 }
3387
3388 #[test]
3389 fn mw_same_distribution() {
3390 let a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0];
3391 let b = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0];
3392 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3393 assert!(
3394 r.p_value > 0.3,
3395 "p = {} (interleaved, same dist)",
3396 r.p_value
3397 );
3398 }
3399
3400 #[test]
3401 fn mw_with_ties() {
3402 let a = [1.0, 2.0, 2.0, 3.0, 3.0];
3403 let b = [3.0, 4.0, 4.0, 5.0, 5.0];
3404 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3405 assert!(r.p_value < 0.05, "p = {} (shifted with ties)", r.p_value);
3406 }
3407
3408 #[test]
3409 fn mw_different_sizes() {
3410 let a = [1.0, 2.0, 3.0];
3411 let b = [4.0, 5.0, 6.0, 7.0, 8.0];
3412 let r = mann_whitney_u_test(&a, &b).expect("should compute");
3413 assert!(r.p_value < 0.05);
3414 }
3415
3416 #[test]
3417 fn mw_edge_cases() {
3418 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());
3421 }
3422
3423 #[test]
3428 fn wilcoxon_significant_increase() {
3429 let before = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
3430 let after = [6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5];
3431 let r = wilcoxon_signed_rank_test(&before, &after).expect("should compute");
3432 assert!(r.p_value < 0.05, "p = {} (consistent increase)", r.p_value);
3433 }
3434
3435 #[test]
3436 fn wilcoxon_no_difference() {
3437 let x = [5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3438 let y = [5.1, 5.9, 7.1, 7.9, 9.1, 9.9];
3439 let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3440 assert!(r.p_value > 0.3, "p = {} (small random diffs)", r.p_value);
3441 }
3442
3443 #[test]
3444 fn wilcoxon_with_ties() {
3445 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3447 let y = [0.0, 1.0, 2.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3449 assert!(r.statistic > 0.0);
3451 }
3452
3453 #[test]
3454 fn wilcoxon_with_zero_diffs() {
3455 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
3457 let y = [1.0, 2.0, 3.0, 3.0, 4.0]; let r = wilcoxon_signed_rank_test(&x, &y).expect("should compute");
3459 assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
3460 }
3461
3462 #[test]
3463 fn wilcoxon_edge_cases() {
3464 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());
3468 }
3469
3470 #[test]
3475 fn mann_whitney_small_samples_use_the_exact_distribution() {
3476 let cases = [
3478 (3, 3, 0, 0.1000, 0.0495),
3479 (4, 4, 0, 0.0286, 0.0209),
3480 (4, 4, 1, 0.0571, 0.0433),
3481 (5, 5, 2, 0.0317, 0.0283),
3482 (8, 8, 13, 0.0499, 0.0460),
3483 (10, 10, 23, 0.0433, 0.0413),
3484 ];
3485 for (n1, n2, u, exact, old) in cases {
3486 let (a, b) = separated_samples(n1, n2, u);
3487 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3488 assert_eq!(r.statistic, u as f64);
3489 assert!(
3490 (r.p_value - exact).abs() < 1e-4,
3491 "n={n1},{n2} U={u}: p={} want {exact} (old {old})",
3492 r.p_value
3493 );
3494 }
3495 }
3496
3497 fn separated_samples(n1: usize, n2: usize, u: usize) -> (Vec<f64>, Vec<f64>) {
3501 let mut ranks_a: Vec<usize> = (1..=n1).collect();
3502 let mut remaining = u;
3503 for k in (0..n1).rev() {
3504 let room = n2.min(remaining);
3505 ranks_a[k] += room;
3506 remaining -= room;
3507 if remaining == 0 {
3508 break;
3509 }
3510 }
3511 let ranks_b: Vec<usize> = (1..=n1 + n2).filter(|r| !ranks_a.contains(r)).collect();
3512 (
3513 ranks_a.iter().map(|&r| r as f64).collect(),
3514 ranks_b.iter().map(|&r| r as f64).collect(),
3515 )
3516 }
3517
3518 #[test]
3520 fn wilcoxon_small_samples_use_the_exact_distribution() {
3521 let cases = [
3523 (5, 0, 0.0625),
3524 (5, 1, 0.125),
3525 (6, 2, 0.09375),
3526 (8, 5, 0.078125),
3527 (10, 8, 0.048828125),
3528 (12, 14, 0.0522460938),
3529 ];
3530 for (n, t_plus, exact) in cases {
3531 let positive = ranks_summing_to(n, t_plus);
3533 let x: Vec<f64> = (1..=n)
3534 .map(|r| {
3535 if positive.contains(&r) {
3536 r as f64
3537 } else {
3538 -(r as f64)
3539 }
3540 })
3541 .collect();
3542 let y = vec![0.0; n];
3543 let r = wilcoxon_signed_rank_test(&x, &y).expect("valid samples");
3544 assert_eq!(r.statistic, t_plus as f64);
3545 assert!(
3546 (r.p_value - exact).abs() < 1e-9,
3547 "n={n} T+={t_plus}: p={} want {exact}",
3548 r.p_value
3549 );
3550 }
3551 }
3552
3553 fn ranks_summing_to(n: usize, target: usize) -> Vec<usize> {
3555 let mut out = Vec::new();
3556 let mut left = target;
3557 for r in (1..=n).rev() {
3558 if r <= left {
3559 out.push(r);
3560 left -= r;
3561 }
3562 }
3563 assert_eq!(left, 0);
3564 out
3565 }
3566
3567 #[test]
3570 fn rank_tests_fall_back_to_continuity_corrected_normal() {
3571 let a: Vec<f64> = (0..60).map(|i| i as f64).collect();
3572 let b: Vec<f64> = (0..60).map(|i| i as f64 + 20.5).collect();
3573 let r = mann_whitney_u_test(&a, &b).expect("valid samples");
3574 let (n1, n2) = (60.0, 60.0);
3575 let mu = n1 * n2 / 2.0;
3576 let sigma = (n1 * n2 * (n1 + n2 + 1.0) / 12.0_f64).sqrt();
3577 let z = ((r.statistic - mu).abs() - 0.5) / sigma;
3578 let want = 2.0 * special::standard_normal_sf(z);
3579 assert!((r.p_value - want).abs() < 1e-12);
3580
3581 let a: Vec<f64> = (0..50).map(|i| i as f64).collect();
3583 let b: Vec<f64> = (0..50).map(|i| i as f64 + 10.5).collect();
3584 let exact = mann_whitney_u_test(&a, &b).expect("valid samples").p_value;
3585 let mu = 1250.0;
3586 let sigma = (2500.0 * 101.0 / 12.0_f64).sqrt();
3587 let u = mann_whitney_u_test(&a, &b)
3588 .expect("valid samples")
3589 .statistic;
3590 let approx = continuity_corrected_p(u, mu, sigma * sigma);
3591 assert!(
3592 (exact - approx).abs() < 1e-3,
3593 "exact {exact} vs approx {approx}"
3594 );
3595 }
3596
3597 #[test]
3602 fn kw_clearly_different() {
3603 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3604 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3605 let g3 = [11.0, 12.0, 13.0, 14.0, 15.0];
3606 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3607 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3608 assert!((r.df - 2.0).abs() < 1e-10);
3609 }
3610
3611 #[test]
3612 fn kw_same_distribution() {
3613 let g1 = [1.0, 3.0, 5.0, 7.0, 9.0];
3614 let g2 = [2.0, 4.0, 6.0, 8.0, 10.0];
3615 let r = kruskal_wallis_test(&[&g1, &g2]).expect("should compute");
3616 assert!(r.p_value > 0.3, "p = {} (interleaved)", r.p_value);
3617 }
3618
3619 #[test]
3620 fn kw_with_ties() {
3621 let g1 = [1.0, 2.0, 2.0, 3.0];
3622 let g2 = [3.0, 4.0, 4.0, 5.0];
3623 let g3 = [5.0, 6.0, 6.0, 7.0];
3624 let r = kruskal_wallis_test(&[&g1, &g2, &g3]).expect("should compute");
3625 assert!(r.statistic > 0.0);
3626 }
3627
3628 #[test]
3629 fn kw_edge_cases() {
3630 let g1 = [1.0, 2.0, 3.0];
3631 assert!(kruskal_wallis_test(&[&g1]).is_none()); }
3633
3634 #[test]
3639 fn levene_equal_variance() {
3640 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3641 let g2 = [6.0, 7.0, 8.0, 9.0, 10.0];
3642 let r = levene_test(&[&g1, &g2]).expect("should compute");
3643 assert!(r.p_value > 0.3, "p = {} (equal variance)", r.p_value);
3645 }
3646
3647 #[test]
3648 fn levene_unequal_variance() {
3649 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");
3652 assert!(r.p_value < 0.05, "p = {} (unequal variance)", r.p_value);
3653 }
3654
3655 #[test]
3656 fn levene_three_groups() {
3657 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3658 let g2 = [0.0, 3.0, 5.0, 7.0, 10.0];
3659 let g3 = [-5.0, 0.0, 5.0, 10.0, 15.0];
3660 let r = levene_test(&[&g1, &g2, &g3]).expect("should compute");
3661 assert!(r.df >= 2.0);
3662 }
3663
3664 #[test]
3665 fn levene_edge_cases() {
3666 let g1 = [1.0, 2.0, 3.0];
3667 assert!(levene_test(&[&g1]).is_none()); }
3669
3670 #[test]
3675 fn bonferroni_basic() {
3676 let ps = [0.01, 0.04, 0.03, 0.005];
3677 let adj = bonferroni_correction(&ps).expect("should compute");
3678 assert!((adj[0] - 0.04).abs() < 1e-10);
3679 assert!((adj[1] - 0.16).abs() < 1e-10);
3680 assert!((adj[2] - 0.12).abs() < 1e-10);
3681 assert!((adj[3] - 0.02).abs() < 1e-10);
3682 }
3683
3684 #[test]
3685 fn bonferroni_capped_at_one() {
3686 let ps = [0.5, 0.6];
3687 let adj = bonferroni_correction(&ps).expect("should compute");
3688 assert!((adj[0] - 1.0).abs() < 1e-10);
3689 assert!((adj[1] - 1.0).abs() < 1e-10);
3690 }
3691
3692 #[test]
3693 fn bh_basic() {
3694 let ps = [0.01, 0.04, 0.03, 0.005];
3695 let adj = benjamini_hochberg(&ps).expect("should compute");
3696 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
3698 assert!(
3699 adjusted >= orig - 1e-15,
3700 "adj[{i}] = {adjusted} < original {orig}"
3701 );
3702 }
3703 }
3706
3707 #[test]
3708 fn bh_all_significant() {
3709 let ps = [0.001, 0.002, 0.003];
3710 let adj = benjamini_hochberg(&ps).expect("should compute");
3711 for &a in &adj {
3712 assert!(a < 0.05);
3713 }
3714 }
3715
3716 #[test]
3717 fn correction_edge_cases() {
3718 assert!(bonferroni_correction(&[]).is_none());
3719 assert!(benjamini_hochberg(&[]).is_none());
3720 assert!(bonferroni_correction(&[f64::NAN]).is_none());
3721 }
3722
3723 #[test]
3728 fn bartlett_equal_variances() {
3729 let g1 = [2.0, 3.0, 4.0, 5.0, 6.0];
3730 let g2 = [12.0, 13.0, 14.0, 15.0, 16.0];
3731 let r = bartlett_test(&[&g1, &g2]).expect("should compute");
3732 assert!(
3733 r.p_value > 0.9,
3734 "equal variance → p high, got {}",
3735 r.p_value
3736 );
3737 }
3738
3739 #[test]
3740 fn bartlett_unequal_variances() {
3741 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");
3744 assert!(
3745 r.p_value < 0.01,
3746 "very different variances → p < 0.01, got {}",
3747 r.p_value
3748 );
3749 assert!((r.df - 1.0).abs() < 1e-10); }
3751
3752 #[test]
3753 fn bartlett_three_groups() {
3754 let g1 = [1.0, 2.0, 3.0, 4.0, 5.0];
3755 let g2 = [1.5, 2.5, 3.5, 4.5, 5.5];
3756 let g3 = [10.0, 30.0, 50.0, 70.0, 90.0]; let r = bartlett_test(&[&g1, &g2, &g3]).expect("should compute");
3758 assert!(r.p_value < 0.05, "one group with high variance");
3759 assert!((r.df - 2.0).abs() < 1e-10); }
3761
3762 #[test]
3763 fn bartlett_edge_cases() {
3764 let g1 = [1.0, 2.0, 3.0];
3765 assert!(bartlett_test(&[&g1]).is_none()); let g2 = [5.0, 5.0, 5.0]; assert!(bartlett_test(&[&g1, &g2]).is_none());
3769
3770 let g3 = [1.0]; assert!(bartlett_test(&[&g1, &g3]).is_none());
3772 }
3773
3774 #[test]
3779 fn fisher_tea_tasting() {
3780 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3782 assert!(
3784 (r.p_value - 0.4857).abs() < 0.01,
3785 "p ≈ 0.4857, got {}",
3786 r.p_value
3787 );
3788 }
3789
3790 #[test]
3791 fn fisher_significant() {
3792 let r = fisher_exact_test(10, 0, 0, 10).expect("should compute");
3794 assert!(r.p_value < 0.001, "perfect association → p very small");
3795 }
3796
3797 #[test]
3798 fn fisher_no_association() {
3799 let r = fisher_exact_test(5, 5, 5, 5).expect("should compute");
3801 assert!(
3802 r.p_value > 0.9,
3803 "no association → p ≈ 1.0, got {}",
3804 r.p_value
3805 );
3806 }
3807
3808 #[test]
3809 fn fisher_small_table() {
3810 let r = fisher_exact_test(1, 0, 0, 1).expect("should compute");
3812 assert!(r.p_value > 0.0 && r.p_value <= 1.0);
3813 }
3814
3815 #[test]
3816 fn fisher_asymmetric() {
3817 let r = fisher_exact_test(8, 2, 1, 5).expect("should compute");
3819 assert!(r.p_value < 0.05, "significant association");
3820 }
3821
3822 #[test]
3823 fn fisher_edge_cases() {
3824 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()); }
3829
3830 #[test]
3831 fn fisher_odds_ratio() {
3832 let r = fisher_exact_test(3, 1, 1, 3).expect("should compute");
3833 assert!(
3835 (r.statistic - 9.0).abs() < 1e-10,
3836 "OR = 9, got {}",
3837 r.statistic
3838 );
3839 }
3840
3841 #[test]
3846 fn mk_increasing_trend() {
3847 let data = [1.0, 2.3, 3.1, 4.5, 5.2, 6.8, 7.1, 8.9, 9.5, 10.2];
3848 let r = mann_kendall_test(&data).expect("should compute");
3849 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3850 assert!(r.kendall_tau > 0.8, "tau = {}", r.kendall_tau);
3851 assert!(r.sen_slope > 0.0, "slope = {}", r.sen_slope);
3852 assert!(r.s_statistic > 0);
3853 }
3854
3855 #[test]
3856 fn mk_decreasing_trend() {
3857 let data = [10.0, 9.2, 8.5, 7.1, 6.3, 5.0, 4.2, 3.1, 2.0, 1.1];
3858 let r = mann_kendall_test(&data).expect("should compute");
3859 assert!(r.p_value < 0.01, "p = {}", r.p_value);
3860 assert!(r.kendall_tau < -0.8, "tau = {}", r.kendall_tau);
3861 assert!(r.sen_slope < 0.0, "slope = {}", r.sen_slope);
3862 assert!(r.s_statistic < 0);
3863 }
3864
3865 #[test]
3866 fn mk_no_trend() {
3867 let data = [5.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 1.0, 9.0, 5.0];
3869 let r = mann_kendall_test(&data).expect("should compute");
3870 assert!(
3872 r.p_value > 0.05,
3873 "p = {} (should be > 0.05 for no trend)",
3874 r.p_value
3875 );
3876 }
3877
3878 #[test]
3879 fn mk_perfect_monotone() {
3880 let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
3881 let r = mann_kendall_test(&data).expect("should compute");
3882 assert_eq!(r.s_statistic, 45);
3884 assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3885 assert!((r.sen_slope - 1.0).abs() < 1e-10);
3886 }
3887
3888 #[test]
3889 fn mk_with_ties() {
3890 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0];
3891 let r = mann_kendall_test(&data).expect("should compute");
3892 assert!(r.s_statistic > 0);
3893 let n = data.len() as f64;
3895 let base_var = n * (n - 1.0) * (2.0 * n + 5.0) / 18.0;
3896 assert!(r.variance < base_var, "ties should reduce variance");
3897 }
3898
3899 #[test]
3903 fn mk_does_not_depend_on_the_unit() {
3904 let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 5.0, 4.5, 6.0, 7.0, 6.5];
3905 let base = mann_kendall_test(&data).expect("computes");
3906 for scale in [1e-15, 1e-12, 1e-6, 1e6, 1e12] {
3907 let scaled: Vec<f64> = data.iter().map(|v| v * scale).collect();
3908 let r = mann_kendall_test(&scaled)
3909 .unwrap_or_else(|| panic!("scale {scale:e}: the series has variation"));
3910 assert_eq!(r.s_statistic, base.s_statistic, "scale {scale:e}");
3911 assert!(
3912 (r.variance - base.variance).abs() <= 1e-12 * base.variance,
3913 "scale {scale:e}: variance {} vs {}",
3914 r.variance,
3915 base.variance
3916 );
3917 assert!(
3918 (r.p_value - base.p_value).abs() <= 1e-12,
3919 "scale {scale:e}: p {} vs {}",
3920 r.p_value,
3921 base.p_value
3922 );
3923 assert!(
3924 (r.sen_slope / scale - base.sen_slope).abs() <= 1e-9,
3925 "scale {scale:e}: slope"
3926 );
3927 }
3928 }
3929
3930 #[test]
3933 fn rank_tests_do_not_depend_on_the_unit() {
3934 let a = [1.0, 2.0, 2.0, 3.5, 4.0, 4.0, 5.5, 6.0];
3935 let b = [2.0, 3.0, 3.5, 5.0, 5.5, 7.0, 7.5, 8.0, 8.0];
3936 let c = [0.5, 1.5, 2.0, 2.5, 3.0];
3937 let paired_x = [1.5, 2.5, 3.0, 4.5, 5.0, 6.5, 7.0, 8.5];
3938 let paired_y = [1.0, 2.0, 3.5, 4.0, 5.5, 6.0, 7.5, 8.0];
3939 let scaled = |v: &[f64], k: f64| -> Vec<f64> { v.iter().map(|x| x * k).collect() };
3940 let mw = mann_whitney_u_test(&a, &b).expect("computes");
3941 let wx = wilcoxon_signed_rank_test(&paired_x, &paired_y).expect("computes");
3942 let kw = kruskal_wallis_test(&[&a, &b, &c]).expect("computes");
3943 for k in [1e-15, 1e-12, 1e-6, 1e6, 1e12] {
3944 let (sa, sb, sc) = (scaled(&a, k), scaled(&b, k), scaled(&c, k));
3945 let got = mann_whitney_u_test(&sa, &sb).expect("computes");
3946 assert!(
3947 (got.statistic - mw.statistic).abs() < 1e-12,
3948 "mann-whitney U at {k:e}"
3949 );
3950 assert!(
3951 (got.p_value - mw.p_value).abs() < 1e-12,
3952 "mann-whitney p at {k:e}"
3953 );
3954 let got = wilcoxon_signed_rank_test(&scaled(&paired_x, k), &scaled(&paired_y, k))
3955 .expect("computes");
3956 assert!(
3957 (got.statistic - wx.statistic).abs() < 1e-12,
3958 "wilcoxon T at {k:e}"
3959 );
3960 assert!(
3961 (got.p_value - wx.p_value).abs() < 1e-12,
3962 "wilcoxon p at {k:e}"
3963 );
3964 let got = kruskal_wallis_test(&[&sa, &sb, &sc]).expect("computes");
3965 assert!(
3966 (got.statistic - kw.statistic).abs() < 1e-12,
3967 "kruskal H at {k:e}"
3968 );
3969 assert!(
3970 (got.p_value - kw.p_value).abs() < 1e-12,
3971 "kruskal p at {k:e}"
3972 );
3973 }
3974 }
3975
3976 #[test]
3977 fn mk_edge_cases() {
3978 assert!(mann_kendall_test(&[1.0, 2.0, 3.0]).is_none());
3980 assert!(mann_kendall_test(&[1.0, f64::NAN, 3.0, 4.0]).is_none());
3982 assert!(mann_kendall_test(&[5.0, 5.0, 5.0, 5.0]).is_none());
3984 }
3985
3986 #[test]
3987 fn mk_minimum_n() {
3988 let data = [1.0, 2.0, 3.0, 4.0];
3990 let r = mann_kendall_test(&data).expect("n=4 should work");
3991 assert_eq!(r.s_statistic, 6); assert!((r.kendall_tau - 1.0).abs() < 1e-10);
3993 }
3994
3995 #[test]
3996 fn mk_sen_slope_robust_to_outlier() {
3997 let data = [1.0, 2.0, 3.0, 100.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3999 let r = mann_kendall_test(&data).expect("should compute");
4000 assert!(
4002 (r.sen_slope - 1.0).abs() < 0.5,
4003 "sen slope = {}, expected ≈ 1.0",
4004 r.sen_slope
4005 );
4006 }
4007
4008 #[test]
4013 fn adf_stationary_mean_reverting() {
4014 let data = [
4017 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,
4018 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,
4019 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,
4020 ];
4021 let r = adf_test(&data, AdfModel::Constant, AdfLags::default()).expect("should compute");
4022 assert!(
4024 r.rejected[2],
4025 "should reject at 10%: stat={}, cv={}",
4026 r.statistic, r.critical_values[2]
4027 );
4028 }
4029
4030 #[test]
4031 fn adf_nonstationary_random_walk() {
4032 let increments = [
4034 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,
4035 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,
4036 -0.13, 0.15, -0.1,
4037 ];
4038 let mut walk = Vec::with_capacity(increments.len());
4039 let mut cum = 0.0;
4040 for &inc in &increments {
4041 cum += inc;
4042 walk.push(cum);
4043 }
4044 let r = adf_test(&walk, AdfModel::Constant, AdfLags::default()).expect("should compute");
4045 assert!(
4047 !r.rejected[0],
4048 "should NOT reject at 1%: stat={}, cv={}",
4049 r.statistic, r.critical_values[0]
4050 );
4051 }
4052
4053 #[test]
4054 fn adf_with_fixed_lags() {
4055 let mut rng = mulberry32(0xadf2);
4059 let data: Vec<f64> = (0..50)
4060 .map(|i| (i as f64 * 0.5).sin() + 0.02 * i as f64 + 0.1 * (rng() - 0.5))
4061 .collect();
4062 let r = adf_test(&data, AdfModel::Constant, AdfLags::Fixed(2)).expect("should compute");
4063 assert_eq!(r.n_lags, 2);
4064 assert!(r.statistic.is_finite());
4065 }
4066
4067 #[test]
4068 fn adf_constant_trend_model() {
4069 let data: Vec<f64> = (0..30)
4072 .map(|i| i as f64 + (i as f64 * 0.3).sin() * 0.5)
4073 .collect();
4074 let r =
4075 adf_test(&data, AdfModel::ConstantTrend, AdfLags::default()).expect("should compute");
4076 assert!(r.statistic.is_finite());
4077 assert_eq!(r.critical_values.len(), 3);
4078 }
4079
4080 #[test]
4081 fn adf_edge_cases() {
4082 assert_eq!(
4083 adf_test(&[1.0; 9], AdfModel::Constant, AdfLags::default()).unwrap_err(),
4084 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
4085 );
4086 let mut data = vec![0.0; 20];
4087 data[5] = f64::NAN;
4088 assert_eq!(
4089 adf_test(&data, AdfModel::Constant, AdfLags::default()).unwrap_err(),
4090 TimeSeriesError::ValueNotFinite { index: 5 }
4091 );
4092 let walk: Vec<f64> = (0..20).map(|i| ((i * 37) % 11) as f64).collect();
4094 assert_eq!(adf_lags_domain(&walk, AdfModel::Constant).unwrap(), 0..=8);
4095 assert_eq!(
4096 adf_lags_domain(&walk, AdfModel::ConstantTrend).unwrap(),
4097 0..=7
4098 );
4099 assert_eq!(
4100 adf_lags_domain(&walk[..9], AdfModel::Constant).unwrap_err(),
4101 TimeSeriesError::TooFewObservations { needed: 10, got: 9 }
4102 );
4103 for lags in [AdfLags::Fixed(9), AdfLags::Aic { max: Some(9) }] {
4104 let option = if matches!(lags, AdfLags::Fixed(_)) {
4105 "lags"
4106 } else {
4107 "max_lags"
4108 };
4109 assert_eq!(
4110 adf_test(&walk, AdfModel::Constant, lags).unwrap_err(),
4111 TimeSeriesError::OptionOutOfRange {
4112 option,
4113 got: 9.0,
4114 min: 0.0,
4115 max: 8.0
4116 }
4117 );
4118 }
4119 assert_eq!(
4120 adf_test(&[3.0; 20], AdfModel::Constant, AdfLags::default()).unwrap_err(),
4121 TimeSeriesError::DegenerateRegression
4122 );
4123 }
4124
4125 #[test]
4131 fn adf_exact_fit_is_undefined_and_not_chosen() {
4132 let shocks = [0.5, -0.8, 0.3, -0.6, 0.9, -0.4, 0.7, -0.2, 0.1, -0.5];
4133 let mut walk = vec![0.0_f64; 60];
4134 for i in 1..walk.len() {
4135 walk[i] = walk[i - 1] + shocks[(i * 7) % 10];
4136 }
4137 assert_eq!(
4138 adf_test(&walk, AdfModel::ConstantTrend, AdfLags::Fixed(9)).unwrap_err(),
4139 TimeSeriesError::DegenerateRegression
4140 );
4141 let r = adf_test(&walk, AdfModel::ConstantTrend, AdfLags::default())
4142 .expect("a lag below 9 fits");
4143 assert!(r.n_lags < 9, "chose the exact fit: {} lags", r.n_lags);
4144 assert!(r.statistic.abs() < 1e3, "t = {}", r.statistic);
4145 }
4146
4147 #[test]
4151 fn adf_matches_statsmodels() {
4152 let mut x = vec![0.0_f64; 120];
4153 for t in 1..x.len() {
4154 let e = ((t * 7919) % 101) as f64 / 50.0 - 1.0;
4155 x[t] = 0.7 * x[t - 1] + e;
4156 }
4157 for (model, lags, stat, n_lags, n_obs) in [
4158 (
4159 AdfModel::Constant,
4160 AdfLags::Aic { max: Some(8) },
4161 -3.436_052_894_8,
4162 5,
4163 114,
4164 ),
4165 (
4166 AdfModel::Constant,
4167 AdfLags::Aic { max: None },
4168 -3.436_052_894_8,
4169 5,
4170 114,
4171 ),
4172 (
4173 AdfModel::ConstantTrend,
4174 AdfLags::Aic { max: Some(6) },
4175 -3.505_247_069_0,
4176 5,
4177 114,
4178 ),
4179 (
4180 AdfModel::None,
4181 AdfLags::Aic { max: Some(5) },
4182 -3.459_240_021_9,
4183 5,
4184 114,
4185 ),
4186 (
4187 AdfModel::Constant,
4188 AdfLags::Fixed(3),
4189 -4.652_930_484_4,
4190 3,
4191 116,
4192 ),
4193 ] {
4194 let r = adf_test(&x, model, lags).expect("fits");
4195 assert_eq!((r.n_lags, r.n_obs), (n_lags, n_obs), "{model:?} {lags:?}");
4196 assert!(
4197 (r.statistic - stat).abs() < 1e-8,
4198 "{model:?} {lags:?}: {}",
4199 r.statistic
4200 );
4201 }
4202 }
4203
4204 #[test]
4205 fn adf_critical_values_constant() {
4206 let cv = adf_critical_values(AdfModel::Constant, 100);
4208 assert!(cv[0] < -3.4 && cv[0] > -3.6, "1% cv = {}", cv[0]);
4210 assert!(cv[1] < -2.8 && cv[1] > -3.0, "5% cv = {}", cv[1]);
4211 assert!(cv[2] < -2.5 && cv[2] > -2.7, "10% cv = {}", cv[2]);
4212 }
4213
4214 #[test]
4215 fn adf_critical_values_ordering() {
4216 let cv = adf_critical_values(AdfModel::Constant, 50);
4217 assert!(cv[0] < cv[1], "1% ({}) should be < 5% ({})", cv[0], cv[1]);
4219 assert!(cv[1] < cv[2], "5% ({}) should be < 10% ({})", cv[1], cv[2]);
4220 }
4221}
4222
4223#[cfg(test)]
4226mod scipy_reference {
4227 use super::*;
4228
4229 const A: [f64; 8] = [5.1, 4.9, 5.6, 5.8, 6.0, 5.2, 4.7, 5.5];
4230 const B: [f64; 7] = [6.3, 5.9, 6.8, 7.1, 6.0, 6.6, 7.4];
4231 const G1: [f64; 5] = [23.1, 25.4, 22.8, 26.0, 24.3];
4232 const G2: [f64; 6] = [27.2, 28.9, 26.5, 29.4, 27.8, 28.1];
4233 const G3: [f64; 4] = [22.0, 21.4, 24.9, 23.3];
4234
4235 fn close(got: f64, want: f64, rel: f64, what: &str) {
4237 let scale = want.abs().max(1.0);
4238 assert!(
4239 (got - want).abs() <= rel * scale,
4240 "{what}: got {got}, SciPy {want} (relative tolerance {rel})"
4241 );
4242 }
4243
4244 #[test]
4245 fn t_tests_reproduce_scipy() {
4246 let r = one_sample_t_test(&A, 5.0).expect("valid");
4248 close(r.statistic, 2.197950389679656, 1e-12, "one-sample t");
4249 close(r.p_value, 0.06392412293886501, 1e-9, "one-sample p");
4250 assert_eq!(r.df, 7.0);
4251 let r = two_sample_t_test(&A, &B).expect("valid");
4253 close(r.statistic, -4.67567567567568, 1e-12, "Welch t");
4254 close(r.df, 11.56763357082458, 1e-12, "Welch df");
4255 close(r.p_value, 0.0005912510074711228, 1e-9, "Welch p");
4256 let x = [12.1, 13.4, 11.8, 14.2, 12.9, 13.0];
4258 let y = [12.9, 13.9, 12.0, 15.1, 13.5, 13.2];
4259 let r = paired_t_test(&x, &y).expect("valid");
4260 close(r.statistic, -4.437601569801824, 1e-12, "paired t");
4261 close(r.p_value, 0.006779965572779817, 1e-9, "paired p");
4262 assert_eq!(r.df, 5.0);
4263 }
4264
4265 #[test]
4266 fn anova_and_equal_variance_tests_reproduce_scipy() {
4267 let groups: [&[f64]; 3] = [&G1, &G2, &G3];
4268 let r = one_way_anova(&groups).expect("valid");
4270 close(r.f_statistic, 20.526701958197258, 1e-12, "ANOVA F");
4271 close(r.p_value, 0.00013390844134272023, 1e-9, "ANOVA p");
4272 let r = levene_test(&groups).expect("valid");
4274 close(r.statistic, 0.4752743705616534, 1e-12, "Brown-Forsythe W");
4275 close(r.p_value, 0.6329340379295331, 1e-9, "Brown-Forsythe p");
4276 let r = bartlett_test(&groups).expect("valid");
4278 close(r.statistic, 0.5282298427149905, 1e-12, "Bartlett T");
4279 close(r.p_value, 0.7678852881660961, 1e-9, "Bartlett p");
4280 }
4281
4282 #[test]
4283 fn kruskal_wallis_with_ties_reproduces_scipy() {
4284 let r = kruskal_wallis_test(&[
4286 &[1.0, 2.0, 2.0, 3.0, 5.0],
4287 &[4.0, 4.0, 6.0, 7.0],
4288 &[5.0, 8.0, 8.0, 9.0, 10.0],
4289 ])
4290 .expect("valid");
4291 close(r.statistic, 9.22394678492239, 1e-12, "H (tie-corrected)");
4292 close(r.p_value, 0.009932198829456881, 1e-9, "Kruskal-Wallis p");
4293 }
4294
4295 #[test]
4296 fn chi_squared_and_fisher_tests_reproduce_scipy() {
4297 let r = chi_squared_goodness_of_fit(&[18.0, 22.0, 30.0, 30.0], &[25.0; 4]).expect("valid");
4299 close(r.statistic, 4.32, 1e-12, "goodness-of-fit chi-squared");
4300 close(r.p_value, 0.22891886433610517, 1e-9, "goodness-of-fit p");
4301 let r = chi_squared_independence(&[12.0, 7.0, 9.0, 5.0, 14.0, 11.0], 2, 3).expect("valid");
4303 close(
4304 r.statistic,
4305 5.353085901027075,
4306 1e-12,
4307 "independence chi-squared",
4308 );
4309 close(r.p_value, 0.06880059054935043, 1e-9, "independence p");
4310 assert_eq!(r.df, 2.0);
4311 let r = fisher_exact_test(8, 2, 1, 5).expect("valid");
4313 close(r.p_value, 0.034965034965034975, 1e-9, "Fisher p");
4314 }
4315
4316 #[test]
4317 fn shapiro_wilk_reproduces_scipy() {
4318 let r = shapiro_wilk_test(&[
4320 148.0, 154.0, 158.0, 160.0, 161.0, 162.0, 166.0, 170.0, 182.0, 195.0, 236.0,
4321 ])
4322 .expect("valid");
4323 close(r.w, 0.7888146948631716, 1e-9, "W, skewed");
4324 close(r.p_value, 0.006703814061898823, 1e-9, "p, skewed");
4325 let r = shapiro_wilk_test(&[
4327 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,
4328 ])
4329 .expect("valid");
4330 close(r.w, 0.9864343014896592, 1e-9, "W, near normal");
4331 close(r.p_value, 0.9958703691266937, 1e-9, "p, near normal");
4332 }
4333
4334 #[test]
4335 fn mann_kendall_tau_and_sen_slope_reproduce_scipy() {
4336 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];
4339 let r = mann_kendall_test(&mk).expect("valid");
4340 close(r.kendall_tau, 0.8484848484848484, 1e-12, "Kendall tau");
4341 close(r.sen_slope, 0.22500000000000003, 1e-12, "Sen's slope");
4342 }
4343
4344 #[test]
4345 fn benjamini_hochberg_reproduces_scipy() {
4346 let adjusted = benjamini_hochberg(&[0.01, 0.04, 0.03, 0.005, 0.20, 0.50]).expect("valid");
4348 for (got, want) in adjusted.iter().zip([0.03, 0.06, 0.06, 0.03, 0.24, 0.5]) {
4349 close(*got, want, 1e-12, "BH-adjusted p");
4350 }
4351 }
4352}
4353
4354#[cfg(test)]
4355mod proptests {
4356 use super::*;
4357 use proptest::prelude::*;
4358
4359 proptest! {
4360 #[test]
4361 fn one_sample_p_bounded(
4362 data in proptest::collection::vec(-1e3_f64..1e3, 3..=30),
4363 mu0 in -1e3_f64..1e3
4364 ) {
4365 if let Some(r) = one_sample_t_test(&data, mu0) {
4366 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4367 }
4368 }
4369
4370 #[test]
4371 fn two_sample_p_bounded(
4372 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4373 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4374 ) {
4375 if let Some(r) = two_sample_t_test(&a, &b) {
4376 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4377 }
4378 }
4379
4380 #[test]
4381 fn anova_p_bounded(
4382 g1 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4383 g2 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4384 g3 in proptest::collection::vec(-1e3_f64..1e3, 3..=15),
4385 ) {
4386 if let Some(r) = one_way_anova(&[&g1, &g2, &g3]) {
4387 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4388 prop_assert!(r.f_statistic >= 0.0, "F = {}", r.f_statistic);
4389 }
4390 }
4391
4392 #[test]
4393 fn bonferroni_monotone(
4394 ps in proptest::collection::vec(0.001_f64..1.0, 2..=10)
4395 ) {
4396 let adj = bonferroni_correction(&ps).expect("should compute");
4397 for (i, (&orig, &adjusted)) in ps.iter().zip(adj.iter()).enumerate() {
4398 prop_assert!(adjusted >= orig - 1e-15,
4399 "adj[{i}] = {adjusted} < orig = {orig}");
4400 }
4401 }
4402
4403 #[test]
4404 fn shapiro_wilk_p_bounded(
4405 data in proptest::collection::vec(-1e3_f64..1e3, 3..=50)
4406 ) {
4407 if let Some(r) = shapiro_wilk_test(&data) {
4408 prop_assert!(r.w > 0.0 && r.w <= 1.0, "W = {}", r.w);
4409 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4410 }
4411 }
4412
4413 #[test]
4414 fn anderson_darling_p_bounded(
4415 data in proptest::collection::vec(-1e3_f64..1e3, 8..=100)
4416 ) {
4417 if let Some(r) = anderson_darling_test(&data) {
4418 prop_assert!(r.statistic >= 0.0, "A2 = {}", r.statistic);
4419 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4420 }
4421 }
4422
4423 #[test]
4424 fn mann_whitney_p_bounded(
4425 a in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4426 b in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4427 ) {
4428 if let Some(r) = mann_whitney_u_test(&a, &b) {
4429 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4430 prop_assert!(r.statistic >= 0.0, "U = {}", r.statistic);
4431 }
4432 }
4433
4434 #[test]
4435 fn wilcoxon_p_bounded(
4436 diffs in proptest::collection::vec(-1e3_f64..1e3, 3..=20),
4437 ) {
4438 let zeros: Vec<f64> = vec![0.0; diffs.len()];
4439 if let Some(r) = wilcoxon_signed_rank_test(&diffs, &zeros) {
4440 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4441 }
4442 }
4443
4444 #[test]
4445 fn bartlett_p_bounded(
4446 g1 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4447 g2 in proptest::collection::vec(0.1_f64..100.0, 3..=15),
4448 ) {
4449 if let Some(r) = bartlett_test(&[&g1, &g2]) {
4450 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4451 prop_assert!(r.statistic >= 0.0, "T = {}", r.statistic);
4452 }
4453 }
4454
4455 #[test]
4456 fn fisher_p_bounded(
4457 a in 0_u64..20,
4458 b in 0_u64..20,
4459 c in 0_u64..20,
4460 d in 0_u64..20,
4461 ) {
4462 if let Some(r) = fisher_exact_test(a, b, c, d) {
4463 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4464 }
4465 }
4466
4467 #[test]
4468 fn mann_kendall_p_bounded(
4469 data in proptest::collection::vec(-1e3_f64..1e3, 4..=30)
4470 ) {
4471 if let Some(r) = mann_kendall_test(&data) {
4472 prop_assert!(r.p_value >= 0.0 && r.p_value <= 1.0, "p = {}", r.p_value);
4473 prop_assert!(r.kendall_tau >= -1.0 && r.kendall_tau <= 1.0,
4474 "tau = {}", r.kendall_tau);
4475 }
4476 }
4477
4478 #[test]
4479 fn mann_kendall_monotone_increasing(n in 5_usize..=30) {
4480 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
4481 let r = mann_kendall_test(&data).expect("should compute");
4482 prop_assert!((r.kendall_tau - 1.0).abs() < 1e-10,
4483 "tau should be 1.0 for monotone, got {}", r.kendall_tau);
4484 prop_assert!(r.sen_slope > 0.0, "slope should be positive");
4485 }
4486 }
4487}
4488
4489#[cfg(test)]
4492mod definition_reference {
4493 use super::*;
4494
4495 #[test]
4496 fn bonferroni_is_p_times_m_capped_at_one() {
4497 let adjusted = bonferroni_correction(&[0.01, 0.02, 0.04, 0.3, 0.0004]).expect("valid");
4498 let want = [0.05, 0.1, 0.2, 1.0, 0.002];
4499 assert_eq!(adjusted.len(), want.len());
4500 for (got, want) in adjusted.iter().zip(want) {
4501 assert!((got - want).abs() <= 1e-15, "got {got}, want {want}");
4502 }
4503 }
4504}
4505
4506#[cfg(test)]
4509mod scipy_tail_reference {
4510 use super::*;
4511
4512 fn close(got: f64, want: f64, what: &str) {
4513 assert!(
4514 (got - want).abs() <= 1e-9 * want.abs(),
4515 "{what}: got {got:e}, SciPy {want:e}"
4516 );
4517 }
4518
4519 #[test]
4520 fn t_tests_keep_their_tail() {
4521 let a = [100.2, 99.8, 100.5, 100.1, 99.9, 100.3, 100.0, 99.7];
4522 let b = [120.1, 119.7, 120.4, 120.0, 119.8, 120.3];
4523 let r = one_sample_t_test(&a, 90.0).expect("valid");
4525 close(r.p_value, 1.6824197797300066e-12, "one-sample p");
4526 let r = two_sample_t_test(&a, &b).expect("valid");
4528 close(r.p_value, 8.838218005160137e-19, "Welch p");
4529 }
4530
4531 #[test]
4532 fn f_and_chi_squared_tests_keep_their_tail() {
4533 let (g1, g2, g3) = (
4535 [10.1, 10.3, 9.9, 10.0, 10.2],
4536 [12.0, 12.2, 11.9, 12.1, 12.3],
4537 [14.1, 13.9, 14.0, 14.2, 13.8],
4538 );
4539 let r = one_way_anova(&[&g1, &g2, &g3]).expect("valid");
4540 close(r.p_value, 2.297566098988527e-13, "ANOVA p");
4541 let r = chi_squared_independence(&[80.0, 20.0, 30.0, 70.0], 2, 2).expect("valid");
4543 close(r.p_value, 1.1885852395646684e-12, "independence p");
4544 let r = chi_squared_goodness_of_fit(&[90.0, 10.0, 5.0, 95.0], &[50.0; 4]).expect("valid");
4546 close(r.p_value, 3.1567404374222543e-31, "goodness-of-fit p");
4547 let low: Vec<f64> = (1..=10).map(f64::from).collect();
4549 let mid: Vec<f64> = (11..=20).map(f64::from).collect();
4550 let high: Vec<f64> = (21..=30).map(f64::from).collect();
4551 let r = kruskal_wallis_test(&[&low, &mid, &high]).expect("valid");
4552 close(r.p_value, 2.49000508218493e-06, "Kruskal-Wallis p");
4553 }
4554}