use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use crate::error::{RegressionError, Result};
use crate::linalg::dmatrix_from_rows;
#[derive(Debug, Clone)]
pub struct RidgeFit {
x: Array2<f64>,
y: Array1<f64>,
lambda: f64,
coefficients: Array1<f64>,
fitted: Array1<f64>,
residuals: Array1<f64>,
leverage: Array1<f64>,
singular_values: Vec<f64>,
effective_df: f64,
rss: f64,
intercept_col: Option<usize>,
n: usize,
p: usize,
}
impl RidgeFit {
pub fn new(x: Array2<f64>, y: Array1<f64>, lambda: f64) -> Result<Self> {
if x.nrows() == 0 || x.ncols() == 0 {
return Err(RegressionError::EmptyInput { what: "X" });
}
if y.len() != x.nrows() {
return Err(RegressionError::ShapeMismatch {
what: "y length vs X rows",
expected: x.nrows(),
got: y.len(),
});
}
if lambda < 0.0 || lambda.is_nan() {
return Err(RegressionError::InvalidParameter {
msg: format!("ridge lambda must be >= 0, got {lambda}"),
});
}
let n = x.nrows();
let p = x.ncols();
let intercept_col = detect_constant_column(&x);
let pred: Vec<usize> = (0..p).filter(|&j| Some(j) != intercept_col).collect();
let q = pred.len();
let has_intercept = intercept_col.is_some();
let y_mean = if has_intercept {
y.sum() / n as f64
} else {
0.0
};
let mut x_means = vec![0.0; q];
if has_intercept {
for (k, &j) in pred.iter().enumerate() {
x_means[k] = x.column(j).sum() / n as f64;
}
}
let mut xc = vec![0.0; n * q];
for i in 0..n {
for (k, &j) in pred.iter().enumerate() {
xc[i * q + k] = x[(i, j)] - x_means[k];
}
}
let yc: Vec<f64> = (0..n).map(|i| y[i] - y_mean).collect();
let xc_dm = dmatrix_from_rows(n, q, &xc);
let svd = xc_dm.svd(true, true);
let u = svd.u.ok_or(RegressionError::RankDeficient)?; let s = svd.singular_values; let v_t = svd.v_t.ok_or(RegressionError::RankDeficient)?; let r = s.len();
let a: Vec<f64> = (0..r)
.map(|j| (0..n).map(|i| u[(i, j)] * yc[i]).sum::<f64>())
.collect();
let filt: Vec<f64> = (0..r).map(|j| s[j] / (s[j] * s[j] + lambda)).collect();
let mut slopes = vec![0.0; q];
for (k, slope) in slopes.iter_mut().enumerate() {
*slope = (0..r).map(|j| v_t[(j, k)] * filt[j] * a[j]).sum();
}
let mut coefficients = Array1::<f64>::zeros(p);
for (k, &j) in pred.iter().enumerate() {
coefficients[j] = slopes[k];
}
if let Some(c) = intercept_col {
let intercept = y_mean - (0..q).map(|k| x_means[k] * slopes[k]).sum::<f64>();
coefficients[c] = intercept;
}
let fitted = x.dot(&coefficients);
let residuals = &y - &fitted;
let rss: f64 = residuals.iter().map(|e| e * e).sum();
let shrink: Vec<f64> = (0..r)
.map(|j| {
let d2 = s[j] * s[j];
d2 / (d2 + lambda)
})
.collect();
let base = if has_intercept { 1.0 / n as f64 } else { 0.0 };
let base_df = if has_intercept { 1.0 } else { 0.0 };
let leverage = Array1::from_shape_fn(n, |i| {
base + (0..r)
.map(|j| u[(i, j)] * u[(i, j)] * shrink[j])
.sum::<f64>()
});
let effective_df = base_df + shrink.iter().sum::<f64>();
Ok(Self {
x,
y,
lambda,
coefficients,
fitted,
residuals,
leverage,
singular_values: s.iter().copied().collect(),
effective_df,
rss,
intercept_col,
n,
p,
})
}
pub fn lambda(&self) -> f64 {
self.lambda
}
pub fn n_observations(&self) -> usize {
self.n
}
pub fn n_parameters(&self) -> usize {
self.p
}
pub fn has_intercept(&self) -> bool {
self.intercept_col.is_some()
}
pub fn design_matrix(&self) -> ArrayView2<'_, f64> {
self.x.view()
}
pub fn response(&self) -> ArrayView1<'_, f64> {
self.y.view()
}
pub fn singular_values(&self) -> &[f64] {
&self.singular_values
}
pub fn coefficients(&self) -> ArrayView1<'_, f64> {
self.coefficients.view()
}
pub fn fitted_values(&self) -> ArrayView1<'_, f64> {
self.fitted.view()
}
pub fn residuals(&self) -> ArrayView1<'_, f64> {
self.residuals.view()
}
pub fn residual_sum_of_squares(&self) -> f64 {
self.rss
}
pub fn leverage(&self) -> ArrayView1<'_, f64> {
self.leverage.view()
}
pub fn effective_df(&self) -> f64 {
self.effective_df
}
pub fn effective_residual_df(&self) -> f64 {
self.n as f64 - self.effective_df
}
pub fn residual_variance(&self) -> f64 {
self.rss / self.effective_residual_df()
}
pub fn gcv(&self) -> f64 {
let denom = 1.0 - self.effective_df / self.n as f64;
if denom <= 0.0 {
return f64::INFINITY;
}
(self.rss / self.n as f64) / (denom * denom)
}
pub fn log_likelihood(&self) -> f64 {
let n = self.n as f64;
-0.5 * n * ((2.0 * std::f64::consts::PI).ln() + 1.0 + (self.rss / n).ln())
}
pub fn aic(&self) -> f64 {
-2.0 * self.log_likelihood() + 2.0 * self.effective_df
}
pub fn bic(&self) -> f64 {
-2.0 * self.log_likelihood() + (self.n as f64).ln() * self.effective_df
}
pub fn ridge_vif(&self) -> Vec<f64> {
let pred: Vec<usize> = (0..self.p)
.filter(|&j| Some(j) != self.intercept_col)
.collect();
let q = pred.len();
let mut out = vec![f64::NAN; self.p];
if q == 0 {
return out;
}
let n = self.n as f64;
let mut means = vec![0.0; q];
let mut sds = vec![0.0; q];
for (k, &j) in pred.iter().enumerate() {
let col = self.x.column(j);
let m = col.sum() / n;
means[k] = m;
sds[k] = (col.iter().map(|v| (v - m).powi(2)).sum::<f64>() / n).sqrt();
}
let corr = |k: usize, l: usize| -> f64 {
if sds[k] <= 0.0 || sds[l] <= 0.0 {
return if k == l { 1.0 } else { 0.0 };
}
let (jk, jl) = (pred[k], pred[l]);
let ck = self.x.column(jk);
let cl = self.x.column(jl);
let cov: f64 = (0..self.n)
.map(|i| (ck[i] - means[k]) * (cl[i] - means[l]))
.sum::<f64>()
/ n;
cov / (sds[k] * sds[l])
};
let lambda_c = self.lambda / n;
let mut a = Array2::<f64>::zeros((q, q));
let mut rmat = Array2::<f64>::zeros((q, q));
for k in 0..q {
for l in 0..q {
let r = corr(k, l);
rmat[(k, l)] = r;
a[(k, l)] = r + if k == l { lambda_c } else { 0.0 };
}
}
let a_dm = dmatrix_from_rows(q, q, a.as_standard_layout().as_slice().unwrap());
let a_inv = match a_dm.try_inverse() {
Some(inv) => inv,
None => return out,
};
let r_dm = dmatrix_from_rows(q, q, rmat.as_standard_layout().as_slice().unwrap());
let m = &a_inv * r_dm * &a_inv;
for (k, &j) in pred.iter().enumerate() {
out[j] = m[(k, k)];
}
out
}
}
pub fn select_lambda_gcv(x: Array2<f64>, y: Array1<f64>, lambdas: &[f64]) -> Result<RidgeFit> {
if lambdas.is_empty() {
return Err(RegressionError::InvalidParameter {
msg: "lambda grid must be non-empty".into(),
});
}
let mut best: Option<RidgeFit> = None;
for &lam in lambdas {
let fit = RidgeFit::new(x.clone(), y.clone(), lam)?;
let better = match &best {
None => true,
Some(b) => fit.gcv() < b.gcv(),
};
if better {
best = Some(fit);
}
}
Ok(best.expect("non-empty grid yields a fit"))
}
fn detect_constant_column(x: &Array2<f64>) -> Option<usize> {
for (j, col) in x.columns().into_iter().enumerate() {
let first = col[0];
let scale = first.abs().max(1.0);
if col.iter().all(|&v| (v - first).abs() <= 1e-12 * scale) {
return Some(j);
}
}
None
}