Skip to main content

prospicio_prob/
sampled.rs

1//! The sampled representation: a distribution known only through draws.
2
3use prospicio_core::{Error, Result};
4
5use crate::distortion::Distortion;
6use crate::distribution::Distribution;
7use crate::risk::{tvar_sorted, var_sorted};
8
9/// Operations that are exact on draws: any statistic of the empirical
10/// distribution.
11///
12/// Every result is exact for the draws and an estimate of the distribution
13/// they came from. Operations that are exact only on a parametric or
14/// discretized distribution (limited expected value, layers) are not
15/// offered here; a caller who wants an estimate from draws asks for it
16/// with [`Empirical::mean_of`].
17pub trait Empirical: Distribution {
18    /// The draws, in the order they were simulated. Draw `i` came from
19    /// simulation `i`, so two distributions from the same simulations can
20    /// be paired draw by draw.
21    fn draws(&self) -> &[f64];
22
23    /// The draws sorted ascending.
24    fn sorted(&self) -> &[f64];
25
26    /// Mean of `f(x)` over the draws.
27    ///
28    /// ```
29    /// use prospicio_prob::{Empirical, Sampled};
30    ///
31    /// let s = Sampled::new(vec![50.0, 150.0, 400.0]).unwrap();
32    /// // Estimated limited expected value at 100.
33    /// assert_eq!(s.mean_of(|x| x.min(100.0)), 250.0 / 3.0);
34    /// ```
35    fn mean_of(&self, f: impl Fn(f64) -> f64) -> f64 {
36        let draws = self.draws();
37        draws.iter().map(|&x| f(x)).sum::<f64>() / draws.len() as f64
38    }
39
40    /// Value at risk at level `p`; see [`var_sorted`].
41    fn var(&self, p: f64) -> Result<f64> {
42        var_sorted(self.sorted(), p)
43    }
44
45    /// Tail value at risk at level `p`; see [`tvar_sorted`].
46    fn tvar(&self, p: f64) -> Result<f64> {
47        tvar_sorted(self.sorted(), p)
48    }
49
50    /// Distortion risk measure of the draws; see
51    /// [`Distortion::apply_sorted`].
52    ///
53    /// ```
54    /// use prospicio_prob::{Distortion, Empirical, Sampled};
55    ///
56    /// let s = Sampled::new(vec![3.0, 1.0, 4.0, 2.0]).unwrap();
57    /// let tvar = Distortion::tvar(0.5).unwrap();
58    /// assert_eq!(s.distortion(&tvar), s.tvar(0.5).unwrap());
59    /// ```
60    fn distortion(&self, d: &Distortion) -> f64 {
61        d.apply_sorted(self.sorted())
62    }
63}
64
65/// A distribution represented by `n` equally weighted draws.
66///
67/// Every [`Distribution`] method describes the empirical distribution of
68/// the draws: `variance` divides by `n`, not `n - 1`, and `quantile` is
69/// the inverse of the empirical distribution function (R `type = 1`).
70/// `sample` resamples the draws with replacement.
71///
72/// # Example
73///
74/// ```
75/// use prospicio_core::StreamRng;
76/// use prospicio_prob::{Distribution, Empirical, Lognormal, Sampled};
77///
78/// let d = Lognormal::from_mean_cv(1000.0, 0.5).unwrap();
79/// let s = Sampled::new(d.sample(&mut StreamRng::new(42, 0), 10_000)).unwrap();
80/// assert!((s.mean() - 1000.0).abs() < 20.0);
81/// assert!(s.tvar(0.99).unwrap() > s.var(0.99).unwrap());
82/// ```
83#[derive(Debug, Clone, PartialEq)]
84pub struct Sampled {
85    draws: Vec<f64>,
86    sorted: Vec<f64>,
87    mean: f64,
88    variance: f64,
89}
90
91impl Sampled {
92    /// The empirical distribution of `draws`.
93    ///
94    /// Fails if `draws` is empty or holds a value that is not finite.
95    pub fn new(draws: Vec<f64>) -> Result<Self> {
96        if draws.is_empty() {
97            return Err(Error::InvalidParameter {
98                name: "draws",
99                value: 0.0,
100                reason: "must not be empty",
101            });
102        }
103        if let Some(&bad) = draws.iter().find(|x| !x.is_finite()) {
104            return Err(Error::InvalidParameter {
105                name: "draws",
106                value: bad,
107                reason: "must all be finite",
108            });
109        }
110        let mut sorted = draws.clone();
111        sorted.sort_by(f64::total_cmp);
112        let n = draws.len() as f64;
113        let mean = draws.iter().sum::<f64>() / n;
114        // Two passes: the centred sum loses no precision to a large mean.
115        let variance = draws.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n;
116        Ok(Self {
117            draws,
118            sorted,
119            mean,
120            variance,
121        })
122    }
123
124    /// Number of draws.
125    pub fn len(&self) -> usize {
126        self.draws.len()
127    }
128
129    /// Always `false`: a `Sampled` holds at least one draw.
130    pub fn is_empty(&self) -> bool {
131        false
132    }
133
134    /// The draws, giving up ownership.
135    pub fn into_draws(self) -> Vec<f64> {
136        self.draws
137    }
138}
139
140impl Distribution for Sampled {
141    fn mean(&self) -> f64 {
142        self.mean
143    }
144
145    fn variance(&self) -> f64 {
146        self.variance
147    }
148
149    fn cdf(&self, x: f64) -> f64 {
150        self.sorted.partition_point(|&d| d <= x) as f64 / self.sorted.len() as f64
151    }
152
153    fn quantile(&self, p: f64) -> Result<f64> {
154        var_sorted(&self.sorted, p)
155    }
156}
157
158impl Empirical for Sampled {
159    fn draws(&self) -> &[f64] {
160        &self.draws
161    }
162
163    fn sorted(&self) -> &[f64] {
164        &self.sorted
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use prospicio_core::StreamRng;
172
173    fn sampled(x: &[f64]) -> Sampled {
174        Sampled::new(x.to_vec()).unwrap()
175    }
176
177    #[test]
178    fn rejects_empty_and_non_finite() {
179        assert!(Sampled::new(vec![]).is_err());
180        assert!(Sampled::new(vec![1.0, f64::NAN]).is_err());
181        assert!(Sampled::new(vec![f64::INFINITY]).is_err());
182    }
183
184    #[test]
185    fn keeps_simulation_order() {
186        let s = sampled(&[3.0, 1.0, 2.0]);
187        assert_eq!(s.draws(), [3.0, 1.0, 2.0]);
188        assert_eq!(s.sorted(), [1.0, 2.0, 3.0]);
189        assert_eq!(s.len(), 3);
190    }
191
192    #[test]
193    fn moments_are_those_of_the_draws() {
194        let s = sampled(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
195        assert_eq!(s.mean(), 5.0);
196        assert_eq!(s.variance(), 4.0);
197        assert_eq!(s.std_dev(), 2.0);
198    }
199
200    #[test]
201    fn variance_survives_a_large_mean() {
202        let s = sampled(&[1e9 + 1.0, 1e9 + 2.0, 1e9 + 3.0]);
203        assert!((s.variance() - 2.0 / 3.0).abs() < 1e-6);
204    }
205
206    #[test]
207    fn cdf_and_quantile_are_inverse() {
208        let s = sampled(&[40.0, 10.0, 30.0, 20.0]);
209        assert_eq!(s.cdf(5.0), 0.0);
210        assert_eq!(s.cdf(10.0), 0.25);
211        assert_eq!(s.cdf(25.0), 0.5);
212        assert_eq!(s.cdf(40.0), 1.0);
213        for x in [10.0, 20.0, 30.0, 40.0] {
214            assert_eq!(s.quantile(s.cdf(x)), Ok(x));
215        }
216        assert!(s.quantile(1.5).is_err());
217    }
218
219    #[test]
220    fn sample_resamples_the_draws() {
221        let s = sampled(&[1.0, 2.0, 3.0]);
222        let r = s.sample(&mut StreamRng::new(9, 0), 1000);
223        assert!(r.iter().all(|x| [1.0, 2.0, 3.0].contains(x)));
224        assert_eq!(r, s.sample(&mut StreamRng::new(9, 0), 1000));
225    }
226
227    #[test]
228    fn lognormal_tail_measures_converge() {
229        use crate::Lognormal;
230        use prospicio_math::special::norm_cdf;
231
232        let d = Lognormal::new(0.0, 0.5).unwrap();
233        let s = Sampled::new(d.sample(&mut StreamRng::new(1, 0), 400_000)).unwrap();
234        let p = 0.99;
235        let q = d.quantile(p).unwrap();
236        // Lognormal TVaR: E[X] * Phi(sdlog - z_p) / (1 - p).
237        let z = (q.ln() - d.meanlog()) / d.sdlog();
238        let tvar = d.mean() * norm_cdf(d.sdlog() - z) / (1.0 - p);
239        let var_err = (s.var(p).unwrap() - q).abs() / q;
240        let tvar_err = (s.tvar(p).unwrap() - tvar).abs() / tvar;
241        assert!(var_err < 0.01, "VaR relative error {var_err}");
242        assert!(tvar_err < 0.01, "TVaR relative error {tvar_err}");
243    }
244}