use scirs2_core::ndarray::{Array1, Array2};
use std::collections::HashMap;
use super::wrapper::WrapperMethods;
use super::FeatureSelectionResult;
use crate::error::{Result, TimeSeriesError};
pub struct EmbeddedMethods;
impl EmbeddedMethods {
pub fn lasso_selection(
features: &Array2<f64>,
target: &Array1<f64>,
alpha: f64,
max_iterations: usize,
) -> Result<FeatureSelectionResult> {
let (n_samples, n_features) = features.dim();
if n_samples != target.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: n_samples,
actual: target.len(),
});
}
let (normalized_features, _feature_means, _feature_stds) =
Self::normalize_features(features);
let target_mean = target.sum() / n_samples as f64;
let normalized_target = target.mapv(|x| x - target_mean);
let mut coefficients = Array1::zeros(n_features);
let _learning_rate = 0.01;
for _iteration in 0..max_iterations {
let mut max_change = 0.0f64;
for j in 0..n_features {
let old_coef = coefficients[j];
let mut residual = normalized_target.clone();
for k in 0..n_features {
if k != j {
for i in 0..n_samples {
residual[i] -= coefficients[k] * normalized_features[[i, k]];
}
}
}
let correlation = normalized_features.column(j).dot(&residual) / n_samples as f64;
let new_coef = Self::soft_threshold(correlation, alpha);
coefficients[j] = new_coef;
max_change = max_change.max((new_coef - old_coef).abs());
}
if max_change < 1e-6 {
break;
}
}
let mut selected_features = Vec::new();
let mut feature_scores = Array1::zeros(n_features);
for i in 0..n_features {
let abs_coef = coefficients[i].abs();
feature_scores[i] = abs_coef;
if abs_coef > 1e-6 {
selected_features.push(i);
}
}
let mut metadata = HashMap::new();
metadata.insert("alpha".to_string(), alpha);
metadata.insert("n_selected".to_string(), selected_features.len() as f64);
Ok(FeatureSelectionResult {
selected_features,
feature_scores,
method: "LASSO".to_string(),
metadata,
})
}
pub fn ridge_selection(
features: &Array2<f64>,
target: &Array1<f64>,
alpha: f64,
n_features: Option<usize>,
) -> Result<FeatureSelectionResult> {
let (n_samples, n_feat) = features.dim();
if n_samples != target.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: n_samples,
actual: target.len(),
});
}
let (normalized_features, _, _) = Self::normalize_features(features);
let target_mean = target.sum() / n_samples as f64;
let normalized_target = target.mapv(|x| x - target_mean);
let xt = normalized_features.t();
let mut xtx = xt.dot(&normalized_features);
for i in 0..n_feat {
xtx[[i, i]] += alpha;
}
let xty = xt.dot(&normalized_target);
let coefficients = WrapperMethods::solve_linear_system(&xtx, &xty)?;
let mut feature_scores = Array1::zeros(n_feat);
let mut indexed_scores: Vec<(usize, f64)> = Vec::new();
for i in 0..n_feat {
let abs_coef = coefficients[i].abs();
feature_scores[i] = abs_coef;
indexed_scores.push((i, abs_coef));
}
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let n_to_select = n_features.unwrap_or(n_feat / 2).min(n_feat);
let selected_features: Vec<usize> = indexed_scores
.into_iter()
.take(n_to_select)
.map(|(idx_, _)| idx_)
.collect();
let mut metadata = HashMap::new();
metadata.insert("alpha".to_string(), alpha);
metadata.insert("n_selected".to_string(), selected_features.len() as f64);
Ok(FeatureSelectionResult {
selected_features,
feature_scores,
method: "Ridge".to_string(),
metadata,
})
}
pub fn tree_based_selection(
features: &Array2<f64>,
target: &Array1<f64>,
n_features: Option<usize>,
) -> Result<FeatureSelectionResult> {
let (n_samples, n_feat) = features.dim();
if n_samples != target.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: n_samples,
actual: target.len(),
});
}
let mut feature_scores = Array1::zeros(n_feat);
for i in 0..n_feat {
let importance = Self::calculate_feature_importance_tree(&features.column(i), target)?;
feature_scores[i] = importance;
}
let n_to_select = n_features.unwrap_or(n_feat / 2).min(n_feat);
let mut indexed_scores: Vec<(usize, f64)> = feature_scores
.iter()
.enumerate()
.map(|(i, &score)| (i, score))
.collect();
indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let selected_features: Vec<usize> = indexed_scores
.into_iter()
.take(n_to_select)
.map(|(idx_, _)| idx_)
.collect();
let mut metadata = HashMap::new();
metadata.insert("n_selected".to_string(), selected_features.len() as f64);
Ok(FeatureSelectionResult {
selected_features,
feature_scores,
method: "TreeBased".to_string(),
metadata,
})
}
fn normalize_features(features: &Array2<f64>) -> (Array2<f64>, Array1<f64>, Array1<f64>) {
let (n_samples, n_features) = features.dim();
let mut normalized = Array2::zeros((n_samples, n_features));
let mut means = Array1::zeros(n_features);
let mut stds = Array1::zeros(n_features);
for j in 0..n_features {
let col = features.column(j);
let mean = col.sum() / n_samples as f64;
let variance = col.mapv(|x| (x - mean).powi(2)).sum() / n_samples as f64;
let std = variance.sqrt().max(1e-8);
means[j] = mean;
stds[j] = std;
for i in 0..n_samples {
normalized[[i, j]] = (features[[i, j]] - mean) / std;
}
}
(normalized, means, stds)
}
fn soft_threshold(x: f64, threshold: f64) -> f64 {
if x > threshold {
x - threshold
} else if x < -threshold {
x + threshold
} else {
0.0
}
}
fn calculate_feature_importance_tree(
feature: &scirs2_core::ndarray::ArrayView1<f64>,
target: &Array1<f64>,
) -> Result<f64> {
let n = feature.len();
if n < 4 {
return Ok(0.0);
}
let target_mean = target.sum() / n as f64;
let initial_variance = target.mapv(|y| (y - target_mean).powi(2)).sum() / n as f64;
if initial_variance == 0.0 {
return Ok(0.0);
}
let mut feature_values: Vec<f64> = feature.iter().cloned().collect();
feature_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut best_gain = 0.0f64;
for i in 1..n {
if feature_values[i] != feature_values[i - 1] {
let threshold = (feature_values[i] + feature_values[i - 1]) / 2.0;
let mut left_targets = Vec::new();
let mut right_targets = Vec::new();
for j in 0..n {
if feature[j] <= threshold {
left_targets.push(target[j]);
} else {
right_targets.push(target[j]);
}
}
if left_targets.is_empty() || right_targets.is_empty() {
continue;
}
let left_mean = left_targets.iter().sum::<f64>() / left_targets.len() as f64;
let right_mean = right_targets.iter().sum::<f64>() / right_targets.len() as f64;
let left_variance = left_targets
.iter()
.map(|&y| (y - left_mean).powi(2))
.sum::<f64>()
/ left_targets.len() as f64;
let right_variance = right_targets
.iter()
.map(|&y| (y - right_mean).powi(2))
.sum::<f64>()
/ right_targets.len() as f64;
let weighted_variance = (left_targets.len() as f64 * left_variance
+ right_targets.len() as f64 * right_variance)
/ n as f64;
let gain = initial_variance - weighted_variance;
best_gain = best_gain.max(gain);
}
}
Ok(best_gain / initial_variance)
}
}