Skip to main content

sklears_feature_selection/
wrapper.rs

1//! Wrapper-based feature selection methods
2
3use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
4use sklears_core::{
5    error::{validate, Result as SklResult, SklearsError},
6    traits::{Estimator, Fit, Score, Trained, Transform, Untrained},
7    types::Float,
8};
9// use sklears_model_selection::{CrossValidator, KFold}; // Removed dependency
10
11/// Simple cross-validator trait replacement
12pub trait CrossValidator: Send + Sync {
13    /// n_samples
14    fn split(&self, n_samples: usize) -> Vec<(Vec<usize>, Vec<usize>)>;
15}
16
17/// Simple KFold implementation replacement
18#[derive(Debug, Clone)]
19pub struct KFold {
20    n_splits: usize,
21    shuffle: bool,
22    random_state: Option<u64>,
23}
24
25impl KFold {
26    /// new
27    pub fn new(n_splits: usize) -> Self {
28        Self {
29            n_splits,
30            shuffle: false,
31            random_state: None,
32        }
33    }
34
35    /// shuffle
36    pub fn shuffle(mut self, shuffle: bool) -> Self {
37        self.shuffle = shuffle;
38        self
39    }
40
41    /// random_state
42    pub fn random_state(mut self, random_state: u64) -> Self {
43        self.random_state = Some(random_state);
44        self
45    }
46}
47
48impl CrossValidator for KFold {
49    fn split(&self, n_samples: usize) -> Vec<(Vec<usize>, Vec<usize>)> {
50        let mut indices: Vec<usize> = (0..n_samples).collect();
51
52        if self.shuffle {
53            use scirs2_core::rand_prelude::SliceRandom;
54            use scirs2_core::random::rngs::StdRng;
55            use scirs2_core::random::SeedableRng;
56            let mut rng = if let Some(seed) = self.random_state {
57                StdRng::seed_from_u64(seed)
58            } else {
59                StdRng::seed_from_u64(42)
60            };
61            indices.shuffle(&mut rng);
62        }
63
64        let mut splits = Vec::new();
65        let fold_size = n_samples / self.n_splits;
66
67        for i in 0..self.n_splits {
68            let test_start = i * fold_size;
69            let test_end = if i == self.n_splits - 1 {
70                n_samples
71            } else {
72                (i + 1) * fold_size
73            };
74
75            let test_indices: Vec<usize> = indices[test_start..test_end].to_vec();
76            let train_indices: Vec<usize> = indices[0..test_start]
77                .iter()
78                .chain(indices[test_end..].iter())
79                .cloned()
80                .collect();
81
82            splits.push((train_indices, test_indices));
83        }
84
85        splits
86    }
87}
88use std::marker::PhantomData;
89
90/// Helper function to compare floats safely
91fn compare_floats(a: &Float, b: &Float) -> std::cmp::Ordering {
92    a.partial_cmp(b).unwrap_or_else(|| {
93        // Handle NaN cases: NaN is considered less than any number
94        match (a.is_nan(), b.is_nan()) {
95            (true, false) => std::cmp::Ordering::Less,
96            (false, true) => std::cmp::Ordering::Greater,
97            _ => std::cmp::Ordering::Equal,
98        }
99    })
100}
101
102/// Trait for target arrays that can be indexed
103pub trait IndexableTarget: Clone {
104    /// Get elements at the specified indices
105    fn select(&self, indices: &[usize]) -> Self;
106}
107
108// Implement for fully expanded ndarray 0.17 1D arrays (explicit 3rd type parameter)
109// ndarray 0.17 added `A = <S as RawData>::Elem` as a 3rd default param to ArrayBase.
110// We implement for each concrete element type used by the Fit impls.
111// Note: we use Array1::from_iter to avoid name collision with ndarray's own `select` method.
112impl IndexableTarget for ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64> {
113    fn select(&self, indices: &[usize]) -> Self {
114        Array1::from_iter(indices.iter().map(|&i| self[i]))
115    }
116}
117
118impl IndexableTarget for ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32> {
119    fn select(&self, indices: &[usize]) -> Self {
120        Array1::from_iter(indices.iter().map(|&i| self[i]))
121    }
122}
123
124impl IndexableTarget for ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize> {
125    fn select(&self, indices: &[usize]) -> Self {
126        Array1::from_iter(indices.iter().map(|&i| self[i]))
127    }
128}
129
130/// Trait for estimators that can provide feature importance scores
131pub trait FeatureImportance {
132    /// Get feature importance scores
133    /// Higher scores indicate more important features
134    fn feature_importances(&self) -> SklResult<Array1<Float>>;
135}
136
137/// Trait for estimators that have linear coefficients
138pub trait HasCoefficients {
139    /// Get the coefficients
140    fn coef(&self) -> SklResult<Array1<Float>>;
141}
142
143// Implement FeatureImportance for estimators with coefficients
144impl<T> FeatureImportance for T
145where
146    T: HasCoefficients,
147{
148    fn feature_importances(&self) -> SklResult<Array1<Float>> {
149        // Use absolute values of coefficients as importance scores
150        self.coef().map(|coef| coef.mapv(|x| x.abs()))
151    }
152}
153
154/// Recursive Feature Elimination
155pub struct RFE<E, State = Untrained> {
156    estimator: E,
157    n_features_to_select: Option<usize>,
158    step: f64,
159    state: PhantomData<State>,
160    // Trained state
161    support_: Option<Array1<bool>>,
162    ranking_: Option<Array1<usize>>,
163    n_features_: Option<usize>,
164}
165
166impl<E: Clone> RFE<E, Untrained> {
167    /// Create a new RFE selector
168    pub fn new(estimator: E) -> Self {
169        Self {
170            estimator,
171            n_features_to_select: None,
172            step: 1.0,
173            state: PhantomData,
174            support_: None,
175            ranking_: None,
176            n_features_: None,
177        }
178    }
179
180    /// Set the number of features to select
181    pub fn n_features_to_select(mut self, n: usize) -> Self {
182        self.n_features_to_select = Some(n);
183        self
184    }
185
186    /// Set the step size for feature elimination
187    /// If step >= 1, remove step features at each iteration
188    /// If 0 < step < 1, remove step * n_features features at each iteration
189    pub fn step(mut self, step: f64) -> Result<Self, SklearsError> {
190        if step <= 0.0 {
191            return Err(SklearsError::InvalidInput("step must be > 0".to_string()));
192        }
193        self.step = step;
194        Ok(self)
195    }
196}
197
198impl<E> Estimator for RFE<E, Untrained> {
199    type Config = ();
200    type Error = SklearsError;
201    type Float = f64;
202
203    fn config(&self) -> &Self::Config {
204        &()
205    }
206}
207
208/// Type aliases for fully expanded ndarray 0.17 array types with explicit 3rd type parameter
209/// The 3rd type parameter was added in ndarray 0.17 as `A = <S as RawData>::Elem`.
210/// Without explicit 3rd param, the Rust trait solver fails to normalize in generic where bounds.
211type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
212type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
213type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
214type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
215
216macro_rules! impl_fit_for_rfe {
217    ($y_type:ty) => {
218        impl<E> Fit<Mat2f64, $y_type> for RFE<E, Untrained>
219        where
220            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
221            E::Fitted: FeatureImportance + Send + Sync,
222        {
223            type Fitted = RFE<E, Trained>;
224
225            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
226                let n_features = x.ncols();
227                let n_features_to_select = self.n_features_to_select.unwrap_or(n_features / 2);
228
229                if n_features_to_select > n_features {
230                    return Err(SklearsError::InvalidInput(format!(
231                        "n_features_to_select ({}) must be <= n_features ({})",
232                        n_features_to_select, n_features
233                    )));
234                }
235
236                let mut support = Array1::from_elem(n_features, true);
237                let mut ranking = Array1::ones(n_features);
238                let mut current_n_features = n_features;
239                let mut current_step = 1;
240
241                while current_n_features > n_features_to_select {
242                    let features_mask: Vec<usize> =
243                        (0..n_features).filter(|&i| support[i]).collect();
244                    let x_subset = x.select(Axis(1), &features_mask);
245                    let fitted_estimator = self.estimator.clone().fit(&x_subset, y)?;
246                    let importances = fitted_estimator.feature_importances()?;
247
248                    let n_features_to_remove = if self.step >= 1.0 {
249                        self.step as usize
250                    } else {
251                        ((current_n_features as f64) * self.step).max(1.0) as usize
252                    };
253                    let n_features_to_remove =
254                        n_features_to_remove.min(current_n_features - n_features_to_select);
255
256                    let mut importance_indices: Vec<(usize, Float)> = importances
257                        .iter()
258                        .enumerate()
259                        .map(|(i, &imp)| (i, imp))
260                        .collect();
261                    importance_indices.sort_by(|a, b| compare_floats(&a.1, &b.1));
262
263                    for i in 0..n_features_to_remove {
264                        let feature_idx = features_mask[importance_indices[i].0];
265                        support[feature_idx] = false;
266                        ranking[feature_idx] = current_step;
267                    }
268
269                    current_n_features -= n_features_to_remove;
270                    current_step += 1;
271                }
272
273                let max_rank = *ranking.iter().max().ok_or_else(|| {
274                    SklearsError::InvalidState("Ranking array is empty".to_string())
275                })?;
276                for i in 0..n_features {
277                    if !support[i] {
278                        ranking[i] = max_rank + 2 - ranking[i];
279                    }
280                }
281
282                Ok(RFE {
283                    estimator: self.estimator,
284                    n_features_to_select: self.n_features_to_select,
285                    step: self.step,
286                    state: PhantomData,
287                    support_: Some(support),
288                    ranking_: Some(ranking),
289                    n_features_: Some(n_features),
290                })
291            }
292        }
293    };
294}
295
296impl_fit_for_rfe!(Vec1f64);
297impl_fit_for_rfe!(Vec1i32);
298impl_fit_for_rfe!(Vec1usize);
299
300impl<E> Transform<Array2<Float>> for RFE<E, Trained> {
301    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
302        let n_features = self.n_features_.ok_or_else(|| {
303            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
304        })?;
305        validate::check_n_features(x, n_features)?;
306
307        let support = self.support_.as_ref().ok_or_else(|| {
308            SklearsError::InvalidState("Model not trained: support_ is None".to_string())
309        })?;
310        let selected_features: Vec<usize> = (0..support.len()).filter(|&i| support[i]).collect();
311
312        Ok(x.select(Axis(1), &selected_features))
313    }
314}
315
316impl<E> RFE<E, Trained> {
317    /// Get the support mask
318    pub fn support(&self) -> SklResult<&Array1<bool>> {
319        self.support_.as_ref().ok_or_else(|| {
320            SklearsError::InvalidState("Model not trained: support_ is None".to_string())
321        })
322    }
323
324    /// Get the feature ranking
325    pub fn ranking(&self) -> SklResult<&Array1<usize>> {
326        self.ranking_.as_ref().ok_or_else(|| {
327            SklearsError::InvalidState("Model not trained: ranking_ is None".to_string())
328        })
329    }
330}
331
332/// Recursive Feature Elimination with Cross-Validation
333pub struct RFECV<E, State = Untrained> {
334    estimator: E,
335    step: f64,
336    min_features_to_select: usize,
337    cv: Option<Box<dyn CrossValidator>>,
338    n_jobs: Option<i32>,
339    state: PhantomData<State>,
340    // Trained state
341    support_: Option<Array1<bool>>,
342    ranking_: Option<Array1<usize>>,
343    n_features_: Option<usize>,
344    cv_results_: Option<RFECVResults>,
345}
346
347#[derive(Debug, Clone)]
348/// RFECVResults
349pub struct RFECVResults {
350    /// mean_test_scores
351    pub mean_test_scores: Array1<Float>,
352    /// std_test_scores
353    pub std_test_scores: Array1<Float>,
354    /// n_features
355    pub n_features: Array1<usize>,
356}
357
358impl<E: Clone> RFECV<E, Untrained> {
359    /// Create a new RFECV selector
360    pub fn new(estimator: E) -> Self {
361        Self {
362            estimator,
363            step: 1.0,
364            min_features_to_select: 1,
365            cv: None,
366            n_jobs: None,
367            state: PhantomData,
368            support_: None,
369            ranking_: None,
370            n_features_: None,
371            cv_results_: None,
372        }
373    }
374
375    /// Set the minimum number of features to consider
376    pub fn min_features_to_select(mut self, min_features: usize) -> Result<Self, SklearsError> {
377        if min_features < 1 {
378            return Err(SklearsError::InvalidInput(
379                "min_features_to_select must be >= 1".to_string(),
380            ));
381        }
382        self.min_features_to_select = min_features;
383        Ok(self)
384    }
385
386    /// Set the step size for feature elimination
387    pub fn step(mut self, step: f64) -> Result<Self, SklearsError> {
388        if step <= 0.0 {
389            return Err(SklearsError::InvalidInput("step must be > 0".to_string()));
390        }
391        self.step = step;
392        Ok(self)
393    }
394
395    /// Set the cross-validation strategy
396    pub fn cv(mut self, cv: Box<dyn CrossValidator>) -> Self {
397        self.cv = Some(cv);
398        self
399    }
400}
401
402impl<E> Estimator for RFECV<E, Untrained> {
403    type Config = ();
404    type Error = SklearsError;
405    type Float = f64;
406
407    fn config(&self) -> &Self::Config {
408        &()
409    }
410}
411
412macro_rules! impl_fit_for_rfecv {
413    ($y_type:ty) => {
414        impl<E> Fit<Mat2f64, $y_type> for RFECV<E, Untrained>
415        where
416            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
417            E::Fitted: FeatureImportance + Score<Mat2f64, $y_type, Float = f64> + Send + Sync,
418            $y_type: IndexableTarget,
419        {
420            type Fitted = RFECV<E, Trained>;
421
422            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
423                let n_features = x.ncols();
424
425                if self.min_features_to_select > n_features {
426                    return Err(SklearsError::InvalidInput(format!(
427                        "min_features_to_select ({}) must be <= n_features ({})",
428                        self.min_features_to_select, n_features
429                    )));
430                }
431
432                let default_cv = KFold::new(5);
433                let cv: &dyn CrossValidator = self
434                    .cv
435                    .as_ref()
436                    .map(|cv| cv.as_ref())
437                    .unwrap_or(&default_cv);
438
439                let mut mean_scores: Vec<f64> = Vec::new();
440                let mut std_scores: Vec<f64> = Vec::new();
441                let mut n_features_list: Vec<usize> = Vec::new();
442
443                let mut current_n_features = n_features;
444                let mut support = Array1::from_elem(n_features, true);
445                let mut ranking: Array1<usize> = Array1::ones(n_features);
446                let mut current_step: usize = 1;
447
448                while current_n_features >= self.min_features_to_select {
449                    let features_mask: Vec<usize> =
450                        (0..n_features).filter(|&i| support[i]).collect();
451                    let x_subset = x.select(Axis(1), &features_mask);
452                    let splits = cv.split(x.nrows());
453                    let mut fold_scores: Vec<f64> = Vec::new();
454
455                    for (train_idx, test_idx) in splits {
456                        let x_train = x_subset.select(Axis(0), &train_idx);
457                        let x_test = x_subset.select(Axis(0), &test_idx);
458                        let y_train = y.select(&train_idx);
459                        let y_test = y.select(&test_idx);
460
461                        let fitted = self.estimator.clone().fit(&x_train, &y_train)?;
462                        let score = fitted.score(&x_test, &y_test)?;
463                        fold_scores.push(score);
464                    }
465
466                    fold_scores.retain(|score| score.is_finite());
467
468                    if fold_scores.is_empty() {
469                        mean_scores.push(f64::NEG_INFINITY);
470                        std_scores.push(0.0);
471                    } else {
472                        let mean_score = fold_scores.iter().sum::<f64>() / fold_scores.len() as f64;
473                        let variance = fold_scores
474                            .iter()
475                            .map(|&score| (score - mean_score).powi(2))
476                            .sum::<f64>()
477                            / fold_scores.len() as f64;
478                        let std_score = variance.sqrt();
479                        mean_scores.push(mean_score);
480                        std_scores.push(std_score);
481                    }
482                    n_features_list.push(current_n_features);
483
484                    if current_n_features == self.min_features_to_select {
485                        break;
486                    }
487
488                    let fitted_estimator = self.estimator.clone().fit(&x_subset, y)?;
489                    let importances = fitted_estimator.feature_importances()?;
490
491                    let n_features_to_remove = if self.step >= 1.0 {
492                        self.step as usize
493                    } else {
494                        ((current_n_features as f64) * self.step).max(1.0) as usize
495                    };
496                    let n_features_to_remove =
497                        n_features_to_remove.min(current_n_features - self.min_features_to_select);
498
499                    if n_features_to_remove == 0 {
500                        break;
501                    }
502
503                    let mut importance_indices: Vec<(usize, Float)> = importances
504                        .iter()
505                        .enumerate()
506                        .map(|(i, &imp)| (i, imp))
507                        .collect();
508                    importance_indices.sort_by(|a, b| compare_floats(&a.1, &b.1));
509
510                    for i in 0..n_features_to_remove {
511                        let feature_idx = features_mask[importance_indices[i].0];
512                        support[feature_idx] = false;
513                        ranking[feature_idx] = current_step;
514                    }
515
516                    current_n_features -= n_features_to_remove;
517                    current_step += 1;
518                }
519
520                use std::cmp::Ordering;
521
522                let best_idx = mean_scores
523                    .iter()
524                    .enumerate()
525                    .filter(|(_, score)| score.is_finite())
526                    .max_by(
527                        |(idx_a, score_a), (idx_b, score_b)| match score_a.total_cmp(score_b) {
528                            Ordering::Equal => {
529                                n_features_list[*idx_a].cmp(&n_features_list[*idx_b])
530                            }
531                            other => other,
532                        },
533                    )
534                    .or_else(|| {
535                        mean_scores.iter().enumerate().max_by(
536                            |(idx_a, score_a), (idx_b, score_b)| match (
537                                score_a.is_nan(),
538                                score_b.is_nan(),
539                            ) {
540                                (true, true) => {
541                                    n_features_list[*idx_a].cmp(&n_features_list[*idx_b])
542                                }
543                                (true, false) => Ordering::Less,
544                                (false, true) => Ordering::Greater,
545                                (false, false) => match score_a.total_cmp(score_b) {
546                                    Ordering::Equal => {
547                                        n_features_list[*idx_a].cmp(&n_features_list[*idx_b])
548                                    }
549                                    other => other,
550                                },
551                            },
552                        )
553                    })
554                    .map(|(idx, _)| idx)
555                    .ok_or_else(|| {
556                        SklearsError::FitError(
557                            "RFECV could not compute any valid cross-validation scores".to_string(),
558                        )
559                    })?;
560                let optimal_n_features = n_features_list[best_idx];
561
562                support = Array1::from_elem(n_features, false);
563                let sorted_ranking: Vec<(usize, usize)> =
564                    ranking.iter().enumerate().map(|(i, &r)| (i, r)).collect();
565                let mut sorted_by_rank = sorted_ranking;
566                sorted_by_rank.sort_by_key(|&(_, r)| r);
567
568                for i in 0..optimal_n_features {
569                    support[sorted_by_rank[i].0] = true;
570                }
571
572                let max_rank = *ranking.iter().max().ok_or_else(|| {
573                    SklearsError::InvalidState("Ranking array is empty".to_string())
574                })?;
575                for i in 0..n_features {
576                    if !support[i] {
577                        ranking[i] = max_rank + 2 - ranking[i];
578                    }
579                }
580
581                let cv_results = RFECVResults {
582                    mean_test_scores: Array1::from_vec(mean_scores),
583                    std_test_scores: Array1::from_vec(std_scores),
584                    n_features: Array1::from_vec(n_features_list),
585                };
586
587                Ok(RFECV {
588                    estimator: self.estimator,
589                    step: self.step,
590                    min_features_to_select: self.min_features_to_select,
591                    cv: self.cv,
592                    n_jobs: self.n_jobs,
593                    state: PhantomData,
594                    support_: Some(support),
595                    ranking_: Some(ranking),
596                    n_features_: Some(n_features),
597                    cv_results_: Some(cv_results),
598                })
599            }
600        }
601    };
602}
603
604impl_fit_for_rfecv!(Vec1f64);
605impl_fit_for_rfecv!(Vec1i32);
606impl_fit_for_rfecv!(Vec1usize);
607
608impl<E> Transform<Array2<Float>> for RFECV<E, Trained> {
609    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
610        let n_features = self.n_features_.ok_or_else(|| {
611            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
612        })?;
613        validate::check_n_features(x, n_features)?;
614
615        let support = self.support_.as_ref().ok_or_else(|| {
616            SklearsError::InvalidState("Model not trained: support_ is None".to_string())
617        })?;
618        let selected_features: Vec<usize> = (0..support.len()).filter(|&i| support[i]).collect();
619
620        Ok(x.select(Axis(1), &selected_features))
621    }
622}
623
624impl<E> RFECV<E, Trained> {
625    /// Get the support mask
626    pub fn support(&self) -> SklResult<&Array1<bool>> {
627        self.support_.as_ref().ok_or_else(|| {
628            SklearsError::InvalidState("Model not trained: support_ is None".to_string())
629        })
630    }
631
632    /// Get the feature ranking
633    pub fn ranking(&self) -> SklResult<&Array1<usize>> {
634        self.ranking_.as_ref().ok_or_else(|| {
635            SklearsError::InvalidState("Model not trained: ranking_ is None".to_string())
636        })
637    }
638
639    /// Get cross-validation results
640    pub fn cv_results(&self) -> SklResult<&RFECVResults> {
641        self.cv_results_.as_ref().ok_or_else(|| {
642            SklearsError::InvalidState("Model not trained: cv_results_ is None".to_string())
643        })
644    }
645}
646
647/// Select features from any estimator with feature importances
648#[derive(Debug, Clone)]
649pub struct SelectFromModel<E, State = Untrained> {
650    estimator: E,
651    threshold: Option<f64>,
652    max_features: Option<usize>,
653    state: PhantomData<State>,
654    // Trained state
655    _estimator_: Option<E>,
656    selected_features_: Option<Vec<usize>>,
657    n_features_: Option<usize>,
658    threshold_: Option<f64>,
659}
660
661impl<E: Clone> SelectFromModel<E, Untrained> {
662    /// Create a new SelectFromModel selector
663    pub fn new(estimator: E) -> Self {
664        Self {
665            estimator,
666            threshold: None,
667            max_features: None,
668            state: PhantomData,
669            _estimator_: None,
670            selected_features_: None,
671            n_features_: None,
672            threshold_: None,
673        }
674    }
675
676    /// Set the threshold for feature selection
677    /// If None, will use median of feature importances
678    pub fn threshold(mut self, threshold: f64) -> Self {
679        self.threshold = Some(threshold);
680        self
681    }
682
683    /// Set the maximum number of features to select
684    pub fn max_features(mut self, max_features: usize) -> Self {
685        self.max_features = Some(max_features);
686        self
687    }
688}
689
690impl<E> Estimator for SelectFromModel<E, Untrained> {
691    type Config = ();
692    type Error = SklearsError;
693    type Float = f64;
694
695    fn config(&self) -> &Self::Config {
696        &()
697    }
698}
699
700macro_rules! impl_fit_for_select_from_model {
701    ($y_type:ty) => {
702        impl<E> Fit<Mat2f64, $y_type> for SelectFromModel<E, Untrained>
703        where
704            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
705            E::Fitted: FeatureImportance + Send + Sync,
706        {
707            type Fitted = SelectFromModel<E, Trained>;
708
709            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
710                let n_features = x.ncols();
711                let fitted_estimator = self.estimator.clone().fit(x, y)?;
712                let importances = fitted_estimator.feature_importances()?;
713
714                let threshold = if let Some(thresh) = self.threshold {
715                    thresh
716                } else {
717                    let mut sorted_importances = importances.to_vec();
718                    sorted_importances.sort_by(compare_floats);
719                    sorted_importances[sorted_importances.len() / 2]
720                };
721
722                let mut selected_features: Vec<usize> = (0..n_features)
723                    .filter(|&i| importances[i] > threshold)
724                    .collect();
725
726                if let Some(max_feat) = self.max_features {
727                    if selected_features.len() > max_feat {
728                        selected_features
729                            .sort_by(|&a, &b| compare_floats(&importances[b], &importances[a]));
730                        selected_features.truncate(max_feat);
731                        selected_features.sort();
732                    }
733                }
734
735                if selected_features.is_empty() {
736                    return Err(SklearsError::InvalidInput(format!(
737                        "No features selected with threshold={}",
738                        threshold
739                    )));
740                }
741
742                Ok(SelectFromModel {
743                    estimator: self.estimator.clone(),
744                    threshold: self.threshold,
745                    max_features: self.max_features,
746                    state: PhantomData,
747                    _estimator_: Some(self.estimator),
748                    selected_features_: Some(selected_features),
749                    n_features_: Some(n_features),
750                    threshold_: Some(threshold),
751                })
752            }
753        }
754    };
755}
756
757impl_fit_for_select_from_model!(Vec1f64);
758impl_fit_for_select_from_model!(Vec1i32);
759impl_fit_for_select_from_model!(Vec1usize);
760
761impl<E> Transform<Array2<Float>> for SelectFromModel<E, Trained> {
762    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
763        let n_features = self.n_features_.ok_or_else(|| {
764            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
765        })?;
766        validate::check_n_features(x, n_features)?;
767
768        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
769            SklearsError::InvalidState("Model not trained: selected_features_ is None".to_string())
770        })?;
771        let n_samples = x.nrows();
772        let k = selected_features.len();
773        let mut x_new = Array2::zeros((n_samples, k));
774
775        for (new_idx, &old_idx) in selected_features.iter().enumerate() {
776            x_new.column_mut(new_idx).assign(&x.column(old_idx));
777        }
778
779        Ok(x_new)
780    }
781}
782
783impl<E> SelectFromModel<E, Trained> {
784    /// Get the support mask
785    pub fn get_support(&self) -> SklResult<Array1<bool>> {
786        let n_features = self.n_features_.ok_or_else(|| {
787            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
788        })?;
789        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
790            SklearsError::InvalidState("Model not trained: selected_features_ is None".to_string())
791        })?;
792        let mut support = Array1::from_elem(n_features, false);
793
794        for &idx in selected_features {
795            support[idx] = true;
796        }
797
798        Ok(support)
799    }
800
801    /// Get the threshold used for selection
802    pub fn threshold(&self) -> SklResult<f64> {
803        self.threshold_.ok_or_else(|| {
804            SklearsError::InvalidState("Model not trained: threshold_ is None".to_string())
805        })
806    }
807}
808
809/// Sequential Feature Selection (forward, backward, or bidirectional)
810///
811/// Implements greedy, cross-validation-based sequential feature selection.
812/// Uses the concrete-Fit macro pattern to avoid ndarray 0.17 HRTB issues
813/// that arise with fully-generic `Y` parameters in where bounds.
814#[derive(Debug, Clone)]
815pub struct SequentialFeatureSelector<E, State = Untrained> {
816    estimator: E,
817    n_features_to_select: Option<usize>,
818    direction: String,
819    cv_folds: usize,
820    state: PhantomData<State>,
821    // Trained state
822    selected_features_: Option<Vec<usize>>,
823    n_features_: Option<usize>,
824}
825
826impl<E: Clone> SequentialFeatureSelector<E, Untrained> {
827    /// Create a new SequentialFeatureSelector
828    pub fn new(estimator: E) -> Self {
829        Self {
830            estimator,
831            n_features_to_select: None,
832            direction: "forward".to_string(),
833            cv_folds: 5,
834            state: PhantomData,
835            selected_features_: None,
836            n_features_: None,
837        }
838    }
839
840    /// Set the number of features to select
841    pub fn n_features_to_select(mut self, n: usize) -> Self {
842        self.n_features_to_select = Some(n);
843        self
844    }
845
846    /// Set the direction ("forward", "backward", or "bidirectional")
847    pub fn direction(mut self, direction: &str) -> Result<Self, SklearsError> {
848        if direction != "forward" && direction != "backward" && direction != "bidirectional" {
849            return Err(SklearsError::InvalidInput(
850                "direction must be 'forward', 'backward', or 'bidirectional'".to_string(),
851            ));
852        }
853        self.direction = direction.to_string();
854        Ok(self)
855    }
856
857    /// Set the number of CV folds
858    pub fn cv(mut self, cv_folds: usize) -> Self {
859        self.cv_folds = cv_folds;
860        self
861    }
862}
863
864impl<E> Estimator for SequentialFeatureSelector<E, Untrained> {
865    type Config = ();
866    type Error = SklearsError;
867    type Float = f64;
868
869    fn config(&self) -> &Self::Config {
870        &()
871    }
872}
873
874// Helper: evaluate a feature subset via cross-validation
875fn sfs_evaluate_features<E, Y>(
876    estimator: &E,
877    x: &Mat2f64,
878    y: &Y,
879    features: &[usize],
880    cv: &KFold,
881) -> SklResult<f64>
882where
883    E: Clone + Fit<Mat2f64, Y>,
884    E::Fitted: Score<Mat2f64, Y, Float = f64>,
885    Y: IndexableTarget + Clone,
886{
887    if features.is_empty() {
888        return Ok(f64::NEG_INFINITY);
889    }
890
891    let x_subset = x.select(Axis(1), features);
892    let splits = cv.split(x.nrows());
893    let mut scores = Vec::new();
894
895    for (train_idx, test_idx) in splits {
896        let x_train = x_subset.select(Axis(0), &train_idx);
897        let x_test = x_subset.select(Axis(0), &test_idx);
898        let y_train = y.select(&train_idx);
899        let y_test = y.select(&test_idx);
900
901        let fitted = estimator.clone().fit(&x_train, &y_train)?;
902        let score = fitted.score(&x_test, &y_test)?;
903        scores.push(score);
904    }
905
906    if scores.is_empty() {
907        return Ok(f64::NEG_INFINITY);
908    }
909    let sum: f64 = scores.iter().sum();
910    Ok(sum / scores.len() as f64)
911}
912
913// Helper: forward selection
914fn sfs_forward_selection<E, Y>(
915    estimator: &E,
916    x: &Mat2f64,
917    y: &Y,
918    n_features_to_select: usize,
919    cv: &KFold,
920) -> SklResult<Vec<usize>>
921where
922    E: Clone + Fit<Mat2f64, Y>,
923    E::Fitted: Score<Mat2f64, Y, Float = f64>,
924    Y: IndexableTarget + Clone,
925{
926    let n_features = x.ncols();
927    let mut selected = Vec::new();
928    let mut remaining: Vec<usize> = (0..n_features).collect();
929
930    while selected.len() < n_features_to_select {
931        let mut best_score = f64::NEG_INFINITY;
932        let mut best_feature: Option<usize> = None;
933
934        for &candidate in &remaining {
935            let mut test_features = selected.clone();
936            test_features.push(candidate);
937            test_features.sort();
938
939            let score = sfs_evaluate_features(estimator, x, y, &test_features, cv)?;
940            if score > best_score {
941                best_score = score;
942                best_feature = Some(candidate);
943            }
944        }
945
946        match best_feature {
947            Some(feature) => {
948                selected.push(feature);
949                remaining.retain(|&f| f != feature);
950            }
951            None => break,
952        }
953    }
954
955    selected.sort();
956    Ok(selected)
957}
958
959// Helper: backward elimination
960fn sfs_backward_elimination<E, Y>(
961    estimator: &E,
962    x: &Mat2f64,
963    y: &Y,
964    n_features_to_select: usize,
965    cv: &KFold,
966) -> SklResult<Vec<usize>>
967where
968    E: Clone + Fit<Mat2f64, Y>,
969    E::Fitted: Score<Mat2f64, Y, Float = f64>,
970    Y: IndexableTarget + Clone,
971{
972    let n_features = x.ncols();
973    let mut selected: Vec<usize> = (0..n_features).collect();
974
975    while selected.len() > n_features_to_select {
976        let mut best_score = f64::NEG_INFINITY;
977        let mut worst_feature: Option<usize> = None;
978
979        for &candidate in &selected {
980            let test_features: Vec<usize> = selected
981                .iter()
982                .cloned()
983                .filter(|&f| f != candidate)
984                .collect();
985            let score = sfs_evaluate_features(estimator, x, y, &test_features, cv)?;
986            if score > best_score {
987                best_score = score;
988                worst_feature = Some(candidate);
989            }
990        }
991
992        match worst_feature {
993            Some(feature) => {
994                selected.retain(|&f| f != feature);
995            }
996            None => break,
997        }
998    }
999
1000    selected.sort();
1001    Ok(selected)
1002}
1003
1004// Helper: bidirectional selection
1005fn sfs_bidirectional_selection<E, Y>(
1006    estimator: &E,
1007    x: &Mat2f64,
1008    y: &Y,
1009    n_features_to_select: usize,
1010    cv: &KFold,
1011) -> SklResult<Vec<usize>>
1012where
1013    E: Clone + Fit<Mat2f64, Y>,
1014    E::Fitted: Score<Mat2f64, Y, Float = f64>,
1015    Y: IndexableTarget + Clone,
1016{
1017    #[derive(Debug, Clone)]
1018    enum SfsAction {
1019        Add(usize),
1020        Remove(usize),
1021        Swap(usize, usize),
1022    }
1023
1024    let n_features = x.ncols();
1025    let mut selected: Vec<usize> = Vec::new();
1026    let mut remaining: Vec<usize> = (0..n_features).collect();
1027
1028    while selected.len() != n_features_to_select {
1029        let mut best_action: Option<SfsAction> = None;
1030        let mut best_score = f64::NEG_INFINITY;
1031
1032        // Consider adding features when below target
1033        if selected.len() < n_features_to_select {
1034            for &candidate in &remaining {
1035                let mut test_features = selected.clone();
1036                test_features.push(candidate);
1037                test_features.sort();
1038
1039                let score = sfs_evaluate_features(estimator, x, y, &test_features, cv)?;
1040                if score > best_score {
1041                    best_score = score;
1042                    best_action = Some(SfsAction::Add(candidate));
1043                }
1044            }
1045        }
1046
1047        // Consider removing features when above target
1048        if selected.len() > n_features_to_select && !selected.is_empty() {
1049            for &candidate in &selected {
1050                let test_features: Vec<usize> = selected
1051                    .iter()
1052                    .cloned()
1053                    .filter(|&f| f != candidate)
1054                    .collect();
1055                let score = sfs_evaluate_features(estimator, x, y, &test_features, cv)?;
1056                if score > best_score {
1057                    best_score = score;
1058                    best_action = Some(SfsAction::Remove(candidate));
1059                }
1060            }
1061        }
1062
1063        // Consider swapping features when at target size
1064        if selected.len() == n_features_to_select && !remaining.is_empty() {
1065            for &add_candidate in &remaining {
1066                for &remove_candidate in &selected {
1067                    let mut test_features: Vec<usize> = selected
1068                        .iter()
1069                        .cloned()
1070                        .filter(|&f| f != remove_candidate)
1071                        .collect();
1072                    test_features.push(add_candidate);
1073                    test_features.sort();
1074
1075                    let score = sfs_evaluate_features(estimator, x, y, &test_features, cv)?;
1076                    if score > best_score {
1077                        best_score = score;
1078                        best_action = Some(SfsAction::Swap(add_candidate, remove_candidate));
1079                    }
1080                }
1081            }
1082        }
1083
1084        match best_action {
1085            Some(SfsAction::Add(feature)) => {
1086                selected.push(feature);
1087                remaining.retain(|&f| f != feature);
1088            }
1089            Some(SfsAction::Remove(feature)) => {
1090                selected.retain(|&f| f != feature);
1091                remaining.push(feature);
1092                remaining.sort();
1093            }
1094            Some(SfsAction::Swap(add_feature, remove_feature)) => {
1095                selected.retain(|&f| f != remove_feature);
1096                selected.push(add_feature);
1097                remaining.retain(|&f| f != add_feature);
1098                remaining.push(remove_feature);
1099                remaining.sort();
1100            }
1101            None => break,
1102        }
1103    }
1104
1105    selected.sort();
1106    Ok(selected)
1107}
1108
1109macro_rules! impl_fit_for_sfs {
1110    ($y_type:ty) => {
1111        impl<E> Fit<Mat2f64, $y_type> for SequentialFeatureSelector<E, Untrained>
1112        where
1113            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
1114            E::Fitted: Score<Mat2f64, $y_type, Float = f64> + Send + Sync,
1115            $y_type: IndexableTarget,
1116        {
1117            type Fitted = SequentialFeatureSelector<E, Trained>;
1118
1119            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
1120                let n_features = x.ncols();
1121                let n_features_to_select = self.n_features_to_select.unwrap_or(n_features / 2);
1122
1123                if n_features_to_select > n_features {
1124                    return Err(SklearsError::InvalidInput(format!(
1125                        "n_features_to_select ({}) must be <= n_features ({})",
1126                        n_features_to_select, n_features
1127                    )));
1128                }
1129
1130                let cv = KFold::new(self.cv_folds);
1131
1132                let selected_features = match self.direction.as_str() {
1133                    "forward" => {
1134                        sfs_forward_selection(&self.estimator, x, y, n_features_to_select, &cv)?
1135                    }
1136                    "backward" => {
1137                        sfs_backward_elimination(&self.estimator, x, y, n_features_to_select, &cv)?
1138                    }
1139                    "bidirectional" => sfs_bidirectional_selection(
1140                        &self.estimator,
1141                        x,
1142                        y,
1143                        n_features_to_select,
1144                        &cv,
1145                    )?,
1146                    _ => {
1147                        return Err(SklearsError::InvalidInput(
1148                            "Invalid direction specified".to_string(),
1149                        ))
1150                    }
1151                };
1152
1153                Ok(SequentialFeatureSelector {
1154                    estimator: self.estimator,
1155                    n_features_to_select: self.n_features_to_select,
1156                    direction: self.direction,
1157                    cv_folds: self.cv_folds,
1158                    state: PhantomData,
1159                    selected_features_: Some(selected_features),
1160                    n_features_: Some(n_features),
1161                })
1162            }
1163        }
1164    };
1165}
1166
1167impl_fit_for_sfs!(Vec1f64);
1168impl_fit_for_sfs!(Vec1i32);
1169impl_fit_for_sfs!(Vec1usize);
1170
1171impl<E> Transform<Array2<Float>> for SequentialFeatureSelector<E, Trained> {
1172    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
1173        let n_features = self.n_features_.ok_or_else(|| {
1174            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
1175        })?;
1176        validate::check_n_features(x, n_features)?;
1177
1178        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
1179            SklearsError::InvalidState("Model not trained: selected_features_ is None".to_string())
1180        })?;
1181        let n_samples = x.nrows();
1182        let k = selected_features.len();
1183        let mut x_new = Array2::zeros((n_samples, k));
1184
1185        for (new_idx, &old_idx) in selected_features.iter().enumerate() {
1186            x_new.column_mut(new_idx).assign(&x.column(old_idx));
1187        }
1188
1189        Ok(x_new)
1190    }
1191}
1192
1193impl<E> SequentialFeatureSelector<E, Trained> {
1194    /// Get the support mask
1195    pub fn get_support(&self) -> SklResult<Array1<bool>> {
1196        let n_features = self.n_features_.ok_or_else(|| {
1197            SklearsError::InvalidState("Model not trained: n_features_ is None".to_string())
1198        })?;
1199        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
1200            SklearsError::InvalidState("Model not trained: selected_features_ is None".to_string())
1201        })?;
1202        let mut support = Array1::from_elem(n_features, false);
1203
1204        for &idx in selected_features {
1205            support[idx] = true;
1206        }
1207
1208        Ok(support)
1209    }
1210
1211    /// Get the selected feature indices
1212    pub fn get_feature_names_out(&self) -> SklResult<&[usize]> {
1213        self.selected_features_.as_deref().ok_or_else(|| {
1214            SklearsError::InvalidState("Model not trained: selected_features_ is None".to_string())
1215        })
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use scirs2_core::ndarray::array;
1223    use scirs2_core::random::rngs::StdRng;
1224    use scirs2_core::random::{RngExt, SeedableRng};
1225    use sklears_core::types::Array1;
1226
1227    // Mock estimator for testing
1228    #[derive(Clone)]
1229    struct MockLinearEstimator {
1230        coef_: Option<Array1<Float>>,
1231    }
1232
1233    impl MockLinearEstimator {
1234        fn new() -> Self {
1235            Self { coef_: None }
1236        }
1237
1238        fn simple_solve(&self, a: &Array2<f64>, b: &Array1<f64>) -> SklResult<Array1<f64>> {
1239            let n = a.nrows();
1240            if n != a.ncols() {
1241                return Err(SklearsError::FitError("Matrix must be square".to_string()));
1242            }
1243            if n != b.len() {
1244                return Err(SklearsError::FitError("Dimension mismatch".to_string()));
1245            }
1246
1247            let mut aug = Array2::zeros((n, n + 1));
1248            for i in 0..n {
1249                for j in 0..n {
1250                    aug[[i, j]] = a[[i, j]];
1251                }
1252                aug[[i, n]] = b[i];
1253            }
1254
1255            // Forward elimination with partial pivoting
1256            for k in 0..n {
1257                let mut pivot_row = k;
1258                for i in (k + 1)..n {
1259                    if aug[[i, k]].abs() > aug[[pivot_row, k]].abs() {
1260                        pivot_row = i;
1261                    }
1262                }
1263
1264                if pivot_row != k {
1265                    for j in 0..=n {
1266                        let temp = aug[[k, j]];
1267                        aug[[k, j]] = aug[[pivot_row, j]];
1268                        aug[[pivot_row, j]] = temp;
1269                    }
1270                }
1271
1272                if aug[[k, k]].abs() < 1e-12 {
1273                    return Err(SklearsError::FitError(
1274                        "Matrix is singular or ill-conditioned".to_string(),
1275                    ));
1276                }
1277
1278                for i in (k + 1)..n {
1279                    let factor = aug[[i, k]] / aug[[k, k]];
1280                    for j in k..=n {
1281                        aug[[i, j]] -= factor * aug[[k, j]];
1282                    }
1283                }
1284            }
1285
1286            // Back substitution
1287            let mut x = Array1::zeros(n);
1288            for i in (0..n).rev() {
1289                let mut sum = aug[[i, n]];
1290                for j in (i + 1)..n {
1291                    sum -= aug[[i, j]] * x[j];
1292                }
1293                x[i] = sum / aug[[i, i]];
1294            }
1295
1296            Ok(x)
1297        }
1298    }
1299
1300    impl Estimator for MockLinearEstimator {
1301        type Config = ();
1302        type Error = SklearsError;
1303        type Float = f64;
1304
1305        fn config(&self) -> &Self::Config {
1306            &()
1307        }
1308    }
1309
1310    impl Fit<Array2<Float>, Array1<Float>> for MockLinearEstimator {
1311        type Fitted = MockLinearEstimator;
1312
1313        fn fit(mut self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
1314            // Simple linear regression using normal equation with regularization
1315            let xtx = x.t().dot(x);
1316            let xty = x.t().dot(y);
1317
1318            // Add small regularization to avoid singular matrix
1319            let n_features = x.ncols();
1320            let mut xtx_reg = xtx.clone();
1321            for i in 0..n_features {
1322                xtx_reg[[i, i]] += 1e-5;
1323            }
1324
1325            // Simple solve using iterative method
1326            let coef = self.simple_solve(&xtx_reg, &xty)?;
1327
1328            self.coef_ = Some(coef);
1329            Ok(self)
1330        }
1331    }
1332
1333    impl Score<Array2<Float>, Array1<Float>> for MockLinearEstimator {
1334        type Float = f64;
1335
1336        fn score(&self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<f64> {
1337            let coef = self.coef_.as_ref().ok_or(SklearsError::NotFitted {
1338                operation: "score".to_string(),
1339            })?;
1340            let predictions = x.dot(coef);
1341
1342            // R² score
1343            let ss_res = (&predictions - y).mapv(|x| x * x).sum();
1344            let y_mean = y.mean().unwrap_or(0.0);
1345            let ss_tot = y.mapv(|yi| (yi - y_mean).powi(2)).sum();
1346
1347            if ss_tot == 0.0 {
1348                Ok(1.0)
1349            } else {
1350                Ok(1.0 - (ss_res / ss_tot))
1351            }
1352        }
1353    }
1354
1355    impl HasCoefficients for MockLinearEstimator {
1356        fn coef(&self) -> SklResult<Array1<Float>> {
1357            self.coef_.clone().ok_or(SklearsError::NotFitted {
1358                operation: "coef".to_string(),
1359            })
1360        }
1361    }
1362
1363    #[test]
1364    fn test_rfe_basic() {
1365        // Create a simple dataset where first two features are most important
1366        let x = array![
1367            [1.0, 2.0, 0.1, 0.0],
1368            [2.0, 4.0, 0.2, 0.1],
1369            [3.0, 6.0, 0.1, 0.0],
1370            [4.0, 8.0, 0.3, 0.1],
1371            [5.0, 10.0, 0.2, 0.0],
1372        ];
1373        let y = array![5.0, 10.0, 15.0, 20.0, 25.0]; // y = x0 + 2*x1
1374
1375        let estimator = MockLinearEstimator::new();
1376        let rfe = RFE::new(estimator)
1377            .n_features_to_select(2)
1378            .step(1.0)
1379            .expect("valid step");
1380
1381        let fitted_rfe = rfe.fit(&x, &y).expect("operation should succeed");
1382
1383        // Check that the first two features are selected
1384        let support = fitted_rfe.support().expect("operation should succeed");
1385        println!("Support: {:?}", support);
1386
1387        // Check ranking
1388        let ranking = fitted_rfe.ranking().expect("operation should succeed");
1389        println!("Ranking: {:?}", ranking);
1390
1391        // The features with highest absolute coefficients should be selected
1392        // In our test data, y = 1*x0 + 2*x1 + small*x2 + small*x3
1393        // So x1 should definitely be selected (coef=2), and x0 (coef=1)
1394        // The support array indicates which features are selected
1395        let n_selected = support.iter().filter(|&&x| x).count();
1396        assert_eq!(n_selected, 2);
1397
1398        // Selected features should have rank 1
1399        for i in 0..4 {
1400            if support[i] {
1401                assert_eq!(ranking[i], 1, "Selected feature {} should have rank 1", i);
1402            } else {
1403                assert!(
1404                    ranking[i] > 1,
1405                    "Non-selected feature {} should have rank > 1",
1406                    i
1407                );
1408            }
1409        }
1410
1411        // Test transform
1412        let x_transformed = fitted_rfe.transform(&x).expect("operation should succeed");
1413        assert_eq!(x_transformed.shape(), &[5, 2]);
1414    }
1415
1416    #[test]
1417    fn test_rfe_step_fraction() {
1418        let x = array![
1419            [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
1420            [2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
1421            [3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
1422            [4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
1423        ];
1424        let y = array![1.0, 2.0, 3.0, 4.0];
1425
1426        let estimator = MockLinearEstimator::new();
1427        let rfe = RFE::new(estimator)
1428            .n_features_to_select(2)
1429            .step(0.5)
1430            .expect("valid step"); // Remove 50% of features at each step
1431
1432        let fitted_rfe = rfe.fit(&x, &y).expect("operation should succeed");
1433
1434        // Should have selected 2 features
1435        let support = fitted_rfe.support().expect("operation should succeed");
1436        let n_selected = support.iter().filter(|&&x| x).count();
1437        assert_eq!(n_selected, 2);
1438    }
1439
1440    #[test]
1441    fn test_rfe_invalid_step() {
1442        let estimator = MockLinearEstimator::new();
1443        let result = RFE::new(estimator).step(0.0);
1444        assert!(result.is_err());
1445    }
1446
1447    #[test]
1448    fn test_rfe_too_many_features() {
1449        let x = array![[1.0, 2.0], [3.0, 4.0]];
1450        let y = array![1.0, 2.0];
1451
1452        let estimator = MockLinearEstimator::new();
1453        let rfe = RFE::new(estimator).n_features_to_select(5);
1454
1455        let result = rfe.fit(&x, &y);
1456        assert!(result.is_err());
1457    }
1458
1459    #[test]
1460    fn test_rfecv_basic() {
1461        // Create a dataset where first two features are informative
1462        let mut rng = StdRng::seed_from_u64(42); // Use deterministic seed
1463        let mut x = Array2::<Float>::zeros((20, 5));
1464        let mut y = Array1::<Float>::zeros(20);
1465
1466        for i in 0..20 {
1467            x[[i, 0]] = i as Float;
1468            x[[i, 1]] = (i * 2) as Float;
1469            x[[i, 2]] = rng.random::<Float>(); // noise
1470            x[[i, 3]] = rng.random::<Float>(); // noise
1471            x[[i, 4]] = rng.random::<Float>(); // noise
1472
1473            y[i] = x[[i, 0]] + 2.0 * x[[i, 1]] + 0.1 * rng.random::<Float>();
1474        }
1475
1476        let estimator = MockLinearEstimator::new();
1477        let rfecv = RFECV::new(estimator)
1478            .min_features_to_select(1)
1479            .expect("valid min_features")
1480            .step(1.0)
1481            .expect("valid step");
1482
1483        let fitted_rfecv = rfecv.fit(&x, &y).expect("operation should succeed");
1484
1485        // Check that CV results were computed
1486        let cv_results = fitted_rfecv.cv_results().expect("operation should succeed");
1487        assert!(!cv_results.mean_test_scores.is_empty());
1488        assert_eq!(
1489            cv_results.mean_test_scores.len(),
1490            cv_results.std_test_scores.len()
1491        );
1492        assert_eq!(
1493            cv_results.mean_test_scores.len(),
1494            cv_results.n_features.len()
1495        );
1496
1497        // The optimal number of features should be at least 2 (the informative ones)
1498        let support = fitted_rfecv.support().expect("operation should succeed");
1499        let n_selected = support.iter().filter(|&&x| x).count();
1500        assert!(n_selected >= 2);
1501    }
1502
1503    #[test]
1504    fn test_rfecv_invalid_min_features() {
1505        let estimator = MockLinearEstimator::new();
1506        let result = RFECV::new(estimator).min_features_to_select(0);
1507        assert!(result.is_err());
1508    }
1509
1510    #[test]
1511    fn test_rfecv_custom_cv() {
1512        let x = array![
1513            [1.0, 2.0, 3.0],
1514            [2.0, 3.0, 4.0],
1515            [3.0, 4.0, 5.0],
1516            [4.0, 5.0, 6.0],
1517            [5.0, 6.0, 7.0],
1518            [6.0, 7.0, 8.0],
1519        ];
1520        let y = array![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1521
1522        let estimator = MockLinearEstimator::new();
1523        let cv = Box::new(KFold::new(3));
1524        let rfecv = RFECV::new(estimator)
1525            .min_features_to_select(1)
1526            .expect("valid min_features")
1527            .cv(cv);
1528
1529        let fitted_rfecv = rfecv.fit(&x, &y).expect("operation should succeed");
1530
1531        // Should have computed CV scores
1532        let cv_results = fitted_rfecv.cv_results().expect("operation should succeed");
1533        assert!(!cv_results.mean_test_scores.is_empty());
1534    }
1535
1536    #[test]
1537    fn test_select_from_model() {
1538        let x = array![
1539            [1.0, 2.0, 0.1, 0.0],
1540            [2.0, 4.0, 0.2, 0.1],
1541            [3.0, 6.0, 0.1, 0.0],
1542            [4.0, 8.0, 0.3, 0.1],
1543            [5.0, 10.0, 0.2, 0.0],
1544        ];
1545        let y = array![5.0, 10.0, 15.0, 20.0, 25.0];
1546
1547        let estimator = MockLinearEstimator::new();
1548        let selector = SelectFromModel::new(estimator).threshold(0.5);
1549
1550        let fitted_selector = selector.fit(&x, &y).expect("operation should succeed");
1551
1552        // Should select features with high coefficients
1553        let support = fitted_selector
1554            .get_support()
1555            .expect("operation should succeed");
1556        let n_selected = support.iter().filter(|&&x| x).count();
1557        assert!(n_selected >= 1);
1558
1559        let x_transformed = fitted_selector
1560            .transform(&x)
1561            .expect("operation should succeed");
1562        assert_eq!(x_transformed.ncols(), n_selected);
1563    }
1564
1565    #[test]
1566    fn test_sequential_feature_selector_forward() {
1567        // Dataset: y = x0 + 2*x1 + noise from x2, x3
1568        let x = array![
1569            [1.0f64, 2.0, 0.1, 0.0],
1570            [2.0, 4.0, 0.2, 0.1],
1571            [3.0, 6.0, 0.1, 0.0],
1572            [4.0, 8.0, 0.3, 0.1],
1573            [5.0, 10.0, 0.2, 0.0],
1574            [6.0, 12.0, 0.1, 0.0],
1575            [7.0, 14.0, 0.2, 0.1],
1576            [8.0, 16.0, 0.1, 0.0],
1577        ];
1578        let y = array![5.0f64, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0];
1579
1580        let estimator = MockLinearEstimator::new();
1581        let sfs = SequentialFeatureSelector::new(estimator)
1582            .n_features_to_select(2)
1583            .cv(3);
1584
1585        let fitted_sfs = sfs.fit(&x, &y).expect("forward SFS should succeed");
1586
1587        let support = fitted_sfs
1588            .get_support()
1589            .expect("get_support should succeed");
1590        let n_selected = support.iter().filter(|&&v| v).count();
1591        assert_eq!(n_selected, 2, "should select exactly 2 features");
1592
1593        let x_transformed = fitted_sfs.transform(&x).expect("transform should succeed");
1594        assert_eq!(x_transformed.ncols(), 2);
1595        assert_eq!(x_transformed.nrows(), 8);
1596    }
1597
1598    #[test]
1599    fn test_sequential_feature_selector_backward() {
1600        let x = array![
1601            [1.0f64, 2.0, 0.05, 0.01],
1602            [2.0, 4.0, 0.04, 0.02],
1603            [3.0, 6.0, 0.06, 0.01],
1604            [4.0, 8.0, 0.03, 0.02],
1605            [5.0, 10.0, 0.05, 0.01],
1606            [6.0, 12.0, 0.04, 0.02],
1607            [7.0, 14.0, 0.06, 0.01],
1608            [8.0, 16.0, 0.03, 0.02],
1609        ];
1610        let y = array![5.0f64, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0];
1611
1612        let estimator = MockLinearEstimator::new();
1613        let sfs = SequentialFeatureSelector::new(estimator)
1614            .n_features_to_select(2)
1615            .direction("backward")
1616            .expect("valid direction")
1617            .cv(3);
1618
1619        let fitted_sfs = sfs.fit(&x, &y).expect("backward SFS should succeed");
1620
1621        let support = fitted_sfs
1622            .get_support()
1623            .expect("get_support should succeed");
1624        let n_selected = support.iter().filter(|&&v| v).count();
1625        assert_eq!(n_selected, 2, "should select exactly 2 features");
1626    }
1627
1628    #[test]
1629    fn test_sequential_feature_selector_invalid_direction() {
1630        let estimator = MockLinearEstimator::new();
1631        let result = SequentialFeatureSelector::new(estimator).direction("invalid");
1632        assert!(result.is_err(), "invalid direction should fail");
1633    }
1634
1635    #[test]
1636    fn test_sequential_feature_selector_too_many_features() {
1637        let x = array![[1.0f64, 2.0], [3.0, 4.0], [5.0, 6.0]];
1638        let y = array![1.0f64, 2.0, 3.0];
1639
1640        let estimator = MockLinearEstimator::new();
1641        let sfs = SequentialFeatureSelector::new(estimator).n_features_to_select(5);
1642
1643        let result = sfs.fit(&x, &y);
1644        assert!(
1645            result.is_err(),
1646            "requesting more features than available should fail"
1647        );
1648    }
1649
1650    #[test]
1651    fn test_sequential_feature_selector_get_feature_names_out() {
1652        let x = array![
1653            [1.0f64, 2.0, 0.1],
1654            [2.0, 4.0, 0.2],
1655            [3.0, 6.0, 0.1],
1656            [4.0, 8.0, 0.3],
1657            [5.0, 10.0, 0.2],
1658            [6.0, 12.0, 0.1],
1659        ];
1660        let y = array![5.0f64, 10.0, 15.0, 20.0, 25.0, 30.0];
1661
1662        let estimator = MockLinearEstimator::new();
1663        let sfs = SequentialFeatureSelector::new(estimator)
1664            .n_features_to_select(2)
1665            .cv(2);
1666
1667        let fitted_sfs = sfs.fit(&x, &y).expect("SFS should succeed");
1668        let feature_names = fitted_sfs
1669            .get_feature_names_out()
1670            .expect("get_feature_names_out should succeed");
1671        assert_eq!(feature_names.len(), 2, "should have 2 selected features");
1672    }
1673}