Skip to main content

shap_rs/explainers/
auto.rs

1use super::{ExactExplainer, KernelExplainer};
2use crate::{
3    Background, EvaluationConfig, Explainer, Explanation, IndependentMasker, Link, Masker, Predict,
4    Result,
5};
6use ndarray::ArrayView2;
7
8/// Algorithm selected by [`AutoExplainer`] for the current feature count.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10pub enum AutoAlgorithm {
11    Exact,
12    Kernel,
13}
14
15/// Automatic model-agnostic explainer selection with one shared configuration.
16///
17/// Exact SHAP is selected when the masker feature count is at most
18/// `exact_threshold`; otherwise Kernel SHAP is used. Native tree and linear
19/// models should still use their specialized explainers directly.
20pub struct AutoExplainer<M, K = IndependentMasker> {
21    model: M,
22    masker: K,
23    exact_threshold: usize,
24    kernel_samples: usize,
25    seed: u64,
26    ridge: f64,
27    link: Link,
28    evaluation: EvaluationConfig,
29}
30
31impl<M> AutoExplainer<M, IndependentMasker> {
32    pub fn new(model: M, background: Background) -> Self {
33        Self::from_masker(model, IndependentMasker::new(background))
34    }
35}
36
37impl<M, K> AutoExplainer<M, K> {
38    pub fn from_masker(model: M, masker: K) -> Self {
39        Self {
40            model,
41            masker,
42            exact_threshold: 12,
43            kernel_samples: 512,
44            seed: 0,
45            ridge: 1e-10,
46            link: Link::Identity,
47            evaluation: EvaluationConfig::default(),
48        }
49    }
50    pub fn with_exact_threshold(mut self, features: usize) -> Self {
51        self.exact_threshold = features;
52        self
53    }
54    pub fn with_kernel_samples(mut self, samples: usize) -> Self {
55        self.kernel_samples = samples;
56        self
57    }
58    pub fn with_seed(mut self, seed: u64) -> Self {
59        self.seed = seed;
60        self
61    }
62    pub fn with_ridge(mut self, ridge: f64) -> Self {
63        self.ridge = ridge;
64        self
65    }
66    pub fn with_link(mut self, link: Link) -> Self {
67        self.link = link;
68        self
69    }
70    pub fn with_evaluation_config(mut self, evaluation: EvaluationConfig) -> Self {
71        self.evaluation = evaluation;
72        self
73    }
74}
75
76impl<M, K: Masker> AutoExplainer<M, K> {
77    pub fn selected_algorithm(&self) -> AutoAlgorithm {
78        if self.link == Link::Identity && self.masker.n_features() <= self.exact_threshold {
79            AutoAlgorithm::Exact
80        } else {
81            AutoAlgorithm::Kernel
82        }
83    }
84}
85
86impl<M: Predict, K: Masker> Explainer for AutoExplainer<M, K> {
87    fn explain(&self, x: ArrayView2<'_, f64>) -> Result<Explanation> {
88        match self.selected_algorithm() {
89            AutoAlgorithm::Exact => ExactExplainer::from_masker(&self.model, &self.masker)
90                .with_max_features(self.exact_threshold)
91                .with_link(self.link)
92                .with_evaluation_config(self.evaluation)
93                .explain(x),
94            AutoAlgorithm::Kernel => KernelExplainer::from_masker(&self.model, &self.masker)
95                .with_nsamples(self.kernel_samples)
96                .with_seed(self.seed)
97                .with_exact_threshold(self.exact_threshold)
98                .with_ridge(self.ridge)
99                .with_link(self.link)
100                .with_evaluation_config(self.evaluation)
101                .explain(x),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::{metrics::check_additivity, FixedMasker, FnModel};
110    use ndarray::{array, Axis};
111
112    #[test]
113    fn selects_exact_below_threshold() {
114        let model = FnModel::new(|x: ArrayView2<'_, f64>| {
115            Ok(x.map_axis(Axis(1), |row| row[0] * row[1] + row[2])
116                .insert_axis(Axis(1)))
117        });
118        let explainer =
119            AutoExplainer::from_masker(model, FixedMasker::new(array![0., 0., 0.]).unwrap());
120        assert_eq!(explainer.selected_algorithm(), AutoAlgorithm::Exact);
121        let explanation = explainer.explain(array![[2., 3., 4.]].view()).unwrap();
122        check_additivity(&explanation, array![[10.]].view(), 1e-12).unwrap();
123    }
124
125    #[test]
126    fn selects_reproducible_kernel_above_threshold() {
127        let model = FnModel::new(|x: ArrayView2<'_, f64>| {
128            Ok(x.map_axis(Axis(1), |row| row[0] * row[1] + row[2] * row[3])
129                .insert_axis(Axis(1)))
130        });
131        let explainer =
132            AutoExplainer::from_masker(model, FixedMasker::new(array![0., 0., 0., 0.]).unwrap())
133                .with_exact_threshold(2)
134                .with_kernel_samples(8)
135                .with_seed(7);
136        assert_eq!(explainer.selected_algorithm(), AutoAlgorithm::Kernel);
137        let sample = array![[1., 2., 3., 4.]];
138        let first = explainer.explain(sample.view()).unwrap();
139        let second = explainer.explain(sample.view()).unwrap();
140        assert_eq!(first, second);
141        check_additivity(&first, array![[14.]].view(), 1e-9).unwrap();
142    }
143}