Skip to main content

fdars_core/explain_generic/
lime.rs

1use crate::error::FdarError;
2use crate::explain::{compute_lime, LimeResult};
3use crate::matrix::FdMatrix;
4
5use super::FpcPredictor;
6
7/// Generic LIME explanation for any FPC-based model.
8///
9/// # Errors
10///
11/// Returns [`FdarError::InvalidParameter`] if `observation >= n`,
12/// `n_samples` is zero, `kernel_width <= 0`, or the model has zero
13/// components.
14/// Returns [`FdarError::InvalidDimension`] if `data` columns do not match
15/// the model.
16/// Returns [`FdarError::ComputationFailed`] if the internal LIME
17/// computation fails.
18#[must_use = "expensive computation whose result should not be discarded"]
19pub fn generic_lime(
20    model: &dyn FpcPredictor,
21    data: &FdMatrix,
22    _scalar_covariates: Option<&FdMatrix>,
23    observation: usize,
24    n_samples: usize,
25    kernel_width: f64,
26    seed: u64,
27) -> Result<LimeResult, FdarError> {
28    let (n, m) = data.shape();
29    if observation >= n {
30        return Err(FdarError::InvalidParameter {
31            parameter: "observation",
32            message: format!("observation {observation} >= n {n}"),
33        });
34    }
35    if m != model.fpca_mean().len() {
36        return Err(FdarError::InvalidDimension {
37            parameter: "data columns",
38            expected: model.fpca_mean().len().to_string(),
39            actual: m.to_string(),
40        });
41    }
42    if n_samples == 0 {
43        return Err(FdarError::InvalidParameter {
44            parameter: "n_samples",
45            message: "n_samples must be > 0".into(),
46        });
47    }
48    if kernel_width <= 0.0 {
49        return Err(FdarError::InvalidParameter {
50            parameter: "kernel_width",
51            message: format!("kernel_width must be > 0, got {kernel_width}"),
52        });
53    }
54    let ncomp = model.ncomp();
55    if ncomp == 0 {
56        return Err(FdarError::InvalidParameter {
57            parameter: "ncomp",
58            message: "model has 0 components".into(),
59        });
60    }
61    let scores = model.project(data);
62    let obs_scores: Vec<f64> = (0..ncomp).map(|k| scores[(observation, k)]).collect();
63
64    let mut score_sd = vec![0.0; ncomp];
65    for k in 0..ncomp {
66        let mut ss = 0.0;
67        for i in 0..n {
68            let s = scores[(i, k)];
69            ss += s * s;
70        }
71        score_sd[k] = (ss / (n - 1).max(1) as f64).sqrt().max(1e-10);
72    }
73
74    let predict = |s: &[f64]| -> f64 { model.predict_from_scores(s, None) };
75
76    compute_lime(
77        &obs_scores,
78        &score_sd,
79        ncomp,
80        n_samples,
81        kernel_width,
82        seed,
83        observation,
84        &predict,
85    )
86    .ok_or_else(|| FdarError::ComputationFailed {
87        operation: "generic_lime",
88        detail: "compute_lime returned None".into(),
89    })
90}