Skip to main content

sklears_feature_selection/
streaming.rs

1//! Streaming and online feature selection methods
2//!
3//! This module provides algorithms for feature selection that can process data incrementally,
4//! handle concept drift, and adapt their selection over time.
5
6use 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/// Online feature selection using incremental statistics
17///
18/// This selector maintains running statistics for each feature and updates
19/// feature selection based on these statistics as new data arrives.
20#[derive(Debug, Clone)]
21pub struct OnlineFeatureSelector<State = Untrained> {
22    // Configuration
23    k: usize,
24    window_size: Option<usize>,
25    decay_factor: f64,
26    min_samples: usize,
27
28    // Online statistics
29    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    // Sliding window for concept drift detection
37    window_data_: Option<VecDeque<Array1<Float>>>,
38    window_targets_: Option<VecDeque<Float>>,
39
40    state: PhantomData<State>,
41}
42
43impl OnlineFeatureSelector<Untrained> {
44    /// Create a new online feature selector
45    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    /// Set the window size for concept drift detection
64    pub fn window_size(mut self, window_size: usize) -> Self {
65        self.window_size = Some(window_size);
66        self
67    }
68
69    /// Set the decay factor for exponential moving statistics
70    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    /// Set minimum samples before selection begins
81    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        // Process initial data
145        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    /// Update the selector with a new sample
155    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        // Update sliding window if enabled
172        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        // Update exponential moving statistics
187        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                // Update mean
201                let old_mean = means[i];
202                means[i] = alpha * value + (1.0 - alpha) * old_mean;
203
204                // Update variance (using Welford's online algorithm)
205                let delta = value - old_mean;
206                let delta2 = value - means[i];
207                vars[i] = (1.0 - alpha) * vars[i] + alpha * delta * delta2;
208
209                // Update correlation with target (simplified)
210                let target_mean = 0.0; // Simplified - in practice maintain running target mean
211                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        // Update feature selection if we have enough samples
219        if self.sample_count_ >= self.min_samples {
220            self.update_feature_selection()?;
221        }
222
223        Ok(())
224    }
225
226    /// Update feature selection based on current statistics
227    fn update_feature_selection(&mut self) -> SklResult<()> {
228        if let Some(correlations) = &self.target_correlation_ {
229            // Select features with highest absolute correlation
230            let mut feature_scores: Vec<(usize, f64)> = correlations
231                .iter()
232                .enumerate()
233                .map(|(i, &corr)| (i, corr.abs()))
234                .collect();
235
236            // Sort by score (descending)
237            feature_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
238
239            // Select top k features
240            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    /// Compute current target mean (simplified for this example)
253    pub fn compute_target_mean(&self) -> Float {
254        // This is a simplified implementation
255        // In practice, you'd maintain a running mean of targets
256        0.0
257    }
258
259    /// Detect concept drift using window statistics
260    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); // Not enough data
266            }
267
268            // Simple drift detection: compare first and second half of window
269            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            // Compute means
275            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            // Simple threshold-based drift detection
281            let drift_threshold = 0.5;
282            Ok((first_mean - second_mean).abs() > drift_threshold)
283        } else {
284            Ok(false)
285        }
286    }
287
288    /// Reset the selector (useful when concept drift is detected)
289    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/// Streaming feature importance calculator
339///
340/// Maintains running importance scores for features in a streaming fashion
341#[derive(Debug, Clone)]
342pub struct StreamingFeatureImportance {
343    // Configuration
344    decay_factor: f64,
345    _min_samples: usize,
346
347    // State
348    importance_scores_: HashMap<usize, Float>,
349    sample_count_: usize,
350    n_features_: Option<usize>,
351}
352
353impl StreamingFeatureImportance {
354    /// Create a new streaming feature importance calculator
355    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    /// Set decay factor for exponential moving average
366    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    /// Update importance scores with new sample
377    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        // Update importance based on feature values and prediction error
399        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    /// Get current importance scores
413    pub fn get_importance_scores(&self) -> &HashMap<usize, Float> {
414        &self.importance_scores_
415    }
416
417    /// Get top k most important features
418    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/// Concept drift-aware feature selector
437///
438/// Adapts feature selection when concept drift is detected
439#[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    // Drift detection state
447    performance_history_: VecDeque<Float>,
448    drift_detected_: bool,
449}
450
451impl ConceptDriftAwareSelector<Untrained> {
452    /// Create a new concept drift-aware selector
453    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    /// Set drift detection window size
465    pub fn drift_detection_window(mut self, window_size: usize) -> Self {
466        self.drift_detection_window = window_size;
467        self
468    }
469
470    /// Set drift detection threshold
471    pub fn drift_threshold(mut self, threshold: f64) -> Self {
472        self.drift_threshold = threshold;
473        self
474    }
475
476    /// Set minimum samples for feature selection
477    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    /// Update with new sample and check for drift
512    pub fn partial_fit_with_performance(
513        &mut self,
514        sample: &Array1<Float>,
515        target: Float,
516        performance: Float,
517    ) -> SklResult<()> {
518        // Update base selector
519        self.base_selector.partial_fit_sample(sample, target)?;
520
521        // Update performance history
522        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        // Check for drift
528        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                // Adapt to drift by partially resetting the selector
533                self.adapt_to_drift()?;
534            }
535        }
536
537        Ok(())
538    }
539
540    /// Detect drift based on performance degradation
541    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        // Drift detected if recent performance is significantly worse
554        Ok(old_perf - recent_perf > self.drift_threshold)
555    }
556
557    /// Adapt to detected concept drift
558    fn adapt_to_drift(&mut self) -> SklResult<()> {
559        // Partially reset the base selector to adapt to new concept
560        // This is a simplified adaptation strategy
561
562        // Clear some of the history to focus on recent data
563        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    /// Check if drift was recently detected
576    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        // Add new sample
621        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        // Add sample with performance
654        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}