stats_claw/distributions/sampling/
students_t.rs1use super::super::{Cdf, LogCdf, Moments, Pdf, Quantile, Sample, bisection_quantile, count_to_f64};
23use crate::distributions::TDistribution;
24use crate::rng::SplitMix64;
25use crate::special::{betai, ln_betai_lower, ln_gamma};
26use std::f64::consts::{LN_2, PI};
27
28impl TDistribution {
29 fn nu(&self) -> f64 {
31 count_to_f64(self.degrees_of_freedom)
32 }
33}
34
35impl Pdf for TDistribution {
36 fn pdf(&self, x: f64) -> f64 {
37 let nu = self.nu();
38 let gamma_ratio = ln_gamma(0.5 * (nu + 1.0)) - ln_gamma(0.5 * nu);
39 let log_norm = 0.5f64.mul_add(-(nu * PI).ln(), gamma_ratio);
40 let ln_kernel = -0.5 * (nu + 1.0) * x.mul_add(x / nu, 1.0).ln();
41 (log_norm + ln_kernel).exp()
42 }
43}
44
45impl Cdf for TDistribution {
46 fn cdf(&self, x: f64) -> f64 {
47 let nu = self.nu();
48 let ib = betai(0.5 * nu, 0.5, nu / x.mul_add(x, nu));
50 if x >= 0.0 {
51 0.5f64.mul_add(-ib, 1.0)
52 } else {
53 0.5 * ib
54 }
55 }
56}
57
58impl LogCdf for TDistribution {
59 fn logsf(&self, x: f64) -> f64 {
60 self.logcdf(-x)
62 }
63 fn logcdf(&self, x: f64) -> f64 {
64 let nu = self.nu();
65 let ln_ib = ln_betai_lower(0.5 * nu, 0.5, nu / x.mul_add(x, nu));
67 if x <= 0.0 {
68 ln_ib - LN_2
70 } else {
71 (-0.5 * ln_ib.exp()).ln_1p()
73 }
74 }
75}
76
77impl Quantile for TDistribution {
78 fn quantile(&self, p: f64) -> f64 {
79 if (p - 0.5).abs() < f64::EPSILON {
84 return 0.0;
85 }
86 if p < 0.5 {
87 return -self.quantile(1.0 - p);
88 }
89 bisection_quantile(p, 0.0, 1.0e6, |x| self.cdf(x))
90 }
91}
92
93impl Moments for TDistribution {
94 fn mean(&self) -> Option<f64> {
95 if self.degrees_of_freedom > 1 {
96 Some(0.0)
97 } else {
98 None
99 }
100 }
101 fn variance(&self) -> Option<f64> {
102 let nu = self.nu();
103 if self.degrees_of_freedom > 2 {
104 Some(nu / (nu - 2.0))
105 } else {
106 None
107 }
108 }
109}
110
111impl Sample for TDistribution {
112 fn sample(&self, rng: &mut SplitMix64) -> f64 {
113 let z = rng.standard_normal();
114 let mut chi2 = 0.0;
115 for _ in 0..self.degrees_of_freedom.max(1) {
116 let g = rng.standard_normal();
117 chi2 = g.mul_add(g, chi2);
118 }
119 z / (chi2 / self.nu()).sqrt()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
129 fn density_is_symmetric() {
130 let d = TDistribution {
131 degrees_of_freedom: 7,
132 ..Default::default()
133 };
134 assert!((d.pdf(1.3) - d.pdf(-1.3)).abs() < 1e-12);
135 }
136
137 #[test]
139 fn cdf_at_zero_is_half() {
140 let d = TDistribution {
141 degrees_of_freedom: 4,
142 ..Default::default()
143 };
144 assert!((d.cdf(0.0) - 0.5).abs() < 1e-12);
145 }
146
147 #[test]
151 fn logsf_matches_scipy_and_stays_finite() {
152 let d = TDistribution {
153 degrees_of_freedom: 5,
154 ..Default::default()
155 };
156 let body = d.logsf(1.0);
157 assert!(
158 ((body - (1.0 - d.cdf(1.0)).ln()) / body.abs()).abs() < 1e-9,
159 "logsf body was {body}"
160 );
161 let tail = d.logsf(50.0);
162 let want = -17.314_140_361_404_83; assert!(tail.is_finite(), "logsf(50) was {tail}");
164 assert!(
165 ((tail - want) / want).abs() < 1e-9,
166 "logsf(50) = {tail}, want {want}"
167 );
168 }
169}