use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
use scirs2_core::random::{rngs::StdRng, thread_rng, RngExt, SeedableRng};
use sklears_core::{
error::{validate, Result as SklResult, SklearsError},
traits::{Estimator, Fit, Trained, Transform, Untrained},
types::Float,
};
use std::marker::PhantomData;
use crate::base::SelectorMixin;
type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
#[derive(Debug, Clone)]
pub struct TreeSelector<E, State = Untrained> {
estimator: E,
threshold: Option<f64>,
max_features: Option<usize>,
state: PhantomData<State>,
_estimator_: Option<E>,
selected_features_: Option<Vec<usize>>,
n_features_: Option<usize>,
threshold_: Option<f64>,
}
pub trait TreeImportance {
fn feature_importances(&self) -> SklResult<Array1<Float>>;
}
impl<E: Clone> TreeSelector<E, Untrained> {
pub fn new(estimator: E) -> Self {
Self {
estimator,
threshold: None,
max_features: None,
state: PhantomData,
_estimator_: None,
selected_features_: None,
n_features_: None,
threshold_: None,
}
}
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = Some(threshold);
self
}
pub fn max_features(mut self, max_features: usize) -> Self {
self.max_features = Some(max_features);
self
}
}
impl<E> Estimator for TreeSelector<E, Untrained> {
type Config = ();
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
&()
}
}
macro_rules! impl_fit_for_tree_selector {
($y_type:ty) => {
impl<E> Fit<Mat2f64, $y_type> for TreeSelector<E, Untrained>
where
E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
E::Fitted: TreeImportance + Send + Sync,
{
type Fitted = TreeSelector<E, Trained>;
fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
let n_features = x.ncols();
let fitted_estimator = self.estimator.clone().fit(x, y)?;
let importances = fitted_estimator.feature_importances()?;
let threshold = if let Some(thresh) = self.threshold {
thresh
} else {
let mut sorted_importances = importances.to_vec();
sorted_importances.sort_by(|a, b| {
a.partial_cmp(b)
.unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
})
});
sorted_importances[sorted_importances.len() / 2]
};
let mut selected_features: Vec<usize> = (0..n_features)
.filter(|&i| importances[i] > threshold)
.collect();
if let Some(max_feat) = self.max_features {
if selected_features.len() > max_feat {
selected_features.sort_by(|&a, &b| {
importances[b]
.partial_cmp(&importances[a])
.unwrap_or_else(|| {
match (importances[b].is_nan(), importances[a].is_nan()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
})
});
selected_features.truncate(max_feat);
selected_features.sort(); }
}
if selected_features.is_empty() {
return Err(SklearsError::InvalidInput(format!(
"No features selected with threshold={}. Try reducing threshold.",
threshold
)));
}
Ok(TreeSelector {
estimator: self.estimator.clone(),
threshold: self.threshold,
max_features: self.max_features,
state: PhantomData,
_estimator_: Some(self.estimator),
selected_features_: Some(selected_features),
n_features_: Some(n_features),
threshold_: Some(threshold),
})
}
}
};
}
impl_fit_for_tree_selector!(Vec1f64);
impl_fit_for_tree_selector!(Vec1i32);
impl_fit_for_tree_selector!(Vec1usize);
impl<E> Transform<Array2<Float>> for TreeSelector<E, Trained> {
fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_features = self.n_features_.ok_or_else(|| {
SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
})?;
validate::check_n_features(x, n_features)?;
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"TreeSelector not fitted: selected_features_ missing".to_string(),
)
})?;
let n_samples = x.nrows();
let k = selected_features.len();
let mut x_new = Array2::zeros((n_samples, k));
for (new_idx, &old_idx) in selected_features.iter().enumerate() {
x_new.column_mut(new_idx).assign(&x.column(old_idx));
}
Ok(x_new)
}
}
impl<E> SelectorMixin for TreeSelector<E, Trained> {
fn get_support(&self) -> SklResult<Array1<bool>> {
let n_features = self.n_features_.ok_or_else(|| {
SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
})?;
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"TreeSelector not fitted: selected_features_ missing".to_string(),
)
})?;
let mut support = Array1::from_elem(n_features, false);
for &idx in selected_features {
support[idx] = true;
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"TreeSelector not fitted: selected_features_ missing".to_string(),
)
})?;
Ok(indices
.iter()
.filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl<E> TreeSelector<E, Trained> {
pub fn get_support(&self) -> SklResult<Array1<bool>> {
let n_features = self.n_features_.ok_or_else(|| {
SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
})?;
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"TreeSelector not fitted: selected_features_ missing".to_string(),
)
})?;
let mut support = Array1::from_elem(n_features, false);
for &idx in selected_features {
support[idx] = true;
}
Ok(support)
}
pub fn threshold(&self) -> SklResult<f64> {
self.threshold_.ok_or_else(|| {
SklearsError::FitError("TreeSelector not fitted: threshold_ missing".to_string())
})
}
pub fn selected_features(&self) -> SklResult<&[usize]> {
self.selected_features_.as_deref().ok_or_else(|| {
SklearsError::FitError(
"TreeSelector not fitted: selected_features_ missing".to_string(),
)
})
}
}
#[derive(Debug, Clone)]
pub struct GradientBoostingSelector<State = Untrained> {
n_estimators: usize,
learning_rate: f64,
max_depth: usize,
min_samples_split: usize,
min_samples_leaf: usize,
subsample: f64,
threshold: Option<f64>,
max_features: Option<usize>,
random_state: Option<u64>,
state: PhantomData<State>,
feature_importances_: Option<Array1<Float>>,
selected_features_: Option<Vec<usize>>,
n_features_: Option<usize>,
_estimators_: Option<Vec<DecisionStump>>,
}
#[derive(Debug, Clone)]
pub struct DecisionStump {
feature_idx: usize,
threshold: f64,
left_value: f64,
right_value: f64,
importance: f64,
}
impl DecisionStump {
fn predict(&self, x: &Array1<Float>) -> f64 {
if x[self.feature_idx] <= self.threshold {
self.left_value
} else {
self.right_value
}
}
}
impl GradientBoostingSelector<Untrained> {
pub fn new() -> Self {
Self {
n_estimators: 100,
learning_rate: 0.1,
max_depth: 3,
min_samples_split: 2,
min_samples_leaf: 1,
subsample: 1.0,
threshold: None,
max_features: None,
random_state: None,
state: PhantomData,
feature_importances_: None,
selected_features_: None,
n_features_: None,
_estimators_: None,
}
}
pub fn n_estimators(mut self, n_estimators: usize) -> Result<Self, SklearsError> {
if n_estimators < 1 {
return Err(SklearsError::InvalidInput(
"n_estimators must be at least 1".to_string(),
));
}
self.n_estimators = n_estimators;
Ok(self)
}
pub fn learning_rate(mut self, learning_rate: f64) -> Result<Self, SklearsError> {
if learning_rate <= 0.0 {
return Err(SklearsError::InvalidInput(
"learning_rate must be positive".to_string(),
));
}
self.learning_rate = learning_rate;
Ok(self)
}
pub fn max_depth(mut self, max_depth: usize) -> Result<Self, SklearsError> {
if max_depth < 1 {
return Err(SklearsError::InvalidInput(
"max_depth must be at least 1".to_string(),
));
}
self.max_depth = max_depth;
Ok(self)
}
pub fn min_samples_split(mut self, min_samples_split: usize) -> Result<Self, SklearsError> {
if min_samples_split < 2 {
return Err(SklearsError::InvalidInput(
"min_samples_split must be at least 2".to_string(),
));
}
self.min_samples_split = min_samples_split;
Ok(self)
}
pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Result<Self, SklearsError> {
if min_samples_leaf < 1 {
return Err(SklearsError::InvalidInput(
"min_samples_leaf must be at least 1".to_string(),
));
}
self.min_samples_leaf = min_samples_leaf;
Ok(self)
}
pub fn subsample(mut self, subsample: f64) -> Result<Self, SklearsError> {
if !(0.0..=1.0).contains(&subsample) {
return Err(SklearsError::InvalidInput(
"subsample must be between 0 and 1".to_string(),
));
}
self.subsample = subsample;
Ok(self)
}
pub fn threshold(mut self, threshold: Option<f64>) -> Self {
self.threshold = threshold;
self
}
pub fn max_features(mut self, max_features: Option<usize>) -> Self {
self.max_features = max_features;
self
}
pub fn random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
}
impl Default for GradientBoostingSelector<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for GradientBoostingSelector<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<Array2<Float>, Array1<Float>> for GradientBoostingSelector<Untrained> {
type Fitted = GradientBoostingSelector<Trained>;
fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
validate::check_consistent_length(x, y)?;
let (n_samples, n_features) = x.dim();
if n_samples < 2 {
return Err(SklearsError::InvalidInput(
"Gradient Boosting requires at least 2 samples".to_string(),
));
}
let mut rng = if let Some(seed) = self.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut thread_rng())
};
let mut feature_importances = Array1::zeros(n_features);
let mut estimators = Vec::new();
let y_mean = y.mean().unwrap_or(0.0);
let mut predictions = Array1::from_elem(n_samples, y_mean);
for _ in 0..self.n_estimators {
let residuals: Array1<Float> = y - &predictions;
let (x_sample, residuals_sample) = if self.subsample < 1.0 {
let sample_size = (n_samples as f64 * self.subsample).round() as usize;
let indices: Vec<usize> = (0..n_samples).collect();
let mut sampled_indices = indices;
for i in 0..sample_size {
let j = rng.random_range(i..n_samples);
sampled_indices.swap(i, j);
}
sampled_indices.truncate(sample_size);
let x_sub = x.select(Axis(0), &sampled_indices);
let r_sub = residuals.select(Axis(0), &sampled_indices);
(x_sub, r_sub)
} else {
(x.clone(), residuals)
};
let mut best_stump = None;
let mut best_improvement = f64::NEG_INFINITY;
for feature_idx in 0..n_features {
let mut feature_values: Vec<f64> = x_sample.column(feature_idx).to_vec();
feature_values.sort_by(|a, b| {
a.partial_cmp(b)
.unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
})
});
feature_values.dedup();
for i in 0..(feature_values.len().saturating_sub(1)) {
let threshold = (feature_values[i] + feature_values[i + 1]) / 2.0;
let (left_indices, right_indices): (Vec<_>, Vec<_>) = (0..x_sample.nrows())
.partition(|&j| x_sample[[j, feature_idx]] <= threshold);
if left_indices.len() < self.min_samples_leaf
|| right_indices.len() < self.min_samples_leaf
{
continue;
}
let left_value = if !left_indices.is_empty() {
left_indices
.iter()
.map(|&idx| residuals_sample[idx])
.sum::<f64>()
/ left_indices.len() as f64
} else {
0.0
};
let right_value = if !right_indices.is_empty() {
right_indices
.iter()
.map(|&idx| residuals_sample[idx])
.sum::<f64>()
/ right_indices.len() as f64
} else {
0.0
};
let total_var = residuals_sample.mapv(|x| x * x).sum();
let left_var: f64 = left_indices
.iter()
.map(|&idx| (residuals_sample[idx] - left_value).powi(2))
.sum();
let right_var: f64 = right_indices
.iter()
.map(|&idx| (residuals_sample[idx] - right_value).powi(2))
.sum();
let improvement = total_var - left_var - right_var;
if improvement > best_improvement {
best_improvement = improvement;
best_stump = Some(DecisionStump {
feature_idx,
threshold,
left_value,
right_value,
importance: improvement,
});
}
}
}
if let Some(stump) = best_stump {
feature_importances[stump.feature_idx] += stump.importance;
for i in 0..n_samples {
let row = x.row(i);
let prediction = stump.predict(&row.to_owned());
predictions[i] += self.learning_rate * prediction;
}
estimators.push(stump);
}
}
let total_importance = feature_importances.sum();
if total_importance > 0.0 {
feature_importances /= total_importance;
}
let threshold = self
.threshold
.unwrap_or_else(|| feature_importances.mean().unwrap_or(0.0));
let mut selected_features: Vec<usize> = feature_importances
.iter()
.enumerate()
.filter(|(_, &importance)| importance > threshold)
.map(|(idx, _)| idx)
.collect();
if let Some(max_feat) = self.max_features {
if selected_features.len() > max_feat {
selected_features.sort_by(|&a, &b| {
feature_importances[b]
.partial_cmp(&feature_importances[a])
.unwrap_or_else(|| {
match (
feature_importances[b].is_nan(),
feature_importances[a].is_nan(),
) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
})
});
selected_features.truncate(max_feat);
selected_features.sort(); }
}
if selected_features.is_empty() {
return Err(SklearsError::InvalidInput(
"No features selected by Gradient Boosting. Try reducing threshold.".to_string(),
));
}
Ok(GradientBoostingSelector {
n_estimators: self.n_estimators,
learning_rate: self.learning_rate,
max_depth: self.max_depth,
min_samples_split: self.min_samples_split,
min_samples_leaf: self.min_samples_leaf,
subsample: self.subsample,
threshold: self.threshold,
max_features: self.max_features,
random_state: self.random_state,
state: PhantomData,
feature_importances_: Some(feature_importances),
selected_features_: Some(selected_features),
n_features_: Some(n_features),
_estimators_: Some(estimators),
})
}
}
impl Transform<Array2<Float>> for GradientBoostingSelector<Trained> {
fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_features = self.n_features_.ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: n_features_ missing".to_string(),
)
})?;
validate::check_n_features(x, n_features)?;
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
)
})?;
extract_features(x, selected_features)
}
}
impl SelectorMixin for GradientBoostingSelector<Trained> {
fn get_support(&self) -> SklResult<Array1<bool>> {
let n_features = self.n_features_.ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: n_features_ missing".to_string(),
)
})?;
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
)
})?;
let mut support = Array1::from_elem(n_features, false);
for &idx in selected_features {
support[idx] = true;
}
Ok(support)
}
fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
)
})?;
Ok(indices
.iter()
.filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
.collect())
}
}
impl GradientBoostingSelector<Trained> {
pub fn feature_importances(&self) -> SklResult<&Array1<Float>> {
self.feature_importances_.as_ref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: feature_importances_ missing".to_string(),
)
})
}
pub fn selected_features(&self) -> SklResult<&[usize]> {
self.selected_features_.as_deref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
)
})
}
pub fn estimators(&self) -> SklResult<&[DecisionStump]> {
self._estimators_.as_deref().ok_or_else(|| {
SklearsError::FitError(
"GradientBoostingSelector not fitted: estimators_ missing".to_string(),
)
})
}
}
#[derive(Debug, Clone)]
pub struct TreeImportanceSelector {
n_estimators: usize,
max_depth: Option<usize>,
random_state: Option<u64>,
}
impl Default for TreeImportanceSelector {
fn default() -> Self {
Self::new()
}
}
impl TreeImportanceSelector {
pub fn new() -> Self {
Self {
n_estimators: 100,
max_depth: None,
random_state: None,
}
}
pub fn n_estimators(mut self, n_estimators: usize) -> Self {
self.n_estimators = n_estimators;
self
}
pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
self.max_depth = max_depth;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
}
impl crate::ensemble_selectors::SelectorFunction for TreeImportanceSelector {
fn compute_scores(
&self,
x: &Array2<f64>,
y_classif: Option<&Array1<i32>>,
y_regression: Option<&Array1<f64>>,
) -> SklResult<Array1<f64>> {
let n_features = x.ncols();
let n_samples = x.nrows();
let mut rng = if let Some(seed) = self.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut thread_rng())
};
let mut feature_importance = Array1::<f64>::zeros(n_features);
if let Some(y) = y_classif {
for i in 0..n_features {
let feature_col = x.column(i);
let mut score = 0.0;
for j in 0..n_samples {
let feature_val = feature_col[j];
let target_val = y[j] as f64;
score += (feature_val * target_val).abs();
}
feature_importance[i] = score / n_samples as f64;
}
} else if let Some(y) = y_regression {
for i in 0..n_features {
let feature_col = x.column(i);
let correlation = compute_pearson_correlation(&feature_col.to_owned(), y);
feature_importance[i] = correlation.abs();
}
} else {
return Err(SklearsError::InvalidInput(
"Need either classification or regression target".to_string(),
));
}
for importance in feature_importance.iter_mut() {
*importance += rng.random::<f64>() * 0.1; }
Ok(feature_importance)
}
fn name(&self) -> &str {
"tree_importance"
}
fn clone_box(&self) -> Box<dyn crate::ensemble_selectors::SelectorFunction> {
Box::new(self.clone())
}
}
fn compute_pearson_correlation(x: &Array1<f64>, y: &Array1<f64>) -> f64 {
let n = x.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean_x = x.mean().unwrap_or(0.0);
let mean_y = y.mean().unwrap_or(0.0);
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for (&xi, &yi) in x.iter().zip(y.iter()) {
let dx = xi - mean_x;
let dy = yi - mean_y;
numerator += dx * dy;
sum_sq_x += dx * dx;
sum_sq_y += dy * dy;
}
let denominator = (sum_sq_x * sum_sq_y).sqrt();
if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
}
fn extract_features(x: &Array2<Float>, features: &[usize]) -> SklResult<Array2<Float>> {
let n_samples = x.nrows();
let n_selected = features.len();
let mut x_subset = Array2::zeros((n_samples, n_selected));
for (new_idx, &old_idx) in features.iter().enumerate() {
if old_idx >= x.ncols() {
return Err(SklearsError::InvalidInput(format!(
"Feature index {} out of bounds",
old_idx
)));
}
x_subset.column_mut(new_idx).assign(&x.column(old_idx));
}
Ok(x_subset)
}