use crate::distributions::Distribution;
use num::Complex;
use statrs::function::gamma::{digamma, gamma, gamma_li};
use RustQuant_error::RustQuantError;
pub struct ChiSquared {
k: usize,
}
impl Default for ChiSquared {
fn default() -> Self {
Self { k: 1 }
}
}
impl ChiSquared {
#[must_use]
pub fn new(k: usize) -> Self {
assert!(k > 0);
Self { k }
}
}
impl Distribution for ChiSquared {
fn cf(&self, t: f64) -> Complex<f64> {
let i: Complex<f64> = Complex::i();
let k = self.k;
(1.0 - 2.0 * i * t).powf(-(k as f64 / 2.0))
}
fn pdf(&self, x: f64) -> f64 {
assert!(if self.k == 1 { x > 0.0 } else { x >= 0.0 });
let k = self.k;
x.powf((k as f64 / 2.0) - 1.0) * (-x / 2.0).exp()
/ (2_f64.powf(k as f64 / 2.0) * gamma(k as f64 / 2.0))
}
fn pmf(&self, x: f64) -> f64 {
self.pdf(x)
}
fn cdf(&self, x: f64) -> f64 {
assert!(if self.k == 1 { x > 0.0 } else { x >= 0.0 });
let k = self.k;
gamma_li(k as f64 / 2.0, x / 2.0) / gamma(k as f64 / 2.0)
}
fn inv_cdf(&self, p: f64) -> f64 {
assert!((0.0..=1.0).contains(&p));
let k = self.k as f64;
let mut x = 0.5 * k;
let mut delta = 0.5 * k;
while delta > 1e-10 {
let cdf = self.cdf(x);
if cdf < p {
x += delta;
} else {
x -= delta;
}
delta *= 0.5;
}
x
}
fn mean(&self) -> f64 {
self.k as f64
}
fn median(&self) -> f64 {
self.k as f64 * (1.0 - (2.0 / (9.0 * self.k as f64))).powf(3.0)
}
fn mode(&self) -> f64 {
0_f64.max(self.k as f64 - 2.0)
}
fn variance(&self) -> f64 {
2.0 * self.k as f64
}
fn skewness(&self) -> f64 {
(8.0 / self.k as f64).sqrt()
}
fn kurtosis(&self) -> f64 {
12.0 / self.k as f64
}
fn entropy(&self) -> f64 {
let k = self.k as f64;
k / 2.0 + (2.0 * gamma(k / 2.0)).ln() + (1.0 - k / 2.0) * digamma(k / 2.0)
}
fn mgf(&self, t: f64) -> f64 {
assert!(t < 0.5);
(1.0 - 2.0 * t).powf(-(self.k as f64) / 2.0)
}
fn sample(&self, n: usize) -> Result<Vec<f64>, RustQuantError> {
use rand::thread_rng;
use rand_distr::{ChiSquared, Distribution};
assert!(n > 0);
let mut rng = thread_rng();
let dist = ChiSquared::new(self.k as f64)?;
let mut variates: Vec<f64> = Vec::with_capacity(n);
for _ in 0..variates.capacity() {
variates.push(dist.sample(&mut rng) as usize as f64);
}
Ok(variates)
}
}
#[cfg(test)]
mod tests {
use super::*;
use RustQuant_utils::assert_approx_equal;
#[test]
fn test_chi_squared_characteristic_function() {
let dist: ChiSquared = ChiSquared::new(1);
let cf = dist.cf(1.0);
assert_approx_equal!(cf.re, 0.568_864_5, 1e-7);
assert_approx_equal!(cf.im, 0.351_577_6, 1e-7);
}
#[test]
fn test_chi_squared_density_function() {
let dist: ChiSquared = ChiSquared::new(1);
assert_approx_equal!(dist.pdf(1.0), 0.241_970_72, 1e-8);
assert_approx_equal!(dist.pdf(2.0), 0.103_776_87, 1e-8);
assert_approx_equal!(dist.pdf(3.0), 0.051_393_44, 1e-8);
assert_approx_equal!(dist.pdf(4.0), 0.026_995_48, 1e-8);
assert_approx_equal!(dist.pdf(5.0), 0.014_644_98, 1e-8);
}
#[test]
fn test_chi_squared_distribution_function() {
let dist: ChiSquared = ChiSquared::new(1);
assert_approx_equal!(dist.cdf(1.0), 0.682_689_5, 1e-7);
assert_approx_equal!(dist.cdf(2.0), 0.842_700_8, 1e-7);
assert_approx_equal!(dist.cdf(3.0), 0.916_735_5, 1e-7);
assert_approx_equal!(dist.cdf(4.0), 0.954_499_7, 1e-7);
assert_approx_equal!(dist.cdf(5.0), 0.974_652_7, 1e-7);
}
}