1use crate::error::{check_option, check_series, TimeSeriesError};
26use u_numflow::matrix::Matrix;
27use u_numflow::special;
28use u_numflow::stats;
29
30#[derive(Debug, Clone, Copy)]
32pub struct CorrelationResult {
33 pub r: f64,
35 pub p_value: f64,
37 pub n: usize,
39}
40
41#[derive(Debug, Clone, Copy)]
43pub struct CorrelationCI {
44 pub lower: f64,
46 pub upper: f64,
48 pub confidence: f64,
50}
51
52pub fn pearson(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
85 let n = x.len();
86 if n < 3 || n != y.len() {
87 return None;
88 }
89
90 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
91 return None;
92 }
93
94 let cov = stats::covariance(x, y)?;
95 let sx = stats::std_dev(x)?;
96 let sy = stats::std_dev(y)?;
97
98 if sx < 1e-300 || sy < 1e-300 {
99 return None; }
101
102 let r = (cov / (sx * sy)).clamp(-1.0, 1.0);
103 let p_value = correlation_p_value(r, n);
104
105 Some(CorrelationResult { r, p_value, n })
106}
107
108pub fn spearman(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
141 let n = x.len();
142 if n < 3 || n != y.len() {
143 return None;
144 }
145
146 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
147 return None;
148 }
149
150 let rx = rank_data(x);
151 let ry = rank_data(y);
152
153 let cov = stats::covariance(&rx, &ry)?;
155 let srx = stats::std_dev(&rx)?;
156 let sry = stats::std_dev(&ry)?;
157
158 if srx < 1e-300 || sry < 1e-300 {
159 return None;
160 }
161
162 let r = (cov / (srx * sry)).clamp(-1.0, 1.0);
163 let p_value = correlation_p_value(r, n);
164
165 Some(CorrelationResult { r, p_value, n })
166}
167
168pub fn kendall_tau_b(x: &[f64], y: &[f64]) -> Option<CorrelationResult> {
207 let n = x.len();
208 if n < 3 || n != y.len() {
209 return None;
210 }
211
212 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
213 return None;
214 }
215
216 let mut concordant: i64 = 0;
217 let mut discordant: i64 = 0;
218 let mut ties_x: i64 = 0;
219 let mut ties_y: i64 = 0;
220 let mut _ties_xy: i64 = 0;
221
222 for i in 0..n {
223 for j in (i + 1)..n {
224 let dx = x[i] - x[j];
225 let dy = y[i] - y[j];
226 let product = dx * dy;
227
228 if dx == 0.0 && dy == 0.0 {
229 _ties_xy += 1;
230 ties_x += 1;
231 ties_y += 1;
232 } else if dx == 0.0 {
233 ties_x += 1;
234 } else if dy == 0.0 {
235 ties_y += 1;
236 } else if product > 0.0 {
237 concordant += 1;
238 } else {
239 discordant += 1;
240 }
241 }
242 }
243
244 let n0 = (n as i64) * (n as i64 - 1) / 2;
245 let denom_sq = (n0 - ties_x) as f64 * (n0 - ties_y) as f64;
246
247 if denom_sq <= 0.0 {
248 return None; }
250
251 let tau = (concordant - discordant) as f64 / denom_sq.sqrt();
252 let tau = tau.clamp(-1.0, 1.0);
253
254 let nf = n as f64;
259 let v0 = nf * (nf - 1.0) * (2.0 * nf + 5.0);
260 let vt = compute_tie_variance_term(x);
261 let vu = compute_tie_variance_term(y);
262 let var_s = (v0 - vt - vu) / 18.0;
263
264 let p_value = if var_s > 0.0 {
265 let s = (concordant - discordant) as f64;
266 let z = s / var_s.sqrt();
267 2.0 * special::standard_normal_sf(z.abs())
268 } else {
269 1.0
270 };
271
272 Some(CorrelationResult { r: tau, p_value, n })
273}
274
275pub fn fisher_z(r: f64) -> Option<f64> {
293 if r <= -1.0 || r >= 1.0 || !r.is_finite() {
294 return None;
295 }
296 Some(r.atanh())
297}
298
299pub fn fisher_z_inv(z: f64) -> f64 {
301 z.tanh()
302}
303
304pub fn correlation_ci(r: f64, n: usize, confidence: f64) -> Option<CorrelationCI> {
329 if n < 4 || r <= -1.0 || r >= 1.0 || !r.is_finite() {
330 return None;
331 }
332 if confidence <= 0.0 || confidence >= 1.0 {
333 return None;
334 }
335
336 let z = r.atanh();
337 let se = 1.0 / ((n as f64 - 3.0).sqrt());
338 let alpha = 1.0 - confidence;
339 let z_crit = special::inverse_normal_cdf(1.0 - alpha / 2.0);
340
341 let z_lower = z - z_crit * se;
342 let z_upper = z + z_crit * se;
343
344 Some(CorrelationCI {
345 lower: z_lower.tanh(),
346 upper: z_upper.tanh(),
347 confidence,
348 })
349}
350
351pub fn correlation_matrix(variables: &[&[f64]]) -> Option<Matrix> {
381 let p = variables.len();
382 if p < 2 {
383 return None;
384 }
385 let n = variables[0].len();
386 if n < 3 {
387 return None;
388 }
389 for v in variables {
390 if v.len() != n {
391 return None;
392 }
393 }
394
395 let mut sds = Vec::with_capacity(p);
397 for v in variables {
398 let m = stats::mean(v)?;
399 if !m.is_finite() {
400 return None;
401 }
402 let s = stats::std_dev(v)?;
403 if !s.is_finite() || s < 1e-300 {
404 return None;
405 }
406 sds.push(s);
407 }
408
409 let mut data = vec![0.0; p * p];
410
411 for i in 0..p {
412 data[i * p + i] = 1.0; for j in (i + 1)..p {
414 let cov = stats::covariance(variables[i], variables[j])?;
415 let r = (cov / (sds[i] * sds[j])).clamp(-1.0, 1.0);
416 data[i * p + j] = r;
417 data[j * p + i] = r;
418 }
419 }
420
421 Matrix::new(p, p, data).ok()
422}
423
424pub fn spearman_matrix(variables: &[&[f64]]) -> Option<Matrix> {
428 let p = variables.len();
429 if p < 2 {
430 return None;
431 }
432 let n = variables[0].len();
433 if n < 3 {
434 return None;
435 }
436 for v in variables {
437 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
438 return None;
439 }
440 }
441
442 let ranked: Vec<Vec<f64>> = variables.iter().map(|v| rank_data(v)).collect();
444 let ranked_refs: Vec<&[f64]> = ranked.iter().map(|v| v.as_slice()).collect();
445
446 correlation_matrix(&ranked_refs)
447}
448
449pub fn kendall_matrix(variables: &[&[f64]]) -> Option<Matrix> {
479 let p = variables.len();
480 if p < 2 {
481 return None;
482 }
483 let n = variables[0].len();
484 if n < 3 {
485 return None;
486 }
487 for v in variables {
488 if v.len() != n || v.iter().any(|x| !x.is_finite()) {
489 return None;
490 }
491 }
492
493 let mut data = vec![0.0_f64; p * p];
494 for i in 0..p {
495 data[i * p + i] = 1.0;
496 for j in (i + 1)..p {
497 let r = kendall_tau_b(variables[i], variables[j])?.r;
498 data[i * p + j] = r;
499 data[j * p + i] = r;
500 }
501 }
502 Matrix::new(p, p, data).ok()
503}
504
505fn correlation_p_value(r: f64, n: usize) -> f64 {
513 if n < 3 {
514 return 1.0;
515 }
516 let df = (n - 2) as f64;
517 let r2 = r * r;
518
519 if r2 >= 1.0 - 1e-15 {
521 return 0.0; }
523
524 let t = r * (df / (1.0 - r2)).sqrt();
525 2.0 * special::t_distribution_sf(t.abs(), df)
526}
527
528fn rank_data(data: &[f64]) -> Vec<f64> {
532 let n = data.len();
533 let mut indexed: Vec<(usize, f64)> = data.iter().copied().enumerate().collect();
534 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
535
536 let mut ranks = vec![0.0; n];
537 let mut i = 0;
538 while i < n {
539 let mut j = i;
540 while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-300 {
542 j += 1;
543 }
544 let avg_rank = (i + j) as f64 / 2.0 + 0.5;
546 for item in indexed.iter().take(j).skip(i) {
547 ranks[item.0] = avg_rank;
548 }
549 i = j;
550 }
551
552 ranks
553}
554
555fn compute_tie_variance_term(data: &[f64]) -> f64 {
559 let mut sorted: Vec<f64> = data.to_vec();
560 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
561
562 let mut result = 0.0;
563 let mut i = 0;
564 while i < sorted.len() {
565 let mut j = i;
566 while j < sorted.len() && (sorted[j] - sorted[i]).abs() < 1e-300 {
567 j += 1;
568 }
569 let t = (j - i) as f64;
570 if t > 1.0 {
571 result += t * (t - 1.0) * (2.0 * t + 5.0);
572 }
573 i = j;
574 }
575 result
576}
577
578#[derive(Debug, Clone)]
584pub struct AcfResult {
585 pub acf: Vec<f64>,
588 pub confidence_threshold: f64,
591 pub bartlett_limits: Vec<f64>,
596}
597
598#[derive(Debug, Clone)]
600pub struct PacfResult {
601 pub pacf: Vec<f64>,
603 pub confidence_threshold: f64,
606}
607
608const Z_975: f64 = 1.959_963_984_540_054;
610
611pub const ACF_MIN_OBSERVATIONS: usize = 2;
613
614pub const PACF_MIN_OBSERVATIONS: usize = 4;
616
617fn lag_range(max_lag: Option<usize>, n: usize, highest: usize) -> Result<usize, TimeSeriesError> {
621 match max_lag {
622 None => Ok((((n as f64).log10() * 10.0).floor() as usize).clamp(1, highest)),
623 Some(k) => check_option("max_lag", k, 1, highest),
624 }
625}
626
627pub fn acf(data: &[f64], max_lag: Option<usize>) -> Result<AcfResult, TimeSeriesError> {
658 check_series(data, ACF_MIN_OBSERVATIONS)?;
659 let n = data.len();
660 let max_lag = lag_range(max_lag, n, n - 1)?;
661 let nf = n as f64;
662
663 let mean = data.iter().sum::<f64>() / nf;
664
665 let c0: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
667
668 let mean_square: f64 = data.iter().map(|&x| x * x).sum::<f64>() / nf;
671 if c0 <= 1e-20 * mean_square {
672 return Err(TimeSeriesError::NoVariation);
673 }
674
675 let mut acf_vals = Vec::with_capacity(max_lag + 1);
676 acf_vals.push(1.0); for lag in 1..=max_lag {
679 let ck: f64 = data[..n - lag]
680 .iter()
681 .zip(&data[lag..])
682 .map(|(&xt, &xt_h)| (xt - mean) * (xt_h - mean))
683 .sum::<f64>()
684 / nf;
685 acf_vals.push(ck / c0);
686 }
687
688 let threshold = Z_975 / nf.sqrt();
689 let mut cumulative = 0.0;
690 let bartlett_limits = acf_vals[1..]
691 .iter()
692 .map(|r| {
693 let limit = Z_975 * ((1.0 + 2.0 * cumulative) / nf).sqrt();
694 cumulative += r * r;
695 limit
696 })
697 .collect();
698
699 Ok(AcfResult {
700 acf: acf_vals,
701 confidence_threshold: threshold,
702 bartlett_limits,
703 })
704}
705
706pub fn pacf(data: &[f64], max_lag: Option<usize>) -> Result<PacfResult, TimeSeriesError> {
741 check_series(data, PACF_MIN_OBSERVATIONS)?;
742 let n = data.len();
743 let max_lag = lag_range(max_lag, n, n / 2 - 1)?;
744
745 let acf_result = acf(data, Some(max_lag))?;
748 let rho = &acf_result.acf;
749
750 let mut pacf_vals = Vec::with_capacity(max_lag);
751
752 pacf_vals.push(rho[1]);
754
755 if max_lag == 1 {
756 return Ok(PacfResult {
757 pacf: pacf_vals,
758 confidence_threshold: Z_975 / (n as f64).sqrt(),
759 });
760 }
761
762 let mut phi_prev = vec![rho[1]];
764
765 for h in 2..=max_lag {
766 let mut num = rho[h];
768 for j in 0..h - 1 {
769 num -= phi_prev[j] * rho[h - 1 - j];
770 }
771
772 let mut den = 1.0;
774 for j in 0..h - 1 {
775 den -= phi_prev[j] * rho[j + 1];
776 }
777
778 let phi_hh = if den.abs() > 1e-14 { num / den } else { 0.0 };
779
780 let mut phi_new = Vec::with_capacity(h);
782 for j in 0..h - 1 {
783 phi_new.push(phi_prev[j] - phi_hh * phi_prev[h - 2 - j]);
784 }
785 phi_new.push(phi_hh);
786
787 pacf_vals.push(phi_hh);
788 phi_prev = phi_new;
789 }
790
791 Ok(PacfResult {
792 pacf: pacf_vals,
793 confidence_threshold: Z_975 / (n as f64).sqrt(),
794 })
795}
796
797#[cfg(test)]
800mod statsmodels_reference {
801 use super::*;
802
803 const X: [f64; 24] = [
804 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,
805 3.0, 4.2, 5.1, 4.5, 3.4, 2.9,
806 ];
807
808 fn close(got: &[f64], want: &[f64], what: &str) {
809 assert_eq!(got.len(), want.len(), "{what}");
810 for (k, (g, w)) in got.iter().zip(want).enumerate() {
811 assert!((g - w).abs() < 1e-12, "{what}[{k}]: {g} vs {w}");
812 }
813 }
814
815 #[test]
816 fn acf_and_bartlett_limits_reproduce_statsmodels() {
817 let r = acf(&X, Some(8)).unwrap();
818 close(
819 &r.acf,
820 &[
821 1.0,
822 0.486403576681892,
823 -0.36186808082372074,
824 -0.6951594797553611,
825 -0.2634125570953007,
826 0.42164395757528844,
827 0.6593055663079663,
828 0.26106100487729356,
829 -0.3171014941549895,
830 ],
831 "acf",
832 );
833 close(
834 &r.bartlett_limits,
835 &[
836 0.40007597302960923,
837 0.48559020426940086,
838 0.5269888881332575,
839 0.6575826795727397,
840 0.6742603293351266,
841 0.7152198020271394,
842 0.80665422242141,
843 0.8200659919957791,
844 ],
845 "bartlett",
846 );
847 assert!((r.confidence_threshold - 0.40007597302960923).abs() < 1e-12);
848 }
849
850 #[test]
851 fn pacf_reproduces_statsmodels_ldb() {
852 let r = pacf(&X, Some(8)).unwrap();
853 close(
854 &r.pacf,
855 &[
856 0.486403576681892,
857 -0.7839238375815395,
858 0.00047345408884359627,
859 0.1902495235598503,
860 0.19973005383776718,
861 0.1225767860916582,
862 0.05567632845800997,
863 0.03539504608148146,
864 ],
865 "pacf",
866 );
867 }
868
869 #[test]
870 fn a_small_unit_is_not_a_constant_series() {
871 let small: Vec<f64> = X.iter().map(|v| v * 1e-150).collect();
872 let r = acf(&small, Some(8)).unwrap();
873 assert!((r.acf[1] - 0.486403576681892).abs() < 1e-12);
874 assert_eq!(
875 acf(&[3.0; 10], Some(3)).unwrap_err(),
876 TimeSeriesError::NoVariation
877 );
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884
885 #[test]
890 fn pearson_perfect_positive() {
891 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
892 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
893 let result = pearson(&x, &y).expect("should compute");
894 assert!((result.r - 1.0).abs() < 1e-10);
895 assert!(result.p_value < 1e-10);
896 }
897
898 #[test]
899 fn pearson_perfect_negative() {
900 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
901 let y = [10.0, 8.0, 6.0, 4.0, 2.0];
902 let result = pearson(&x, &y).expect("should compute");
903 assert!((result.r + 1.0).abs() < 1e-10);
904 assert!(result.p_value < 1e-10);
905 }
906
907 #[test]
908 fn pearson_uncorrelated() {
909 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
911 let y = [3.0, 1.0, 5.0, 1.0, 5.0];
912 let result = pearson(&x, &y).expect("should compute");
913 assert!(result.r.abs() < 0.5);
914 }
915
916 #[test]
917 fn pearson_known_value() {
918 let x = [68.0, 71.0, 62.0, 75.0, 58.0, 60.0, 67.0, 68.0, 71.0, 69.0];
920 let y = [4.1, 4.6, 3.8, 4.4, 3.2, 3.1, 3.8, 4.1, 4.3, 3.7];
921 let result = pearson(&x, &y).expect("should compute");
922 assert!((result.r - 0.8816).abs() < 0.01, "r = {}", result.r);
924 }
925
926 #[test]
935 fn pearson_numeric_reference() {
936 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
937 let y = [2.0, 4.0, 5.0, 4.0, 5.0];
938 let result = pearson(&x, &y).expect("should compute");
939
940 let expected_r = 6.0_f64 / (60.0_f64).sqrt(); assert!(
942 (result.r - expected_r).abs() < 1e-3,
943 "r expected {:.6}, got {}",
944 expected_r,
945 result.r
946 );
947 assert!(
948 (result.r - 0.7746).abs() < 1e-3,
949 "r expected ≈0.7746, got {}",
950 result.r
951 );
952 }
953
954 #[test]
955 fn pearson_insufficient_data() {
956 assert!(pearson(&[1.0, 2.0], &[3.0, 4.0]).is_none());
957 assert!(pearson(&[1.0], &[2.0]).is_none());
958 }
959
960 #[test]
961 fn pearson_length_mismatch() {
962 assert!(pearson(&[1.0, 2.0, 3.0], &[4.0, 5.0]).is_none());
963 }
964
965 #[test]
966 fn pearson_zero_variance() {
967 assert!(pearson(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]).is_none());
968 }
969
970 #[test]
971 fn pearson_nan_input() {
972 assert!(pearson(&[1.0, f64::NAN, 3.0], &[4.0, 5.0, 6.0]).is_none());
973 }
974
975 #[test]
980 fn spearman_perfect_monotone() {
981 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
982 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let result = spearman(&x, &y).expect("should compute");
984 assert!((result.r - 1.0).abs() < 1e-10);
985 }
986
987 #[test]
988 fn spearman_perfect_negative() {
989 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
990 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
991 let result = spearman(&x, &y).expect("should compute");
992 assert!((result.r + 1.0).abs() < 1e-10);
993 }
994
995 #[test]
996 fn spearman_with_ties() {
997 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
998 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
999 let result = spearman(&x, &y).expect("should compute");
1000 assert!(result.r > 0.9); }
1002
1003 #[test]
1004 fn spearman_nonlinear_monotone() {
1005 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1007 let y: Vec<f64> = x.iter().map(|&v: &f64| v.powi(3)).collect();
1008 let result = spearman(&x, &y).expect("should compute");
1009 assert!((result.r - 1.0).abs() < 1e-10);
1010 }
1011
1012 #[test]
1017 fn kendall_perfect_concordance() {
1018 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1019 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1020 let result = kendall_tau_b(&x, &y).expect("should compute");
1021 assert!((result.r - 1.0).abs() < 1e-10);
1022 }
1023
1024 #[test]
1025 fn kendall_perfect_discordance() {
1026 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1027 let y = [5.0, 4.0, 3.0, 2.0, 1.0];
1028 let result = kendall_tau_b(&x, &y).expect("should compute");
1029 assert!((result.r + 1.0).abs() < 1e-10);
1030 }
1031
1032 #[test]
1033 fn kendall_known_value() {
1034 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1036 let y = [3.0, 4.0, 1.0, 2.0, 5.0];
1037 let result = kendall_tau_b(&x, &y).expect("should compute");
1038 assert!((result.r - 0.2).abs() < 0.01);
1039 }
1040
1041 #[test]
1042 fn kendall_with_ties() {
1043 let x = [1.0, 2.0, 2.0, 4.0, 5.0];
1044 let y = [1.0, 3.0, 3.0, 4.0, 5.0];
1045 let result = kendall_tau_b(&x, &y).expect("should compute");
1046 assert!(result.r > 0.8);
1047 }
1048
1049 #[test]
1050 fn kendall_all_ties() {
1051 let x = [1.0, 1.0, 1.0];
1052 let y = [2.0, 2.0, 2.0];
1053 assert!(kendall_tau_b(&x, &y).is_none()); }
1055
1056 #[test]
1061 fn fisher_z_zero() {
1062 let z = fisher_z(0.0).expect("should compute");
1063 assert!(z.abs() < 1e-15);
1064 }
1065
1066 #[test]
1067 fn fisher_z_roundtrip() {
1068 for &r in &[0.0, 0.3, 0.5, 0.8, 0.95, -0.5, -0.95] {
1069 let z = fisher_z(r).expect("should compute");
1070 let r_back = fisher_z_inv(z);
1071 assert!((r - r_back).abs() < 1e-10, "Roundtrip failed for r={r}");
1072 }
1073 }
1074
1075 #[test]
1076 fn fisher_z_boundary() {
1077 assert!(fisher_z(1.0).is_none());
1078 assert!(fisher_z(-1.0).is_none());
1079 assert!(fisher_z(1.5).is_none());
1080 assert!(fisher_z(f64::NAN).is_none());
1081 }
1082
1083 #[test]
1088 fn ci_contains_r() {
1089 let ci = correlation_ci(0.6, 50, 0.95).expect("should compute");
1090 assert!(ci.lower < 0.6);
1091 assert!(ci.upper > 0.6);
1092 assert!(ci.lower > -1.0);
1093 assert!(ci.upper < 1.0);
1094 }
1095
1096 #[test]
1097 fn ci_wider_at_higher_confidence() {
1098 let ci_95 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1099 let ci_99 = correlation_ci(0.5, 30, 0.99).expect("should compute");
1100 assert!(ci_99.upper - ci_99.lower > ci_95.upper - ci_95.lower);
1101 }
1102
1103 #[test]
1104 fn ci_narrower_with_more_data() {
1105 let ci_30 = correlation_ci(0.5, 30, 0.95).expect("should compute");
1106 let ci_100 = correlation_ci(0.5, 100, 0.95).expect("should compute");
1107 assert!(ci_100.upper - ci_100.lower < ci_30.upper - ci_30.lower);
1108 }
1109
1110 #[test]
1111 fn ci_edge_cases() {
1112 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());
1116 }
1117
1118 #[test]
1123 fn corr_matrix_identity() {
1124 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1125 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
1126 let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1127 let mat = correlation_matrix(&[&x, &y, &z]).expect("should compute");
1128
1129 assert!((mat.get(0, 0) - 1.0).abs() < 1e-10);
1131 assert!((mat.get(1, 1) - 1.0).abs() < 1e-10);
1132 assert!((mat.get(2, 2) - 1.0).abs() < 1e-10);
1133
1134 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1136
1137 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1139
1140 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1142 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1143 }
1144
1145 #[test]
1146 fn corr_matrix_insufficient_variables() {
1147 let x = [1.0, 2.0, 3.0];
1148 assert!(correlation_matrix(&[&x]).is_none());
1149 }
1150
1151 #[test]
1152 fn corr_matrix_length_mismatch() {
1153 let x = [1.0, 2.0, 3.0];
1154 let y = [4.0, 5.0];
1155 assert!(correlation_matrix(&[&x, &y]).is_none());
1156 }
1157
1158 #[test]
1159 fn spearman_matrix_basic() {
1160 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1161 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; let z = [5.0, 4.0, 3.0, 2.0, 1.0];
1163 let mat = spearman_matrix(&[&x, &y, &z]).expect("should compute");
1164
1165 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1167 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1169 }
1170
1171 #[test]
1172 fn kendall_matrix_basic() {
1173 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1174 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");
1177
1178 assert_eq!(mat.rows(), 3);
1179 assert!((mat.get(0, 0) - 1.0).abs() < 1e-15);
1180 assert!((mat.get(1, 1) - 1.0).abs() < 1e-15);
1181 assert!((mat.get(2, 2) - 1.0).abs() < 1e-15);
1182
1183 assert!((mat.get(0, 1) - 1.0).abs() < 1e-10);
1184 assert!((mat.get(0, 2) + 1.0).abs() < 1e-10);
1185
1186 assert!((mat.get(0, 1) - mat.get(1, 0)).abs() < 1e-15);
1188 assert!((mat.get(0, 2) - mat.get(2, 0)).abs() < 1e-15);
1189 }
1190
1191 #[test]
1192 fn kendall_matrix_insufficient_variables() {
1193 let x = [1.0, 2.0, 3.0];
1194 assert!(kendall_matrix(&[&x]).is_none());
1195 }
1196
1197 #[test]
1198 fn kendall_matrix_length_mismatch() {
1199 let x = [1.0, 2.0, 3.0, 4.0];
1200 let y = [4.0, 5.0];
1201 assert!(kendall_matrix(&[&x, &y]).is_none());
1202 }
1203
1204 #[test]
1205 fn kendall_matrix_with_ties() {
1206 let x = [1.0, 2.0, 2.0, 3.0, 4.0];
1207 let y = [1.0, 2.0, 3.0, 4.0, 5.0];
1208 let mat = kendall_matrix(&[&x, &y]).expect("should compute");
1209 assert!(mat.get(0, 1) > 0.7);
1210 }
1211
1212 #[test]
1217 fn rank_no_ties() {
1218 let ranks = rank_data(&[3.0, 1.0, 2.0]);
1219 assert!((ranks[0] - 3.0).abs() < 1e-10); assert!((ranks[1] - 1.0).abs() < 1e-10); assert!((ranks[2] - 2.0).abs() < 1e-10); }
1223
1224 #[test]
1225 fn rank_with_ties() {
1226 let ranks = rank_data(&[1.0, 2.0, 2.0, 4.0]);
1227 assert!((ranks[0] - 1.0).abs() < 1e-10);
1228 assert!((ranks[1] - 2.5).abs() < 1e-10); assert!((ranks[2] - 2.5).abs() < 1e-10);
1230 assert!((ranks[3] - 4.0).abs() < 1e-10);
1231 }
1232
1233 #[test]
1234 fn rank_all_same() {
1235 let ranks = rank_data(&[5.0, 5.0, 5.0]);
1236 assert!((ranks[0] - 2.0).abs() < 1e-10); assert!((ranks[1] - 2.0).abs() < 1e-10);
1238 assert!((ranks[2] - 2.0).abs() < 1e-10);
1239 }
1240
1241 #[test]
1246 fn pvalue_significant() {
1247 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1249 let y: Vec<f64> = x.iter().map(|&v| v * 2.0 + 1.0).collect();
1250 let result = pearson(&x, &y).expect("should compute");
1251 assert!(result.p_value < 1e-10);
1252 }
1253
1254 #[test]
1255 fn pvalue_not_significant() {
1256 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1258 let y = [5.0, 1.0, 3.0, 5.0, 1.0];
1259 let result = pearson(&x, &y).expect("should compute");
1260 assert!(result.p_value > 0.3); }
1262
1263 #[test]
1268 fn acf_lag_zero_is_one() {
1269 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0];
1270 let r = acf(&data, Some(3)).expect("should compute");
1271 assert!((r.acf[0] - 1.0).abs() < 1e-10);
1272 }
1273
1274 #[test]
1275 fn acf_linear_trend() {
1276 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1278 let r = acf(&data, Some(5)).expect("should compute");
1279 assert!(r.acf[1] > 0.8, "lag-1 ACF for linear trend should be high");
1280 assert!(r.acf.len() == 6);
1281 }
1282
1283 #[test]
1284 fn acf_alternating() {
1285 let data: Vec<f64> = (0..30)
1287 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1288 .collect();
1289 let r = acf(&data, Some(5)).expect("should compute");
1290 assert!(r.acf[1] < -0.8, "alternating → negative lag-1 ACF");
1291 assert!(r.acf[2] > 0.8, "alternating → positive lag-2 ACF");
1292 }
1293
1294 #[test]
1295 fn acf_white_noise_threshold() {
1296 let data: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 13) as f64).collect();
1297 let r = acf(&data, Some(10)).expect("should compute");
1298 assert!((r.confidence_threshold - 1.96 / 10.0).abs() < 0.01);
1300 }
1301
1302 #[test]
1303 fn acf_edge_cases() {
1304 assert_eq!(
1305 acf(&[1.0], Some(3)).unwrap_err(),
1306 TimeSeriesError::TooFewObservations { needed: 2, got: 1 }
1307 );
1308 assert_eq!(
1309 acf(&[1.0, 2.0], Some(0)).unwrap_err(),
1310 TimeSeriesError::OptionOutOfRange {
1311 option: "max_lag",
1312 got: 0.0,
1313 min: 1.0,
1314 max: 1.0
1315 }
1316 );
1317 assert_eq!(
1318 acf(&[5.0, 5.0, 5.0, 5.0], Some(2)).unwrap_err(),
1319 TimeSeriesError::NoVariation
1320 );
1321 assert_eq!(
1323 acf(&[f64::NAN], Some(1)).unwrap_err(),
1324 TimeSeriesError::ValueNotFinite { index: 0 }
1325 );
1326 }
1327
1328 #[test]
1329 fn acf_max_lag_is_refused_past_n_minus_1_and_defaults_to_ten_log_n() {
1330 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1331 assert_eq!(
1332 acf(&data, Some(100)).unwrap_err(),
1333 TimeSeriesError::OptionOutOfRange {
1334 option: "max_lag",
1335 got: 100.0,
1336 min: 1.0,
1337 max: 4.0
1338 }
1339 );
1340 assert_eq!(acf(&data, None).expect("defined").acf.len(), 5);
1342 let long: Vec<f64> = (0..100).map(|i| ((i * 37) % 11) as f64).collect();
1343 assert_eq!(acf(&long, None).expect("defined").acf.len(), 21);
1344 }
1345
1346 #[test]
1351 fn pacf_lag1_equals_acf_lag1() {
1352 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1353 let acf_r = acf(&data, Some(4)).expect("should compute ACF");
1354 let pacf_r = pacf(&data, Some(4)).expect("should compute PACF");
1355 assert!(
1356 (pacf_r.pacf[0] - acf_r.acf[1]).abs() < 1e-10,
1357 "PACF[1] should equal ACF[1]: {} vs {}",
1358 pacf_r.pacf[0],
1359 acf_r.acf[1]
1360 );
1361 }
1362
1363 #[test]
1364 fn pacf_linear_trend() {
1365 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1366 let r = pacf(&data, Some(5)).expect("should compute");
1367 assert!(r.pacf[0].abs() > 0.5, "lag-1 PACF should be strong");
1369 }
1370
1371 #[test]
1372 fn pacf_bounded() {
1373 let data: Vec<f64> = (0..30).map(|i| (i as f64 * 0.5).sin()).collect();
1374 let r = pacf(&data, Some(10)).expect("should compute");
1375 for (i, &p) in r.pacf.iter().enumerate() {
1376 assert!(
1377 (-1.0..=1.0).contains(&p),
1378 "PACF[{}] = {} out of [-1, 1]",
1379 i + 1,
1380 p
1381 );
1382 }
1383 }
1384
1385 #[test]
1386 fn pacf_edge_cases() {
1387 assert_eq!(
1388 pacf(&[1.0, 2.0, 3.0], Some(1)).unwrap_err(),
1389 TimeSeriesError::TooFewObservations { needed: 4, got: 3 }
1390 );
1391 let data = [1.0, 3.0, 2.0, 5.0, 4.0, 7.0, 6.0, 8.0, 5.0, 3.0];
1393 for k in [0, 5] {
1394 assert_eq!(
1395 pacf(&data, Some(k)).unwrap_err(),
1396 TimeSeriesError::OptionOutOfRange {
1397 option: "max_lag",
1398 got: k as f64,
1399 min: 1.0,
1400 max: 4.0
1401 }
1402 );
1403 }
1404 assert_eq!(pacf(&data, None).expect("defined").pacf.len(), 4);
1405 }
1406}
1407
1408#[cfg(test)]
1409mod proptests {
1410 use super::*;
1411 use proptest::prelude::*;
1412
1413 fn bounded_vec(min_len: usize, max_len: usize) -> BoxedStrategy<Vec<f64>> {
1414 proptest::collection::vec(-1e6_f64..1e6, min_len..=max_len).boxed()
1415 }
1416
1417 proptest! {
1418 #[test]
1419 fn pearson_bounded(
1420 data in bounded_vec(5, 50).prop_flat_map(|x| {
1421 let n = x.len();
1422 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1423 })
1424 ) {
1425 let (x, y) = data;
1426 if let Some(result) = pearson(&x, &y) {
1427 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1428 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1429 }
1430 }
1431
1432 #[test]
1433 fn spearman_bounded(
1434 data in bounded_vec(5, 50).prop_flat_map(|x| {
1435 let n = x.len();
1436 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1437 })
1438 ) {
1439 let (x, y) = data;
1440 if let Some(result) = spearman(&x, &y) {
1441 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "r out of bounds: {}", result.r);
1442 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1443 }
1444 }
1445
1446 #[test]
1447 fn kendall_bounded(
1448 data in bounded_vec(5, 30).prop_flat_map(|x| {
1449 let n = x.len();
1450 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1451 })
1452 ) {
1453 let (x, y) = data;
1454 if let Some(result) = kendall_tau_b(&x, &y) {
1455 prop_assert!(result.r >= -1.0 && result.r <= 1.0, "tau out of bounds: {}", result.r);
1456 prop_assert!(result.p_value >= 0.0 && result.p_value <= 1.0, "p out of bounds: {}", result.p_value);
1457 }
1458 }
1459
1460 #[test]
1461 fn pearson_symmetric(
1462 data in bounded_vec(5, 50).prop_flat_map(|x| {
1463 let n = x.len();
1464 (Just(x), proptest::collection::vec(-1e6_f64..1e6, n..=n))
1465 })
1466 ) {
1467 let (x, y) = data;
1468 let r_xy = pearson(&x, &y);
1469 let r_yx = pearson(&y, &x);
1470 match (r_xy, r_yx) {
1471 (Some(a), Some(b)) => {
1472 prop_assert!((a.r - b.r).abs() < 1e-10, "not symmetric: {} vs {}", a.r, b.r);
1473 }
1474 (None, None) => {}
1475 _ => prop_assert!(false, "one is None but not the other"),
1476 }
1477 }
1478
1479 #[test]
1480 fn fisher_z_roundtrip_prop(r in -0.99_f64..0.99) {
1481 let z = fisher_z(r).expect("should compute");
1482 let r_back = fisher_z_inv(z);
1483 prop_assert!((r - r_back).abs() < 1e-10);
1484 }
1485
1486 #[test]
1487 fn ci_contains_true_r(
1488 r in -0.99_f64..0.99,
1489 n in 10_usize..200
1490 ) {
1491 let ci = correlation_ci(r, n, 0.95).expect("should compute");
1492 prop_assert!(ci.lower < ci.upper, "CI inverted");
1493 prop_assert!(ci.lower >= -1.0 && ci.upper <= 1.0, "CI out of bounds");
1494 }
1495
1496 #[test]
1497 fn acf_bounded_prop(
1498 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1499 ) {
1500 if let Ok(r) = acf(&data, Some(10)) {
1501 prop_assert!((r.acf[0] - 1.0).abs() < 1e-10, "ACF[0] must be 1.0");
1502 for (i, &v) in r.acf.iter().enumerate() {
1503 prop_assert!((-1.0..=1.0).contains(&v), "ACF[{i}] = {v} out of [-1,1]");
1504 }
1505 }
1506 }
1507
1508 #[test]
1509 fn pacf_bounded_prop(
1510 data in proptest::collection::vec(-1e3_f64..1e3, 5..=50),
1511 ) {
1512 if let Ok(r) = pacf(&data, Some(5)) {
1513 for (i, &v) in r.pacf.iter().enumerate() {
1514 prop_assert!((-1.0 - 1e-10..=1.0 + 1e-10).contains(&v), "PACF[{i}] = {v} out of bounds");
1515 }
1516 }
1517 }
1518 }
1519}
1520
1521#[cfg(test)]
1523mod scipy_tail_reference {
1524 use super::*;
1525
1526 #[test]
1527 fn pearson_p_value_keeps_its_tail() {
1528 let x: Vec<f64> = (1..=20).map(f64::from).collect();
1529 let y = [
1530 5.3999999999999995,
1531 7.0,
1532 9.4,
1533 11.8,
1534 13.2,
1535 15.8,
1536 17.6,
1537 19.8,
1538 22.250000000000004,
1539 23.75,
1540 26.25,
1541 28.150000000000002,
1542 30.55,
1543 32.050000000000004,
1544 34.55,
1545 36.7,
1546 38.550000000000004,
1547 41.1,
1548 42.699999999999996,
1549 45.0,
1550 ];
1551 let got = pearson(&x, &y).expect("valid").p_value;
1552 let want = 8.20335692044105e-33;
1553 assert!(
1554 (got - want).abs() <= 1e-9 * want,
1555 "p: got {got:e}, SciPy {want:e}"
1556 );
1557 }
1558}