statrs/distribution/
chi_squared.rs

1use crate::distribution::{Continuous, ContinuousCDF, Gamma};
2use crate::statistics::*;
3use crate::Result;
4use rand::Rng;
5use std::f64;
6
7/// Implements the
8/// [Chi-squared](https://en.wikipedia.org/wiki/Chi-squared_distribution)
9/// distribution which is a special case of the
10/// [Gamma](https://en.wikipedia.org/wiki/Gamma_distribution) distribution
11/// (referenced [Here](./struct.Gamma.html))
12///
13/// # Examples
14///
15/// ```
16/// use statrs::distribution::{ChiSquared, Continuous};
17/// use statrs::statistics::Distribution;
18/// use statrs::prec;
19///
20/// let n = ChiSquared::new(3.0).unwrap();
21/// assert_eq!(n.mean().unwrap(), 3.0);
22/// assert!(prec::almost_eq(n.pdf(4.0), 0.107981933026376103901, 1e-15));
23/// ```
24#[derive(Debug, Copy, Clone, PartialEq)]
25pub struct ChiSquared {
26    freedom: f64,
27    g: Gamma,
28}
29
30impl ChiSquared {
31    /// Constructs a new chi-squared distribution with `freedom`
32    /// degrees of freedom. This is equivalent to a Gamma distribution
33    /// with a shape of `freedom / 2.0` and a rate of `0.5`.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if `freedom` is `NaN` or less than
38    /// or equal to `0.0`
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use statrs::distribution::ChiSquared;
44    ///
45    /// let mut result = ChiSquared::new(3.0);
46    /// assert!(result.is_ok());
47    ///
48    /// result = ChiSquared::new(0.0);
49    /// assert!(result.is_err());
50    /// ```
51    pub fn new(freedom: f64) -> Result<ChiSquared> {
52        Gamma::new(freedom / 2.0, 0.5).map(|g| ChiSquared { freedom, g })
53    }
54
55    /// Returns the degrees of freedom of the chi-squared
56    /// distribution
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use statrs::distribution::ChiSquared;
62    ///
63    /// let n = ChiSquared::new(3.0).unwrap();
64    /// assert_eq!(n.freedom(), 3.0);
65    /// ```
66    pub fn freedom(&self) -> f64 {
67        self.freedom
68    }
69
70    /// Returns the shape of the underlying Gamma distribution
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use statrs::distribution::ChiSquared;
76    ///
77    /// let n = ChiSquared::new(3.0).unwrap();
78    /// assert_eq!(n.shape(), 3.0 / 2.0);
79    /// ```
80    pub fn shape(&self) -> f64 {
81        self.g.shape()
82    }
83
84    /// Returns the rate of the underlying Gamma distribution
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use statrs::distribution::ChiSquared;
90    ///
91    /// let n = ChiSquared::new(3.0).unwrap();
92    /// assert_eq!(n.rate(), 0.5);
93    /// ```
94    pub fn rate(&self) -> f64 {
95        self.g.rate()
96    }
97}
98
99impl ::rand::distributions::Distribution<f64> for ChiSquared {
100    fn sample<R: Rng + ?Sized>(&self, r: &mut R) -> f64 {
101        ::rand::distributions::Distribution::sample(&self.g, r)
102    }
103}
104
105impl ContinuousCDF<f64, f64> for ChiSquared {
106    /// Calculates the cumulative distribution function for the
107    /// chi-squared distribution at `x`
108    ///
109    /// # Formula
110    ///
111    /// ```ignore
112    /// (1 / Γ(k / 2)) * γ(k / 2, x / 2)
113    /// ```
114    ///
115    /// where `k` is the degrees of freedom, `Γ` is the gamma function,
116    /// and `γ` is the lower incomplete gamma function
117    fn cdf(&self, x: f64) -> f64 {
118        self.g.cdf(x)
119    }
120
121    /// Calculates the cumulative distribution function for the
122    /// chi-squared distribution at `x`
123    ///
124    /// # Formula
125    ///
126    /// ```ignore
127    /// (1 / Γ(k / 2)) * γ(k / 2, x / 2)
128    /// ```
129    ///
130    /// where `k` is the degrees of freedom, `Γ` is the gamma function,
131    /// and `γ` is the upper incomplete gamma function
132    fn sf(&self, x: f64) -> f64 {
133        self.g.sf(x)
134    }
135}
136
137impl Min<f64> for ChiSquared {
138    /// Returns the minimum value in the domain of the
139    /// chi-squared distribution representable by a double precision
140    /// float
141    ///
142    /// # Formula
143    ///
144    /// ```ignore
145    /// 0
146    /// ```
147    fn min(&self) -> f64 {
148        0.0
149    }
150}
151
152impl Max<f64> for ChiSquared {
153    /// Returns the maximum value in the domain of the
154    /// chi-squared distribution representable by a double precision
155    /// float
156    ///
157    /// # Formula
158    ///
159    /// ```ignore
160    /// INF
161    /// ```
162    fn max(&self) -> f64 {
163        f64::INFINITY
164    }
165}
166
167impl Distribution<f64> for ChiSquared {
168    /// Returns the mean of the chi-squared distribution
169    ///
170    /// # Formula
171    ///
172    /// ```ignore
173    /// k
174    /// ```
175    ///
176    /// where `k` is the degrees of freedom
177    fn mean(&self) -> Option<f64> {
178        self.g.mean()
179    }
180    /// Returns the variance of the chi-squared distribution
181    ///
182    /// # Formula
183    ///
184    /// ```ignore
185    /// 2k
186    /// ```
187    ///
188    /// where `k` is the degrees of freedom
189    fn variance(&self) -> Option<f64> {
190        self.g.variance()
191    }
192    /// Returns the entropy of the chi-squared distribution
193    ///
194    /// # Formula
195    ///
196    /// ```ignore
197    /// (k / 2) + ln(2 * Γ(k / 2)) + (1 - (k / 2)) * ψ(k / 2)
198    /// ```
199    ///
200    /// where `k` is the degrees of freedom, `Γ` is the gamma function,
201    /// and `ψ` is the digamma function
202    fn entropy(&self) -> Option<f64> {
203        self.g.entropy()
204    }
205    /// Returns the skewness of the chi-squared distribution
206    ///
207    /// # Formula
208    ///
209    /// ```ignore
210    /// sqrt(8 / k)
211    /// ```
212    ///
213    /// where `k` is the degrees of freedom
214    fn skewness(&self) -> Option<f64> {
215        self.g.skewness()
216    }
217}
218
219impl Median<f64> for ChiSquared {
220    /// Returns the median  of the chi-squared distribution
221    ///
222    /// # Formula
223    ///
224    /// ```ignore
225    /// k * (1 - (2 / 9k))^3
226    /// ```
227    fn median(&self) -> f64 {
228        if self.freedom < 1.0 {
229            // if k is small, calculate using expansion of formula
230            self.freedom - 2.0 / 3.0 + 12.0 / (81.0 * self.freedom)
231                - 8.0 / (729.0 * self.freedom * self.freedom)
232        } else {
233            // if k is large enough, median heads toward k - 2/3
234            self.freedom - 2.0 / 3.0
235        }
236    }
237}
238
239impl Mode<Option<f64>> for ChiSquared {
240    /// Returns the mode of the chi-squared distribution
241    ///
242    /// # Formula
243    ///
244    /// ```ignore
245    /// k - 2
246    /// ```
247    ///
248    /// where `k` is the degrees of freedom
249    fn mode(&self) -> Option<f64> {
250        self.g.mode()
251    }
252}
253
254impl Continuous<f64, f64> for ChiSquared {
255    /// Calculates the probability density function for the chi-squared
256    /// distribution at `x`
257    ///
258    /// # Formula
259    ///
260    /// ```ignore
261    /// 1 / (2^(k / 2) * Γ(k / 2)) * x^((k / 2) - 1) * e^(-x / 2)
262    /// ```
263    ///
264    /// where `k` is the degrees of freedom and `Γ` is the gamma function
265    fn pdf(&self, x: f64) -> f64 {
266        self.g.pdf(x)
267    }
268
269    /// Calculates the log probability density function for the chi-squared
270    /// distribution at `x`
271    ///
272    /// # Formula
273    ///
274    /// ```ignore
275    /// ln(1 / (2^(k / 2) * Γ(k / 2)) * x^((k / 2) - 1) * e^(-x / 2))
276    /// ```
277    fn ln_pdf(&self, x: f64) -> f64 {
278        self.g.ln_pdf(x)
279    }
280}
281
282#[rustfmt::skip]
283#[cfg(all(test, feature = "nightly"))]
284mod tests {
285    use crate::statistics::Median;
286    use crate::distribution::ChiSquared;
287    use crate::distribution::internal::*;
288    use crate::consts::ACC;
289
290    fn try_create(freedom: f64) -> ChiSquared {
291        let n = ChiSquared::new(freedom);
292        assert!(n.is_ok());
293        n.unwrap()
294    }
295
296    fn test_case<F>(freedom: f64, expected: f64, eval: F)
297        where F: Fn(ChiSquared) -> f64
298    {
299        let n = try_create(freedom);
300        let x = eval(n);
301        assert_eq!(expected, x);
302    }
303
304    fn test_almost<F>(freedom: f64, expected: f64, acc: f64, eval: F)
305        where F: Fn(ChiSquared) -> f64
306    {
307        let n = try_create(freedom);
308        let x = eval(n);
309        assert_almost_eq!(expected, x, acc);
310    }
311
312    #[test]
313    fn test_median() {
314        let median = |x: ChiSquared| x.median();
315        test_almost(0.5, 0.0857338820301783264746, 1e-16, median);
316        test_case(1.0, 1.0 - 2.0 / 3.0, median);
317        test_case(2.0, 2.0 - 2.0 / 3.0, median);
318        test_case(2.5, 2.5 - 2.0 / 3.0, median);
319        test_case(3.0, 3.0 - 2.0 / 3.0, median);
320    }
321
322    #[test]
323    fn test_continuous() {
324        // TODO: figure out why this test fails:
325        //test::check_continuous_distribution(&try_create(1.0), 0.0, 10.0);
326        test::check_continuous_distribution(&try_create(2.0), 0.0, 10.0);
327        test::check_continuous_distribution(&try_create(5.0), 0.0, 50.0);
328    }
329}