1use scirs2_core::numeric::Float;
7use std::collections::VecDeque;
8use std::iter::Sum;
9use std::time::{Duration, Instant};
10
11use crate::error::Result;
12use crate::utils::scalar_or;
13
14#[cfg(test)]
15mod drift_regression_tests;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum DriftDetectionMethod {
20 PageHinkley,
22 Adwin,
24 DriftDetectionMethod,
26 EarlyDriftDetection,
28 StatisticalTest,
30 Ensemble,
32}
33
34#[derive(Debug, Clone)]
36pub struct DriftDetectorConfig {
37 pub method: DriftDetectionMethod,
39 pub min_samples: usize,
41 pub threshold: f64,
43 pub window_size: usize,
45 pub alpha: f64,
47 pub warningthreshold: f64,
49 pub enable_ensemble: bool,
51}
52
53impl Default for DriftDetectorConfig {
54 fn default() -> Self {
55 Self {
56 method: DriftDetectionMethod::PageHinkley,
57 min_samples: 30,
58 threshold: 3.0,
59 window_size: 100,
60 alpha: 0.005,
61 warningthreshold: 2.0,
62 enable_ensemble: false,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69pub enum DriftStatus {
70 Stable,
72 Warning,
74 Drift,
76}
77
78#[derive(Debug, Clone)]
80pub struct DriftEvent<A: Float + Send + Sync> {
81 pub timestamp: Instant,
83 pub confidence: A,
85 pub drift_type: DriftType,
87 pub adaptation_recommendation: AdaptationRecommendation,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum DriftType {
94 Sudden,
96 Gradual,
98 Incremental,
100 Recurring,
102 Blip,
104}
105
106#[derive(Debug, Clone)]
108pub enum AdaptationRecommendation {
109 Reset,
111 IncreaseLearningRate { factor: f64 },
113 DecreaseLearningRate { factor: f64 },
115 SwitchOptimizer { new_optimizer: String },
117 AdjustWindow { new_size: usize },
119 NoAction,
121}
122
123#[derive(Debug, Clone)]
125pub struct PageHinkleyDetector<A: Float + Send + Sync> {
126 sum: A,
128 min_sum: A,
130 threshold: A,
132 warningthreshold: A,
134 sample_count: usize,
136 last_drift: Option<Instant>,
138 running_mean: A,
148}
149
150impl<A: Float + Send + Sync + Send + Sync> PageHinkleyDetector<A> {
151 pub fn new(threshold: A, warningthreshold: A) -> Self {
153 Self {
154 sum: A::zero(),
155 min_sum: A::zero(),
156 threshold,
157 warningthreshold,
158 sample_count: 0,
159 last_drift: None,
160 running_mean: A::zero(),
161 }
162 }
163
164 pub fn update(&mut self, loss: A) -> DriftStatus {
166 self.sample_count += 1;
167
168 let count = A::from(self.sample_count).unwrap_or(A::one());
172 self.running_mean = self.running_mean + (loss - self.running_mean) / count;
173
174 self.sum = self.sum + loss - self.running_mean;
176
177 if self.sum < self.min_sum {
179 self.min_sum = self.sum;
180 }
181
182 let test_stat = self.sum - self.min_sum;
184
185 if test_stat > self.threshold {
186 self.last_drift = Some(Instant::now());
187 self.reset();
188 DriftStatus::Drift
189 } else if test_stat > self.warningthreshold {
190 DriftStatus::Warning
191 } else {
192 DriftStatus::Stable
193 }
194 }
195
196 pub fn set_thresholds(&mut self, threshold: A, warningthreshold: A) {
201 self.threshold = threshold;
202 self.warningthreshold = warningthreshold;
203 }
204
205 pub fn threshold(&self) -> A {
207 self.threshold
208 }
209
210 pub fn warning_threshold(&self) -> A {
212 self.warningthreshold
213 }
214
215 pub fn reset(&mut self) {
217 self.sum = A::zero();
218 self.min_sum = A::zero();
219 self.sample_count = 0;
220 self.running_mean = A::zero();
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct AdwinDetector<A: Float + Send + Sync> {
227 window: VecDeque<A>,
229 max_windowsize: usize,
231 delta: A,
233 min_window_size: usize,
235}
236
237impl<A: Float + Sum + Send + Sync + Send + Sync> AdwinDetector<A> {
238 pub fn new(delta: A, max_windowsize: usize) -> Self {
240 Self {
241 window: VecDeque::new(),
242 max_windowsize,
243 delta,
244 min_window_size: 10,
245 }
246 }
247
248 pub fn set_delta(&mut self, delta: A) {
250 self.delta = delta;
251 }
252
253 pub fn delta(&self) -> A {
255 self.delta
256 }
257
258 pub fn update(&mut self, value: A) -> DriftStatus {
260 self.window.push_back(value);
261
262 if self.window.len() > self.max_windowsize {
264 self.window.pop_front();
265 }
266
267 if self.window.len() >= self.min_window_size {
269 if self.detect_change() {
270 self.shrink_window();
271 DriftStatus::Drift
272 } else {
273 DriftStatus::Stable
274 }
275 } else {
276 DriftStatus::Stable
277 }
278 }
279
280 fn detect_change(&self) -> bool {
297 let n = self.window.len();
298 if n < 2 {
299 return false;
300 }
301
302 let values: Vec<A> = self.window.iter().cloned().collect();
303 let mut prefix = Vec::with_capacity(n + 1);
305 prefix.push(A::zero());
306 for &v in &values {
307 prefix.push(*prefix.last().unwrap_or(&A::zero()) + v);
308 }
309 let total = prefix[n];
310
311 let min_v = values
318 .iter()
319 .cloned()
320 .fold(values[0], |a, b| if b < a { b } else { a });
321 let max_v = values
322 .iter()
323 .cloned()
324 .fold(values[0], |a, b| if b > a { b } else { a });
325 let range = (max_v - min_v).max(A::from(1e-12).unwrap_or(A::zero()));
326
327 let four = A::from(4.0).unwrap_or(A::one());
328 let two = A::from(2.0).unwrap_or(A::one());
329 let ln_term = (four / self.delta.max(A::from(1e-12).unwrap_or(A::zero()))).ln();
330
331 for (offset, &sum0) in prefix[1..n].iter().enumerate() {
332 let n0 = offset + 1;
333 let n1 = n - n0;
334 let n0_a = A::from(n0).unwrap_or(A::one());
335 let n1_a = A::from(n1).unwrap_or(A::one());
336
337 let sum1 = total - sum0;
338 let mean0 = sum0 / n0_a;
339 let mean1 = sum1 / n1_a;
340
341 let m = A::one() / (A::one() / n0_a + A::one() / n1_a);
343 let eps_cut = range * (ln_term / (two * m)).sqrt();
344
345 if (mean0 - mean1).abs() > eps_cut {
346 return true;
347 }
348 }
349
350 false
351 }
352
353 fn shrink_window(&mut self) {
355 let new_size = self.window.len() / 2;
356 while self.window.len() > new_size {
357 self.window.pop_front();
358 }
359 }
360}
361
362#[derive(Debug, Clone)]
380pub struct DdmDetector<A: Float + Send + Sync> {
381 error_rate: A,
383 error_std: A,
385 p_min: Option<A>,
387 s_min: Option<A>,
389 sample_count: usize,
391 error_count: usize,
393 warmup: usize,
395}
396
397impl<A: Float + Send + Sync + Send + Sync> DdmDetector<A> {
398 pub const DEFAULT_WARMUP: usize = 30;
401
402 pub fn new() -> Self {
404 Self::with_warmup(Self::DEFAULT_WARMUP)
405 }
406
407 pub fn with_warmup(warmup: usize) -> Self {
409 Self {
410 error_rate: A::zero(),
411 error_std: A::zero(),
412 p_min: None,
413 s_min: None,
414 sample_count: 0,
415 error_count: 0,
416 warmup: warmup.max(2),
417 }
418 }
419
420 pub fn error_rate(&self) -> A {
422 self.error_rate
423 }
424
425 pub fn warning_level(&self) -> Option<A> {
427 let (p_min, s_min) = (self.p_min?, self.s_min?);
428 Some(p_min + A::from(2.0)? * s_min)
429 }
430
431 pub fn drift_level(&self) -> Option<A> {
433 let (p_min, s_min) = (self.p_min?, self.s_min?);
434 Some(p_min + A::from(3.0)? * s_min)
435 }
436
437 pub fn update(&mut self, iserror: bool) -> DriftStatus {
439 self.sample_count += 1;
440 if iserror {
441 self.error_count += 1;
442 }
443
444 let n = match A::from(self.sample_count as f64) {
445 Some(n) if n > A::zero() => n,
446 _ => return DriftStatus::Stable,
447 };
448 let p = A::from(self.error_count as f64).unwrap_or_else(A::zero) / n;
449 let variance = (p * (A::one() - p) / n).max(A::zero());
452 self.error_rate = p;
453 self.error_std = variance.sqrt();
454
455 if self.sample_count < self.warmup {
456 return DriftStatus::Stable;
460 }
461
462 let level = p + self.error_std;
463 match (self.p_min, self.s_min) {
464 (Some(p_min), Some(s_min)) if level >= p_min + s_min => {}
465 _ => {
466 self.p_min = Some(p);
467 self.s_min = Some(self.error_std);
468 }
469 }
470
471 let Some(warning_level) = self.warning_level() else {
472 return DriftStatus::Stable;
473 };
474 let Some(drift_level) = self.drift_level() else {
475 return DriftStatus::Stable;
476 };
477
478 if level > drift_level {
483 self.reset();
484 DriftStatus::Drift
485 } else if level > warning_level {
486 DriftStatus::Warning
487 } else {
488 DriftStatus::Stable
489 }
490 }
491
492 pub fn reset(&mut self) {
494 self.sample_count = 0;
495 self.error_count = 0;
496 self.error_rate = A::zero();
497 self.error_std = A::zero();
498 self.p_min = None;
499 self.s_min = None;
500 }
501}
502
503impl<A: Float + Send + Sync + Send + Sync> Default for DdmDetector<A> {
504 fn default() -> Self {
505 Self::new()
506 }
507}
508
509pub struct ConceptDriftDetector<A: Float + Send + Sync> {
511 config: DriftDetectorConfig,
513
514 ph_detector: PageHinkleyDetector<A>,
516
517 adwin_detector: AdwinDetector<A>,
519
520 ddm_detector: DdmDetector<A>,
522
523 ensemble_history: VecDeque<DriftStatus>,
525
526 drift_events: Vec<DriftEvent<A>>,
528
529 performance_tracker: PerformanceDriftTracker<A>,
531}
532
533impl<A: Float + std::fmt::Debug + Sum + Send + Sync + Send + Sync> ConceptDriftDetector<A> {
534 pub const ENSEMBLE_HISTORY_CAPACITY: usize = 64;
536
537 pub const DRIFT_EVENT_CAPACITY: usize = 1024;
540
541 pub fn new(config: DriftDetectorConfig) -> Self {
543 let threshold = scalar_or(config.threshold, A::zero());
544 let warningthreshold = scalar_or(config.warningthreshold, A::zero());
545 let delta = scalar_or(config.alpha, A::zero());
546
547 Self {
548 ph_detector: PageHinkleyDetector::new(threshold, warningthreshold),
549 adwin_detector: AdwinDetector::new(delta, config.window_size),
550 ddm_detector: DdmDetector::new(),
551 ensemble_history: VecDeque::with_capacity(10),
552 drift_events: Vec::new(),
553 performance_tracker: PerformanceDriftTracker::new(),
554 config,
555 }
556 }
557
558 pub fn update(&mut self, loss: A, is_predictionerror: bool) -> Result<DriftStatus> {
560 let ph_status = self.ph_detector.update(loss);
561 let adwin_status = self.adwin_detector.update(loss);
562 let ddm_status = self.ddm_detector.update(is_predictionerror);
563
564 let final_status = if self.config.enable_ensemble {
565 self.ensemble_vote(ph_status, adwin_status, ddm_status)
566 } else {
567 match self.config.method {
568 DriftDetectionMethod::PageHinkley => ph_status,
569 DriftDetectionMethod::Adwin => adwin_status,
570 DriftDetectionMethod::DriftDetectionMethod => ddm_status,
571 _ => ddm_status, }
573 };
574
575 self.ensemble_history.push_back(final_status);
580 while self.ensemble_history.len() > Self::ENSEMBLE_HISTORY_CAPACITY {
581 self.ensemble_history.pop_front();
582 }
583
584 if final_status == DriftStatus::Drift {
586 let event = DriftEvent {
587 timestamp: Instant::now(),
588 confidence: self.detection_confidence(ph_status, adwin_status, ddm_status),
591 drift_type: self.classify_drift_type(),
592 adaptation_recommendation: self.generate_adaptation_recommendation(),
593 };
594 self.drift_events.push(event);
595 while self.drift_events.len() > Self::DRIFT_EVENT_CAPACITY {
596 self.drift_events.remove(0);
597 }
598 }
599
600 self.performance_tracker.update(loss, final_status);
602
603 Ok(final_status)
604 }
605
606 fn detection_confidence(&self, ph: DriftStatus, adwin: DriftStatus, ddm: DriftStatus) -> A {
610 let votes = [ph, adwin, ddm];
611 let drift_votes = votes.iter().filter(|&&s| s == DriftStatus::Drift).count();
612 let warning_votes = votes.iter().filter(|&&s| s == DriftStatus::Warning).count();
613 let agreement = (drift_votes as f64 + 0.5 * warning_votes as f64) / votes.len() as f64;
614
615 let persistence = if self.ensemble_history.is_empty() {
619 0.0
620 } else {
621 self.ensemble_history
622 .iter()
623 .filter(|status| **status != DriftStatus::Stable)
624 .count() as f64
625 / self.ensemble_history.len() as f64
626 };
627
628 let confidence = (0.7 * agreement + 0.3 * persistence).clamp(0.0, 1.0);
629 A::from(confidence).unwrap_or_else(A::zero)
630 }
631
632 pub fn ensemble_history(&self) -> &VecDeque<DriftStatus> {
634 &self.ensemble_history
635 }
636
637 fn ensemble_vote(
639 &mut self,
640 ph: DriftStatus,
641 adwin: DriftStatus,
642 ddm: DriftStatus,
643 ) -> DriftStatus {
644 let votes = [ph, adwin, ddm];
645
646 let drift_votes = votes.iter().filter(|&&s| s == DriftStatus::Drift).count();
648 let warning_votes = votes.iter().filter(|&&s| s == DriftStatus::Warning).count();
649
650 if drift_votes >= 2 {
651 DriftStatus::Drift
652 } else if warning_votes >= 2 || drift_votes >= 1 {
653 DriftStatus::Warning
654 } else {
655 DriftStatus::Stable
656 }
657 }
658
659 fn classify_drift_type(&self) -> DriftType {
661 if self.drift_events.len() < 2 {
663 return DriftType::Sudden;
664 }
665
666 let recent_events = self.drift_events.iter().rev().take(5);
667 let time_intervals: Vec<_> = recent_events
668 .map(|event| event.timestamp)
669 .collect::<Vec<_>>()
670 .windows(2)
671 .map(|window| window[0].duration_since(window[1]))
672 .collect();
673
674 if time_intervals.iter().all(|&d| d < Duration::from_secs(60)) {
675 DriftType::Sudden
676 } else if time_intervals.len() > 2 {
677 DriftType::Gradual
678 } else {
679 DriftType::Incremental
680 }
681 }
682
683 fn generate_adaptation_recommendation(&self) -> AdaptationRecommendation {
685 let recent_performance = self.performance_tracker.get_recent_performance_change();
686
687 if recent_performance > scalar_or(0.5, A::zero()) {
688 AdaptationRecommendation::Reset
690 } else if recent_performance > scalar_or(0.2, A::zero()) {
691 AdaptationRecommendation::IncreaseLearningRate { factor: 1.5 }
693 } else if recent_performance < scalar_or(-0.1, A::zero()) {
694 AdaptationRecommendation::DecreaseLearningRate { factor: 0.8 }
696 } else {
697 AdaptationRecommendation::NoAction
698 }
699 }
700
701 pub fn get_statistics(&self) -> DriftStatistics<A> {
703 DriftStatistics {
704 total_drifts: self.drift_events.len(),
705 recent_drift_rate: self.calculate_recent_drift_rate(),
706 average_drift_confidence: self.calculate_average_confidence(),
707 drift_types_distribution: self.calculate_drift_type_distribution(),
708 time_since_last_drift: self.time_since_last_drift(),
709 }
710 }
711
712 fn calculate_recent_drift_rate(&self) -> f64 {
713 let recent_window = Duration::from_secs(3600);
720 let now = Instant::now();
721 let recent_drifts = self
722 .drift_events
723 .iter()
724 .filter(|event| now.duration_since(event.timestamp) <= recent_window)
725 .count();
726 recent_drifts as f64 / recent_window.as_secs_f64() }
728
729 fn calculate_average_confidence(&self) -> Option<A> {
730 if self.drift_events.is_empty() {
731 None
732 } else {
733 let sum = self
734 .drift_events
735 .iter()
736 .map(|event| event.confidence)
737 .sum::<A>();
738 Some(sum / scalar_or(self.drift_events.len(), A::one()))
739 }
740 }
741
742 fn calculate_drift_type_distribution(&self) -> std::collections::HashMap<DriftType, usize> {
743 let mut distribution = std::collections::HashMap::new();
744 for event in &self.drift_events {
745 *distribution.entry(event.drift_type).or_insert(0) += 1;
746 }
747 distribution
748 }
749
750 fn time_since_last_drift(&self) -> Option<Duration> {
751 self.drift_events
752 .last()
753 .map(|event| event.timestamp.elapsed())
754 }
755}
756
757#[derive(Debug, Clone)]
759struct PerformanceDriftTracker<A: Float + Send + Sync> {
760 performance_history: VecDeque<(A, DriftStatus, Instant)>,
762 window_size: usize,
764}
765
766impl<A: Float + std::iter::Sum + Send + Sync + Send + Sync> PerformanceDriftTracker<A> {
767 fn new() -> Self {
768 Self {
769 performance_history: VecDeque::new(),
770 window_size: 100,
771 }
772 }
773
774 fn update(&mut self, performance: A, driftstatus: DriftStatus) {
775 self.performance_history
776 .push_back((performance, driftstatus, Instant::now()));
777
778 if self.performance_history.len() > self.window_size {
780 self.performance_history.pop_front();
781 }
782 }
783
784 fn get_recent_performance_change(&self) -> A {
786 if self.performance_history.len() < 10 {
787 return A::zero();
788 }
789
790 let recent: Vec<_> = self.performance_history.iter().rev().take(10).collect();
791 let older: Vec<_> = self
792 .performance_history
793 .iter()
794 .rev()
795 .skip(10)
796 .take(10)
797 .collect();
798
799 if older.is_empty() {
800 return A::zero();
801 }
802
803 let recent_avg =
804 recent.iter().map(|(p, _, _)| *p).sum::<A>() / scalar_or(recent.len(), A::one());
805 let older_avg =
806 older.iter().map(|(p, _, _)| *p).sum::<A>() / scalar_or(older.len(), A::one());
807
808 recent_avg - older_avg
809 }
810}
811
812#[derive(Debug, Clone)]
814pub struct DriftStatistics<A: Float + Send + Sync> {
815 pub total_drifts: usize,
817 pub recent_drift_rate: f64,
819 pub average_drift_confidence: Option<A>,
821 pub drift_types_distribution: std::collections::HashMap<DriftType, usize>,
823 pub time_since_last_drift: Option<Duration>,
825}
826
827pub mod advanced_drift_analysis {
829 use super::*;
830 use std::collections::HashMap;
831
832 #[derive(Debug)]
834 pub struct AdvancedDriftDetector<A: Float + Send + Sync> {
835 base_detectors: Vec<Box<dyn DriftDetectorTrait<A>>>,
837
838 pattern_analyzer: DriftPatternAnalyzer<A>,
840
841 threshold_manager: AdaptiveThresholdManager<A>,
843
844 context_detector: ContextAwareDriftDetector<A>,
846
847 impact_analyzer: DriftImpactAnalyzer<A>,
849
850 adaptation_selector: AdaptationStrategySelector<A>,
852
853 drift_database: DriftDatabase<A>,
855 }
856
857 pub trait DriftDetectorTrait<A: Float + Send + Sync>: std::fmt::Debug {
859 fn update(&mut self, value: A) -> DriftStatus;
860 fn reset(&mut self);
861 fn get_confidence(&self) -> A;
862
863 fn name(&self) -> &str;
865
866 fn set_threshold(&mut self, threshold: A);
869
870 fn threshold(&self) -> A;
872 }
873
874 #[derive(Debug)]
876 pub struct DriftPatternAnalyzer<A: Float + Send + Sync> {
877 pub(crate) pattern_buffer: VecDeque<PatternFeatures<A>>,
879
880 pub(crate) value_buffer: VecDeque<A>,
883
884 pub(crate) window: usize,
886
887 pub(crate) known_patterns: HashMap<String, DriftPattern<A>>,
889
890 pub(crate) matching_threshold: A,
892
893 pub(crate) feature_extractors: Vec<Box<dyn FeatureExtractor<A>>>,
895 }
896
897 #[derive(Debug, Clone)]
907 pub struct PatternFeatures<A: Float + Send + Sync> {
908 pub mean: A,
910 pub variance: A,
911 pub skewness: Option<A>,
912 pub kurtosis: Option<A>,
913
914 pub trend_slope: Option<A>,
916 pub trend_strength: Option<A>,
917
918 pub dominant_frequency: Option<A>,
920 pub spectral_entropy: Option<A>,
921
922 pub temporal_locality: Option<A>,
924 pub persistence: Option<A>,
925
926 pub entropy: Option<A>,
928 pub fractal_dimension: Option<A>,
929 }
930
931 impl<A: Float + Send + Sync> PatternFeatures<A> {
932 pub fn named_values(&self) -> Vec<(&'static str, A)> {
935 let mut values: Vec<(&'static str, A)> =
936 vec![("mean", self.mean), ("variance", self.variance)];
937 let optional: [(&'static str, Option<A>); 10] = [
938 ("skewness", self.skewness),
939 ("kurtosis", self.kurtosis),
940 ("trend_slope", self.trend_slope),
941 ("trend_strength", self.trend_strength),
942 ("dominant_frequency", self.dominant_frequency),
943 ("spectral_entropy", self.spectral_entropy),
944 ("temporal_locality", self.temporal_locality),
945 ("persistence", self.persistence),
946 ("entropy", self.entropy),
947 ("fractal_dimension", self.fractal_dimension),
948 ];
949 for (name, value) in optional {
950 if let Some(value) = value {
951 values.push((name, value));
952 }
953 }
954 values
955 }
956
957 pub fn feature(&self, name: &str) -> Option<A> {
960 match name {
961 "mean" => Some(self.mean),
962 "variance" => Some(self.variance),
963 "skewness" => self.skewness,
964 "kurtosis" => self.kurtosis,
965 "trend_slope" => self.trend_slope,
966 "trend_strength" => self.trend_strength,
967 "dominant_frequency" => self.dominant_frequency,
968 "spectral_entropy" => self.spectral_entropy,
969 "temporal_locality" => self.temporal_locality,
970 "persistence" => self.persistence,
971 "entropy" => self.entropy,
972 "fractal_dimension" => self.fractal_dimension,
973 _ => None,
974 }
975 }
976 }
977
978 #[derive(Debug, Clone)]
980 pub struct DriftPattern<A: Float + Send + Sync> {
981 pub id: String,
983
984 pub features: PatternFeatures<A>,
986
987 pub pattern_type: DriftType,
989
990 pub typical_duration: Duration,
992
993 pub optimal_adaptation: AdaptationRecommendation,
995
996 pub adaptation_success_rate: A,
998
999 pub occurrence_count: usize,
1001 }
1002
1003 pub trait FeatureExtractor<A: Float + Send + Sync>: std::fmt::Debug {
1005 fn extract(&self, data: &[A]) -> A;
1006 fn name(&self) -> &str;
1007 }
1008
1009 #[derive(Debug)]
1011 pub struct AdaptiveThresholdManager<A: Float + Send + Sync> {
1012 thresholds: HashMap<String, A>,
1014
1015 threshold_history: VecDeque<ThresholdUpdate<A>>,
1017
1018 performance_feedback: VecDeque<PerformanceFeedback<A>>,
1020
1021 learning_rate: A,
1023 }
1024
1025 #[derive(Debug, Clone)]
1027 pub struct ThresholdUpdate<A: Float + Send + Sync> {
1028 pub detector_name: String,
1029 pub old_threshold: A,
1030 pub new_threshold: A,
1031 pub timestamp: Instant,
1032 pub reason: String,
1033 }
1034
1035 #[derive(Debug, Clone)]
1037 pub struct PerformanceFeedback<A: Float + Send + Sync> {
1038 pub true_positive_rate: A,
1039 pub false_positive_rate: A,
1040 pub detection_delay: Duration,
1041 pub adaptation_effectiveness: A,
1042 pub timestamp: Instant,
1043 }
1044
1045 #[derive(Debug)]
1062 pub struct ContextAwareDriftDetector<A: Float + Send + Sync> {
1063 context_features: Vec<ContextFeature<A>>,
1065
1066 current_context: Option<String>,
1068
1069 transition_matrix: HashMap<(String, String), A>,
1071
1072 detector_config: DriftDetectorConfig,
1074
1075 context_models: HashMap<String, Vec<Box<dyn DriftDetectorTrait<A>>>>,
1077
1078 context_status: HashMap<String, DriftStatus>,
1080 }
1081
1082 #[derive(Debug, Clone)]
1084 pub struct ContextFeature<A: Float + Send + Sync> {
1085 pub name: String,
1086 pub value: A,
1087 pub importance_weight: A,
1088 pub temporal_stability: A,
1089 }
1090
1091 #[derive(Debug)]
1093 pub struct DriftImpactAnalyzer<A: Float + Send + Sync> {
1094 impact_history: VecDeque<DriftImpact<A>>,
1096
1097 severity_classifier: SeverityClassifier<A>,
1099
1100 recovery_predictor: RecoveryTimePredictor<A>,
1102
1103 business_impact_estimator: BusinessImpactEstimator<A>,
1105 }
1106
1107 #[derive(Debug, Clone)]
1109 pub struct DriftImpact<A: Float + Send + Sync> {
1110 pub performance_degradation: A,
1112
1113 pub affected_metrics: Vec<String>,
1115
1116 pub estimated_recovery_time: Duration,
1118
1119 pub confidence: A,
1121
1122 pub business_impact_score: A,
1124
1125 pub urgency_level: UrgencyLevel,
1127 }
1128
1129 #[derive(Debug, Clone, Copy, PartialEq)]
1131 pub enum UrgencyLevel {
1132 Low,
1133 Medium,
1134 High,
1135 Critical,
1136 }
1137
1138 #[derive(Debug)]
1140 pub struct AdaptationStrategySelector<A: Float + Send + Sync> {
1141 strategies: Vec<AdaptationStrategy<A>>,
1143
1144 strategy_performance: HashMap<String, StrategyPerformance<A>>,
1146
1147 bandit: EpsilonGreedyBandit<A>,
1149
1150 context_strategy_map: HashMap<String, Vec<String>>,
1152 }
1153
1154 #[derive(Debug, Clone)]
1156 pub struct AdaptationStrategy<A: Float + Send + Sync> {
1157 pub id: String,
1159
1160 pub strategy_type: AdaptationStrategyType,
1162
1163 pub parameters: HashMap<String, A>,
1165
1166 pub applicability_conditions: Vec<ApplicabilityCondition<A>>,
1168
1169 pub expected_effectiveness: A,
1171
1172 pub computational_cost: A,
1174 }
1175
1176 #[derive(Debug, Clone, Copy)]
1178 pub enum AdaptationStrategyType {
1179 ParameterTuning,
1180 ModelReplacement,
1181 EnsembleReweighting,
1182 ArchitectureChange,
1183 DataAugmentation,
1184 FeatureSelection,
1185 Hybrid,
1186 }
1187
1188 #[derive(Debug, Clone)]
1190 pub struct ApplicabilityCondition<A: Float + Send + Sync> {
1191 pub feature_name: String,
1192 pub operator: ComparisonOperator,
1193 pub threshold: A,
1194 pub weight: A,
1195 }
1196
1197 #[derive(Debug, Clone, Copy)]
1198 pub enum ComparisonOperator {
1199 GreaterThan,
1200 LessThan,
1201 Equal,
1202 NotEqual,
1203 GreaterEqual,
1204 LessEqual,
1205 }
1206
1207 #[derive(Debug, Clone)]
1209 pub struct StrategyPerformance<A: Float + Send + Sync> {
1210 pub success_rate: A,
1211 pub average_improvement: A,
1212 pub average_adaptation_time: Duration,
1213 pub stability_after_adaptation: A,
1214 pub usage_count: usize,
1215 }
1216
1217 pub struct EpsilonGreedyBandit<A: Float + Send + Sync> {
1222 epsilon: A,
1223 action_values: HashMap<String, A>,
1224 action_counts: HashMap<String, usize>,
1225 total_trials: usize,
1226 rng: scirs2_core::random::Random<scirs2_core::random::rngs::StdRng>,
1228 }
1229
1230 impl<A: Float + Send + Sync> std::fmt::Debug for EpsilonGreedyBandit<A> {
1231 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1232 formatter
1233 .debug_struct("EpsilonGreedyBandit")
1234 .field("action_values", &self.action_values.len())
1235 .field("total_trials", &self.total_trials)
1236 .finish()
1237 }
1238 }
1239
1240 #[derive(Debug)]
1242 pub struct DriftDatabase<A: Float + Send + Sync> {
1243 drift_events: Vec<StoredDriftEvent<A>>,
1245
1246 pattern_outcomes: HashMap<String, Vec<AdaptationOutcome<A>>>,
1248
1249 seasonal_patterns: HashMap<String, SeasonalPattern<A>>,
1251
1252 similarity_index: SimilarityIndex<A>,
1254 }
1255
1256 #[derive(Debug, Clone)]
1265 pub struct StoredDriftEvent<A: Float + Send + Sync> {
1266 pub features: PatternFeatures<A>,
1267 pub context: Vec<ContextFeature<A>>,
1268 pub applied_strategy: String,
1269 pub outcome: Option<AdaptationOutcome<A>>,
1270 pub timestamp: Instant,
1271 }
1272
1273 #[derive(Debug, Clone)]
1275 pub struct AdaptationOutcome<A: Float + Send + Sync> {
1276 pub success: bool,
1277 pub performance_improvement: A,
1278 pub adaptation_time: Duration,
1279 pub stability_period: Duration,
1280 pub side_effects: Vec<String>,
1281 }
1282
1283 #[derive(Debug, Clone)]
1285 pub struct SeasonalPattern<A: Float + Send + Sync> {
1286 pub period: Duration,
1287 pub amplitude: A,
1288 pub phase_offset: Duration,
1289 pub pattern_strength: A,
1290 pub last_occurrence: Instant,
1291 }
1292
1293 #[derive(Debug)]
1295 pub struct SimilarityIndex<A: Float + Send + Sync> {
1296 feature_vectors: Vec<(String, Vec<A>)>,
1298
1299 similarity_threshold: A,
1301
1302 distance_metric: DistanceMetric,
1304 }
1305
1306 #[derive(Debug, Clone, Copy)]
1307 pub enum DistanceMetric {
1308 Euclidean,
1309 Manhattan,
1310 Cosine,
1311 Mahalanobis,
1312 }
1313
1314 impl<A: Float + Default + Clone + std::fmt::Debug + std::iter::Sum + Send + Sync + 'static>
1315 AdvancedDriftDetector<A>
1316 {
1317 pub fn new(config: DriftDetectorConfig) -> Self {
1326 let threshold = A::from(config.threshold).unwrap_or_else(A::one);
1327 let warning = A::from(config.warningthreshold).unwrap_or_else(A::zero);
1328 let delta =
1329 A::from(config.alpha).unwrap_or_else(|| A::from(0.002).unwrap_or_else(A::zero));
1330
1331 let base_detectors: Vec<Box<dyn DriftDetectorTrait<A>>> = vec![
1332 Box::new(impls::PageHinkleyAdapter::new(threshold, warning)),
1333 Box::new(impls::AdwinAdapter::new(delta, config.window_size)),
1334 Box::new(impls::DdmAdapter::new(config.min_samples)),
1335 ];
1336
1337 Self {
1338 base_detectors,
1339 pattern_analyzer: DriftPatternAnalyzer::new(config.window_size),
1340 threshold_manager: AdaptiveThresholdManager::new(),
1341 context_detector: ContextAwareDriftDetector::new(config.clone()),
1342 impact_analyzer: DriftImpactAnalyzer::new(),
1343 adaptation_selector: AdaptationStrategySelector::new(),
1344 drift_database: DriftDatabase::new(),
1345 }
1346 }
1347
1348 pub fn context_detector(&self) -> &ContextAwareDriftDetector<A> {
1355 &self.context_detector
1356 }
1357
1358 pub fn detect_drift_advanced(
1360 &mut self,
1361 value: A,
1362 context_features: &[ContextFeature<A>],
1363 ) -> Result<AdvancedDriftResult<A>> {
1364 self.context_detector.update_context(context_features);
1366
1367 let base_results: Vec<_> = self
1369 .base_detectors
1370 .iter_mut()
1371 .map(|detector| (detector.name().to_string(), detector.update(value)))
1372 .collect();
1373 let mut statuses: Vec<DriftStatus> =
1374 base_results.iter().map(|(_, status)| *status).collect();
1375
1376 let context_statuses = self.context_detector.observe_in_context(value);
1386 if self.context_detector.context_count() > 1 {
1387 statuses.extend(context_statuses);
1388 }
1389
1390 let pattern_features = self.pattern_analyzer.ingest(value)?;
1394 let matched_pattern = self.pattern_analyzer.match_pattern(&pattern_features);
1395
1396 self.threshold_manager
1399 .update_thresholds(&base_results, &pattern_features);
1400 self.threshold_manager.apply_to(&mut self.base_detectors);
1401
1402 let combined_result = self.combine_detection_results(&statuses, &matched_pattern);
1404
1405 let impact = if combined_result.status == DriftStatus::Drift {
1407 Some(
1408 self.impact_analyzer
1409 .analyze_impact(&pattern_features, &matched_pattern)?,
1410 )
1411 } else {
1412 None
1413 };
1414
1415 let adaptation_strategy = if let Some(ref impact) = impact {
1417 self.adaptation_selector.select_strategy(
1418 &pattern_features,
1419 impact,
1420 &matched_pattern,
1421 )?
1422 } else {
1423 None
1424 };
1425
1426 if combined_result.status == DriftStatus::Drift {
1430 self.drift_database.store_event(
1431 &pattern_features,
1432 context_features,
1433 &adaptation_strategy,
1434 );
1435 }
1436
1437 Ok(AdvancedDriftResult {
1438 status: combined_result.status,
1439 confidence: combined_result.confidence,
1440 matched_pattern,
1441 impact,
1442 recommended_strategy: adaptation_strategy,
1443 feature_importance: self.calculate_feature_importance(&pattern_features),
1444 prediction_horizon: self.estimate_drift_duration(&pattern_features),
1445 })
1446 }
1447
1448 pub fn record_adaptation_outcome(&mut self, outcome: AdaptationOutcome<A>) -> Result<()> {
1457 let Some((strategy_id, features)) =
1458 self.drift_database.complete_pending_event(outcome.clone())
1459 else {
1460 return Err(crate::error::OptimError::InvalidState(
1461 "no adaptation is awaiting an outcome".to_string(),
1462 ));
1463 };
1464 self.adaptation_selector
1465 .record_outcome(&strategy_id, &outcome);
1466 self.pattern_analyzer.learn_pattern(
1467 &features,
1468 &strategy_id,
1469 &outcome,
1470 self.impact_analyzer.last_drift_type(),
1471 );
1472 self.impact_analyzer.record_observed_recovery(&outcome);
1473 Ok(())
1474 }
1475
1476 pub fn record_threshold_feedback(&mut self, feedback: PerformanceFeedback<A>) {
1478 self.threshold_manager.record_feedback(feedback);
1479 }
1480
1481 pub fn known_patterns(&self) -> &HashMap<String, DriftPattern<A>> {
1483 &self.pattern_analyzer.known_patterns
1484 }
1485
1486 pub fn detector_thresholds(&self) -> Vec<(String, A)> {
1488 self.base_detectors
1489 .iter()
1490 .map(|detector| (detector.name().to_string(), detector.threshold()))
1491 .collect()
1492 }
1493
1494 pub fn stored_events(&self) -> &[StoredDriftEvent<A>] {
1496 &self.drift_database.drift_events
1497 }
1498
1499 fn combine_detection_results(
1500 &self,
1501 base_results: &[DriftStatus],
1502 matched_pattern: &Option<DriftPattern<A>>,
1503 ) -> CombinedDetectionResult<A> {
1504 if base_results.is_empty() {
1508 return CombinedDetectionResult {
1509 status: DriftStatus::Stable,
1510 confidence: A::zero(),
1511 };
1512 }
1513
1514 let drift_votes = base_results
1516 .iter()
1517 .filter(|&&s| s == DriftStatus::Drift)
1518 .count();
1519 let warning_votes = base_results
1520 .iter()
1521 .filter(|&&s| s == DriftStatus::Warning)
1522 .count();
1523
1524 let neutral = A::from(0.5).unwrap_or_else(A::zero);
1527 let pattern_confidence = matched_pattern
1528 .as_ref()
1529 .map(|p| p.adaptation_success_rate)
1530 .unwrap_or(neutral);
1531 let strong = A::from(0.7).unwrap_or_else(A::one);
1532
1533 let status = if drift_votes >= 2 {
1534 DriftStatus::Drift
1535 } else if warning_votes >= 2 || (drift_votes >= 1 && pattern_confidence > strong) {
1536 DriftStatus::Warning
1537 } else {
1538 DriftStatus::Stable
1539 };
1540
1541 let vote_share =
1542 A::from(drift_votes as f64 / base_results.len() as f64).unwrap_or_else(A::zero);
1543 let confidence = vote_share * pattern_confidence;
1544
1545 CombinedDetectionResult { status, confidence }
1546 }
1547
1548 fn calculate_feature_importance(
1549 &self,
1550 features: &PatternFeatures<A>,
1551 ) -> HashMap<String, A> {
1552 let mut magnitudes: Vec<(String, A)> = features
1555 .named_values()
1556 .into_iter()
1557 .filter(|(_, value)| value.is_finite())
1558 .map(|(name, value)| (name.to_string(), value.abs()))
1559 .collect();
1560 let total = magnitudes
1561 .iter()
1562 .fold(A::zero(), |acc, (_, value)| acc + *value);
1563 if total > A::zero() {
1564 for entry in magnitudes.iter_mut() {
1565 entry.1 = entry.1 / total;
1566 }
1567 }
1568 magnitudes.into_iter().collect()
1569 }
1570
1571 fn estimate_drift_duration(&self, features: &PatternFeatures<A>) -> Duration {
1572 let base_duration = Duration::from_secs(300);
1578 let (Some(strength), Some(persistence)) =
1579 (features.trend_strength, features.persistence)
1580 else {
1581 return base_duration;
1582 };
1583 let multiplier = (strength * persistence).to_f64().unwrap_or(1.0);
1584 if !multiplier.is_finite() || multiplier <= 0.0 {
1585 return base_duration;
1586 }
1587 let seconds = (base_duration.as_secs() as f64 * multiplier).clamp(1.0, 86_400.0);
1588 Duration::from_secs(seconds as u64)
1589 }
1590 }
1591
1592 #[derive(Debug, Clone)]
1594 pub struct AdvancedDriftResult<A: Float + Send + Sync> {
1595 pub status: DriftStatus,
1596 pub confidence: A,
1597 pub matched_pattern: Option<DriftPattern<A>>,
1598 pub impact: Option<DriftImpact<A>>,
1599 pub recommended_strategy: Option<AdaptationStrategy<A>>,
1600 pub feature_importance: HashMap<String, A>,
1601 pub prediction_horizon: Duration,
1602 }
1603
1604 #[derive(Debug, Clone)]
1605 struct CombinedDetectionResult<A: Float + Send + Sync> {
1606 status: DriftStatus,
1607 confidence: A,
1608 }
1609
1610 mod impls;
1611
1612 #[cfg(test)]
1613 mod tests;
1614
1615 pub(crate) use impls::{BusinessImpactEstimator, RecoveryTimePredictor, SeverityClassifier};
1616}
1617
1618#[cfg(test)]
1619mod tests {
1620 use super::*;
1621
1622 #[test]
1623 fn test_page_hinkley_detector() {
1624 let mut detector = PageHinkleyDetector::new(3.0f64, 2.0f64);
1625
1626 for _ in 0..10 {
1628 let status = detector.update(0.1);
1629 assert_eq!(status, DriftStatus::Stable);
1630 }
1631
1632 for _ in 0..5 {
1634 let status = detector.update(0.5); if status == DriftStatus::Drift {
1636 break;
1637 }
1638 }
1639 }
1640
1641 #[test]
1649 fn page_hinkley_does_not_falsely_drift_on_stable_stream_away_from_0_1() {
1650 let mut detector = PageHinkleyDetector::new(5.0f64, 3.0f64);
1651
1652 for _ in 0..200 {
1655 let status = detector.update(5.0);
1656 assert_eq!(
1657 status,
1658 DriftStatus::Stable,
1659 "C1 regression: false drift reported on a stationary stream \
1660 whose baseline (5.0) differs from the old hardcoded mean_loss (0.1)"
1661 );
1662 }
1663 }
1664
1665 #[test]
1670 fn page_hinkley_detects_genuine_drift_away_from_0_1_baseline() {
1671 let mut detector = PageHinkleyDetector::new(5.0f64, 3.0f64);
1672
1673 for _ in 0..30 {
1675 detector.update(5.0);
1676 }
1677
1678 let mut drifted = false;
1680 for _ in 0..50 {
1681 let status = detector.update(20.0);
1682 if status == DriftStatus::Drift {
1683 drifted = true;
1684 break;
1685 }
1686 }
1687 assert!(
1688 drifted,
1689 "C1 regression: detector failed to flag a genuine sustained \
1690 increase in loss away from a non-0.1 baseline"
1691 );
1692 }
1693
1694 #[test]
1695 fn test_adwin_detector() {
1696 let mut detector = AdwinDetector::new(0.005f64, 100);
1697
1698 for i in 0..20 {
1700 let value = 0.1 + (i as f64) * 0.001; detector.update(value);
1702 }
1703
1704 for i in 0..10 {
1706 let value = 0.5 + (i as f64) * 0.01; let status = detector.update(value);
1708 if status == DriftStatus::Drift {
1709 break;
1710 }
1711 }
1712 }
1713
1714 #[test]
1721 fn adwin_delta_affects_sensitivity() {
1722 fn feed(mut detector: AdwinDetector<f64>) -> Option<usize> {
1723 for i in 0..20 {
1725 let value = 1.0 + 0.02 * ((i % 3) as f64 - 1.0);
1726 detector.update(value);
1727 }
1728 for i in 0..40 {
1730 let value = 1.15 + 0.02 * ((i % 3) as f64 - 1.0);
1731 if detector.update(value) == DriftStatus::Drift {
1732 return Some(i);
1733 }
1734 }
1735 None
1736 }
1737
1738 let lenient = feed(AdwinDetector::new(0.5f64, 200)); let strict = feed(AdwinDetector::new(1e-6f64, 200)); match (lenient, strict) {
1744 (Some(_), None) => {} (Some(l), Some(s)) => assert!(
1746 s >= l,
1747 "C2 regression: stricter delta (1e-6) fired sooner ({s}) than \
1748 lenient delta (0.5, fired at {l}) — delta has no effect on sensitivity"
1749 ),
1750 (None, Some(_)) => {
1751 panic!("C2 regression: stricter delta fired but the more lenient delta did not")
1752 }
1753 (None, None) => {
1754 }
1757 }
1758 }
1759
1760 #[test]
1761 fn test_ddm_detector() {
1762 let mut detector = DdmDetector::<f64>::new();
1763
1764 for i in 0..50 {
1766 let iserror = i % 10 == 0; detector.update(iserror);
1768 }
1769
1770 for i in 0..20 {
1772 let iserror = i % 2 == 0; let status = detector.update(iserror);
1774 if status == DriftStatus::Drift {
1775 break;
1776 }
1777 }
1778 }
1779
1780 #[test]
1781 fn test_concept_drift_detector() {
1782 let config = DriftDetectorConfig::default();
1783 let mut detector = ConceptDriftDetector::new(config);
1784
1785 for i in 0..30 {
1787 let loss = 0.1 + (i as f64) * 0.001;
1788 let iserror = i % 10 == 0;
1789 let status = detector.update(loss, iserror).expect("unwrap failed");
1790 assert_ne!(status, DriftStatus::Drift); }
1792
1793 for i in 0..20 {
1795 let loss = 0.5 + (i as f64) * 0.01; let iserror = i % 2 == 0; let _status = detector.update(loss, iserror).expect("unwrap failed");
1798 }
1799
1800 let stats = detector.get_statistics();
1801 assert!(stats.total_drifts > 0 || stats.recent_drift_rate > 0.0);
1802 }
1803
1804 #[test]
1805 fn test_drift_event() {
1806 let event = DriftEvent {
1807 timestamp: Instant::now(),
1808 confidence: 0.85f64,
1809 drift_type: DriftType::Sudden,
1810 adaptation_recommendation: AdaptationRecommendation::Reset,
1811 };
1812
1813 assert_eq!(event.drift_type, DriftType::Sudden);
1814 assert!(event.confidence > 0.8);
1815 }
1816}