Skip to main content

shap_rs/explainers/
sampling.rs

1use super::PermutationExplainer;
2use crate::{
3    Background, EvaluationConfig, Explainer, Explanation, IndependentMasker, Link, Masker, Predict,
4    Result, ShapError, UncertainExplanation,
5};
6use ndarray::ArrayView2;
7/// Monte-Carlo Sampling SHAP. Samples random feature orderings and averages
8/// marginal contributions; the execution engine is shared with permutation
9/// SHAP because those are the defining samples of the Shapley distribution.
10pub struct SamplingExplainer<M, K = IndependentMasker> {
11    model: M,
12    masker: K,
13    nsamples: usize,
14    seed: u64,
15    antithetic: bool,
16    link: Link,
17    evaluation: EvaluationConfig,
18}
19impl<M> SamplingExplainer<M, IndependentMasker> {
20    pub fn new(model: M, background: Background) -> Self {
21        Self::from_masker(model, IndependentMasker::new(background))
22    }
23}
24impl<M, K> SamplingExplainer<M, K> {
25    pub fn from_masker(model: M, masker: K) -> Self {
26        Self {
27            model,
28            masker,
29            nsamples: 256,
30            seed: 0,
31            antithetic: true,
32            link: Link::Identity,
33            evaluation: EvaluationConfig {
34                coalition_batch_size: 64,
35                cache_capacity: 65536,
36                max_model_rows: None,
37            },
38        }
39    }
40    pub fn with_nsamples(mut self, n: usize) -> Self {
41        self.nsamples = n;
42        self
43    }
44    pub fn with_seed(mut self, s: u64) -> Self {
45        self.seed = s;
46        self
47    }
48    pub fn with_antithetic(mut self, enabled: bool) -> Self {
49        self.antithetic = enabled;
50        self
51    }
52    pub fn with_link(mut self, link: Link) -> Self {
53        self.link = link;
54        self
55    }
56    pub fn with_evaluation_config(mut self, c: EvaluationConfig) -> Self {
57        self.evaluation = c;
58        self
59    }
60}
61impl<M: Predict, K: Masker> SamplingExplainer<M, K> {
62    pub fn explain_with_uncertainty(
63        &self,
64        x: ArrayView2<'_, f64>,
65        repeats: usize,
66    ) -> Result<UncertainExplanation> {
67        if repeats < 2 {
68            return Err(ShapError::InvalidConfiguration(
69                "uncertainty estimation requires at least two repeats".into(),
70            ));
71        }
72        crate::error::checked_f64_shape(
73            &[repeats, x.nrows(), x.ncols()],
74            "sampling uncertainty runs",
75        )?;
76        let mut runs = Vec::with_capacity(repeats);
77        for r in 0..repeats {
78            runs.push(
79                PermutationExplainer::from_masker(&self.model, &self.masker)
80                    .with_n_permutations(self.nsamples)
81                    .with_seed(self.seed.wrapping_add(r as u64))
82                    .with_antithetic(self.antithetic)
83                    .with_link(self.link)
84                    .with_evaluation_config(self.evaluation)
85                    .explain(x)?,
86            )
87        }
88        let shape = runs[0].values().dim();
89        let mut mean = ndarray::Array3::zeros(shape);
90        for run in &runs {
91            ndarray::Zip::from(&mut mean)
92                .and(run.values())
93                .for_each(|m, &x| *m += x)
94        }
95        mean.mapv_inplace(|v| v / repeats as f64);
96        let mut variance = ndarray::Array3::<f64>::zeros(shape);
97        for run in &runs {
98            ndarray::Zip::from(&mut variance)
99                .and(run.values())
100                .and(&mean)
101                .for_each(|v, &x, &m| *v += (x - m) * (x - m));
102        }
103        let standard_errors = variance.mapv(|v| (v / ((repeats - 1) * repeats) as f64).sqrt());
104        let explanation = Explanation::new(mean, runs[0].base_values().to_owned(), x.to_owned())?;
105        UncertainExplanation::new(explanation, standard_errors, repeats)
106    }
107}
108impl<M: Predict, K: Masker> Explainer for SamplingExplainer<M, K> {
109    fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
110        PermutationExplainer::from_masker(&self.model, &self.masker)
111            .with_n_permutations(self.nsamples)
112            .with_seed(self.seed)
113            .with_antithetic(self.antithetic)
114            .with_link(self.link)
115            .with_evaluation_config(self.evaluation)
116            .explain(x)
117    }
118}
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::{FixedMasker, FnModel};
123    use ndarray::{array, Axis};
124    #[test]
125    fn reports_zero_error_for_order_independent_model() {
126        let model =
127            FnModel::new(|x: ArrayView2<'_, f64>| Ok(x.sum_axis(Axis(1)).insert_axis(Axis(1))));
128        let e = SamplingExplainer::from_masker(model, FixedMasker::new(array![0., 0.]).unwrap())
129            .with_nsamples(8)
130            .explain_with_uncertainty(array![[2., 3.]].view(), 4)
131            .unwrap();
132        assert!(e.standard_errors().iter().all(|x| *x < 1e-12));
133        assert!((e.explanation().reconstructed()[[0, 0]] - 5.).abs() < 1e-12);
134    }
135}