Skip to main content

fdars_core/explain/
sensitivity.rs

1//! Sobol sensitivity indices, functional saliency, and domain selection.
2
3use super::helpers::*;
4use crate::matrix::FdMatrix;
5use crate::scalar_on_function::{sigmoid, FregreLmResult, FunctionalLogisticResult};
6use rand::prelude::*;
7
8// ===========================================================================
9// Sobol Sensitivity Indices
10// ===========================================================================
11
12/// Sobol first-order and total-order sensitivity indices.
13pub struct SobolIndicesResult {
14    /// First-order indices S_k, length ncomp.
15    pub first_order: Vec<f64>,
16    /// Total-order indices ST_k, length ncomp.
17    pub total_order: Vec<f64>,
18    /// Total variance of Y.
19    pub var_y: f64,
20    /// Per-component variance contribution, length ncomp.
21    pub component_variance: Vec<f64>,
22}
23
24/// Exact Sobol sensitivity indices for a linear functional regression model.
25///
26/// For an additive model with orthogonal FPC predictors, first-order = total-order.
27pub fn sobol_indices(
28    fit: &FregreLmResult,
29    data: &FdMatrix,
30    y: &[f64],
31    scalar_covariates: Option<&FdMatrix>,
32) -> Option<SobolIndicesResult> {
33    let (n, m) = data.shape();
34    if n < 2 || n != y.len() || m != fit.fpca.mean.len() {
35        return None;
36    }
37    let _ = scalar_covariates; // not needed for variance decomposition
38    let ncomp = fit.ncomp;
39    if ncomp == 0 {
40        return None;
41    }
42
43    let score_var = compute_score_variance(&fit.fpca.scores, n, ncomp);
44
45    let component_variance: Vec<f64> = (0..ncomp)
46        .map(|k| fit.coefficients[1 + k].powi(2) * score_var[k])
47        .collect();
48
49    let y_mean: f64 = y.iter().sum::<f64>() / n as f64;
50    let var_y: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum::<f64>() / (n - 1) as f64;
51    if var_y == 0.0 {
52        return None;
53    }
54
55    let first_order: Vec<f64> = component_variance.iter().map(|&cv| cv / var_y).collect();
56    let total_order = first_order.clone(); // additive + orthogonal -> S_k = ST_k
57
58    Some(SobolIndicesResult {
59        first_order,
60        total_order,
61        var_y,
62        component_variance,
63    })
64}
65
66/// Sobol sensitivity indices for a functional logistic regression model (Saltelli MC).
67pub fn sobol_indices_logistic(
68    fit: &FunctionalLogisticResult,
69    data: &FdMatrix,
70    scalar_covariates: Option<&FdMatrix>,
71    n_samples: usize,
72    seed: u64,
73) -> Option<SobolIndicesResult> {
74    let (n, m) = data.shape();
75    if n < 2 || m != fit.fpca.mean.len() || n_samples == 0 {
76        return None;
77    }
78    let ncomp = fit.ncomp;
79    if ncomp == 0 {
80        return None;
81    }
82    let p_scalar = fit.gamma.len();
83    let scores = project_scores(data, &fit.fpca.mean, &fit.fpca.rotation, ncomp);
84    let mean_z = compute_mean_scalar(scalar_covariates, p_scalar, n);
85
86    let eval_model = |s: &[f64]| -> f64 {
87        let mut eta = fit.intercept;
88        for k in 0..ncomp {
89            eta += fit.coefficients[1 + k] * s[k];
90        }
91        for j in 0..p_scalar {
92            eta += fit.gamma[j] * mean_z[j];
93        }
94        sigmoid(eta)
95    };
96
97    let mut rng = StdRng::seed_from_u64(seed);
98    let (mat_a, mat_b) = generate_sobol_matrices(&scores, n, ncomp, n_samples, &mut rng);
99
100    let f_a: Vec<f64> = mat_a.iter().map(|s| eval_model(s)).collect();
101    let f_b: Vec<f64> = mat_b.iter().map(|s| eval_model(s)).collect();
102
103    let mean_fa = f_a.iter().sum::<f64>() / n_samples as f64;
104    let var_fa = f_a.iter().map(|&v| (v - mean_fa).powi(2)).sum::<f64>() / n_samples as f64;
105
106    if var_fa < 1e-15 {
107        return None;
108    }
109
110    let mut first_order = vec![0.0; ncomp];
111    let mut total_order = vec![0.0; ncomp];
112    let mut component_variance = vec![0.0; ncomp];
113
114    for k in 0..ncomp {
115        let (s_k, st_k) = compute_sobol_component(
116            &mat_a,
117            &mat_b,
118            &f_a,
119            &f_b,
120            var_fa,
121            k,
122            n_samples,
123            &eval_model,
124        );
125        first_order[k] = s_k;
126        total_order[k] = st_k;
127        component_variance[k] = s_k * var_fa;
128    }
129
130    Some(SobolIndicesResult {
131        first_order,
132        total_order,
133        var_y: var_fa,
134        component_variance,
135    })
136}
137
138// ===========================================================================
139// Functional Saliency Maps
140// ===========================================================================
141
142/// Functional saliency map result.
143pub struct FunctionalSaliencyResult {
144    /// Saliency map (n x m).
145    pub saliency_map: FdMatrix,
146    /// Mean absolute saliency at each grid point (length m).
147    pub mean_absolute_saliency: Vec<f64>,
148}
149
150/// Functional saliency maps for a linear functional regression model.
151///
152/// Lifts FPC-level SHAP attributions to the function domain via the rotation matrix.
153pub fn functional_saliency(
154    fit: &FregreLmResult,
155    data: &FdMatrix,
156    scalar_covariates: Option<&FdMatrix>,
157) -> Option<FunctionalSaliencyResult> {
158    let (n, m) = data.shape();
159    if n == 0 || m != fit.fpca.mean.len() {
160        return None;
161    }
162    let _ = scalar_covariates;
163    let ncomp = fit.ncomp;
164    if ncomp == 0 {
165        return None;
166    }
167    let scores = project_scores(data, &fit.fpca.mean, &fit.fpca.rotation, ncomp);
168    let mean_scores = compute_column_means(&scores, ncomp);
169
170    let weights: Vec<f64> = (0..ncomp).map(|k| fit.coefficients[1 + k]).collect();
171    let saliency_map = compute_saliency_map(
172        &scores,
173        &mean_scores,
174        &weights,
175        &fit.fpca.rotation,
176        n,
177        m,
178        ncomp,
179    );
180    let mean_absolute_saliency = mean_absolute_column(&saliency_map, n, m);
181
182    Some(FunctionalSaliencyResult {
183        saliency_map,
184        mean_absolute_saliency,
185    })
186}
187
188/// Functional saliency maps for a functional logistic regression model (gradient-based).
189pub fn functional_saliency_logistic(
190    fit: &FunctionalLogisticResult,
191) -> Option<FunctionalSaliencyResult> {
192    let m = fit.beta_t.len();
193    let n = fit.probabilities.len();
194    if n == 0 || m == 0 {
195        return None;
196    }
197
198    // saliency[(i,j)] = p_i * (1 - p_i) * beta_t[j]
199    let mut saliency_map = FdMatrix::zeros(n, m);
200    for i in 0..n {
201        let pi = fit.probabilities[i];
202        let w = pi * (1.0 - pi);
203        for j in 0..m {
204            saliency_map[(i, j)] = w * fit.beta_t[j];
205        }
206    }
207
208    let mut mean_absolute_saliency = vec![0.0; m];
209    for j in 0..m {
210        for i in 0..n {
211            mean_absolute_saliency[j] += saliency_map[(i, j)].abs();
212        }
213        mean_absolute_saliency[j] /= n as f64;
214    }
215
216    Some(FunctionalSaliencyResult {
217        saliency_map,
218        mean_absolute_saliency,
219    })
220}
221
222// ===========================================================================
223// Domain Selection / Interval Importance
224// ===========================================================================
225
226/// An important interval in the function domain.
227pub struct ImportantInterval {
228    /// Start index (inclusive).
229    pub start_idx: usize,
230    /// End index (inclusive).
231    pub end_idx: usize,
232    /// Summed importance of the interval.
233    pub importance: f64,
234}
235
236/// Result of domain selection analysis.
237pub struct DomainSelectionResult {
238    /// Pointwise importance: |beta(t)|^2, length m.
239    pub pointwise_importance: Vec<f64>,
240    /// Important intervals sorted by importance descending.
241    pub intervals: Vec<ImportantInterval>,
242    /// Sliding window width used.
243    pub window_width: usize,
244    /// Threshold used.
245    pub threshold: f64,
246}
247
248/// Domain selection for a linear functional regression model.
249pub fn domain_selection(
250    fit: &FregreLmResult,
251    window_width: usize,
252    threshold: f64,
253) -> Option<DomainSelectionResult> {
254    compute_domain_selection(&fit.beta_t, window_width, threshold)
255}
256
257/// Domain selection for a functional logistic regression model.
258pub fn domain_selection_logistic(
259    fit: &FunctionalLogisticResult,
260    window_width: usize,
261    threshold: f64,
262) -> Option<DomainSelectionResult> {
263    compute_domain_selection(&fit.beta_t, window_width, threshold)
264}