Skip to main content

sklears_feature_selection/
tree_based_selectors.rs

1//! Tree-based feature selection methods
2
3use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
4use scirs2_core::random::{rngs::StdRng, thread_rng, RngExt, SeedableRng};
5use sklears_core::{
6    error::{validate, Result as SklResult, SklearsError},
7    traits::{Estimator, Fit, Trained, Transform, Untrained},
8    types::Float,
9};
10use std::marker::PhantomData;
11
12use crate::base::SelectorMixin;
13
14// ndarray 0.17 adds a 3rd default type parameter `A = <S as RawData>::Elem` to ArrayBase.
15// The Rust trait solver cannot normalize `Array2<Float>` (2-param alias) to
16// `ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>` (3-param form) in where bounds when
17// any generic type parameters remain. We use the fully expanded form to bypass this.
18type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
19type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
20type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
21type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
22
23/// Tree-based feature importance selector
24/// Generic tree selector that can work with any tree-based estimator
25#[derive(Debug, Clone)]
26pub struct TreeSelector<E, State = Untrained> {
27    estimator: E,
28    threshold: Option<f64>,
29    max_features: Option<usize>,
30    state: PhantomData<State>,
31    // Trained state
32    _estimator_: Option<E>,
33    selected_features_: Option<Vec<usize>>,
34    n_features_: Option<usize>,
35    threshold_: Option<f64>,
36}
37
38/// Trait for tree-based estimators that provide feature importance
39pub trait TreeImportance {
40    /// Get feature importance scores from the tree
41    /// Higher scores indicate more important features
42    fn feature_importances(&self) -> SklResult<Array1<Float>>;
43}
44
45impl<E: Clone> TreeSelector<E, Untrained> {
46    /// Create a new TreeSelector
47    pub fn new(estimator: E) -> Self {
48        Self {
49            estimator,
50            threshold: None,
51            max_features: None,
52            state: PhantomData,
53            _estimator_: None,
54            selected_features_: None,
55            n_features_: None,
56            threshold_: None,
57        }
58    }
59
60    /// Set the threshold for feature selection
61    /// If None, will use median of feature importances
62    pub fn threshold(mut self, threshold: f64) -> Self {
63        self.threshold = Some(threshold);
64        self
65    }
66
67    /// Set the maximum number of features to select
68    pub fn max_features(mut self, max_features: usize) -> Self {
69        self.max_features = Some(max_features);
70        self
71    }
72}
73
74impl<E> Estimator for TreeSelector<E, Untrained> {
75    type Config = ();
76    type Error = SklearsError;
77    type Float = f64;
78
79    fn config(&self) -> &Self::Config {
80        &()
81    }
82}
83
84macro_rules! impl_fit_for_tree_selector {
85    ($y_type:ty) => {
86        impl<E> Fit<Mat2f64, $y_type> for TreeSelector<E, Untrained>
87        where
88            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
89            E::Fitted: TreeImportance + Send + Sync,
90        {
91            type Fitted = TreeSelector<E, Trained>;
92
93            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
94                let n_features = x.ncols();
95
96                // Fit the tree estimator
97                let fitted_estimator = self.estimator.clone().fit(x, y)?;
98
99                // Get feature importances
100                let importances = fitted_estimator.feature_importances()?;
101
102                // Determine threshold
103                let threshold = if let Some(thresh) = self.threshold {
104                    thresh
105                } else {
106                    // Use median as default threshold
107                    let mut sorted_importances = importances.to_vec();
108                    sorted_importances.sort_by(|a, b| {
109                        a.partial_cmp(b)
110                            .unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
111                                (true, false) => std::cmp::Ordering::Less,
112                                (false, true) => std::cmp::Ordering::Greater,
113                                _ => std::cmp::Ordering::Equal,
114                            })
115                    });
116                    sorted_importances[sorted_importances.len() / 2]
117                };
118
119                // Select features above threshold
120                let mut selected_features: Vec<usize> = (0..n_features)
121                    .filter(|&i| importances[i] > threshold)
122                    .collect();
123
124                // Apply max_features limit if specified
125                if let Some(max_feat) = self.max_features {
126                    if selected_features.len() > max_feat {
127                        // Sort by importance and take top max_feat
128                        selected_features.sort_by(|&a, &b| {
129                            importances[b]
130                                .partial_cmp(&importances[a])
131                                .unwrap_or_else(|| {
132                                    match (importances[b].is_nan(), importances[a].is_nan()) {
133                                        (true, false) => std::cmp::Ordering::Less,
134                                        (false, true) => std::cmp::Ordering::Greater,
135                                        _ => std::cmp::Ordering::Equal,
136                                    }
137                                })
138                        });
139                        selected_features.truncate(max_feat);
140                        selected_features.sort(); // Restore original order
141                    }
142                }
143
144                if selected_features.is_empty() {
145                    return Err(SklearsError::InvalidInput(format!(
146                        "No features selected with threshold={}. Try reducing threshold.",
147                        threshold
148                    )));
149                }
150
151                Ok(TreeSelector {
152                    estimator: self.estimator.clone(),
153                    threshold: self.threshold,
154                    max_features: self.max_features,
155                    state: PhantomData,
156                    _estimator_: Some(self.estimator),
157                    selected_features_: Some(selected_features),
158                    n_features_: Some(n_features),
159                    threshold_: Some(threshold),
160                })
161            }
162        }
163    };
164}
165
166impl_fit_for_tree_selector!(Vec1f64);
167impl_fit_for_tree_selector!(Vec1i32);
168impl_fit_for_tree_selector!(Vec1usize);
169
170impl<E> Transform<Array2<Float>> for TreeSelector<E, Trained> {
171    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
172        let n_features = self.n_features_.ok_or_else(|| {
173            SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
174        })?;
175        validate::check_n_features(x, n_features)?;
176
177        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
178            SklearsError::FitError(
179                "TreeSelector not fitted: selected_features_ missing".to_string(),
180            )
181        })?;
182        let n_samples = x.nrows();
183        let k = selected_features.len();
184        let mut x_new = Array2::zeros((n_samples, k));
185
186        for (new_idx, &old_idx) in selected_features.iter().enumerate() {
187            x_new.column_mut(new_idx).assign(&x.column(old_idx));
188        }
189
190        Ok(x_new)
191    }
192}
193
194impl<E> SelectorMixin for TreeSelector<E, Trained> {
195    fn get_support(&self) -> SklResult<Array1<bool>> {
196        let n_features = self.n_features_.ok_or_else(|| {
197            SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
198        })?;
199        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
200            SklearsError::FitError(
201                "TreeSelector not fitted: selected_features_ missing".to_string(),
202            )
203        })?;
204        let mut support = Array1::from_elem(n_features, false);
205
206        for &idx in selected_features {
207            support[idx] = true;
208        }
209
210        Ok(support)
211    }
212
213    fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
214        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
215            SklearsError::FitError(
216                "TreeSelector not fitted: selected_features_ missing".to_string(),
217            )
218        })?;
219        Ok(indices
220            .iter()
221            .filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
222            .collect())
223    }
224}
225
226impl<E> TreeSelector<E, Trained> {
227    /// Get the support mask
228    pub fn get_support(&self) -> SklResult<Array1<bool>> {
229        let n_features = self.n_features_.ok_or_else(|| {
230            SklearsError::FitError("TreeSelector not fitted: n_features_ missing".to_string())
231        })?;
232        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
233            SklearsError::FitError(
234                "TreeSelector not fitted: selected_features_ missing".to_string(),
235            )
236        })?;
237        let mut support = Array1::from_elem(n_features, false);
238
239        for &idx in selected_features {
240            support[idx] = true;
241        }
242
243        Ok(support)
244    }
245
246    /// Get the threshold used for selection
247    pub fn threshold(&self) -> SklResult<f64> {
248        self.threshold_.ok_or_else(|| {
249            SklearsError::FitError("TreeSelector not fitted: threshold_ missing".to_string())
250        })
251    }
252
253    /// Get selected features
254    pub fn selected_features(&self) -> SklResult<&[usize]> {
255        self.selected_features_.as_deref().ok_or_else(|| {
256            SklearsError::FitError(
257                "TreeSelector not fitted: selected_features_ missing".to_string(),
258            )
259        })
260    }
261}
262
263/// Gradient Boosting Feature Selection
264/// Uses gradient boosting to estimate feature importance and select features
265#[derive(Debug, Clone)]
266pub struct GradientBoostingSelector<State = Untrained> {
267    n_estimators: usize,
268    learning_rate: f64,
269    max_depth: usize,
270    min_samples_split: usize,
271    min_samples_leaf: usize,
272    subsample: f64,
273    threshold: Option<f64>,
274    max_features: Option<usize>,
275    random_state: Option<u64>,
276    state: PhantomData<State>,
277    // Trained state
278    feature_importances_: Option<Array1<Float>>,
279    selected_features_: Option<Vec<usize>>,
280    n_features_: Option<usize>,
281    _estimators_: Option<Vec<DecisionStump>>,
282}
283
284/// Simple decision stump for gradient boosting
285#[derive(Debug, Clone)]
286pub struct DecisionStump {
287    feature_idx: usize,
288    threshold: f64,
289    left_value: f64,
290    right_value: f64,
291    importance: f64,
292}
293
294impl DecisionStump {
295    fn predict(&self, x: &Array1<Float>) -> f64 {
296        if x[self.feature_idx] <= self.threshold {
297            self.left_value
298        } else {
299            self.right_value
300        }
301    }
302}
303
304impl GradientBoostingSelector<Untrained> {
305    /// Create a new GradientBoostingSelector
306    pub fn new() -> Self {
307        Self {
308            n_estimators: 100,
309            learning_rate: 0.1,
310            max_depth: 3,
311            min_samples_split: 2,
312            min_samples_leaf: 1,
313            subsample: 1.0,
314            threshold: None,
315            max_features: None,
316            random_state: None,
317            state: PhantomData,
318            feature_importances_: None,
319            selected_features_: None,
320            n_features_: None,
321            _estimators_: None,
322        }
323    }
324
325    /// Set the number of boosting estimators
326    pub fn n_estimators(mut self, n_estimators: usize) -> Result<Self, SklearsError> {
327        if n_estimators < 1 {
328            return Err(SklearsError::InvalidInput(
329                "n_estimators must be at least 1".to_string(),
330            ));
331        }
332        self.n_estimators = n_estimators;
333        Ok(self)
334    }
335
336    /// Set the learning rate
337    pub fn learning_rate(mut self, learning_rate: f64) -> Result<Self, SklearsError> {
338        if learning_rate <= 0.0 {
339            return Err(SklearsError::InvalidInput(
340                "learning_rate must be positive".to_string(),
341            ));
342        }
343        self.learning_rate = learning_rate;
344        Ok(self)
345    }
346
347    /// Set the maximum depth of decision stumps
348    pub fn max_depth(mut self, max_depth: usize) -> Result<Self, SklearsError> {
349        if max_depth < 1 {
350            return Err(SklearsError::InvalidInput(
351                "max_depth must be at least 1".to_string(),
352            ));
353        }
354        self.max_depth = max_depth;
355        Ok(self)
356    }
357
358    /// Set the minimum samples required to split a node
359    pub fn min_samples_split(mut self, min_samples_split: usize) -> Result<Self, SklearsError> {
360        if min_samples_split < 2 {
361            return Err(SklearsError::InvalidInput(
362                "min_samples_split must be at least 2".to_string(),
363            ));
364        }
365        self.min_samples_split = min_samples_split;
366        Ok(self)
367    }
368
369    /// Set the minimum samples required at a leaf
370    pub fn min_samples_leaf(mut self, min_samples_leaf: usize) -> Result<Self, SklearsError> {
371        if min_samples_leaf < 1 {
372            return Err(SklearsError::InvalidInput(
373                "min_samples_leaf must be at least 1".to_string(),
374            ));
375        }
376        self.min_samples_leaf = min_samples_leaf;
377        Ok(self)
378    }
379
380    /// Set the subsample ratio for training each estimator
381    pub fn subsample(mut self, subsample: f64) -> Result<Self, SklearsError> {
382        if !(0.0..=1.0).contains(&subsample) {
383            return Err(SklearsError::InvalidInput(
384                "subsample must be between 0 and 1".to_string(),
385            ));
386        }
387        self.subsample = subsample;
388        Ok(self)
389    }
390
391    /// Set the threshold for feature selection
392    pub fn threshold(mut self, threshold: Option<f64>) -> Self {
393        self.threshold = threshold;
394        self
395    }
396
397    /// Set the maximum number of features to select
398    pub fn max_features(mut self, max_features: Option<usize>) -> Self {
399        self.max_features = max_features;
400        self
401    }
402
403    /// Set the random state for reproducibility
404    pub fn random_state(mut self, seed: u64) -> Self {
405        self.random_state = Some(seed);
406        self
407    }
408}
409
410impl Default for GradientBoostingSelector<Untrained> {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416impl Estimator for GradientBoostingSelector<Untrained> {
417    type Config = ();
418    type Error = SklearsError;
419    type Float = f64;
420
421    fn config(&self) -> &Self::Config {
422        &()
423    }
424}
425
426impl Fit<Array2<Float>, Array1<Float>> for GradientBoostingSelector<Untrained> {
427    type Fitted = GradientBoostingSelector<Trained>;
428
429    fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
430        validate::check_consistent_length(x, y)?;
431
432        let (n_samples, n_features) = x.dim();
433        if n_samples < 2 {
434            return Err(SklearsError::InvalidInput(
435                "Gradient Boosting requires at least 2 samples".to_string(),
436            ));
437        }
438
439        // Initialize random number generator
440        let mut rng = if let Some(seed) = self.random_state {
441            StdRng::seed_from_u64(seed)
442        } else {
443            StdRng::from_rng(&mut thread_rng())
444        };
445
446        // Initialize feature importances
447        let mut feature_importances = Array1::zeros(n_features);
448        let mut estimators = Vec::new();
449
450        // Initialize predictions with mean of target
451        let y_mean = y.mean().unwrap_or(0.0);
452        let mut predictions = Array1::from_elem(n_samples, y_mean);
453
454        // Boosting iterations
455        for _ in 0..self.n_estimators {
456            // Compute residuals (negative gradients for regression)
457            let residuals: Array1<Float> = y - &predictions;
458
459            // Sample data if subsample < 1.0
460            let (x_sample, residuals_sample) = if self.subsample < 1.0 {
461                let sample_size = (n_samples as f64 * self.subsample).round() as usize;
462                let indices: Vec<usize> = (0..n_samples).collect();
463                let mut sampled_indices = indices;
464                for i in 0..sample_size {
465                    let j = rng.random_range(i..n_samples);
466                    sampled_indices.swap(i, j);
467                }
468                sampled_indices.truncate(sample_size);
469
470                let x_sub = x.select(Axis(0), &sampled_indices);
471                let r_sub = residuals.select(Axis(0), &sampled_indices);
472                (x_sub, r_sub)
473            } else {
474                (x.clone(), residuals)
475            };
476
477            // Find best split for this iteration
478            let mut best_stump = None;
479            let mut best_improvement = f64::NEG_INFINITY;
480
481            for feature_idx in 0..n_features {
482                // Get unique values for this feature
483                let mut feature_values: Vec<f64> = x_sample.column(feature_idx).to_vec();
484                feature_values.sort_by(|a, b| {
485                    a.partial_cmp(b)
486                        .unwrap_or_else(|| match (a.is_nan(), b.is_nan()) {
487                            (true, false) => std::cmp::Ordering::Less,
488                            (false, true) => std::cmp::Ordering::Greater,
489                            _ => std::cmp::Ordering::Equal,
490                        })
491                });
492                feature_values.dedup();
493
494                // Try different thresholds
495                for i in 0..(feature_values.len().saturating_sub(1)) {
496                    let threshold = (feature_values[i] + feature_values[i + 1]) / 2.0;
497
498                    // Split data based on threshold
499                    let (left_indices, right_indices): (Vec<_>, Vec<_>) = (0..x_sample.nrows())
500                        .partition(|&j| x_sample[[j, feature_idx]] <= threshold);
501
502                    if left_indices.len() < self.min_samples_leaf
503                        || right_indices.len() < self.min_samples_leaf
504                    {
505                        continue;
506                    }
507
508                    // Compute values for left and right splits
509                    let left_value = if !left_indices.is_empty() {
510                        left_indices
511                            .iter()
512                            .map(|&idx| residuals_sample[idx])
513                            .sum::<f64>()
514                            / left_indices.len() as f64
515                    } else {
516                        0.0
517                    };
518
519                    let right_value = if !right_indices.is_empty() {
520                        right_indices
521                            .iter()
522                            .map(|&idx| residuals_sample[idx])
523                            .sum::<f64>()
524                            / right_indices.len() as f64
525                    } else {
526                        0.0
527                    };
528
529                    // Compute improvement (reduction in squared error)
530                    let total_var = residuals_sample.mapv(|x| x * x).sum();
531                    let left_var: f64 = left_indices
532                        .iter()
533                        .map(|&idx| (residuals_sample[idx] - left_value).powi(2))
534                        .sum();
535                    let right_var: f64 = right_indices
536                        .iter()
537                        .map(|&idx| (residuals_sample[idx] - right_value).powi(2))
538                        .sum();
539
540                    let improvement = total_var - left_var - right_var;
541
542                    if improvement > best_improvement {
543                        best_improvement = improvement;
544                        best_stump = Some(DecisionStump {
545                            feature_idx,
546                            threshold,
547                            left_value,
548                            right_value,
549                            importance: improvement,
550                        });
551                    }
552                }
553            }
554
555            if let Some(stump) = best_stump {
556                // Update feature importance
557                feature_importances[stump.feature_idx] += stump.importance;
558
559                // Update predictions
560                for i in 0..n_samples {
561                    let row = x.row(i);
562                    let prediction = stump.predict(&row.to_owned());
563                    predictions[i] += self.learning_rate * prediction;
564                }
565
566                estimators.push(stump);
567            }
568        }
569
570        // Normalize feature importances
571        let total_importance = feature_importances.sum();
572        if total_importance > 0.0 {
573            feature_importances /= total_importance;
574        }
575
576        // Select features based on threshold
577        let threshold = self
578            .threshold
579            .unwrap_or_else(|| feature_importances.mean().unwrap_or(0.0));
580
581        let mut selected_features: Vec<usize> = feature_importances
582            .iter()
583            .enumerate()
584            .filter(|(_, &importance)| importance > threshold)
585            .map(|(idx, _)| idx)
586            .collect();
587
588        // Apply max_features limit if specified
589        if let Some(max_feat) = self.max_features {
590            if selected_features.len() > max_feat {
591                // Sort by importance and take top max_feat
592                selected_features.sort_by(|&a, &b| {
593                    feature_importances[b]
594                        .partial_cmp(&feature_importances[a])
595                        .unwrap_or_else(|| {
596                            match (
597                                feature_importances[b].is_nan(),
598                                feature_importances[a].is_nan(),
599                            ) {
600                                (true, false) => std::cmp::Ordering::Less,
601                                (false, true) => std::cmp::Ordering::Greater,
602                                _ => std::cmp::Ordering::Equal,
603                            }
604                        })
605                });
606                selected_features.truncate(max_feat);
607                selected_features.sort(); // Restore original order
608            }
609        }
610
611        if selected_features.is_empty() {
612            return Err(SklearsError::InvalidInput(
613                "No features selected by Gradient Boosting. Try reducing threshold.".to_string(),
614            ));
615        }
616
617        Ok(GradientBoostingSelector {
618            n_estimators: self.n_estimators,
619            learning_rate: self.learning_rate,
620            max_depth: self.max_depth,
621            min_samples_split: self.min_samples_split,
622            min_samples_leaf: self.min_samples_leaf,
623            subsample: self.subsample,
624            threshold: self.threshold,
625            max_features: self.max_features,
626            random_state: self.random_state,
627            state: PhantomData,
628            feature_importances_: Some(feature_importances),
629            selected_features_: Some(selected_features),
630            n_features_: Some(n_features),
631            _estimators_: Some(estimators),
632        })
633    }
634}
635
636impl Transform<Array2<Float>> for GradientBoostingSelector<Trained> {
637    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
638        let n_features = self.n_features_.ok_or_else(|| {
639            SklearsError::FitError(
640                "GradientBoostingSelector not fitted: n_features_ missing".to_string(),
641            )
642        })?;
643        validate::check_n_features(x, n_features)?;
644
645        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
646            SklearsError::FitError(
647                "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
648            )
649        })?;
650        extract_features(x, selected_features)
651    }
652}
653
654impl SelectorMixin for GradientBoostingSelector<Trained> {
655    fn get_support(&self) -> SklResult<Array1<bool>> {
656        let n_features = self.n_features_.ok_or_else(|| {
657            SklearsError::FitError(
658                "GradientBoostingSelector not fitted: n_features_ missing".to_string(),
659            )
660        })?;
661        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
662            SklearsError::FitError(
663                "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
664            )
665        })?;
666        let mut support = Array1::from_elem(n_features, false);
667
668        for &idx in selected_features {
669            support[idx] = true;
670        }
671
672        Ok(support)
673    }
674
675    fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
676        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
677            SklearsError::FitError(
678                "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
679            )
680        })?;
681        Ok(indices
682            .iter()
683            .filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
684            .collect())
685    }
686}
687
688impl GradientBoostingSelector<Trained> {
689    /// Get feature importances
690    pub fn feature_importances(&self) -> SklResult<&Array1<Float>> {
691        self.feature_importances_.as_ref().ok_or_else(|| {
692            SklearsError::FitError(
693                "GradientBoostingSelector not fitted: feature_importances_ missing".to_string(),
694            )
695        })
696    }
697
698    /// Get selected features
699    pub fn selected_features(&self) -> SklResult<&[usize]> {
700        self.selected_features_.as_deref().ok_or_else(|| {
701            SklearsError::FitError(
702                "GradientBoostingSelector not fitted: selected_features_ missing".to_string(),
703            )
704        })
705    }
706
707    /// Get the trained estimators (decision stumps)
708    pub fn estimators(&self) -> SklResult<&[DecisionStump]> {
709        self._estimators_.as_deref().ok_or_else(|| {
710            SklearsError::FitError(
711                "GradientBoostingSelector not fitted: estimators_ missing".to_string(),
712            )
713        })
714    }
715}
716
717/// Tree-based importance selector for ensemble feature ranking
718#[derive(Debug, Clone)]
719pub struct TreeImportanceSelector {
720    n_estimators: usize,
721    max_depth: Option<usize>,
722    random_state: Option<u64>,
723}
724
725impl Default for TreeImportanceSelector {
726    fn default() -> Self {
727        Self::new()
728    }
729}
730
731impl TreeImportanceSelector {
732    /// new
733    pub fn new() -> Self {
734        Self {
735            n_estimators: 100,
736            max_depth: None,
737            random_state: None,
738        }
739    }
740
741    /// n_estimators
742    pub fn n_estimators(mut self, n_estimators: usize) -> Self {
743        self.n_estimators = n_estimators;
744        self
745    }
746
747    /// max_depth
748    pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
749        self.max_depth = max_depth;
750        self
751    }
752
753    /// random_state
754    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
755        self.random_state = random_state;
756        self
757    }
758}
759
760impl crate::ensemble_selectors::SelectorFunction for TreeImportanceSelector {
761    fn compute_scores(
762        &self,
763        x: &Array2<f64>,
764        y_classif: Option<&Array1<i32>>,
765        y_regression: Option<&Array1<f64>>,
766    ) -> SklResult<Array1<f64>> {
767        let n_features = x.ncols();
768        let n_samples = x.nrows();
769
770        // Simple random forest importance simulation
771        // In a real implementation, this would use actual tree algorithms
772        let mut rng = if let Some(seed) = self.random_state {
773            StdRng::seed_from_u64(seed)
774        } else {
775            StdRng::from_rng(&mut thread_rng())
776        };
777
778        let mut feature_importance = Array1::<f64>::zeros(n_features);
779
780        // Simulate feature importance by computing correlation-based importance
781        if let Some(y) = y_classif {
782            // For classification, use chi-squared like measure
783            for i in 0..n_features {
784                let feature_col = x.column(i);
785                let mut score = 0.0;
786
787                // Simple correlation measure
788                for j in 0..n_samples {
789                    let feature_val = feature_col[j];
790                    let target_val = y[j] as f64;
791                    score += (feature_val * target_val).abs();
792                }
793
794                feature_importance[i] = score / n_samples as f64;
795            }
796        } else if let Some(y) = y_regression {
797            // For regression, use correlation
798            for i in 0..n_features {
799                let feature_col = x.column(i);
800                let correlation = compute_pearson_correlation(&feature_col.to_owned(), y);
801                feature_importance[i] = correlation.abs();
802            }
803        } else {
804            return Err(SklearsError::InvalidInput(
805                "Need either classification or regression target".to_string(),
806            ));
807        }
808
809        // Add some randomness to simulate ensemble of trees
810        for importance in feature_importance.iter_mut() {
811            *importance += rng.random::<f64>() * 0.1; // Add small random component
812        }
813
814        Ok(feature_importance)
815    }
816
817    fn name(&self) -> &str {
818        "tree_importance"
819    }
820
821    fn clone_box(&self) -> Box<dyn crate::ensemble_selectors::SelectorFunction> {
822        Box::new(self.clone())
823    }
824}
825
826/// Helper function to compute Pearson correlation between two arrays
827fn compute_pearson_correlation(x: &Array1<f64>, y: &Array1<f64>) -> f64 {
828    let n = x.len() as f64;
829    if n < 2.0 {
830        return 0.0;
831    }
832
833    let mean_x = x.mean().unwrap_or(0.0);
834    let mean_y = y.mean().unwrap_or(0.0);
835
836    let mut numerator = 0.0;
837    let mut sum_sq_x = 0.0;
838    let mut sum_sq_y = 0.0;
839
840    for (&xi, &yi) in x.iter().zip(y.iter()) {
841        let dx = xi - mean_x;
842        let dy = yi - mean_y;
843        numerator += dx * dy;
844        sum_sq_x += dx * dx;
845        sum_sq_y += dy * dy;
846    }
847
848    let denominator = (sum_sq_x * sum_sq_y).sqrt();
849    if denominator == 0.0 {
850        0.0
851    } else {
852        numerator / denominator
853    }
854}
855
856/// Extract selected features from the data matrix
857fn extract_features(x: &Array2<Float>, features: &[usize]) -> SklResult<Array2<Float>> {
858    let n_samples = x.nrows();
859    let n_selected = features.len();
860    let mut x_subset = Array2::zeros((n_samples, n_selected));
861
862    for (new_idx, &old_idx) in features.iter().enumerate() {
863        if old_idx >= x.ncols() {
864            return Err(SklearsError::InvalidInput(format!(
865                "Feature index {} out of bounds",
866                old_idx
867            )));
868        }
869        x_subset.column_mut(new_idx).assign(&x.column(old_idx));
870    }
871
872    Ok(x_subset)
873}