1use crate::dist::{Distribution, DistributionMethods, RegressionDistn};
2use crate::scores::{LogScore, Scorable};
3use ndarray::{array, Array1, Array2, Array3};
4use rand::prelude::*;
5use statrs::distribution::{Cauchy as CauchyDist, Continuous, ContinuousCDF};
6
7#[derive(Debug, Clone)]
15pub struct Cauchy {
16 pub loc: Array1<f64>,
18 pub scale: Array1<f64>,
20 pub var: Array1<f64>,
22 _params: Array2<f64>,
24}
25
26const CAUCHY_DF: f64 = 1.0;
28
29impl Distribution for Cauchy {
30 fn from_params(params: &Array2<f64>) -> Self {
31 let loc = params.column(0).to_owned();
32 let scale = params.column(1).mapv(f64::exp);
33 let var = &scale * &scale;
34 Cauchy {
35 loc,
36 scale,
37 var,
38 _params: params.clone(),
39 }
40 }
41
42 fn fit(y: &Array1<f64>) -> Array1<f64> {
43 let n = y.len();
45 if n == 0 {
46 return array![0.0, 0.0];
47 }
48
49 let mut sorted: Vec<f64> = y.to_vec();
50 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
51
52 let median = if n % 2 == 0 {
54 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
55 } else {
56 sorted[n / 2]
57 };
58
59 let q1_idx = n / 4;
61 let q3_idx = 3 * n / 4;
62 let iqr = sorted[q3_idx] - sorted[q1_idx];
63 let scale = (iqr / 2.0).max(1e-6);
64
65 array![median, scale.ln()]
66 }
67
68 fn n_params(&self) -> usize {
69 2
70 }
71
72 fn predict(&self) -> Array1<f64> {
73 self.loc.clone()
75 }
76
77 fn params(&self) -> &Array2<f64> {
78 &self._params
79 }
80}
81
82impl RegressionDistn for Cauchy {}
83
84impl DistributionMethods for Cauchy {
85 fn mean(&self) -> Array1<f64> {
86 Array1::from_elem(self.loc.len(), f64::NAN)
88 }
89
90 fn variance(&self) -> Array1<f64> {
91 Array1::from_elem(self.loc.len(), f64::NAN)
93 }
94
95 fn std(&self) -> Array1<f64> {
96 Array1::from_elem(self.loc.len(), f64::NAN)
98 }
99
100 fn pdf(&self, y: &Array1<f64>) -> Array1<f64> {
101 let mut result = Array1::zeros(y.len());
102 for i in 0..y.len() {
103 if let Ok(d) = CauchyDist::new(self.loc[i], self.scale[i]) {
104 result[i] = d.pdf(y[i]);
105 }
106 }
107 result
108 }
109
110 fn logpdf(&self, y: &Array1<f64>) -> Array1<f64> {
111 let mut result = Array1::zeros(y.len());
112 for i in 0..y.len() {
113 if let Ok(d) = CauchyDist::new(self.loc[i], self.scale[i]) {
114 result[i] = d.ln_pdf(y[i]);
115 }
116 }
117 result
118 }
119
120 fn cdf(&self, y: &Array1<f64>) -> Array1<f64> {
121 let mut result = Array1::zeros(y.len());
122 for i in 0..y.len() {
123 if let Ok(d) = CauchyDist::new(self.loc[i], self.scale[i]) {
124 result[i] = d.cdf(y[i]);
125 }
126 }
127 result
128 }
129
130 fn ppf(&self, q: &Array1<f64>) -> Array1<f64> {
131 let mut result = Array1::zeros(q.len());
133 for i in 0..q.len() {
134 let q_clamped = q[i].clamp(1e-15, 1.0 - 1e-15);
135 result[i] =
136 self.loc[i] + self.scale[i] * (std::f64::consts::PI * (q_clamped - 0.5)).tan();
137 }
138 result
139 }
140
141 fn sample(&self, n_samples: usize) -> Array2<f64> {
142 let n_obs = self.loc.len();
143 let mut samples = Array2::zeros((n_samples, n_obs));
144 let mut rng = rand::rng();
145
146 for i in 0..n_obs {
147 for s in 0..n_samples {
148 let u: f64 = rng.random();
150 samples[[s, i]] =
151 self.loc[i] + self.scale[i] * (std::f64::consts::PI * (u - 0.5)).tan();
152 }
153 }
154 samples
155 }
156
157 fn median(&self) -> Array1<f64> {
158 self.loc.clone()
160 }
161
162 fn mode(&self) -> Array1<f64> {
163 self.loc.clone()
165 }
166}
167
168impl Scorable<LogScore> for Cauchy {
169 fn score(&self, y: &Array1<f64>) -> Array1<f64> {
170 let mut scores = Array1::zeros(y.len());
171 for (i, &y_i) in y.iter().enumerate() {
172 let d = CauchyDist::new(self.loc[i], self.scale[i]).unwrap();
173 scores[i] = -d.ln_pdf(y_i);
174 }
175 scores
176 }
177
178 fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
179 let n_obs = y.len();
181 let mut d_params = Array2::zeros((n_obs, 2));
182
183 for i in 0..n_obs {
184 let loc_i = self.loc[i];
185 let var_i = self.var[i];
186 let y_i = y[i];
187
188 let diff = y_i - loc_i;
189 let diff_sq = diff * diff;
190 let denom = CAUCHY_DF * var_i + diff_sq;
191
192 d_params[[i, 0]] = -(CAUCHY_DF + 1.0) * diff / denom;
194
195 d_params[[i, 1]] = 1.0 - (CAUCHY_DF + 1.0) * diff_sq / denom;
197 }
198
199 d_params
200 }
201
202 fn metric(&self) -> Array3<f64> {
203 let n_obs = self.loc.len();
207 let mut fi = Array3::zeros((n_obs, 2, 2));
208
209 for i in 0..n_obs {
210 let var_i = self.var[i];
211 fi[[i, 0, 0]] = (CAUCHY_DF + 1.0) / ((CAUCHY_DF + 3.0) * var_i);
212 fi[[i, 1, 1]] = CAUCHY_DF / (2.0 * (CAUCHY_DF + 3.0) * var_i);
213 }
214
215 fi
216 }
217}
218
219#[derive(Debug, Clone)]
223pub struct CauchyFixedVar {
224 pub loc: Array1<f64>,
226 pub scale: Array1<f64>,
228 pub var: Array1<f64>,
230 _params: Array2<f64>,
232}
233
234impl Distribution for CauchyFixedVar {
235 fn from_params(params: &Array2<f64>) -> Self {
236 let loc = params.column(0).to_owned();
237 let n = loc.len();
238 let scale = Array1::ones(n);
239 let var = Array1::ones(n);
240 CauchyFixedVar {
241 loc,
242 scale,
243 var,
244 _params: params.clone(),
245 }
246 }
247
248 fn fit(y: &Array1<f64>) -> Array1<f64> {
249 let n = y.len();
251 if n == 0 {
252 return array![0.0];
253 }
254
255 let mut sorted: Vec<f64> = y.to_vec();
256 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
257
258 let median = if n % 2 == 0 {
259 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
260 } else {
261 sorted[n / 2]
262 };
263
264 array![median]
265 }
266
267 fn n_params(&self) -> usize {
268 1
269 }
270
271 fn predict(&self) -> Array1<f64> {
272 self.loc.clone()
273 }
274
275 fn params(&self) -> &Array2<f64> {
276 &self._params
277 }
278}
279
280impl RegressionDistn for CauchyFixedVar {}
281
282impl DistributionMethods for CauchyFixedVar {
283 fn mean(&self) -> Array1<f64> {
284 Array1::from_elem(self.loc.len(), f64::NAN)
285 }
286
287 fn variance(&self) -> Array1<f64> {
288 Array1::from_elem(self.loc.len(), f64::NAN)
289 }
290
291 fn pdf(&self, y: &Array1<f64>) -> Array1<f64> {
292 let mut result = Array1::zeros(y.len());
293 for i in 0..y.len() {
294 if let Ok(d) = CauchyDist::new(self.loc[i], self.scale[i]) {
295 result[i] = d.pdf(y[i]);
296 }
297 }
298 result
299 }
300
301 fn cdf(&self, y: &Array1<f64>) -> Array1<f64> {
302 let mut result = Array1::zeros(y.len());
303 for i in 0..y.len() {
304 if let Ok(d) = CauchyDist::new(self.loc[i], self.scale[i]) {
305 result[i] = d.cdf(y[i]);
306 }
307 }
308 result
309 }
310
311 fn ppf(&self, q: &Array1<f64>) -> Array1<f64> {
312 let mut result = Array1::zeros(q.len());
313 for i in 0..q.len() {
314 let q_clamped = q[i].clamp(1e-15, 1.0 - 1e-15);
315 result[i] =
316 self.loc[i] + self.scale[i] * (std::f64::consts::PI * (q_clamped - 0.5)).tan();
317 }
318 result
319 }
320
321 fn sample(&self, n_samples: usize) -> Array2<f64> {
322 let n_obs = self.loc.len();
323 let mut samples = Array2::zeros((n_samples, n_obs));
324 let mut rng = rand::rng();
325
326 for i in 0..n_obs {
327 for s in 0..n_samples {
328 let u: f64 = rng.random();
329 samples[[s, i]] =
330 self.loc[i] + self.scale[i] * (std::f64::consts::PI * (u - 0.5)).tan();
331 }
332 }
333 samples
334 }
335
336 fn median(&self) -> Array1<f64> {
337 self.loc.clone()
338 }
339
340 fn mode(&self) -> Array1<f64> {
341 self.loc.clone()
342 }
343}
344
345impl Scorable<LogScore> for CauchyFixedVar {
346 fn score(&self, y: &Array1<f64>) -> Array1<f64> {
347 let mut scores = Array1::zeros(y.len());
348 for (i, &y_i) in y.iter().enumerate() {
349 let d = CauchyDist::new(self.loc[i], self.scale[i]).unwrap();
350 scores[i] = -d.ln_pdf(y_i);
351 }
352 scores
353 }
354
355 fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
356 let n_obs = y.len();
357 let mut d_params = Array2::zeros((n_obs, 1));
358
359 for i in 0..n_obs {
360 let loc_i = self.loc[i];
361 let var_i = self.var[i];
362 let y_i = y[i];
363
364 let diff = y_i - loc_i;
365 let diff_sq = diff * diff;
366
367 let num = (CAUCHY_DF + 1.0) * (2.0 / (CAUCHY_DF * var_i)) * diff;
369 let den = 2.0 * (1.0 + (1.0 / (CAUCHY_DF * var_i)) * diff_sq);
370 d_params[[i, 0]] = -num / den;
371 }
372
373 d_params
374 }
375
376 fn metric(&self) -> Array3<f64> {
377 let n_obs = self.loc.len();
378 let mut fi = Array3::zeros((n_obs, 1, 1));
379
380 for i in 0..n_obs {
381 let var_i = self.var[i];
382 fi[[i, 0, 0]] = (CAUCHY_DF + 1.0) / ((CAUCHY_DF + 3.0) * var_i);
383 }
384
385 fi
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use approx::assert_relative_eq;
393
394 #[test]
395 fn test_cauchy_distribution_methods() {
396 let params = Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 5.0, 1.0_f64.ln()]).unwrap();
397 let dist = Cauchy::from_params(¶ms);
398
399 assert!(dist.mean()[0].is_nan());
401 assert!(dist.variance()[0].is_nan());
402
403 let median = dist.median();
405 assert_relative_eq!(median[0], 0.0, epsilon = 1e-10);
406 assert_relative_eq!(median[1], 5.0, epsilon = 1e-10);
407
408 let mode = dist.mode();
410 assert_relative_eq!(mode[0], 0.0, epsilon = 1e-10);
411 assert_relative_eq!(mode[1], 5.0, epsilon = 1e-10);
412 }
413
414 #[test]
415 fn test_cauchy_cdf_ppf() {
416 let params = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).unwrap();
418 let dist = Cauchy::from_params(¶ms);
419
420 let y = Array1::from_vec(vec![0.0, 0.0, 0.0]);
422 let cdf = dist.cdf(&y);
423 assert_relative_eq!(cdf[0], 0.5, epsilon = 1e-10);
424
425 let q = Array1::from_vec(vec![0.5, 0.5, 0.5]);
427 let ppf = dist.ppf(&q);
428 assert_relative_eq!(ppf[0], 0.0, epsilon = 1e-10);
429
430 let q = Array1::from_vec(vec![0.25, 0.5, 0.75]);
432 let ppf = dist.ppf(&q);
433 let cdf_of_ppf = dist.cdf(&ppf);
434 assert_relative_eq!(cdf_of_ppf[0], 0.25, epsilon = 1e-6);
435 assert_relative_eq!(cdf_of_ppf[1], 0.5, epsilon = 1e-6);
436 assert_relative_eq!(cdf_of_ppf[2], 0.75, epsilon = 1e-6);
437 }
438
439 #[test]
440 fn test_cauchy_pdf() {
441 let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
442 let dist = Cauchy::from_params(¶ms);
443
444 let y = Array1::from_vec(vec![0.0]);
446 let pdf = dist.pdf(&y);
447 assert_relative_eq!(pdf[0], 1.0 / std::f64::consts::PI, epsilon = 1e-10);
448 }
449
450 #[test]
451 fn test_cauchy_sample() {
452 let params = Array2::from_shape_vec((1, 2), vec![5.0, 0.0]).unwrap();
453 let dist = Cauchy::from_params(¶ms);
454
455 let samples = dist.sample(1000);
456 assert_eq!(samples.shape(), &[1000, 1]);
457
458 let mut sample_vec: Vec<f64> = samples.column(0).to_vec();
461 sample_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
462 let sample_median = sample_vec[500];
463 assert!((sample_median - 5.0).abs() < 1.0);
464 }
465
466 #[test]
467 fn test_cauchy_fit() {
468 let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
469 let params = Cauchy::fit(&y);
470 assert_eq!(params.len(), 2);
471 assert_relative_eq!(params[0], 3.0, epsilon = 1e-10);
473 }
474
475 #[test]
476 fn test_cauchy_logscore() {
477 let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
478 let dist = Cauchy::from_params(¶ms);
479
480 let y = Array1::from_vec(vec![0.0]);
481 let score = Scorable::<LogScore>::score(&dist, &y);
482
483 assert_relative_eq!(score[0], std::f64::consts::PI.ln(), epsilon = 1e-10);
485 }
486
487 #[test]
488 fn test_cauchy_fixed_var() {
489 let params = Array2::from_shape_vec((1, 1), vec![5.0]).unwrap();
490 let dist = CauchyFixedVar::from_params(¶ms);
491
492 assert_relative_eq!(dist.median()[0], 5.0, epsilon = 1e-10);
493 assert_relative_eq!(dist.scale[0], 1.0, epsilon = 1e-10);
494
495 let y = Array1::from_vec(vec![5.0]);
496 let cdf = dist.cdf(&y);
497 assert_relative_eq!(cdf[0], 0.5, epsilon = 1e-10);
498 }
499
500 #[test]
501 fn test_cauchy_interval() {
502 let params = Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).unwrap();
503 let dist = Cauchy::from_params(¶ms);
504
505 let (lower, upper) = dist.interval(0.5);
506 assert_relative_eq!(lower[0], -1.0, epsilon = 1e-10);
508 assert_relative_eq!(upper[0], 1.0, epsilon = 1e-10);
509 }
510}