use crate::linalg::Matrix;
const LAMBDA_EFFECTIVE_INFINITY: f64 = f64::INFINITY;
const LAMBDA_MIN_RATIO_MINIMUM: f64 = 1.0e-6;
#[derive(Clone, Debug)]
pub struct LambdaPathOptions {
pub nlambda: usize,
pub lambda_min_ratio: Option<f64>,
pub alpha: f64,
pub eps_for_ridge: f64,
}
impl Default for LambdaPathOptions {
fn default() -> Self {
LambdaPathOptions {
nlambda: 100,
lambda_min_ratio: None,
alpha: 1.0,
eps_for_ridge: 1e-3,
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn compute_lambda_max(
x: &Matrix,
y: &[f64],
alpha: f64,
penalty_factor: Option<&[f64]>,
intercept_col: Option<usize>,
) -> f64 {
let alpha_clamped = alpha.max(1e-3);
let p = x.cols;
let mut max_corr: f64 = 0.0;
for j in 0..p {
if let Some(ic) = intercept_col {
if j == ic {
continue;
}
}
let mut corr = 0.0;
for i in 0..x.rows {
corr += x.get(i, j) * y[i];
}
let corr = corr.abs();
let effective_corr = if let Some(penalty_factors) = penalty_factor {
if j < penalty_factors.len() && penalty_factors[j] > 0.0 {
corr / penalty_factors[j]
} else {
corr
}
} else {
corr
};
max_corr = max_corr.max(effective_corr);
}
max_corr / alpha_clamped
}
pub fn make_lambda_path(
x: &Matrix,
y: &[f64],
options: &LambdaPathOptions,
penalty_factor: Option<&[f64]>,
intercept_col: Option<usize>,
) -> Vec<f64> {
let n = x.rows;
let p = x.cols;
let default_lambda_min_ratio = if n >= p { 0.0001 } else { 0.01 };
let lambda_min_ratio = options.lambda_min_ratio.unwrap_or(default_lambda_min_ratio);
let lambda_min_ratio_clamped = lambda_min_ratio.max(LAMBDA_MIN_RATIO_MINIMUM);
let lambda_decay_factor = lambda_min_ratio_clamped.powf(1.0 / (options.nlambda - 1) as f64);
let lambda_max = compute_lambda_max(x, y, options.alpha, penalty_factor, intercept_col);
let mut lambdas = Vec::with_capacity(options.nlambda);
for k in 0..options.nlambda {
if k == 0 {
lambdas.push(LAMBDA_EFFECTIVE_INFINITY);
} else if k == 1 {
lambdas.push(lambda_decay_factor * lambda_max);
} else {
lambdas.push(lambdas[k - 1] * lambda_decay_factor);
}
}
lambdas
}
pub fn extract_lambdas(full_path: &[f64], indices: &[usize]) -> Vec<f64> {
indices.iter().map(|&i| full_path[i]).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_lambda_max() {
let x_data = vec![1.0, -1.0, 1.0, 0.0, 1.0, 1.0];
let x = Matrix::new(3, 2, x_data);
let y = vec![-1.0, 0.0, 1.0];
let y_norm: f64 = y.iter().map(|&yi| yi * yi).sum::<f64>().sqrt();
let y_standardized: Vec<f64> = y.iter().map(|&yi| yi / y_norm).collect();
let lambda_max = compute_lambda_max(&x, &y_standardized, 1.0, None, Some(0));
assert!((lambda_max - 2.0_f64.sqrt()).abs() < 1e-10);
}
#[test]
fn test_make_lambda_path_glmnet_style() {
let x_data = vec![1.0, -1.0, 1.0, 0.0, 1.0, 1.0];
let x = Matrix::new(3, 2, x_data);
let y = vec![-1.0, 0.0, 1.0];
let y_norm: f64 = y.iter().map(|&yi| yi * yi).sum::<f64>().sqrt();
let y_standardized: Vec<f64> = y.iter().map(|&yi| yi / y_norm).collect();
let options = LambdaPathOptions {
nlambda: 10,
lambda_min_ratio: Some(0.01),
alpha: 1.0,
eps_for_ridge: 0.001,
};
let path = make_lambda_path(&x, &y_standardized, &options, None, Some(0));
assert_eq!(path.len(), 10);
assert_eq!(path[0], LAMBDA_EFFECTIVE_INFINITY);
let lambda_max = 2.0_f64.sqrt();
let lambda_decay_factor = 0.01_f64.powf(1.0 / 9.0);
assert!((path[1] - lambda_decay_factor * lambda_max).abs() < 1e-10);
for i in 1..path.len() {
assert!(path[i] < path[i - 1]);
}
}
#[test]
fn test_lambda_max_with_small_alpha() {
let x_data = vec![1.0, -1.0, 1.0, 0.0, 1.0, 1.0];
let x = Matrix::new(3, 2, x_data);
let y = vec![-1.0, 0.0, 1.0];
let y_norm: f64 = y.iter().map(|&yi| yi * yi).sum::<f64>().sqrt();
let y_standardized: Vec<f64> = y.iter().map(|&yi| yi / y_norm).collect();
let lambda_max = compute_lambda_max(&x, &y_standardized, 0.0001, None, Some(0));
let expected = 2.0_f64.sqrt() / 1e-3;
assert!((lambda_max - expected).abs() < 1e-10);
}
#[test]
fn test_extract_lambdas() {
let path = vec![10.0, 5.0, 2.5, 1.25, 0.625];
let indices = vec![0, 2, 4];
let extracted = extract_lambdas(&path, &indices);
assert_eq!(extracted, vec![10.0, 2.5, 0.625]);
}
}