1use sklears_core::{
8 error::{Result, SklearsError},
9 traits::Estimator,
10 types::Float,
11};
12use std::collections::{HashMap, VecDeque};
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone)]
17pub struct MonitoringConfig {
18 pub window_size: usize,
20 pub degradation_threshold: Float,
22 pub drift_threshold: Float,
24 pub min_samples: usize,
26 pub monitoring_frequency: usize,
28 pub enable_auto_retrain: bool,
30 pub max_training_time: Duration,
32 pub metrics_to_track: Vec<PerformanceMetric>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub enum PerformanceMetric {
39 Accuracy,
41 Precision,
43 Recall,
45 F1Score,
47 AUC,
49 MeanSquaredError,
51 MeanAbsoluteError,
53 R2Score,
55 Latency,
57 MemoryUsage,
59 Confidence,
61 Entropy,
63 Custom(String),
65}
66
67#[derive(Debug, Clone)]
69pub struct PerformanceDataPoint {
70 pub timestamp: u64,
72 pub metrics: HashMap<PerformanceMetric, Float>,
74 pub sample_size: usize,
76 pub metadata: HashMap<String, String>,
78}
79
80#[derive(Debug, Clone)]
82pub struct DriftDetectionResult {
83 pub drift_detected: bool,
85 pub confidence: Float,
87 pub drift_type: DriftType,
89 pub affected_features: Vec<usize>,
91 pub severity: Float,
93 pub recommended_action: RecommendedAction,
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub enum DriftType {
100 Sudden,
102 Gradual,
104 Recurring,
106 None,
108}
109
110#[derive(Debug, Clone, PartialEq)]
112pub enum RecommendedAction {
113 ContinueMonitoring,
115 IncreaseMonitoring,
117 Retrain,
119 UpdateWeights,
121 ExpandEnsemble,
123 PruneEnsemble,
125 RebuildEnsemble,
127}
128
129#[derive(Debug, Clone, PartialEq)]
131pub enum ModelHealth {
132 Healthy,
134 Warning,
136 Critical,
138 Failed,
140}
141
142#[derive(Debug, Clone)]
144pub struct MonitoringResults {
145 pub health_status: ModelHealth,
147 pub performance_trend: PerformanceTrend,
149 pub drift_results: Vec<DriftDetectionResult>,
151 pub degradation_indicators: DegradationIndicators,
153 pub recommendations: Vec<RecommendedAction>,
155 pub metrics_history: Vec<PerformanceDataPoint>,
157}
158
159#[derive(Debug, Clone)]
161pub struct PerformanceTrend {
162 pub direction: Float,
164 pub significance: Float,
166 pub rate_of_change: Float,
168 pub confidence_interval: (Float, Float),
170 pub projection: Float,
172}
173
174#[derive(Debug, Clone)]
176pub struct DegradationIndicators {
177 pub accuracy_drop: Float,
179 pub variance_increase: Float,
181 pub latency_increase: Float,
183 pub memory_increase: Float,
185 pub degradation_score: Float,
187}
188
189pub struct EnsembleMonitor {
191 config: MonitoringConfig,
193 performance_history: VecDeque<PerformanceDataPoint>,
195 baseline_metrics: HashMap<PerformanceMetric, Float>,
197 drift_detector: DriftDetector,
199 sample_count: usize,
201 last_monitoring: Option<Instant>,
203}
204
205struct DriftDetector {
207 adwin_detector: ADWINDetector,
209 page_hinkley: PageHinkleyDetector,
211 statistical_tests: StatisticalDriftTests,
213}
214
215struct ADWINDetector {
217 window: VecDeque<Float>,
219 min_window_size: usize,
221 confidence: Float,
223 total_sum: Float,
225 sum_squares: Float,
227}
228
229struct PageHinkleyDetector {
231 cumsum: Float,
233 min_cumsum: Float,
235 threshold: Float,
237 min_samples: usize,
239 sample_count: usize,
241}
242
243struct StatisticalDriftTests {
245 reference_samples: VecDeque<Float>,
247 recent_samples: VecDeque<Float>,
249 max_samples: usize,
251}
252
253impl EnsembleMonitor {
254 pub fn new(config: MonitoringConfig) -> Self {
256 Self {
257 performance_history: VecDeque::with_capacity(config.window_size),
258 baseline_metrics: HashMap::new(),
259 drift_detector: DriftDetector::new(&config),
260 sample_count: 0,
261 last_monitoring: None,
262 config,
263 }
264 }
265
266 pub fn set_baseline(&mut self, metrics: HashMap<PerformanceMetric, Float>) {
268 self.baseline_metrics = metrics;
269 }
270
271 pub fn add_measurement(&mut self, data_point: PerformanceDataPoint) -> Result<()> {
273 self.performance_history.push_back(data_point.clone());
275
276 if self.performance_history.len() > self.config.window_size {
278 self.performance_history.pop_front();
279 }
280
281 if let Some(&accuracy) = data_point.metrics.get(&PerformanceMetric::Accuracy) {
283 self.drift_detector.update(accuracy)?;
284 }
285
286 self.sample_count += data_point.sample_size;
287
288 Ok(())
289 }
290
291 pub fn monitor_performance(&mut self) -> Result<MonitoringResults> {
293 if self.performance_history.len() < self.config.min_samples {
295 return Err(SklearsError::InvalidInput(
296 "Insufficient data for monitoring".to_string(),
297 ));
298 }
299
300 let health_status = self.assess_health_status()?;
302
303 let performance_trend = self.analyze_performance_trend()?;
305
306 let drift_results = self.detect_drift()?;
308
309 let degradation_indicators = self.compute_degradation_indicators()?;
311
312 let recommendations = self.generate_recommendations(
314 &health_status,
315 &performance_trend,
316 &drift_results,
317 °radation_indicators,
318 )?;
319
320 Ok(MonitoringResults {
321 health_status,
322 performance_trend,
323 drift_results,
324 degradation_indicators,
325 recommendations,
326 metrics_history: self.performance_history.iter().cloned().collect(),
327 })
328 }
329
330 fn assess_health_status(&self) -> Result<ModelHealth> {
332 if self.performance_history.is_empty() {
333 return Ok(ModelHealth::Warning);
334 }
335
336 let recent_performance = self.get_recent_average_performance()?;
337 let degradation_score = self.compute_overall_degradation_score(&recent_performance)?;
338
339 if degradation_score > 0.5 {
340 Ok(ModelHealth::Failed)
341 } else if degradation_score > 0.3 {
342 Ok(ModelHealth::Critical)
343 } else if degradation_score > 0.1 {
344 Ok(ModelHealth::Warning)
345 } else {
346 Ok(ModelHealth::Healthy)
347 }
348 }
349
350 fn get_recent_average_performance(&self) -> Result<HashMap<PerformanceMetric, Float>> {
352 if self.performance_history.is_empty() {
353 return Ok(HashMap::new());
354 }
355
356 let recent_window = self.config.window_size.min(10);
357 let start_idx = if self.performance_history.len() > recent_window {
358 self.performance_history.len() - recent_window
359 } else {
360 0
361 };
362
363 let mut metric_sums: HashMap<PerformanceMetric, Float> = HashMap::new();
364 let mut metric_counts: HashMap<PerformanceMetric, usize> = HashMap::new();
365
366 for data_point in self.performance_history.range(start_idx..) {
367 for (metric, value) in &data_point.metrics {
368 *metric_sums.entry(metric.clone()).or_insert(0.0) += value;
369 *metric_counts.entry(metric.clone()).or_insert(0) += 1;
370 }
371 }
372
373 let mut averages = HashMap::new();
374 for (metric, sum) in metric_sums {
375 if let Some(&count) = metric_counts.get(&metric) {
376 averages.insert(metric, sum / count as Float);
377 }
378 }
379
380 Ok(averages)
381 }
382
383 fn compute_overall_degradation_score(
385 &self,
386 recent_performance: &HashMap<PerformanceMetric, Float>,
387 ) -> Result<Float> {
388 if self.baseline_metrics.is_empty() {
389 return Ok(0.0);
390 }
391
392 let mut degradation_sum = 0.0;
393 let mut count = 0;
394
395 for (metric, &recent_value) in recent_performance {
396 if let Some(&baseline_value) = self.baseline_metrics.get(metric) {
397 let degradation = match metric {
398 PerformanceMetric::Accuracy
400 | PerformanceMetric::Precision
401 | PerformanceMetric::Recall
402 | PerformanceMetric::F1Score
403 | PerformanceMetric::AUC
404 | PerformanceMetric::R2Score => {
405 (baseline_value - recent_value) / baseline_value.max(1e-8)
406 }
407 PerformanceMetric::MeanSquaredError
409 | PerformanceMetric::MeanAbsoluteError
410 | PerformanceMetric::Latency
411 | PerformanceMetric::MemoryUsage => {
412 (recent_value - baseline_value) / baseline_value.max(1e-8)
413 }
414 _ => 0.0,
416 };
417
418 degradation_sum += degradation.max(0.0); count += 1;
420 }
421 }
422
423 Ok(if count > 0 {
424 degradation_sum / count as Float
425 } else {
426 0.0
427 })
428 }
429
430 fn analyze_performance_trend(&self) -> Result<PerformanceTrend> {
432 if self.performance_history.len() < 3 {
433 return Ok(PerformanceTrend {
434 direction: 0.0,
435 significance: 0.0,
436 rate_of_change: 0.0,
437 confidence_interval: (0.0, 0.0),
438 projection: 0.0,
439 });
440 }
441
442 let accuracy_values: Vec<Float> = self
444 .performance_history
445 .iter()
446 .filter_map(|dp| dp.metrics.get(&PerformanceMetric::Accuracy))
447 .copied()
448 .collect();
449
450 if accuracy_values.len() < 3 {
451 return Ok(PerformanceTrend {
452 direction: 0.0,
453 significance: 0.0,
454 rate_of_change: 0.0,
455 confidence_interval: (0.0, 0.0),
456 projection: 0.0,
457 });
458 }
459
460 let n = accuracy_values.len() as Float;
462 let x_values: Vec<Float> = (0..accuracy_values.len()).map(|i| i as Float).collect();
463
464 let x_mean = x_values.iter().sum::<Float>() / n;
465 let y_mean = accuracy_values.iter().sum::<Float>() / n;
466
467 let mut numerator = 0.0;
468 let mut denominator = 0.0;
469
470 for i in 0..accuracy_values.len() {
471 let x_diff = x_values[i] - x_mean;
472 let y_diff = accuracy_values[i] - y_mean;
473 numerator += x_diff * y_diff;
474 denominator += x_diff * x_diff;
475 }
476
477 let slope = if denominator != 0.0 {
478 numerator / denominator
479 } else {
480 0.0
481 };
482 let intercept = y_mean - slope * x_mean;
483
484 let mut ss_res = 0.0;
486 let mut ss_tot = 0.0;
487
488 for i in 0..accuracy_values.len() {
489 let y_pred = slope * x_values[i] + intercept;
490 ss_res += (accuracy_values[i] - y_pred).powi(2);
491 ss_tot += (accuracy_values[i] - y_mean).powi(2);
492 }
493
494 let r_squared = if ss_tot != 0.0 {
495 1.0 - (ss_res / ss_tot)
496 } else {
497 0.0
498 };
499
500 let std_error = (ss_res / (n - 2.0)).sqrt();
502 let t_value = 1.96; let margin_error = t_value * std_error;
504
505 let projection = slope * n + intercept;
507
508 Ok(PerformanceTrend {
509 direction: slope,
510 significance: r_squared,
511 rate_of_change: slope,
512 confidence_interval: (projection - margin_error, projection + margin_error),
513 projection,
514 })
515 }
516
517 fn detect_drift(&mut self) -> Result<Vec<DriftDetectionResult>> {
519 let mut results = Vec::new();
520
521 if let Some(adwin_result) = self.drift_detector.adwin_detector.check_drift() {
523 results.push(DriftDetectionResult {
524 drift_detected: true,
525 confidence: adwin_result.confidence,
526 drift_type: DriftType::Sudden,
527 affected_features: vec![],
528 severity: adwin_result.severity,
529 recommended_action: RecommendedAction::Retrain,
530 });
531 }
532
533 if self.drift_detector.page_hinkley.detect_drift() {
535 results.push(DriftDetectionResult {
536 drift_detected: true,
537 confidence: 0.95,
538 drift_type: DriftType::Gradual,
539 affected_features: vec![],
540 severity: 0.7,
541 recommended_action: RecommendedAction::UpdateWeights,
542 });
543 }
544
545 if let Some(stat_result) = self
547 .drift_detector
548 .statistical_tests
549 .kolmogorov_smirnov_test()?
550 {
551 results.push(DriftDetectionResult {
552 drift_detected: stat_result.p_value < 0.05,
553 confidence: 1.0 - stat_result.p_value,
554 drift_type: DriftType::Sudden,
555 affected_features: vec![],
556 severity: stat_result.test_statistic,
557 recommended_action: if stat_result.p_value < 0.01 {
558 RecommendedAction::RebuildEnsemble
559 } else {
560 RecommendedAction::Retrain
561 },
562 });
563 }
564
565 if results.is_empty() {
566 results.push(DriftDetectionResult {
567 drift_detected: false,
568 confidence: 0.0,
569 drift_type: DriftType::None,
570 affected_features: vec![],
571 severity: 0.0,
572 recommended_action: RecommendedAction::ContinueMonitoring,
573 });
574 }
575
576 Ok(results)
577 }
578
579 fn compute_degradation_indicators(&self) -> Result<DegradationIndicators> {
581 let recent_performance = self.get_recent_average_performance()?;
582
583 let accuracy_drop = if let (Some(&baseline), Some(&recent)) = (
584 self.baseline_metrics.get(&PerformanceMetric::Accuracy),
585 recent_performance.get(&PerformanceMetric::Accuracy),
586 ) {
587 baseline - recent
588 } else {
589 0.0
590 };
591
592 let variance_increase = self.compute_prediction_variance_increase()?;
594
595 let latency_increase = if let (Some(&baseline), Some(&recent)) = (
596 self.baseline_metrics.get(&PerformanceMetric::Latency),
597 recent_performance.get(&PerformanceMetric::Latency),
598 ) {
599 recent - baseline
600 } else {
601 0.0
602 };
603
604 let memory_increase = if let (Some(&baseline), Some(&recent)) = (
605 self.baseline_metrics.get(&PerformanceMetric::MemoryUsage),
606 recent_performance.get(&PerformanceMetric::MemoryUsage),
607 ) {
608 recent - baseline
609 } else {
610 0.0
611 };
612
613 let degradation_score =
614 (accuracy_drop + variance_increase + latency_increase + memory_increase) / 4.0;
615
616 Ok(DegradationIndicators {
617 accuracy_drop,
618 variance_increase,
619 latency_increase,
620 memory_increase,
621 degradation_score,
622 })
623 }
624
625 fn compute_prediction_variance_increase(&self) -> Result<Float> {
627 if self.performance_history.len() < 5 {
629 return Ok(0.0);
630 }
631
632 let recent_confidence: Vec<Float> = self
633 .performance_history
634 .iter()
635 .rev()
636 .take(5)
637 .filter_map(|dp| dp.metrics.get(&PerformanceMetric::Confidence))
638 .copied()
639 .collect();
640
641 let baseline_confidence: Vec<Float> = self
642 .performance_history
643 .iter()
644 .take(5)
645 .filter_map(|dp| dp.metrics.get(&PerformanceMetric::Confidence))
646 .copied()
647 .collect();
648
649 if recent_confidence.is_empty() || baseline_confidence.is_empty() {
650 return Ok(0.0);
651 }
652
653 let recent_var = self.compute_variance(&recent_confidence);
654 let baseline_var = self.compute_variance(&baseline_confidence);
655
656 Ok(recent_var - baseline_var)
657 }
658
659 fn compute_variance(&self, values: &[Float]) -> Float {
661 if values.len() < 2 {
662 return 0.0;
663 }
664
665 let mean = values.iter().sum::<Float>() / values.len() as Float;
666 let variance =
667 values.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / values.len() as Float;
668
669 variance
670 }
671
672 fn generate_recommendations(
674 &self,
675 health_status: &ModelHealth,
676 performance_trend: &PerformanceTrend,
677 drift_results: &[DriftDetectionResult],
678 degradation_indicators: &DegradationIndicators,
679 ) -> Result<Vec<RecommendedAction>> {
680 let mut recommendations = Vec::new();
681
682 match health_status {
684 ModelHealth::Failed => {
685 recommendations.push(RecommendedAction::RebuildEnsemble);
686 }
687 ModelHealth::Critical => {
688 recommendations.push(RecommendedAction::Retrain);
689 recommendations.push(RecommendedAction::PruneEnsemble);
690 }
691 ModelHealth::Warning => {
692 recommendations.push(RecommendedAction::IncreaseMonitoring);
693 recommendations.push(RecommendedAction::UpdateWeights);
694 }
695 ModelHealth::Healthy => {
696 recommendations.push(RecommendedAction::ContinueMonitoring);
697 }
698 }
699
700 if performance_trend.direction < -0.01 && performance_trend.significance > 0.5 {
702 recommendations.push(RecommendedAction::Retrain);
703 }
704
705 for drift_result in drift_results {
707 if drift_result.drift_detected {
708 recommendations.push(drift_result.recommended_action.clone());
709 }
710 }
711
712 if degradation_indicators.accuracy_drop > 0.1 {
714 recommendations.push(RecommendedAction::Retrain);
715 }
716
717 if degradation_indicators.latency_increase > 0.5 {
718 recommendations.push(RecommendedAction::PruneEnsemble);
719 }
720
721 recommendations.sort_by(|a, b| format!("{:?}", a).cmp(&format!("{:?}", b)));
723 recommendations.dedup();
724
725 Ok(recommendations)
726 }
727
728 pub fn should_trigger_retrain(&self, monitoring_results: &MonitoringResults) -> bool {
730 if !self.config.enable_auto_retrain {
731 return false;
732 }
733
734 matches!(
736 monitoring_results.health_status,
737 ModelHealth::Critical | ModelHealth::Failed
738 ) || monitoring_results
739 .drift_results
740 .iter()
741 .any(|dr| dr.drift_detected && dr.confidence > 0.8)
742 || monitoring_results.degradation_indicators.degradation_score
743 > self.config.degradation_threshold
744 }
745}
746
747impl DriftDetector {
748 fn new(config: &MonitoringConfig) -> Self {
750 Self {
751 adwin_detector: ADWINDetector::new(0.002, 100), page_hinkley: PageHinkleyDetector::new(0.01, 30), statistical_tests: StatisticalDriftTests::new(1000), }
755 }
756
757 fn update(&mut self, value: Float) -> Result<()> {
759 self.adwin_detector.add_value(value);
760 self.page_hinkley.add_value(value);
761 self.statistical_tests.add_value(value);
762 Ok(())
763 }
764}
765
766impl ADWINDetector {
767 fn new(delta: Float, min_window_size: usize) -> Self {
769 Self {
770 window: VecDeque::new(),
771 min_window_size,
772 confidence: 1.0 - delta,
773 total_sum: 0.0,
774 sum_squares: 0.0,
775 }
776 }
777
778 fn add_value(&mut self, value: Float) {
780 self.window.push_back(value);
781 self.total_sum += value;
782 self.sum_squares += value * value;
783
784 self.check_and_adjust_window();
786 }
787
788 fn check_and_adjust_window(&mut self) {
790 if self.window.len() < self.min_window_size {
791 return;
792 }
793
794 let n = self.window.len() as Float;
795 let mean = self.total_sum / n;
796
797 let variance = (self.sum_squares / n) - (mean * mean);
799
800 if variance > 0.1 && self.window.len() > self.min_window_size {
802 let removed = self.window.pop_front().unwrap();
803 self.total_sum -= removed;
804 self.sum_squares -= removed * removed;
805 }
806 }
807
808 fn check_drift(&self) -> Option<ADWINDriftResult> {
810 if self.window.len() < self.min_window_size {
811 return None;
812 }
813
814 let n = self.window.len() as Float;
816 let mean = self.total_sum / n;
817 let variance = (self.sum_squares / n) - (mean * mean);
818
819 if variance > 0.05 {
821 Some(ADWINDriftResult {
822 confidence: self.confidence,
823 severity: variance,
824 })
825 } else {
826 None
827 }
828 }
829}
830
831impl PageHinkleyDetector {
832 fn new(threshold: Float, min_samples: usize) -> Self {
834 Self {
835 cumsum: 0.0,
836 min_cumsum: 0.0,
837 threshold,
838 min_samples,
839 sample_count: 0,
840 }
841 }
842
843 fn add_value(&mut self, value: Float) {
845 self.sample_count += 1;
846
847 let normalized_value = 0.8 - value; self.cumsum += normalized_value;
852 self.min_cumsum = self.min_cumsum.min(self.cumsum);
853 }
854
855 fn detect_drift(&self) -> bool {
857 if self.sample_count < self.min_samples {
858 return false;
859 }
860
861 (self.cumsum - self.min_cumsum) > self.threshold
862 }
863}
864
865impl StatisticalDriftTests {
866 fn new(max_samples: usize) -> Self {
868 Self {
869 reference_samples: VecDeque::with_capacity(max_samples),
870 recent_samples: VecDeque::with_capacity(max_samples / 2),
871 max_samples,
872 }
873 }
874
875 fn add_value(&mut self, value: Float) {
877 if self.reference_samples.len() < self.max_samples / 2 {
879 self.reference_samples.push_back(value);
880 } else {
881 self.recent_samples.push_back(value);
882 if self.recent_samples.len() > self.max_samples / 2 {
883 self.recent_samples.pop_front();
884 }
885 }
886 }
887
888 fn kolmogorov_smirnov_test(&self) -> Result<Option<KSTestResult>> {
890 if self.reference_samples.len() < 10 || self.recent_samples.len() < 10 {
891 return Ok(None);
892 }
893
894 let mut ref_sorted: Vec<Float> = self.reference_samples.iter().copied().collect();
896 let mut recent_sorted: Vec<Float> = self.recent_samples.iter().copied().collect();
897
898 ref_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
899 recent_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
900
901 let mut max_diff: Float = 0.0;
903 let n1 = ref_sorted.len() as Float;
904 let n2 = recent_sorted.len() as Float;
905
906 let mut all_values: Vec<Float> = ref_sorted
908 .iter()
909 .chain(recent_sorted.iter())
910 .copied()
911 .collect();
912 all_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
913 all_values.dedup();
914
915 for value in &all_values {
916 let cdf1 = ref_sorted.iter().filter(|&&x| x <= *value).count() as Float / n1;
917 let cdf2 = recent_sorted.iter().filter(|&&x| x <= *value).count() as Float / n2;
918
919 max_diff = max_diff.max((cdf1 - cdf2).abs());
920 }
921
922 let ks_statistic = max_diff;
924 let p_value = self.approximate_ks_p_value(ks_statistic, n1 as usize, n2 as usize);
925
926 Ok(Some(KSTestResult {
927 test_statistic: ks_statistic,
928 p_value,
929 }))
930 }
931
932 fn approximate_ks_p_value(&self, d: Float, n1: usize, n2: usize) -> Float {
934 let n = ((n1 * n2) as Float / (n1 + n2) as Float).sqrt();
935 let z = d * n;
936
937 if z > 3.0 {
939 0.0
940 } else if z > 2.0 {
941 0.001
942 } else if z > 1.5 {
943 0.01
944 } else if z > 1.0 {
945 0.05
946 } else {
947 0.5
948 }
949 }
950}
951
952#[derive(Debug, Clone)]
954struct ADWINDriftResult {
955 confidence: Float,
956 severity: Float,
957}
958
959#[derive(Debug, Clone)]
961struct KSTestResult {
962 test_statistic: Float,
963 p_value: Float,
964}
965
966impl Default for MonitoringConfig {
967 fn default() -> Self {
968 Self {
969 window_size: 1000,
970 degradation_threshold: 0.1,
971 drift_threshold: 0.05,
972 min_samples: 50,
973 monitoring_frequency: 10,
974 enable_auto_retrain: false,
975 max_training_time: Duration::from_secs(3600), metrics_to_track: vec![
977 PerformanceMetric::Accuracy,
978 PerformanceMetric::Latency,
979 PerformanceMetric::Confidence,
980 ],
981 }
982 }
983}
984
985impl MonitoringConfig {
987 pub fn high_frequency() -> Self {
989 Self {
990 monitoring_frequency: 1,
991 min_samples: 10,
992 window_size: 500,
993 ..Default::default()
994 }
995 }
996
997 pub fn production() -> Self {
999 Self {
1000 window_size: 5000,
1001 degradation_threshold: 0.05,
1002 drift_threshold: 0.03,
1003 min_samples: 100,
1004 monitoring_frequency: 50,
1005 enable_auto_retrain: true,
1006 ..Default::default()
1007 }
1008 }
1009
1010 pub fn development() -> Self {
1012 Self {
1013 window_size: 100,
1014 degradation_threshold: 0.2,
1015 min_samples: 20,
1016 monitoring_frequency: 5,
1017 enable_auto_retrain: false,
1018 ..Default::default()
1019 }
1020 }
1021}
1022
1023#[allow(non_snake_case)]
1024#[cfg(test)]
1025mod tests {
1026 use super::*;
1027 use std::time::{SystemTime, UNIX_EPOCH};
1028
1029 #[test]
1030 fn test_monitoring_config_creation() {
1031 let config = MonitoringConfig::default();
1032 assert_eq!(config.window_size, 1000);
1033 assert_eq!(config.min_samples, 50);
1034 assert!(!config.enable_auto_retrain);
1035 }
1036
1037 #[test]
1038 fn test_ensemble_monitor_creation() {
1039 let config = MonitoringConfig::default();
1040 let monitor = EnsembleMonitor::new(config);
1041 assert_eq!(monitor.sample_count, 0);
1042 assert!(monitor.performance_history.is_empty());
1043 }
1044
1045 #[test]
1046 fn test_performance_data_point_creation() {
1047 let mut metrics = HashMap::new();
1048 metrics.insert(PerformanceMetric::Accuracy, 0.85);
1049 metrics.insert(PerformanceMetric::Latency, 100.0);
1050
1051 let data_point = PerformanceDataPoint {
1052 timestamp: SystemTime::now()
1053 .duration_since(UNIX_EPOCH)
1054 .unwrap()
1055 .as_secs(),
1056 metrics,
1057 sample_size: 100,
1058 metadata: HashMap::new(),
1059 };
1060
1061 assert_eq!(data_point.sample_size, 100);
1062 assert!(data_point
1063 .metrics
1064 .contains_key(&PerformanceMetric::Accuracy));
1065 }
1066
1067 #[test]
1068 fn test_add_measurement() {
1069 let config = MonitoringConfig::default();
1070 let mut monitor = EnsembleMonitor::new(config);
1071
1072 let mut metrics = HashMap::new();
1073 metrics.insert(PerformanceMetric::Accuracy, 0.85);
1074
1075 let data_point = PerformanceDataPoint {
1076 timestamp: SystemTime::now()
1077 .duration_since(UNIX_EPOCH)
1078 .unwrap()
1079 .as_secs(),
1080 metrics,
1081 sample_size: 100,
1082 metadata: HashMap::new(),
1083 };
1084
1085 monitor.add_measurement(data_point).unwrap();
1086
1087 assert_eq!(monitor.performance_history.len(), 1);
1088 assert_eq!(monitor.sample_count, 100);
1089 }
1090
1091 #[test]
1092 fn test_baseline_setting() {
1093 let config = MonitoringConfig::default();
1094 let mut monitor = EnsembleMonitor::new(config);
1095
1096 let mut baseline = HashMap::new();
1097 baseline.insert(PerformanceMetric::Accuracy, 0.9);
1098 baseline.insert(PerformanceMetric::Latency, 50.0);
1099
1100 monitor.set_baseline(baseline.clone());
1101
1102 assert_eq!(monitor.baseline_metrics.len(), 2);
1103 assert_eq!(monitor.baseline_metrics[&PerformanceMetric::Accuracy], 0.9);
1104 }
1105
1106 #[test]
1107 fn test_health_assessment() {
1108 let config = MonitoringConfig::default();
1109 let mut monitor = EnsembleMonitor::new(config);
1110
1111 let mut baseline = HashMap::new();
1113 baseline.insert(PerformanceMetric::Accuracy, 0.9);
1114 monitor.set_baseline(baseline);
1115
1116 for i in 0..60 {
1118 let mut metrics = HashMap::new();
1119 metrics.insert(PerformanceMetric::Accuracy, 0.9 - (i as Float * 0.01)); let data_point = PerformanceDataPoint {
1122 timestamp: SystemTime::now()
1123 .duration_since(UNIX_EPOCH)
1124 .unwrap()
1125 .as_secs(),
1126 metrics,
1127 sample_size: 10,
1128 metadata: HashMap::new(),
1129 };
1130
1131 monitor.add_measurement(data_point).unwrap();
1132 }
1133
1134 let health = monitor.assess_health_status().unwrap();
1135 assert!(matches!(
1136 health,
1137 ModelHealth::Warning | ModelHealth::Critical | ModelHealth::Failed
1138 ));
1139 }
1140
1141 #[test]
1142 fn test_adwin_detector() {
1143 let mut adwin = ADWINDetector::new(0.002, 10);
1144
1145 for _ in 0..20 {
1147 adwin.add_value(0.8);
1148 }
1149
1150 assert!(adwin.check_drift().is_none());
1151
1152 for _ in 0..10 {
1154 adwin.add_value(0.6);
1155 }
1156
1157 let _drift_result = adwin.check_drift();
1159 }
1160
1161 #[test]
1162 fn test_page_hinkley_detector() {
1163 let mut ph = PageHinkleyDetector::new(0.1, 10);
1164
1165 for _ in 0..15 {
1167 ph.add_value(0.8);
1168 }
1169
1170 assert!(!ph.detect_drift());
1171
1172 for i in 0..10 {
1174 ph.add_value(0.8 - (i as Float * 0.05));
1175 }
1176
1177 assert!(ph.detect_drift());
1179 }
1180
1181 #[test]
1182 fn test_performance_trend_analysis() {
1183 let config = MonitoringConfig::default();
1184 let mut monitor = EnsembleMonitor::new(config);
1185
1186 for i in 0..20 {
1188 let mut metrics = HashMap::new();
1189 metrics.insert(PerformanceMetric::Accuracy, 0.9 - (i as Float * 0.01));
1190
1191 let data_point = PerformanceDataPoint {
1192 timestamp: SystemTime::now()
1193 .duration_since(UNIX_EPOCH)
1194 .unwrap()
1195 .as_secs(),
1196 metrics,
1197 sample_size: 10,
1198 metadata: HashMap::new(),
1199 };
1200
1201 monitor.add_measurement(data_point).unwrap();
1202 }
1203
1204 let trend = monitor.analyze_performance_trend().unwrap();
1205 assert!(trend.direction < 0.0); }
1207
1208 #[test]
1209 fn test_degradation_indicators() {
1210 let config = MonitoringConfig::default();
1211 let mut monitor = EnsembleMonitor::new(config);
1212
1213 let mut baseline = HashMap::new();
1215 baseline.insert(PerformanceMetric::Accuracy, 0.9);
1216 baseline.insert(PerformanceMetric::Latency, 50.0);
1217 monitor.set_baseline(baseline);
1218
1219 let mut metrics = HashMap::new();
1221 metrics.insert(PerformanceMetric::Accuracy, 0.7); metrics.insert(PerformanceMetric::Latency, 100.0); for _ in 0..60 {
1225 let data_point = PerformanceDataPoint {
1226 timestamp: SystemTime::now()
1227 .duration_since(UNIX_EPOCH)
1228 .unwrap()
1229 .as_secs(),
1230 metrics: metrics.clone(),
1231 sample_size: 10,
1232 metadata: HashMap::new(),
1233 };
1234
1235 monitor.add_measurement(data_point).unwrap();
1236 }
1237
1238 let indicators = monitor.compute_degradation_indicators().unwrap();
1239 assert!(indicators.accuracy_drop > 0.0);
1240 assert!(indicators.latency_increase > 0.0);
1241 assert!(indicators.degradation_score > 0.0);
1242 }
1243
1244 #[test]
1245 fn test_monitoring_configurations() {
1246 let prod_config = MonitoringConfig::production();
1247 assert_eq!(prod_config.window_size, 5000);
1248 assert!(prod_config.enable_auto_retrain);
1249
1250 let dev_config = MonitoringConfig::development();
1251 assert_eq!(dev_config.window_size, 100);
1252 assert!(!dev_config.enable_auto_retrain);
1253
1254 let hf_config = MonitoringConfig::high_frequency();
1255 assert_eq!(hf_config.monitoring_frequency, 1);
1256 }
1257
1258 #[test]
1259 fn test_recommendation_generation() {
1260 let config = MonitoringConfig::default();
1261 let mut monitor = EnsembleMonitor::new(config);
1262
1263 let health_status = ModelHealth::Critical;
1264 let performance_trend = PerformanceTrend {
1265 direction: -0.02,
1266 significance: 0.8,
1267 rate_of_change: -0.02,
1268 confidence_interval: (0.7, 0.8),
1269 projection: 0.75,
1270 };
1271 let drift_results = vec![DriftDetectionResult {
1272 drift_detected: true,
1273 confidence: 0.9,
1274 drift_type: DriftType::Sudden,
1275 affected_features: vec![],
1276 severity: 0.8,
1277 recommended_action: RecommendedAction::Retrain,
1278 }];
1279 let degradation_indicators = DegradationIndicators {
1280 accuracy_drop: 0.15,
1281 variance_increase: 0.1,
1282 latency_increase: 0.2,
1283 memory_increase: 0.05,
1284 degradation_score: 0.125,
1285 };
1286
1287 let recommendations = monitor
1288 .generate_recommendations(
1289 &health_status,
1290 &performance_trend,
1291 &drift_results,
1292 °radation_indicators,
1293 )
1294 .unwrap();
1295
1296 assert!(!recommendations.is_empty());
1297 assert!(recommendations.contains(&RecommendedAction::Retrain));
1298 }
1299}