1use crate::error::{check_option, check_series, TimeSeriesError};
26use std::ops::RangeInclusive;
27use u_numflow::matrix::Matrix;
28use u_numflow::special;
29use u_numflow::stats;
30
31#[derive(Debug, Clone, Copy)]
33pub struct CorrelationResult {
34 pub r: f64,
36 pub p_value: f64,
38 pub n: usize,
40}
41
42#[derive(Debug, Clone, Copy)]
44pub struct CorrelationCI {
45 pub lower: f64,
47 pub upper: f64,
49 pub confidence: f64,
51}
52
53pub fn pearson(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
86 let n = x.len();
87 if n < 3 || n != y.len() {
88 return None;
89 }
90
91 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
92 return None;
93 }
94
95 let cov = stats::covariance(x, y)?;
96 let sx = stats::std_dev(x)?;
97 let sy = stats::std_dev(y)?;
98
99 if sx < 1e-300 || sy < 1e-300 {
100 return None; }
102
103 let r = (cov / (sx * sy)).clamp(-1.0, 1.0);
104 let p_value = correlation_p_value(r, n);
105
106 Some(CorrelationResult { r, p_value, n })
107}
108
109pub fn spearman(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
142 let n = x.len();
143 if n < 3 || n != y.len() {
144 return None;
145 }
146
147 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
148 return None;
149 }
150
151 let rx = rank_data(x);
152 let ry = rank_data(y);
153
154 let cov = stats::covariance(&rx, &ry)?;
156 let srx = stats::std_dev(&rx)?;
157 let sry = stats::std_dev(&ry)?;
158
159 if srx < 1e-300 || sry < 1e-300 {
160 return None;
161 }
162
163 let r = (cov / (srx * sry)).clamp(-1.0, 1.0);
164 let p_value = correlation_p_value(r, n);
165
166 Some(CorrelationResult { r, p_value, n })
167}
168
169pub fn kendall_tau_b(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
208 let n = x.len();
209 if n < 3 || n != y.len() {
210 return None;
211 }
212
213 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
214 return None;
215 }
216
217 let mut concordant: i64 = 0;
218 let mut discordant: i64 = 0;
219 let mut ties_x: i64 = 0;
220 let mut ties_y: i64 = 0;
221 let mut _ties_xy: i64 = 0;
222
223 for i in 0..n {
224 for j in (i + 1)..n {
225 let dx = x[i] - x[j];
226 let dy = y[i] - y[j];
227 let product = dx * dy;
228
229 if dx == 0.0 && dy == 0.0 {
230 _ties_xy += 1;
231 ties_x += 1;
232 ties_y += 1;
233 } else if dx == 0.0 {
234 ties_x += 1;
235 } else if dy == 0.0 {
236 ties_y += 1;
237 } else if product > 0.0 {
238 concordant += 1;
239 } else {
240 discordant += 1;
241 }
242 }
243 }
244
245 let n0 = (n as i64) * (n as i64 - 1) / 2;
246 let denom_sq = (n0 - ties_x) as f64 * (n0 - ties_y) as f64;
247
248 if denom_sq <= 0.0 {
249 return None; }
251
252 let tau = (concordant - discordant) as f64 / denom_sq.sqrt();
253 let tau = tau.clamp(-1.0, 1.0);
254
255 let nf = n as f64;
260 let v0 = nf * (nf - 1.0) * (2.0 * nf + 5.0);
261 let vt = compute_tie_variance_term(x);
262 let vu = compute_tie_variance_term(y);
263 let var_s = (v0 - vt - vu) / 18.0;
264
265 let p_value = if var_s > 0.0 {
266 let s = (concordant - discordant) as f64;
267 let z = s / var_s.sqrt();
268 2.0 * special::standard_normal_sf(z.abs())
269 } else {
270 1.0
271 };
272
273 Some(CorrelationResult { r: tau, p_value, n })
274}
275
276pub fn fisher_z(r: f64) -> Option<f64> {
294 if r <= -1.0 || r >= 1.0 || !r.is_finite() {
295 return None;
296 }
297 Some(r.atanh())
298}
299
300pub fn fisher_z_inv(z: f64) -> f64 {
302 z.tanh()
303}
304
305pub fn correlation_ci(r: f64, n: usize, confidence: f64) -> Option<CorrelationCI> {
330 if n < 4 || r <= -1.0 || r >= 1.0 || !r.is_finite() {
331 return None;
332 }
333 if confidence <= 0.0 || confidence >= 1.0 {
334 return None;
335 }
336
337 let z = r.atanh();
338 let se = 1.0 / ((n as f64 - 3.0).sqrt());
339 let alpha = 1.0 - confidence;
340 let z_crit = special::inverse_normal_cdf(1.0 - alpha / 2.0);
341
342 let z_lower = z - z_crit * se;
343 let z_upper = z + z_crit * se;
344
345 Some(CorrelationCI {
346 lower: z_lower.tanh(),
347 upper: z_upper.tanh(),
348 confidence,
349 })
350}
351
352pub fn correlation_matrix(variables: &[&[f64]]) -> Option<Matrix> {
382 let p = variables.len();
383 if p < 2 {
384 return None;
385 }
386 let n = variables[0].len();
387 if n < 3 {
388 return None;
389 }
390 for v in variables {
391 if v.len() != n {
392 return None;
393 }
394 }
395
396 let mut sds = Vec::with_capacity(p);
398 for v in variables {
399 let m = stats::mean(v)?;
400 if !m.is_finite() {
401 return None;
402 }
403 let s = stats::std_dev(v)?;
404 if !s.is_finite() || s < 1e-300 {
405 return None;
406 }
407 sds.push(s);
408 }
409
410 let mut data = vec![0.0; p * p];
411
412 for i in 0..p {
413 data[i * p + i] = 1.0; for j in (i + 1)..p {
415 let cov = stats::covariance(variables[i], variables[j])?;
416 let r = (cov / (sds[i] * sds[j])).clamp(-1.0, 1.0);
417 data[i * p + j] = r;
418 data[j * p + i] = r;
419 }
420 }
421
422 Matrix::new(p, p, data).ok()
423}
424
425pub fn spearman_matrix(variables: &[&[f64]]) -> Option<Matrix> {
429 let p = variables.len();
430 if p < 2 {
431 return None;
432 }
433 let n = variables[0].len();
434 if n < 3 {
435 return None;
436 }
437 for v in variables {
438 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
439 return None;
440 }
441 }
442
443 let ranked: Vec<Vec<f64>> = variables.iter().map(|v| rank_data(v)).collect();
445 let ranked_refs: Vec<&[f64]> = ranked.iter().map(|v| v.as_slice()).collect();
446
447 correlation_matrix(&ranked_refs)
448}
449
450pub fn kendall_matrix(variables: &[&[f64]]) -> Option<Matrix> {
480 let p = variables.len();
481 if p < 2 {
482 return None;
483 }
484 let n = variables[0].len();
485 if n < 3 {
486 return None;
487 }
488 for v in variables {
489 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
490 return None;
491 }
492 }
493
494 let mut data = vec![0.0_f64; p * p];
495 for i in 0..p {
496 data[i * p + i] = 1.0;
497 for j in (i + 1)..p {
498 let r = kendall_tau_b(variables[i], variables[j])?.r;
499 data[i * p + j] = r;
500 data[j * p + i] = r;
501 }
502 }
503 Matrix::new(p, p, data).ok()
504}
505
506fn correlation_p_value(r: f64, n: usize) -> f64 {
514 if n < 3 {
515 return 1.0;
516 }
517 let df = (n - 2) as f64;
518 let r2 = r * r;
519
520 if r2 >= 1.0 - 1e-15 {
522 return 0.0; }
524
525 let t = r * (df / (1.0 - r2)).sqrt();
526 2.0 * special::t_distribution_sf(t.abs(), df)
527}
528
529fn rank_data(data: &[f64]) -> Vec<f64> {
533 let n = data.len();
534 let mut indexed: Vec<(usize, f64)> = data.iter().copied().enumerate().collect();
535 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
536
537 let mut ranks = vec![0.0; n];
538 let mut i = 0;
539 while i < n {
540 let mut j = i;
541 while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-300 {
543 j += 1;
544 }
545 let avg_rank = (i + j) as f64 / 2.0 + 0.5;
547 for item in indexed.iter().take(j).skip(i) {
548 ranks[item.0] = avg_rank;
549 }
550 i = j;
551 }
552
553 ranks
554}
555
556fn compute_tie_variance_term(data: &[f64]) -> f64 {
560 let mut sorted: Vec<f64> = data.to_vec();
561 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
562
563 let mut result = 0.0;
564 let mut i = 0;
565 while i < sorted.len() {
566 let mut j = i;
567 while j < sorted.len() && (sorted[j] - sorted[i]).abs() < 1e-300 {
568 j += 1;
569 }
570 let t = (j - i) as f64;
571 if t > 1.0 {
572 result += t * (t - 1.0) * (2.0 * t + 5.0);
573 }
574 i = j;
575 }
576 result
577}
578
579#[derive(Debug, Clone)]
585pub struct AcfResult {
586 pub acf: Vec<f64>,
589 pub confidence_threshold: f64,
592 pub bartlett_limits: Vec<f64>,
597}
598
599#[derive(Debug, Clone)]
601pub struct PacfResult {
602 pub pacf: Vec<f64>,
604 pub confidence_threshold: f64,
607}
608
609const Z_975: f64 = 1.959_963_984_540_054;
611
612pub const ACF_MIN_OBSERVATIONS: usize = 2;
614
615pub const PACF_MIN_OBSERVATIONS: usize = 4;
617
618fn lag_choice(
622 max_lag: Option<usize>,
623 n: usize,
624 domain: RangeInclusive<usize>,
625) -> Result<usize, TimeSeriesError> {
626 match max_lag {
627 None => Ok((((n as f64).log10() * 10.0).floor() as usize).clamp(1, *domain.end())),
628 Some(k) => check_option("max_lag", k, &domain),
629 }
630}
631
632pub fn acf_max_lag_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
647 check_series(data, ACF_MIN_OBSERVATIONS)?;
648 Ok(1..=data.len() - 1)
649}
650
651pub fn pacf_max_lag_domain(data: &[f64]) -> Result<RangeInclusive<usize>, TimeSeriesError> {
658 check_series(data, PACF_MIN_OBSERVATIONS)?;
659 Ok(1..=data.len() / 2 - 1)
660}
661
662pub fn acf(data: &[f64], max_lag: Option<usize>) -> Result<AcfResult, TimeSeriesError> {
693 let domain = acf_max_lag_domain(data)?;
694 let n = data.len();
695 let max_lag = lag_choice(max_lag, n, domain)?;
696 let nf = n as f64;
697
698 let mean = data.iter().sum::<f64>() / nf;
699
700 let c0: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
702
703 let mean_square: f64 = data.iter().map(|&x| x * x).sum::<f64>() / nf;
706 if c0 <= 1e-20 * mean_square {
707 return Err(TimeSeriesError::NoVariation);
708 }
709
710 let mut acf_vals = Vec::with_capacity(max_lag + 1);
711 acf_vals.push(1.0); for lag in 1..=max_lag {
714 let ck: f64 = data[..n - lag]
715 .iter()
716 .zip(&data[lag..])
717 .map(|(&xt, &xt_h)| (xt - mean) * (xt_h - mean))
718 .sum::<f64>()
719 / nf;
720 acf_vals.push(ck / c0);
721 }
722
723 let threshold = Z_975 / nf.sqrt();
724 let mut cumulative = 0.0;
725 let bartlett_limits = acf_vals[1..]
726 .iter()
727 .map(|r| {
728 let limit = Z_975 * ((1.0 + 2.0 * cumulative) / nf).sqrt();
729 cumulative += r * r;
730 limit
731 })
732 .collect();
733
734 Ok(AcfResult {
735 acf: acf_vals,
736 confidence_threshold: threshold,
737 bartlett_limits,
738 })
739}
740
741pub fn pacf(data: &[f64], max_lag: Option<usize>) -> Result<PacfResult, TimeSeriesError> {
776 let domain = pacf_max_lag_domain(data)?;
777 let n = data.len();
778 let max_lag = lag_choice(max_lag, n, domain)?;
779
780 let acf_result = acf(data, Some(max_lag))?;
783 let rho = &acf_result.acf;
784
785 let mut pacf_vals = Vec::with_capacity(max_lag);
786
787 pacf_vals.push(rho[1]);
789
790 if max_lag == 1 {
791 return Ok(PacfResult {
792 pacf: pacf_vals,
793 confidence_threshold: Z_975 / (n as f64).sqrt(),
794 });
795 }
796
797 let mut phi_prev = vec![rho[1]];
799
800 for h in 2..=max_lag {
801 let mut num = rho[h];
803 for j in 0..h - 1 {
804 num -= phi_prev[j] * rho[h - 1 - j];
805 }
806
807 let mut den = 1.0;
809 for j in 0..h - 1 {
810 den -= phi_prev[j] * rho[j + 1];
811 }
812
813 let phi_hh = if den.abs() > 1e-14 { num / den } else { 0.0 };
814
815 let mut phi_new = Vec::with_capacity(h);
817 for j in 0..h - 1 {
818 phi_new.push(phi_prev[j] - phi_hh * phi_prev[h - 2 - j]);
819 }
820 phi_new.push(phi_hh);
821
822 pacf_vals.push(phi_hh);
823 phi_prev = phi_new;
824 }
825
826 Ok(PacfResult {
827 pacf: pacf_vals,
828 confidence_threshold: Z_975 / (n as f64).sqrt(),
829 })
830}
831
832#[cfg(test)]
835mod statsmodels_reference {
836 use super::*;
837
838 const X: [f64; 24] = [
839 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,
840 3.0, 4.2, 5.1, 4.5, 3.4, 2.9,
841 ];
842
843 fn close(got: &[f64], want: &[f64], what: &str) {
844 assert_eq!(got.len(), want.len(), "{what}");
845 for (k, (g, w)) in got.iter().zip(want).enumerate() {
846 assert!((g - w).abs() < 1e-12, "{what}[{k}]: {g} vs {w}");
847 }
848 }
849
850 #[test]
851 fn acf_and_bartlett_limits_reproduce_statsmodels() {
852 let r = acf(&X, Some(8)).unwrap();
853 close(
854 &r.acf,
855 &[
856 1.0,
857 0.486403576681892,
858 -0.36186808082372074,
859 -0.6951594797553611,
860 -0.2634125570953007,
861 0.42164395757528844,
862 0.6593055663079663,
863 0.26106100487729356,
864 -0.3171014941549895,
865 ],
866 "acf",
867 );
868 close(
869 &r.bartlett_limits,
870 &[
871 0.40007597302960923,
872 0.48559020426940086,
873 0.5269888881332575,
874 0.6575826795727397,
875 0.6742603293351266,
876 0.7152198020271394,
877 0.80665422242141,
878 0.8200659919957791,
879 ],
880 "bartlett",
881 );
882 assert!((r.confidence_threshold - 0.40007597302960923).abs() < 1e-12);
883 }
884
885 #[test]
886 fn pacf_reproduces_statsmodels_ldb() {
887 let r = pacf(&X, Some(8)).unwrap();
888 close(
889 &r.pacf,
890 &[
891 0.486403576681892,
892 -0.7839238375815395,
893 0.00047345408884359627,
894 0.1902495235598503,
895 0.19973005383776718,
896 0.1225767860916582,
897 0.05567632845800997,
898 0.03539504608148146,
899 ],
900 "pacf",
901 );
902 }
903
904 #[test]
905 fn a_small_unit_is_not_a_constant_series() {
906 let small: Vec<f64> = X.iter().map(|v| v * 1e-150).collect();
907 let r = acf(&small, Some(8)).unwrap();
908 assert!((r.acf[1] - 0.486403576681892).abs() < 1e-12);
909 assert_eq!(
910 acf(&[3.0; 10], Some(3)).unwrap_err(),
911 TimeSeriesError::NoVariation
912 );
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
925 fn pearson_perfect_positive() {
926 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
927 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
928 let result = pearson(&x, &y).expect("should compute");
929 assert!((result.r - 1.0).abs() < 1e-10);
930 assert!(result.p_value < 1e-10);
931 }
932
933 #[test]
934 fn pearson_perfect_negative() {
935 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
936 let y = [10.0, 8.0, 6.0, 4.0, 2.0];
937 let result = pearson(&x, &y).expect("should compute");
938 assert!((result.r + 1.0).abs() < 1e-10);
939 assert!(result.p_value < 1e-10);
940 }
941
942 #[test]
943 fn pearson_uncorrelated() {
944 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
946 let y = [3.0, 1.0, 5.0, 1.0, 5.0];
947 let result = pearson(&x, &y).expect("should compute");
948 assert!(result.r.abs() < 0.5);
949 }
950
951 #[test]
952 fn pearson_known_value() {
953 let x = [68.0, 71.0, 62.0, 75.0, 58.0, 60.0, 67.0, 68.0, 71.0, 69.0];
955 let y = [4.1, 4.6, 3.8, 4.4, 3.2, 3.1, 3.8, 4.1, 4.3, 3.7];
956 let result = pearson(&x, &y).expect("should compute");
957 assert!((result.r - 0.8816).abs() < 0.01, "r = {}", result.r);
959 }
960
961 #[test]
970 fn pearson_numeric_reference() {
971 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
972 let y = [2.0, 4.0, 5.0, 4.0, 5.0];
973 let result = pearson(&x, &y).expect("should compute");
974
975 let expected_r = 6.0_f64 / (60.0_f64).sqrt(); assert!(
977 (result.r - expected_r).abs() < 1e-3,
978 "r expected {:.6}, got {}",
979 expected_r,
980 result.r
981 );
982 assert!(
983 (result.r - 0.7746).abs() < 1e-3,
984 "r expected ≈0.7746, got {}",
985 result.r
986 );
987 }
988
989 #[test]
990 fn pearson_insufficient_data() {
991 assert!(pearson(&[1.0, 2.0], &[3.0, 4.0]).is_none());
992 assert!(pearson(&[1.0], &[2.0]).is_none());
993 }
994
995 #[test]
996 fn pearson_length_mismatch() {
997 assert!(pearson(&[1.0, 2.0, 3.0], &[4.0, 5.0]).is_none());
998 }
999
1000 #[test]
1001 fn pearson_zero_variance() {
1002 assert!(pearson(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]).is_none());
1003 }
1004
1005 #[test]
1006 fn pearson_nan_input() {
1007 assert!(pearson(&[1.0, f64::NAN, 3.0], &[4.0, 5.0, 6.0]).is_none());
1008 }
1009
1010 #[test]
1015 fn spearman_perfect_monotone() {
1016 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1017 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let result = spearman(&x, &y).expect("should compute");
1019 assert!((result.r - 1.0).abs() < 1e-10);
1020 }
1021
1022 #[test]
1023 fn spearman_perfect_negative() {
1024 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1025 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1026 let result = spearman(&x, &y).expect("should compute");
1027 assert!((result.r + 1.0).abs() < 1e-10);
1028 }
1029
1030 #[test]
1031 fn spearman_with_ties() {
1032 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1033 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1034 let result = spearman(&x, &y).expect("should compute");
1035 assert!(result.r > 0.9); }
1037
1038 #[test]
1039 fn spearman_nonlinear_monotone() {
1040 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1042 let y: Vec<f64> = x.iter().map(|&v: &f64| v.powi(3)).collect();
1043 let result = spearman(&x, &y).expect("should compute");
1044 assert!((result.r - 1.0).abs() < 1e-10);
1045 }
1046
1047 #[test]
1052 fn kendall_perfect_concordance() {
1053 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1054 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1055 let result = kendall_tau_b(&x, &y).expect("should compute");
1056 assert!((result.r - 1.0).abs() < 1e-10);
1057 }
1058
1059 #[test]
1060 fn kendall_perfect_discordance() {
1061 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1062 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1063 let result = kendall_tau_b(&x, &y).expect("should compute");
1064 assert!((result.r + 1.0).abs() < 1e-10);
1065 }
1066
1067 #[test]
1068 fn kendall_known_value() {
1069 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1071 let y = [3.0, 4.0, 1.0, 2.0, 5.0];
1072 let result = kendall_tau_b(&x, &y).expect("should compute");
1073 assert!((result.r - 0.2).abs() < 0.01);
1074 }
1075
1076 #[test]
1077 fn kendall_with_ties() {
1078 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1079 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1080 let result = kendall_tau_b(&x, &y).expect("should compute");
1081 assert!(result.r > 0.8);
1082 }
1083
1084 #[test]
1085 fn kendall_all_ties() {
1086 let x = [1.0, 1.0, 1.0];
1087 let y = [2.0, 2.0, 2.0];
1088 assert!(kendall_tau_b(&x, &y).is_none()); }
1090
1091 #[test]
1096 fn fisher_z_zero() {
1097 let z = fisher_z(0.0).expect("should compute");
1098 assert!(z.abs() < 1e-15);
1099 }
1100
1101 #[test]
1102 fn fisher_z_roundtrip() {
1103 for &r in &[0.0, 0.3, 0.5, 0.8, 0.95, -0.5, -0.95] {
1104 let z = fisher_z(r).expect("should compute");
1105 let r_back = fisher_z_inv(z);
1106 assert!((r - r_back).abs() < 1e-10, "Roundtrip failed for r={r}");
1107 }
1108 }
1109
1110 #[test]
1111 fn fisher_z_boundary() {
1112 assert!(fisher_z(1.0).is_none());
1113 assert!(fisher_z(-1.0).is_none());
1114 assert!(fisher_z(1.5).is_none());
1115 assert!(fisher_z(f64::NAN).is_none());
1116 }
1117
1118 #[test]
1123 fn ci_contains_r() {
1124 let ci = correlation_ci(0.6, 50, 0.95).expect("should compute");
1125 assert!(ci.lower < 0.6);
1126 assert!(ci.upper > 0.6);
1127 assert!(ci.lower > -1.0);
1128 assert!(ci.upper < 1.0);
1129 }
1130
1131 #[test]
1132 fn ci_wider_at_higher_confidence() {
1133 let ci_95 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1134 let ci_99 = correlation_ci(0.5, 30, 0.99).expect("should compute");
1135 assert!(ci_99.upper - ci_99.lower > ci_95.upper - ci_95.lower);
1136 }
1137
1138 #[test]
1139 fn ci_narrower_with_more_data() {
1140 let ci_30 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1141 let ci_100 = correlation_ci(0.5, 100, 0.95).expect("should compute");
1142 assert!(ci_100.upper - ci_100.lower < ci_30.upper - ci_30.lower);
1143 }
1144
1145 #[test]
1146 fn ci_edge_cases() {
1147 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());
1151 }
1152
1153 #[test]
1158 fn corr_matrix_identity() {
1159 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1160 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
1161 let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1162 let mat = correlation_matrix(&[&x, &y, &z]).expect("should compute");
1163
1164 assert!((mat.get(0, 0) - 1.0).abs() < 1e-10);
1166 assert!((mat.get(1, 1) - 1.0).abs() < 1e-10);
1167 assert!((mat.get(2, 2) - 1.0).abs() < 1e-10);
1168
1169 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1171
1172 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1174
1175 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1177 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1178 }
1179
1180 #[test]
1181 fn corr_matrix_insufficient_variables() {
1182 let x = [1.0, 2.0, 3.0];
1183 assert!(correlation_matrix(&[&x]).is_none());
1184 }
1185
1186 #[test]
1187 fn corr_matrix_length_mismatch() {
1188 let x = [1.0, 2.0, 3.0];
1189 let y = [4.0, 5.0];
1190 assert!(correlation_matrix(&[&x, &y]).is_none());
1191 }
1192
1193 #[test]
1194 fn spearman_matrix_basic() {
1195 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1196 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1198 let mat = spearman_matrix(&[&x, &y, &z]).expect("should compute");
1199
1200 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1202 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1204 }
1205
1206 #[test]
1207 fn kendall_matrix_basic() {
1208 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1209 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");
1212
1213 assert_eq!(mat.rows(), 3);
1214 assert!((mat.get(0, 0) - 1.0).abs() < 1e-15);
1215 assert!((mat.get(1, 1) - 1.0).abs() < 1e-15);
1216 assert!((mat.get(2, 2) - 1.0).abs() < 1e-15);
1217
1218 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1219 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1220
1221 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1223 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1224 }
1225
1226 #[test]
1227 fn kendall_matrix_insufficient_variables() {
1228 let x = [1.0, 2.0, 3.0];
1229 assert!(kendall_matrix(&[&x]).is_none());
1230 }
1231
1232 #[test]
1233 fn kendall_matrix_length_mismatch() {
1234 let x = [1.0, 2.0, 3.0, 4.0];
1235 let y = [4.0, 5.0];
1236 assert!(kendall_matrix(&[&x, &y]).is_none());
1237 }
1238
1239 #[test]
1240 fn kendall_matrix_with_ties() {
1241 let x = [1.0, 2.0, 2.0, 3.0, 4.0];
1242 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1243 let mat = kendall_matrix(&[&x, &y]).expect("should compute");
1244 assert!(mat.get(0, 1) > 0.7);
1245 }
1246
1247 #[test]
1252 fn rank_no_ties() {
1253 let ranks = rank_data(&[3.0, 1.0, 2.0]);
1254 assert!((ranks[0] - 3.0).abs() < 1e-10); assert!((ranks[1] - 1.0).abs() < 1e-10); assert!((ranks[2] - 2.0).abs() < 1e-10); }
1258
1259 #[test]
1260 fn rank_with_ties() {
1261 let ranks = rank_data(&[1.0, 2.0, 2.0, 4.0]);
1262 assert!((ranks[0] - 1.0).abs() < 1e-10);
1263 assert!((ranks[1] - 2.5).abs() < 1e-10); assert!((ranks[2] - 2.5).abs() < 1e-10);
1265 assert!((ranks[3] - 4.0).abs() < 1e-10);
1266 }
1267
1268 #[test]
1269 fn rank_all_same() {
1270 let ranks = rank_data(&[5.0, 5.0, 5.0]);
1271 assert!((ranks[0] - 2.0).abs() < 1e-10); assert!((ranks[1] - 2.0).abs() < 1e-10);
1273 assert!((ranks[2] - 2.0).abs() < 1e-10);
1274 }
1275
1276 #[test]
1281 fn pvalue_significant() {
1282 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1284 let y: Vec<f64> = x.iter().map(|&v| v * 2.0 + 1.0).collect();
1285 let result = pearson(&x, &y).expect("should compute");
1286 assert!(result.p_value < 1e-10);
1287 }
1288
1289 #[test]
1290 fn pvalue_not_significant() {
1291 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1293 let y = [5.0, 1.0, 3.0, 5.0, 1.0];
1294 let result = pearson(&x, &y).expect("should compute");
1295 assert!(result.p_value > 0.3); }
1297
1298 #[test]
1303 fn acf_lag_zero_is_one() {
1304 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0];
1305 let r = acf(&data, Some(3)).expect("should compute");
1306 assert!((r.acf[0] - 1.0).abs() < 1e-10);
1307 }
1308
1309 #[test]
1310 fn acf_linear_trend() {
1311 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1313 let r = acf(&data, Some(5)).expect("should compute");
1314 assert!(r.acf[1] > 0.8, "lag-1 ACF for linear trend should be high");
1315 assert!(r.acf.len() == 6);
1316 }
1317
1318 #[test]
1319 fn acf_alternating() {
1320 let data: Vec<f64> = (0..30)
1322 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1323 .collect();
1324 let r = acf(&data, Some(5)).expect("should compute");
1325 assert!(r.acf[1] < -0.8, "alternating → negative lag-1 ACF");
1326 assert!(r.acf[2] > 0.8, "alternating → positive lag-2 ACF");
1327 }
1328
1329 #[test]
1330 fn acf_white_noise_threshold() {
1331 let data: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 13) as f64).collect();
1332 let r = acf(&data, Some(10)).expect("should compute");
1333 assert!((r.confidence_threshold - 1.96 / 10.0).abs() < 0.01);
1335 }
1336
1337 #[test]
1338 fn acf_edge_cases() {
1339 assert_eq!(
1340 acf(&[1.0], Some(3)).unwrap_err(),
1341 TimeSeriesError::TooFewObservations { needed: 2, got: 1 }
1342 );
1343 assert_eq!(
1344 acf(&[1.0, 2.0], Some(0)).unwrap_err(),
1345 TimeSeriesError::OptionOutOfRange {
1346 option: "max_lag",
1347 got: 0.0,
1348 min: 1.0,
1349 max: 1.0
1350 }
1351 );
1352 assert_eq!(
1353 acf(&[5.0, 5.0, 5.0, 5.0], Some(2)).unwrap_err(),
1354 TimeSeriesError::NoVariation
1355 );
1356 assert_eq!(
1358 acf(&[f64::NAN], Some(1)).unwrap_err(),
1359 TimeSeriesError::ValueNotFinite { index: 0 }
1360 );
1361 }
1362
1363 #[test]
1364 fn acf_max_lag_is_refused_past_n_minus_1_and_defaults_to_ten_log_n() {
1365 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1366 assert_eq!(
1367 acf(&data, Some(100)).unwrap_err(),
1368 TimeSeriesError::OptionOutOfRange {
1369 option: "max_lag",
1370 got: 100.0,
1371 min: 1.0,
1372 max: 4.0
1373 }
1374 );
1375 assert_eq!(acf(&data, None).expect("defined").acf.len(), 5);
1377 let long: Vec<f64> = (0..100).map(|i| ((i * 37) % 11) as f64).collect();
1378 assert_eq!(acf(&long, None).expect("defined").acf.len(), 21);
1379 }
1380
1381 #[test]
1386 fn pacf_lag1_equals_acf_lag1() {
1387 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1388 let acf_r = acf(&data, Some(4)).expect("should compute ACF");
1389 let pacf_r = pacf(&data, Some(4)).expect("should compute PACF");
1390 assert!(
1391 (pacf_r.pacf[0] - acf_r.acf[1]).abs() < 1e-10,
1392 "PACF[1] should equal ACF[1]: {} vs {}",
1393 pacf_r.pacf[0],
1394 acf_r.acf[1]
1395 );
1396 }
1397
1398 #[test]
1399 fn pacf_linear_trend() {
1400 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1401 let r = pacf(&data, Some(5)).expect("should compute");
1402 assert!(r.pacf[0].abs() > 0.5, "lag-1 PACF should be strong");
1404 }
1405
1406 #[test]
1407 fn pacf_bounded() {
1408 let data: Vec<f64> = (0..30).map(|i| (i as f64 * 0.5).sin()).collect();
1409 let r = pacf(&data, Some(10)).expect("should compute");
1410 for (i, &p) in r.pacf.iter().enumerate() {
1411 assert!(
1412 (-1.0..=1.0).contains(&p),
1413 "PACF[{}] = {} out of [-1, 1]",
1414 i + 1,
1415 p
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn pacf_edge_cases() {
1422 assert_eq!(
1423 pacf(&[1.0, 2.0, 3.0], Some(1)).unwrap_err(),
1424 TimeSeriesError::TooFewObservations { needed: 4, got: 3 }
1425 );
1426 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1428 for k in [0, 5] {
1429 assert_eq!(
1430 pacf(&data, Some(k)).unwrap_err(),
1431 TimeSeriesError::OptionOutOfRange {
1432 option: "max_lag",
1433 got: k as f64,
1434 min: 1.0,
1435 max: 4.0
1436 }
1437 );
1438 }
1439 assert_eq!(pacf(&data, None).expect("defined").pacf.len(), 4);
1440 }
1441}
1442
1443#[cfg(test)]
1444mod proptests {
1445 use super::*;
1446 use proptest::prelude::*;
1447
1448 fn bounded_vec(min_len: usize, max_len: usize) -> BoxedStrategy<Vec<f64>> {
1449 proptest::collection::vec(-1e6_f64..1e6, min_len..=max_len).boxed()
1450 }
1451
1452 proptest! {
1453 #[test]
1454 fn pearson_bounded(
1455 data in bounded_vec(5, 50).prop_flat_map(|x| {
1456 let n = x.len();
1457 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1458 })
1459 ) {
1460 let (x, y) = data;
1461 if let Some(result) = pearson(&x, &y) {
1462 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1463 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1464 }
1465 }
1466
1467 #[test]
1468 fn spearman_bounded(
1469 data in bounded_vec(5, 50).prop_flat_map(|x| {
1470 let n = x.len();
1471 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1472 })
1473 ) {
1474 let (x, y) = data;
1475 if let Some(result) = spearman(&x, &y) {
1476 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1477 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1478 }
1479 }
1480
1481 #[test]
1482 fn kendall_bounded(
1483 data in bounded_vec(5, 30).prop_flat_map(|x| {
1484 let n = x.len();
1485 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1486 })
1487 ) {
1488 let (x, y) = data;
1489 if let Some(result) = kendall_tau_b(&x, &y) {
1490 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "tau out of bounds: {}", result.r);
1491 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1492 }
1493 }
1494
1495 #[test]
1496 fn pearson_symmetric(
1497 data in bounded_vec(5, 50).prop_flat_map(|x| {
1498 let n = x.len();
1499 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1500 })
1501 ) {
1502 let (x, y) = data;
1503 let r_xy = pearson(&x, &y);
1504 let r_yx = pearson(&y, &x);
1505 match (r_xy, r_yx) {
1506 (Some(a), Some(b)) => {
1507 prop_assert!((a.r - b.r).abs() < 1e-10, "not symmetric: {} vs {}", a.r, b.r);
1508 }
1509 (None, None) => {}
1510 _ => prop_assert!(false, "one is None but not the other"),
1511 }
1512 }
1513
1514 #[test]
1515 fn fisher_z_roundtrip_prop(r in -0.99_f64..0.99) {
1516 let z = fisher_z(r).expect("should compute");
1517 let r_back = fisher_z_inv(z);
1518 prop_assert!((r - r_back).abs() < 1e-10);
1519 }
1520
1521 #[test]
1522 fn ci_contains_true_r(
1523 r in -0.99_f64..0.99,
1524 n in 10_usize..200
1525 ) {
1526 let ci = correlation_ci(r, n, 0.95).expect("should compute");
1527 prop_assert!(ci.lower < ci.upper, "CI inverted");
1528 prop_assert!(ci.lower >= -1.0 && ci.upper <= 1.0, "CI out of bounds");
1529 }
1530
1531 #[test]
1532 fn acf_bounded_prop(
1533 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1534 ) {
1535 if let Ok(r) = acf(&data, Some(10)) {
1536 prop_assert!((r.acf[0] - 1.0).abs() < 1e-10, "ACF[0] must be 1.0");
1537 for (i, &v) in r.acf.iter().enumerate() {
1538 prop_assert!((-1.0..=1.0).contains(&v), "ACF[{i}] = {v} out of [-1,1]");
1539 }
1540 }
1541 }
1542
1543 #[test]
1544 fn pacf_bounded_prop(
1545 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1546 ) {
1547 if let Ok(r) = pacf(&data, Some(5)) {
1548 for (i, &v) in r.pacf.iter().enumerate() {
1549 prop_assert!((-1.0 - 1e-10..=1.0 + 1e-10).contains(&v), "PACF[{i}] = {v} out of bounds");
1550 }
1551 }
1552 }
1553 }
1554}
1555
1556#[cfg(test)]
1558mod scipy_tail_reference {
1559 use super::*;
1560
1561 #[test]
1562 fn pearson_p_value_keeps_its_tail() {
1563 let x: Vec<f64> = (1..=20).map(f64::from).collect();
1564 let y = [
1565 5.3999999999999995,
1566 7.0,
1567 9.4,
1568 11.8,
1569 13.2,
1570 15.8,
1571 17.6,
1572 19.8,
1573 22.250000000000004,
1574 23.75,
1575 26.25,
1576 28.150000000000002,
1577 30.55,
1578 32.050000000000004,
1579 34.55,
1580 36.7,
1581 38.550000000000004,
1582 41.1,
1583 42.699999999999996,
1584 45.0,
1585 ];
1586 let got = pearson(&x, &y).expect("valid").p_value;
1587 let want = 8.20335692044105e-33;
1588 assert!(
1589 (got - want).abs() <= 1e-9 * want,
1590 "p: got {got:e}, SciPy {want:e}"
1591 );
1592 }
1593}