use crate::error::FdarError;
use crate::linalg::cholesky_solve as linalg_cholesky_solve;
use crate::matrix::FdMatrix;
use crate::regression::{FpcaResult, PlsResult};
mod bootstrap;
mod cv;
mod fregre_lm;
mod logistic;
mod nonparametric;
mod pls;
mod robust;
#[cfg(test)]
mod tests;
pub use bootstrap::{bootstrap_ci_fregre_lm, bootstrap_ci_functional_logistic};
pub use cv::{fregre_basis_cv, fregre_np_cv};
pub use fregre_lm::{fregre_cv, fregre_lm, model_selection_ncomp, predict_fregre_lm};
pub use logistic::{functional_logistic, predict_functional_logistic};
pub use nonparametric::{
fregre_np_from_distances, fregre_np_mixed, predict_fregre_np, predict_fregre_np_from_distances,
};
pub use pls::{fregre_pls, predict_fregre_pls};
pub use robust::{fregre_huber, fregre_l1, predict_fregre_robust};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreLmResult {
pub intercept: f64,
pub beta_t: Vec<f64>,
pub beta_se: Vec<f64>,
pub gamma: Vec<f64>,
pub fitted_values: Vec<f64>,
pub residuals: Vec<f64>,
pub r_squared: f64,
pub r_squared_adj: f64,
pub std_errors: Vec<f64>,
pub ncomp: usize,
pub fpca: FpcaResult,
pub coefficients: Vec<f64>,
pub residual_se: f64,
pub gcv: f64,
pub aic: f64,
pub bic: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreNpResult {
pub fitted_values: Vec<f64>,
pub residuals: Vec<f64>,
pub r_squared: f64,
pub h_func: f64,
pub h_scalar: f64,
pub cv_error: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreRobustResult {
pub intercept: f64,
pub beta_t: Vec<f64>,
pub fitted_values: Vec<f64>,
pub residuals: Vec<f64>,
pub coefficients: Vec<f64>,
pub ncomp: usize,
pub fpca: FpcaResult,
pub iterations: usize,
pub converged: bool,
pub weights: Vec<f64>,
pub r_squared: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FunctionalLogisticResult {
pub intercept: f64,
pub beta_t: Vec<f64>,
pub beta_se: Vec<f64>,
pub gamma: Vec<f64>,
pub probabilities: Vec<f64>,
pub predicted_classes: Vec<usize>,
pub ncomp: usize,
pub accuracy: f64,
pub std_errors: Vec<f64>,
pub coefficients: Vec<f64>,
pub log_likelihood: f64,
pub iterations: usize,
pub fpca: FpcaResult,
pub aic: f64,
pub bic: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreCvResult {
pub k_values: Vec<usize>,
pub cv_errors: Vec<f64>,
pub optimal_k: usize,
pub min_cv_error: f64,
pub oof_predictions: Vec<f64>,
pub fold_assignments: Vec<usize>,
pub fold_errors: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct PlsRegressionResult {
pub intercept: f64,
pub beta_t: Vec<f64>,
pub gamma: Vec<f64>,
pub fitted_values: Vec<f64>,
pub residuals: Vec<f64>,
pub r_squared: f64,
pub r_squared_adj: f64,
pub ncomp: usize,
pub pls: PlsResult,
pub coefficients: Vec<f64>,
pub residual_se: f64,
pub aic: f64,
pub bic: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SelectionCriterion {
Aic,
Bic,
Gcv,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ModelSelectionResult {
pub best_ncomp: usize,
pub criteria: Vec<(usize, f64, f64, f64)>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct BootstrapCiResult {
pub lower: Vec<f64>,
pub upper: Vec<f64>,
pub center: Vec<f64>,
pub sim_lower: Vec<f64>,
pub sim_upper: Vec<f64>,
pub n_boot_success: usize,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreBasisCvResult {
pub optimal_lambda: f64,
pub cv_errors: Vec<f64>,
pub cv_se: Vec<f64>,
pub lambda_values: Vec<f64>,
pub min_cv_error: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FregreNpCvResult {
pub optimal_h: f64,
pub cv_errors: Vec<f64>,
pub cv_se: Vec<f64>,
pub h_values: Vec<f64>,
pub min_cv_error: f64,
}
pub(crate) use crate::linalg::cholesky_factor;
pub(crate) use crate::linalg::cholesky_forward_back;
pub(crate) use crate::linalg::compute_xtx;
fn compute_xty(x: &FdMatrix, y: &[f64]) -> Vec<f64> {
let (n, p) = x.shape();
(0..p)
.map(|k| {
let mut s = 0.0;
for i in 0..n {
s += x[(i, k)] * y[i];
}
s
})
.collect()
}
pub(super) fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Result<Vec<f64>, FdarError> {
linalg_cholesky_solve(a, b, p)
}
pub(crate) fn compute_hat_diagonal(x: &FdMatrix, l: &[f64]) -> Vec<f64> {
let (n, p) = x.shape();
let mut hat_diag = vec![0.0; n];
for i in 0..n {
let mut v = vec![0.0; p];
for j in 0..p {
v[j] = x[(i, j)];
for k in 0..j {
v[j] -= l[j * p + k] * v[k];
}
v[j] /= l[j * p + j];
}
hat_diag[i] = v.iter().map(|vi| vi * vi).sum();
}
hat_diag
}
fn compute_ols_std_errors(l: &[f64], p: usize, sigma2: f64) -> Vec<f64> {
let mut se = vec![0.0; p];
for j in 0..p {
let mut v = vec![0.0; p];
v[j] = 1.0;
for k in 0..p {
for kk in 0..k {
v[k] -= l[k * p + kk] * v[kk];
}
v[k] /= l[k * p + k];
}
se[j] = (sigma2 * v.iter().map(|vi| vi * vi).sum::<f64>()).sqrt();
}
se
}
fn validate_fregre_inputs(
n: usize,
m: usize,
y: &[f64],
scalar_covariates: Option<&FdMatrix>,
) -> Result<(), FdarError> {
if n < 3 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "at least 3 rows".to_string(),
actual: format!("{n}"),
});
}
if m == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "at least 1 column".to_string(),
actual: "0".to_string(),
});
}
if y.len() != n {
return Err(FdarError::InvalidDimension {
parameter: "y",
expected: format!("{n}"),
actual: format!("{}", y.len()),
});
}
if let Some(sc) = scalar_covariates {
if sc.nrows() != n {
return Err(FdarError::InvalidDimension {
parameter: "scalar_covariates",
expected: format!("{n} rows"),
actual: format!("{} rows", sc.nrows()),
});
}
}
Ok(())
}
fn resolve_ncomp(
ncomp: usize,
data: &FdMatrix,
y: &[f64],
scalar_covariates: Option<&FdMatrix>,
n: usize,
m: usize,
) -> Result<usize, FdarError> {
if ncomp == 0 {
let cv = fregre_cv(data, y, scalar_covariates, 1, m.min(n - 1).min(20), 5)?;
Ok(cv.optimal_k)
} else {
Ok(ncomp.min(n - 1).min(m))
}
}
pub(crate) fn build_design_matrix(
fpca_scores: &FdMatrix,
ncomp: usize,
scalar_covariates: Option<&FdMatrix>,
n: usize,
) -> FdMatrix {
let p_scalar = scalar_covariates.map_or(0, super::matrix::FdMatrix::ncols);
let p_total = 1 + ncomp + p_scalar;
let mut design = FdMatrix::zeros(n, p_total);
for i in 0..n {
design[(i, 0)] = 1.0;
for k in 0..ncomp {
design[(i, 1 + k)] = fpca_scores[(i, k)];
}
if let Some(sc) = scalar_covariates {
for j in 0..p_scalar {
design[(i, 1 + ncomp + j)] = sc[(i, j)];
}
}
}
design
}
fn recover_beta_t(fpc_coeffs: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
let ncomp = fpc_coeffs.len();
let mut beta_t = vec![0.0; m];
for k in 0..ncomp {
for j in 0..m {
beta_t[j] += fpc_coeffs[k] * rotation[(j, k)];
}
}
beta_t
}
fn compute_beta_se(gamma_se: &[f64], rotation: &FdMatrix, m: usize) -> Vec<f64> {
let ncomp = gamma_se.len();
let mut beta_se = vec![0.0; m];
for j in 0..m {
let mut var_j = 0.0;
for k in 0..ncomp {
var_j += rotation[(j, k)].powi(2) * gamma_se[k].powi(2);
}
beta_se[j] = var_j.sqrt();
}
beta_se
}
fn compute_fitted(design: &FdMatrix, coeffs: &[f64]) -> Vec<f64> {
let (n, p) = design.shape();
(0..n)
.map(|i| {
let mut yhat = 0.0;
for j in 0..p {
yhat += design[(i, j)] * coeffs[j];
}
yhat
})
.collect()
}
fn compute_r_squared(y: &[f64], residuals: &[f64], p_total: usize) -> (f64, f64) {
let n = y.len();
let y_mean = y.iter().sum::<f64>() / n as f64;
let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
let r_squared = if ss_tot > 0.0 {
1.0 - ss_res / ss_tot
} else {
0.0
};
let df_model = (p_total - 1) as f64;
let r_squared_adj = if n as f64 - df_model - 1.0 > 0.0 {
1.0 - (1.0 - r_squared) * (n as f64 - 1.0) / (n as f64 - df_model - 1.0)
} else {
r_squared
};
(r_squared, r_squared_adj)
}
fn ols_solve(x: &FdMatrix, y: &[f64]) -> Result<(Vec<f64>, Vec<f64>), FdarError> {
let (n, p) = x.shape();
if n < p || p == 0 {
return Err(FdarError::InvalidDimension {
parameter: "design matrix",
expected: format!("n >= p and p > 0 (p={p})"),
actual: format!("n={n}, p={p}"),
});
}
let xtx = compute_xtx(x);
let xty = compute_xty(x, y);
let l = cholesky_factor(&xtx, p)?;
let b = cholesky_forward_back(&l, &xty, p);
let hat_diag = compute_hat_diagonal(x, &l);
Ok((b, hat_diag))
}
pub(crate) fn sigmoid(x: f64) -> f64 {
if x >= 0.0 {
1.0 / (1.0 + (-x).exp())
} else {
let ex = x.exp();
ex / (1.0 + ex)
}
}
impl FregreLmResult {
pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
predict_fregre_lm(self, new_data, new_scalar)
}
}
impl FregreRobustResult {
pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
predict_fregre_robust(self, new_data, new_scalar)
}
}
impl FunctionalLogisticResult {
pub fn predict(&self, new_data: &FdMatrix, new_scalar: Option<&FdMatrix>) -> Vec<f64> {
predict_functional_logistic(self, new_data, new_scalar)
}
}