use super::helpers::{ice_to_pdp, logistic_eta_base, make_grid, project_scores};
use crate::error::FdarError;
use crate::matrix::FdMatrix;
use crate::scalar_on_function::{sigmoid, FregreLmResult, FunctionalLogisticResult};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FunctionalPdpResult {
pub grid_values: Vec<f64>,
pub pdp_curve: Vec<f64>,
pub ice_curves: FdMatrix,
pub component: usize,
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn functional_pdp(
fit: &FregreLmResult,
data: &FdMatrix,
_scalar_covariates: Option<&FdMatrix>,
component: usize,
n_grid: usize,
) -> Result<FunctionalPdpResult, FdarError> {
let (n, m) = data.shape();
if n == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: ">0 rows".into(),
actual: "0".into(),
});
}
if m != fit.fpca.mean.len() {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: format!("{} columns", fit.fpca.mean.len()),
actual: format!("{m}"),
});
}
if n != fit.fitted_values.len() {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: format!("{} rows matching fitted_values", fit.fitted_values.len()),
actual: format!("{n}"),
});
}
if component >= fit.ncomp {
return Err(FdarError::InvalidParameter {
parameter: "component",
message: format!("component {} >= ncomp {}", component, fit.ncomp),
});
}
if n_grid < 2 {
return Err(FdarError::InvalidParameter {
parameter: "n_grid",
message: "must be >= 2".into(),
});
}
let ncomp = fit.ncomp;
let scores = project_scores(
data,
&fit.fpca.mean,
&fit.fpca.rotation,
ncomp,
&fit.fpca.weights,
);
let grid_values = make_grid(&scores, component, n_grid);
let coef_c = fit.coefficients[1 + component];
let mut ice_curves = FdMatrix::zeros(n, n_grid);
for i in 0..n {
let base = fit.fitted_values[i] - coef_c * scores[(i, component)];
for g in 0..n_grid {
ice_curves[(i, g)] = base + coef_c * grid_values[g];
}
}
let pdp_curve = ice_to_pdp(&ice_curves, n, n_grid);
Ok(FunctionalPdpResult {
grid_values,
pdp_curve,
ice_curves,
component,
})
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn functional_pdp_logistic(
fit: &FunctionalLogisticResult,
data: &FdMatrix,
scalar_covariates: Option<&FdMatrix>,
component: usize,
n_grid: usize,
) -> Result<FunctionalPdpResult, FdarError> {
let (n, m) = data.shape();
if n == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: ">0 rows".into(),
actual: "0".into(),
});
}
if m != fit.fpca.mean.len() {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: format!("{} columns", fit.fpca.mean.len()),
actual: format!("{m}"),
});
}
if component >= fit.ncomp {
return Err(FdarError::InvalidParameter {
parameter: "component",
message: format!("component {} >= ncomp {}", component, fit.ncomp),
});
}
if n_grid < 2 {
return Err(FdarError::InvalidParameter {
parameter: "n_grid",
message: "must be >= 2".into(),
});
}
let ncomp = fit.ncomp;
let p_scalar = fit.gamma.len();
if p_scalar > 0 && scalar_covariates.is_none() {
return Err(FdarError::InvalidParameter {
parameter: "scalar_covariates",
message: "required when model has scalar covariates".into(),
});
}
let scores = project_scores(
data,
&fit.fpca.mean,
&fit.fpca.rotation,
ncomp,
&fit.fpca.weights,
);
let grid_values = make_grid(&scores, component, n_grid);
let mut ice_curves = FdMatrix::zeros(n, n_grid);
let coef_c = fit.coefficients[1 + component];
for i in 0..n {
let eta_base = logistic_eta_base(
fit.intercept,
&fit.coefficients,
&fit.gamma,
&scores,
scalar_covariates,
i,
ncomp,
component,
);
for g in 0..n_grid {
ice_curves[(i, g)] = sigmoid(eta_base + coef_c * grid_values[g]);
}
}
let pdp_curve = ice_to_pdp(&ice_curves, n, n_grid);
Ok(FunctionalPdpResult {
grid_values,
pdp_curve,
ice_curves,
component,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct BetaDecomposition {
pub components: Vec<Vec<f64>>,
pub coefficients: Vec<f64>,
pub variance_proportion: Vec<f64>,
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn beta_decomposition(fit: &FregreLmResult) -> Result<BetaDecomposition, FdarError> {
let ncomp = fit.ncomp;
let m = fit.fpca.mean.len();
if ncomp == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: "must be > 0".into(),
});
}
if m == 0 {
return Err(FdarError::InvalidDimension {
parameter: "fpca.mean",
expected: ">0 length".into(),
actual: "0".into(),
});
}
Ok(decompose_beta(
&fit.coefficients,
&fit.fpca.rotation,
ncomp,
m,
))
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn beta_decomposition_logistic(
fit: &FunctionalLogisticResult,
) -> Result<BetaDecomposition, FdarError> {
let ncomp = fit.ncomp;
let m = fit.fpca.mean.len();
if ncomp == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: "must be > 0".into(),
});
}
if m == 0 {
return Err(FdarError::InvalidDimension {
parameter: "fpca.mean",
expected: ">0 length".into(),
actual: "0".into(),
});
}
Ok(decompose_beta(
&fit.coefficients,
&fit.fpca.rotation,
ncomp,
m,
))
}
fn decompose_beta(
coefficients: &[f64],
rotation: &FdMatrix,
ncomp: usize,
m: usize,
) -> BetaDecomposition {
let mut components = Vec::with_capacity(ncomp);
let mut coefs = Vec::with_capacity(ncomp);
let mut norms_sq = Vec::with_capacity(ncomp);
for k in 0..ncomp {
let ck = coefficients[1 + k];
coefs.push(ck);
let comp: Vec<f64> = (0..m).map(|j| ck * rotation[(j, k)]).collect();
let nsq: f64 = comp.iter().map(|v| v * v).sum();
norms_sq.push(nsq);
components.push(comp);
}
let total_sq: f64 = norms_sq.iter().sum();
let variance_proportion = if total_sq > 0.0 {
norms_sq.iter().map(|&s| s / total_sq).collect()
} else {
vec![0.0; ncomp]
};
BetaDecomposition {
components,
coefficients: coefs,
variance_proportion,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignificanceDirection {
Positive,
Negative,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct SignificantRegion {
pub start_idx: usize,
pub end_idx: usize,
pub direction: SignificanceDirection,
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn significant_regions(
lower: &[f64],
upper: &[f64],
) -> Result<Vec<SignificantRegion>, FdarError> {
if lower.is_empty() {
return Err(FdarError::InvalidDimension {
parameter: "lower",
expected: ">0 length".into(),
actual: "0".into(),
});
}
if lower.len() != upper.len() {
return Err(FdarError::InvalidDimension {
parameter: "upper",
expected: format!("{} (matching lower)", lower.len()),
actual: format!("{}", upper.len()),
});
}
let n = lower.len();
let mut regions = Vec::new();
let mut i = 0;
while i < n {
if let Some(d) = detect_direction(lower[i], upper[i]) {
let start = i;
i += 1;
while i < n && detect_direction(lower[i], upper[i]) == Some(d) {
i += 1;
}
regions.push(SignificantRegion {
start_idx: start,
end_idx: i - 1,
direction: d,
});
} else {
i += 1;
}
}
Ok(regions)
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn significant_regions_from_se(
beta_t: &[f64],
beta_se: &[f64],
z_alpha: f64,
) -> Result<Vec<SignificantRegion>, FdarError> {
if beta_t.is_empty() {
return Err(FdarError::InvalidDimension {
parameter: "beta_t",
expected: ">0 length".into(),
actual: "0".into(),
});
}
if beta_t.len() != beta_se.len() {
return Err(FdarError::InvalidDimension {
parameter: "beta_se",
expected: format!("{} (matching beta_t)", beta_t.len()),
actual: format!("{}", beta_se.len()),
});
}
let lower: Vec<f64> = beta_t
.iter()
.zip(beta_se)
.map(|(b, s)| b - z_alpha * s)
.collect();
let upper: Vec<f64> = beta_t
.iter()
.zip(beta_se)
.map(|(b, s)| b + z_alpha * s)
.collect();
significant_regions(&lower, &upper)
}
fn detect_direction(lower: f64, upper: f64) -> Option<SignificanceDirection> {
if lower > 0.0 {
Some(SignificanceDirection::Positive)
} else if upper < 0.0 {
Some(SignificanceDirection::Negative)
} else {
None
}
}