use std::f64::consts::PI;
use super::{Cdf, Density, Probability, Quantile, Support};
use crate::errors::QlResult;
use crate::fail;
use crate::math::beta::incomplete_beta;
use crate::math::gammafunction::log_gamma;
use crate::require;
use crate::types::Real;
const QUANTILE_ACCURACY: Real = 1e-6;
const QUANTILE_MAX_ITERATIONS: u32 = 50;
#[derive(Clone, Copy, Debug)]
pub struct StudentT {
degrees_of_freedom: Real,
ln_normalization: Real,
}
impl StudentT {
pub fn new(degrees_of_freedom: Real) -> QlResult<Self> {
require!(
degrees_of_freedom.is_finite() && degrees_of_freedom > 0.0,
"degrees of freedom must be a finite positive number, got {degrees_of_freedom}"
);
let half = 0.5 * degrees_of_freedom;
let ln_normalization =
log_gamma(half + 0.5)? - log_gamma(half)? - 0.5 * (degrees_of_freedom * PI).ln();
Ok(StudentT {
degrees_of_freedom,
ln_normalization,
})
}
}
impl Density for StudentT {
fn pdf(&self, x: Real) -> Real {
self.ln_pdf(x).exp()
}
fn ln_pdf(&self, x: Real) -> Real {
let nu = self.degrees_of_freedom;
self.ln_normalization - 0.5 * (nu + 1.0) * (1.0 + x * x / nu).ln()
}
}
impl Cdf for StudentT {
fn cdf(&self, x: Real) -> Real {
if x.is_nan() {
return Real::NAN;
}
let nu = self.degrees_of_freedom;
let z = nu / (nu + x * x);
let tail = 0.5
* incomplete_beta(0.5 * nu, 0.5, z)
.expect("incomplete beta must converge for valid Student-t parameters");
if x <= 0.0 { tail } else { 1.0 - tail }
}
fn survival(&self, x: Real) -> Real {
self.cdf(-x)
}
}
impl Support for StudentT {
fn lower_bound(&self) -> Real {
Real::NEG_INFINITY
}
fn upper_bound(&self) -> Real {
Real::INFINITY
}
}
impl Quantile for StudentT {
fn quantile(&self, p: Probability) -> QlResult<Real> {
let p = p.value();
if p == 0.0 {
return Ok(self.lower_bound());
}
if p == 1.0 {
return Ok(self.upper_bound());
}
let mut x = 0.0;
for _ in 0..QUANTILE_MAX_ITERATIONS {
let diff = (self.cdf(x) - p) / self.pdf(x);
x -= diff;
if diff.abs() <= QUANTILE_ACCURACY {
return Ok(x);
}
}
fail!(
"Student-t quantile did not converge for p={p} (df={})",
self.degrees_of_freedom
);
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: Real = 1e-12;
fn assert_close(got: Real, expected: Real, tol: Real) {
assert!(
(got - expected).abs() <= tol,
"got {got}, expected {expected}, diff {}",
(got - expected).abs()
);
}
#[test]
fn cdf_stays_in_unit_interval_across_supported_domain() {
let dofs = [1.0, 2.0, 3.0, 5.0, 10.0, 30.0, 100.0, 1_000.0, 1.0e6];
let xs = [
Real::NEG_INFINITY,
-1.0e100,
-1.0e10,
-100.0,
-10.0,
-1.0,
-Real::MIN_POSITIVE,
0.0,
Real::MIN_POSITIVE,
1.0,
10.0,
100.0,
1.0e10,
1.0e100,
Real::INFINITY,
];
for nu in dofs {
let dist = StudentT::new(nu).unwrap();
for x in xs {
let p = dist.cdf(x);
assert!((0.0..=1.0).contains(&p), "cdf({x}) = {p} for nu = {nu}");
}
}
}
#[test]
fn cdf_matches_cauchy_closed_form() {
let dist = StudentT::new(1.0).unwrap();
for x in [-3.0, -1.0, -0.25, 0.0, 0.25, 1.0, 3.0_f64] {
let expected = 0.5 + x.atan() / PI;
assert_close(dist.cdf(x), expected, 1e-12);
}
}
#[test]
fn cdf_matches_nu2_closed_form() {
let dist = StudentT::new(2.0).unwrap();
for x in [-4.0, -1.0, 0.0, 0.5, 1.0, 4.0_f64] {
let expected = 0.5 * (1.0 + x / (x * x + 2.0).sqrt());
assert_close(dist.cdf(x), expected, 1e-12);
}
}
#[test]
fn cdf_approaches_normal_for_large_dof() {
let dist = StudentT::new(1.0e8).unwrap();
assert_close(dist.cdf(1.0), 0.841_344_746_068_542_9, 1e-6);
assert_close(dist.cdf(1.96), 0.975_002_104_851_779_5, 1e-6);
}
#[test]
fn pdf_at_zero_matches_analytic() {
assert_close(StudentT::new(1.0).unwrap().pdf(0.0), 1.0 / PI, 1e-13);
assert_close(
StudentT::new(2.0).unwrap().pdf(0.0),
1.0 / (2.0 * 2.0_f64.sqrt()),
1e-13,
);
}
#[test]
fn pdf_is_exp_of_ln_pdf() {
let dist = StudentT::new(7.0).unwrap();
for x in [-5.0, -1.0, 0.0, 0.5, 3.0] {
assert_close(dist.pdf(x), dist.ln_pdf(x).exp(), TOL);
}
}
#[test]
fn limits_at_infinity() {
let dist = StudentT::new(4.0).unwrap();
assert_eq!(dist.cdf(Real::NEG_INFINITY), 0.0);
assert_eq!(dist.cdf(Real::INFINITY), 1.0);
assert_eq!(dist.pdf(Real::INFINITY), 0.0);
assert_eq!(dist.pdf(Real::NEG_INFINITY), 0.0);
assert_eq!(dist.ln_pdf(Real::INFINITY), Real::NEG_INFINITY);
}
#[test]
fn cdf_of_nan_is_nan() {
assert!(StudentT::new(3.0).unwrap().cdf(Real::NAN).is_nan());
}
#[test]
fn cdf_is_symmetric() {
for nu in [1.0, 3.0, 12.0, 250.0] {
let dist = StudentT::new(nu).unwrap();
for x in [0.1, 0.7, 1.5, 4.0, 25.0] {
assert_close(dist.cdf(-x), 1.0 - dist.cdf(x), 1e-12);
}
}
}
#[test]
fn cdf_is_strictly_increasing() {
let dist = StudentT::new(6.0).unwrap();
let mut prev = dist.cdf(-50.0);
let mut x = -50.0;
while x < 50.0 {
x += 0.1;
let cur = dist.cdf(x);
assert!(cur > prev, "cdf not increasing at x = {x}: {prev} -> {cur}");
prev = cur;
}
}
#[test]
fn survival_is_complement_and_uses_symmetry() {
let dist = StudentT::new(9.0).unwrap();
for x in [-2.0, 0.0, 1.0, 8.0] {
assert_close(dist.survival(x), 1.0 - dist.cdf(x), 1e-12);
assert_eq!(dist.survival(x), dist.cdf(-x));
}
}
#[test]
fn quantile_endpoints_are_support_bounds() {
let dist = StudentT::new(5.0).unwrap();
assert_eq!(
dist.quantile(Probability::try_from(0.0).unwrap()).unwrap(),
Real::NEG_INFINITY
);
assert_eq!(
dist.quantile(Probability::try_from(1.0).unwrap()).unwrap(),
Real::INFINITY
);
}
#[test]
fn quantile_of_half_is_zero() {
for nu in [1.0, 4.0, 40.0] {
let dist = StudentT::new(nu).unwrap();
let x = dist.quantile(Probability::try_from(0.5).unwrap()).unwrap();
assert_close(x, 0.0, 1e-12);
}
}
#[test]
fn quantile_round_trips_with_cdf() {
for nu in [1.0, 3.0, 10.0, 100.0] {
let dist = StudentT::new(nu).unwrap();
for p in [0.05, 0.25, 0.5, 0.75, 0.95] {
let x = dist.quantile(Probability::try_from(p).unwrap()).unwrap();
assert_close(dist.cdf(x), p, 1e-6);
}
}
}
#[test]
fn quantile_matches_cauchy_known_value() {
let dist = StudentT::new(1.0).unwrap();
let x = dist.quantile(Probability::try_from(0.75).unwrap()).unwrap();
assert_close(x, 1.0, 1e-6);
}
#[test]
fn new_rejects_nonpositive_nan_and_infinite_dof() {
assert!(StudentT::new(0.0).is_err());
assert!(StudentT::new(-2.0).is_err());
assert!(StudentT::new(Real::NAN).is_err());
assert!(StudentT::new(Real::INFINITY).is_err());
assert!(StudentT::new(Real::NEG_INFINITY).is_err());
}
}