1use 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};
9pub trait CrossValidator: Send + Sync {
13 fn split(&self, n_samples: usize) -> Vec<(Vec<usize>, Vec<usize>)>;
15}
16
17#[derive(Debug, Clone)]
19pub struct KFold {
20 n_splits: usize,
21 shuffle: bool,
22 random_state: Option<u64>,
23}
24
25impl KFold {
26 pub fn new(n_splits: usize) -> Self {
28 Self {
29 n_splits,
30 shuffle: false,
31 random_state: None,
32 }
33 }
34
35 pub fn shuffle(mut self, shuffle: bool) -> Self {
37 self.shuffle = shuffle;
38 self
39 }
40
41 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
90fn compare_floats(a: &Float, b: &Float) -> std::cmp::Ordering {
92 a.partial_cmp(b).unwrap_or_else(|| {
93 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
102pub trait IndexableTarget: Clone {
104 fn select(&self, indices: &[usize]) -> Self;
106}
107
108impl 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
130pub trait FeatureImportance {
132 fn feature_importances(&self) -> SklResult<Array1<Float>>;
135}
136
137pub trait HasCoefficients {
139 fn coef(&self) -> SklResult<Array1<Float>>;
141}
142
143impl<T> FeatureImportance for T
145where
146 T: HasCoefficients,
147{
148 fn feature_importances(&self) -> SklResult<Array1<Float>> {
149 self.coef().map(|coef| coef.mapv(|x| x.abs()))
151 }
152}
153
154pub struct RFE<E, State = Untrained> {
156 estimator: E,
157 n_features_to_select: Option<usize>,
158 step: f64,
159 state: PhantomData<State>,
160 support_: Option<Array1<bool>>,
162 ranking_: Option<Array1<usize>>,
163 n_features_: Option<usize>,
164}
165
166impl<E: Clone> RFE<E, Untrained> {
167 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 pub fn n_features_to_select(mut self, n: usize) -> Self {
182 self.n_features_to_select = Some(n);
183 self
184 }
185
186 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
208type 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 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 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
332pub 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 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)]
348pub struct RFECVResults {
350 pub mean_test_scores: Array1<Float>,
352 pub std_test_scores: Array1<Float>,
354 pub n_features: Array1<usize>,
356}
357
358impl<E: Clone> RFECV<E, Untrained> {
359 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 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 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 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 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 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 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#[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 _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 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 pub fn threshold(mut self, threshold: f64) -> Self {
679 self.threshold = Some(threshold);
680 self
681 }
682
683 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 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 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#[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 selected_features_: Option<Vec<usize>>,
823 n_features_: Option<usize>,
824}
825
826impl<E: Clone> SequentialFeatureSelector<E, Untrained> {
827 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 pub fn n_features_to_select(mut self, n: usize) -> Self {
842 self.n_features_to_select = Some(n);
843 self
844 }
845
846 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 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
874fn 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
913fn 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
959fn 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
1004fn 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 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 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 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 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 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 #[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 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 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 let xtx = x.t().dot(x);
1316 let xty = x.t().dot(y);
1317
1318 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 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 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 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]; 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 let support = fitted_rfe.support().expect("operation should succeed");
1385 println!("Support: {:?}", support);
1386
1387 let ranking = fitted_rfe.ranking().expect("operation should succeed");
1389 println!("Ranking: {:?}", ranking);
1390
1391 let n_selected = support.iter().filter(|&&x| x).count();
1396 assert_eq!(n_selected, 2);
1397
1398 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 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"); let fitted_rfe = rfe.fit(&x, &y).expect("operation should succeed");
1433
1434 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 let mut rng = StdRng::seed_from_u64(42); 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>(); x[[i, 3]] = rng.random::<Float>(); x[[i, 4]] = rng.random::<Float>(); 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 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 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 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 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 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}