Skip to main content

sklears_feature_selection/
ensemble_selectors.rs

1//! Ensemble-based feature selection methods
2//!
3//! This module provides feature selection methods that combine multiple approaches
4//! or use ensemble techniques for more robust feature selection.
5
6use 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
17// ndarray 0.17 adds a 3rd default type parameter `A = <S as RawData>::Elem` to ArrayBase.
18// The Rust trait solver cannot normalize `Array2<Float>` (2-param alias) to
19// `ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>` (3-param form) in where bounds when
20// any generic type parameters remain. We use the fully expanded form to bypass this.
21type 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
28/// Extract features from array based on selected indices
29pub 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/// Bootstrap-based Feature Selection
48/// Uses bootstrap resampling to estimate feature stability and importance
49#[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    // Trained state
60    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    /// Create a new BootstrapSelector
69    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    /// Set the number of bootstrap iterations
88    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    /// Set the sample ratio for each bootstrap iteration
99    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    /// Set the stability threshold (features selected in at least this fraction of bootstrap iterations)
110    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    /// Set the minimum selection frequency for a feature to be considered
121    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    /// Set the maximum number of features to select
132    pub fn max_features(mut self, max_features: Option<usize>) -> Self {
133        self.max_features = max_features;
134        self
135    }
136
137    /// Set the random state for reproducibility
138    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                // Initialize random number generator
174                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                // Bootstrap iterations
185                for _ in 0..self.n_bootstrap {
186                    // Generate bootstrap sample indices
187                    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                    // Extract bootstrap sample
193                    let x_bootstrap = x.select(Axis(0), &bootstrap_indices);
194                    let y_bootstrap = y.select(&bootstrap_indices);
195
196                    // Fit estimator on bootstrap sample
197                    match self.estimator.clone().fit(&x_bootstrap, &y_bootstrap) {
198                        Ok(fitted_estimator) => {
199                            // Get feature importances
200                            if let Ok(importances) = fitted_estimator.feature_importances() {
201                                // Find features with above-threshold importance
202                                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                                // Update feature counts
211                                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                            // If fitting fails for this bootstrap sample, skip it
220                            continue;
221                        }
222                    }
223                }
224
225                // Calculate selection frequencies
226                let feature_frequencies = feature_counts / self.n_bootstrap as f64;
227
228                // Calculate stability scores (Jaccard similarity between consecutive selections)
229                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                            // Simple stability measure: consistency across consecutive selections
239                            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                // Select features based on frequency and stability thresholds
253                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                // Apply max_features limit if specified
261                if let Some(max_feat) = self.max_features {
262                    if selected_features.len() > max_feat {
263                        // Sort by combined score (frequency * stability) and take top max_feat
264                        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(); // Restore original order
277                    }
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    /// Get feature selection frequencies across bootstrap iterations
361    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    /// Get feature stability scores
370    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    /// Get selected features
379    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    /// Get all bootstrap selections for analysis
388    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    /// Calculate the overall stability of the feature selection
397    /// Returns the average Jaccard similarity across all bootstrap iterations
398    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); // Perfect stability with only one selection
406        }
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/// Boruta feature selection algorithm
433/// Boruta is an all-relevant feature selection wrapper algorithm that finds all features
434/// carrying information usable for prediction, rather than finding a minimal feature subset.
435/// It works by creating shadow features (permuted versions) and comparing real feature
436/// importance against these random features.
437#[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    // Trained state
448    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    /// Create a new Boruta selector
459    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    /// Set the number of estimators for the ensemble
480    pub fn n_estimators(mut self, n_estimators: usize) -> Self {
481        self.n_estimators = n_estimators;
482        self
483    }
484
485    /// Set maximum number of iterations
486    pub fn max_iter(mut self, max_iter: usize) -> Self {
487        self.max_iter = max_iter;
488        self
489    }
490
491    /// Set the significance level alpha for statistical tests
492    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    /// Enable/disable two-step procedure for rejecting features
503    pub fn two_step(mut self, two_step: bool) -> Self {
504        self.two_step = two_step;
505        self
506    }
507
508    /// Set percentile of shadow feature importance used as threshold
509    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    /// Set random state for reproducibility
520    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            // Create augmented dataset with shadow features
569            let (x_augmented, _feature_mapping) =
570                self.create_shadow_features(x, &tentative_features, &mut rng)?;
571
572            // Train ensemble and get feature importances
573            let importances = self.get_ensemble_importances(&x_augmented, y)?;
574
575            // Calculate threshold based on shadow feature importances
576            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            // Update hit counts
581            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            // Statistical tests for feature confirmation/rejection
589            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                // Binomial test for confirmation
599                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                // Binomial test for rejection (if two-step enabled)
607                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            // Update feature lists
620            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        // Final feature importances (from last iteration)
638        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        // Remaining tentative features are treated as rejected if max_iter reached
646        if iteration >= self.max_iter {
647            rejected_features.extend(tentative_features.clone());
648            tentative_features.clear();
649        }
650
651        // Sort the lists for consistency
652        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    /// Create shadow features by permuting columns
682    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        // Create augmented matrix with original + shadow features
692        let mut x_augmented = Array2::<f64>::zeros((n_samples, n_features * 2));
693
694        // Copy original features
695        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        // Create shadow features by permuting
700        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    /// Get ensemble feature importances
715    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        // Simple ensemble - train multiple estimators and average importances
720        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        // Average the importances
728        total_importances /= self.n_estimators as Float;
729
730        Ok(total_importances)
731    }
732
733    /// Calculate threshold from shadow feature importances
734    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    /// Calculate critical value for statistical test
762    fn calculate_critical_value(&self, n_trials: usize) -> SklResult<f64> {
763        // Bonferroni correction for multiple testing
764        let corrected_alpha = self.alpha / n_trials as f64;
765        Ok(corrected_alpha)
766    }
767
768    /// Simple binomial test (approximated)
769    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        // Use normal approximation for large n
777        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                // Two-tailed test
785                2.0 * (1.0 - self.standard_normal_cdf(z_score.abs()))
786            } else {
787                1.0
788            }
789        } else {
790            // For small n, use exact binomial (simplified)
791            if (observed_prob - expected_prob).abs() < 1e-10 {
792                1.0
793            } else {
794                0.01 // Conservative estimate
795            }
796        }
797    }
798
799    /// Standard normal CDF approximation
800    fn standard_normal_cdf(&self, x: f64) -> f64 {
801        0.5 * (1.0 + self.erf(x / 2.0_f64.sqrt()))
802    }
803
804    /// Error function approximation
805    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        // Include confirmed features
869        for &idx in confirmed {
870            support[idx] = true;
871        }
872
873        // Optionally include tentative features
874        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    /// Get confirmed features (definitively relevant)
914    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    /// Get tentative features (undecided)
923    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    /// Get rejected features (definitively irrelevant)
932    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    /// Get feature importances from the final iteration
941    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    /// Get shadow feature importances from the final iteration
950    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    /// Get number of iterations performed
959    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    /// Get all selected features (confirmed + tentative)
966    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    /// Check if the algorithm converged (no tentative features remaining)
984    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/// Method for aggregating rankings from multiple selectors
995#[derive(Debug, Clone)]
996pub enum AggregationMethod {
997    /// Simple average of ranks
998    Average,
999    /// Weighted average of ranks
1000    WeightedAverage,
1001    /// Median of ranks
1002    Median,
1003    /// Borda count method
1004    BordaCount,
1005    /// Rank aggregation using sum of reciprocal ranks
1006    ReciprocalRankFusion,
1007    /// Majority voting (for feature selection rather than ranking)
1008    MajorityVoting,
1009}
1010
1011/// Trait for selector functions that can be used in ensemble
1012pub trait SelectorFunction: Send + Sync + std::fmt::Debug {
1013    /// fn
1014    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
1021    fn name(&self) -> &str;
1022    /// fn
1023    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/// Wrapper for univariate statistical tests
1033#[derive(Debug, Clone)]
1034pub struct UnivariateSelector {
1035    method: UnivariateMethod,
1036}
1037
1038#[derive(Debug, Clone)]
1039/// UnivariateMethod
1040pub enum UnivariateMethod {
1041    /// Chi2
1042    Chi2,
1043    /// FClassif
1044    FClassif,
1045    /// FRegression
1046    FRegression,
1047    /// MutualInfoClassif
1048    MutualInfoClassif,
1049    /// MutualInfoRegression
1050    MutualInfoRegression,
1051    /// VarianceThreshold
1052    VarianceThreshold(f64),
1053}
1054
1055impl UnivariateSelector {
1056    /// new
1057    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/// Ensemble feature ranking that combines multiple feature selection methods
1153/// Uses various ranking aggregation methods to combine rankings from different selectors
1154#[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    // Trained state
1161    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    /// Create a new ensemble feature ranking
1174    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    /// Add a selector to the ensemble
1187    pub fn add_selector(mut self, selector: Box<dyn SelectorFunction>) -> Self {
1188        self.selectors.push(selector);
1189        self
1190    }
1191
1192    /// Set the aggregation method
1193    pub fn aggregation_method(mut self, method: AggregationMethod) -> Self {
1194        self.aggregation_method = method;
1195        self
1196    }
1197
1198    /// Set weights for weighted aggregation
1199    pub fn weights(mut self, weights: Vec<f64>) -> Self {
1200        self.weights = Some(weights);
1201        self
1202    }
1203
1204    /// Convenience method to add common selectors
1205    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    /// Convenience method to add common selectors for regression
1218    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    /// Add tree-based importance selector
1232    pub fn add_tree_selector(mut self) -> Self {
1233        self.selectors.push(Box::new(TreeImportanceSelector::new()));
1234        self
1235    }
1236}
1237
1238/// Tree-based importance selector placeholder
1239/// This is a placeholder implementation - the actual implementation should use tree-based models
1240#[derive(Debug, Clone)]
1241pub struct TreeImportanceSelector {}
1242
1243impl Default for TreeImportanceSelector {
1244    fn default() -> Self {
1245        Self::new()
1246    }
1247}
1248
1249impl TreeImportanceSelector {
1250    /// new
1251    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        // Placeholder implementation - should use actual tree-based feature importance
1264        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        // Compute rankings from each selector
1293        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        // Aggregate rankings
1301        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        // Compute rankings from each selector
1331        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        // Aggregate rankings
1339        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    /// Aggregate rankings using the specified method
1355    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                // Convert scores to ranks, then aggregate ranks
1410                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                // Lower rank sum means better feature (invert for final score)
1418                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); // RRF with k=60
1430                    }
1431                }
1432
1433                Ok(aggregated)
1434            }
1435
1436            AggregationMethod::MajorityVoting => {
1437                // For majority voting, we need to threshold each ranking
1438                // This is more suitable for feature selection than ranking
1439                let mut vote_count = Array1::<f64>::zeros(n_features);
1440
1441                for ranking in rankings {
1442                    // Use median as threshold for each selector
1443                    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    /// Convert scores to ranks (higher score = lower rank number)
1461    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        // Sort by score descending (higher score gets lower rank)
1468        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    /// Get the final aggregated ranking
1481    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    /// Get rankings from individual selectors
1490    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    /// Select top k features based on ensemble ranking
1499    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    /// Select features based on threshold
1519    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        // Default: select top 50% of features
1563        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}