use crate::error::FdarError;
use crate::explain::{
compute_column_means, compute_domain_selection, compute_saliency_map, mean_absolute_column,
DomainSelectionResult, FunctionalSaliencyResult,
};
use crate::matrix::FdMatrix;
use super::shap::generic_shap_values;
use super::FpcPredictor;
#[must_use = "expensive computation whose result should not be discarded"]
pub fn generic_saliency(
model: &dyn FpcPredictor,
data: &FdMatrix,
scalar_covariates: Option<&FdMatrix>,
n_samples: usize,
seed: u64,
) -> Result<FunctionalSaliencyResult, FdarError> {
let (n, m) = data.shape();
if n == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "n > 0".into(),
actual: "0 rows".into(),
});
}
if m != model.fpca_mean().len() {
return Err(FdarError::InvalidDimension {
parameter: "data columns",
expected: model.fpca_mean().len().to_string(),
actual: m.to_string(),
});
}
let ncomp = model.ncomp();
if ncomp == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: "model has 0 components".into(),
});
}
let shap = generic_shap_values(model, data, scalar_covariates, n_samples, seed)?;
let scores = model.project(data);
let mean_scores = compute_column_means(&scores, ncomp);
let mut weights = vec![0.0; ncomp];
for k in 0..ncomp {
let mut sum_shap = 0.0;
let mut sum_score_dev = 0.0;
for i in 0..n {
sum_shap += shap.values[(i, k)].abs();
sum_score_dev += (scores[(i, k)] - mean_scores[k]).abs();
}
weights[k] = if sum_score_dev > 1e-15 {
sum_shap / sum_score_dev
} else {
0.0
};
}
let saliency_map = compute_saliency_map(
&scores,
&mean_scores,
&weights,
model.fpca_rotation(),
n,
m,
ncomp,
);
let mean_absolute_saliency = mean_absolute_column(&saliency_map, n, m);
Ok(FunctionalSaliencyResult {
saliency_map,
mean_absolute_saliency,
})
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn generic_domain_selection(
model: &dyn FpcPredictor,
data: &FdMatrix,
scalar_covariates: Option<&FdMatrix>,
window_width: usize,
threshold: f64,
n_samples: usize,
seed: u64,
) -> Result<DomainSelectionResult, FdarError> {
let (n, m) = data.shape();
if n == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "n > 0".into(),
actual: "0 rows".into(),
});
}
if m != model.fpca_mean().len() {
return Err(FdarError::InvalidDimension {
parameter: "data columns",
expected: model.fpca_mean().len().to_string(),
actual: m.to_string(),
});
}
let ncomp = model.ncomp();
if ncomp == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: "model has 0 components".into(),
});
}
let shap = generic_shap_values(model, data, scalar_covariates, n_samples, seed)?;
let scores = model.project(data);
let mean_scores = compute_column_means(&scores, ncomp);
let mut effective_weights = vec![0.0; ncomp];
for k in 0..ncomp {
let mut sum_shap = 0.0;
let mut sum_score_dev = 0.0;
for i in 0..n {
sum_shap += shap.values[(i, k)].abs();
sum_score_dev += (scores[(i, k)] - mean_scores[k]).abs();
}
effective_weights[k] = if sum_score_dev > 1e-15 {
sum_shap / sum_score_dev
} else {
0.0
};
}
let rotation = model.fpca_rotation();
let mut beta_t = vec![0.0; m];
for j in 0..m {
for k in 0..ncomp {
beta_t[j] += effective_weights[k] * rotation[(j, k)];
}
}
compute_domain_selection(&beta_t, window_width, threshold).ok_or_else(|| {
FdarError::ComputationFailed {
operation: "generic_domain_selection",
detail: "domain selection failed; the effective beta curve may be near-zero — check that the model has predictive signal".into(),
}
})
}