Skip to main content

prospicio_models/
simulate.rs

1//! Predictive distributions from fitted means, for engines that give a
2//! mean per row and nothing else (gradient boosting through its Python or
3//! R package, an imported network): the family adds the process noise, and
4//! several mean vectors (bootstrap refits) add the parameter uncertainty.
5
6use prospicio_core::{Error, Result};
7use prospicio_prob::{ComponentKey, KeyValue, PredictiveDistribution, Provenance};
8
9use crate::family::Family;
10
11/// Joint draws of the responses of `n` rows, components keyed
12/// `row = 0, 1, …`, as `prospicio_glm` keys its fits.
13///
14/// `means` holds one or more mean vectors of length `n` (one per bootstrap
15/// refit, say). Simulation `i` takes stream `i` of `seed`, picks one vector
16/// uniformly from it, then draws each row's response from `family` with
17/// that mean, the row's dispersion and the row's weight. With one vector
18/// the draws carry process noise only. `dispersion` holds one value for
19/// every row, or one per row (from a dispersion model); `weights` empty
20/// means every weight is 1.
21///
22/// ```
23/// use prospicio_models::Family;
24/// use prospicio_models::simulate::from_means;
25/// use prospicio_prob::{Distribution, Provenance};
26///
27/// // Two policies with expected claim counts 0.1 and 0.4.
28/// let pd = from_means(Family::Poisson, &[vec![0.1, 0.4]], &[1.0], &[], 20_000, 7,
29///                     Provenance::new("example")).unwrap();
30/// assert!((pd.total().mean() - 0.5).abs() < 0.02);
31/// ```
32pub fn from_means(
33    family: Family,
34    means: &[Vec<f64>],
35    dispersion: &[f64],
36    weights: &[f64],
37    n_sims: usize,
38    seed: u64,
39    provenance: Provenance,
40) -> Result<PredictiveDistribution> {
41    family.validate()?;
42    let Some(n) = means.first().map(Vec::len) else {
43        return Err(Error::Data("needs at least one mean vector".into()));
44    };
45    if n == 0 || means.iter().any(|m| m.len() != n) {
46        return Err(Error::Data(
47            "every mean vector needs the same, non-zero number of rows".into(),
48        ));
49    }
50    if let Some(&bad) = means.iter().flatten().find(|&&m| !family.valid_mu(m)) {
51        return Err(Error::InvalidParameter {
52            name: "means",
53            value: bad,
54            reason: "outside the family's range",
55        });
56    }
57    if dispersion.len() != 1 && dispersion.len() != n {
58        return Err(Error::Data(format!(
59            "dispersion needs 1 or {n} values, got {}",
60            dispersion.len()
61        )));
62    }
63    if let Some(&bad) = dispersion.iter().find(|&&d| !(d.is_finite() && d > 0.0)) {
64        return Err(Error::InvalidParameter {
65            name: "dispersion",
66            value: bad,
67            reason: "must be positive and finite",
68        });
69    }
70    let phi = |i: usize| dispersion[if dispersion.len() == 1 { 0 } else { i }];
71    let ones = vec![1.0; n];
72    let weights = if weights.is_empty() {
73        &ones[..]
74    } else {
75        weights
76    };
77    if weights.len() != n || weights.iter().any(|w| !(w.is_finite() && *w > 0.0)) {
78        return Err(Error::Data(format!(
79            "weights need {n} positive finite values"
80        )));
81    }
82    let k = means.len();
83    let components: Vec<ComponentKey> = (0..n).map(|i| vec![KeyValue::from(i as i64)]).collect();
84    let provenance = provenance
85        .param("family", family.name())
86        .param(
87            "dispersion",
88            if dispersion.len() == 1 {
89                dispersion[0].to_string()
90            } else {
91                "per row".to_string()
92            },
93        )
94        .param("mean_vectors", k);
95    // Every mean is in the family's range, so a draw only fails for an
96    // unusual (weight, dispersion) pair; it becomes NaN there, as prospicio_glm's
97    // draws do, and from_draws reports it.
98    PredictiveDistribution::simulate(
99        vec!["row".into()],
100        components,
101        n_sims,
102        seed,
103        provenance,
104        |rng, row| {
105            let mu = &means[((rng.next_open01() * k as f64) as usize).min(k - 1)];
106            for (i, (out, (&m, &w))) in row.iter_mut().zip(mu.iter().zip(weights)).enumerate() {
107                *out = family
108                    .draw(m, phi(i), w, rng.next_open01())
109                    .unwrap_or(f64::NAN);
110            }
111        },
112    )
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use prospicio_prob::Distribution;
119
120    #[test]
121    fn draws_centre_on_the_means_with_the_family_variance() {
122        let pd = from_means(
123            Family::Gamma,
124            &[vec![100.0, 300.0]],
125            &[0.5],
126            &[1.0, 2.0],
127            40_000,
128            3,
129            Provenance::new("test"),
130        )
131        .unwrap();
132        // Gamma: variance φ μ² / w.
133        for (j, (mu, w)) in [(100.0, 1.0), (300.0, 2.0)].into_iter().enumerate() {
134            let m = pd.marginal(&vec![KeyValue::from(j as i64)]).unwrap();
135            assert!((m.mean() / mu - 1.0).abs() < 0.02, "{j}");
136            assert!(
137                (m.variance() / (0.5 * mu * mu / w) - 1.0).abs() < 0.05,
138                "{j}"
139            );
140        }
141    }
142
143    #[test]
144    fn several_mean_vectors_add_their_spread() {
145        // Two refits that disagree: the mixture's variance is the process
146        // variance plus the spread of the means.
147        let means = [vec![2.0], vec![6.0]];
148        let pd = from_means(
149            Family::Gaussian,
150            &means,
151            &[1.0],
152            &[],
153            40_000,
154            5,
155            Provenance::new("test"),
156        )
157        .unwrap();
158        let total = pd.total();
159        assert!((total.mean() - 4.0).abs() < 0.05);
160        assert!((total.variance() - (1.0 + 4.0)).abs() < 0.15);
161        assert_eq!(pd.provenance().seed, Some(5));
162    }
163
164    #[test]
165    fn per_row_dispersion() {
166        // Gamma variance φ μ²: the second row's φ is four times the first.
167        let pd = from_means(
168            Family::Gamma,
169            &[vec![100.0, 100.0]],
170            &[0.1, 0.4],
171            &[],
172            40_000,
173            9,
174            Provenance::new("test"),
175        )
176        .unwrap();
177        let v = |j: i64| pd.marginal(&vec![KeyValue::from(j)]).unwrap().variance();
178        assert!((v(0) / 1000.0 - 1.0).abs() < 0.05);
179        assert!((v(1) / 4000.0 - 1.0).abs() < 0.05);
180        let p = || Provenance::new("test");
181        assert!(
182            from_means(
183                Family::Gamma,
184                &[vec![1.0, 2.0]],
185                &[1.0, 1.0, 1.0],
186                &[],
187                10,
188                1,
189                p()
190            )
191            .is_err()
192        );
193        assert!(
194            from_means(
195                Family::Gamma,
196                &[vec![1.0, 2.0]],
197                &[1.0, -1.0],
198                &[],
199                10,
200                1,
201                p()
202            )
203            .is_err()
204        );
205    }
206
207    #[test]
208    fn rejects_bad_input() {
209        let p = || Provenance::new("test");
210        assert!(from_means(Family::Poisson, &[], &[1.0], &[], 10, 1, p()).is_err());
211        assert!(
212            from_means(
213                Family::Poisson,
214                &[vec![1.0], vec![]],
215                &[1.0],
216                &[],
217                10,
218                1,
219                p()
220            )
221            .is_err()
222        );
223        assert!(from_means(Family::Poisson, &[vec![-1.0]], &[1.0], &[], 10, 1, p()).is_err());
224        assert!(from_means(Family::Poisson, &[vec![1.0]], &[0.0], &[], 10, 1, p()).is_err());
225        assert!(
226            from_means(
227                Family::Poisson,
228                &[vec![1.0]],
229                &[1.0],
230                &[1.0, 2.0],
231                10,
232                1,
233                p()
234            )
235            .is_err()
236        );
237    }
238}