1use crate::error::{validate_finite_data, CopulaError, Result};
7use nalgebra::DMatrix;
8
9pub fn to_pseudo_observations(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
50 let (n_rows, n_cols) = data.shape();
51
52 if n_rows == 0 || n_cols == 0 {
53 return Err(CopulaError::data_error("Data matrix is empty"));
54 }
55
56 let mut pseudo_obs = DMatrix::<f64>::zeros(n_rows, n_cols);
57
58 for j in 0..n_cols {
59 let column: Vec<f64> = data.column(j).iter().cloned().collect();
60 validate_finite_data(&column, &format!("column {}", j))?;
61
62 let ranks = empirical_ranks(&column)?;
63
64 for i in 0..n_rows {
65 pseudo_obs[(i, j)] = ranks[i] / (n_rows as f64 + 1.0);
66 }
67 }
68
69 Ok(pseudo_obs)
70}
71
72pub fn empirical_ranks(data: &[f64]) -> Result<Vec<f64>> {
100 let n = data.len();
101 if n == 0 {
102 return Ok(vec![]);
103 }
104
105 validate_finite_data(data, "input data")?;
106
107 let mut indexed_data: Vec<(f64, usize)> =
109 data.iter().enumerate().map(|(i, &x)| (x, i)).collect();
110
111 indexed_data.sort_by(|a, b| a.0.total_cmp(&b.0));
113
114 let mut ranks = vec![0.0; n];
115 let mut i = 0;
116
117 while i < n {
118 let current_value = indexed_data[i].0;
119 let start_rank = i + 1; let mut j = i;
123 while j < n && (indexed_data[j].0 - current_value).abs() < f64::EPSILON {
124 j += 1;
125 }
126
127 let avg_rank = (start_rank + (i + (j - i))) as f64 / 2.0;
129 for &(_, idx) in &indexed_data[i..j] {
130 ranks[idx] = avg_rank;
131 }
132
133 i = j;
134 }
135
136 Ok(ranks)
137}
138
139pub fn kendall_tau(x: &[f64], y: &[f64]) -> Result<f64> {
179 if x.len() != y.len() {
180 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
181 }
182
183 let n = x.len();
184 if n < 2 {
185 return Err(CopulaError::data_error("Need at least 2 observations"));
186 }
187
188 validate_finite_data(x, "x variable")?;
189 validate_finite_data(y, "y variable")?;
190
191 let mut concordant = 0;
192 let mut discordant = 0;
193
194 for i in 0..n {
195 for j in (i + 1)..n {
196 let x_diff = x[i] - x[j];
197 let y_diff = y[i] - y[j];
198 let product = x_diff * y_diff;
199
200 if product > 0.0 {
201 concordant += 1;
202 } else if product < 0.0 {
203 discordant += 1;
204 }
205 }
207 }
208
209 let total_pairs = concordant + discordant;
210 if total_pairs == 0 {
211 Ok(0.0) } else {
213 Ok((concordant as f64 - discordant as f64) / total_pairs as f64)
214 }
215}
216
217pub fn spearman_rho(x: &[f64], y: &[f64]) -> Result<f64> {
252 if x.len() != y.len() {
253 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
254 }
255
256 let n = x.len();
257 if n < 2 {
258 return Err(CopulaError::data_error("Need at least 2 observations"));
259 }
260
261 let ranks_x = empirical_ranks(x)?;
262 let ranks_y = empirical_ranks(y)?;
263
264 pearson_correlation(&ranks_x, &ranks_y)
265}
266
267fn pearson_correlation(x: &[f64], y: &[f64]) -> Result<f64> {
278 let n = x.len() as f64;
279
280 let mean_x = x.iter().sum::<f64>() / n;
281 let mean_y = y.iter().sum::<f64>() / n;
282
283 let mut numerator = 0.0;
284 let mut sum_sq_x = 0.0;
285 let mut sum_sq_y = 0.0;
286
287 for i in 0..x.len() {
288 let dx = x[i] - mean_x;
289 let dy = y[i] - mean_y;
290
291 numerator += dx * dy;
292 sum_sq_x += dx * dx;
293 sum_sq_y += dy * dy;
294 }
295
296 let denominator = (sum_sq_x * sum_sq_y).sqrt();
297
298 if denominator < f64::EPSILON {
299 Ok(0.0) } else {
301 Ok(numerator / denominator)
302 }
303}
304
305pub fn empirical_cdf_transform(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
319 let (n_rows, n_cols) = data.shape();
320 let mut transformed = DMatrix::<f64>::zeros(n_rows, n_cols);
321
322 for j in 0..n_cols {
323 let column: Vec<f64> = data.column(j).iter().cloned().collect();
324 validate_finite_data(&column, &format!("column {}", j))?;
325
326 let mut sorted_column = column.clone();
327 sorted_column.sort_by(|a, b| a.total_cmp(b));
328
329 for i in 0..n_rows {
330 let value = column[i];
331 let rank = sorted_column
332 .iter()
333 .position(|&x| x >= value)
334 .unwrap_or(n_rows - 1);
335
336 transformed[(i, j)] = (rank + 1) as f64 / (n_rows + 1) as f64;
337 }
338 }
339
340 Ok(transformed)
341}
342
343pub(crate) fn copula_boundary_value(u: &[f64]) -> Option<f64> {
351 if u.contains(&0.0) {
352 return Some(0.0);
353 }
354 let mut interior = u.iter().copied().filter(|&x| x < 1.0);
355 match (interior.next(), interior.next()) {
356 (None, _) => Some(1.0),
357 (Some(only), None) => Some(only),
358 _ => None,
359 }
360}
361
362pub(crate) fn clamp_to_frechet_bounds(u: &[f64], value: f64) -> f64 {
370 let d = u.len() as f64;
371 let upper = u.iter().copied().fold(1.0, f64::min);
372 let lower = (u.iter().sum::<f64>() - d + 1.0).max(0.0).min(upper);
375 value.clamp(lower, upper)
376}
377
378pub fn validate_correlation_matrix(matrix: &DMatrix<f64>) -> Result<()> {
395 let (n_rows, n_cols) = matrix.shape();
396
397 if n_rows != n_cols {
399 return Err(CopulaError::invalid_parameter(
400 "Correlation matrix must be square",
401 ));
402 }
403
404 let n = n_rows;
405
406 if matrix.iter().any(|x| !x.is_finite()) {
408 return Err(CopulaError::invalid_parameter(
409 "Correlation matrix entries must be finite",
410 ));
411 }
412
413 for i in 0..n {
415 if (matrix[(i, i)] - 1.0).abs() > 1e-10 {
417 return Err(CopulaError::invalid_parameter(
418 "Correlation matrix must have unit diagonal",
419 ));
420 }
421
422 for j in 0..n {
424 if (matrix[(i, j)] - matrix[(j, i)]).abs() > 1e-10 {
425 return Err(CopulaError::invalid_parameter(
426 "Correlation matrix must be symmetric",
427 ));
428 }
429 }
430
431 for j in 0..n {
433 if i != j && (matrix[(i, j)].abs() > 1.0) {
434 return Err(CopulaError::invalid_parameter(
435 "Correlation coefficients must be in [-1, 1]",
436 ));
437 }
438 }
439 }
440
441 let eigenvalues = matrix.symmetric_eigenvalues();
443 let min_eigenvalue = eigenvalues.iter().fold(f64::INFINITY, |a, &b| a.min(b));
444
445 if min_eigenvalue < -1e-10 {
446 return Err(CopulaError::invalid_parameter(
447 "Correlation matrix must be positive semi-definite",
448 ));
449 }
450
451 Ok(())
452}
453
454pub fn random_correlation_matrix<R: rand::Rng + ?Sized>(
479 dimension: usize,
480 rng: &mut R,
481) -> Result<DMatrix<f64>> {
482 use rand_distr::{Distribution, StandardNormal};
483
484 if dimension == 0 {
485 return Err(CopulaError::invalid_parameter("Dimension must be positive"));
486 }
487
488 if dimension == 1 {
489 return Ok(DMatrix::from_element(1, 1, 1.0));
490 }
491
492 let mut a = DMatrix::<f64>::zeros(dimension, dimension);
494 let normal = StandardNormal;
495
496 for i in 0..dimension {
497 for j in 0..dimension {
498 a[(i, j)] = normal.sample(rng);
499 }
500 }
501
502 let ata = a.transpose() * &a;
504
505 let mut corr = DMatrix::<f64>::zeros(dimension, dimension);
507 for i in 0..dimension {
508 for j in 0..dimension {
509 corr[(i, j)] = ata[(i, j)] / (ata[(i, i)] * ata[(j, j)]).sqrt();
510 }
511 }
512
513 Ok(corr)
514}
515
516pub fn empirical_copula_cdf(pseudo_obs: &DMatrix<f64>, u: &[f64]) -> Result<f64> {
543 let (n_rows, n_cols) = pseudo_obs.shape();
544
545 if u.len() != n_cols {
546 return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
547 }
548
549 crate::error::validate_unit_range(u)?;
550
551 let count = (0..n_rows)
552 .filter(|&i| (0..n_cols).all(|j| pseudo_obs[(i, j)] <= u[j]))
553 .count();
554
555 Ok(count as f64 / n_rows as f64)
556}
557
558pub fn multivariate_kendall_tau(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
570 let (n_rows, n_cols) = data.shape();
571
572 if n_rows < 2 {
573 return Err(CopulaError::data_error("Need at least 2 observations"));
574 }
575
576 let mut tau_matrix = DMatrix::<f64>::zeros(n_cols, n_cols);
577
578 for i in 0..n_cols {
579 tau_matrix[(i, i)] = 1.0; for j in (i + 1)..n_cols {
582 let col_i: Vec<f64> = data.column(i).iter().cloned().collect();
583 let col_j: Vec<f64> = data.column(j).iter().cloned().collect();
584
585 let tau = kendall_tau(&col_i, &col_j)?;
586 tau_matrix[(i, j)] = tau;
587 tau_matrix[(j, i)] = tau; }
589 }
590
591 Ok(tau_matrix)
592}
593
594pub fn multivariate_spearman_rho(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
606 let (n_rows, n_cols) = data.shape();
607
608 if n_rows < 2 {
609 return Err(CopulaError::data_error("Need at least 2 observations"));
610 }
611
612 let mut rho_matrix = DMatrix::<f64>::zeros(n_cols, n_cols);
613
614 for i in 0..n_cols {
615 rho_matrix[(i, i)] = 1.0; for j in (i + 1)..n_cols {
618 let col_i: Vec<f64> = data.column(i).iter().cloned().collect();
619 let col_j: Vec<f64> = data.column(j).iter().cloned().collect();
620
621 let rho = spearman_rho(&col_i, &col_j)?;
622 rho_matrix[(i, j)] = rho;
623 rho_matrix[(j, i)] = rho; }
625 }
626
627 Ok(rho_matrix)
628}
629
630pub fn remove_missing_values(data: &DMatrix<f64>) -> DMatrix<f64> {
642 let (n_rows, n_cols) = data.shape();
643
644 let valid_rows: Vec<usize> = (0..n_rows)
645 .filter(|&i| (0..n_cols).all(|j| data[(i, j)].is_finite()))
646 .collect();
647
648 if valid_rows.is_empty() {
649 return DMatrix::<f64>::zeros(0, n_cols);
650 }
651
652 let mut clean_data = DMatrix::<f64>::zeros(valid_rows.len(), n_cols);
653
654 for (new_i, &old_i) in valid_rows.iter().enumerate() {
655 for j in 0..n_cols {
656 clean_data[(new_i, j)] = data[(old_i, j)];
657 }
658 }
659
660 clean_data
661}
662
663pub fn bootstrap_sample<R: rand::Rng + ?Sized>(data: &DMatrix<f64>, rng: &mut R) -> DMatrix<f64> {
676 let (n_rows, n_cols) = data.shape();
677 let mut bootstrap_data = DMatrix::<f64>::zeros(n_rows, n_cols);
678
679 use rand::seq::IndexedRandom;
680 let indices: Vec<usize> = (0..n_rows).collect();
681
682 for i in 0..n_rows {
683 let &sampled_idx = indices.choose(rng).unwrap();
684 for j in 0..n_cols {
685 bootstrap_data[(i, j)] = data[(sampled_idx, j)];
686 }
687 }
688
689 bootstrap_data
690}
691
692pub fn information_criteria(log_likelihood: f64, n_params: usize, n_obs: usize) -> (f64, f64) {
704 let aic = -2.0 * log_likelihood + 2.0 * n_params as f64;
705 let bic = -2.0 * log_likelihood + (n_params as f64) * (n_obs as f64).ln();
706 (aic, bic)
707}
708
709pub fn validate_pseudo_observations(pseudo_obs: &DMatrix<f64>) -> Result<()> {
721 let (n_rows, n_cols) = pseudo_obs.shape();
722
723 for i in 0..n_rows {
724 for j in 0..n_cols {
725 let val = pseudo_obs[(i, j)];
726 if !val.is_finite() || val <= 0.0 || val >= 1.0 {
727 return Err(CopulaError::data_error(format!(
728 "Pseudo-observation at ({}, {}) = {} is not in (0, 1)",
729 i, j, val
730 )));
731 }
732 }
733 }
734
735 Ok(())
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use approx::assert_relative_eq;
742 use nalgebra::DMatrix;
743
744 #[test]
745 fn test_empirical_ranks() {
746 let data = vec![3.0, 1.0, 4.0, 1.0, 5.0];
747 let ranks = empirical_ranks(&data).unwrap();
748
749 assert_relative_eq!(ranks[0], 3.0);
751 assert_relative_eq!(ranks[1], 1.5);
752 assert_relative_eq!(ranks[2], 4.0);
753 assert_relative_eq!(ranks[3], 1.5);
754 assert_relative_eq!(ranks[4], 5.0);
755 }
756
757 #[test]
758 fn test_to_pseudo_observations() {
759 let data = DMatrix::from_row_slice(3, 2, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
760
761 let pseudo_obs = to_pseudo_observations(&data).unwrap();
762
763 for j in 0..2 {
765 assert_relative_eq!(pseudo_obs[(0, j)], 0.25, epsilon = 1e-10);
766 assert_relative_eq!(pseudo_obs[(1, j)], 0.5, epsilon = 1e-10);
767 assert_relative_eq!(pseudo_obs[(2, j)], 0.75, epsilon = 1e-10);
768 }
769 }
770
771 #[test]
772 fn test_kendall_tau() {
773 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
775 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
776 let tau = kendall_tau(&x, &y).unwrap();
777 assert_relative_eq!(tau, 1.0, epsilon = 1e-10);
778
779 let y_neg = vec![5.0, 4.0, 3.0, 2.0, 1.0];
781 let tau_neg = kendall_tau(&x, &y_neg).unwrap();
782 assert_relative_eq!(tau_neg, -1.0, epsilon = 1e-10);
783 }
784
785 #[test]
786 fn test_spearman_rho() {
787 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
788 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
789 let rho = spearman_rho(&x, &y).unwrap();
790 assert_relative_eq!(rho, 1.0, epsilon = 1e-10);
791 }
792
793 #[test]
794 fn test_validate_correlation_matrix() {
795 let valid = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.5, 1.0]);
797 assert!(validate_correlation_matrix(&valid).is_ok());
798
799 let invalid = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.3, 1.0]);
801 assert!(validate_correlation_matrix(&invalid).is_err());
802
803 let invalid2 = DMatrix::from_row_slice(2, 2, &[0.9, 0.5, 0.5, 1.0]);
805 assert!(validate_correlation_matrix(&invalid2).is_err());
806 }
807
808 #[test]
809 fn test_empirical_copula_cdf() {
810 let pseudo_obs = DMatrix::from_row_slice(4, 2, &[0.1, 0.1, 0.3, 0.4, 0.6, 0.4, 0.8, 0.9]);
811
812 let cdf = empirical_copula_cdf(&pseudo_obs, &[0.5, 0.5]).unwrap();
814 assert_relative_eq!(cdf, 0.5, epsilon = 1e-10);
815 }
816
817 #[test]
818 fn test_remove_missing_values() {
819 let data = DMatrix::from_row_slice(3, 2, &[1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0]);
820
821 let clean = remove_missing_values(&data);
822 assert_eq!(clean.nrows(), 2);
823 assert_eq!(clean[(0, 0)], 1.0);
824 assert_eq!(clean[(1, 0)], 5.0);
825 }
826
827 #[test]
828 fn test_information_criteria() {
829 let (aic, bic) = information_criteria(-100.0, 3, 100);
830 assert_eq!(aic, 206.0); assert_relative_eq!(bic, 200.0 + 3.0 * 100.0_f64.ln(), epsilon = 1e-10);
832 }
833
834 #[test]
835 fn test_validate_pseudo_observations() {
836 let valid = DMatrix::from_row_slice(2, 2, &[0.5, 0.6, 0.7, 0.8]);
837 assert!(validate_pseudo_observations(&valid).is_ok());
838
839 let invalid = DMatrix::from_row_slice(1, 2, &[1.0, 0.5]);
840 assert!(validate_pseudo_observations(&invalid).is_err());
841
842 let invalid_nan = DMatrix::from_row_slice(1, 1, &[f64::NAN]);
843 assert!(validate_pseudo_observations(&invalid_nan).is_err());
844 }
845
846 #[test]
847 fn test_random_correlation_matrix() {
848 let mut rng = rand::rng();
849 let corr = random_correlation_matrix(3, &mut rng).unwrap();
850 assert_eq!(corr.nrows(), 3);
851 assert!(validate_correlation_matrix(&corr).is_ok());
852
853 assert!(random_correlation_matrix(0, &mut rng).is_err());
854 }
855
856 #[test]
857 fn test_empirical_cdf_transform() {
858 let data = DMatrix::from_row_slice(
859 5,
860 2,
861 &[1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0, 5.0, 50.0],
862 );
863 let transformed = empirical_cdf_transform(&data).unwrap();
864 assert_eq!(transformed.nrows(), 5);
865 assert_eq!(transformed.ncols(), 2);
866 for i in 0..5 {
867 for j in 0..2 {
868 let v = transformed[(i, j)];
869 assert!(v > 0.0 && v < 1.0, "value {} not in (0,1)", v);
870 }
871 }
872 }
873
874 #[test]
875 fn test_empirical_cdf_transform_rejects_nan() {
876 let data = DMatrix::from_row_slice(2, 1, &[1.0, f64::NAN]);
877 assert!(empirical_cdf_transform(&data).is_err());
878 }
879
880 #[test]
881 fn test_multivariate_kendall_tau() {
882 #[rustfmt::skip]
883 let data = DMatrix::from_row_slice(5, 3, &[
884 1.0, 1.0, 1.0,
885 2.0, 2.0, 2.0,
886 3.0, 3.0, 3.0,
887 4.0, 4.0, 4.0,
888 5.0, 5.0, 5.0,
889 ]);
890 let tau = multivariate_kendall_tau(&data).unwrap();
891 assert_eq!(tau.nrows(), 3);
892 assert_eq!(tau.ncols(), 3);
893 for i in 0..3 {
894 assert_relative_eq!(tau[(i, i)], 1.0, epsilon = 1e-10);
895 for j in 0..3 {
896 assert_relative_eq!(tau[(i, j)], 1.0, epsilon = 1e-10);
897 }
898 }
899 }
900
901 #[test]
902 fn test_multivariate_kendall_tau_rejects_insufficient_data() {
903 let data = DMatrix::from_row_slice(1, 2, &[1.0, 2.0]);
904 assert!(multivariate_kendall_tau(&data).is_err());
905 }
906
907 #[test]
908 fn test_multivariate_spearman_rho() {
909 let data =
910 DMatrix::from_row_slice(5, 2, &[1.0, 5.0, 2.0, 4.0, 3.0, 3.0, 4.0, 2.0, 5.0, 1.0]);
911 let rho = multivariate_spearman_rho(&data).unwrap();
912 assert_eq!(rho.nrows(), 2);
913 assert_relative_eq!(rho[(0, 0)], 1.0, epsilon = 1e-10);
914 assert_relative_eq!(rho[(1, 1)], 1.0, epsilon = 1e-10);
915 assert_relative_eq!(rho[(0, 1)], -1.0, epsilon = 1e-10);
916 }
917
918 #[test]
919 fn test_empirical_ranks_empty() {
920 let ranks = empirical_ranks(&[]).unwrap();
921 assert!(ranks.is_empty());
922 }
923
924 #[test]
925 fn test_empirical_ranks_single() {
926 let ranks = empirical_ranks(&[42.0]).unwrap();
927 assert_eq!(ranks.len(), 1);
928 assert_relative_eq!(ranks[0], 1.0);
929 }
930
931 #[test]
932 fn test_empirical_ranks_rejects_nan() {
933 assert!(empirical_ranks(&[1.0, f64::NAN, 3.0]).is_err());
934 }
935
936 #[test]
937 fn test_pseudo_observations_rejects_empty() {
938 let data = DMatrix::<f64>::zeros(0, 2);
939 assert!(to_pseudo_observations(&data).is_err());
940 }
941
942 #[test]
943 fn test_bootstrap_sample_dimensions() {
944 let mut rng = rand::rng();
945 let data =
946 DMatrix::from_row_slice(5, 2, &[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1]);
947 let boot = bootstrap_sample(&data, &mut rng);
948 assert_eq!(boot.nrows(), 5);
949 assert_eq!(boot.ncols(), 2);
950 }
951
952 #[test]
953 fn test_random_correlation_matrix_1d() {
954 let mut rng = rand::rng();
955 let corr = random_correlation_matrix(1, &mut rng).unwrap();
956 assert_eq!(corr.nrows(), 1);
957 assert_relative_eq!(corr[(0, 0)], 1.0);
958 }
959}