Skip to main content

stats_claw/distributions/symmetric/
laplace.rs

1//! Laplace (double-exponential) distribution numerics, for the
2//! [`LaplaceDistribution`] parameter struct.
3//!
4//! Equivalent to `scipy.stats.laplace(loc=location, scale=scale)`: a symmetric
5//! exponential decay about `location` with rate `1/scale`. The CDF and quantile
6//! are piecewise closed forms split at the median, and sampling uses the
7//! inverse-CDF transform of a single uniform draw.
8//!
9//! # Examples
10//!
11//! ```
12//! use stats_claw::distributions::{Pdf, Quantile};
13//! use stats_claw::distributions::LaplaceDistribution;
14//!
15//! let d = LaplaceDistribution { location: 0.0, scale: 1.0, ..Default::default() };
16//! // Peak density of Laplace(0, 1) is 1/(2b) = 0.5.
17//! assert!((d.pdf(0.0) - 0.5).abs() < 1e-12, "peak was {}", d.pdf(0.0));
18//! // The median equals the location.
19//! assert!((d.quantile(0.5) - 0.0).abs() < 1e-12);
20//! ```
21
22use super::super::{Cdf, Moments, Pdf, Quantile, Sample};
23use crate::distributions::LaplaceDistribution;
24use crate::rng::SplitMix64;
25
26impl Pdf for LaplaceDistribution {
27    fn pdf(&self, x: f64) -> f64 {
28        (-(x - self.location).abs() / self.scale).exp() / (2.0 * self.scale)
29    }
30}
31
32impl Cdf for LaplaceDistribution {
33    fn cdf(&self, x: f64) -> f64 {
34        let z = (x - self.location) / self.scale;
35        if z < 0.0 {
36            0.5 * z.exp()
37        } else {
38            (-z).exp().mul_add(-0.5, 1.0)
39        }
40    }
41}
42
43impl Quantile for LaplaceDistribution {
44    fn quantile(&self, p: f64) -> f64 {
45        if p <= 0.5 {
46            self.scale.mul_add((2.0 * p).ln(), self.location)
47        } else {
48            let upper_tail = 2.0f64.mul_add(-p, 2.0);
49            self.scale.mul_add(-upper_tail.ln(), self.location)
50        }
51    }
52}
53
54impl Moments for LaplaceDistribution {
55    fn mean(&self) -> Option<f64> {
56        Some(self.location)
57    }
58    fn variance(&self) -> Option<f64> {
59        Some(2.0 * self.scale * self.scale)
60    }
61}
62
63impl Sample for LaplaceDistribution {
64    fn sample(&self, rng: &mut SplitMix64) -> f64 {
65        self.quantile(rng.next_f64())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    /// The density peaks at `1/(2b)` at the location.
74    #[test]
75    fn peak_density_at_location() {
76        let d = LaplaceDistribution {
77            location: 0.0,
78            scale: 1.0,
79            ..Default::default()
80        };
81        assert!((d.pdf(0.0) - 0.5).abs() < 1e-12, "peak was {}", d.pdf(0.0));
82    }
83
84    /// The median equals the location.
85    #[test]
86    fn median_is_location() {
87        let d = LaplaceDistribution {
88            location: 3.0,
89            scale: 2.0,
90            ..Default::default()
91        };
92        assert!((d.quantile(0.5) - 3.0).abs() < 1e-12);
93    }
94}