1use crate::base::FeatureSelector;
7use scirs2_core::ndarray::{Array1, Array2, Axis};
8use sklears_core::{
9 error::{Result as SklResult, SklearsError},
10 traits::{Estimator, Fit, Trained, Transform, Untrained},
11 types::Float,
12};
13use std::collections::{HashMap, VecDeque};
14use std::marker::PhantomData;
15
16#[derive(Debug, Clone)]
21pub struct OnlineFeatureSelector<State = Untrained> {
22 k: usize,
24 window_size: Option<usize>,
25 decay_factor: f64,
26 min_samples: usize,
27
28 feature_means_: Option<Array1<Float>>,
30 feature_vars_: Option<Array1<Float>>,
31 sample_count_: usize,
32 target_correlation_: Option<Array1<Float>>,
33 selected_features_: Option<Vec<usize>>,
34 n_features_: Option<usize>,
35
36 window_data_: Option<VecDeque<Array1<Float>>>,
38 window_targets_: Option<VecDeque<Float>>,
39
40 state: PhantomData<State>,
41}
42
43impl OnlineFeatureSelector<Untrained> {
44 pub fn new(k: usize) -> Self {
46 Self {
47 k,
48 window_size: None,
49 decay_factor: 0.95,
50 min_samples: 10,
51 feature_means_: None,
52 feature_vars_: None,
53 sample_count_: 0,
54 target_correlation_: None,
55 selected_features_: None,
56 n_features_: None,
57 window_data_: None,
58 window_targets_: None,
59 state: PhantomData,
60 }
61 }
62
63 pub fn window_size(mut self, window_size: usize) -> Self {
65 self.window_size = Some(window_size);
66 self
67 }
68
69 pub fn decay_factor(mut self, decay_factor: f64) -> Result<Self, SklearsError> {
71 if !(0.0..=1.0).contains(&decay_factor) {
72 return Err(SklearsError::InvalidInput(
73 "decay_factor must be between 0 and 1".to_string(),
74 ));
75 }
76 self.decay_factor = decay_factor;
77 Ok(self)
78 }
79
80 pub fn min_samples(mut self, min_samples: usize) -> Self {
82 self.min_samples = min_samples;
83 self
84 }
85}
86
87impl Default for OnlineFeatureSelector<Untrained> {
88 fn default() -> Self {
89 Self::new(10)
90 }
91}
92
93impl Estimator for OnlineFeatureSelector<Untrained> {
94 type Config = ();
95 type Error = SklearsError;
96 type Float = f64;
97
98 fn config(&self) -> &Self::Config {
99 &()
100 }
101}
102
103impl Fit<Array2<Float>, Array1<Float>> for OnlineFeatureSelector<Untrained> {
104 type Fitted = OnlineFeatureSelector<Trained>;
105
106 fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
107 let (n_samples, n_features) = x.dim();
108 if n_samples == 0 || n_features == 0 {
109 return Err(SklearsError::InvalidInput(
110 "Input data cannot be empty".to_string(),
111 ));
112 }
113
114 if self.k > n_features {
115 return Err(SklearsError::InvalidInput(
116 "k cannot be larger than number of features".to_string(),
117 ));
118 }
119
120 let mut selector = OnlineFeatureSelector {
121 k: self.k,
122 window_size: self.window_size,
123 decay_factor: self.decay_factor,
124 min_samples: self.min_samples,
125 feature_means_: Some(Array1::zeros(n_features)),
126 feature_vars_: Some(Array1::zeros(n_features)),
127 sample_count_: 0,
128 target_correlation_: Some(Array1::zeros(n_features)),
129 selected_features_: Some(Vec::new()),
130 n_features_: Some(n_features),
131 window_data_: if self.window_size.is_some() {
132 Some(VecDeque::new())
133 } else {
134 None
135 },
136 window_targets_: if self.window_size.is_some() {
137 Some(VecDeque::new())
138 } else {
139 None
140 },
141 state: PhantomData,
142 };
143
144 for (sample_idx, target) in x.axis_iter(Axis(0)).zip(y.iter()) {
146 selector.partial_fit_sample(&sample_idx.to_owned(), *target)?;
147 }
148
149 Ok(selector)
150 }
151}
152
153impl OnlineFeatureSelector<Trained> {
154 pub fn partial_fit_sample(&mut self, sample: &Array1<Float>, target: Float) -> SklResult<()> {
156 let n_features = sample.len();
157
158 if let Some(expected_features) = self.n_features_ {
159 if n_features != expected_features {
160 return Err(SklearsError::InvalidInput(
161 "Sample has different number of features than expected".to_string(),
162 ));
163 }
164 } else {
165 self.n_features_ = Some(n_features);
166 self.feature_means_ = Some(Array1::zeros(n_features));
167 self.feature_vars_ = Some(Array1::zeros(n_features));
168 self.target_correlation_ = Some(Array1::zeros(n_features));
169 }
170
171 if let (Some(window_data), Some(window_targets)) =
173 (self.window_data_.as_mut(), self.window_targets_.as_mut())
174 {
175 if let Some(window_size) = self.window_size {
176 window_data.push_back(sample.clone());
177 window_targets.push_back(target);
178
179 if window_data.len() > window_size {
180 window_data.pop_front();
181 window_targets.pop_front();
182 }
183 }
184 }
185
186 if let (Some(means), Some(vars), Some(correlations)) = (
188 self.feature_means_.as_mut(),
189 self.feature_vars_.as_mut(),
190 self.target_correlation_.as_mut(),
191 ) {
192 self.sample_count_ += 1;
193 let alpha = if self.sample_count_ == 1 {
194 1.0
195 } else {
196 1.0 - self.decay_factor
197 };
198
199 for (i, &value) in sample.iter().enumerate() {
200 let old_mean = means[i];
202 means[i] = alpha * value + (1.0 - alpha) * old_mean;
203
204 let delta = value - old_mean;
206 let delta2 = value - means[i];
207 vars[i] = (1.0 - alpha) * vars[i] + alpha * delta * delta2;
208
209 let target_mean = 0.0; let target_centered = target - target_mean;
212 let feature_centered = value - means[i];
213 correlations[i] =
214 alpha * (feature_centered * target_centered) + (1.0 - alpha) * correlations[i];
215 }
216 }
217
218 if self.sample_count_ >= self.min_samples {
220 self.update_feature_selection()?;
221 }
222
223 Ok(())
224 }
225
226 fn update_feature_selection(&mut self) -> SklResult<()> {
228 if let Some(correlations) = &self.target_correlation_ {
229 let mut feature_scores: Vec<(usize, f64)> = correlations
231 .iter()
232 .enumerate()
233 .map(|(i, &corr)| (i, corr.abs()))
234 .collect();
235
236 feature_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
238
239 let selected: Vec<usize> = feature_scores
241 .into_iter()
242 .take(self.k)
243 .map(|(idx, _)| idx)
244 .collect();
245
246 self.selected_features_ = Some(selected);
247 }
248
249 Ok(())
250 }
251
252 pub fn compute_target_mean(&self) -> Float {
254 0.0
257 }
258
259 pub fn detect_concept_drift(&self) -> SklResult<bool> {
261 if let (Some(window_data), Some(window_targets)) =
262 (&self.window_data_, &self.window_targets_)
263 {
264 if window_data.len() < 20 {
265 return Ok(false); }
267
268 let mid = window_data.len() / 2;
270 let first_half_targets: Vec<Float> = window_targets.iter().take(mid).cloned().collect();
271 let second_half_targets: Vec<Float> =
272 window_targets.iter().skip(mid).cloned().collect();
273
274 let first_mean =
276 first_half_targets.iter().sum::<Float>() / first_half_targets.len() as Float;
277 let second_mean =
278 second_half_targets.iter().sum::<Float>() / second_half_targets.len() as Float;
279
280 let drift_threshold = 0.5;
282 Ok((first_mean - second_mean).abs() > drift_threshold)
283 } else {
284 Ok(false)
285 }
286 }
287
288 pub fn reset(&mut self) -> SklResult<()> {
290 if let Some(n_features) = self.n_features_ {
291 self.feature_means_ = Some(Array1::zeros(n_features));
292 self.feature_vars_ = Some(Array1::zeros(n_features));
293 self.target_correlation_ = Some(Array1::zeros(n_features));
294 self.sample_count_ = 0;
295
296 if let Some(window_data) = self.window_data_.as_mut() {
297 window_data.clear();
298 }
299 if let Some(window_targets) = self.window_targets_.as_mut() {
300 window_targets.clear();
301 }
302 }
303 Ok(())
304 }
305}
306
307impl FeatureSelector for OnlineFeatureSelector<Trained> {
308 fn selected_features(&self) -> &Vec<usize> {
309 match &self.selected_features_ {
310 Some(features) => features,
311 None => {
312 static EMPTY: Vec<usize> = Vec::new();
313 &EMPTY
314 }
315 }
316 }
317}
318
319impl Transform<Array2<Float>, Array2<Float>> for OnlineFeatureSelector<Trained> {
320 fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
321 if let Some(selected) = &self.selected_features_ {
322 if selected.is_empty() {
323 return Err(SklearsError::InvalidData {
324 reason: "No features selected yet".to_string(),
325 });
326 }
327
328 let selected_cols = x.select(Axis(1), selected);
329 Ok(selected_cols)
330 } else {
331 Err(SklearsError::InvalidData {
332 reason: "Selector not fitted yet".to_string(),
333 })
334 }
335 }
336}
337
338#[derive(Debug, Clone)]
342pub struct StreamingFeatureImportance {
343 decay_factor: f64,
345 _min_samples: usize,
346
347 importance_scores_: HashMap<usize, Float>,
349 sample_count_: usize,
350 n_features_: Option<usize>,
351}
352
353impl StreamingFeatureImportance {
354 pub fn new() -> Self {
356 Self {
357 decay_factor: 0.95,
358 _min_samples: 10,
359 importance_scores_: HashMap::new(),
360 sample_count_: 0,
361 n_features_: None,
362 }
363 }
364
365 pub fn decay_factor(mut self, decay_factor: f64) -> Result<Self, SklearsError> {
367 if !(0.0..=1.0).contains(&decay_factor) {
368 return Err(SklearsError::InvalidInput(
369 "decay_factor must be between 0 and 1".to_string(),
370 ));
371 }
372 self.decay_factor = decay_factor;
373 Ok(self)
374 }
375
376 pub fn update(
378 &mut self,
379 features: &Array1<Float>,
380 target: Float,
381 prediction: Float,
382 ) -> SklResult<()> {
383 let n_features = features.len();
384
385 if let Some(expected) = self.n_features_ {
386 if n_features != expected {
387 return Err(SklearsError::InvalidInput(
388 "Inconsistent number of features".to_string(),
389 ));
390 }
391 } else {
392 self.n_features_ = Some(n_features);
393 }
394
395 self.sample_count_ += 1;
396 let prediction_error = (target - prediction).abs();
397
398 for (i, &feature_value) in features.iter().enumerate() {
400 let contribution = feature_value.abs() * prediction_error;
401
402 let current_importance = self.importance_scores_.get(&i).cloned().unwrap_or(0.0);
403 let alpha = 1.0 - self.decay_factor;
404 let new_importance = alpha * contribution + self.decay_factor * current_importance;
405
406 self.importance_scores_.insert(i, new_importance);
407 }
408
409 Ok(())
410 }
411
412 pub fn get_importance_scores(&self) -> &HashMap<usize, Float> {
414 &self.importance_scores_
415 }
416
417 pub fn get_top_features(&self, k: usize) -> Vec<usize> {
419 let mut scores: Vec<(usize, Float)> = self
420 .importance_scores_
421 .iter()
422 .map(|(&idx, &score)| (idx, score))
423 .collect();
424
425 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
426 scores.into_iter().take(k).map(|(idx, _)| idx).collect()
427 }
428}
429
430impl Default for StreamingFeatureImportance {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436#[derive(Debug, Clone)]
440pub struct ConceptDriftAwareSelector<State = Untrained> {
441 base_selector: OnlineFeatureSelector<State>,
442 drift_detection_window: usize,
443 drift_threshold: f64,
444 adaptation_rate: f64,
445
446 performance_history_: VecDeque<Float>,
448 drift_detected_: bool,
449}
450
451impl ConceptDriftAwareSelector<Untrained> {
452 pub fn new(k: usize) -> Self {
454 Self {
455 base_selector: OnlineFeatureSelector::new(k),
456 drift_detection_window: 100,
457 drift_threshold: 0.05,
458 adaptation_rate: 0.1,
459 performance_history_: VecDeque::new(),
460 drift_detected_: false,
461 }
462 }
463
464 pub fn drift_detection_window(mut self, window_size: usize) -> Self {
466 self.drift_detection_window = window_size;
467 self
468 }
469
470 pub fn drift_threshold(mut self, threshold: f64) -> Self {
472 self.drift_threshold = threshold;
473 self
474 }
475
476 pub fn min_samples(mut self, min_samples: usize) -> Self {
478 self.base_selector = self.base_selector.min_samples(min_samples);
479 self
480 }
481}
482
483impl Estimator for ConceptDriftAwareSelector<Untrained> {
484 type Config = ();
485 type Error = SklearsError;
486 type Float = f64;
487
488 fn config(&self) -> &Self::Config {
489 &()
490 }
491}
492
493impl Fit<Array2<Float>, Array1<Float>> for ConceptDriftAwareSelector<Untrained> {
494 type Fitted = ConceptDriftAwareSelector<Trained>;
495
496 fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> SklResult<Self::Fitted> {
497 let fitted_base = self.base_selector.fit(x, y)?;
498
499 Ok(ConceptDriftAwareSelector {
500 base_selector: fitted_base,
501 drift_detection_window: self.drift_detection_window,
502 drift_threshold: self.drift_threshold,
503 adaptation_rate: self.adaptation_rate,
504 performance_history_: VecDeque::new(),
505 drift_detected_: false,
506 })
507 }
508}
509
510impl ConceptDriftAwareSelector<Trained> {
511 pub fn partial_fit_with_performance(
513 &mut self,
514 sample: &Array1<Float>,
515 target: Float,
516 performance: Float,
517 ) -> SklResult<()> {
518 self.base_selector.partial_fit_sample(sample, target)?;
520
521 self.performance_history_.push_back(performance);
523 if self.performance_history_.len() > self.drift_detection_window {
524 self.performance_history_.pop_front();
525 }
526
527 if self.performance_history_.len() >= self.drift_detection_window / 2 {
529 self.drift_detected_ = self.detect_performance_drift()?;
530
531 if self.drift_detected_ {
532 self.adapt_to_drift()?;
534 }
535 }
536
537 Ok(())
538 }
539
540 fn detect_performance_drift(&self) -> SklResult<bool> {
542 if self.performance_history_.len() < 20 {
543 return Ok(false);
544 }
545
546 let mid = self.performance_history_.len() / 2;
547 let recent_perf: Float = self.performance_history_.iter().skip(mid).sum::<Float>()
548 / (self.performance_history_.len() - mid) as Float;
549
550 let old_perf: Float =
551 self.performance_history_.iter().take(mid).sum::<Float>() / mid as Float;
552
553 Ok(old_perf - recent_perf > self.drift_threshold)
555 }
556
557 fn adapt_to_drift(&mut self) -> SklResult<()> {
559 let reset_fraction = self.adaptation_rate;
564 let samples_to_keep =
565 ((1.0 - reset_fraction) * self.performance_history_.len() as f64) as usize;
566
567 while self.performance_history_.len() > samples_to_keep {
568 self.performance_history_.pop_front();
569 }
570
571 self.drift_detected_ = false;
572 Ok(())
573 }
574
575 pub fn drift_detected(&self) -> bool {
577 self.drift_detected_
578 }
579}
580
581impl FeatureSelector for ConceptDriftAwareSelector<Trained> {
582 fn selected_features(&self) -> &Vec<usize> {
583 self.base_selector.selected_features()
584 }
585}
586
587impl Transform<Array2<Float>, Array2<Float>> for ConceptDriftAwareSelector<Trained> {
588 fn transform(&self, x: &Array2<Float>) -> SklResult<Array2<Float>> {
589 self.base_selector.transform(x)
590 }
591}
592
593#[allow(non_snake_case)]
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 use scirs2_core::ndarray::array;
599
600 #[test]
601 fn test_online_feature_selector_basic() {
602 let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
603 let y = array![1.0, 2.0, 3.0];
604
605 let selector = OnlineFeatureSelector::new(2).min_samples(2);
606 let fitted = selector.fit(&x, &y).expect("operation should succeed");
607
608 assert_eq!(fitted.selected_features().len(), 2);
609 assert_eq!(fitted.sample_count_, 3);
610 }
611
612 #[test]
613 fn test_online_selector_partial_fit() {
614 let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
615 let y = array![1.0, 2.0];
616
617 let selector = OnlineFeatureSelector::new(2);
618 let mut fitted = selector.fit(&x, &y).expect("operation should succeed");
619
620 let new_sample = array![10.0, 11.0, 12.0];
622 fitted
623 .partial_fit_sample(&new_sample, 3.0)
624 .expect("operation should succeed");
625
626 assert_eq!(fitted.sample_count_, 3);
627 }
628
629 #[test]
630 fn test_streaming_importance() {
631 let mut importance = StreamingFeatureImportance::new();
632
633 let features = array![1.0, 2.0, 3.0];
634 importance
635 .update(&features, 5.0, 4.8)
636 .expect("operation should succeed");
637
638 let scores = importance.get_importance_scores();
639 assert_eq!(scores.len(), 3);
640
641 let top_features = importance.get_top_features(2);
642 assert_eq!(top_features.len(), 2);
643 }
644
645 #[test]
646 fn test_concept_drift_selector() {
647 let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
648 let y = array![1.0, 2.0, 3.0];
649
650 let selector = ConceptDriftAwareSelector::new(1).min_samples(2);
651 let mut fitted = selector.fit(&x, &y).expect("operation should succeed");
652
653 let sample = array![7.0, 8.0];
655 fitted
656 .partial_fit_with_performance(&sample, 4.0, 0.9)
657 .expect("operation should succeed");
658
659 assert_eq!(fitted.selected_features().len(), 1);
660 }
661
662 #[test]
663 fn test_online_selector_transform() {
664 let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
665 let y = array![1.0, 2.0];
666
667 let selector = OnlineFeatureSelector::new(2).min_samples(2);
668 let fitted = selector.fit(&x, &y).expect("operation should succeed");
669
670 let test_x = array![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]];
671 let transformed = fitted.transform(&test_x).expect("operation should succeed");
672
673 assert_eq!(transformed.ncols(), 2);
674 assert_eq!(transformed.nrows(), 2);
675 }
676}