1use crate::{Copula, CopulaError, Result};
28use nalgebra::DMatrix;
29
30pub struct EmpiricalCdf {
34 data: Vec<f64>,
36 n: usize,
37}
38
39impl EmpiricalCdf {
40 pub fn new(mut data: Vec<f64>) -> Result<Self> {
48 if data.is_empty() {
49 return Err(CopulaError::data_error(
50 "EmpiricalCdf requires non-empty data",
51 ));
52 }
53 if data.iter().any(|x| !x.is_finite()) {
54 return Err(CopulaError::data_error(
55 "EmpiricalCdf data contains non-finite values (NaN or infinite)",
56 ));
57 }
58 data.sort_by(|a, b| a.total_cmp(b));
59 let n = data.len();
60 Ok(Self { data, n })
61 }
62
63 pub fn eval(&self, x: f64) -> f64 {
71 if self.n == 0 {
72 return 0.0;
73 }
74
75 let count = self.data.iter().filter(|&&xi| xi <= x).count();
77 count as f64 / self.n as f64
78 }
79
80 pub fn to_pseudo_observations(&self) -> Vec<f64> {
84 let mut pseudo = Vec::with_capacity(self.n);
85
86 for &x in &self.data {
87 let rank = self.data.iter().filter(|&&xi| xi < x).count() + 1;
89 pseudo.push(rank as f64 / (self.n + 1) as f64);
90 }
91
92 pseudo
93 }
94}
95
96pub fn to_pseudo_observations(data: &DMatrix<f64>) -> Result<DMatrix<f64>> {
104 let n = data.nrows();
105 let d = data.ncols();
106 if n == 0 || d == 0 {
107 return Err(CopulaError::data_error("Data matrix must be non-empty"));
108 }
109 let mut pseudo = DMatrix::<f64>::zeros(n, d);
110
111 for j in 0..d {
113 let column: Vec<f64> = (0..n).map(|i| data[(i, j)]).collect();
114 let _ecdf = EmpiricalCdf::new(column.clone())?;
115
116 let mut indexed: Vec<(usize, f64)> =
118 column.iter().enumerate().map(|(i, &x)| (i, x)).collect();
119 indexed.sort_by(|a, b| a.1.total_cmp(&b.1));
120
121 for (new_idx, (orig_idx, _)) in indexed.iter().enumerate() {
122 pseudo[(*orig_idx, j)] = (new_idx + 1) as f64 / (n + 1) as f64;
123 }
124 }
125
126 Ok(pseudo)
127}
128
129pub fn kendall_tau(x: &[f64], y: &[f64]) -> Result<f64> {
141 if x.len() != y.len() {
142 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
143 }
144
145 let n = x.len();
146 if n < 2 {
147 return Err(CopulaError::invalid_parameter(
148 "need at least 2 observations",
149 ));
150 }
151
152 let mut concordant = 0;
153 let mut discordant = 0;
154
155 for i in 0..n {
156 for j in (i + 1)..n {
157 let dx = x[j] - x[i];
158 let dy = y[j] - y[i];
159
160 if dx * dy > 0.0 {
161 concordant += 1;
162 } else if dx * dy < 0.0 {
163 discordant += 1;
164 }
165 }
167 }
168
169 let total_pairs = (n * (n - 1)) / 2;
170 Ok((concordant - discordant) as f64 / total_pairs as f64)
171}
172
173pub fn spearman_rho(x: &[f64], y: &[f64]) -> Result<f64> {
185 if x.len() != y.len() {
186 return Err(CopulaError::dimension_mismatch(x.len(), y.len()));
187 }
188
189 let n = x.len();
190 if n < 2 {
191 return Err(CopulaError::invalid_parameter(
192 "need at least 2 observations",
193 ));
194 }
195
196 let rank_x = rank(x);
198 let rank_y = rank(y);
199
200 pearson_correlation(&rank_x, &rank_y)
202}
203
204fn rank(data: &[f64]) -> Vec<f64> {
206 let n = data.len();
207 let mut indexed: Vec<(usize, f64)> = data.iter().enumerate().map(|(i, &x)| (i, x)).collect();
208 indexed.sort_by(|a, b| a.1.total_cmp(&b.1));
209
210 let mut ranks = vec![0.0; n];
211 for (rank_pos, (orig_idx, _)) in indexed.iter().enumerate() {
212 ranks[*orig_idx] = (rank_pos + 1) as f64;
213 }
214
215 ranks
216}
217
218fn pearson_correlation(x: &[f64], y: &[f64]) -> Result<f64> {
220 let n = x.len();
221 if n < 2 {
222 return Err(CopulaError::invalid_parameter(
223 "need at least 2 observations",
224 ));
225 }
226
227 let mean_x: f64 = x.iter().sum::<f64>() / n as f64;
228 let mean_y: f64 = y.iter().sum::<f64>() / n as f64;
229
230 let mut cov = 0.0;
231 let mut var_x = 0.0;
232 let mut var_y = 0.0;
233
234 for i in 0..n {
235 let dx = x[i] - mean_x;
236 let dy = y[i] - mean_y;
237 cov += dx * dy;
238 var_x += dx * dx;
239 var_y += dy * dy;
240 }
241
242 if var_x.abs() < 1e-10 || var_y.abs() < 1e-10 {
243 return Ok(0.0);
244 }
245
246 Ok(cov / (var_x * var_y).sqrt())
247}
248
249pub struct CMLEstimator<'a, C: Copula> {
253 copula: &'a C,
254}
255
256impl<'a, C: Copula> CMLEstimator<'a, C> {
257 pub fn new(copula: &'a C) -> Self {
259 Self { copula }
260 }
261
262 pub fn neg_log_likelihood(&self, pseudo_obs: &DMatrix<f64>) -> Result<f64> {
270 let n = pseudo_obs.nrows();
271 let d = pseudo_obs.ncols();
272
273 if d != self.copula.dimension() {
274 return Err(CopulaError::dimension_mismatch(self.copula.dimension(), d));
275 }
276
277 let mut log_lik = 0.0;
278
279 for i in 0..n {
280 let u: Vec<f64> = (0..d).map(|j| pseudo_obs[(i, j)]).collect();
281
282 let c = self.copula.pdf(&u)?;
284
285 if c > 0.0 {
286 log_lik += c.ln();
287 } else {
288 log_lik += (-10.0_f64).ln();
290 }
291 }
292
293 Ok(-log_lik)
294 }
295
296 pub fn fit(&self, data: &DMatrix<f64>) -> Result<f64> {
301 let pseudo = to_pseudo_observations(data)?;
303
304 self.neg_log_likelihood(&pseudo)
307 }
308}
309
310pub struct TauEstimator;
318
319impl TauEstimator {
320 pub fn clayton_from_tau(tau: f64) -> Result<f64> {
324 if tau <= -1.0 || tau >= 1.0 {
325 return Err(CopulaError::invalid_parameter("tau must be in (-1, 1)"));
326 }
327 if tau <= 0.0 {
328 return Err(CopulaError::invalid_parameter(
329 "Clayton requires positive tau",
330 ));
331 }
332 Ok(2.0 * tau / (1.0 - tau))
333 }
334
335 pub fn gumbel_from_tau(tau: f64) -> Result<f64> {
339 if tau <= 0.0 || tau >= 1.0 {
340 return Err(CopulaError::invalid_parameter(
341 "Gumbel requires tau in (0, 1)",
342 ));
343 }
344 Ok(1.0 / (1.0 - tau))
345 }
346
347 pub fn gaussian_from_tau(tau: f64) -> Result<f64> {
351 if tau <= -1.0 || tau >= 1.0 {
352 return Err(CopulaError::invalid_parameter("tau must be in (-1, 1)"));
353 }
354 Ok((std::f64::consts::PI * tau / 2.0).sin())
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_empirical_cdf() {
364 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
365 let ecdf = EmpiricalCdf::new(data).unwrap();
366
367 assert_eq!(ecdf.eval(0.0), 0.0);
368 assert_eq!(ecdf.eval(3.0), 0.6); assert_eq!(ecdf.eval(6.0), 1.0);
370 }
371
372 #[test]
373 fn test_pseudo_observations() {
374 let data = vec![1.0, 3.0, 2.0, 5.0, 4.0];
375 let ecdf = EmpiricalCdf::new(data).unwrap();
376 let pseudo = ecdf.to_pseudo_observations();
377
378 for &p in &pseudo {
380 assert!(p > 0.0 && p < 1.0);
381 }
382 }
383
384 #[test]
385 fn test_kendall_tau_perfect_concordance() {
386 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
387 let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
388 let tau = kendall_tau(&x, &y).unwrap();
389 assert!((tau - 1.0).abs() < 1e-10);
390 }
391
392 #[test]
393 fn test_kendall_tau_perfect_discordance() {
394 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
395 let y = vec![10.0, 8.0, 6.0, 4.0, 2.0];
396 let tau = kendall_tau(&x, &y).unwrap();
397 assert!((tau + 1.0).abs() < 1e-10);
398 }
399
400 #[test]
401 fn test_spearman_rho() {
402 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
403 let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
404 let rho = spearman_rho(&x, &y).unwrap();
405 assert!((rho - 1.0).abs() < 1e-10);
406 }
407
408 #[test]
409 fn test_clayton_from_tau() {
410 let tau = 0.5;
411 let theta = TauEstimator::clayton_from_tau(tau).unwrap();
412 assert!((theta - 2.0).abs() < 1e-10);
414 }
415
416 #[test]
417 fn test_gumbel_from_tau() {
418 let tau = 0.5;
419 let theta = TauEstimator::gumbel_from_tau(tau).unwrap();
420 assert!((theta - 2.0).abs() < 1e-10);
422 }
423
424 #[test]
425 fn test_to_pseudo_observations() {
426 let data = DMatrix::from_row_slice(3, 2, &[1.0, 5.0, 2.0, 3.0, 3.0, 1.0]);
427
428 let pseudo = to_pseudo_observations(&data).unwrap();
429
430 assert_eq!(pseudo.nrows(), 3);
432 assert_eq!(pseudo.ncols(), 2);
433
434 for i in 0..3 {
436 for j in 0..2 {
437 assert!(pseudo[(i, j)] > 0.0 && pseudo[(i, j)] < 1.0);
438 }
439 }
440 }
441
442 #[test]
443 fn empirical_cdf_rejects_nan() {
444 let data = vec![1.0, f64::NAN, 3.0];
445 assert!(EmpiricalCdf::new(data).is_err());
446 }
447
448 #[test]
449 fn empirical_cdf_rejects_infinity() {
450 let data = vec![1.0, f64::INFINITY, 3.0];
451 assert!(EmpiricalCdf::new(data).is_err());
452 }
453
454 #[test]
455 fn to_pseudo_observations_rejects_nan() {
456 let data = DMatrix::from_row_slice(2, 2, &[1.0, 2.0, f64::NAN, 4.0]);
457 assert!(to_pseudo_observations(&data).is_err());
458 }
459}