1use 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
16type 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
22pub use crate::regularization_selectors::{ElasticNetSelector, LassoSelector, RidgeSelector};
24
25pub use crate::tree_based_selectors::{
27 GradientBoostingSelector, TreeImportance, TreeImportanceSelector, TreeSelector,
28};
29
30pub use crate::ensemble_selectors::{
32 extract_features, AggregationMethod, BootstrapSelector, BorutaSelector, EnsembleFeatureRanking,
33 SelectorFunction, UnivariateMethod, UnivariateSelector,
34};
35
36pub 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#[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 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 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 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 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 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 pub fn max_features(mut self, max_features: Option<usize>) -> Self {
119 self.max_features = max_features;
120 self
121 }
122
123 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 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 for _ in 0..self.n_bootstrap {
185 let mut indices: Vec<usize> = (0..n_samples).collect();
187 indices.shuffle(&mut rng);
188 indices.truncate(subsample_size);
189
190 let x_bootstrap = x.select(Axis(0), &indices);
192 let y_bootstrap = y.clone();
193
194 let fitted_estimator =
196 self.estimator.clone().fit(&x_bootstrap, &y_bootstrap)?;
197
198 let importances = fitted_estimator.feature_importances()?;
200
201 let mut indexed_importances: Vec<(usize, Float)> = (0..importances.len())
203 .map(|i| (i, importances[i]))
204 .collect();
205
206 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 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 for &feature_idx in &selected_features {
227 feature_selection_counts[feature_idx] += 1;
228 }
229
230 bootstrap_results.push(selected_features);
231 }
232
233 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 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 let final_stable_features = if let Some(max_feat) = self.max_features {
250 if stable_features.len() > max_feat {
251 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 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 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 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 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 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 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#[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 selector_decisions_: Option<Vec<Vec<bool>>>, consensus_scores_: Option<Array1<f64>>, selected_features_: Option<Vec<usize>>, n_features_: Option<usize>,
425}
426
427#[derive(Debug, Clone)]
429pub enum ConsensusMethod {
430 MajorityVoting,
432 Unanimous,
434 SuperMajority(f64), WeightedVoting,
438 Intersection,
440 Union,
442 KOutOfN(usize), BordaConsensus,
446 CondorcetConsensus,
448}
449
450#[derive(Debug, Clone)]
452pub struct ConsensusThresholdParams {
453 pub k_per_selector: Option<usize>,
455 pub percentile_per_selector: Option<f64>,
457 pub score_threshold_per_selector: Option<f64>,
459 pub selector_weights: Option<Vec<f64>>,
461}
462
463impl Default for ConsensusThresholdParams {
464 fn default() -> Self {
465 Self {
466 k_per_selector: Some(10), 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 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 pub fn add_selector(mut self, selector: Box<dyn SelectorFunction>) -> Self {
497 self.selectors.push(selector);
498 self
499 }
500
501 pub fn consensus_method(mut self, method: ConsensusMethod) -> Self {
503 self.consensus_method = method;
504 self
505 }
506
507 pub fn threshold_params(mut self, params: ConsensusThresholdParams) -> Self {
509 self.threshold_params = params;
510 self
511 }
512
513 pub fn k_per_selector(mut self, k: usize) -> Self {
515 self.threshold_params.k_per_selector = Some(k);
516 self
517 }
518
519 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 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 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 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 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 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 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 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 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 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 for (idx, &score) in scores.iter().enumerate() {
679 if score >= threshold {
680 decisions[idx] = true;
681 }
682 }
683 } else {
684 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 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 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 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 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 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 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 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 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 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 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 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#[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 selected_features_: Option<Vec<usize>>,
1011 feature_scores_: Option<Array2<f64>>, task_specific_scores_: Option<Vec<Array1<f64>>>, n_features_: Option<usize>,
1014 _n_tasks_: Option<usize>,
1015}
1016
1017impl MultiTaskFeatureSelector<Untrained> {
1018 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 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 pub fn task_weights(mut self, weights: Option<Vec<f64>>) -> Self {
1044 self.task_weights = weights;
1045 self
1046 }
1047
1048 pub fn regularization(mut self, reg: f64) -> Self {
1050 self.regularization = reg.max(0.0);
1051 self
1052 }
1053
1054 pub fn max_iter(mut self, max_iter: usize) -> Self {
1056 self.max_iter = max_iter;
1057 self
1058 }
1059
1060 pub fn tolerance(mut self, tolerance: f64) -> Self {
1062 self.tolerance = tolerance;
1063 self
1064 }
1065
1066 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 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 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 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 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 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 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 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 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 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; }
1210 }
1211 }
1212 task_consistency /= (n_tasks * (n_tasks - 1)) as f64; weighted_scores[feature_idx] = weighted_score + task_consistency;
1215 }
1216
1217 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 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 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 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 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 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 if above_threshold_count == n_tasks {
1308 consensus_features.push(feature_idx);
1309 }
1310 }
1311
1312 Ok(consensus_features)
1313 }
1314
1315 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
1408fn 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}