Skip to main content

sklears_feature_selection/
embedded.rs

1//! Embedded feature selection methods
2
3use crate::FeatureImportance;
4use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Dim, OwnedRepr};
5use scirs2_core::rand_prelude::SliceRandom;
6use scirs2_core::random::{rngs::StdRng, thread_rng, SeedableRng};
7use sklears_core::{
8    error::{validate, Result as SklResult, SklearsError},
9    traits::{Estimator, Fit, Trained, Transform, Untrained},
10    types::Float,
11};
12use std::marker::PhantomData;
13
14use crate::base::SelectorMixin;
15
16// ndarray 0.17 3-param form to avoid trait-solver normalization failure in where bounds
17type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
18type Vec1f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 1]>, f64>;
19type Vec1i32 = ArrayBase<OwnedRepr<i32>, Dim<[usize; 1]>, i32>;
20type Vec1usize = ArrayBase<OwnedRepr<usize>, Dim<[usize; 1]>, usize>;
21
22// Re-export regularization selectors for backward compatibility
23pub use crate::regularization_selectors::{ElasticNetSelector, LassoSelector, RidgeSelector};
24
25// Re-export tree-based selectors for backward compatibility
26pub use crate::tree_based_selectors::{
27    GradientBoostingSelector, TreeImportance, TreeImportanceSelector, TreeSelector,
28};
29
30// Re-export ensemble selectors for backward compatibility
31pub use crate::ensemble_selectors::{
32    extract_features, AggregationMethod, BootstrapSelector, BorutaSelector, EnsembleFeatureRanking,
33    SelectorFunction, UnivariateMethod, UnivariateSelector,
34};
35
36// Re-export genetic optimization components for backward compatibility
37pub use crate::genetic_optimization::{
38    CostSensitiveObjective, CrossValidator, FairnessAwareObjective, FairnessMetric,
39    FeatureCountObjective, FeatureDiversityObjective, FeatureImportanceObjective, GeneticSelector,
40    Individual, KFold, MultiObjectiveFeatureSelector, MultiObjectiveMethod, ObjectiveFunction,
41    PredictivePerformanceObjective,
42};
43
44/// Stability selection using randomized feature selection with subsampling
45///
46/// Stability selection combines subsampling with feature selection algorithms to provide
47/// reproducible and stable feature selection results. It selects features that are consistently
48/// chosen across different bootstrap samples of the data.
49#[derive(Debug, Clone)]
50pub struct StabilitySelector<E, State = Untrained> {
51    estimator: E,
52    n_bootstrap: usize,
53    subsample_fraction: f64,
54    selection_threshold: f64,
55    max_features: Option<usize>,
56    random_state: Option<u64>,
57    state: PhantomData<State>,
58    // Trained state
59    selection_probabilities_: Option<Array1<Float>>,
60    stable_features_: Option<Vec<usize>>,
61    n_features_: Option<usize>,
62    bootstrap_results_: Option<Vec<Vec<usize>>>,
63}
64
65impl<E> StabilitySelector<E, Untrained> {
66    /// Create a new stability selector
67    pub fn new(estimator: E) -> Self {
68        Self {
69            estimator,
70            n_bootstrap: 100,
71            subsample_fraction: 0.5,
72            selection_threshold: 0.6,
73            max_features: None,
74            random_state: None,
75            state: PhantomData,
76            selection_probabilities_: None,
77            stable_features_: None,
78            n_features_: None,
79            bootstrap_results_: None,
80        }
81    }
82
83    /// Set the number of bootstrap iterations
84    pub fn n_bootstrap(mut self, n_bootstrap: usize) -> Result<Self, SklearsError> {
85        if n_bootstrap < 1 {
86            return Err(SklearsError::InvalidInput(
87                "n_bootstrap must be at least 1".to_string(),
88            ));
89        }
90        self.n_bootstrap = n_bootstrap;
91        Ok(self)
92    }
93
94    /// Set the fraction of samples to use in each bootstrap iteration
95    pub fn subsample_fraction(mut self, fraction: f64) -> Result<Self, SklearsError> {
96        if fraction <= 0.0 || fraction > 1.0 {
97            return Err(SklearsError::InvalidInput(
98                "subsample_fraction must be in (0, 1]".to_string(),
99            ));
100        }
101        self.subsample_fraction = fraction;
102        Ok(self)
103    }
104
105    /// Set the threshold for feature selection stability
106    /// Features selected in at least this fraction of bootstrap iterations are considered stable
107    pub fn selection_threshold(mut self, threshold: f64) -> Result<Self, SklearsError> {
108        if threshold <= 0.0 || threshold > 1.0 {
109            return Err(SklearsError::InvalidInput(
110                "selection_threshold must be in (0, 1]".to_string(),
111            ));
112        }
113        self.selection_threshold = threshold;
114        Ok(self)
115    }
116
117    /// Set maximum number of features to select (if None, all stable features are selected)
118    pub fn max_features(mut self, max_features: Option<usize>) -> Self {
119        self.max_features = max_features;
120        self
121    }
122
123    /// Set random state for reproducible results
124    pub fn random_state(mut self, seed: u64) -> Self {
125        self.random_state = Some(seed);
126        self
127    }
128}
129
130impl<E> Default for StabilitySelector<E, Untrained>
131where
132    E: Default,
133{
134    fn default() -> Self {
135        Self::new(E::default())
136    }
137}
138
139impl<E> Estimator for StabilitySelector<E, Untrained>
140where
141    E: Estimator,
142{
143    type Config = E::Config;
144    type Error = SklearsError;
145    type Float = f64;
146
147    fn config(&self) -> &Self::Config {
148        self.estimator.config()
149    }
150}
151
152macro_rules! impl_fit_for_stability_selector {
153    ($y_type:ty) => {
154        impl<E> Fit<Mat2f64, $y_type> for StabilitySelector<E, Untrained>
155        where
156            E: Clone + Fit<Mat2f64, $y_type> + Send + Sync,
157            E::Fitted: FeatureImportance + Send + Sync,
158        {
159            type Fitted = StabilitySelector<E, Trained>;
160
161            fn fit(self, x: &Mat2f64, y: &$y_type) -> SklResult<Self::Fitted> {
162                let (n_samples, n_features) = x.dim();
163
164                if n_samples < 2 {
165                    return Err(SklearsError::InvalidInput(
166                        "Need at least 2 samples for stability selection".to_string(),
167                    ));
168                }
169
170                let subsample_size =
171                    ((n_samples as f64) * self.subsample_fraction).max(1.0) as usize;
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 mut feature_selection_counts = vec![0usize; n_features];
181                let mut bootstrap_results = Vec::with_capacity(self.n_bootstrap);
182
183                // Perform bootstrap iterations
184                for _ in 0..self.n_bootstrap {
185                    // Create bootstrap sample by subsampling without replacement
186                    let mut indices: Vec<usize> = (0..n_samples).collect();
187                    indices.shuffle(&mut rng);
188                    indices.truncate(subsample_size);
189
190                    // Extract bootstrap sample
191                    let x_bootstrap = x.select(Axis(0), &indices);
192                    let y_bootstrap = y.clone();
193
194                    // Fit estimator on bootstrap sample
195                    let fitted_estimator =
196                        self.estimator.clone().fit(&x_bootstrap, &y_bootstrap)?;
197
198                    // Get feature importances
199                    let importances = fitted_estimator.feature_importances()?;
200
201                    // Select features based on importance threshold (top 50% by default)
202                    let mut indexed_importances: Vec<(usize, Float)> = (0..importances.len())
203                        .map(|i| (i, importances[i]))
204                        .collect();
205
206                    // Sort by importance in descending order
207                    indexed_importances.sort_by(|a, b| {
208                        b.1.partial_cmp(&a.1).unwrap_or_else(|| {
209                            match (b.1.is_nan(), a.1.is_nan()) {
210                                (true, false) => std::cmp::Ordering::Less,
211                                (false, true) => std::cmp::Ordering::Greater,
212                                _ => std::cmp::Ordering::Equal,
213                            }
214                        })
215                    });
216
217                    // Select top features (half of the features by default)
218                    let n_select = self.max_features.unwrap_or(n_features / 2).min(n_features);
219                    let selected_features: Vec<usize> = indexed_importances
220                        .into_iter()
221                        .take(n_select)
222                        .map(|(i, _)| i)
223                        .collect();
224
225                    // Update selection counts
226                    for &feature_idx in &selected_features {
227                        feature_selection_counts[feature_idx] += 1;
228                    }
229
230                    bootstrap_results.push(selected_features);
231                }
232
233                // Calculate selection probabilities
234                let selection_probabilities = Array1::from_iter(
235                    feature_selection_counts
236                        .iter()
237                        .map(|&count| count as f64 / self.n_bootstrap as f64),
238                );
239
240                // Select stable features based on threshold
241                let stable_features: Vec<usize> = selection_probabilities
242                    .iter()
243                    .enumerate()
244                    .filter(|(_, &prob)| prob >= self.selection_threshold)
245                    .map(|(i, _)| i)
246                    .collect();
247
248                // Apply max_features constraint if specified
249                let final_stable_features = if let Some(max_feat) = self.max_features {
250                    if stable_features.len() > max_feat {
251                        // If we have more stable features than max_features, take the most stable ones
252                        let mut indexed_probs: Vec<(usize, f64)> = stable_features
253                            .iter()
254                            .map(|&i| (i, selection_probabilities[i]))
255                            .collect();
256
257                        indexed_probs.sort_by(|a, b| {
258                            b.1.partial_cmp(&a.1).unwrap_or_else(|| {
259                                match (b.1.is_nan(), a.1.is_nan()) {
260                                    (true, false) => std::cmp::Ordering::Less,
261                                    (false, true) => std::cmp::Ordering::Greater,
262                                    _ => std::cmp::Ordering::Equal,
263                                }
264                            })
265                        });
266                        indexed_probs
267                            .into_iter()
268                            .take(max_feat)
269                            .map(|(i, _)| i)
270                            .collect()
271                    } else {
272                        stable_features
273                    }
274                } else {
275                    stable_features
276                };
277
278                Ok(StabilitySelector {
279                    estimator: self.estimator,
280                    n_bootstrap: self.n_bootstrap,
281                    subsample_fraction: self.subsample_fraction,
282                    selection_threshold: self.selection_threshold,
283                    max_features: self.max_features,
284                    random_state: self.random_state,
285                    state: PhantomData,
286                    selection_probabilities_: Some(selection_probabilities),
287                    stable_features_: Some(final_stable_features),
288                    n_features_: Some(n_features),
289                    bootstrap_results_: Some(bootstrap_results),
290                })
291            }
292        }
293    };
294}
295
296impl_fit_for_stability_selector!(Vec1f64);
297impl_fit_for_stability_selector!(Vec1i32);
298impl_fit_for_stability_selector!(Vec1usize);
299
300impl<E> Transform<Array2<Float>> for StabilitySelector<E, Trained> {
301    fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
302        let selected_features = self.stable_features_.as_ref().ok_or_else(|| {
303            SklearsError::FitError(
304                "StabilitySelector not fitted: stable_features_ missing".to_string(),
305            )
306        })?;
307        if selected_features.is_empty() {
308            return Err(SklearsError::InvalidInput(
309                "No features were selected".to_string(),
310            ));
311        }
312
313        // Extract selected features
314        let result = x.select(Axis(1), selected_features);
315        Ok(result)
316    }
317}
318
319impl<E> SelectorMixin for StabilitySelector<E, Trained> {
320    fn get_support(&self) -> SklResult<Array1<bool>> {
321        let n_features = self.n_features_.ok_or_else(|| {
322            SklearsError::FitError("StabilitySelector not fitted: n_features_ missing".to_string())
323        })?;
324        let selected = self.stable_features_.as_ref().ok_or_else(|| {
325            SklearsError::FitError(
326                "StabilitySelector not fitted: stable_features_ missing".to_string(),
327            )
328        })?;
329        let mut support = Array1::<bool>::from_elem(n_features, false);
330        for &idx in selected {
331            support[idx] = true;
332        }
333        Ok(support)
334    }
335
336    fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
337        let selected = self.stable_features_.as_ref().ok_or_else(|| {
338            SklearsError::FitError(
339                "StabilitySelector not fitted: stable_features_ missing".to_string(),
340            )
341        })?;
342        let selected_set: std::collections::HashSet<usize> = selected.iter().cloned().collect();
343
344        let transformed: Vec<usize> = indices
345            .iter()
346            .filter(|&&idx| selected_set.contains(&idx))
347            .enumerate()
348            .map(|(new_idx, _)| new_idx)
349            .collect();
350
351        Ok(transformed)
352    }
353}
354
355impl<E> StabilitySelector<E, Trained> {
356    /// Get the selection probabilities for all features
357    pub fn selection_probabilities(&self) -> SklResult<&Array1<Float>> {
358        self.selection_probabilities_.as_ref().ok_or_else(|| {
359            SklearsError::FitError(
360                "StabilitySelector not fitted: selection_probabilities_ missing".to_string(),
361            )
362        })
363    }
364
365    /// Get the stable features
366    pub fn stable_features(&self) -> SklResult<&[usize]> {
367        self.stable_features_.as_deref().ok_or_else(|| {
368            SklearsError::FitError(
369                "StabilitySelector not fitted: stable_features_ missing".to_string(),
370            )
371        })
372    }
373
374    /// Get all bootstrap selection results for analysis
375    pub fn bootstrap_results(&self) -> SklResult<&[Vec<usize>]> {
376        self.bootstrap_results_.as_deref().ok_or_else(|| {
377            SklearsError::FitError(
378                "StabilitySelector not fitted: bootstrap_results_ missing".to_string(),
379            )
380        })
381    }
382
383    /// Calculate the stability score as the average selection probability of selected features
384    pub fn stability_score(&self) -> SklResult<f64> {
385        let probs = self.selection_probabilities_.as_ref().ok_or_else(|| {
386            SklearsError::FitError(
387                "StabilitySelector not fitted: selection_probabilities_ missing".to_string(),
388            )
389        })?;
390        let selected = self.stable_features_.as_ref().ok_or_else(|| {
391            SklearsError::FitError(
392                "StabilitySelector not fitted: stable_features_ missing".to_string(),
393            )
394        })?;
395
396        if selected.is_empty() {
397            return Ok(0.0);
398        }
399
400        let sum: f64 = selected.iter().map(|&i| probs[i]).sum();
401        Ok(sum / selected.len() as f64)
402    }
403
404    /// Get the number of features in the original dataset
405    pub fn n_features(&self) -> SklResult<usize> {
406        self.n_features_.ok_or_else(|| {
407            SklearsError::FitError("StabilitySelector not fitted: n_features_ missing".to_string())
408        })
409    }
410}
411
412/// Consensus feature selection using multiple selectors and voting mechanisms
413/// Focuses on feature selection agreement rather than ranking aggregation
414#[derive(Debug, Clone)]
415pub struct ConsensusFeatureSelector<State = Untrained> {
416    selectors: Vec<Box<dyn SelectorFunction>>,
417    consensus_method: ConsensusMethod,
418    threshold_params: ConsensusThresholdParams,
419    state: PhantomData<State>,
420    // Trained state
421    selector_decisions_: Option<Vec<Vec<bool>>>, // Each selector's binary decisions
422    consensus_scores_: Option<Array1<f64>>,      // Final consensus scores
423    selected_features_: Option<Vec<usize>>,      // Final selected features
424    n_features_: Option<usize>,
425}
426
427/// Method for reaching consensus among selectors
428#[derive(Debug, Clone)]
429pub enum ConsensusMethod {
430    /// Simple majority voting (more than 50% agree)
431    MajorityVoting,
432    /// Unanimous agreement required
433    Unanimous,
434    /// Super majority required (configurable threshold)
435    SuperMajority(f64), // threshold between 0.5 and 1.0
436    /// Weighted voting with selector weights
437    WeightedVoting,
438    /// Intersection of all selections
439    Intersection,
440    /// Union of all selections
441    Union,
442    /// Select features that appear in at least k selectors
443    KOutOfN(usize), // k selectors
444    /// Borda count consensus
445    BordaConsensus,
446    /// Condorcet voting method
447    CondorcetConsensus,
448}
449
450/// Parameters for consensus thresholding
451#[derive(Debug, Clone)]
452pub struct ConsensusThresholdParams {
453    /// Number of top features to select from each method
454    pub k_per_selector: Option<usize>,
455    /// Percentage of top features to select from each method
456    pub percentile_per_selector: Option<f64>,
457    /// Absolute threshold for each selector
458    pub score_threshold_per_selector: Option<f64>,
459    /// Weights for weighted voting (if using WeightedVoting)
460    pub selector_weights: Option<Vec<f64>>,
461}
462
463impl Default for ConsensusThresholdParams {
464    fn default() -> Self {
465        Self {
466            k_per_selector: Some(10), // Select top 10 features from each selector by default
467            percentile_per_selector: None,
468            score_threshold_per_selector: None,
469            selector_weights: None,
470        }
471    }
472}
473
474impl Default for ConsensusFeatureSelector<Untrained> {
475    fn default() -> Self {
476        Self::new()
477    }
478}
479
480impl ConsensusFeatureSelector<Untrained> {
481    /// Create a new consensus feature selector
482    pub fn new() -> Self {
483        Self {
484            selectors: Vec::new(),
485            consensus_method: ConsensusMethod::MajorityVoting,
486            threshold_params: ConsensusThresholdParams::default(),
487            state: PhantomData,
488            selector_decisions_: None,
489            consensus_scores_: None,
490            selected_features_: None,
491            n_features_: None,
492        }
493    }
494
495    /// Add a selector to the consensus
496    pub fn add_selector(mut self, selector: Box<dyn SelectorFunction>) -> Self {
497        self.selectors.push(selector);
498        self
499    }
500
501    /// Set the consensus method
502    pub fn consensus_method(mut self, method: ConsensusMethod) -> Self {
503        self.consensus_method = method;
504        self
505    }
506
507    /// Set the threshold parameters
508    pub fn threshold_params(mut self, params: ConsensusThresholdParams) -> Self {
509        self.threshold_params = params;
510        self
511    }
512
513    /// Set number of top features to select per selector
514    pub fn k_per_selector(mut self, k: usize) -> Self {
515        self.threshold_params.k_per_selector = Some(k);
516        self
517    }
518
519    /// Set percentile of features to select per selector
520    pub fn percentile_per_selector(mut self, percentile: f64) -> Result<Self, SklearsError> {
521        if percentile <= 0.0 || percentile >= 1.0 {
522            return Err(SklearsError::InvalidInput(
523                "Percentile must be between 0 and 1".to_string(),
524            ));
525        }
526        self.threshold_params.percentile_per_selector = Some(percentile);
527        Ok(self)
528    }
529
530    /// Convenience method to add common selectors for classification
531    pub fn add_common_selectors_classif(mut self) -> Self {
532        self.selectors
533            .push(Box::new(UnivariateSelector::new(UnivariateMethod::Chi2)));
534        self.selectors.push(Box::new(UnivariateSelector::new(
535            UnivariateMethod::FClassif,
536        )));
537        self.selectors.push(Box::new(UnivariateSelector::new(
538            UnivariateMethod::MutualInfoClassif,
539        )));
540        self.selectors.push(Box::new(TreeImportanceSelector::new()));
541        self
542    }
543
544    /// Convenience method to add common selectors for regression
545    pub fn add_common_selectors_regression(mut self) -> Self {
546        self.selectors.push(Box::new(UnivariateSelector::new(
547            UnivariateMethod::FRegression,
548        )));
549        self.selectors.push(Box::new(UnivariateSelector::new(
550            UnivariateMethod::MutualInfoRegression,
551        )));
552        self.selectors.push(Box::new(UnivariateSelector::new(
553            UnivariateMethod::VarianceThreshold(0.0),
554        )));
555        self.selectors.push(Box::new(TreeImportanceSelector::new()));
556        self
557    }
558}
559
560impl Fit<Array2<f64>, Array1<i32>> for ConsensusFeatureSelector<Untrained> {
561    type Fitted = ConsensusFeatureSelector<Trained>;
562
563    fn fit(self, x: &Array2<f64>, y: &Array1<i32>) -> SklResult<Self::Fitted> {
564        validate::check_consistent_length(x, y)?;
565
566        let n_features = x.ncols();
567        let n_selectors = self.selectors.len();
568
569        if n_selectors == 0 {
570            return Err(SklearsError::InvalidInput(
571                "No selectors added to consensus".to_string(),
572            ));
573        }
574
575        // Get binary decisions from each selector
576        let mut selector_decisions = Vec::with_capacity(n_selectors);
577
578        for selector in &self.selectors {
579            let scores = selector.compute_scores(x, Some(y), None)?;
580            let decisions = self.scores_to_binary_decisions(&scores, n_features)?;
581            selector_decisions.push(decisions);
582        }
583
584        // Apply consensus method
585        let (consensus_scores, selected_features) = self.apply_consensus(&selector_decisions)?;
586
587        Ok(ConsensusFeatureSelector {
588            selectors: self.selectors,
589            consensus_method: self.consensus_method,
590            threshold_params: self.threshold_params,
591            state: PhantomData,
592            selector_decisions_: Some(selector_decisions),
593            consensus_scores_: Some(consensus_scores),
594            selected_features_: Some(selected_features),
595            n_features_: Some(n_features),
596        })
597    }
598}
599
600impl Fit<Array2<f64>, Array1<f64>> for ConsensusFeatureSelector<Untrained> {
601    type Fitted = ConsensusFeatureSelector<Trained>;
602
603    fn fit(self, x: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
604        validate::check_consistent_length(x, y)?;
605
606        let n_features = x.ncols();
607        let n_selectors = self.selectors.len();
608
609        if n_selectors == 0 {
610            return Err(SklearsError::InvalidInput(
611                "No selectors added to consensus".to_string(),
612            ));
613        }
614
615        // Get binary decisions from each selector
616        let mut selector_decisions = Vec::with_capacity(n_selectors);
617
618        for selector in &self.selectors {
619            let scores = selector.compute_scores(x, None, Some(y))?;
620            let decisions = self.scores_to_binary_decisions(&scores, n_features)?;
621            selector_decisions.push(decisions);
622        }
623
624        // Apply consensus method
625        let (consensus_scores, selected_features) = self.apply_consensus(&selector_decisions)?;
626
627        Ok(ConsensusFeatureSelector {
628            selectors: self.selectors,
629            consensus_method: self.consensus_method,
630            threshold_params: self.threshold_params,
631            state: PhantomData,
632            selector_decisions_: Some(selector_decisions),
633            consensus_scores_: Some(consensus_scores),
634            selected_features_: Some(selected_features),
635            n_features_: Some(n_features),
636        })
637    }
638}
639
640impl ConsensusFeatureSelector<Untrained> {
641    /// Convert feature scores to binary selection decisions
642    fn scores_to_binary_decisions(
643        &self,
644        scores: &Array1<f64>,
645        n_features: usize,
646    ) -> SklResult<Vec<bool>> {
647        let mut decisions = vec![false; n_features];
648
649        if let Some(k) = self.threshold_params.k_per_selector {
650            // Select top k features
651            let mut indexed_scores: Vec<(usize, f64)> = scores
652                .indexed_iter()
653                .map(|(idx, &score)| (idx, score))
654                .collect();
655
656            indexed_scores
657                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
658
659            for (idx, _) in indexed_scores.into_iter().take(k.min(n_features)) {
660                decisions[idx] = true;
661            }
662        } else if let Some(percentile) = self.threshold_params.percentile_per_selector {
663            // Select top percentile of features
664            let k = ((n_features as f64) * percentile).ceil() as usize;
665            let mut indexed_scores: Vec<(usize, f64)> = scores
666                .indexed_iter()
667                .map(|(idx, &score)| (idx, score))
668                .collect();
669
670            indexed_scores
671                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
672
673            for (idx, _) in indexed_scores.into_iter().take(k) {
674                decisions[idx] = true;
675            }
676        } else if let Some(threshold) = self.threshold_params.score_threshold_per_selector {
677            // Select features above threshold
678            for (idx, &score) in scores.iter().enumerate() {
679                if score >= threshold {
680                    decisions[idx] = true;
681                }
682            }
683        } else {
684            // Default: select top 10 features
685            let mut indexed_scores: Vec<(usize, f64)> = scores
686                .indexed_iter()
687                .map(|(idx, &score)| (idx, score))
688                .collect();
689
690            indexed_scores
691                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
692
693            for (idx, _) in indexed_scores.into_iter().take(10.min(n_features)) {
694                decisions[idx] = true;
695            }
696        }
697
698        Ok(decisions)
699    }
700
701    /// Apply consensus method to selector decisions
702    fn apply_consensus(
703        &self,
704        selector_decisions: &[Vec<bool>],
705    ) -> SklResult<(Array1<f64>, Vec<usize>)> {
706        let n_features = selector_decisions[0].len();
707        let n_selectors = selector_decisions.len();
708
709        let mut consensus_scores = Array1::<f64>::zeros(n_features);
710
711        match &self.consensus_method {
712            ConsensusMethod::MajorityVoting => {
713                for i in 0..n_features {
714                    let votes: usize = selector_decisions
715                        .iter()
716                        .map(|decisions| if decisions[i] { 1 } else { 0 })
717                        .sum();
718                    consensus_scores[i] = votes as f64 / n_selectors as f64;
719                }
720                // Select features with > 50% support
721                let selected_features = consensus_scores
722                    .indexed_iter()
723                    .filter_map(|(idx, &score)| if score > 0.5 { Some(idx) } else { None })
724                    .collect();
725                Ok((consensus_scores, selected_features))
726            }
727
728            ConsensusMethod::Unanimous => {
729                for i in 0..n_features {
730                    let all_agree = selector_decisions.iter().all(|decisions| decisions[i]);
731                    consensus_scores[i] = if all_agree { 1.0 } else { 0.0 };
732                }
733                let selected_features = consensus_scores
734                    .indexed_iter()
735                    .filter_map(|(idx, &score)| if score == 1.0 { Some(idx) } else { None })
736                    .collect();
737                Ok((consensus_scores, selected_features))
738            }
739
740            ConsensusMethod::SuperMajority(threshold) => {
741                for i in 0..n_features {
742                    let votes: usize = selector_decisions
743                        .iter()
744                        .map(|decisions| if decisions[i] { 1 } else { 0 })
745                        .sum();
746                    let support = votes as f64 / n_selectors as f64;
747                    consensus_scores[i] = support;
748                }
749                let selected_features = consensus_scores
750                    .indexed_iter()
751                    .filter_map(|(idx, &score)| if score >= *threshold { Some(idx) } else { None })
752                    .collect();
753                Ok((consensus_scores, selected_features))
754            }
755
756            ConsensusMethod::WeightedVoting => {
757                let weights = self
758                    .threshold_params
759                    .selector_weights
760                    .as_ref()
761                    .ok_or_else(|| {
762                        SklearsError::InvalidInput(
763                            "Weights required for weighted voting".to_string(),
764                        )
765                    })?;
766
767                if weights.len() != n_selectors {
768                    return Err(SklearsError::InvalidInput(
769                        "Number of weights must match number of selectors".to_string(),
770                    ));
771                }
772
773                let weight_sum: f64 = weights.iter().sum();
774
775                for i in 0..n_features {
776                    let weighted_votes: f64 = selector_decisions
777                        .iter()
778                        .zip(weights.iter())
779                        .map(|(decisions, &weight)| if decisions[i] { weight } else { 0.0 })
780                        .sum();
781                    consensus_scores[i] = weighted_votes / weight_sum;
782                }
783
784                let selected_features = consensus_scores
785                    .indexed_iter()
786                    .filter_map(|(idx, &score)| if score > 0.5 { Some(idx) } else { None })
787                    .collect();
788                Ok((consensus_scores, selected_features))
789            }
790
791            ConsensusMethod::Intersection => {
792                for i in 0..n_features {
793                    let all_selected = selector_decisions.iter().all(|decisions| decisions[i]);
794                    consensus_scores[i] = if all_selected { 1.0 } else { 0.0 };
795                }
796                let selected_features = consensus_scores
797                    .indexed_iter()
798                    .filter_map(|(idx, &score)| if score == 1.0 { Some(idx) } else { None })
799                    .collect();
800                Ok((consensus_scores, selected_features))
801            }
802
803            ConsensusMethod::Union => {
804                for i in 0..n_features {
805                    let any_selected = selector_decisions.iter().any(|decisions| decisions[i]);
806                    consensus_scores[i] = if any_selected { 1.0 } else { 0.0 };
807                }
808                let selected_features = consensus_scores
809                    .indexed_iter()
810                    .filter_map(|(idx, &score)| if score == 1.0 { Some(idx) } else { None })
811                    .collect();
812                Ok((consensus_scores, selected_features))
813            }
814
815            ConsensusMethod::KOutOfN(k) => {
816                for i in 0..n_features {
817                    let votes: usize = selector_decisions
818                        .iter()
819                        .map(|decisions| if decisions[i] { 1 } else { 0 })
820                        .sum();
821                    consensus_scores[i] = votes as f64;
822                }
823                let selected_features = consensus_scores
824                    .indexed_iter()
825                    .filter_map(|(idx, &score)| if score >= *k as f64 { Some(idx) } else { None })
826                    .collect();
827                Ok((consensus_scores, selected_features))
828            }
829
830            ConsensusMethod::BordaConsensus => {
831                // For Borda, we need the original scores, not just binary decisions
832                // This is a simplified version using vote counts
833                for i in 0..n_features {
834                    let votes: usize = selector_decisions
835                        .iter()
836                        .map(|decisions| if decisions[i] { 1 } else { 0 })
837                        .sum();
838                    consensus_scores[i] = votes as f64;
839                }
840
841                // Select features in Borda order (top voted)
842                let mut indexed_scores: Vec<(usize, f64)> = consensus_scores
843                    .indexed_iter()
844                    .map(|(idx, &score)| (idx, score))
845                    .collect();
846
847                indexed_scores
848                    .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
849
850                // Select top half by default
851                let k = n_features / 2;
852                let selected_features = indexed_scores
853                    .into_iter()
854                    .take(k)
855                    .map(|(idx, _)| idx)
856                    .collect();
857                Ok((consensus_scores, selected_features))
858            }
859
860            ConsensusMethod::CondorcetConsensus => {
861                // Simplified Condorcet: select features that beat majority in pairwise comparisons
862                for i in 0..n_features {
863                    let votes: usize = selector_decisions
864                        .iter()
865                        .map(|decisions| if decisions[i] { 1 } else { 0 })
866                        .sum();
867                    consensus_scores[i] = votes as f64 / n_selectors as f64;
868                }
869
870                let selected_features = consensus_scores
871                    .indexed_iter()
872                    .filter_map(|(idx, &score)| if score > 0.5 { Some(idx) } else { None })
873                    .collect();
874                Ok((consensus_scores, selected_features))
875            }
876        }
877    }
878}
879
880impl ConsensusFeatureSelector<Trained> {
881    /// Get the consensus scores for all features
882    pub fn consensus_scores(&self) -> SklResult<&Array1<f64>> {
883        self.consensus_scores_.as_ref().ok_or_else(|| {
884            SklearsError::FitError(
885                "ConsensusFeatureSelector not fitted: consensus_scores_ missing".to_string(),
886            )
887        })
888    }
889
890    /// Get the final selected features
891    pub fn selected_features(&self) -> SklResult<&[usize]> {
892        self.selected_features_.as_deref().ok_or_else(|| {
893            SklearsError::FitError(
894                "ConsensusFeatureSelector not fitted: selected_features_ missing".to_string(),
895            )
896        })
897    }
898
899    /// Get individual selector decisions
900    pub fn selector_decisions(&self) -> SklResult<&[Vec<bool>]> {
901        self.selector_decisions_.as_deref().ok_or_else(|| {
902            SklearsError::FitError(
903                "ConsensusFeatureSelector not fitted: selector_decisions_ missing".to_string(),
904            )
905        })
906    }
907
908    /// Get support mask for selected features
909    pub fn get_support_mask(&self) -> SklResult<Vec<bool>> {
910        let n_features = self.n_features_.ok_or_else(|| {
911            SklearsError::FitError(
912                "ConsensusFeatureSelector not fitted: n_features_ missing".to_string(),
913            )
914        })?;
915        let mut support = vec![false; n_features];
916
917        for &idx in self.selected_features()? {
918            support[idx] = true;
919        }
920
921        Ok(support)
922    }
923
924    /// Get agreement matrix between selectors
925    pub fn selector_agreement_matrix(&self) -> SklResult<Array2<f64>> {
926        let decisions = self.selector_decisions_.as_ref().ok_or_else(|| {
927            SklearsError::FitError(
928                "ConsensusFeatureSelector not fitted: selector_decisions_ missing".to_string(),
929            )
930        })?;
931        let n_selectors = decisions.len();
932        let n_features = decisions[0].len();
933
934        let mut agreement_matrix = Array2::<f64>::zeros((n_selectors, n_selectors));
935
936        for i in 0..n_selectors {
937            for j in 0..n_selectors {
938                if i == j {
939                    agreement_matrix[[i, j]] = 1.0;
940                } else {
941                    let mut agreements = 0;
942                    for k in 0..n_features {
943                        if decisions[i][k] == decisions[j][k] {
944                            agreements += 1;
945                        }
946                    }
947                    agreement_matrix[[i, j]] = agreements as f64 / n_features as f64;
948                }
949            }
950        }
951
952        Ok(agreement_matrix)
953    }
954}
955
956impl Transform<Array2<f64>> for ConsensusFeatureSelector<Trained> {
957    fn transform(&self, x: &Array2<f64>) -> SklResult<Array2<f64>> {
958        let n_features = self.n_features_.ok_or_else(|| {
959            SklearsError::FitError(
960                "ConsensusFeatureSelector not fitted: n_features_ missing".to_string(),
961            )
962        })?;
963        if x.ncols() != n_features {
964            return Err(SklearsError::InvalidInput(format!(
965                "Expected {} features, got {}",
966                n_features,
967                x.ncols()
968            )));
969        }
970
971        let selected = self.selected_features()?;
972        let n_samples = x.nrows();
973        let n_selected = selected.len();
974
975        let mut x_new = Array2::<f64>::zeros((n_samples, n_selected));
976        for (new_idx, &old_idx) in selected.iter().enumerate() {
977            x_new.column_mut(new_idx).assign(&x.column(old_idx));
978        }
979
980        Ok(x_new)
981    }
982}
983
984impl SelectorMixin for ConsensusFeatureSelector<Trained> {
985    fn get_support(&self) -> SklResult<Array1<bool>> {
986        Ok(Array1::from(self.get_support_mask()?))
987    }
988
989    fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
990        let selected = self.selected_features()?;
991        Ok(indices
992            .iter()
993            .filter_map(|&idx| selected.iter().position(|&f| f == idx))
994            .collect())
995    }
996}
997
998/// Multi-task feature selection
999/// Selects features that are relevant across multiple related tasks simultaneously
1000#[derive(Debug, Clone)]
1001pub struct MultiTaskFeatureSelector<State = Untrained> {
1002    n_features_to_select: usize,
1003    task_weights: Option<Vec<f64>>,
1004    regularization: f64,
1005    max_iter: usize,
1006    tolerance: f64,
1007    random_state: Option<u64>,
1008    state: PhantomData<State>,
1009    // Trained state
1010    selected_features_: Option<Vec<usize>>,
1011    feature_scores_: Option<Array2<f64>>, // scores per task
1012    task_specific_scores_: Option<Vec<Array1<f64>>>, // task-specific feature importance
1013    n_features_: Option<usize>,
1014    _n_tasks_: Option<usize>,
1015}
1016
1017impl MultiTaskFeatureSelector<Untrained> {
1018    /// Create a new multi-task feature selector
1019    pub fn new() -> Self {
1020        Self {
1021            n_features_to_select: 10,
1022            task_weights: None,
1023            regularization: 0.1,
1024            max_iter: 1000,
1025            tolerance: 1e-4,
1026            random_state: None,
1027            state: PhantomData,
1028            selected_features_: None,
1029            feature_scores_: None,
1030            task_specific_scores_: None,
1031            n_features_: None,
1032            _n_tasks_: None,
1033        }
1034    }
1035
1036    /// Set the number of features to select
1037    pub fn n_features_to_select(mut self, n_features: usize) -> Self {
1038        self.n_features_to_select = n_features;
1039        self
1040    }
1041
1042    /// Set task weights (if None, all tasks are weighted equally)
1043    pub fn task_weights(mut self, weights: Option<Vec<f64>>) -> Self {
1044        self.task_weights = weights;
1045        self
1046    }
1047
1048    /// Set regularization strength for task coupling
1049    pub fn regularization(mut self, reg: f64) -> Self {
1050        self.regularization = reg.max(0.0);
1051        self
1052    }
1053
1054    /// Set maximum iterations for optimization
1055    pub fn max_iter(mut self, max_iter: usize) -> Self {
1056        self.max_iter = max_iter;
1057        self
1058    }
1059
1060    /// Set convergence tolerance
1061    pub fn tolerance(mut self, tolerance: f64) -> Self {
1062        self.tolerance = tolerance;
1063        self
1064    }
1065
1066    /// Set random state for reproducibility
1067    pub fn random_state(mut self, seed: Option<u64>) -> Self {
1068        self.random_state = seed;
1069        self
1070    }
1071}
1072
1073impl Default for MultiTaskFeatureSelector<Untrained> {
1074    fn default() -> Self {
1075        Self::new()
1076    }
1077}
1078
1079impl Estimator for MultiTaskFeatureSelector<Untrained> {
1080    type Config = ();
1081    type Error = SklearsError;
1082    type Float = f64;
1083
1084    fn config(&self) -> &Self::Config {
1085        &()
1086    }
1087}
1088
1089impl Fit<Array2<f64>, Vec<Array1<f64>>> for MultiTaskFeatureSelector<Untrained> {
1090    type Fitted = MultiTaskFeatureSelector<Trained>;
1091
1092    fn fit(self, x: &Array2<f64>, y_tasks: &Vec<Array1<f64>>) -> SklResult<Self::Fitted> {
1093        let (n_samples, n_features) = x.dim();
1094        let n_tasks = y_tasks.len();
1095
1096        if n_tasks == 0 {
1097            return Err(SklearsError::InvalidInput(
1098                "At least one task is required".to_string(),
1099            ));
1100        }
1101
1102        // Validate that all tasks have the same number of samples
1103        for (i, y_task) in y_tasks.iter().enumerate() {
1104            if y_task.len() != n_samples {
1105                return Err(SklearsError::InvalidInput(format!(
1106                    "Task {} has {} samples, expected {}",
1107                    i,
1108                    y_task.len(),
1109                    n_samples
1110                )));
1111            }
1112        }
1113
1114        if self.n_features_to_select > n_features {
1115            return Err(SklearsError::InvalidInput(format!(
1116                "Cannot select {} features from {} available",
1117                self.n_features_to_select, n_features
1118            )));
1119        }
1120
1121        // Set task weights
1122        let task_weights = self
1123            .task_weights
1124            .clone()
1125            .unwrap_or_else(|| vec![1.0 / n_tasks as f64; n_tasks]);
1126
1127        if task_weights.len() != n_tasks {
1128            return Err(SklearsError::InvalidInput(format!(
1129                "Task weights length {} doesn't match number of tasks {}",
1130                task_weights.len(),
1131                n_tasks
1132            )));
1133        }
1134
1135        // Calculate feature scores for each task
1136        let mut task_scores = Vec::with_capacity(n_tasks);
1137        let mut feature_scores = Array2::<f64>::zeros((n_features, n_tasks));
1138
1139        for (task_idx, y_task) in y_tasks.iter().enumerate() {
1140            let mut scores = Array1::<f64>::zeros(n_features);
1141
1142            // Calculate correlation-based scores for each feature
1143            for feature_idx in 0..n_features {
1144                let feature_col = x.column(feature_idx).to_owned();
1145                let correlation = compute_pearson_correlation(&feature_col, y_task).abs();
1146                scores[feature_idx] = correlation;
1147            }
1148
1149            // Store task-specific scores
1150            for (feature_idx, &score) in scores.iter().enumerate() {
1151                feature_scores[[feature_idx, task_idx]] = score;
1152            }
1153
1154            task_scores.push(scores);
1155        }
1156
1157        // Multi-task feature selection using joint optimization
1158        let selected_features = self.select_multi_task_features(
1159            &feature_scores,
1160            &task_weights,
1161            self.n_features_to_select,
1162        )?;
1163
1164        Ok(MultiTaskFeatureSelector {
1165            n_features_to_select: self.n_features_to_select,
1166            task_weights: Some(task_weights),
1167            regularization: self.regularization,
1168            max_iter: self.max_iter,
1169            tolerance: self.tolerance,
1170            random_state: self.random_state,
1171            state: PhantomData,
1172            selected_features_: Some(selected_features),
1173            feature_scores_: Some(feature_scores),
1174            task_specific_scores_: Some(task_scores),
1175            n_features_: Some(n_features),
1176            _n_tasks_: Some(n_tasks),
1177        })
1178    }
1179}
1180
1181impl MultiTaskFeatureSelector<Untrained> {
1182    /// Select features using multi-task optimization
1183    fn select_multi_task_features(
1184        &self,
1185        feature_scores: &Array2<f64>,
1186        task_weights: &[f64],
1187        n_select: usize,
1188    ) -> SklResult<Vec<usize>> {
1189        let (n_features, n_tasks) = feature_scores.dim();
1190
1191        // Calculate weighted average scores across tasks
1192        let mut weighted_scores = Array1::<f64>::zeros(n_features);
1193
1194        for feature_idx in 0..n_features {
1195            let mut weighted_score = 0.0;
1196
1197            for task_idx in 0..n_tasks {
1198                weighted_score += task_weights[task_idx] * feature_scores[[feature_idx, task_idx]];
1199            }
1200
1201            // Add regularization term to encourage feature sharing across tasks
1202            let mut task_consistency = 0.0;
1203            for task_idx in 0..n_tasks {
1204                for other_task_idx in 0..n_tasks {
1205                    if task_idx != other_task_idx {
1206                        let diff = feature_scores[[feature_idx, task_idx]]
1207                            - feature_scores[[feature_idx, other_task_idx]];
1208                        task_consistency -= self.regularization * diff * diff; // Penalty for inconsistency
1209                    }
1210                }
1211            }
1212            task_consistency /= (n_tasks * (n_tasks - 1)) as f64; // Normalize
1213
1214            weighted_scores[feature_idx] = weighted_score + task_consistency;
1215        }
1216
1217        // Select top features based on weighted scores
1218        let mut feature_indices: Vec<usize> = (0..n_features).collect();
1219        feature_indices.sort_by(|&a, &b| {
1220            weighted_scores[b]
1221                .partial_cmp(&weighted_scores[a])
1222                .unwrap_or(std::cmp::Ordering::Equal)
1223        });
1224
1225        Ok(feature_indices.into_iter().take(n_select).collect())
1226    }
1227}
1228
1229impl MultiTaskFeatureSelector<Trained> {
1230    /// Get selected features
1231    pub fn get_selected_features(&self) -> SklResult<&[usize]> {
1232        self.selected_features_.as_deref().ok_or_else(|| {
1233            SklearsError::FitError(
1234                "MultiTaskFeatureSelector not fitted: selected_features_ missing".to_string(),
1235            )
1236        })
1237    }
1238
1239    /// Get feature scores for all tasks
1240    pub fn get_feature_scores(&self) -> SklResult<&Array2<f64>> {
1241        self.feature_scores_.as_ref().ok_or_else(|| {
1242            SklearsError::FitError(
1243                "MultiTaskFeatureSelector not fitted: feature_scores_ missing".to_string(),
1244            )
1245        })
1246    }
1247
1248    /// Get task-specific feature scores
1249    pub fn get_task_scores(&self) -> SklResult<&[Array1<f64>]> {
1250        self.task_specific_scores_.as_deref().ok_or_else(|| {
1251            SklearsError::FitError(
1252                "MultiTaskFeatureSelector not fitted: task_specific_scores_ missing".to_string(),
1253            )
1254        })
1255    }
1256
1257    /// Get the most important features for a specific task
1258    pub fn get_task_specific_features(
1259        &self,
1260        task_idx: usize,
1261        n_features: usize,
1262    ) -> SklResult<Vec<usize>> {
1263        let task_scores = self.task_specific_scores_.as_ref().ok_or_else(|| {
1264            SklearsError::FitError(
1265                "MultiTaskFeatureSelector not fitted: task_specific_scores_ missing".to_string(),
1266            )
1267        })?;
1268
1269        if task_idx >= task_scores.len() {
1270            return Err(SklearsError::InvalidInput(format!(
1271                "Task index {} out of range",
1272                task_idx
1273            )));
1274        }
1275
1276        let scores = &task_scores[task_idx];
1277        let mut feature_indices: Vec<usize> = (0..scores.len()).collect();
1278        feature_indices.sort_by(|&a, &b| {
1279            scores[b]
1280                .partial_cmp(&scores[a])
1281                .unwrap_or(std::cmp::Ordering::Equal)
1282        });
1283
1284        Ok(feature_indices.into_iter().take(n_features).collect())
1285    }
1286
1287    /// Get features that are consistently important across all tasks
1288    pub fn get_consensus_features(&self, threshold: f64) -> SklResult<Vec<usize>> {
1289        let feature_scores = self.feature_scores_.as_ref().ok_or_else(|| {
1290            SklearsError::FitError(
1291                "MultiTaskFeatureSelector not fitted: feature_scores_ missing".to_string(),
1292            )
1293        })?;
1294        let (n_features, n_tasks) = feature_scores.dim();
1295        let mut consensus_features = Vec::new();
1296
1297        for feature_idx in 0..n_features {
1298            let mut above_threshold_count = 0;
1299
1300            for task_idx in 0..n_tasks {
1301                if feature_scores[[feature_idx, task_idx]] >= threshold {
1302                    above_threshold_count += 1;
1303                }
1304            }
1305
1306            // Feature is consensus if it's above threshold for all tasks
1307            if above_threshold_count == n_tasks {
1308                consensus_features.push(feature_idx);
1309            }
1310        }
1311
1312        Ok(consensus_features)
1313    }
1314
1315    /// Get task similarity matrix based on feature importance patterns
1316    pub fn get_task_similarity(&self) -> SklResult<Array2<f64>> {
1317        let feature_scores = self.feature_scores_.as_ref().ok_or_else(|| {
1318            SklearsError::FitError(
1319                "MultiTaskFeatureSelector not fitted: feature_scores_ missing".to_string(),
1320            )
1321        })?;
1322        let (_, n_tasks) = feature_scores.dim();
1323        let mut similarity_matrix = Array2::<f64>::zeros((n_tasks, n_tasks));
1324
1325        for i in 0..n_tasks {
1326            for j in 0..n_tasks {
1327                if i == j {
1328                    similarity_matrix[[i, j]] = 1.0;
1329                } else {
1330                    let task_i_scores = feature_scores.column(i).to_owned();
1331                    let task_j_scores = feature_scores.column(j).to_owned();
1332                    let correlation = compute_pearson_correlation(&task_i_scores, &task_j_scores);
1333                    similarity_matrix[[i, j]] = correlation.abs();
1334                }
1335            }
1336        }
1337
1338        Ok(similarity_matrix)
1339    }
1340}
1341
1342impl Transform<Array2<f64>> for MultiTaskFeatureSelector<Trained> {
1343    fn transform(&self, x: &Array2<f64>) -> SklResult<Array2<f64>> {
1344        let n_features = self.n_features_.ok_or_else(|| {
1345            SklearsError::FitError(
1346                "MultiTaskFeatureSelector not fitted: n_features_ missing".to_string(),
1347            )
1348        })?;
1349        if x.ncols() != n_features {
1350            return Err(SklearsError::InvalidInput(format!(
1351                "Expected {} features, got {}",
1352                n_features,
1353                x.ncols()
1354            )));
1355        }
1356
1357        let selected = self.selected_features_.as_ref().ok_or_else(|| {
1358            SklearsError::FitError(
1359                "MultiTaskFeatureSelector not fitted: selected_features_ missing".to_string(),
1360            )
1361        })?;
1362        let n_samples = x.nrows();
1363        let n_selected = selected.len();
1364
1365        let mut x_new = Array2::<f64>::zeros((n_samples, n_selected));
1366        for (new_idx, &old_idx) in selected.iter().enumerate() {
1367            x_new.column_mut(new_idx).assign(&x.column(old_idx));
1368        }
1369
1370        Ok(x_new)
1371    }
1372}
1373
1374impl SelectorMixin for MultiTaskFeatureSelector<Trained> {
1375    fn get_support(&self) -> SklResult<Array1<bool>> {
1376        let selected_features = self.selected_features_.as_ref().ok_or_else(|| {
1377            SklearsError::FitError(
1378                "MultiTaskFeatureSelector not fitted: selected_features_ missing".to_string(),
1379            )
1380        })?;
1381        let n_features = self.n_features_.ok_or_else(|| {
1382            SklearsError::FitError(
1383                "MultiTaskFeatureSelector not fitted: n_features_ missing".to_string(),
1384            )
1385        })?;
1386        let mut support = vec![false; n_features];
1387
1388        for &idx in selected_features {
1389            support[idx] = true;
1390        }
1391
1392        Ok(Array1::from(support))
1393    }
1394
1395    fn transform_features(&self, indices: &[usize]) -> SklResult<Vec<usize>> {
1396        let selected = self.selected_features_.as_ref().ok_or_else(|| {
1397            SklearsError::FitError(
1398                "MultiTaskFeatureSelector not fitted: selected_features_ missing".to_string(),
1399            )
1400        })?;
1401        Ok(indices
1402            .iter()
1403            .filter_map(|&idx| selected.iter().position(|&f| f == idx))
1404            .collect())
1405    }
1406}
1407
1408/// Helper function to compute Pearson correlation between two arrays
1409fn compute_pearson_correlation(x: &Array1<f64>, y: &Array1<f64>) -> f64 {
1410    let n = x.len() as f64;
1411    if n < 2.0 {
1412        return 0.0;
1413    }
1414
1415    let mean_x = x.mean().unwrap_or(0.0);
1416    let mean_y = y.mean().unwrap_or(0.0);
1417
1418    let mut numerator = 0.0;
1419    let mut sum_sq_x = 0.0;
1420    let mut sum_sq_y = 0.0;
1421
1422    for (&xi, &yi) in x.iter().zip(y.iter()) {
1423        let dx = xi - mean_x;
1424        let dy = yi - mean_y;
1425        numerator += dx * dy;
1426        sum_sq_x += dx * dx;
1427        sum_sq_y += dy * dy;
1428    }
1429
1430    let denominator = (sum_sq_x * sum_sq_y).sqrt();
1431    if denominator == 0.0 {
1432        0.0
1433    } else {
1434        numerator / denominator
1435    }
1436}