1use super::config::AnomalyType;
4use crate::error::{MLError, Result};
5use scirs2_core::ndarray::{Array1, Array2};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ForecastMetrics {
12 pub mae: f64,
14
15 pub mse: f64,
17
18 pub rmse: f64,
20
21 pub mape: f64,
23
24 pub smape: f64,
26
27 pub directional_accuracy: f64,
29
30 pub quantum_fidelity: f64,
32
33 pub coverage: f64,
35
36 pub custom_metrics: HashMap<String, f64>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ForecastResult {
43 pub predictions: Array2<f64>,
45
46 pub lower_bound: Array2<f64>,
48
49 pub upper_bound: Array2<f64>,
51
52 pub anomalies: Vec<AnomalyPoint>,
54
55 pub confidence_scores: Array1<f64>,
57
58 pub quantum_uncertainty: f64,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct AnomalyPoint {
65 pub timestamp: usize,
67
68 pub value: f64,
70
71 pub anomaly_score: f64,
73
74 pub anomaly_type: AnomalyType,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct TrainingHistory {
81 pub losses: Vec<f64>,
83
84 pub val_losses: Vec<f64>,
86
87 pub metrics: Vec<HashMap<String, f64>>,
89
90 pub best_params: Option<Array1<f64>>,
92
93 pub training_time: f64,
95
96 pub learning_curves: LearningCurves,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct LearningCurves {
103 pub training_accuracy: Vec<f64>,
105
106 pub validation_accuracy: Vec<f64>,
108
109 pub learning_rates: Vec<f64>,
111
112 pub quantum_coherence: Vec<f64>,
114}
115
116#[derive(Debug, Clone)]
118pub struct ModelEvaluator {
119 metrics: Vec<MetricType>,
121
122 cv_config: CrossValidationConfig,
124
125 statistical_tests: StatisticalTestConfig,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub enum MetricType {
132 MAE,
133 MSE,
134 RMSE,
135 MAPE,
136 SMAPE,
137 DirectionalAccuracy,
138 QuantumFidelity,
139 Coverage,
140 MSIS, CRPS, Custom(String),
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CrossValidationConfig {
148 pub n_folds: usize,
150
151 pub strategy: ValidationStrategy,
153
154 pub time_series_split: TimeSeriesSplitConfig,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum ValidationStrategy {
161 KFold,
162 TimeSeriesSplit,
163 WalkForward,
164 BlockingTimeSeriesSplit,
165 QuantumBootstrap,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TimeSeriesSplitConfig {
171 pub test_size: f64,
173
174 pub min_train_size: Option<usize>,
176
177 pub gap: usize,
179
180 pub expanding_window: bool,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct StatisticalTestConfig {
187 pub alpha: f64,
189
190 pub tests: Vec<StatisticalTest>,
192
193 pub correction: MultipleComparisonCorrection,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub enum StatisticalTest {
200 DieboldMariano,
201 WilcoxonSignedRank,
202 PairedTTest,
203 McNemar,
204 QuantumSignificanceTest,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum MultipleComparisonCorrection {
210 None,
211 Bonferroni,
212 BenjaminiHochberg,
213 Holm,
214 QuantumCorrection,
215}
216
217#[derive(Debug, Clone)]
219pub struct BenchmarkSuite {
220 datasets: Vec<BenchmarkDataset>,
222
223 models: Vec<String>,
225
226 metrics: Vec<MetricType>,
228
229 results: BenchmarkResults,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct BenchmarkDataset {
236 pub name: String,
238
239 pub description: String,
241
242 pub characteristics: DataCharacteristics,
244
245 pub source: String,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct DataCharacteristics {
252 pub n_series: usize,
254
255 pub series_length: usize,
257
258 pub frequency: String,
260
261 pub seasonality: Vec<usize>,
263
264 pub trend_type: TrendType,
266
267 pub noise_level: f64,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub enum TrendType {
274 None,
275 Linear,
276 Exponential,
277 Polynomial,
278 Cyclic,
279 Random,
280 QuantumSuperposition,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct BenchmarkResults {
286 pub results: HashMap<String, HashMap<String, ModelPerformance>>,
288
289 pub statistical_comparisons: Vec<StatisticalComparison>,
291
292 pub rankings: HashMap<String, Vec<String>>,
294
295 pub summary: BenchmarkSummary,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ModelPerformance {
302 pub metrics: HashMap<String, f64>,
304
305 pub confidence_intervals: HashMap<String, (f64, f64)>,
307
308 pub execution_time: f64,
310
311 pub memory_usage: f64,
313
314 pub quantum_enhancement: f64,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct StatisticalComparison {
321 pub models: (String, String),
323
324 pub test_type: StatisticalTest,
326
327 pub statistic: f64,
329
330 pub p_value: f64,
332
333 pub effect_size: f64,
335
336 pub is_significant: bool,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct BenchmarkSummary {
343 pub best_model: String,
345
346 pub average_performance: HashMap<String, f64>,
348
349 pub win_rates: HashMap<String, f64>,
351
352 pub quantum_advantage: HashMap<String, f64>,
354}
355
356impl ForecastMetrics {
357 pub fn new() -> Self {
359 Self {
360 mae: 0.0,
361 mse: 0.0,
362 rmse: 0.0,
363 mape: 0.0,
364 smape: 0.0,
365 directional_accuracy: 0.0,
366 quantum_fidelity: 0.0,
367 coverage: 0.0,
368 custom_metrics: HashMap::new(),
369 }
370 }
371
372 pub fn calculate_metrics(
374 &mut self,
375 predictions: &Array2<f64>,
376 actuals: &Array2<f64>,
377 ) -> Result<()> {
378 if predictions.shape() != actuals.shape() {
379 return Err(MLError::DimensionMismatch(
380 "Predictions and actuals must have the same shape".to_string(),
381 ));
382 }
383
384 let n = predictions.len() as f64;
385
386 self.mae = 0.0;
388 self.mse = 0.0;
389 self.mape = 0.0;
390 self.smape = 0.0;
391
392 for (pred, actual) in predictions.iter().zip(actuals.iter()) {
394 let error = pred - actual;
395 let abs_error = error.abs();
396
397 self.mae += abs_error;
398 self.mse += error * error;
399
400 if actual.abs() > 1e-10 {
402 self.mape += (abs_error / actual.abs()) * 100.0;
403 }
404
405 let denominator = (pred.abs() + actual.abs()) / 2.0;
407 if denominator > 1e-10 {
408 self.smape += (abs_error / denominator) * 100.0;
409 }
410 }
411
412 self.mae /= n;
414 self.mse /= n;
415 self.mape /= n;
416 self.smape /= n;
417
418 self.rmse = self.mse.sqrt();
420
421 self.directional_accuracy = self.calculate_directional_accuracy(predictions, actuals)?;
423
424 self.quantum_fidelity = self.calculate_quantum_fidelity(predictions, actuals)?;
426
427 self.coverage = 0.95; Ok(())
431 }
432
433 fn calculate_directional_accuracy(
435 &self,
436 predictions: &Array2<f64>,
437 actuals: &Array2<f64>,
438 ) -> Result<f64> {
439 if predictions.nrows() < 2 {
440 return Ok(0.0);
441 }
442
443 let mut correct_directions = 0;
444 let mut total_directions = 0;
445
446 for i in 1..predictions.nrows() {
447 let pred_change = predictions[[i, 0]] - predictions[[i - 1, 0]];
448 let actual_change = actuals[[i, 0]] - actuals[[i - 1, 0]];
449
450 if pred_change * actual_change > 0.0 {
451 correct_directions += 1;
452 }
453 total_directions += 1;
454 }
455
456 Ok(if total_directions > 0 {
457 correct_directions as f64 / total_directions as f64
458 } else {
459 0.0
460 })
461 }
462
463 fn calculate_quantum_fidelity(
465 &self,
466 predictions: &Array2<f64>,
467 actuals: &Array2<f64>,
468 ) -> Result<f64> {
469 let pred_flat: Vec<f64> = predictions.iter().cloned().collect();
471 let actual_flat: Vec<f64> = actuals.iter().cloned().collect();
472
473 let pred_mean = pred_flat.iter().sum::<f64>() / pred_flat.len() as f64;
474 let actual_mean = actual_flat.iter().sum::<f64>() / actual_flat.len() as f64;
475
476 let mut numerator = 0.0;
477 let mut pred_sq_sum = 0.0;
478 let mut actual_sq_sum = 0.0;
479
480 for (pred, actual) in pred_flat.iter().zip(actual_flat.iter()) {
481 let pred_dev = pred - pred_mean;
482 let actual_dev = actual - actual_mean;
483
484 numerator += pred_dev * actual_dev;
485 pred_sq_sum += pred_dev * pred_dev;
486 actual_sq_sum += actual_dev * actual_dev;
487 }
488
489 let denominator = (pred_sq_sum * actual_sq_sum).sqrt();
490 let correlation = if denominator > 1e-10 {
491 numerator / denominator
492 } else {
493 0.0
494 };
495
496 Ok((correlation + 1.0) / 2.0)
498 }
499
500 pub fn add_custom_metric(&mut self, name: String, value: f64) {
502 self.custom_metrics.insert(name, value);
503 }
504
505 pub fn get_summary(&self) -> HashMap<String, f64> {
507 let mut summary = HashMap::new();
508
509 summary.insert("MAE".to_string(), self.mae);
510 summary.insert("MSE".to_string(), self.mse);
511 summary.insert("RMSE".to_string(), self.rmse);
512 summary.insert("MAPE".to_string(), self.mape);
513 summary.insert("SMAPE".to_string(), self.smape);
514 summary.insert("DirectionalAccuracy".to_string(), self.directional_accuracy);
515 summary.insert("QuantumFidelity".to_string(), self.quantum_fidelity);
516 summary.insert("Coverage".to_string(), self.coverage);
517
518 for (name, value) in &self.custom_metrics {
520 summary.insert(name.clone(), *value);
521 }
522
523 summary
524 }
525}
526
527impl TrainingHistory {
528 pub fn new() -> Self {
530 Self {
531 losses: Vec::new(),
532 val_losses: Vec::new(),
533 metrics: Vec::new(),
534 best_params: None,
535 training_time: 0.0,
536 learning_curves: LearningCurves::new(),
537 }
538 }
539
540 pub fn add_epoch_metrics(&mut self, metrics: HashMap<String, f64>, loss: f64, val_loss: f64) {
542 self.losses.push(loss);
543 self.val_losses.push(val_loss);
544 self.metrics.push(metrics);
545
546 if let Some(accuracy) = self.metrics.last().and_then(|m| m.get("accuracy")) {
548 self.learning_curves.training_accuracy.push(*accuracy);
549 }
550
551 if let Some(val_accuracy) = self.metrics.last().and_then(|m| m.get("val_accuracy")) {
552 self.learning_curves.validation_accuracy.push(*val_accuracy);
553 }
554 }
555
556 pub fn get_best_epoch(&self) -> Option<usize> {
558 if self.val_losses.is_empty() {
559 return None;
560 }
561
562 self.val_losses
563 .iter()
564 .enumerate()
565 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
566 .map(|(idx, _)| idx)
567 }
568
569 pub fn is_converged(&self, patience: usize, min_delta: f64) -> bool {
571 if self.val_losses.len() < patience {
572 return false;
573 }
574
575 let recent_losses = &self.val_losses[self.val_losses.len() - patience..];
576 let min_recent = recent_losses.iter().fold(f64::INFINITY, |a, &b| a.min(b));
577
578 recent_losses
580 .iter()
581 .any(|&loss| min_recent - loss > min_delta)
582 }
583}
584
585impl LearningCurves {
586 pub fn new() -> Self {
588 Self {
589 training_accuracy: Vec::new(),
590 validation_accuracy: Vec::new(),
591 learning_rates: Vec::new(),
592 quantum_coherence: Vec::new(),
593 }
594 }
595
596 pub fn add_learning_rate(&mut self, lr: f64) {
598 self.learning_rates.push(lr);
599 }
600
601 pub fn add_quantum_coherence(&mut self, coherence: f64) {
603 self.quantum_coherence.push(coherence);
604 }
605}
606
607impl ModelEvaluator {
608 pub fn new() -> Self {
610 Self {
611 metrics: vec![
612 MetricType::MAE,
613 MetricType::MSE,
614 MetricType::RMSE,
615 MetricType::MAPE,
616 MetricType::DirectionalAccuracy,
617 ],
618 cv_config: CrossValidationConfig::default(),
619 statistical_tests: StatisticalTestConfig::default(),
620 }
621 }
622
623 pub fn evaluate(
625 &self,
626 predictions: &Array2<f64>,
627 actuals: &Array2<f64>,
628 ) -> Result<HashMap<String, f64>> {
629 let mut results = HashMap::new();
630
631 for metric_type in &self.metrics {
632 let value = self.calculate_metric(metric_type, predictions, actuals)?;
633 let metric_name = format!("{:?}", metric_type);
634 results.insert(metric_name, value);
635 }
636
637 Ok(results)
638 }
639
640 fn calculate_metric(
642 &self,
643 metric_type: &MetricType,
644 predictions: &Array2<f64>,
645 actuals: &Array2<f64>,
646 ) -> Result<f64> {
647 match metric_type {
648 MetricType::MAE => self.calculate_mae(predictions, actuals),
649 MetricType::MSE => self.calculate_mse(predictions, actuals),
650 MetricType::RMSE => {
651 let mse = self.calculate_mse(predictions, actuals)?;
652 Ok(mse.sqrt())
653 }
654 MetricType::MAPE => self.calculate_mape(predictions, actuals),
655 MetricType::SMAPE => self.calculate_smape(predictions, actuals),
656 MetricType::DirectionalAccuracy => {
657 self.calculate_directional_accuracy(predictions, actuals)
658 }
659 MetricType::QuantumFidelity => self.calculate_quantum_fidelity(predictions, actuals),
660 _ => Ok(0.0), }
662 }
663
664 fn calculate_mae(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
666 if predictions.shape() != actuals.shape() {
667 return Err(MLError::DimensionMismatch(
668 "Predictions and actuals must have same shape".to_string(),
669 ));
670 }
671
672 let mae = predictions
673 .iter()
674 .zip(actuals.iter())
675 .map(|(p, a)| (p - a).abs())
676 .sum::<f64>()
677 / predictions.len() as f64;
678
679 Ok(mae)
680 }
681
682 fn calculate_mse(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
684 if predictions.shape() != actuals.shape() {
685 return Err(MLError::DimensionMismatch(
686 "Predictions and actuals must have same shape".to_string(),
687 ));
688 }
689
690 let mse = predictions
691 .iter()
692 .zip(actuals.iter())
693 .map(|(p, a)| (p - a).powi(2))
694 .sum::<f64>()
695 / predictions.len() as f64;
696
697 Ok(mse)
698 }
699
700 fn calculate_mape(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
702 if predictions.shape() != actuals.shape() {
703 return Err(MLError::DimensionMismatch(
704 "Predictions and actuals must have same shape".to_string(),
705 ));
706 }
707
708 let mut mape_sum = 0.0;
709 let mut count = 0;
710
711 for (pred, actual) in predictions.iter().zip(actuals.iter()) {
712 if actual.abs() > 1e-10 {
713 mape_sum += ((pred - actual) / actual).abs() * 100.0;
714 count += 1;
715 }
716 }
717
718 Ok(if count > 0 {
719 mape_sum / count as f64
720 } else {
721 0.0
722 })
723 }
724
725 fn calculate_smape(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
727 if predictions.shape() != actuals.shape() {
728 return Err(MLError::DimensionMismatch(
729 "Predictions and actuals must have same shape".to_string(),
730 ));
731 }
732
733 let smape = predictions
734 .iter()
735 .zip(actuals.iter())
736 .map(|(pred, actual)| {
737 let numerator = (pred - actual).abs();
738 let denominator = (pred.abs() + actual.abs()) / 2.0;
739 if denominator > 1e-10 {
740 numerator / denominator * 100.0
741 } else {
742 0.0
743 }
744 })
745 .sum::<f64>()
746 / predictions.len() as f64;
747
748 Ok(smape)
749 }
750
751 fn calculate_directional_accuracy(
753 &self,
754 predictions: &Array2<f64>,
755 actuals: &Array2<f64>,
756 ) -> Result<f64> {
757 if predictions.nrows() < 2 {
758 return Ok(0.0);
759 }
760
761 let mut correct = 0;
762 let mut total = 0;
763
764 for i in 1..predictions.nrows() {
765 let pred_change = predictions[[i, 0]] - predictions[[i - 1, 0]];
766 let actual_change = actuals[[i, 0]] - actuals[[i - 1, 0]];
767
768 if pred_change * actual_change > 0.0 {
769 correct += 1;
770 }
771 total += 1;
772 }
773
774 Ok(correct as f64 / total as f64)
775 }
776
777 fn calculate_quantum_fidelity(
779 &self,
780 predictions: &Array2<f64>,
781 actuals: &Array2<f64>,
782 ) -> Result<f64> {
783 let mae = self.calculate_mae(predictions, actuals)?;
785 let max_possible_error = actuals.iter().map(|x| x.abs()).fold(0.0, f64::max);
786
787 let fidelity = if max_possible_error > 1e-10 {
788 1.0 - (mae / max_possible_error).min(1.0)
789 } else {
790 1.0
791 };
792
793 Ok(fidelity)
794 }
795}
796
797impl Default for CrossValidationConfig {
798 fn default() -> Self {
799 Self {
800 n_folds: 5,
801 strategy: ValidationStrategy::TimeSeriesSplit,
802 time_series_split: TimeSeriesSplitConfig::default(),
803 }
804 }
805}
806
807impl Default for TimeSeriesSplitConfig {
808 fn default() -> Self {
809 Self {
810 test_size: 0.2,
811 min_train_size: None,
812 gap: 0,
813 expanding_window: false,
814 }
815 }
816}
817
818impl Default for StatisticalTestConfig {
819 fn default() -> Self {
820 Self {
821 alpha: 0.05,
822 tests: vec![StatisticalTest::DieboldMariano],
823 correction: MultipleComparisonCorrection::BenjaminiHochberg,
824 }
825 }
826}
827
828pub fn calculate_coverage(
832 actuals: &Array2<f64>,
833 lower_bounds: &Array2<f64>,
834 upper_bounds: &Array2<f64>,
835) -> Result<f64> {
836 if actuals.shape() != lower_bounds.shape() || actuals.shape() != upper_bounds.shape() {
837 return Err(MLError::DimensionMismatch(
838 "All arrays must have the same shape".to_string(),
839 ));
840 }
841
842 let mut covered = 0;
843 let total = actuals.len();
844
845 for ((actual, lower), upper) in actuals
846 .iter()
847 .zip(lower_bounds.iter())
848 .zip(upper_bounds.iter())
849 {
850 if actual >= lower && actual <= upper {
851 covered += 1;
852 }
853 }
854
855 Ok(covered as f64 / total as f64)
856}
857
858pub fn calculate_msis(
860 actuals: &Array2<f64>,
861 predictions: &Array2<f64>,
862 lower_bounds: &Array2<f64>,
863 upper_bounds: &Array2<f64>,
864 alpha: f64,
865 seasonal_period: Option<usize>,
866) -> Result<f64> {
867 let coverage = calculate_coverage(actuals, lower_bounds, upper_bounds)?;
869 let interval_width: f64 = upper_bounds
870 .iter()
871 .zip(lower_bounds.iter())
872 .map(|(u, l)| u - l)
873 .sum::<f64>()
874 / upper_bounds.len() as f64;
875
876 let scaling_factor = if let Some(period) = seasonal_period {
878 period as f64
879 } else {
880 1.0
881 };
882
883 Ok(interval_width / scaling_factor * (1.0 - coverage + alpha))
884}