1use u_numflow::matrix::Matrix;
26use u_numflow::special;
27use u_numflow::stats;
28
29#[derive(Debug, Clone, Copy)]
31pub struct CorrelationResult {
32 pub r: f64,
34 pub p_value: f64,
36 pub n: usize,
38}
39
40#[derive(Debug, Clone, Copy)]
42pub struct CorrelationCI {
43 pub lower: f64,
45 pub upper: f64,
47 pub confidence: f64,
49}
50
51pub fn pearson(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
84 let n = x.len();
85 if n < 3 || n != y.len() {
86 return None;
87 }
88
89 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
90 return None;
91 }
92
93 let cov = stats::covariance(x, y)?;
94 let sx = stats::std_dev(x)?;
95 let sy = stats::std_dev(y)?;
96
97 if sx < 1e-300 || sy < 1e-300 {
98 return None; }
100
101 let r = (cov / (sx * sy)).clamp(-1.0, 1.0);
102 let p_value = correlation_p_value(r, n);
103
104 Some(CorrelationResult { r, p_value, n })
105}
106
107pub fn spearman(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
140 let n = x.len();
141 if n < 3 || n != y.len() {
142 return None;
143 }
144
145 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
146 return None;
147 }
148
149 let rx = rank_data(x);
150 let ry = rank_data(y);
151
152 let cov = stats::covariance(&rx, &ry)?;
154 let srx = stats::std_dev(&rx)?;
155 let sry = stats::std_dev(&ry)?;
156
157 if srx < 1e-300 || sry < 1e-300 {
158 return None;
159 }
160
161 let r = (cov / (srx * sry)).clamp(-1.0, 1.0);
162 let p_value = correlation_p_value(r, n);
163
164 Some(CorrelationResult { r, p_value, n })
165}
166
167pub fn kendall_tau_b(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
206 let n = x.len();
207 if n < 3 || n != y.len() {
208 return None;
209 }
210
211 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
212 return None;
213 }
214
215 let mut concordant: i64 = 0;
216 let mut discordant: i64 = 0;
217 let mut ties_x: i64 = 0;
218 let mut ties_y: i64 = 0;
219 let mut _ties_xy: i64 = 0;
220
221 for i in 0..n {
222 for j in (i + 1)..n {
223 let dx = x[i] - x[j];
224 let dy = y[i] - y[j];
225 let product = dx * dy;
226
227 if dx == 0.0 && dy == 0.0 {
228 _ties_xy += 1;
229 ties_x += 1;
230 ties_y += 1;
231 } else if dx == 0.0 {
232 ties_x += 1;
233 } else if dy == 0.0 {
234 ties_y += 1;
235 } else if product > 0.0 {
236 concordant += 1;
237 } else {
238 discordant += 1;
239 }
240 }
241 }
242
243 let n0 = (n as i64) * (n as i64 - 1) / 2;
244 let denom_sq = (n0 - ties_x) as f64 * (n0 - ties_y) as f64;
245
246 if denom_sq <= 0.0 {
247 return None; }
249
250 let tau = (concordant - discordant) as f64 / denom_sq.sqrt();
251 let tau = tau.clamp(-1.0, 1.0);
252
253 let nf = n as f64;
258 let v0 = nf * (nf - 1.0) * (2.0 * nf + 5.0);
259 let vt = compute_tie_variance_term(x);
260 let vu = compute_tie_variance_term(y);
261 let var_s = (v0 - vt - vu) / 18.0;
262
263 let p_value = if var_s > 0.0 {
264 let s = (concordant - discordant) as f64;
265 let z = s / var_s.sqrt();
266 2.0 * special::standard_normal_sf(z.abs())
267 } else {
268 1.0
269 };
270
271 Some(CorrelationResult { r: tau, p_value, n })
272}
273
274pub fn fisher_z(r: f64) -> Option<f64> {
292 if r <= -1.0 || r >= 1.0 || !r.is_finite() {
293 return None;
294 }
295 Some(r.atanh())
296}
297
298pub fn fisher_z_inv(z: f64) -> f64 {
300 z.tanh()
301}
302
303pub fn correlation_ci(r: f64, n: usize, confidence: f64) -> Option<CorrelationCI> {
328 if n < 4 || r <= -1.0 || r >= 1.0 || !r.is_finite() {
329 return None;
330 }
331 if confidence <= 0.0 || confidence >= 1.0 {
332 return None;
333 }
334
335 let z = r.atanh();
336 let se = 1.0 / ((n as f64 - 3.0).sqrt());
337 let alpha = 1.0 - confidence;
338 let z_crit = special::inverse_normal_cdf(1.0 - alpha / 2.0);
339
340 let z_lower = z - z_crit * se;
341 let z_upper = z + z_crit * se;
342
343 Some(CorrelationCI {
344 lower: z_lower.tanh(),
345 upper: z_upper.tanh(),
346 confidence,
347 })
348}
349
350pub fn correlation_matrix(variables: &[&[f64]]) -> Option<Matrix> {
380 let p = variables.len();
381 if p < 2 {
382 return None;
383 }
384 let n = variables[0].len();
385 if n < 3 {
386 return None;
387 }
388 for v in variables {
389 if v.len() != n {
390 return None;
391 }
392 }
393
394 let mut sds = Vec::with_capacity(p);
396 for v in variables {
397 let m = stats::mean(v)?;
398 if !m.is_finite() {
399 return None;
400 }
401 let s = stats::std_dev(v)?;
402 if !s.is_finite() || s < 1e-300 {
403 return None;
404 }
405 sds.push(s);
406 }
407
408 let mut data = vec![0.0; p * p];
409
410 for i in 0..p {
411 data[i * p + i] = 1.0; for j in (i + 1)..p {
413 let cov = stats::covariance(variables[i], variables[j])?;
414 let r = (cov / (sds[i] * sds[j])).clamp(-1.0, 1.0);
415 data[i * p + j] = r;
416 data[j * p + i] = r;
417 }
418 }
419
420 Matrix::new(p, p, data).ok()
421}
422
423pub fn spearman_matrix(variables: &[&[f64]]) -> Option<Matrix> {
427 let p = variables.len();
428 if p < 2 {
429 return None;
430 }
431 let n = variables[0].len();
432 if n < 3 {
433 return None;
434 }
435 for v in variables {
436 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
437 return None;
438 }
439 }
440
441 let ranked: Vec<Vec<f64>> = variables.iter().map(|v| rank_data(v)).collect();
443 let ranked_refs: Vec<&[f64]> = ranked.iter().map(|v| v.as_slice()).collect();
444
445 correlation_matrix(&ranked_refs)
446}
447
448pub fn kendall_matrix(variables: &[&[f64]]) -> Option<Matrix> {
478 let p = variables.len();
479 if p < 2 {
480 return None;
481 }
482 let n = variables[0].len();
483 if n < 3 {
484 return None;
485 }
486 for v in variables {
487 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
488 return None;
489 }
490 }
491
492 let mut data = vec![0.0_f64; p * p];
493 for i in 0..p {
494 data[i * p + i] = 1.0;
495 for j in (i + 1)..p {
496 let r = kendall_tau_b(variables[i], variables[j])?.r;
497 data[i * p + j] = r;
498 data[j * p + i] = r;
499 }
500 }
501 Matrix::new(p, p, data).ok()
502}
503
504fn correlation_p_value(r: f64, n: usize) -> f64 {
512 if n < 3 {
513 return 1.0;
514 }
515 let df = (n - 2) as f64;
516 let r2 = r * r;
517
518 if r2 >= 1.0 - 1e-15 {
520 return 0.0; }
522
523 let t = r * (df / (1.0 - r2)).sqrt();
524 2.0 * special::t_distribution_sf(t.abs(), df)
525}
526
527fn rank_data(data: &[f64]) -> Vec<f64> {
531 let n = data.len();
532 let mut indexed: Vec<(usize, f64)> = data.iter().copied().enumerate().collect();
533 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
534
535 let mut ranks = vec![0.0; n];
536 let mut i = 0;
537 while i < n {
538 let mut j = i;
539 while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-300 {
541 j += 1;
542 }
543 let avg_rank = (i + j) as f64 / 2.0 + 0.5;
545 for item in indexed.iter().take(j).skip(i) {
546 ranks[item.0] = avg_rank;
547 }
548 i = j;
549 }
550
551 ranks
552}
553
554fn compute_tie_variance_term(data: &[f64]) -> f64 {
558 let mut sorted: Vec<f64> = data.to_vec();
559 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
560
561 let mut result = 0.0;
562 let mut i = 0;
563 while i < sorted.len() {
564 let mut j = i;
565 while j < sorted.len() && (sorted[j] - sorted[i]).abs() < 1e-300 {
566 j += 1;
567 }
568 let t = (j - i) as f64;
569 if t > 1.0 {
570 result += t * (t - 1.0) * (2.0 * t + 5.0);
571 }
572 i = j;
573 }
574 result
575}
576
577#[derive(Debug, Clone)]
583pub struct AcfResult {
584 pub acf: Vec<f64>,
587 pub confidence_threshold: f64,
590 pub bartlett_limits: Vec<f64>,
595}
596
597#[derive(Debug, Clone)]
599pub struct PacfResult {
600 pub pacf: Vec<f64>,
602 pub confidence_threshold: f64,
605}
606
607const Z_975: f64 = 1.959_963_984_540_054;
609
610pub fn acf(data: &[f64], max_lag: usize) -> Option<AcfResult> {
639 let n = data.len();
640 if n < 2 || max_lag == 0 || data.iter().any(|v| !v.is_finite()) {
641 return None;
642 }
643 let max_lag = max_lag.min(n - 1);
644 let nf = n as f64;
645
646 let mean = data.iter().sum::<f64>() / nf;
647
648 let c0: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
650
651 let mean_square: f64 = data.iter().map(|&x| x * x).sum::<f64>() / nf;
654 if c0 <= 1e-20 * mean_square {
655 return None;
656 }
657
658 let mut acf_vals = Vec::with_capacity(max_lag + 1);
659 acf_vals.push(1.0); for lag in 1..=max_lag {
662 let ck: f64 = data[..n - lag]
663 .iter()
664 .zip(&data[lag..])
665 .map(|(&xt, &xt_h)| (xt - mean) * (xt_h - mean))
666 .sum::<f64>()
667 / nf;
668 acf_vals.push(ck / c0);
669 }
670
671 let threshold = Z_975 / nf.sqrt();
672 let mut cumulative = 0.0;
673 let bartlett_limits = acf_vals[1..]
674 .iter()
675 .map(|r| {
676 let limit = Z_975 * ((1.0 + 2.0 * cumulative) / nf).sqrt();
677 cumulative += r * r;
678 limit
679 })
680 .collect();
681
682 Some(AcfResult {
683 acf: acf_vals,
684 confidence_threshold: threshold,
685 bartlett_limits,
686 })
687}
688
689pub fn pacf(data: &[f64], max_lag: usize) -> Option<PacfResult> {
721 let n = data.len();
722 if n < 3 || max_lag == 0 {
723 return None;
724 }
725
726 let acf_result = acf(data, max_lag)?;
728 let rho = &acf_result.acf;
729 let max_lag = rho.len() - 1; if max_lag == 0 {
732 return None;
733 }
734
735 let mut pacf_vals = Vec::with_capacity(max_lag);
736
737 pacf_vals.push(rho[1]);
739
740 if max_lag == 1 {
741 return Some(PacfResult {
742 pacf: pacf_vals,
743 confidence_threshold: Z_975 / (n as f64).sqrt(),
744 });
745 }
746
747 let mut phi_prev = vec![rho[1]];
749
750 for h in 2..=max_lag {
751 let mut num = rho[h];
753 for j in 0..h - 1 {
754 num -= phi_prev[j] * rho[h - 1 - j];
755 }
756
757 let mut den = 1.0;
759 for j in 0..h - 1 {
760 den -= phi_prev[j] * rho[j + 1];
761 }
762
763 let phi_hh = if den.abs() > 1e-14 { num / den } else { 0.0 };
764
765 let mut phi_new = Vec::with_capacity(h);
767 for j in 0..h - 1 {
768 phi_new.push(phi_prev[j] - phi_hh * phi_prev[h - 2 - j]);
769 }
770 phi_new.push(phi_hh);
771
772 pacf_vals.push(phi_hh);
773 phi_prev = phi_new;
774 }
775
776 Some(PacfResult {
777 pacf: pacf_vals,
778 confidence_threshold: Z_975 / (n as f64).sqrt(),
779 })
780}
781
782#[cfg(test)]
785mod statsmodels_reference {
786 use super::*;
787
788 const X: [f64; 24] = [
789 2.3, 3.1, 4.0, 3.6, 2.9, 2.2, 2.8, 3.9, 4.6, 4.1, 3.3, 2.5, 2.7, 3.8, 4.9, 4.4, 3.5, 2.6,
790 3.0, 4.2, 5.1, 4.5, 3.4, 2.9,
791 ];
792
793 fn close(got: &[f64], want: &[f64], what: &str) {
794 assert_eq!(got.len(), want.len(), "{what}");
795 for (k, (g, w)) in got.iter().zip(want).enumerate() {
796 assert!((g - w).abs() < 1e-12, "{what}[{k}]: {g} vs {w}");
797 }
798 }
799
800 #[test]
801 fn acf_and_bartlett_limits_reproduce_statsmodels() {
802 let r = acf(&X, 8).unwrap();
803 close(
804 &r.acf,
805 &[
806 1.0,
807 0.486403576681892,
808 -0.36186808082372074,
809 -0.6951594797553611,
810 -0.2634125570953007,
811 0.42164395757528844,
812 0.6593055663079663,
813 0.26106100487729356,
814 -0.3171014941549895,
815 ],
816 "acf",
817 );
818 close(
819 &r.bartlett_limits,
820 &[
821 0.40007597302960923,
822 0.48559020426940086,
823 0.5269888881332575,
824 0.6575826795727397,
825 0.6742603293351266,
826 0.7152198020271394,
827 0.80665422242141,
828 0.8200659919957791,
829 ],
830 "bartlett",
831 );
832 assert!((r.confidence_threshold - 0.40007597302960923).abs() < 1e-12);
833 }
834
835 #[test]
836 fn pacf_reproduces_statsmodels_ldb() {
837 let r = pacf(&X, 8).unwrap();
838 close(
839 &r.pacf,
840 &[
841 0.486403576681892,
842 -0.7839238375815395,
843 0.00047345408884359627,
844 0.1902495235598503,
845 0.19973005383776718,
846 0.1225767860916582,
847 0.05567632845800997,
848 0.03539504608148146,
849 ],
850 "pacf",
851 );
852 }
853
854 #[test]
855 fn a_small_unit_is_not_a_constant_series() {
856 let small: Vec<f64> = X.iter().map(|v| v * 1e-150).collect();
857 let r = acf(&small, 8).unwrap();
858 assert!((r.acf[1] - 0.486403576681892).abs() < 1e-12);
859 assert!(acf(&[3.0; 10], 3).is_none());
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866
867 #[test]
872 fn pearson_perfect_positive() {
873 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
874 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
875 let result = pearson(&x, &y).expect("should compute");
876 assert!((result.r - 1.0).abs() < 1e-10);
877 assert!(result.p_value < 1e-10);
878 }
879
880 #[test]
881 fn pearson_perfect_negative() {
882 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
883 let y = [10.0, 8.0, 6.0, 4.0, 2.0];
884 let result = pearson(&x, &y).expect("should compute");
885 assert!((result.r + 1.0).abs() < 1e-10);
886 assert!(result.p_value < 1e-10);
887 }
888
889 #[test]
890 fn pearson_uncorrelated() {
891 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
893 let y = [3.0, 1.0, 5.0, 1.0, 5.0];
894 let result = pearson(&x, &y).expect("should compute");
895 assert!(result.r.abs() < 0.5);
896 }
897
898 #[test]
899 fn pearson_known_value() {
900 let x = [68.0, 71.0, 62.0, 75.0, 58.0, 60.0, 67.0, 68.0, 71.0, 69.0];
902 let y = [4.1, 4.6, 3.8, 4.4, 3.2, 3.1, 3.8, 4.1, 4.3, 3.7];
903 let result = pearson(&x, &y).expect("should compute");
904 assert!((result.r - 0.8816).abs() < 0.01, "r = {}", result.r);
906 }
907
908 #[test]
917 fn pearson_numeric_reference() {
918 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
919 let y = [2.0, 4.0, 5.0, 4.0, 5.0];
920 let result = pearson(&x, &y).expect("should compute");
921
922 let expected_r = 6.0_f64 / (60.0_f64).sqrt(); assert!(
924 (result.r - expected_r).abs() < 1e-3,
925 "r expected {:.6}, got {}",
926 expected_r,
927 result.r
928 );
929 assert!(
930 (result.r - 0.7746).abs() < 1e-3,
931 "r expected ≈0.7746, got {}",
932 result.r
933 );
934 }
935
936 #[test]
937 fn pearson_insufficient_data() {
938 assert!(pearson(&[1.0, 2.0], &[3.0, 4.0]).is_none());
939 assert!(pearson(&[1.0], &[2.0]).is_none());
940 }
941
942 #[test]
943 fn pearson_length_mismatch() {
944 assert!(pearson(&[1.0, 2.0, 3.0], &[4.0, 5.0]).is_none());
945 }
946
947 #[test]
948 fn pearson_zero_variance() {
949 assert!(pearson(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]).is_none());
950 }
951
952 #[test]
953 fn pearson_nan_input() {
954 assert!(pearson(&[1.0, f64::NAN, 3.0], &[4.0, 5.0, 6.0]).is_none());
955 }
956
957 #[test]
962 fn spearman_perfect_monotone() {
963 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
964 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let result = spearman(&x, &y).expect("should compute");
966 assert!((result.r - 1.0).abs() < 1e-10);
967 }
968
969 #[test]
970 fn spearman_perfect_negative() {
971 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
972 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
973 let result = spearman(&x, &y).expect("should compute");
974 assert!((result.r + 1.0).abs() < 1e-10);
975 }
976
977 #[test]
978 fn spearman_with_ties() {
979 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
980 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
981 let result = spearman(&x, &y).expect("should compute");
982 assert!(result.r > 0.9); }
984
985 #[test]
986 fn spearman_nonlinear_monotone() {
987 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
989 let y: Vec<f64> = x.iter().map(|&v: &f64| v.powi(3)).collect();
990 let result = spearman(&x, &y).expect("should compute");
991 assert!((result.r - 1.0).abs() < 1e-10);
992 }
993
994 #[test]
999 fn kendall_perfect_concordance() {
1000 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1001 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1002 let result = kendall_tau_b(&x, &y).expect("should compute");
1003 assert!((result.r - 1.0).abs() < 1e-10);
1004 }
1005
1006 #[test]
1007 fn kendall_perfect_discordance() {
1008 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1009 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1010 let result = kendall_tau_b(&x, &y).expect("should compute");
1011 assert!((result.r + 1.0).abs() < 1e-10);
1012 }
1013
1014 #[test]
1015 fn kendall_known_value() {
1016 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1018 let y = [3.0, 4.0, 1.0, 2.0, 5.0];
1019 let result = kendall_tau_b(&x, &y).expect("should compute");
1020 assert!((result.r - 0.2).abs() < 0.01);
1021 }
1022
1023 #[test]
1024 fn kendall_with_ties() {
1025 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1026 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1027 let result = kendall_tau_b(&x, &y).expect("should compute");
1028 assert!(result.r > 0.8);
1029 }
1030
1031 #[test]
1032 fn kendall_all_ties() {
1033 let x = [1.0, 1.0, 1.0];
1034 let y = [2.0, 2.0, 2.0];
1035 assert!(kendall_tau_b(&x, &y).is_none()); }
1037
1038 #[test]
1043 fn fisher_z_zero() {
1044 let z = fisher_z(0.0).expect("should compute");
1045 assert!(z.abs() < 1e-15);
1046 }
1047
1048 #[test]
1049 fn fisher_z_roundtrip() {
1050 for &r in &[0.0, 0.3, 0.5, 0.8, 0.95, -0.5, -0.95] {
1051 let z = fisher_z(r).expect("should compute");
1052 let r_back = fisher_z_inv(z);
1053 assert!((r - r_back).abs() < 1e-10, "Roundtrip failed for r={r}");
1054 }
1055 }
1056
1057 #[test]
1058 fn fisher_z_boundary() {
1059 assert!(fisher_z(1.0).is_none());
1060 assert!(fisher_z(-1.0).is_none());
1061 assert!(fisher_z(1.5).is_none());
1062 assert!(fisher_z(f64::NAN).is_none());
1063 }
1064
1065 #[test]
1070 fn ci_contains_r() {
1071 let ci = correlation_ci(0.6, 50, 0.95).expect("should compute");
1072 assert!(ci.lower < 0.6);
1073 assert!(ci.upper > 0.6);
1074 assert!(ci.lower > -1.0);
1075 assert!(ci.upper < 1.0);
1076 }
1077
1078 #[test]
1079 fn ci_wider_at_higher_confidence() {
1080 let ci_95 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1081 let ci_99 = correlation_ci(0.5, 30, 0.99).expect("should compute");
1082 assert!(ci_99.upper - ci_99.lower > ci_95.upper - ci_95.lower);
1083 }
1084
1085 #[test]
1086 fn ci_narrower_with_more_data() {
1087 let ci_30 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1088 let ci_100 = correlation_ci(0.5, 100, 0.95).expect("should compute");
1089 assert!(ci_100.upper - ci_100.lower < ci_30.upper - ci_30.lower);
1090 }
1091
1092 #[test]
1093 fn ci_edge_cases() {
1094 assert!(correlation_ci(0.5, 3, 0.95).is_none()); assert!(correlation_ci(1.0, 30, 0.95).is_none()); assert!(correlation_ci(0.5, 30, 0.0).is_none()); assert!(correlation_ci(0.5, 30, 1.0).is_none());
1098 }
1099
1100 #[test]
1105 fn corr_matrix_identity() {
1106 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1107 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
1108 let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1109 let mat = correlation_matrix(&[&x, &y, &z]).expect("should compute");
1110
1111 assert!((mat.get(0, 0) - 1.0).abs() < 1e-10);
1113 assert!((mat.get(1, 1) - 1.0).abs() < 1e-10);
1114 assert!((mat.get(2, 2) - 1.0).abs() < 1e-10);
1115
1116 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1118
1119 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1121
1122 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1124 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1125 }
1126
1127 #[test]
1128 fn corr_matrix_insufficient_variables() {
1129 let x = [1.0, 2.0, 3.0];
1130 assert!(correlation_matrix(&[&x]).is_none());
1131 }
1132
1133 #[test]
1134 fn corr_matrix_length_mismatch() {
1135 let x = [1.0, 2.0, 3.0];
1136 let y = [4.0, 5.0];
1137 assert!(correlation_matrix(&[&x, &y]).is_none());
1138 }
1139
1140 #[test]
1141 fn spearman_matrix_basic() {
1142 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1143 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1145 let mat = spearman_matrix(&[&x, &y, &z]).expect("should compute");
1146
1147 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1149 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1151 }
1152
1153 #[test]
1154 fn kendall_matrix_basic() {
1155 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1156 let y = [2.0, 4.0, 6.0, 8.0, 10.0]; let z = [5.0, 4.0, 3.0, 2.0, 1.0]; let mat = kendall_matrix(&[&x, &y, &z]).expect("should compute");
1159
1160 assert_eq!(mat.rows(), 3);
1161 assert!((mat.get(0, 0) - 1.0).abs() < 1e-15);
1162 assert!((mat.get(1, 1) - 1.0).abs() < 1e-15);
1163 assert!((mat.get(2, 2) - 1.0).abs() < 1e-15);
1164
1165 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1166 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1167
1168 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1170 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1171 }
1172
1173 #[test]
1174 fn kendall_matrix_insufficient_variables() {
1175 let x = [1.0, 2.0, 3.0];
1176 assert!(kendall_matrix(&[&x]).is_none());
1177 }
1178
1179 #[test]
1180 fn kendall_matrix_length_mismatch() {
1181 let x = [1.0, 2.0, 3.0, 4.0];
1182 let y = [4.0, 5.0];
1183 assert!(kendall_matrix(&[&x, &y]).is_none());
1184 }
1185
1186 #[test]
1187 fn kendall_matrix_with_ties() {
1188 let x = [1.0, 2.0, 2.0, 3.0, 4.0];
1189 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1190 let mat = kendall_matrix(&[&x, &y]).expect("should compute");
1191 assert!(mat.get(0, 1) > 0.7);
1192 }
1193
1194 #[test]
1199 fn rank_no_ties() {
1200 let ranks = rank_data(&[3.0, 1.0, 2.0]);
1201 assert!((ranks[0] - 3.0).abs() < 1e-10); assert!((ranks[1] - 1.0).abs() < 1e-10); assert!((ranks[2] - 2.0).abs() < 1e-10); }
1205
1206 #[test]
1207 fn rank_with_ties() {
1208 let ranks = rank_data(&[1.0, 2.0, 2.0, 4.0]);
1209 assert!((ranks[0] - 1.0).abs() < 1e-10);
1210 assert!((ranks[1] - 2.5).abs() < 1e-10); assert!((ranks[2] - 2.5).abs() < 1e-10);
1212 assert!((ranks[3] - 4.0).abs() < 1e-10);
1213 }
1214
1215 #[test]
1216 fn rank_all_same() {
1217 let ranks = rank_data(&[5.0, 5.0, 5.0]);
1218 assert!((ranks[0] - 2.0).abs() < 1e-10); assert!((ranks[1] - 2.0).abs() < 1e-10);
1220 assert!((ranks[2] - 2.0).abs() < 1e-10);
1221 }
1222
1223 #[test]
1228 fn pvalue_significant() {
1229 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1231 let y: Vec<f64> = x.iter().map(|&v| v * 2.0 + 1.0).collect();
1232 let result = pearson(&x, &y).expect("should compute");
1233 assert!(result.p_value < 1e-10);
1234 }
1235
1236 #[test]
1237 fn pvalue_not_significant() {
1238 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1240 let y = [5.0, 1.0, 3.0, 5.0, 1.0];
1241 let result = pearson(&x, &y).expect("should compute");
1242 assert!(result.p_value > 0.3); }
1244
1245 #[test]
1250 fn acf_lag_zero_is_one() {
1251 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0];
1252 let r = acf(&data, 3).expect("should compute");
1253 assert!((r.acf[0] - 1.0).abs() < 1e-10);
1254 }
1255
1256 #[test]
1257 fn acf_linear_trend() {
1258 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1260 let r = acf(&data, 5).expect("should compute");
1261 assert!(r.acf[1] > 0.8, "lag-1 ACF for linear trend should be high");
1262 assert!(r.acf.len() == 6);
1263 }
1264
1265 #[test]
1266 fn acf_alternating() {
1267 let data: Vec<f64> = (0..30)
1269 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1270 .collect();
1271 let r = acf(&data, 5).expect("should compute");
1272 assert!(r.acf[1] < -0.8, "alternating → negative lag-1 ACF");
1273 assert!(r.acf[2] > 0.8, "alternating → positive lag-2 ACF");
1274 }
1275
1276 #[test]
1277 fn acf_white_noise_threshold() {
1278 let data: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 13) as f64).collect();
1279 let r = acf(&data, 10).expect("should compute");
1280 assert!((r.confidence_threshold - 1.96 / 10.0).abs() < 0.01);
1282 }
1283
1284 #[test]
1285 fn acf_edge_cases() {
1286 assert!(acf(&[1.0], 3).is_none()); assert!(acf(&[1.0, 2.0], 0).is_none()); assert!(acf(&[5.0, 5.0, 5.0, 5.0], 2).is_none()); assert!(acf(&[1.0, f64::NAN, 3.0], 1).is_none()); }
1291
1292 #[test]
1293 fn acf_max_lag_clamped() {
1294 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1295 let r = acf(&data, 100).expect("should compute");
1296 assert_eq!(r.acf.len(), 5); }
1298
1299 #[test]
1304 fn pacf_lag1_equals_acf_lag1() {
1305 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1306 let acf_r = acf(&data, 5).expect("should compute ACF");
1307 let pacf_r = pacf(&data, 5).expect("should compute PACF");
1308 assert!(
1309 (pacf_r.pacf[0] - acf_r.acf[1]).abs() < 1e-10,
1310 "PACF[1] should equal ACF[1]: {} vs {}",
1311 pacf_r.pacf[0],
1312 acf_r.acf[1]
1313 );
1314 }
1315
1316 #[test]
1317 fn pacf_linear_trend() {
1318 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1319 let r = pacf(&data, 5).expect("should compute");
1320 assert!(r.pacf[0].abs() > 0.5, "lag-1 PACF should be strong");
1322 }
1323
1324 #[test]
1325 fn pacf_bounded() {
1326 let data: Vec<f64> = (0..30).map(|i| (i as f64 * 0.5).sin()).collect();
1327 let r = pacf(&data, 10).expect("should compute");
1328 for (i, &p) in r.pacf.iter().enumerate() {
1329 assert!(
1330 (-1.0..=1.0).contains(&p),
1331 "PACF[{}] = {} out of [-1, 1]",
1332 i + 1,
1333 p
1334 );
1335 }
1336 }
1337
1338 #[test]
1339 fn pacf_edge_cases() {
1340 assert!(pacf(&[1.0, 2.0], 3).is_none()); assert!(pacf(&[1.0, 2.0, 3.0], 0).is_none()); }
1343}
1344
1345#[cfg(test)]
1346mod proptests {
1347 use super::*;
1348 use proptest::prelude::*;
1349
1350 fn bounded_vec(min_len: usize, max_len: usize) -> BoxedStrategy<Vec<f64>> {
1351 proptest::collection::vec(-1e6_f64..1e6, min_len..=max_len).boxed()
1352 }
1353
1354 proptest! {
1355 #[test]
1356 fn pearson_bounded(
1357 data in bounded_vec(5, 50).prop_flat_map(|x| {
1358 let n = x.len();
1359 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1360 })
1361 ) {
1362 let (x, y) = data;
1363 if let Some(result) = pearson(&x, &y) {
1364 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1365 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1366 }
1367 }
1368
1369 #[test]
1370 fn spearman_bounded(
1371 data in bounded_vec(5, 50).prop_flat_map(|x| {
1372 let n = x.len();
1373 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1374 })
1375 ) {
1376 let (x, y) = data;
1377 if let Some(result) = spearman(&x, &y) {
1378 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1379 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1380 }
1381 }
1382
1383 #[test]
1384 fn kendall_bounded(
1385 data in bounded_vec(5, 30).prop_flat_map(|x| {
1386 let n = x.len();
1387 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1388 })
1389 ) {
1390 let (x, y) = data;
1391 if let Some(result) = kendall_tau_b(&x, &y) {
1392 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "tau out of bounds: {}", result.r);
1393 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1394 }
1395 }
1396
1397 #[test]
1398 fn pearson_symmetric(
1399 data in bounded_vec(5, 50).prop_flat_map(|x| {
1400 let n = x.len();
1401 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1402 })
1403 ) {
1404 let (x, y) = data;
1405 let r_xy = pearson(&x, &y);
1406 let r_yx = pearson(&y, &x);
1407 match (r_xy, r_yx) {
1408 (Some(a), Some(b)) => {
1409 prop_assert!((a.r - b.r).abs() < 1e-10, "not symmetric: {} vs {}", a.r, b.r);
1410 }
1411 (None, None) => {}
1412 _ => prop_assert!(false, "one is None but not the other"),
1413 }
1414 }
1415
1416 #[test]
1417 fn fisher_z_roundtrip_prop(r in -0.99_f64..0.99) {
1418 let z = fisher_z(r).expect("should compute");
1419 let r_back = fisher_z_inv(z);
1420 prop_assert!((r - r_back).abs() < 1e-10);
1421 }
1422
1423 #[test]
1424 fn ci_contains_true_r(
1425 r in -0.99_f64..0.99,
1426 n in 10_usize..200
1427 ) {
1428 let ci = correlation_ci(r, n, 0.95).expect("should compute");
1429 prop_assert!(ci.lower < ci.upper, "CI inverted");
1430 prop_assert!(ci.lower >= -1.0 && ci.upper <= 1.0, "CI out of bounds");
1431 }
1432
1433 #[test]
1434 fn acf_bounded_prop(
1435 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1436 ) {
1437 if let Some(r) = acf(&data, 10) {
1438 prop_assert!((r.acf[0] - 1.0).abs() < 1e-10, "ACF[0] must be 1.0");
1439 for (i, &v) in r.acf.iter().enumerate() {
1440 prop_assert!((-1.0..=1.0).contains(&v), "ACF[{i}] = {v} out of [-1,1]");
1441 }
1442 }
1443 }
1444
1445 #[test]
1446 fn pacf_bounded_prop(
1447 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1448 ) {
1449 if let Some(r) = pacf(&data, 5) {
1450 for (i, &v) in r.pacf.iter().enumerate() {
1451 prop_assert!((-1.0 - 1e-10..=1.0 + 1e-10).contains(&v), "PACF[{i}] = {v} out of bounds");
1452 }
1453 }
1454 }
1455 }
1456}
1457
1458#[cfg(test)]
1460mod scipy_tail_reference {
1461 use super::*;
1462
1463 #[test]
1464 fn pearson_p_value_keeps_its_tail() {
1465 let x: Vec<f64> = (1..=20).map(f64::from).collect();
1466 let y = [
1467 5.3999999999999995,
1468 7.0,
1469 9.4,
1470 11.8,
1471 13.2,
1472 15.8,
1473 17.6,
1474 19.8,
1475 22.250000000000004,
1476 23.75,
1477 26.25,
1478 28.150000000000002,
1479 30.55,
1480 32.050000000000004,
1481 34.55,
1482 36.7,
1483 38.550000000000004,
1484 41.1,
1485 42.699999999999996,
1486 45.0,
1487 ];
1488 let got = pearson(&x, &y).expect("valid").p_value;
1489 let want = 8.20335692044105e-33;
1490 assert!(
1491 (got - want).abs() <= 1e-9 * want,
1492 "p: got {got:e}, SciPy {want:e}"
1493 );
1494 }
1495}