1use crate::{FeatureImportance, IndexableTarget};
7use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
8use scirs2_core::rand_prelude::SliceRandom;
9use scirs2_core::random::{rngs::StdRng, thread_rng, RngExt, SeedableRng};
10use sklears_core::{
11 error::{validate, Result as SklResult, SklearsError},
12 traits::{Estimator, Fit, Trained, Transform, Untrained},
13 types::Float,
14};
15use std::marker::PhantomData;
16
17type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
22type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
23type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
24type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
25
26use crate::base::SelectorMixin;
27
28pub fn extract_features(x: &Array2<Float>, features: &[usize]) -> SklResult<Array2<Float>> {
30 let n_samples = x.nrows();
31 let n_selected = features.len();
32 let mut x_subset = Array2::zeros((n_samples, n_selected));
33
34 for (new_idx, &old_idx) in features.iter().enumerate() {
35 if old_idx >= x.ncols() {
36 return Err(SklearsError::InvalidInput(format!(
37 "Feature index {} out of bounds",
38 old_idx
39 )));
40 }
41 x_subset.column_mut(new_idx).assign(&x.column(old_idx));
42 }
43
44 Ok(x_subset)
45}
46
47#[derive(Debug, Clone)]
50pub struct BootstrapSelector<E, State = Untrained> {
51 estimator: E,
52 n_bootstrap: usize,
53 sample_ratio: f64,
54 stability_threshold: f64,
55 min_selection_frequency: f64,
56 max_features: Option<usize>,
57 random_state: Option<u64>,
58 state: PhantomData<State>,
59 feature_frequencies_: Option<Array1<Float>>,
61 stability_scores_: Option<Array1<Float>>,
62 selected_features_: Option<Vec<usize>>,
63 n_features_: Option<usize>,
64 bootstrap_selections_: Option<Vec<Vec<usize>>>,
65}
66
67impl<E: Clone> BootstrapSelector<E, Untrained> {
68 pub fn new(estimator: E) -> Self {
70 Self {
71 estimator,
72 n_bootstrap: 100,
73 sample_ratio: 0.8,
74 stability_threshold: 0.7,
75 min_selection_frequency: 0.5,
76 max_features: None,
77 random_state: None,
78 state: PhantomData,
79 feature_frequencies_: None,
80 stability_scores_: None,
81 selected_features_: None,
82 n_features_: None,
83 bootstrap_selections_: None,
84 }
85 }
86
87 pub fn n_bootstrap(mut self, n_bootstrap: usize) -> Result<Self, SklearsError> {
89 if n_bootstrap < 1 {
90 return Err(SklearsError::InvalidInput(
91 "n_bootstrap must be at least 1".to_string(),
92 ));
93 }
94 self.n_bootstrap = n_bootstrap;
95 Ok(self)
96 }
97
98 pub fn sample_ratio(mut self, sample_ratio: f64) -> Result<Self, SklearsError> {
100 if !(0.0..=1.0).contains(&sample_ratio) {
101 return Err(SklearsError::InvalidInput(
102 "sample_ratio must be between 0 and 1".to_string(),
103 ));
104 }
105 self.sample_ratio = sample_ratio;
106 Ok(self)
107 }
108
109 pub fn stability_threshold(mut self, threshold: f64) -> Result<Self, SklearsError> {
111 if !(0.0..=1.0).contains(&threshold) {
112 return Err(SklearsError::InvalidInput(
113 "stability_threshold must be between 0 and 1".to_string(),
114 ));
115 }
116 self.stability_threshold = threshold;
117 Ok(self)
118 }
119
120 pub fn min_selection_frequency(mut self, frequency: f64) -> Result<Self, SklearsError> {
122 if !(0.0..=1.0).contains(&frequency) {
123 return Err(SklearsError::InvalidInput(
124 "min_selection_frequency must be between 0 and 1".to_string(),
125 ));
126 }
127 self.min_selection_frequency = frequency;
128 Ok(self)
129 }
130
131 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
133 self.max_features = max_features;
134 self
135 }
136
137 pub fn random_state(mut self, seed: u64) -> Self {
139 self.random_state = Some(seed);
140 self
141 }
142}
143
144impl<E> Estimator for BootstrapSelector<E, Untrained> {
145 type Config = ();
146 type Error = SklearsError;
147 type Float = f64;
148
149 fn config(&self) -> &Self::Config {
150 &()
151 }
152}
153
154macro_rules! impl_fit_for_bootstrap {
155 ($y_type:ty) => {
156 impl<E> Fit<Mat2f64, $y_type> for BootstrapSelector<E, Untrained>
157 where
158 E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
159 E::Fitted: FeatureImportance + Send + Sync,
160 $y_type: IndexableTarget,
161 {
162 type Fitted = BootstrapSelector<E, Trained>;
163
164 fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
165 let (n_samples, n_features) = x.dim();
166
167 if n_samples < 2 {
168 return Err(SklearsError::InvalidInput(
169 "Bootstrap selection requires at least 2 samples".to_string(),
170 ));
171 }
172
173 let mut rng = if let Some(seed) = self.random_state {
175 StdRng::seed_from_u64(seed)
176 } else {
177 StdRng::from_rng(&mut thread_rng())
178 };
179
180 let sample_size = (n_samples as f64 * self.sample_ratio).round() as usize;
181 let mut feature_counts = Array1::zeros(n_features);
182 let mut bootstrap_selections = Vec::with_capacity(self.n_bootstrap);
183
184 for _ in 0..self.n_bootstrap {
186 let mut bootstrap_indices = Vec::with_capacity(sample_size);
188 for _ in 0..sample_size {
189 bootstrap_indices.push(rng.random_range(0..n_samples));
190 }
191
192 let x_bootstrap = x.select(Axis(0), &bootstrap_indices);
194 let y_bootstrap = y.select(&bootstrap_indices);
195
196 match self.estimator.clone().fit(&x_bootstrap, &y_bootstrap) {
198 Ok(fitted_estimator) => {
199 if let Ok(importances) = fitted_estimator.feature_importances() {
201 let threshold = importances.mean().unwrap_or(0.0);
203 let selected_features: Vec<usize> = importances
204 .iter()
205 .enumerate()
206 .filter(|(_, &importance)| importance > threshold)
207 .map(|(idx, _)| idx)
208 .collect();
209
210 for &feature_idx in &selected_features {
212 feature_counts[feature_idx] += 1.0;
213 }
214
215 bootstrap_selections.push(selected_features);
216 }
217 }
218 Err(_) => {
219 continue;
221 }
222 }
223 }
224
225 let feature_frequencies = feature_counts / self.n_bootstrap as f64;
227
228 let mut stability_scores = Array1::zeros(n_features);
230 if bootstrap_selections.len() >= 2 {
231 for feature_idx in 0..n_features {
232 let mut jaccard_similarities = Vec::new();
233
234 for i in 0..(bootstrap_selections.len() - 1) {
235 let set1_contains = bootstrap_selections[i].contains(&feature_idx);
236 let set2_contains = bootstrap_selections[i + 1].contains(&feature_idx);
237
238 let similarity = if set1_contains == set2_contains {
240 1.0
241 } else {
242 0.0
243 };
244 jaccard_similarities.push(similarity);
245 }
246
247 stability_scores[feature_idx] = jaccard_similarities.iter().sum::<f64>()
248 / jaccard_similarities.len() as f64;
249 }
250 }
251
252 let mut selected_features: Vec<usize> = (0..n_features)
254 .filter(|&i| {
255 feature_frequencies[i] >= self.min_selection_frequency
256 && stability_scores[i] >= self.stability_threshold
257 })
258 .collect();
259
260 if let Some(max_feat) = self.max_features {
262 if selected_features.len() > max_feat {
263 selected_features.sort_by(|&a, &b| {
265 let score_a: f64 = feature_frequencies[a] * stability_scores[a];
266 let score_b: f64 = feature_frequencies[b] * stability_scores[b];
267 score_b.partial_cmp(&score_a).unwrap_or_else(|| {
268 match (score_b.is_nan(), score_a.is_nan()) {
269 (true, false) => std::cmp::Ordering::Less,
270 (false, true) => std::cmp::Ordering::Greater,
271 _ => std::cmp::Ordering::Equal,
272 }
273 })
274 });
275 selected_features.truncate(max_feat);
276 selected_features.sort(); }
278 }
279
280 if selected_features.is_empty() {
281 return Err(SklearsError::InvalidInput(
282 "No features selected by Bootstrap method. Try reducing thresholds."
283 .to_string(),
284 ));
285 }
286
287 Ok(BootstrapSelector {
288 estimator: self.estimator,
289 n_bootstrap: self.n_bootstrap,
290 sample_ratio: self.sample_ratio,
291 stability_threshold: self.stability_threshold,
292 min_selection_frequency: self.min_selection_frequency,
293 max_features: self.max_features,
294 random_state: self.random_state,
295 state: PhantomData,
296 feature_frequencies_: Some(feature_frequencies),
297 stability_scores_: Some(stability_scores),
298 selected_features_: Some(selected_features),
299 n_features_: Some(n_features),
300 bootstrap_selections_: Some(bootstrap_selections),
301 })
302 }
303 }
304 };
305}
306
307impl_fit_for_bootstrap!(Vec1f64);
308impl_fit_for_bootstrap!(Vec1i32);
309impl_fit_for_bootstrap!(Vec1usize);
310
311impl<E> Transform<Array2<Float>> for BootstrapSelector<E, Trained> {
312 fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
313 let n_features = self.n_features_.ok_or_else(|| {
314 SklearsError::FitError("BootstrapSelector not fitted: n_features_ missing".to_string())
315 })?;
316 validate::check_n_features(x, n_features)?;
317
318 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
319 SklearsError::FitError(
320 "BootstrapSelector not fitted: selected_features_ missing".to_string(),
321 )
322 })?;
323 extract_features(x, selected_features)
324 }
325}
326
327impl<E> SelectorMixin for BootstrapSelector<E, Trained> {
328 fn get_support(&self) -> SklResult<Array1<bool>> {
329 let n_features = self.n_features_.ok_or_else(|| {
330 SklearsError::FitError("BootstrapSelector not fitted: n_features_ missing".to_string())
331 })?;
332 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
333 SklearsError::FitError(
334 "BootstrapSelector not fitted: selected_features_ missing".to_string(),
335 )
336 })?;
337 let mut support = Array1::from_elem(n_features, false);
338
339 for &idx in selected_features {
340 support[idx] = true;
341 }
342
343 Ok(support)
344 }
345
346 fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
347 let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
348 SklearsError::FitError(
349 "BootstrapSelector not fitted: selected_features_ missing".to_string(),
350 )
351 })?;
352 Ok(indices
353 .iter()
354 .filter_map(|&idx| selected_features.iter().position(|&f| f == idx))
355 .collect())
356 }
357}
358
359impl<E> BootstrapSelector<E, Trained> {
360 pub fn feature_frequencies(&self) -> SklResult<&Array1<Float>> {
362 self.feature_frequencies_.as_ref().ok_or_else(|| {
363 SklearsError::FitError(
364 "BootstrapSelector not fitted: feature_frequencies_ missing".to_string(),
365 )
366 })
367 }
368
369 pub fn stability_scores(&self) -> SklResult<&Array1<Float>> {
371 self.stability_scores_.as_ref().ok_or_else(|| {
372 SklearsError::FitError(
373 "BootstrapSelector not fitted: stability_scores_ missing".to_string(),
374 )
375 })
376 }
377
378 pub fn selected_features(&self) -> SklResult<&[usize]> {
380 self.selected_features_.as_deref().ok_or_else(|| {
381 SklearsError::FitError(
382 "BootstrapSelector not fitted: selected_features_ missing".to_string(),
383 )
384 })
385 }
386
387 pub fn bootstrap_selections(&self) -> SklResult<&[Vec<usize>]> {
389 self.bootstrap_selections_.as_deref().ok_or_else(|| {
390 SklearsError::FitError(
391 "BootstrapSelector not fitted: bootstrap_selections_ missing".to_string(),
392 )
393 })
394 }
395
396 pub fn selection_stability(&self) -> SklResult<f64> {
399 let selections = self.bootstrap_selections_.as_ref().ok_or_else(|| {
400 SklearsError::FitError(
401 "BootstrapSelector not fitted: bootstrap_selections_ missing".to_string(),
402 )
403 })?;
404 if selections.len() < 2 {
405 return Ok(1.0); }
407
408 let mut similarities = Vec::new();
409 for i in 0..(selections.len() - 1) {
410 for j in (i + 1)..selections.len() {
411 let set1: std::collections::HashSet<usize> =
412 selections[i].iter().cloned().collect();
413 let set2: std::collections::HashSet<usize> =
414 selections[j].iter().cloned().collect();
415
416 let intersection = set1.intersection(&set2).count();
417 let union = set1.union(&set2).count();
418
419 let jaccard = if union == 0 {
420 1.0
421 } else {
422 intersection as f64 / union as f64
423 };
424 similarities.push(jaccard);
425 }
426 }
427
428 Ok(similarities.iter().sum::<f64>() / similarities.len() as f64)
429 }
430}
431
432#[derive(Debug, Clone)]
438pub struct BorutaSelector<E, State = Untrained> {
439 estimator: E,
440 n_estimators: usize,
441 max_iter: usize,
442 alpha: f64,
443 two_step: bool,
444 percentile: f64,
445 random_state: Option<u64>,
446 state: PhantomData<State>,
447 confirmed_features_: Option<Vec<usize>>,
449 tentative_features_: Option<Vec<usize>>,
450 rejected_features_: Option<Vec<usize>>,
451 feature_importances_: Option<Array1<Float>>,
452 shadow_importances_: Option<Array1<Float>>,
453 n_features_: Option<usize>,
454 n_iterations_: Option<usize>,
455}
456
457impl<E> BorutaSelector<E, Untrained> {
458 pub fn new(estimator: E) -> Self {
460 Self {
461 estimator,
462 n_estimators: 100,
463 max_iter: 100,
464 alpha: 0.05,
465 two_step: true,
466 percentile: 100.0,
467 random_state: None,
468 state: PhantomData,
469 confirmed_features_: None,
470 tentative_features_: None,
471 rejected_features_: None,
472 feature_importances_: None,
473 shadow_importances_: None,
474 n_features_: None,
475 n_iterations_: None,
476 }
477 }
478
479 pub fn n_estimators(mut self, n_estimators: usize) -> Self {
481 self.n_estimators = n_estimators;
482 self
483 }
484
485 pub fn max_iter(mut self, max_iter: usize) -> Self {
487 self.max_iter = max_iter;
488 self
489 }
490
491 pub fn alpha(mut self, alpha: f64) -> Result<Self, SklearsError> {
493 if alpha <= 0.0 || alpha >= 1.0 {
494 return Err(SklearsError::InvalidInput(
495 "alpha must be between 0 and 1".to_string(),
496 ));
497 }
498 self.alpha = alpha;
499 Ok(self)
500 }
501
502 pub fn two_step(mut self, two_step: bool) -> Self {
504 self.two_step = two_step;
505 self
506 }
507
508 pub fn percentile(mut self, percentile: f64) -> Result<Self, SklearsError> {
510 if percentile <= 0.0 || percentile > 100.0 {
511 return Err(SklearsError::InvalidInput(
512 "percentile must be between 0 and 100".to_string(),
513 ));
514 }
515 self.percentile = percentile;
516 Ok(self)
517 }
518
519 pub fn random_state(mut self, random_state: u64) -> Self {
521 self.random_state = Some(random_state);
522 self
523 }
524}
525
526impl<E> Estimator for BorutaSelector<E, Untrained> {
527 type Config = ();
528 type Error = SklearsError;
529 type Float = f64;
530
531 fn config(&self) -> &Self::Config {
532 &()
533 }
534}
535
536impl<E> Fit<Mat2f64, Vec1f64> for BorutaSelector<E, Untrained>
537where
538 E: Clone + Fit<Mat2f64, Vec1f64> + FeatureImportance + Send + Sync,
539 E::Fitted: FeatureImportance + Send + Sync,
540{
541 type Fitted = BorutaSelector<E, Trained>;
542
543 fn fit(self, x: &Mat2f64, y: &Vec1f64) -> SklResult<Self::Fitted> {
544 validate::check_consistent_length(x, y)?;
545
546 let (n_samples, n_features) = x.dim();
547 if n_samples < 2 {
548 return Err(SklearsError::InvalidInput(
549 "Boruta requires at least 2 samples".to_string(),
550 ));
551 }
552
553 let mut rng = if let Some(seed) = self.random_state {
554 StdRng::seed_from_u64(seed)
555 } else {
556 StdRng::from_rng(&mut thread_rng())
557 };
558
559 let mut confirmed_features = Vec::new();
560 let mut tentative_features: Vec<usize> = (0..n_features).collect();
561 let mut rejected_features = Vec::new();
562
563 let mut feature_hits = vec![0; n_features];
564 let mut shadow_hits = 0;
565 let mut iteration = 0;
566
567 while iteration < self.max_iter && !tentative_features.is_empty() {
568 let (x_augmented, _feature_mapping) =
570 self.create_shadow_features(x, &tentative_features, &mut rng)?;
571
572 let importances = self.get_ensemble_importances(&x_augmented, y)?;
574
575 let n_real_features = tentative_features.len();
577 let shadow_importances = &importances.slice(s![n_real_features..]);
578 let shadow_threshold = self.calculate_threshold(shadow_importances)?;
579
580 shadow_hits += 1;
582 for (i, &feature_idx) in tentative_features.iter().enumerate() {
583 if importances[i] > shadow_threshold {
584 feature_hits[feature_idx] += 1;
585 }
586 }
587
588 let _critical_value = self.calculate_critical_value(iteration + 1)?;
590
591 let mut to_confirm = Vec::new();
592 let mut to_reject = Vec::new();
593
594 for &feature_idx in &tentative_features {
595 let hit_rate = feature_hits[feature_idx] as f64 / (iteration + 1) as f64;
596 let shadow_hit_rate = shadow_hits as f64 / (iteration + 1) as f64;
597
598 if self.binomial_test(feature_hits[feature_idx], iteration + 1, shadow_hit_rate)
600 < self.alpha
601 && hit_rate > shadow_hit_rate
602 {
603 to_confirm.push(feature_idx);
604 }
605
606 if self.two_step
608 && self.binomial_test(
609 shadow_hits - feature_hits[feature_idx],
610 iteration + 1,
611 1.0 - shadow_hit_rate,
612 ) < self.alpha
613 && hit_rate < shadow_hit_rate
614 {
615 to_reject.push(feature_idx);
616 }
617 }
618
619 for feature in to_confirm {
621 if let Some(pos) = tentative_features.iter().position(|&x| x == feature) {
622 tentative_features.remove(pos);
623 confirmed_features.push(feature);
624 }
625 }
626
627 for feature in to_reject {
628 if let Some(pos) = tentative_features.iter().position(|&x| x == feature) {
629 tentative_features.remove(pos);
630 rejected_features.push(feature);
631 }
632 }
633
634 iteration += 1;
635 }
636
637 let final_x_augmented = self
639 .create_shadow_features(x, &(0..n_features).collect::<Vec<_>>(), &mut rng)?
640 .0;
641 let final_importances = self.get_ensemble_importances(&final_x_augmented, y)?;
642 let feature_importances = final_importances.slice(s![..n_features]).to_owned();
643 let shadow_importances = final_importances.slice(s![n_features..]).to_owned();
644
645 if iteration >= self.max_iter {
647 rejected_features.extend(tentative_features.clone());
648 tentative_features.clear();
649 }
650
651 confirmed_features.sort_unstable();
653 tentative_features.sort_unstable();
654 rejected_features.sort_unstable();
655
656 Ok(BorutaSelector {
657 estimator: self.estimator,
658 n_estimators: self.n_estimators,
659 max_iter: self.max_iter,
660 alpha: self.alpha,
661 two_step: self.two_step,
662 percentile: self.percentile,
663 random_state: self.random_state,
664 state: PhantomData,
665 confirmed_features_: Some(confirmed_features),
666 tentative_features_: Some(tentative_features),
667 rejected_features_: Some(rejected_features),
668 feature_importances_: Some(feature_importances),
669 shadow_importances_: Some(shadow_importances),
670 n_features_: Some(n_features),
671 n_iterations_: Some(iteration),
672 })
673 }
674}
675
676impl<E> BorutaSelector<E, Untrained>
677where
678 E: Clone + Fit<Mat2f64, Vec1f64> + FeatureImportance,
679 E::Fitted: FeatureImportance,
680{
681 fn create_shadow_features(
683 &self,
684 x: &Mat2f64,
685 feature_indices: &[usize],
686 rng: &mut StdRng,
687 ) -> SklResult<(Mat2f64, Vec<usize>)> {
688 let (n_samples, _) = x.dim();
689 let n_features = feature_indices.len();
690
691 let mut x_augmented = Array2::<f64>::zeros((n_samples, n_features * 2));
693
694 for (new_idx, &orig_idx) in feature_indices.iter().enumerate() {
696 x_augmented.column_mut(new_idx).assign(&x.column(orig_idx));
697 }
698
699 for (new_idx, &orig_idx) in feature_indices.iter().enumerate() {
701 let mut shadow_column: Vec<Float> = x.column(orig_idx).to_vec();
702 shadow_column.shuffle(rng);
703
704 let shadow_idx = n_features + new_idx;
705 for (row, &value) in shadow_column.iter().enumerate() {
706 x_augmented[[row, shadow_idx]] = value;
707 }
708 }
709
710 let feature_mapping = feature_indices.to_vec();
711 Ok((x_augmented, feature_mapping))
712 }
713
714 fn get_ensemble_importances(&self, x: &Mat2f64, y: &Vec1f64) -> SklResult<Array1<Float>> {
716 let n_features = x.ncols();
717 let mut total_importances = Array1::<Float>::zeros(n_features);
718
719 for _ in 0..self.n_estimators {
721 let estimator = self.estimator.clone();
722 let trained = estimator.fit(x, y)?;
723 let importances = trained.feature_importances()?;
724 total_importances = total_importances + importances;
725 }
726
727 total_importances /= self.n_estimators as Float;
729
730 Ok(total_importances)
731 }
732
733 fn calculate_threshold(
735 &self,
736 shadow_importances: &ArrayBase<
737 scirs2_core::ndarray::ViewRepr<&Float>,
738 scirs2_core::ndarray::Dim<[usize; 1]>,
739 >,
740 ) -> SklResult<Float> {
741 if shadow_importances.is_empty() {
742 return Ok(0.0);
743 }
744
745 let mut sorted_importances: Vec<Float> = shadow_importances.to_vec();
746 sorted_importances.sort_by(|a, b| {
747 b.partial_cmp(a)
748 .unwrap_or_else(|| match (b.is_nan(), a.is_nan()) {
749 (true, false) => std::cmp::Ordering::Less,
750 (false, true) => std::cmp::Ordering::Greater,
751 _ => std::cmp::Ordering::Equal,
752 })
753 });
754
755 let index = ((self.percentile / 100.0) * sorted_importances.len() as f64).ceil() as usize;
756 let threshold_index = (index - 1).min(sorted_importances.len() - 1);
757
758 Ok(sorted_importances[threshold_index])
759 }
760
761 fn calculate_critical_value(&self, n_trials: usize) -> SklResult<f64> {
763 let corrected_alpha = self.alpha / n_trials as f64;
765 Ok(corrected_alpha)
766 }
767
768 fn binomial_test(&self, successes: usize, trials: usize, expected_prob: f64) -> f64 {
770 if trials == 0 {
771 return 1.0;
772 }
773
774 let observed_prob = successes as f64 / trials as f64;
775
776 if trials > 30 {
778 let mean = expected_prob;
779 let variance = expected_prob * (1.0 - expected_prob) / trials as f64;
780 let std_dev = variance.sqrt();
781
782 if std_dev > 1e-10 {
783 let z_score = (observed_prob - mean) / std_dev;
784 2.0 * (1.0 - self.standard_normal_cdf(z_score.abs()))
786 } else {
787 1.0
788 }
789 } else {
790 if (observed_prob - expected_prob).abs() < 1e-10 {
792 1.0
793 } else {
794 0.01 }
796 }
797 }
798
799 fn standard_normal_cdf(&self, x: f64) -> f64 {
801 0.5 * (1.0 + self.erf(x / 2.0_f64.sqrt()))
802 }
803
804 fn erf(&self, x: f64) -> f64 {
806 let a1 = 0.254829592;
807 let a2 = -0.284496736;
808 let a3 = 1.421413741;
809 let a4 = -1.453152027;
810 let a5 = 1.061405429;
811 let p = 0.3275911;
812
813 let sign = if x < 0.0 { -1.0 } else { 1.0 };
814 let x = x.abs();
815
816 let t = 1.0 / (1.0 + p * x);
817 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
818
819 sign * y
820 }
821}
822
823impl<E> Transform<Array2<f64>> for BorutaSelector<E, Trained> {
824 fn transform(&self, x: &Array2<f64>) -> SklResult<Array2<f64>> {
825 let support = self.get_support()?;
826 let selected_indices: Vec<usize> = support
827 .iter()
828 .enumerate()
829 .filter_map(|(i, &selected)| if selected { Some(i) } else { None })
830 .collect();
831
832 if selected_indices.is_empty() {
833 return Err(SklearsError::InvalidInput(
834 "No features were selected".to_string(),
835 ));
836 }
837
838 let n_samples = x.nrows();
839 let n_selected = selected_indices.len();
840 let mut result = Array2::<f64>::zeros((n_samples, n_selected));
841
842 for (new_idx, &orig_idx) in selected_indices.iter().enumerate() {
843 result.column_mut(new_idx).assign(&x.column(orig_idx));
844 }
845
846 Ok(result)
847 }
848}
849
850impl<E> SelectorMixin for BorutaSelector<E, Trained> {
851 fn get_support(&self) -> SklResult<Array1<bool>> {
852 let n_features = self.n_features_.ok_or_else(|| {
853 SklearsError::FitError("BorutaSelector not fitted: n_features_ missing".to_string())
854 })?;
855 let confirmed = self.confirmed_features_.as_ref().ok_or_else(|| {
856 SklearsError::FitError(
857 "BorutaSelector not fitted: confirmed_features_ missing".to_string(),
858 )
859 })?;
860 let tentative = self.tentative_features_.as_ref().ok_or_else(|| {
861 SklearsError::FitError(
862 "BorutaSelector not fitted: tentative_features_ missing".to_string(),
863 )
864 })?;
865
866 let mut support = Array1::<bool>::from_elem(n_features, false);
867
868 for &idx in confirmed {
870 support[idx] = true;
871 }
872
873 for &idx in tentative {
875 support[idx] = true;
876 }
877
878 Ok(support)
879 }
880
881 fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
882 let confirmed = self.confirmed_features_.as_ref().ok_or_else(|| {
883 SklearsError::FitError(
884 "BorutaSelector not fitted: confirmed_features_ missing".to_string(),
885 )
886 })?;
887 let tentative = self.tentative_features_.as_ref().ok_or_else(|| {
888 SklearsError::FitError(
889 "BorutaSelector not fitted: tentative_features_ missing".to_string(),
890 )
891 })?;
892
893 let mut selected_set = std::collections::HashSet::new();
894 for &idx in confirmed {
895 selected_set.insert(idx);
896 }
897 for &idx in tentative {
898 selected_set.insert(idx);
899 }
900
901 let transformed: Vec<usize> = indices
902 .iter()
903 .filter(|&&idx| selected_set.contains(&idx))
904 .enumerate()
905 .map(|(new_idx, _)| new_idx)
906 .collect();
907
908 Ok(transformed)
909 }
910}
911
912impl<E> BorutaSelector<E, Trained> {
913 pub fn confirmed_features(&self) -> SklResult<&[usize]> {
915 self.confirmed_features_.as_deref().ok_or_else(|| {
916 SklearsError::FitError(
917 "BorutaSelector not fitted: confirmed_features_ missing".to_string(),
918 )
919 })
920 }
921
922 pub fn tentative_features(&self) -> SklResult<&[usize]> {
924 self.tentative_features_.as_deref().ok_or_else(|| {
925 SklearsError::FitError(
926 "BorutaSelector not fitted: tentative_features_ missing".to_string(),
927 )
928 })
929 }
930
931 pub fn rejected_features(&self) -> SklResult<&[usize]> {
933 self.rejected_features_.as_deref().ok_or_else(|| {
934 SklearsError::FitError(
935 "BorutaSelector not fitted: rejected_features_ missing".to_string(),
936 )
937 })
938 }
939
940 pub fn feature_importances(&self) -> SklResult<&Array1<Float>> {
942 self.feature_importances_.as_ref().ok_or_else(|| {
943 SklearsError::FitError(
944 "BorutaSelector not fitted: feature_importances_ missing".to_string(),
945 )
946 })
947 }
948
949 pub fn shadow_importances(&self) -> SklResult<&Array1<Float>> {
951 self.shadow_importances_.as_ref().ok_or_else(|| {
952 SklearsError::FitError(
953 "BorutaSelector not fitted: shadow_importances_ missing".to_string(),
954 )
955 })
956 }
957
958 pub fn n_iterations(&self) -> SklResult<usize> {
960 self.n_iterations_.ok_or_else(|| {
961 SklearsError::FitError("BorutaSelector not fitted: n_iterations_ missing".to_string())
962 })
963 }
964
965 pub fn selected_features(&self) -> SklResult<Vec<usize>> {
967 let confirmed = self.confirmed_features_.as_ref().ok_or_else(|| {
968 SklearsError::FitError(
969 "BorutaSelector not fitted: confirmed_features_ missing".to_string(),
970 )
971 })?;
972 let tentative = self.tentative_features_.as_ref().ok_or_else(|| {
973 SklearsError::FitError(
974 "BorutaSelector not fitted: tentative_features_ missing".to_string(),
975 )
976 })?;
977 let mut selected = confirmed.clone();
978 selected.extend_from_slice(tentative);
979 selected.sort_unstable();
980 Ok(selected)
981 }
982
983 pub fn converged(&self) -> SklResult<bool> {
985 let tentative = self.tentative_features_.as_ref().ok_or_else(|| {
986 SklearsError::FitError(
987 "BorutaSelector not fitted: tentative_features_ missing".to_string(),
988 )
989 })?;
990 Ok(tentative.is_empty())
991 }
992}
993
994#[derive(Debug, Clone)]
996pub enum AggregationMethod {
997 Average,
999 WeightedAverage,
1001 Median,
1003 BordaCount,
1005 ReciprocalRankFusion,
1007 MajorityVoting,
1009}
1010
1011pub trait SelectorFunction: Send + Sync + std::fmt::Debug {
1013 fn compute_scores(
1015 &self,
1016 x: &Array2<f64>,
1017 y_classif: Option<&Array1<i32>>,
1018 y_regression: Option<&Array1<f64>>,
1019 ) -> SklResult<Array1<f64>>;
1020 fn name(&self) -> &str;
1022 fn clone_box(&self) -> Box<dyn SelectorFunction>;
1024}
1025
1026impl Clone for Box<dyn SelectorFunction> {
1027 fn clone(&self) -> Self {
1028 self.clone_box()
1029 }
1030}
1031
1032#[derive(Debug, Clone)]
1034pub struct UnivariateSelector {
1035 method: UnivariateMethod,
1036}
1037
1038#[derive(Debug, Clone)]
1039pub enum UnivariateMethod {
1041 Chi2,
1043 FClassif,
1045 FRegression,
1047 MutualInfoClassif,
1049 MutualInfoRegression,
1051 VarianceThreshold(f64),
1053}
1054
1055impl UnivariateSelector {
1056 pub fn new(method: UnivariateMethod) -> Self {
1058 Self { method }
1059 }
1060}
1061
1062impl SelectorFunction for UnivariateSelector {
1063 fn compute_scores(
1064 &self,
1065 x: &Array2<f64>,
1066 y_classif: Option<&Array1<i32>>,
1067 y_regression: Option<&Array1<f64>>,
1068 ) -> SklResult<Array1<f64>> {
1069 use crate::statistical_tests::*;
1070
1071 match &self.method {
1072 UnivariateMethod::Chi2 => {
1073 let y = y_classif.ok_or_else(|| {
1074 SklearsError::InvalidInput("Chi2 requires classification target".to_string())
1075 })?;
1076 let y_f64 = y.mapv(|v| v as f64);
1077 let (scores, _) = chi2(&x.view(), &y_f64.view())
1078 .map_err(|e| SklearsError::FitError(format!("Chi2 test failed: {:?}", e)))?;
1079 Ok(scores)
1080 }
1081 UnivariateMethod::FClassif => {
1082 let y = y_classif.ok_or_else(|| {
1083 SklearsError::InvalidInput(
1084 "F-classif requires classification target".to_string(),
1085 )
1086 })?;
1087 let y_f64 = y.mapv(|v| v as f64);
1088 let (scores, _) = f_classif(&x.view(), &y_f64.view())
1089 .map_err(|e| SklearsError::FitError(format!("F-test failed: {:?}", e)))?;
1090 Ok(scores)
1091 }
1092 UnivariateMethod::FRegression => {
1093 let y = y_regression.ok_or_else(|| {
1094 SklearsError::InvalidInput(
1095 "F-regression requires regression target".to_string(),
1096 )
1097 })?;
1098 let (scores, _) = f_regression(&x.view(), &y.view())
1099 .map_err(|e| SklearsError::FitError(format!("F-regression failed: {:?}", e)))?;
1100 Ok(scores)
1101 }
1102 UnivariateMethod::MutualInfoClassif => {
1103 let y = y_classif.ok_or_else(|| {
1104 SklearsError::InvalidInput(
1105 "Mutual info classif requires classification target".to_string(),
1106 )
1107 })?;
1108 let y_f64 = y.mapv(|v| v as f64);
1109 mutual_info_classif(&x.view(), &y_f64.view()).map_err(|e| {
1110 SklearsError::FitError(format!("Mutual info classif failed: {:?}", e))
1111 })
1112 }
1113 UnivariateMethod::MutualInfoRegression => {
1114 let y = y_regression.ok_or_else(|| {
1115 SklearsError::InvalidInput(
1116 "Mutual info regression requires regression target".to_string(),
1117 )
1118 })?;
1119 mutual_info_regression(&x.view(), &y.view()).map_err(|e| {
1120 SklearsError::FitError(format!("Mutual info regression failed: {:?}", e))
1121 })
1122 }
1123 UnivariateMethod::VarianceThreshold(_threshold) => {
1124 let variances = x
1125 .axis_iter(Axis(1))
1126 .map(|col| {
1127 let mean = col.mean().unwrap_or(0.0);
1128 col.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0)
1129 })
1130 .collect::<Vec<f64>>();
1131 Ok(Array1::from(variances))
1132 }
1133 }
1134 }
1135
1136 fn name(&self) -> &str {
1137 match &self.method {
1138 UnivariateMethod::Chi2 => "chi2",
1139 UnivariateMethod::FClassif => "f_classif",
1140 UnivariateMethod::FRegression => "f_regression",
1141 UnivariateMethod::MutualInfoClassif => "mutual_info_classif",
1142 UnivariateMethod::MutualInfoRegression => "mutual_info_regression",
1143 UnivariateMethod::VarianceThreshold(_) => "variance_threshold",
1144 }
1145 }
1146
1147 fn clone_box(&self) -> Box<dyn SelectorFunction> {
1148 Box::new(self.clone())
1149 }
1150}
1151
1152#[derive(Debug, Clone)]
1155pub struct EnsembleFeatureRanking<State = Untrained> {
1156 selectors: Vec<Box<dyn SelectorFunction>>,
1157 aggregation_method: AggregationMethod,
1158 weights: Option<Vec<f64>>,
1159 state: PhantomData<State>,
1160 rankings_: Option<Vec<Array1<f64>>>,
1162 final_ranking_: Option<Array1<f64>>,
1163 n_features_: Option<usize>,
1164}
1165
1166impl Default for EnsembleFeatureRanking<Untrained> {
1167 fn default() -> Self {
1168 Self::new()
1169 }
1170}
1171
1172impl EnsembleFeatureRanking<Untrained> {
1173 pub fn new() -> Self {
1175 Self {
1176 selectors: Vec::new(),
1177 aggregation_method: AggregationMethod::Average,
1178 weights: None,
1179 state: PhantomData,
1180 rankings_: None,
1181 final_ranking_: None,
1182 n_features_: None,
1183 }
1184 }
1185
1186 pub fn add_selector(mut self, selector: Box<dyn SelectorFunction>) -> Self {
1188 self.selectors.push(selector);
1189 self
1190 }
1191
1192 pub fn aggregation_method(mut self, method: AggregationMethod) -> Self {
1194 self.aggregation_method = method;
1195 self
1196 }
1197
1198 pub fn weights(mut self, weights: Vec<f64>) -> Self {
1200 self.weights = Some(weights);
1201 self
1202 }
1203
1204 pub fn add_univariate_selectors_classif(mut self) -> Self {
1206 self.selectors
1207 .push(Box::new(UnivariateSelector::new(UnivariateMethod::Chi2)));
1208 self.selectors.push(Box::new(UnivariateSelector::new(
1209 UnivariateMethod::FClassif,
1210 )));
1211 self.selectors.push(Box::new(UnivariateSelector::new(
1212 UnivariateMethod::MutualInfoClassif,
1213 )));
1214 self
1215 }
1216
1217 pub fn add_univariate_selectors_regression(mut self) -> Self {
1219 self.selectors.push(Box::new(UnivariateSelector::new(
1220 UnivariateMethod::FRegression,
1221 )));
1222 self.selectors.push(Box::new(UnivariateSelector::new(
1223 UnivariateMethod::MutualInfoRegression,
1224 )));
1225 self.selectors.push(Box::new(UnivariateSelector::new(
1226 UnivariateMethod::VarianceThreshold(0.0),
1227 )));
1228 self
1229 }
1230
1231 pub fn add_tree_selector(mut self) -> Self {
1233 self.selectors.push(Box::new(TreeImportanceSelector::new()));
1234 self
1235 }
1236}
1237
1238#[derive(Debug, Clone)]
1241pub struct TreeImportanceSelector {}
1242
1243impl Default for TreeImportanceSelector {
1244 fn default() -> Self {
1245 Self::new()
1246 }
1247}
1248
1249impl TreeImportanceSelector {
1250 pub fn new() -> Self {
1252 Self {}
1253 }
1254}
1255
1256impl SelectorFunction for TreeImportanceSelector {
1257 fn compute_scores(
1258 &self,
1259 x: &Array2<f64>,
1260 _y_classif: Option<&Array1<i32>>,
1261 _y_regression: Option<&Array1<f64>>,
1262 ) -> SklResult<Array1<f64>> {
1263 let n_features = x.ncols();
1265 Ok(Array1::ones(n_features))
1266 }
1267
1268 fn name(&self) -> &str {
1269 "tree_importance"
1270 }
1271
1272 fn clone_box(&self) -> Box<dyn SelectorFunction> {
1273 Box::new(self.clone())
1274 }
1275}
1276
1277impl Fit<Array2<f64>, Array1<i32>> for EnsembleFeatureRanking<Untrained> {
1278 type Fitted = EnsembleFeatureRanking<Trained>;
1279
1280 fn fit(self, x: &Array2<f64>, y: &Array1<i32>) -> SklResult<Self::Fitted> {
1281 validate::check_consistent_length(x, y)?;
1282
1283 let n_features = x.ncols();
1284 let n_selectors = self.selectors.len();
1285
1286 if n_selectors == 0 {
1287 return Err(SklearsError::InvalidInput(
1288 "No selectors added to ensemble".to_string(),
1289 ));
1290 }
1291
1292 let mut rankings = Vec::with_capacity(n_selectors);
1294
1295 for selector in &self.selectors {
1296 let scores = selector.compute_scores(x, Some(y), None)?;
1297 rankings.push(scores);
1298 }
1299
1300 let final_ranking = self.aggregate_rankings(&rankings)?;
1302
1303 Ok(EnsembleFeatureRanking {
1304 selectors: self.selectors,
1305 aggregation_method: self.aggregation_method,
1306 weights: self.weights,
1307 state: PhantomData,
1308 rankings_: Some(rankings),
1309 final_ranking_: Some(final_ranking),
1310 n_features_: Some(n_features),
1311 })
1312 }
1313}
1314
1315impl Fit<Array2<f64>, Array1<f64>> for EnsembleFeatureRanking<Untrained> {
1316 type Fitted = EnsembleFeatureRanking<Trained>;
1317
1318 fn fit(self, x: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
1319 validate::check_consistent_length(x, y)?;
1320
1321 let n_features = x.ncols();
1322 let n_selectors = self.selectors.len();
1323
1324 if n_selectors == 0 {
1325 return Err(SklearsError::InvalidInput(
1326 "No selectors added to ensemble".to_string(),
1327 ));
1328 }
1329
1330 let mut rankings = Vec::with_capacity(n_selectors);
1332
1333 for selector in &self.selectors {
1334 let scores = selector.compute_scores(x, None, Some(y))?;
1335 rankings.push(scores);
1336 }
1337
1338 let final_ranking = self.aggregate_rankings(&rankings)?;
1340
1341 Ok(EnsembleFeatureRanking {
1342 selectors: self.selectors,
1343 aggregation_method: self.aggregation_method,
1344 weights: self.weights,
1345 state: PhantomData,
1346 rankings_: Some(rankings),
1347 final_ranking_: Some(final_ranking),
1348 n_features_: Some(n_features),
1349 })
1350 }
1351}
1352
1353impl EnsembleFeatureRanking<Untrained> {
1354 fn aggregate_rankings(&self, rankings: &[Array1<f64>]) -> SklResult<Array1<f64>> {
1356 let n_features = rankings[0].len();
1357 let n_selectors = rankings.len();
1358
1359 match &self.aggregation_method {
1360 AggregationMethod::Average => {
1361 let mut aggregated = Array1::<f64>::zeros(n_features);
1362 for ranking in rankings {
1363 aggregated += ranking;
1364 }
1365 aggregated /= n_selectors as f64;
1366 Ok(aggregated)
1367 }
1368
1369 AggregationMethod::WeightedAverage => {
1370 let weights = self.weights.as_ref().ok_or_else(|| {
1371 SklearsError::InvalidInput("Weights required for weighted average".to_string())
1372 })?;
1373
1374 if weights.len() != n_selectors {
1375 return Err(SklearsError::InvalidInput(
1376 "Number of weights must match number of selectors".to_string(),
1377 ));
1378 }
1379
1380 let mut aggregated = Array1::<f64>::zeros(n_features);
1381 let weight_sum: f64 = weights.iter().sum();
1382
1383 for (ranking, &weight) in rankings.iter().zip(weights.iter()) {
1384 aggregated += &(ranking * weight);
1385 }
1386 aggregated /= weight_sum;
1387 Ok(aggregated)
1388 }
1389
1390 AggregationMethod::Median => {
1391 let mut aggregated = Array1::<f64>::zeros(n_features);
1392 for i in 0..n_features {
1393 let mut feature_scores: Vec<f64> = rankings.iter().map(|r| r[i]).collect();
1394 feature_scores
1395 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1396
1397 if n_selectors.is_multiple_of(2) {
1398 aggregated[i] = (feature_scores[n_selectors / 2 - 1]
1399 + feature_scores[n_selectors / 2])
1400 / 2.0;
1401 } else {
1402 aggregated[i] = feature_scores[n_selectors / 2];
1403 }
1404 }
1405 Ok(aggregated)
1406 }
1407
1408 AggregationMethod::BordaCount => {
1409 let mut aggregated = Array1::<f64>::zeros(n_features);
1411
1412 for ranking in rankings {
1413 let ranks = self.scores_to_ranks(ranking);
1414 aggregated += &ranks;
1415 }
1416
1417 let max_rank = aggregated.iter().fold(0.0f64, |acc, &x| acc.max(x));
1419 aggregated.mapv_inplace(|rank| max_rank - rank);
1420 Ok(aggregated)
1421 }
1422
1423 AggregationMethod::ReciprocalRankFusion => {
1424 let mut aggregated = Array1::<f64>::zeros(n_features);
1425
1426 for ranking in rankings {
1427 let ranks = self.scores_to_ranks(ranking);
1428 for (i, &rank) in ranks.iter().enumerate() {
1429 aggregated[i] += 1.0 / (rank + 60.0); }
1431 }
1432
1433 Ok(aggregated)
1434 }
1435
1436 AggregationMethod::MajorityVoting => {
1437 let mut vote_count = Array1::<f64>::zeros(n_features);
1440
1441 for ranking in rankings {
1442 let mut sorted_scores = ranking.to_vec();
1444 sorted_scores
1445 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1446 let threshold = sorted_scores[sorted_scores.len() / 2];
1447
1448 for (i, &score) in ranking.iter().enumerate() {
1449 if score >= threshold {
1450 vote_count[i] += 1.0;
1451 }
1452 }
1453 }
1454
1455 Ok(vote_count)
1456 }
1457 }
1458 }
1459
1460 fn scores_to_ranks(&self, scores: &Array1<f64>) -> Array1<f64> {
1462 let mut indexed_scores: Vec<(usize, f64)> = scores
1463 .indexed_iter()
1464 .map(|(idx, &score)| (idx, score))
1465 .collect();
1466
1467 indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1469
1470 let mut ranks = Array1::<f64>::zeros(scores.len());
1471 for (rank, &(idx, _)) in indexed_scores.iter().enumerate() {
1472 ranks[idx] = rank as f64;
1473 }
1474
1475 ranks
1476 }
1477}
1478
1479impl EnsembleFeatureRanking<Trained> {
1480 pub fn feature_importances(&self) -> SklResult<&Array1<f64>> {
1482 self.final_ranking_.as_ref().ok_or_else(|| {
1483 SklearsError::FitError(
1484 "EnsembleFeatureRanking not fitted: final_ranking_ missing".to_string(),
1485 )
1486 })
1487 }
1488
1489 pub fn individual_rankings(&self) -> SklResult<&[Array1<f64>]> {
1491 self.rankings_.as_deref().ok_or_else(|| {
1492 SklearsError::FitError(
1493 "EnsembleFeatureRanking not fitted: rankings_ missing".to_string(),
1494 )
1495 })
1496 }
1497
1498 pub fn select_k_best(&self, k: usize) -> SklResult<Vec<usize>> {
1500 let ranking = self.final_ranking_.as_ref().ok_or_else(|| {
1501 SklearsError::FitError(
1502 "EnsembleFeatureRanking not fitted: final_ranking_ missing".to_string(),
1503 )
1504 })?;
1505 let mut indexed_scores: Vec<(usize, f64)> = ranking
1506 .indexed_iter()
1507 .map(|(idx, &score)| (idx, score))
1508 .collect();
1509
1510 indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1511 Ok(indexed_scores
1512 .into_iter()
1513 .take(k)
1514 .map(|(idx, _)| idx)
1515 .collect())
1516 }
1517
1518 pub fn select_by_threshold(&self, threshold: f64) -> SklResult<Vec<usize>> {
1520 let ranking = self.final_ranking_.as_ref().ok_or_else(|| {
1521 SklearsError::FitError(
1522 "EnsembleFeatureRanking not fitted: final_ranking_ missing".to_string(),
1523 )
1524 })?;
1525 Ok(ranking
1526 .indexed_iter()
1527 .filter_map(|(idx, &score)| if score >= threshold { Some(idx) } else { None })
1528 .collect())
1529 }
1530}
1531
1532impl Transform<Array2<f64>> for EnsembleFeatureRanking<Trained> {
1533 fn transform(&self, x: &Array2<f64>) -> SklResult<Array2<f64>> {
1534 let n_features = self.n_features_.ok_or_else(|| {
1535 SklearsError::FitError(
1536 "EnsembleFeatureRanking not fitted: n_features_ missing".to_string(),
1537 )
1538 })?;
1539 if x.ncols() != n_features {
1540 return Err(SklearsError::InvalidInput(format!(
1541 "Expected {} features, got {}",
1542 n_features,
1543 x.ncols()
1544 )));
1545 }
1546
1547 let selected = self.select_k_best(n_features / 2)?;
1548 let n_samples = x.nrows();
1549 let n_selected = selected.len();
1550
1551 let mut x_new = Array2::<f64>::zeros((n_samples, n_selected));
1552 for (new_idx, &old_idx) in selected.iter().enumerate() {
1553 x_new.column_mut(new_idx).assign(&x.column(old_idx));
1554 }
1555
1556 Ok(x_new)
1557 }
1558}
1559
1560impl SelectorMixin for EnsembleFeatureRanking<Trained> {
1561 fn get_support(&self) -> SklResult<Array1<bool>> {
1562 let n_features = self.n_features_.ok_or_else(|| {
1564 SklearsError::FitError(
1565 "EnsembleFeatureRanking not fitted: n_features_ missing".to_string(),
1566 )
1567 })?;
1568 let k = n_features / 2;
1569 let selected = self.select_k_best(k)?;
1570
1571 let mut support = vec![false; n_features];
1572 for &idx in &selected {
1573 support[idx] = true;
1574 }
1575 Ok(Array1::from(support))
1576 }
1577
1578 fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
1579 let n_features = self.n_features_.ok_or_else(|| {
1580 SklearsError::FitError(
1581 "EnsembleFeatureRanking not fitted: n_features_ missing".to_string(),
1582 )
1583 })?;
1584 let selected = self.select_k_best(n_features / 2)?;
1585 Ok(indices
1586 .iter()
1587 .filter_map(|&idx| selected.iter().position(|&f| f == idx))
1588 .collect())
1589 }
1590}