Skip to main content

copula_core/other/
mod.rs

1//! Other copula families module (placeholder).
2//!
3//! ## Bibliography
4//! - Marshall, A. W., & Olkin, I. (1967). A multivariate exponential
5//!   distribution. *Journal of the American Statistical Association*, 62(317),
6//!   30-44.
7//! - Genest, C., & Nešlehová, J. (2007). A primer on copulas for count data.
8//!   *ASTIN Bulletin*, 37(2), 475-515.
9//! - Nelsen, R. B. (2006). *An Introduction to Copulas*. Springer.
10
11use crate::{Copula, CopulaError, Result};
12use nalgebra::DMatrix;
13use rand::seq::IndexedRandom;
14use rand::Rng;
15
16/// Marshall-Olkin copula with parameters α and β in [0,1).
17#[derive(Debug, Clone)]
18pub struct MarshallOlkinCopula {
19    alpha: f64,
20    beta: f64,
21}
22
23validated_serde!("MarshallOlkinCopula", MarshallOlkinCopula { alpha: f64, beta: f64 } => MarshallOlkinCopula::new(alpha, beta));
24
25impl MarshallOlkinCopula {
26    /// Create a new Marshall-Olkin copula.
27    pub fn new(alpha: f64, beta: f64) -> Result<Self> {
28        if !(0.0..1.0).contains(&alpha) || !(0.0..1.0).contains(&beta) {
29            return Err(CopulaError::invalid_parameter(
30                "alpha and beta must be in [0,1)",
31            ));
32        }
33        Ok(Self { alpha, beta })
34    }
35}
36
37impl Copula for MarshallOlkinCopula {
38    fn cdf(&self, u: &[f64]) -> Result<f64> {
39        if u.len() != 2 {
40            return Err(CopulaError::dimension_mismatch(2, u.len()));
41        }
42        crate::error::validate_unit_range(u)?;
43        let u1 = u[0];
44        let u2 = u[1];
45        let term1 = u1.powf(1.0 - self.alpha) * u2;
46        let term2 = u1 * u2.powf(1.0 - self.beta);
47        Ok(crate::utils::clamp_to_frechet_bounds(u, term1.min(term2)))
48    }
49
50    fn pdf(&self, u: &[f64]) -> Result<f64> {
51        if u.len() != 2 {
52            return Err(CopulaError::dimension_mismatch(2, u.len()));
53        }
54        crate::error::validate_unit_range(u)?;
55
56        // Marshall-Olkin copula has a singular component (absolutely continuous + singular part)
57        // The absolutely continuous part has density on the region where the CDF is differentiable
58        // This is a simplified implementation that returns the continuous density component
59
60        let u1 = u[0];
61        let u2 = u[1];
62
63        // Check which term gives the minimum in CDF to determine the region
64        let term1 = u1.powf(1.0 - self.alpha) * u2;
65        let term2 = u1 * u2.powf(1.0 - self.beta);
66
67        // The density exists only in certain regions and is complex
68        // For practical purposes, we provide an approximation
69        if (term1 - term2).abs() < 1e-10 {
70            // On the singular diagonal component - technically has infinite density
71            // Return a large but finite value
72            return Ok(1e6);
73        }
74
75        // Off the diagonal, compute the continuous density component
76        if term1 < term2 {
77            Ok((1.0 - self.alpha) * u1.powf(-self.alpha) * u2.powf(0.0))
78        } else {
79            Ok((1.0 - self.beta) * u1.powf(0.0) * u2.powf(-self.beta))
80        }
81    }
82
83    fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
84        use rand_distr::{Distribution, Exp};
85
86        let mut samples = DMatrix::<f64>::zeros(n, 2);
87
88        // Marshall-Olkin copula can be sampled using exponential random variables
89        // Let X1 ~ Exp(1), X2 ~ Exp(1), X12 ~ Exp(1) be independent
90        // Then U1 = exp(-X1 - X12), U2 = exp(-X2 - X12) follows Marshall-Olkin copula
91
92        let exp_dist =
93            Exp::new(1.0).map_err(|_| CopulaError::computation("failed to create Exp(1)"))?;
94
95        for i in 0..n {
96            let x1 = exp_dist.sample(rng);
97            let x2 = exp_dist.sample(rng);
98            let x12 = exp_dist.sample(rng);
99
100            // Transform using the parameters
101            let u1 = (-x1 / (1.0 - self.alpha) - x12).exp();
102            let u2 = (-x2 / (1.0 - self.beta) - x12).exp();
103
104            samples[(i, 0)] = u1.clamp(1e-10, 1.0 - 1e-10);
105            samples[(i, 1)] = u2.clamp(1e-10, 1.0 - 1e-10);
106        }
107
108        Ok(samples)
109    }
110
111    fn dimension(&self) -> usize {
112        2
113    }
114}
115
116/// Empirical copula based on pseudo-observation data.
117#[derive(Debug, Clone)]
118pub struct EmpiricalCopula {
119    data: DMatrix<f64>,
120}
121
122validated_serde!("EmpiricalCopula", EmpiricalCopula { data: DMatrix<f64> } => EmpiricalCopula::new(data));
123
124impl EmpiricalCopula {
125    /// Create an empirical copula from pseudo-observations.
126    pub fn new(data: DMatrix<f64>) -> Result<Self> {
127        crate::utils::validate_pseudo_observations(&data)?;
128        Ok(Self { data })
129    }
130}
131
132impl Copula for EmpiricalCopula {
133    fn cdf(&self, u: &[f64]) -> Result<f64> {
134        let (n_rows, n_cols) = self.data.shape();
135        if u.len() != n_cols {
136            return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
137        }
138        crate::error::validate_unit_range(u)?;
139        let count = (0..n_rows)
140            .filter(|&i| (0..n_cols).all(|j| self.data[(i, j)] <= u[j]))
141            .count();
142        Ok(count as f64 / n_rows as f64)
143    }
144
145    fn pdf(&self, u: &[f64]) -> Result<f64> {
146        let (_n_rows, n_cols) = self.data.shape();
147        if u.len() != n_cols {
148            return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
149        }
150        crate::error::validate_unit_range(u)?;
151
152        // Empirical copula is discrete, so PDF is not well-defined in the continuous sense
153        // We could use kernel density estimation, but for now return an error with explanation
154        Err(CopulaError::not_implemented(
155            "Empirical copula PDF is not well-defined (discrete distribution). Use CDF instead or implement kernel density estimation."
156        ))
157    }
158
159    fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
160        let (n_rows, n_cols) = self.data.shape();
161        let mut samples = DMatrix::<f64>::zeros(n, n_cols);
162
163        // Sample with replacement from the empirical data
164        let indices: Vec<usize> = (0..n_rows).collect();
165
166        for i in 0..n {
167            let &idx = indices
168                .choose(rng)
169                .ok_or_else(|| CopulaError::computation("failed to sample from indices"))?;
170
171            for j in 0..n_cols {
172                samples[(i, j)] = self.data[(idx, j)];
173            }
174        }
175
176        Ok(samples)
177    }
178
179    fn dimension(&self) -> usize {
180        self.data.ncols()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn marshall_olkin_cdf_product() {
190        let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
191        let cdf = cop.cdf(&[0.4, 0.5]).unwrap();
192        let term1 = 0.4_f64.powf(0.7) * 0.5;
193        let term2 = 0.4 * 0.5_f64.powf(0.6);
194        let expected = term1.min(term2);
195        assert!((cdf - expected).abs() < 1e-12);
196    }
197
198    #[test]
199    fn empirical_cdf_product() {
200        let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.4, 0.6, 0.9, 0.8]);
201        let cop = EmpiricalCopula::new(data).unwrap();
202        let cdf = cop.cdf(&[0.5, 0.7]).unwrap();
203        // manually count
204        assert!((cdf - 2.0 / 3.0).abs() < 1e-12);
205    }
206
207    #[test]
208    fn marshall_olkin_rejects_invalid_params() {
209        assert!(MarshallOlkinCopula::new(-0.1, 0.5).is_err());
210        assert!(MarshallOlkinCopula::new(0.5, 1.0).is_err());
211        assert!(MarshallOlkinCopula::new(1.0, 0.5).is_err());
212    }
213
214    #[test]
215    fn marshall_olkin_dimension() {
216        let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
217        assert_eq!(cop.dimension(), 2);
218    }
219
220    #[test]
221    fn marshall_olkin_cdf_validates_input() {
222        let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
223        assert!(cop.cdf(&[0.5]).is_err());
224        assert!(cop.cdf(&[0.5, 1.1]).is_err());
225    }
226
227    #[test]
228    fn marshall_olkin_sampling() {
229        let mut rng = rand::rng();
230        let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
231        let samples = cop.sample(50, &mut rng).unwrap();
232        assert_eq!(samples.nrows(), 50);
233        assert_eq!(samples.ncols(), 2);
234        for i in 0..50 {
235            for j in 0..2 {
236                let v = samples[(i, j)];
237                assert!(v > 0.0 && v < 1.0);
238            }
239        }
240    }
241
242    #[test]
243    fn empirical_copula_rejects_invalid_data() {
244        let data = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.3, 0.7]);
245        assert!(EmpiricalCopula::new(data).is_err()); // 1.0 not in (0,1)
246    }
247
248    #[test]
249    fn empirical_copula_dimension() {
250        let data = DMatrix::from_row_slice(3, 3, &[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1]);
251        let cop = EmpiricalCopula::new(data).unwrap();
252        assert_eq!(cop.dimension(), 3);
253    }
254
255    #[test]
256    fn empirical_copula_cdf_validates_dimension() {
257        let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
258        let cop = EmpiricalCopula::new(data).unwrap();
259        assert!(cop.cdf(&[0.5]).is_err());
260        assert!(cop.cdf(&[0.5, 0.5, 0.5]).is_err());
261    }
262
263    #[test]
264    fn empirical_copula_pdf_not_implemented() {
265        let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
266        let cop = EmpiricalCopula::new(data).unwrap();
267        assert!(cop.pdf(&[0.5, 0.5]).is_err());
268    }
269
270    #[test]
271    fn empirical_copula_sampling() {
272        let mut rng = rand::rng();
273        let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
274        let cop = EmpiricalCopula::new(data).unwrap();
275        let samples = cop.sample(10, &mut rng).unwrap();
276        assert_eq!(samples.nrows(), 10);
277        assert_eq!(samples.ncols(), 2);
278    }
279}