Skip to main content

copula_core/
utils.rs

1// src/utils.rs
2
3//! Utility functions for copula modeling and data preprocessing.
4//!
5//! This module provides essential utility functions for working with copulas,
6//! including data transformation, rank computation, and dependence measures.
7
8use crate::error::{validate_finite_data, CopulaError, Result};
9use nalgebra::DMatrix;
10
11/// Convert raw data to pseudo-observations (empirical copula).
12///
13/// Pseudo-observations are the key input for copula modeling. This function
14/// transforms each marginal distribution to uniform [0,1] using empirical
15/// ranks, which removes the marginal effects and isolates the dependence structure.
16///
17/// # Mathematical Background
18///
19/// For data X₁, ..., Xₙ, the pseudo-observation for Xᵢ is:
20/// Û_i = R_i / (n + 1)
21/// where R_i is the rank of X_i among X₁, ..., Xₙ.
22///
23/// # Arguments
24///
25/// * `data` - Matrix where each row is an observation and each column is a variable
26///
27/// # Returns
28///
29/// Matrix of pseudo-observations with the same dimensions as input,
30/// where all values are in (0,1).
31///
32/// # Examples
33///
34/// ```rust
35/// use copula_core::to_pseudo_observations;
36/// use nalgebra::DMatrix;
37///
38/// let data = DMatrix::from_row_slice(3, 2, &[
39///     1.0, 4.0,
40///     2.0, 5.0,
41///     3.0, 6.0,
42/// ]);
43///
44/// let pseudo_obs = to_pseudo_observations(&data);
45/// // Result: each column has ranks [0.25, 0.5, 0.75]
46/// ```
47///
48/// # Errors
49///
50/// Returns [`CopulaError::DataError`] if the data contains non-finite values.
51pub 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
74/// Compute empirical ranks of data points.
75///
76/// Ranks are computed using the standard competition ranking ("1224" ranking):
77/// equal values receive the same rank, and the next value gets the rank it
78/// would have received if all values were distinct.
79///
80/// # Arguments
81///
82/// * `data` - Vector of data points
83///
84/// # Returns
85///
86/// Vector of ranks (1-indexed) with the same length as input.
87///
88/// # Examples
89///
90/// ```rust
91/// use copula_core::empirical_ranks;
92///
93/// let data = vec![3.0, 1.0, 4.0, 1.0, 5.0];
94/// let ranks = empirical_ranks(&data).unwrap();
95/// // ranks = [3.0, 1.5, 4.0, 1.5, 5.0] (average rank for ties)
96/// ```
97///
98/// # Errors
99///
100/// Returns [`CopulaError::DataError`] if the data contains non-finite values.
101pub 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    // Create indexed data for sorting while preserving original positions
110    let mut indexed_data: Vec<(f64, usize)> =
111        data.iter().enumerate().map(|(i, &x)| (x, i)).collect();
112
113    // Sort by value
114    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; // 1-indexed
122
123        // Find all equal values
124        let mut j = i;
125        while j < n && (indexed_data[j].0 - current_value).abs() < f64::EPSILON {
126            j += 1;
127        }
128
129        // Assign average rank to all tied values
130        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
141/// Compute Kendall's tau correlation coefficient.
142///
143/// Kendall's tau measures the ordinal association between two variables
144/// and is particularly suitable for copula modeling as it depends only
145/// on ranks, not on marginal distributions.
146///
147/// # Mathematical Background
148///
149/// For paired observations (x₁, y₁), ..., (xₙ, yₙ), Kendall's tau is:
150/// τ = (C - D) / (C + D)
151/// where C is the number of concordant pairs and D is the number of discordant pairs.
152///
153/// # Arguments
154///
155/// * `x` - First variable
156/// * `y` - Second variable (must have same length as x)
157///
158/// # Returns
159///
160/// Kendall's tau in [-1, 1], where:
161/// - 1 indicates perfect positive dependence
162/// - 0 indicates independence  
163/// - -1 indicates perfect negative dependence
164///
165/// # Examples
166///
167/// ```rust
168/// use copula_core::kendall_tau;
169///
170/// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
171/// let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
172/// let tau = kendall_tau(&x, &y).unwrap();
173/// // tau ≈ 1.0 (perfect positive correlation)
174/// ```
175///
176/// # Errors
177///
178/// Returns [`CopulaError::DimensionMismatch`] if x and y have different lengths.
179/// Returns [`CopulaError::DataError`] if data contains non-finite values.
180pub 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            // Equal values contribute neither to concordant nor discordant
208        }
209    }
210
211    let total_pairs = concordant + discordant;
212    if total_pairs == 0 {
213        Ok(0.0) // All pairs are tied
214    } else {
215        Ok((concordant as f64 - discordant as f64) / total_pairs as f64)
216    }
217}
218
219/// Compute Spearman's rho correlation coefficient.
220///
221/// Spearman's rho is the Pearson correlation of the ranks, providing another
222/// measure of monotonic association that's robust to outliers.
223///
224/// # Mathematical Background
225///
226/// Spearman's rho is computed as the Pearson correlation between the ranks:
227/// ρ = cor(rank(X), rank(Y))
228///
229/// # Arguments
230///
231/// * `x` - First variable
232/// * `y` - Second variable (must have same length as x)
233///
234/// # Returns
235///
236/// Spearman's rho in [-1, 1].
237///
238/// # Examples
239///
240/// ```rust
241/// use copula_core::spearman_rho;
242///
243/// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
244/// let y = vec![5.0, 4.0, 3.0, 2.0, 1.0];
245/// let rho = spearman_rho(&x, &y).unwrap();
246/// // rho ≈ -1.0 (perfect negative correlation)
247/// ```
248///
249/// # Errors
250///
251/// Returns [`CopulaError::DimensionMismatch`] if x and y have different lengths.
252/// Returns [`CopulaError::DataError`] if data contains non-finite values.
253pub 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
269/// Compute Pearson correlation coefficient.
270///
271/// # Arguments
272///
273/// * `x` - First variable
274/// * `y` - Second variable
275///
276/// # Returns
277///
278/// Pearson correlation in [-1, 1].
279fn 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) // No variance in one or both variables
302    } else {
303        Ok(numerator / denominator)
304    }
305}
306
307/// Transform data using the empirical CDF.
308///
309/// This is an alternative to `to_pseudo_observations` that uses the empirical
310/// CDF directly rather than ranks.
311///
312/// # Arguments
313///
314/// * `data` - Matrix of data
315///
316/// # Returns
317///
318/// Matrix of transformed data where each column has been transformed
319/// to approximately uniform using its empirical CDF.
320pub 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
345/// Check if a matrix is a valid correlation matrix.
346///
347/// A valid correlation matrix must be:
348/// 1. Square
349/// 2. Symmetric
350/// 3. Have unit diagonal
351/// 4. Be positive semi-definite
352///
353/// # Arguments
354///
355/// * `matrix` - Matrix to validate
356///
357/// # Returns
358///
359/// `Ok(())` if valid, error otherwise.
360pub fn validate_correlation_matrix(matrix: &DMatrix<f64>) -> Result<()> {
361    let (n_rows, n_cols) = matrix.shape();
362
363    // Check if square
364    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    // Check symmetry and unit diagonal
373    for i in 0..n {
374        // Check diagonal
375        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        // Check symmetry
382        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        // Check off-diagonal bounds
391        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    // Check positive semi-definiteness using eigenvalues
401    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
413/// Generate a random correlation matrix.
414///
415/// Uses the method of Joe (2006) to generate a random correlation matrix
416/// that is guaranteed to be positive definite.
417///
418/// # Arguments
419///
420/// * `dimension` - Size of the correlation matrix
421/// * `rng` - Random number generator
422///
423/// # Returns
424///
425/// A random positive definite correlation matrix.
426///
427/// # Examples
428///
429/// ```rust
430/// use copula_core::random_correlation_matrix;
431/// use rand::thread_rng;
432///
433/// let mut rng = thread_rng();
434/// let corr = random_correlation_matrix(3, &mut rng).unwrap();
435/// ```
436pub 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    // Generate random matrix
451    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    // Compute A'A to get positive semi-definite matrix
461    let ata = a.transpose() * &a;
462
463    // Extract diagonal for normalization
464    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
474/// Compute the empirical copula CDF at a given point.
475///
476/// The empirical copula is the non-parametric maximum likelihood estimator
477/// of the copula function.
478///
479/// # Arguments
480///
481/// * `pseudo_obs` - Matrix of pseudo-observations
482/// * `u` - Point at which to evaluate the empirical copula
483///
484/// # Returns
485///
486/// Empirical copula value at u.
487///
488/// # Examples
489///
490/// ```rust
491/// use copula_core::{empirical_copula_cdf, to_pseudo_observations};
492/// use nalgebra::DMatrix;
493///
494/// let data = DMatrix::from_row_slice(100, 2, &[/* your data */]);
495/// let pseudo_obs = to_pseudo_observations(&data).unwrap();
496/// let cdf_val = empirical_copula_cdf(&pseudo_obs, &[0.5, 0.5]).unwrap();
497/// ```
498pub 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
514/// Compute the sample version of Kendall's tau for multivariate data.
515///
516/// This computes the pairwise Kendall's tau for all variable pairs.
517///
518/// # Arguments
519///
520/// * `data` - Data matrix where each column is a variable
521///
522/// # Returns
523///
524/// Symmetric matrix of pairwise Kendall's tau values.
525pub 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; // Diagonal is 1
536
537        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; // Symmetric
544        }
545    }
546
547    Ok(tau_matrix)
548}
549
550/// Compute the sample version of Spearman's rho for multivariate data.
551///
552/// This computes the pairwise Spearman's rho for all variable pairs.
553///
554/// # Arguments
555///
556/// * `data` - Data matrix where each column is a variable
557///
558/// # Returns
559///
560/// Symmetric matrix of pairwise Spearman's rho values.
561pub 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; // Diagonal is 1
572
573        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; // Symmetric
580        }
581    }
582
583    Ok(rho_matrix)
584}
585
586/// Remove observations with missing values (NaN).
587///
588/// This function removes entire rows that contain any NaN values.
589///
590/// # Arguments
591///
592/// * `data` - Data matrix that may contain NaN values
593///
594/// # Returns
595///
596/// Data matrix with rows containing NaN removed.
597pub 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
619/// Bootstrap resample from a dataset.
620///
621/// Generate a bootstrap sample by sampling with replacement.
622///
623/// # Arguments
624///
625/// * `data` - Original data matrix
626/// * `rng` - Random number generator
627///
628/// # Returns
629///
630/// Bootstrap sample with the same dimensions as the original data.
631pub 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
648/// Compute information criteria for model selection.
649///
650/// # Arguments
651///
652/// * `log_likelihood` - Log-likelihood of the model
653/// * `n_params` - Number of parameters in the model
654/// * `n_obs` - Number of observations
655///
656/// # Returns
657///
658/// Tuple of (AIC, BIC) values.
659pub 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
665/// Validate that pseudo-observations are in the correct range.
666///
667/// Pseudo-observations should be in (0, 1), not exactly 0 or 1.
668///
669/// # Arguments
670///
671/// * `pseudo_obs` - Matrix of pseudo-observations to validate
672///
673/// # Returns
674///
675/// `Ok(())` if valid, error otherwise.
676pub 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        // Expected: [3, 1.5, 4, 1.5, 5] (average rank for ties)
707        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        // Each column should have values [0.25, 0.5, 0.75]
721        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        // Perfect positive correlation
731        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        // Perfect negative correlation
737        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        // Valid correlation matrix
753        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        // Invalid: not symmetric
757        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        // Invalid: diagonal not 1
761        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        // Point (0.5, 0.5) should have 2 observations ≤ it
770        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); // -2*(-100) + 2*3
788        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}