use greeners_core::linalg::LinalgInverse as _;
use greeners_core::GreenersError;
use ndarray::{Array1, Array2};
use std::fmt;
#[derive(Debug)]
pub struct RegPathResult {
pub lambdas: Array1<f64>,
pub coef_path: Array2<f64>,
pub intercept_path: Array1<f64>,
pub optimal_lambda: f64,
pub optimal_coefs: Array1<f64>,
pub optimal_intercept: f64,
pub bic_path: Array1<f64>,
pub reg_type: String,
pub alpha: f64,
pub n_lambdas: usize,
pub n_obs: usize,
pub n_pred: usize,
pub variable_names: Vec<String>,
pub selected_vars: Vec<String>,
}
impl fmt::Display for RegPathResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Regularization Path ")?;
writeln!(f, "{} (alpha={:.2})", self.reg_type, self.alpha)?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Predictors:", self.n_pred)?;
writeln!(f, "{:<20} {:>12}", "Lambda values:", self.n_lambdas)?;
writeln!(f, "{:<20} {:>12.6}", "Optimal lambda:", self.optimal_lambda)?;
writeln!(
f,
"{:<20} {:>12}",
"Selected vars:",
self.selected_vars.len()
)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Coefficients at optimal lambda:")?;
writeln!(f, " {:<14} {:>12}", "Variable", "Coef")?;
writeln!(f, "{:-^78}", "")?;
writeln!(f, " {:<14} {:>12.6}", "Intercept", self.optimal_intercept)?;
for (j, name) in self.variable_names.iter().enumerate() {
let coef = self.optimal_coefs[j];
let marker = if coef.abs() < 1e-10 { " (0)" } else { "" };
writeln!(f, " {:<14} {:>12.6}{}", name, coef, marker)?;
}
writeln!(f, "\n BIC path (selected):")?;
writeln!(f, " {:<10} {:>12} {:>12}", "Lambda", "BIC", "Non-zero")?;
writeln!(f, "{:-^78}", "")?;
let n_show = 10.min(self.n_lambdas);
let step = (self.n_lambdas / n_show).max(1);
for idx in (0..self.n_lambdas).step_by(step) {
let n_nonzero = (0..self.n_pred)
.filter(|&j| self.coef_path[(idx, j)].abs() > 1e-10)
.count();
writeln!(
f,
" {:<10.4} {:>12.2} {:>12}",
self.lambdas[idx], self.bic_path[idx], n_nonzero
)?;
}
write!(f, "{:=^78}", "")
}
}
pub struct RegPath;
impl RegPath {
pub fn fit(
y: &Array1<f64>,
x: &Array2<f64>,
reg_type: &str,
alpha: Option<f64>,
n_lambdas: Option<usize>,
variable_names: Option<Vec<String>>,
) -> Result<RegPathResult, GreenersError> {
let n = y.len();
let p = x.ncols();
if x.nrows() != n {
return Err(GreenersError::ShapeMismatch(
"RegPath: y and x must have same n".into(),
));
}
if n < p + 2 {
return Err(GreenersError::InvalidOperation(
"RegPath: need more observations than predictors".into(),
));
}
let names = variable_names.unwrap_or_else(|| (0..p).map(|i| format!("x{}", i)).collect());
let a = alpha.unwrap_or(1.0).clamp(0.0, 1.0);
let n_lam = n_lambdas.unwrap_or(50);
let x_mean: Array1<f64> = (0..p)
.map(|j| (0..n).map(|i| x[(i, j)]).sum::<f64>() / n as f64)
.collect();
let x_std: Array1<f64> = (0..p)
.map(|j| {
let var = (0..n).map(|i| (x[(i, j)] - x_mean[j]).powi(2)).sum::<f64>() / n as f64;
var.sqrt().max(1e-10)
})
.collect();
let y_mean = y.mean().unwrap_or(0.0);
let y_std = y.std(0.0).max(1e-10);
let x_norm: Array2<f64> = {
let mut data: Vec<f64> = Vec::with_capacity(n * p);
for i in 0..n {
for j in 0..p {
data.push((x[(i, j)] - x_mean[j]) / x_std[j]);
}
}
Array2::from_shape_vec((n, p), data)
.map_err(|e| GreenersError::ShapeMismatch(format!("{}", e)))?
};
let y_norm: Array1<f64> = y.mapv(|v| (v - y_mean) / y_std);
let xty: Array1<f64> = x_norm.t().dot(&y_norm);
let lambda_max =
xty.iter().map(|&v| v.abs()).fold(0.0_f64, f64::max) / (n as f64 * a.max(0.01));
let lambda_min = lambda_max * 1e-4;
let lambdas: Array1<f64> = (0..n_lam)
.map(|i| {
let t = i as f64 / (n_lam - 1) as f64;
lambda_max * (lambda_min / lambda_max).powf(t)
})
.collect();
let mut coef_path = Array2::zeros((n_lam, p));
let mut intercept_path = Array1::zeros(n_lam);
let mut bic_path = Array1::zeros(n_lam);
for (idx, &lam) in lambdas.iter().enumerate() {
let coefs = if reg_type == "ridge" {
Self::ridge_fit(&x_norm, &y_norm, lam, n, p)?
} else if reg_type == "lasso" {
Self::lasso_fit(&x_norm, &y_norm, lam, 1.0, n, p)?
} else {
Self::lasso_fit(&x_norm, &y_norm, lam, a, n, p)?
};
for j in 0..p {
coef_path[(idx, j)] = coefs[j] * y_std / x_std[j];
}
intercept_path[idx] =
y_mean - (0..p).map(|j| coef_path[(idx, j)] * x_mean[j]).sum::<f64>();
let mut sse = 0.0;
for i in 0..n {
let pred = intercept_path[idx]
+ (0..p).map(|j| coef_path[(idx, j)] * x[(i, j)]).sum::<f64>();
sse += (y[i] - pred).powi(2);
}
let n_nonzero = (0..p)
.filter(|&j| coef_path[(idx, j)].abs() > 1e-10)
.count();
let sigma2 = sse / n as f64;
bic_path[idx] = n as f64 * sigma2.ln() + n_nonzero as f64 * (n as f64).ln();
}
let mut best_idx = 0;
let mut best_bic = bic_path[0];
for i in 1..n_lam {
if bic_path[i] < best_bic {
best_bic = bic_path[i];
best_idx = i;
}
}
let optimal_lambda = lambdas[best_idx];
let optimal_coefs = coef_path.row(best_idx).to_owned();
let optimal_intercept = intercept_path[best_idx];
let selected_vars: Vec<String> = (0..p)
.filter(|&j| optimal_coefs[j].abs() > 1e-10)
.map(|j| names[j].clone())
.collect();
Ok(RegPathResult {
lambdas,
coef_path,
intercept_path,
optimal_lambda,
optimal_coefs,
optimal_intercept,
bic_path,
reg_type: reg_type.to_string(),
alpha: a,
n_lambdas: n_lam,
n_obs: n,
n_pred: p,
variable_names: names,
selected_vars,
})
}
fn ridge_fit(
x: &Array2<f64>,
y: &Array1<f64>,
lambda: f64,
_n: usize,
p: usize,
) -> Result<Array1<f64>, GreenersError> {
let xt = x.t();
let xtx = xt.dot(x);
let mut xtx_reg = xtx.clone();
for j in 0..p {
xtx_reg[(j, j)] += lambda;
}
let xtx_inv = xtx_reg.inv()?;
let xty = xt.dot(y);
Ok(xtx_inv.dot(&xty))
}
fn lasso_fit(
x: &Array2<f64>,
y: &Array1<f64>,
lambda: f64,
alpha: f64,
n: usize,
p: usize,
) -> Result<Array1<f64>, GreenersError> {
let mut beta: Array1<f64> = Array1::zeros(p);
let lam = lambda * n as f64;
let mut col_sq = vec![0.0_f64; p];
for j in 0..p {
for i in 0..n {
col_sq[j] += x[(i, j)] * x[(i, j)];
}
}
let max_iter = 100;
for _ in 0..max_iter {
let mut max_change = 0.0;
for j in 0..p {
let mut rho = 0.0;
for i in 0..n {
let mut pred = 0.0;
for jj in 0..p {
if jj != j {
pred += x[(i, jj)] * beta[jj];
}
}
rho += x[(i, j)] * (y[i] - pred);
}
let l1 = lam * alpha;
let l2 = lam * (1.0 - alpha);
let denom = col_sq[j] + l2;
if denom < 1e-15 {
continue;
}
let new_beta = if rho > l1 {
(rho - l1) / denom
} else if rho < -l1 {
(rho + l1) / denom
} else {
0.0
};
let change = (new_beta - beta[j]).abs();
if change > max_change {
max_change = change;
}
beta[j] = new_beta;
}
if max_change < 1e-8 {
break;
}
}
Ok(beta)
}
}