use crate::error::{StatsError, StatsResult};
use crate::regression::utils::*;
use crate::regression::RegressionResults;
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::numeric::Float;
use scirs2_linalg::{inv, lstsq};
use std::collections::HashSet;
type PreprocessingResult<F> = (Array2<F>, F, Array1<F>, Array1<F>);
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn ridge_regression<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
alpha: Option<F>,
fit_intercept: Option<bool>,
normalize: Option<bool>,
tol: Option<F>,
max_iter: Option<usize>,
conf_level: Option<F>,
) -> StatsResult<RegressionResults<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if x.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
let n = x.nrows();
let p_features = x.ncols();
let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
let fit_intercept = fit_intercept.unwrap_or(true);
let normalize = normalize.unwrap_or(false);
let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
let max_iter = max_iter.unwrap_or(1000);
let conf_level =
conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
if alpha < F::zero() {
return Err(StatsError::InvalidArgument(
"alpha must be non-negative".to_string(),
));
}
let (x_processed, y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
let p = if fit_intercept {
p_features + 1
} else {
p_features
};
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 observations required for ridge regression".to_string(),
));
}
let ridgesize = if fit_intercept { p_features } else { p };
let mut x_ridge = Array2::zeros((n + ridgesize, p));
for i in 0..n {
for j in 0..p {
x_ridge[[i, j]] = x_processed[[i, j]];
}
}
let sqrt_alpha = scirs2_core::numeric::Float::sqrt(alpha);
for i in 0..ridgesize {
let j = if fit_intercept { i + 1 } else { i }; x_ridge[[n + i, j]] = sqrt_alpha;
}
let mut y_ridge = Array1::zeros(n + ridgesize);
for i in 0..n {
y_ridge[i] = y[i];
}
let coefficients = solve_ridge_system(&x_ridge.view(), &y_ridge.view(), tol, max_iter)?;
let transformed_coefficients = if normalize || fit_intercept {
transform_coefficients(&coefficients, y_mean, &x_mean, &x_std, fit_intercept)
} else {
coefficients.clone()
};
let x_design = if fit_intercept {
add_intercept(x)
} else {
x.to_owned()
};
let fitted_values = x_design.dot(&transformed_coefficients);
let residuals = y.to_owned() - &fitted_values;
let df_model = p - 1; let df_residuals = n - p;
let (_y_mean, ss_total, ss_residual, ss_explained) =
calculate_sum_of_squares(y, &residuals.view());
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
/ F::from(df_residuals).expect("Failed to convert to float");
let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors = match calculate_ridge_std_errors(
&x_design.view(),
&residuals.view(),
alpha,
df_residuals,
) {
Ok(se) => se,
Err(_) => Array1::<F>::zeros(p),
};
let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
let p_values = t_values.mapv(|t| {
let t_abs = crate::regression::utils::float_abs(t);
let df_f = F::from(df_residuals).expect("Failed to convert to float");
let ratio = t_abs / crate::regression::utils::float_sqrt(df_f + t_abs * t_abs);
let one_minus_ratio = F::one() - ratio;
F::from(2.0).expect("Failed to convert constant to float") * one_minus_ratio
});
let mut conf_intervals = Array2::<F>::zeros((p, 2));
let z = norm_ppf(
F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
);
for i in 0..p {
let margin = std_errors[i] * z;
conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
}
let f_statistic = if df_model > 0 && df_residuals > 0 {
(ss_explained / F::from(df_model).expect("Failed to convert to float"))
/ (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
} else {
F::infinity()
};
let f_p_value = F::zero();
Ok(RegressionResults {
coefficients: transformed_coefficients,
std_errors,
t_values,
p_values,
conf_intervals,
r_squared,
adj_r_squared,
f_statistic,
f_p_value,
residual_std_error,
df_residuals,
residuals,
fitted_values,
inlier_mask: vec![true; n], })
}
#[allow(dead_code)]
fn solve_ridge_system<F>(
x_ridge: &ArrayView2<F>,
y_ridge: &ArrayView1<F>,
_tol: F,
_max_iter: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
match lstsq(x_ridge, y_ridge, None) {
Ok(result) => Ok(result.x),
Err(e) => Err(StatsError::ComputationError(format!(
"Least squares computation failed: {:?}",
e
))),
}
}
#[allow(dead_code)]
fn preprocessdata<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
fit_intercept: bool,
normalize: bool,
) -> StatsResult<PreprocessingResult<F>>
where
F: Float + std::iter::Sum<F> + 'static + std::fmt::Display,
{
let n = x.nrows();
let p = x.ncols();
let y_mean = if fit_intercept {
y.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float")
} else {
F::zero()
};
let mut x_mean = Array1::<F>::zeros(p);
let mut x_std = Array1::<F>::ones(p);
if fit_intercept || normalize {
for j in 0..p {
let col = x.column(j);
let mean =
col.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
x_mean[j] = mean;
if normalize {
let mut ss = F::zero();
for &val in col {
ss = ss + scirs2_core::numeric::Float::powi(val - mean, 2);
}
let std_dev = scirs2_core::numeric::Float::sqrt(
ss / F::from(n).expect("Failed to convert to float"),
);
x_std[j] = if std_dev > F::epsilon() {
std_dev
} else {
F::one()
};
}
}
}
let mut x_processed = if fit_intercept {
Array2::<F>::zeros((n, p + 1))
} else {
Array2::<F>::zeros((n, p))
};
if fit_intercept {
for i in 0..n {
x_processed[[i, 0]] = F::one();
}
}
let offset = if fit_intercept { 1 } else { 0 };
for i in 0..n {
for j in 0..p {
let val = if normalize || fit_intercept {
(x[[i, j]] - x_mean[j]) / x_std[j]
} else {
x[[i, j]]
};
x_processed[[i, j + offset]] = val;
}
}
Ok((x_processed, y_mean, x_mean, x_std))
}
#[allow(dead_code)]
fn transform_coefficients<F>(
coefficients: &Array1<F>,
y_mean: F,
x_mean: &Array1<F>,
x_std: &Array1<F>,
fit_intercept: bool,
) -> Array1<F>
where
F: Float + 'static + std::fmt::Display,
{
let _p = coefficients.len();
let p_features = x_mean.len();
let mut transformed = coefficients.clone();
if fit_intercept {
let mut _intercept = coefficients[0];
for j in 0..p_features {
_intercept = _intercept - coefficients[j + 1] * x_mean[j] / x_std[j];
}
_intercept = _intercept + y_mean;
transformed[0] = _intercept;
for j in 0..p_features {
transformed[j + 1] = coefficients[j + 1] / x_std[j];
}
} else {
for j in 0..p_features {
transformed[j] = coefficients[j] / x_std[j];
}
}
transformed
}
#[allow(dead_code)]
fn calculate_ridge_std_errors<F>(
x: &ArrayView2<F>,
residuals: &ArrayView1<F>,
alpha: F,
df: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
let mse = residuals
.iter()
.map(|&r| scirs2_core::numeric::Float::powi(r, 2))
.sum::<F>()
/ F::from(df).expect("Failed to convert to float");
let xtx = x.t().dot(x);
let p = x.ncols();
let mut xtx_reg = xtx.clone();
for i in 0..p {
xtx_reg[[i, i]] += alpha;
}
let xtx_reg_inv = match inv(&xtx_reg.view(), None) {
Ok(inv_result) => inv_result,
Err(_) => {
return Ok(Array1::<F>::zeros(p));
}
};
let std_errors = (xtx_reg_inv.dot(&xtx).dot(&xtx_reg_inv))
.diag()
.mapv(|v| scirs2_core::numeric::Float::sqrt(v * mse));
Ok(std_errors)
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn lasso_regression<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
alpha: Option<F>,
fit_intercept: Option<bool>,
normalize: Option<bool>,
tol: Option<F>,
max_iter: Option<usize>,
conf_level: Option<F>,
) -> StatsResult<RegressionResults<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if x.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
let n = x.nrows();
let p_features = x.ncols();
let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
let fit_intercept = fit_intercept.unwrap_or(true);
let normalize = normalize.unwrap_or(false);
let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
let max_iter = max_iter.unwrap_or(1000);
let conf_level =
conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
if alpha < F::zero() {
return Err(StatsError::InvalidArgument(
"alpha must be non-negative".to_string(),
));
}
let (x_processed, y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
let p = if fit_intercept {
p_features + 1
} else {
p_features
};
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 observations required for lasso regression".to_string(),
));
}
let mut coefficients = Array1::<F>::zeros(p);
let xtx = x_processed.t().dot(&x_processed);
let xty = x_processed.t().dot(y);
let mut converged = false;
let mut _iter = 0;
while !converged && _iter < max_iter {
converged = true;
let old_coefs = coefficients.clone();
for j in 0..p {
let r_partial = xty[j]
- xtx
.row(j)
.iter()
.zip(coefficients.iter())
.enumerate()
.filter(|&(i_, _)| i_ != j)
.map(|(_, (&xtx_ij, &coef_i))| xtx_ij * coef_i)
.sum::<F>();
let xtx_jj = xtx[[j, j]];
if xtx_jj < F::epsilon() {
coefficients[j] = F::zero();
continue;
}
if j == 0 && fit_intercept {
coefficients[j] = r_partial / xtx_jj;
} else {
if crate::regression::utils::float_abs(r_partial) <= alpha {
coefficients[j] = F::zero();
} else if r_partial > F::zero() {
coefficients[j] = (r_partial - alpha) / xtx_jj;
} else {
coefficients[j] = (r_partial + alpha) / xtx_jj;
}
}
}
let coef_diff = (&coefficients - &old_coefs)
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum();
let coef_norm = old_coefs
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum()
.max(F::epsilon());
if coef_diff / coef_norm < tol {
converged = true;
}
_iter += 1;
}
let transformed_coefficients = if normalize || fit_intercept {
transform_coefficients(&coefficients, y_mean, &x_mean, &x_std, fit_intercept)
} else {
coefficients.clone()
};
let x_design = if fit_intercept {
add_intercept(x)
} else {
x.to_owned()
};
let fitted_values = x_design.dot(&transformed_coefficients);
let residuals = y.to_owned() - &fitted_values;
let nonzero_coefs = transformed_coefficients
.iter()
.filter(|&&x| crate::regression::utils::float_abs(x) > F::epsilon())
.count();
let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
let df_residuals = n - nonzero_coefs;
let (_y_mean, ss_total, ss_residual, ss_explained) =
calculate_sum_of_squares(y, &residuals.view());
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
/ F::from(df_residuals).expect("Failed to convert to float");
let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors = match calculate_lasso_std_errors(
&x_design.view(),
&residuals.view(),
&transformed_coefficients,
df_residuals,
) {
Ok(se) => se,
Err(_) => Array1::<F>::zeros(p),
};
let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
let p_values = t_values.mapv(|t| {
let t_abs = crate::regression::utils::float_abs(t);
let df_f = F::from(df_residuals).expect("Failed to convert to float");
let ratio = t_abs / crate::regression::utils::float_sqrt(df_f + t_abs * t_abs);
let one_minus_ratio = F::one() - ratio;
F::from(2.0).expect("Failed to convert constant to float") * one_minus_ratio
});
let mut conf_intervals = Array2::<F>::zeros((p, 2));
let z = norm_ppf(
F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
);
for i in 0..p {
let margin = std_errors[i] * z;
conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
}
let f_statistic = if df_model > 0 && df_residuals > 0 {
(ss_explained / F::from(df_model).expect("Failed to convert to float"))
/ (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
} else {
F::infinity()
};
let f_p_value = F::zero();
Ok(RegressionResults {
coefficients: transformed_coefficients,
std_errors,
t_values,
p_values,
conf_intervals,
r_squared,
adj_r_squared,
f_statistic,
f_p_value,
residual_std_error,
df_residuals,
residuals,
fitted_values,
inlier_mask: vec![true; n], })
}
#[allow(dead_code)]
fn calculate_lasso_std_errors<F>(
x: &ArrayView2<F>,
residuals: &ArrayView1<F>,
coefficients: &Array1<F>,
df: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
let mse = residuals
.iter()
.map(|&r| scirs2_core::numeric::Float::powi(r, 2))
.sum::<F>()
/ F::from(df).expect("Failed to convert to float");
let p = coefficients.len();
let mut active_set = Vec::new();
for j in 0..p {
if crate::regression::utils::float_abs(coefficients[j]) > F::epsilon() {
active_set.push(j);
}
}
if active_set.is_empty() {
return Ok(Array1::<F>::zeros(p));
}
let n_active = active_set.len();
let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
for (i, &idx_i) in active_set.iter().enumerate() {
for (j, &idx_j) in active_set.iter().enumerate() {
let x_i = x.column(idx_i);
let x_j = x.column(idx_j);
xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
}
}
let xtx_active_inv = match inv(&xtx_active.view(), None) {
Ok(inv_result) => inv_result,
Err(_) => {
return Ok(Array1::<F>::zeros(p));
}
};
let mut std_errors = Array1::<F>::zeros(p);
for (i, &idx) in active_set.iter().enumerate() {
std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
}
Ok(std_errors)
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn elastic_net<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
alpha: Option<F>,
l1_ratio: Option<F>,
fit_intercept: Option<bool>,
normalize: Option<bool>,
tol: Option<F>,
max_iter: Option<usize>,
conf_level: Option<F>,
) -> StatsResult<RegressionResults<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if x.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
let n = x.nrows();
let p_features = x.ncols();
let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
let l1_ratio =
l1_ratio.unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"));
let fit_intercept = fit_intercept.unwrap_or(true);
let normalize = normalize.unwrap_or(false);
let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
let max_iter = max_iter.unwrap_or(1000);
let conf_level =
conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
if alpha < F::zero() {
return Err(StatsError::InvalidArgument(
"alpha must be non-negative".to_string(),
));
}
if l1_ratio < F::zero() || l1_ratio > F::one() {
return Err(StatsError::InvalidArgument(
"l1_ratio must be between 0 and 1".to_string(),
));
}
if l1_ratio < F::epsilon() {
return ridge_regression(
x,
y,
Some(alpha),
Some(fit_intercept),
Some(normalize),
Some(tol),
Some(max_iter),
Some(conf_level),
);
}
if crate::regression::utils::float_abs(l1_ratio - F::one()) < F::epsilon() {
return lasso_regression(
x,
y,
Some(alpha),
Some(fit_intercept),
Some(normalize),
Some(tol),
Some(max_iter),
Some(conf_level),
);
}
let (x_processed, y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
let p = if fit_intercept {
p_features + 1
} else {
p_features
};
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 observations required for elastic net regression".to_string(),
));
}
let mut coefficients = Array1::<F>::zeros(p);
let xtx = x_processed.t().dot(&x_processed);
let xty = x_processed.t().dot(y);
let alpha_l1 = alpha * l1_ratio;
let one_minus_l1_ratio = F::one() - l1_ratio;
let alpha_l2 = alpha * one_minus_l1_ratio;
let mut converged = false;
let mut _iter = 0;
while !converged && _iter < max_iter {
converged = true;
let old_coefs = coefficients.clone();
for j in 0..p {
let r_partial = xty[j]
- xtx
.row(j)
.iter()
.zip(coefficients.iter())
.enumerate()
.filter(|&(i_, _)| i_ != j)
.map(|(_, (&xtx_ij, &coef_i))| xtx_ij * coef_i)
.sum::<F>();
let xtx_jj = xtx[[j, j]] + alpha_l2;
if xtx_jj < F::epsilon() {
coefficients[j] = F::zero();
continue;
}
if j == 0 && fit_intercept {
coefficients[j] = r_partial / xtx_jj;
} else {
if crate::regression::utils::float_abs(r_partial) <= alpha_l1 {
coefficients[j] = F::zero();
} else if r_partial > F::zero() {
coefficients[j] = (r_partial - alpha_l1) / xtx_jj;
} else {
coefficients[j] = (r_partial + alpha_l1) / xtx_jj;
}
}
}
let coef_diff = (&coefficients - &old_coefs)
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum();
let coef_norm = old_coefs
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum()
.max(F::epsilon());
if coef_diff / coef_norm < tol {
converged = true;
}
_iter += 1;
}
let transformed_coefficients = if normalize || fit_intercept {
transform_coefficients(&coefficients, y_mean, &x_mean, &x_std, fit_intercept)
} else {
coefficients.clone()
};
let x_design = if fit_intercept {
add_intercept(x)
} else {
x.to_owned()
};
let fitted_values = x_design.dot(&transformed_coefficients);
let residuals = y.to_owned() - &fitted_values;
let nonzero_coefs = transformed_coefficients
.iter()
.filter(|&&x| crate::regression::utils::float_abs(x) > F::epsilon())
.count();
let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
let df_residuals = n - nonzero_coefs;
let (_y_mean, ss_total, ss_residual, ss_explained) =
calculate_sum_of_squares(y, &residuals.view());
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
/ F::from(df_residuals).expect("Failed to convert to float");
let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors = match calculate_elastic_net_std_errors(
&x_design.view(),
&residuals.view(),
&transformed_coefficients,
alpha_l2,
df_residuals,
) {
Ok(se) => se,
Err(_) => Array1::<F>::zeros(p),
};
let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
let p_values = t_values.mapv(|t| {
let t_abs = crate::regression::utils::float_abs(t);
let df_f = F::from(df_residuals).expect("Failed to convert to float");
let _ratio = t_abs / crate::regression::utils::float_sqrt(df_f + t_abs * t_abs);
let one_minus_ratio = F::one() - _ratio;
F::from(2.0).expect("Failed to convert constant to float") * one_minus_ratio
});
let mut conf_intervals = Array2::<F>::zeros((p, 2));
let z = norm_ppf(
F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
);
for i in 0..p {
let margin = std_errors[i] * z;
conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
}
let f_statistic = if df_model > 0 && df_residuals > 0 {
(ss_explained / F::from(df_model).expect("Failed to convert to float"))
/ (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
} else {
F::infinity()
};
let f_p_value = F::zero();
Ok(RegressionResults {
coefficients: transformed_coefficients,
std_errors,
t_values,
p_values,
conf_intervals,
r_squared,
adj_r_squared,
f_statistic,
f_p_value,
residual_std_error,
df_residuals,
residuals,
fitted_values,
inlier_mask: vec![true; n], })
}
#[allow(dead_code)]
fn calculate_elastic_net_std_errors<F>(
x: &ArrayView2<F>,
residuals: &ArrayView1<F>,
coefficients: &Array1<F>,
alpha_l2: F,
df: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
let mse = residuals
.iter()
.map(|&r| scirs2_core::numeric::Float::powi(r, 2))
.sum::<F>()
/ F::from(df).expect("Failed to convert to float");
let p = coefficients.len();
let mut active_set = Vec::new();
for j in 0..p {
if crate::regression::utils::float_abs(coefficients[j]) > F::epsilon() {
active_set.push(j);
}
}
if active_set.is_empty() {
return Ok(Array1::<F>::zeros(p));
}
let n_active = active_set.len();
let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
for (i, &idx_i) in active_set.iter().enumerate() {
for (j, &idx_j) in active_set.iter().enumerate() {
let x_i = x.column(idx_i);
let x_j = x.column(idx_j);
xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
if i == j {
xtx_active[[i, j]] += alpha_l2;
}
}
}
let xtx_active_inv = match inv(&xtx_active.view(), None) {
Ok(inv_result) => inv_result,
Err(_) => {
return Ok(Array1::<F>::zeros(p));
}
};
let mut std_errors = Array1::<F>::zeros(p);
for (i, &idx) in active_set.iter().enumerate() {
std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
}
Ok(std_errors)
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn group_lasso<F>(
x: &ArrayView2<F>,
y: &ArrayView1<F>,
groups: &[usize],
alpha: Option<F>,
fit_intercept: Option<bool>,
normalize: Option<bool>,
tol: Option<F>,
max_iter: Option<usize>,
conf_level: Option<F>,
) -> StatsResult<RegressionResults<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ std::fmt::Debug
+ std::fmt::Display
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ Send
+ Sync,
{
if x.nrows() != y.len() {
return Err(StatsError::DimensionMismatch(format!(
"Input x has {} rows but y has length {}",
x.nrows(),
y.len()
)));
}
if x.ncols() != groups.len() {
return Err(StatsError::DimensionMismatch(format!(
"Number of columns in x ({}) must match length of groups ({})",
x.ncols(),
groups.len()
)));
}
let n = x.nrows();
let p_features = x.ncols();
let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
let fit_intercept = fit_intercept.unwrap_or(true);
let normalize = normalize.unwrap_or(false);
let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
let max_iter = max_iter.unwrap_or(1000);
let conf_level =
conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
if alpha < F::zero() {
return Err(StatsError::InvalidArgument(
"alpha must be non-negative".to_string(),
));
}
let (x_processed, y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
let p = if fit_intercept {
p_features + 1
} else {
p_features
};
if n < 2 {
return Err(StatsError::InvalidArgument(
"At least 2 observations required for group lasso regression".to_string(),
));
}
let mut unique_groups = HashSet::new();
for &g in groups {
unique_groups.insert(g);
}
let mut group_indices = Vec::new();
for &g in &unique_groups {
let mut indices = Vec::new();
for (i, &group) in groups.iter().enumerate() {
if group == g {
indices.push(if fit_intercept { i + 1 } else { i });
}
}
group_indices.push(indices);
}
let mut coefficients = Array1::<F>::zeros(p);
let mut converged = false;
let mut _iter = 0;
while !converged && _iter < max_iter {
converged = true;
let old_coefs = coefficients.clone();
if fit_intercept {
let r = y - &x_processed
.slice(s![.., 1..])
.dot(&coefficients.slice(s![1..]));
let r_sum: F = r.iter().cloned().sum();
coefficients[0] = r_sum / F::from(r.len()).expect("Operation failed");
}
for group in &group_indices {
if group.is_empty() {
continue;
}
let mut r = y.to_owned();
for j in 0..p {
if !group.contains(&j) {
let x_j = x_processed.column(j);
let beta_j = coefficients[j];
for i in 0..n {
r[i] -= x_j[i] * beta_j;
}
}
}
let mut x_group = Array2::<F>::zeros((n, group.len()));
for (i, &idx) in group.iter().enumerate() {
x_group.column_mut(i).assign(&x_processed.column(idx));
}
let xtr = x_group.t().dot(&r);
let xtx = x_group.t().dot(&x_group);
let xtr_norm = scirs2_core::numeric::Float::sqrt(
xtr.iter()
.map(|&x| scirs2_core::numeric::Float::powi(x, 2))
.sum::<F>(),
);
if xtr_norm < alpha {
for &idx in group {
coefficients[idx] = F::zero();
}
continue;
}
let mut beta_group = match solve_group(xtr, xtx, alpha, tol, max_iter) {
Ok(beta) => beta,
Err(_) => Array1::<F>::zeros(group.len()),
};
let beta_norm = scirs2_core::numeric::Float::sqrt(
beta_group
.iter()
.map(|&x| scirs2_core::numeric::Float::powi(x, 2))
.sum::<F>(),
);
if beta_norm > F::epsilon() {
let shrinkage = F::one().max((beta_norm - alpha) / beta_norm);
beta_group = beta_group.mapv(|x| x * shrinkage);
} else {
beta_group.fill(F::zero());
}
for (i, &idx) in group.iter().enumerate() {
coefficients[idx] = beta_group[i];
}
}
let coef_diff = (&coefficients - &old_coefs)
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum();
let coef_norm = old_coefs
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum()
.max(F::epsilon());
if coef_diff / coef_norm < tol {
converged = true;
}
_iter += 1;
}
let transformed_coefficients = if normalize || fit_intercept {
transform_coefficients(&coefficients, y_mean, &x_mean, &x_std, fit_intercept)
} else {
coefficients.clone()
};
let x_design = if fit_intercept {
add_intercept(x)
} else {
x.to_owned()
};
let fitted_values = x_design.dot(&transformed_coefficients);
let residuals = y.to_owned() - &fitted_values;
let mut nonzero_coefs = 0;
let mut nonzero_groups = HashSet::new();
for (i, &g) in groups.iter().enumerate() {
let idx = if fit_intercept { i + 1 } else { i };
if crate::regression::utils::float_abs(transformed_coefficients[idx]) > F::epsilon() {
nonzero_groups.insert(g);
}
}
for &g in &nonzero_groups {
let groupsize = groups.iter().filter(|&&group| group == g).count();
nonzero_coefs += groupsize;
}
if fit_intercept
&& crate::regression::utils::float_abs(transformed_coefficients[0]) > F::epsilon()
{
nonzero_coefs += 1;
}
let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
let df_residuals = n - nonzero_coefs;
let (_y_mean, ss_total, ss_residual, ss_explained) =
calculate_sum_of_squares(y, &residuals.view());
let r_squared = ss_explained / ss_total;
let adj_r_squared = F::one()
- (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
/ F::from(df_residuals).expect("Failed to convert to float");
let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
let std_errors = match calculate_group_lasso_std_errors(
&x_design.view(),
&residuals.view(),
&transformed_coefficients,
groups,
fit_intercept,
df_residuals,
) {
Ok(se) => se,
Err(_) => Array1::<F>::zeros(p),
};
let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
let p_values = t_values.mapv(|t| {
let t_abs = crate::regression::utils::float_abs(t);
let df_f = F::from(df_residuals).expect("Failed to convert to float");
let ratio = t_abs / crate::regression::utils::float_sqrt(df_f + t_abs * t_abs);
let one_minus_ratio = F::one() - ratio;
F::from(2.0).expect("Failed to convert constant to float") * one_minus_ratio
});
let mut conf_intervals = Array2::<F>::zeros((p, 2));
let z = norm_ppf(
F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
);
for i in 0..p {
let margin = std_errors[i] * z;
conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
}
let f_statistic = if df_model > 0 && df_residuals > 0 {
(ss_explained / F::from(df_model).expect("Failed to convert to float"))
/ (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
} else {
F::infinity()
};
let f_p_value = F::zero();
Ok(RegressionResults {
coefficients: transformed_coefficients,
std_errors,
t_values,
p_values,
conf_intervals,
r_squared,
adj_r_squared,
f_statistic,
f_p_value,
residual_std_error,
df_residuals,
residuals,
fitted_values,
inlier_mask: vec![true; n], })
}
#[allow(dead_code)]
fn solve_group<F>(
xtr: Array1<F>,
xtx: Array2<F>,
_alpha: F,
tol: F,
max_iter: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
let p = xtr.len();
let mut beta = Array1::<F>::zeros(p);
match inv(&xtx.view(), None) {
Ok(xtx_inv) => {
beta = xtx_inv.dot(&xtr);
return Ok(beta);
}
Err(_) => {
}
}
let mut _iter = 0;
let mut converged = false;
let lr = F::from(0.01).expect("Failed to convert constant to float");
while !converged && _iter < max_iter {
let old_beta = beta.clone();
let xtx_beta = xtx.dot(&beta);
let grad = &xtx_beta - &xtr;
let lr_grad = grad.mapv(|g| g * lr);
beta = &beta - &lr_grad;
let beta_diff = (&beta - &old_beta)
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum();
let beta_norm = old_beta
.mapv(|x| scirs2_core::numeric::Float::abs(x))
.sum()
.max(F::epsilon());
if beta_diff / beta_norm < tol {
converged = true;
}
_iter += 1;
}
Ok(beta)
}
#[allow(dead_code)]
fn calculate_group_lasso_std_errors<F>(
x: &ArrayView2<F>,
residuals: &ArrayView1<F>,
coefficients: &Array1<F>,
groups: &[usize],
fit_intercept: bool,
df: usize,
) -> StatsResult<Array1<F>>
where
F: Float
+ std::iter::Sum<F>
+ std::ops::Div<Output = F>
+ 'static
+ scirs2_core::numeric::NumAssign
+ scirs2_core::numeric::One
+ scirs2_core::ndarray::ScalarOperand
+ std::fmt::Display
+ Send
+ Sync,
{
let mse = residuals
.iter()
.map(|&r| scirs2_core::numeric::Float::powi(r, 2))
.sum::<F>()
/ F::from(df).expect("Failed to convert to float");
let p = coefficients.len();
let mut active_groups = HashSet::new();
for (i, &g) in groups.iter().enumerate() {
let idx = if fit_intercept { i + 1 } else { i };
if crate::regression::utils::float_abs(coefficients[idx]) > F::epsilon() {
active_groups.insert(g);
}
}
let mut active_set = Vec::new();
if fit_intercept && crate::regression::utils::float_abs(coefficients[0]) > F::epsilon() {
active_set.push(0);
}
for (i, &g) in groups.iter().enumerate() {
if active_groups.contains(&g) {
let idx = if fit_intercept { i + 1 } else { i };
active_set.push(idx);
}
}
if active_set.is_empty() {
return Ok(Array1::<F>::zeros(p));
}
let n_active = active_set.len();
let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
for (i, &idx_i) in active_set.iter().enumerate() {
for (j, &idx_j) in active_set.iter().enumerate() {
let x_i = x.column(idx_i);
let x_j = x.column(idx_j);
xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
}
}
let xtx_active_inv = match inv(&xtx_active.view(), None) {
Ok(inv_result) => inv_result,
Err(_) => {
return Ok(Array1::<F>::zeros(p));
}
};
let mut std_errors = Array1::<F>::zeros(p);
for (i, &idx) in active_set.iter().enumerate() {
std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
}
Ok(std_errors)
}