quantrs2_ml/time_series/
metrics.rs

1//! Performance metrics and evaluation tools for time series forecasting
2
3use super::config::AnomalyType;
4use crate::error::{MLError, Result};
5use scirs2_core::ndarray::{Array1, Array2};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Comprehensive forecast metrics
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ForecastMetrics {
12    /// Mean Absolute Error
13    pub mae: f64,
14
15    /// Mean Squared Error
16    pub mse: f64,
17
18    /// Root Mean Squared Error
19    pub rmse: f64,
20
21    /// Mean Absolute Percentage Error
22    pub mape: f64,
23
24    /// Symmetric MAPE
25    pub smape: f64,
26
27    /// Directional accuracy
28    pub directional_accuracy: f64,
29
30    /// Quantum fidelity of predictions
31    pub quantum_fidelity: f64,
32
33    /// Coverage of prediction intervals
34    pub coverage: f64,
35
36    /// Additional custom metrics
37    pub custom_metrics: HashMap<String, f64>,
38}
39
40/// Forecast result with predictions and metadata
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ForecastResult {
43    /// Point predictions
44    pub predictions: Array2<f64>,
45
46    /// Lower prediction interval
47    pub lower_bound: Array2<f64>,
48
49    /// Upper prediction interval
50    pub upper_bound: Array2<f64>,
51
52    /// Detected anomalies
53    pub anomalies: Vec<AnomalyPoint>,
54
55    /// Confidence scores for each prediction
56    pub confidence_scores: Array1<f64>,
57
58    /// Quantum uncertainty measure
59    pub quantum_uncertainty: f64,
60}
61
62/// Anomaly point in forecasts
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct AnomalyPoint {
65    /// Time index
66    pub timestamp: usize,
67
68    /// Anomalous value
69    pub value: f64,
70
71    /// Anomaly score
72    pub anomaly_score: f64,
73
74    /// Type of anomaly
75    pub anomaly_type: AnomalyType,
76}
77
78/// Training history tracking
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct TrainingHistory {
81    /// Loss values per epoch
82    pub losses: Vec<f64>,
83
84    /// Validation losses per epoch
85    pub val_losses: Vec<f64>,
86
87    /// Metrics per epoch
88    pub metrics: Vec<HashMap<String, f64>>,
89
90    /// Best model parameters
91    pub best_params: Option<Array1<f64>>,
92
93    /// Total training time in seconds
94    pub training_time: f64,
95
96    /// Learning curves
97    pub learning_curves: LearningCurves,
98}
99
100/// Learning curves for training analysis
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct LearningCurves {
103    /// Training accuracy over epochs
104    pub training_accuracy: Vec<f64>,
105
106    /// Validation accuracy over epochs
107    pub validation_accuracy: Vec<f64>,
108
109    /// Learning rate schedule
110    pub learning_rates: Vec<f64>,
111
112    /// Quantum coherence measures
113    pub quantum_coherence: Vec<f64>,
114}
115
116/// Model evaluation suite
117#[derive(Debug, Clone)]
118pub struct ModelEvaluator {
119    /// Evaluation metrics to compute
120    metrics: Vec<MetricType>,
121
122    /// Cross-validation configuration
123    cv_config: CrossValidationConfig,
124
125    /// Statistical test configuration
126    statistical_tests: StatisticalTestConfig,
127}
128
129/// Types of evaluation metrics
130#[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, // Mean Scaled Interval Score
141    CRPS, // Continuous Ranked Probability Score
142    Custom(String),
143}
144
145/// Cross-validation configuration
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CrossValidationConfig {
148    /// Number of folds
149    pub n_folds: usize,
150
151    /// Validation strategy
152    pub strategy: ValidationStrategy,
153
154    /// Time series specific settings
155    pub time_series_split: TimeSeriesSplitConfig,
156}
157
158/// Validation strategies
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum ValidationStrategy {
161    KFold,
162    TimeSeriesSplit,
163    WalkForward,
164    BlockingTimeSeriesSplit,
165    QuantumBootstrap,
166}
167
168/// Time series splitting configuration
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TimeSeriesSplitConfig {
171    /// Test size as fraction of total data
172    pub test_size: f64,
173
174    /// Minimum training size
175    pub min_train_size: Option<usize>,
176
177    /// Gap between train and test
178    pub gap: usize,
179
180    /// Enable expanding window
181    pub expanding_window: bool,
182}
183
184/// Statistical test configuration
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct StatisticalTestConfig {
187    /// Significance level
188    pub alpha: f64,
189
190    /// Tests to perform
191    pub tests: Vec<StatisticalTest>,
192
193    /// Multiple comparison correction
194    pub correction: MultipleComparisonCorrection,
195}
196
197/// Statistical tests for model comparison
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub enum StatisticalTest {
200    DieboldMariano,
201    WilcoxonSignedRank,
202    PairedTTest,
203    McNemar,
204    QuantumSignificanceTest,
205}
206
207/// Multiple comparison corrections
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum MultipleComparisonCorrection {
210    None,
211    Bonferroni,
212    BenjaminiHochberg,
213    Holm,
214    QuantumCorrection,
215}
216
217/// Benchmark suite for model comparison
218#[derive(Debug, Clone)]
219pub struct BenchmarkSuite {
220    /// Benchmark datasets
221    datasets: Vec<BenchmarkDataset>,
222
223    /// Models to compare
224    models: Vec<String>,
225
226    /// Evaluation metrics
227    metrics: Vec<MetricType>,
228
229    /// Results storage
230    results: BenchmarkResults,
231}
232
233/// Benchmark dataset metadata
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct BenchmarkDataset {
236    /// Dataset name
237    pub name: String,
238
239    /// Dataset description
240    pub description: String,
241
242    /// Data characteristics
243    pub characteristics: DataCharacteristics,
244
245    /// Source reference
246    pub source: String,
247}
248
249/// Data characteristics for benchmarking
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct DataCharacteristics {
252    /// Number of time series
253    pub n_series: usize,
254
255    /// Length of time series
256    pub series_length: usize,
257
258    /// Frequency of observations
259    pub frequency: String,
260
261    /// Seasonality periods
262    pub seasonality: Vec<usize>,
263
264    /// Trend characteristics
265    pub trend_type: TrendType,
266
267    /// Noise level
268    pub noise_level: f64,
269}
270
271/// Types of trends in data
272#[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/// Benchmark results storage
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct BenchmarkResults {
286    /// Results by model and dataset
287    pub results: HashMap<String, HashMap<String, ModelPerformance>>,
288
289    /// Statistical comparisons
290    pub statistical_comparisons: Vec<StatisticalComparison>,
291
292    /// Rankings
293    pub rankings: HashMap<String, Vec<String>>,
294
295    /// Summary statistics
296    pub summary: BenchmarkSummary,
297}
298
299/// Model performance on a dataset
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ModelPerformance {
302    /// Metric values
303    pub metrics: HashMap<String, f64>,
304
305    /// Confidence intervals
306    pub confidence_intervals: HashMap<String, (f64, f64)>,
307
308    /// Execution time
309    pub execution_time: f64,
310
311    /// Memory usage
312    pub memory_usage: f64,
313
314    /// Quantum enhancement factor
315    pub quantum_enhancement: f64,
316}
317
318/// Statistical comparison between models
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct StatisticalComparison {
321    /// Model names being compared
322    pub models: (String, String),
323
324    /// Test type
325    pub test_type: StatisticalTest,
326
327    /// Test statistic
328    pub statistic: f64,
329
330    /// p-value
331    pub p_value: f64,
332
333    /// Effect size
334    pub effect_size: f64,
335
336    /// Significant difference
337    pub is_significant: bool,
338}
339
340/// Benchmark summary statistics
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct BenchmarkSummary {
343    /// Best performing model overall
344    pub best_model: String,
345
346    /// Average performance by model
347    pub average_performance: HashMap<String, f64>,
348
349    /// Win rates by model
350    pub win_rates: HashMap<String, f64>,
351
352    /// Quantum advantage metrics
353    pub quantum_advantage: HashMap<String, f64>,
354}
355
356impl ForecastMetrics {
357    /// Create new forecast metrics
358    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    /// Calculate all metrics from predictions and actuals
373    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        // Reset metrics
387        self.mae = 0.0;
388        self.mse = 0.0;
389        self.mape = 0.0;
390        self.smape = 0.0;
391
392        // Calculate basic metrics
393        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            // MAPE calculation (avoid division by zero)
401            if actual.abs() > 1e-10 {
402                self.mape += (abs_error / actual.abs()) * 100.0;
403            }
404
405            // SMAPE calculation
406            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        // Normalize by number of observations
413        self.mae /= n;
414        self.mse /= n;
415        self.mape /= n;
416        self.smape /= n;
417
418        // Calculate RMSE
419        self.rmse = self.mse.sqrt();
420
421        // Calculate directional accuracy
422        self.directional_accuracy = self.calculate_directional_accuracy(predictions, actuals)?;
423
424        // Calculate quantum fidelity (simplified)
425        self.quantum_fidelity = self.calculate_quantum_fidelity(predictions, actuals)?;
426
427        // Coverage would be calculated if prediction intervals are available
428        self.coverage = 0.95; // Placeholder
429
430        Ok(())
431    }
432
433    /// Calculate directional accuracy
434    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    /// Calculate quantum fidelity measure
464    fn calculate_quantum_fidelity(
465        &self,
466        predictions: &Array2<f64>,
467        actuals: &Array2<f64>,
468    ) -> Result<f64> {
469        // Simplified quantum fidelity based on normalized correlation
470        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        // Convert correlation to fidelity-like measure
497        Ok((correlation + 1.0) / 2.0)
498    }
499
500    /// Add custom metric
501    pub fn add_custom_metric(&mut self, name: String, value: f64) {
502        self.custom_metrics.insert(name, value);
503    }
504
505    /// Get summary of all metrics
506    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        // Add custom metrics
519        for (name, value) in &self.custom_metrics {
520            summary.insert(name.clone(), *value);
521        }
522
523        summary
524    }
525}
526
527impl TrainingHistory {
528    /// Create new training history
529    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    /// Add metrics for an epoch
541    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        // Update learning curves
547        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    /// Get best epoch based on validation loss
557    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    /// Check if training is converged
570    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        // Check if there's been improvement recently
579        recent_losses
580            .iter()
581            .any(|&loss| min_recent - loss > min_delta)
582    }
583}
584
585impl LearningCurves {
586    /// Create new learning curves
587    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    /// Add learning rate
597    pub fn add_learning_rate(&mut self, lr: f64) {
598        self.learning_rates.push(lr);
599    }
600
601    /// Add quantum coherence measure
602    pub fn add_quantum_coherence(&mut self, coherence: f64) {
603        self.quantum_coherence.push(coherence);
604    }
605}
606
607impl ModelEvaluator {
608    /// Create new model evaluator
609    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    /// Evaluate model performance
624    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    /// Calculate specific metric
641    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), // Placeholder for other metrics
661        }
662    }
663
664    /// Calculate Mean Absolute Error
665    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    /// Calculate Mean Squared Error
683    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    /// Calculate Mean Absolute Percentage Error
701    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    /// Calculate Symmetric Mean Absolute Percentage Error
726    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    /// Calculate directional accuracy
752    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    /// Calculate quantum fidelity
778    fn calculate_quantum_fidelity(
779        &self,
780        predictions: &Array2<f64>,
781        actuals: &Array2<f64>,
782    ) -> Result<f64> {
783        // Simplified quantum fidelity calculation
784        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
828/// Utility functions for metrics calculation
829
830/// Calculate prediction interval coverage
831pub 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
858/// Calculate Mean Scaled Interval Score (MSIS)
859pub 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    // Simplified MSIS calculation
868    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    // Simplified scaling factor
877    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}