1use crate::error::{validate_finite_data, CopulaError, Result};
9use nalgebra::DMatrix;
10
11pub fn to_pseudo_observations(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
52 let (n_rows, n_cols) = data.shape();
53
54 if n_rows == 0 || n_cols == 0 {
55 return Err(CopulaError::data_error("Data matrix is empty"));
56 }
57
58 let mut pseudo_obs = DMatrix::<f64>::zeros(n_rows, n_cols);
59
60 for j in 0..n_cols {
61 let column: Vec<f64> = data.column(j).iter().cloned().collect();
62 validate_finite_data(&column, &format!("column {}", j))?;
63
64 let ranks = empirical_ranks(&column)?;
65
66 for i in 0..n_rows {
67 pseudo_obs[(i, j)] = ranks[i] / (n_rows as f64 + 1.0);
68 }
69 }
70
71 Ok(pseudo_obs)
72}
73
74pub fn empirical_ranks(data: &[f64]) -> Result<Vec<f64>> {
102 let n = data.len();
103 if n == 0 {
104 return Ok(vec![]);
105 }
106
107 validate_finite_data(data, "input data")?;
108
109 let mut indexed_data: Vec<(f64, usize)> =
111 data.iter().enumerate().map(|(i, &x)| (x, i)).collect();
112
113 indexed_data.sort_by(|a, b| a.0.total_cmp(&b.0));
115
116 let mut ranks = vec![0.0; n];
117 let mut i = 0;
118
119 while i < n {
120 let current_value = indexed_data[i].0;
121 let start_rank = i + 1; let mut j = i;
125 while j < n && (indexed_data[j].0 - current_value).abs() < f64::EPSILON {
126 j += 1;
127 }
128
129 let avg_rank = (start_rank + (i + (j - i))) as f64 / 2.0;
131 for &(_, idx) in &indexed_data[i..j] {
132 ranks[idx] = avg_rank;
133 }
134
135 i = j;
136 }
137
138 Ok(ranks)
139}
140
141pub fn kendall_tau(x: &[f64], y: &[f64]) -> Result<f64> {
181 if x.len() != y.len() {
182 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
183 }
184
185 let n = x.len();
186 if n < 2 {
187 return Err(CopulaError::data_error("Need at least 2 observations"));
188 }
189
190 validate_finite_data(x, "x variable")?;
191 validate_finite_data(y, "y variable")?;
192
193 let mut concordant = 0;
194 let mut discordant = 0;
195
196 for i in 0..n {
197 for j in (i + 1)..n {
198 let x_diff = x[i] - x[j];
199 let y_diff = y[i] - y[j];
200 let product = x_diff * y_diff;
201
202 if product > 0.0 {
203 concordant += 1;
204 } else if product < 0.0 {
205 discordant += 1;
206 }
207 }
209 }
210
211 let total_pairs = concordant + discordant;
212 if total_pairs == 0 {
213 Ok(0.0) } else {
215 Ok((concordant as f64 - discordant as f64) / total_pairs as f64)
216 }
217}
218
219pub fn spearman_rho(x: &[f64], y: &[f64]) -> Result<f64> {
254 if x.len() != y.len() {
255 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
256 }
257
258 let n = x.len();
259 if n < 2 {
260 return Err(CopulaError::data_error("Need at least 2 observations"));
261 }
262
263 let ranks_x = empirical_ranks(x)?;
264 let ranks_y = empirical_ranks(y)?;
265
266 pearson_correlation(&ranks_x, &ranks_y)
267}
268
269fn pearson_correlation(x: &[f64], y: &[f64]) -> Result<f64> {
280 let n = x.len() as f64;
281
282 let mean_x = x.iter().sum::<f64>() / n;
283 let mean_y = y.iter().sum::<f64>() / n;
284
285 let mut numerator = 0.0;
286 let mut sum_sq_x = 0.0;
287 let mut sum_sq_y = 0.0;
288
289 for i in 0..x.len() {
290 let dx = x[i] - mean_x;
291 let dy = y[i] - mean_y;
292
293 numerator += dx * dy;
294 sum_sq_x += dx * dx;
295 sum_sq_y += dy * dy;
296 }
297
298 let denominator = (sum_sq_x * sum_sq_y).sqrt();
299
300 if denominator < f64::EPSILON {
301 Ok(0.0) } else {
303 Ok(numerator / denominator)
304 }
305}
306
307pub fn empirical_cdf_transform(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
321 let (n_rows, n_cols) = data.shape();
322 let mut transformed = DMatrix::<f64>::zeros(n_rows, n_cols);
323
324 for j in 0..n_cols {
325 let column: Vec<f64> = data.column(j).iter().cloned().collect();
326 validate_finite_data(&column, &format!("column {}", j))?;
327
328 let mut sorted_column = column.clone();
329 sorted_column.sort_by(|a, b| a.total_cmp(b));
330
331 for i in 0..n_rows {
332 let value = column[i];
333 let rank = sorted_column
334 .iter()
335 .position(|&x| x >= value)
336 .unwrap_or(n_rows - 1);
337
338 transformed[(i, j)] = (rank + 1) as f64 / (n_rows + 1) as f64;
339 }
340 }
341
342 Ok(transformed)
343}
344
345pub fn validate_correlation_matrix(matrix: &DMatrix<f64>) -> Result<()> {
361 let (n_rows, n_cols) = matrix.shape();
362
363 if n_rows != n_cols {
365 return Err(CopulaError::invalid_parameter(
366 "Correlation matrix must be square",
367 ));
368 }
369
370 let n = n_rows;
371
372 for i in 0..n {
374 if (matrix[(i, i)] - 1.0).abs() > 1e-10 {
376 return Err(CopulaError::invalid_parameter(
377 "Correlation matrix must have unit diagonal",
378 ));
379 }
380
381 for j in 0..n {
383 if (matrix[(i, j)] - matrix[(j, i)]).abs() > 1e-10 {
384 return Err(CopulaError::invalid_parameter(
385 "Correlation matrix must be symmetric",
386 ));
387 }
388 }
389
390 for j in 0..n {
392 if i != j && (matrix[(i, j)].abs() > 1.0) {
393 return Err(CopulaError::invalid_parameter(
394 "Correlation coefficients must be in [-1, 1]",
395 ));
396 }
397 }
398 }
399
400 let eigenvalues = matrix.symmetric_eigenvalues();
402 let min_eigenvalue = eigenvalues.iter().fold(f64::INFINITY, |a, &b| a.min(b));
403
404 if min_eigenvalue < -1e-10 {
405 return Err(CopulaError::invalid_parameter(
406 "Correlation matrix must be positive semi-definite",
407 ));
408 }
409
410 Ok(())
411}
412
413pub fn random_correlation_matrix<R: rand::Rng + ?Sized>(
437 dimension: usize,
438 rng: &mut R,
439) -> Result<DMatrix<f64>> {
440 use rand_distr::{Distribution, StandardNormal};
441
442 if dimension == 0 {
443 return Err(CopulaError::invalid_parameter("Dimension must be positive"));
444 }
445
446 if dimension == 1 {
447 return Ok(DMatrix::from_element(1, 1, 1.0));
448 }
449
450 let mut a = DMatrix::<f64>::zeros(dimension, dimension);
452 let normal = StandardNormal;
453
454 for i in 0..dimension {
455 for j in 0..dimension {
456 a[(i, j)] = normal.sample(rng);
457 }
458 }
459
460 let ata = a.transpose() * &a;
462
463 let mut corr = DMatrix::<f64>::zeros(dimension, dimension);
465 for i in 0..dimension {
466 for j in 0..dimension {
467 corr[(i, j)] = ata[(i, j)] / (ata[(i, i)] * ata[(j, j)]).sqrt();
468 }
469 }
470
471 Ok(corr)
472}
473
474pub fn empirical_copula_cdf(pseudo_obs: &DMatrix<f64>, u: &[f64]) -> Result<f64> {
499 let (n_rows, n_cols) = pseudo_obs.shape();
500
501 if u.len() != n_cols {
502 return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
503 }
504
505 crate::error::validate_unit_range(u)?;
506
507 let count = (0..n_rows)
508 .filter(|&i| (0..n_cols).all(|j| pseudo_obs[(i, j)] <= u[j]))
509 .count();
510
511 Ok(count as f64 / n_rows as f64)
512}
513
514pub fn multivariate_kendall_tau(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
526 let (n_rows, n_cols) = data.shape();
527
528 if n_rows < 2 {
529 return Err(CopulaError::data_error("Need at least 2 observations"));
530 }
531
532 let mut tau_matrix = DMatrix::<f64>::zeros(n_cols, n_cols);
533
534 for i in 0..n_cols {
535 tau_matrix[(i, i)] = 1.0; for j in (i + 1)..n_cols {
538 let col_i: Vec<f64> = data.column(i).iter().cloned().collect();
539 let col_j: Vec<f64> = data.column(j).iter().cloned().collect();
540
541 let tau = kendall_tau(&col_i, &col_j)?;
542 tau_matrix[(i, j)] = tau;
543 tau_matrix[(j, i)] = tau; }
545 }
546
547 Ok(tau_matrix)
548}
549
550pub fn multivariate_spearman_rho(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
562 let (n_rows, n_cols) = data.shape();
563
564 if n_rows < 2 {
565 return Err(CopulaError::data_error("Need at least 2 observations"));
566 }
567
568 let mut rho_matrix = DMatrix::<f64>::zeros(n_cols, n_cols);
569
570 for i in 0..n_cols {
571 rho_matrix[(i, i)] = 1.0; for j in (i + 1)..n_cols {
574 let col_i: Vec<f64> = data.column(i).iter().cloned().collect();
575 let col_j: Vec<f64> = data.column(j).iter().cloned().collect();
576
577 let rho = spearman_rho(&col_i, &col_j)?;
578 rho_matrix[(i, j)] = rho;
579 rho_matrix[(j, i)] = rho; }
581 }
582
583 Ok(rho_matrix)
584}
585
586pub fn remove_missing_values(data: &DMatrix<f64>) -> DMatrix<f64> {
598 let (n_rows, n_cols) = data.shape();
599
600 let valid_rows: Vec<usize> = (0..n_rows)
601 .filter(|&i| (0..n_cols).all(|j| data[(i, j)].is_finite()))
602 .collect();
603
604 if valid_rows.is_empty() {
605 return DMatrix::<f64>::zeros(0, n_cols);
606 }
607
608 let mut clean_data = DMatrix::<f64>::zeros(valid_rows.len(), n_cols);
609
610 for (new_i, &old_i) in valid_rows.iter().enumerate() {
611 for j in 0..n_cols {
612 clean_data[(new_i, j)] = data[(old_i, j)];
613 }
614 }
615
616 clean_data
617}
618
619pub fn bootstrap_sample<R: rand::Rng + ?Sized>(data: &DMatrix<f64>, rng: &mut R) -> DMatrix<f64> {
632 let (n_rows, n_cols) = data.shape();
633 let mut bootstrap_data = DMatrix::<f64>::zeros(n_rows, n_cols);
634
635 use rand::seq::SliceRandom;
636 let indices: Vec<usize> = (0..n_rows).collect();
637
638 for i in 0..n_rows {
639 let &sampled_idx = indices.choose(rng).unwrap();
640 for j in 0..n_cols {
641 bootstrap_data[(i, j)] = data[(sampled_idx, j)];
642 }
643 }
644
645 bootstrap_data
646}
647
648pub fn information_criteria(log_likelihood: f64, n_params: usize, n_obs: usize) -> (f64, f64) {
660 let aic = -2.0 * log_likelihood + 2.0 * n_params as f64;
661 let bic = -2.0 * log_likelihood + (n_params as f64) * (n_obs as f64).ln();
662 (aic, bic)
663}
664
665pub fn validate_pseudo_observations(pseudo_obs: &DMatrix<f64>) -> Result<()> {
677 let (n_rows, n_cols) = pseudo_obs.shape();
678
679 for i in 0..n_rows {
680 for j in 0..n_cols {
681 let val = pseudo_obs[(i, j)];
682 if !val.is_finite() || val <= 0.0 || val >= 1.0 {
683 return Err(CopulaError::data_error(format!(
684 "Pseudo-observation at ({}, {}) = {} is not in (0, 1)",
685 i, j, val
686 )));
687 }
688 }
689 }
690
691 Ok(())
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use approx::assert_relative_eq;
698 use nalgebra::DMatrix;
699 use rand::thread_rng;
700
701 #[test]
702 fn test_empirical_ranks() {
703 let data = vec![3.0, 1.0, 4.0, 1.0, 5.0];
704 let ranks = empirical_ranks(&data).unwrap();
705
706 assert_relative_eq!(ranks[0], 3.0);
708 assert_relative_eq!(ranks[1], 1.5);
709 assert_relative_eq!(ranks[2], 4.0);
710 assert_relative_eq!(ranks[3], 1.5);
711 assert_relative_eq!(ranks[4], 5.0);
712 }
713
714 #[test]
715 fn test_to_pseudo_observations() {
716 let data = DMatrix::from_row_slice(3, 2, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
717
718 let pseudo_obs = to_pseudo_observations(&data).unwrap();
719
720 for j in 0..2 {
722 assert_relative_eq!(pseudo_obs[(0, j)], 0.25, epsilon = 1e-10);
723 assert_relative_eq!(pseudo_obs[(1, j)], 0.5, epsilon = 1e-10);
724 assert_relative_eq!(pseudo_obs[(2, j)], 0.75, epsilon = 1e-10);
725 }
726 }
727
728 #[test]
729 fn test_kendall_tau() {
730 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
732 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
733 let tau = kendall_tau(&x, &y).unwrap();
734 assert_relative_eq!(tau, 1.0, epsilon = 1e-10);
735
736 let y_neg = vec![5.0, 4.0, 3.0, 2.0, 1.0];
738 let tau_neg = kendall_tau(&x, &y_neg).unwrap();
739 assert_relative_eq!(tau_neg, -1.0, epsilon = 1e-10);
740 }
741
742 #[test]
743 fn test_spearman_rho() {
744 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
745 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
746 let rho = spearman_rho(&x, &y).unwrap();
747 assert_relative_eq!(rho, 1.0, epsilon = 1e-10);
748 }
749
750 #[test]
751 fn test_validate_correlation_matrix() {
752 let valid = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.5, 1.0]);
754 assert!(validate_correlation_matrix(&valid).is_ok());
755
756 let invalid = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.3, 1.0]);
758 assert!(validate_correlation_matrix(&invalid).is_err());
759
760 let invalid2 = DMatrix::from_row_slice(2, 2, &[0.9, 0.5, 0.5, 1.0]);
762 assert!(validate_correlation_matrix(&invalid2).is_err());
763 }
764
765 #[test]
766 fn test_empirical_copula_cdf() {
767 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]);
768
769 let cdf = empirical_copula_cdf(&pseudo_obs, &[0.5, 0.5]).unwrap();
771 assert_relative_eq!(cdf, 0.5, epsilon = 1e-10);
772 }
773
774 #[test]
775 fn test_remove_missing_values() {
776 let data = DMatrix::from_row_slice(3, 2, &[1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0]);
777
778 let clean = remove_missing_values(&data);
779 assert_eq!(clean.nrows(), 2);
780 assert_eq!(clean[(0, 0)], 1.0);
781 assert_eq!(clean[(1, 0)], 5.0);
782 }
783
784 #[test]
785 fn test_information_criteria() {
786 let (aic, bic) = information_criteria(-100.0, 3, 100);
787 assert_eq!(aic, 206.0); assert_relative_eq!(bic, 200.0 + 3.0 * 100.0_f64.ln(), epsilon = 1e-10);
789 }
790
791 #[test]
792 fn test_validate_pseudo_observations() {
793 let valid = DMatrix::from_row_slice(2, 2, &[0.5, 0.6, 0.7, 0.8]);
794 assert!(validate_pseudo_observations(&valid).is_ok());
795
796 let invalid = DMatrix::from_row_slice(1, 2, &[1.0, 0.5]);
797 assert!(validate_pseudo_observations(&invalid).is_err());
798
799 let invalid_nan = DMatrix::from_row_slice(1, 1, &[f64::NAN]);
800 assert!(validate_pseudo_observations(&invalid_nan).is_err());
801 }
802
803 #[test]
804 fn test_random_correlation_matrix() {
805 let mut rng = thread_rng();
806 let corr = random_correlation_matrix(3, &mut rng).unwrap();
807 assert_eq!(corr.nrows(), 3);
808 assert!(validate_correlation_matrix(&corr).is_ok());
809
810 assert!(random_correlation_matrix(0, &mut rng).is_err());
811 }
812
813 #[test]
814 fn test_empirical_cdf_transform() {
815 let data = DMatrix::from_row_slice(5, 2, &[
816 1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0, 5.0, 50.0,
817 ]);
818 let transformed = empirical_cdf_transform(&data).unwrap();
819 assert_eq!(transformed.nrows(), 5);
820 assert_eq!(transformed.ncols(), 2);
821 for i in 0..5 {
822 for j in 0..2 {
823 let v = transformed[(i, j)];
824 assert!(v > 0.0 && v < 1.0, "value {} not in (0,1)", v);
825 }
826 }
827 }
828
829 #[test]
830 fn test_empirical_cdf_transform_rejects_nan() {
831 let data = DMatrix::from_row_slice(2, 1, &[1.0, f64::NAN]);
832 assert!(empirical_cdf_transform(&data).is_err());
833 }
834
835 #[test]
836 fn test_multivariate_kendall_tau() {
837 let data = DMatrix::from_row_slice(5, 3, &[
838 1.0, 1.0, 1.0,
839 2.0, 2.0, 2.0,
840 3.0, 3.0, 3.0,
841 4.0, 4.0, 4.0,
842 5.0, 5.0, 5.0,
843 ]);
844 let tau = multivariate_kendall_tau(&data).unwrap();
845 assert_eq!(tau.nrows(), 3);
846 assert_eq!(tau.ncols(), 3);
847 for i in 0..3 {
848 assert_relative_eq!(tau[(i, i)], 1.0, epsilon = 1e-10);
849 for j in 0..3 {
850 assert_relative_eq!(tau[(i, j)], 1.0, epsilon = 1e-10);
851 }
852 }
853 }
854
855 #[test]
856 fn test_multivariate_kendall_tau_rejects_insufficient_data() {
857 let data = DMatrix::from_row_slice(1, 2, &[1.0, 2.0]);
858 assert!(multivariate_kendall_tau(&data).is_err());
859 }
860
861 #[test]
862 fn test_multivariate_spearman_rho() {
863 let data = DMatrix::from_row_slice(5, 2, &[
864 1.0, 5.0, 2.0, 4.0, 3.0, 3.0, 4.0, 2.0, 5.0, 1.0,
865 ]);
866 let rho = multivariate_spearman_rho(&data).unwrap();
867 assert_eq!(rho.nrows(), 2);
868 assert_relative_eq!(rho[(0, 0)], 1.0, epsilon = 1e-10);
869 assert_relative_eq!(rho[(1, 1)], 1.0, epsilon = 1e-10);
870 assert_relative_eq!(rho[(0, 1)], -1.0, epsilon = 1e-10);
871 }
872
873 #[test]
874 fn test_empirical_ranks_empty() {
875 let ranks = empirical_ranks(&[]).unwrap();
876 assert!(ranks.is_empty());
877 }
878
879 #[test]
880 fn test_empirical_ranks_single() {
881 let ranks = empirical_ranks(&[42.0]).unwrap();
882 assert_eq!(ranks.len(), 1);
883 assert_relative_eq!(ranks[0], 1.0);
884 }
885
886 #[test]
887 fn test_empirical_ranks_rejects_nan() {
888 assert!(empirical_ranks(&[1.0, f64::NAN, 3.0]).is_err());
889 }
890
891 #[test]
892 fn test_pseudo_observations_rejects_empty() {
893 let data = DMatrix::<f64>::zeros(0, 2);
894 assert!(to_pseudo_observations(&data).is_err());
895 }
896
897 #[test]
898 fn test_bootstrap_sample_dimensions() {
899 let mut rng = thread_rng();
900 let data = DMatrix::from_row_slice(5, 2, &[
901 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1,
902 ]);
903 let boot = bootstrap_sample(&data, &mut rng);
904 assert_eq!(boot.nrows(), 5);
905 assert_eq!(boot.ncols(), 2);
906 }
907
908 #[test]
909 fn test_random_correlation_matrix_1d() {
910 let mut rng = thread_rng();
911 let corr = random_correlation_matrix(1, &mut rng).unwrap();
912 assert_eq!(corr.nrows(), 1);
913 assert_relative_eq!(corr[(0, 0)], 1.0);
914 }
915}