1use anyhow::{anyhow, Result};
31use scirs2_core::ndarray_ext::{s, Array1, Array2, ArrayView1};
32use scirs2_core::random::Random;
33use serde::{Deserialize, Serialize};
34use std::collections::VecDeque;
35use std::sync::Arc;
36use tokio::sync::{Mutex, RwLock};
37use tracing::info;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum ForecastAlgorithm {
42 Arima,
44 ExponentialSmoothing,
46 SeasonalDecomposition,
48 Lstm,
50 MovingAverage,
52 WeightedMovingAverage,
54 ExponentialMovingAverage,
56 HoltWinters,
58 AutoRegressive,
60 MovingAverageModel,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum TrendDirection {
67 Increasing,
69 Decreasing,
71 Stable,
73 Oscillating,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum SeasonalityType {
80 Additive,
82 Multiplicative,
84 None,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ForecastingConfig {
91 pub algorithm: ForecastAlgorithm,
93 pub horizon: usize,
95 pub confidence_level: f64,
97 pub window_size: usize,
99 pub min_data_points: usize,
101 pub auto_retrain: bool,
103 pub retrain_threshold: f64,
105 pub seasonal_period: Option<usize>,
107 pub enable_trend_detection: bool,
109 pub enable_seasonality_detection: bool,
111 pub max_ar_terms: usize,
113 pub max_ma_terms: usize,
115 pub differencing_order: usize,
117}
118
119impl Default for ForecastingConfig {
120 fn default() -> Self {
121 Self {
122 algorithm: ForecastAlgorithm::AutoRegressive,
123 horizon: 10,
124 confidence_level: 0.95,
125 window_size: 100,
126 min_data_points: 20,
127 auto_retrain: true,
128 retrain_threshold: 0.15, seasonal_period: None,
130 enable_trend_detection: true,
131 enable_seasonality_detection: true,
132 max_ar_terms: 5,
133 max_ma_terms: 5,
134 differencing_order: 1,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ForecastResult {
142 pub predictions: Vec<f64>,
144 pub lower_bound: Vec<f64>,
146 pub upper_bound: Vec<f64>,
148 pub steps_ahead: Vec<usize>,
150 pub confidence_level: f64,
152 pub trend: TrendDirection,
154 pub seasonality: SeasonalityType,
156 pub accuracy_metrics: AccuracyMetrics,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AccuracyMetrics {
163 pub mae: f64,
165 pub mse: f64,
167 pub rmse: f64,
169 pub mape: f64,
171 pub r_squared: f64,
173 pub bias: f64,
175}
176
177impl Default for AccuracyMetrics {
178 fn default() -> Self {
179 Self {
180 mae: 0.0,
181 mse: 0.0,
182 rmse: 0.0,
183 mape: 0.0,
184 r_squared: 0.0,
185 bias: 0.0,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct PredictiveStats {
193 pub total_forecasts: u64,
195 pub retraining_count: u64,
197 pub avg_accuracy: f64,
199 pub model_params_count: usize,
201 pub training_data_size: usize,
203 pub last_forecast_time: Option<chrono::DateTime<chrono::Utc>>,
205}
206
207impl Default for PredictiveStats {
208 fn default() -> Self {
209 Self {
210 total_forecasts: 0,
211 retraining_count: 0,
212 avg_accuracy: 0.0,
213 model_params_count: 0,
214 training_data_size: 0,
215 last_forecast_time: None,
216 }
217 }
218}
219
220pub struct PredictiveAnalytics {
222 config: ForecastingConfig,
223 data: Arc<RwLock<VecDeque<f64>>>,
225 model_params: Arc<RwLock<ModelParameters>>,
227 stats: Arc<RwLock<PredictiveStats>>,
229 #[allow(clippy::arc_with_non_send_sync)]
231 rng: Arc<Mutex<Random>>,
232}
233
234#[derive(Debug, Clone)]
236struct ModelParameters {
237 ar_coeffs: Vec<f64>,
239 ma_coeffs: Vec<f64>,
241 trend_coeffs: Vec<f64>,
243 seasonal_components: Vec<f64>,
245 intercept: f64,
247 residual_variance: f64,
249 fitted_values: Vec<f64>,
251}
252
253impl Default for ModelParameters {
254 fn default() -> Self {
255 Self {
256 ar_coeffs: Vec::new(),
257 ma_coeffs: Vec::new(),
258 trend_coeffs: Vec::new(),
259 seasonal_components: Vec::new(),
260 intercept: 0.0,
261 residual_variance: 1.0,
262 fitted_values: Vec::new(),
263 }
264 }
265}
266
267impl PredictiveAnalytics {
268 #[allow(clippy::arc_with_non_send_sync)]
270 pub fn new(config: ForecastingConfig) -> Result<Self> {
271 Ok(Self {
272 config,
273 data: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
274 model_params: Arc::new(RwLock::new(ModelParameters::default())),
275 stats: Arc::new(RwLock::new(PredictiveStats::default())),
276 rng: Arc::new(Mutex::new(Random::default())),
277 })
278 }
279
280 pub async fn add_data_point(&mut self, value: f64) -> Result<()> {
282 let mut data = self.data.write().await;
283
284 data.push_back(value);
286
287 if data.len() > self.config.window_size {
289 data.pop_front();
290 }
291
292 if self.config.auto_retrain && data.len() >= self.config.min_data_points {
294 drop(data); self.train_internal().await?;
296 }
297
298 Ok(())
299 }
300
301 pub async fn train(&mut self, data: &[f64]) -> Result<()> {
303 let mut data_buffer = self.data.write().await;
304 data_buffer.clear();
305 data_buffer.extend(data.iter().copied());
306
307 while data_buffer.len() > self.config.window_size {
309 data_buffer.pop_front();
310 }
311
312 drop(data_buffer);
313 self.train_internal().await
314 }
315
316 async fn train_internal(&mut self) -> Result<()> {
318 let data = self.data.read().await;
319
320 if data.len() < self.config.min_data_points {
321 return Err(anyhow!(
322 "Insufficient data points: {} < {}",
323 data.len(),
324 self.config.min_data_points
325 ));
326 }
327
328 let data_vec: Vec<f64> = data.iter().copied().collect();
329 let data_array = Array1::from_vec(data_vec);
330
331 drop(data);
332
333 match self.config.algorithm {
335 ForecastAlgorithm::AutoRegressive => {
336 self.train_ar_model(&data_array).await?;
337 }
338 ForecastAlgorithm::Arima => {
339 self.train_arima_model(&data_array).await?;
340 }
341 ForecastAlgorithm::ExponentialSmoothing => {
342 self.train_exponential_smoothing(&data_array).await?;
343 }
344 ForecastAlgorithm::HoltWinters => {
345 self.train_holt_winters(&data_array).await?;
346 }
347 ForecastAlgorithm::MovingAverage => {
348 self.train_moving_average(&data_array).await?;
349 }
350 _ => {
351 self.train_ar_model(&data_array).await?;
353 }
354 }
355
356 let mut stats = self.stats.write().await;
358 stats.retraining_count += 1;
359 stats.training_data_size = data_array.len();
360
361 info!(
362 "Model trained successfully with {} data points using {:?}",
363 data_array.len(),
364 self.config.algorithm
365 );
366
367 Ok(())
368 }
369
370 async fn train_ar_model(&mut self, data: &Array1<f64>) -> Result<()> {
372 let p = self.config.max_ar_terms.min(data.len() / 3);
373
374 if p == 0 {
375 return Err(anyhow!("Insufficient data for AR model"));
376 }
377
378 let ar_coeffs = self.estimate_ar_coefficients(data, p)?;
380
381 let (fitted, residual_var) = self.compute_fitted_values(data, &ar_coeffs)?;
383
384 let mut params = self.model_params.write().await;
386 params.ar_coeffs = ar_coeffs;
387 params.residual_variance = residual_var;
388 params.fitted_values = fitted;
389 params.intercept = data.mean();
390
391 Ok(())
392 }
393
394 fn estimate_ar_coefficients(&self, data: &Array1<f64>, p: usize) -> Result<Vec<f64>> {
396 let n = data.len();
397
398 let mean_val = data.mean();
400 let centered: Vec<f64> = data.iter().map(|&x| x - mean_val).collect();
401
402 let mut r = vec![0.0; p + 1];
404 for k in 0..=p {
405 let mut sum = 0.0;
406 for i in 0..(n - k) {
407 sum += centered[i] * centered[i + k];
408 }
409 r[k] = sum / n as f64;
410 }
411
412 let mut matrix = Array2::<f64>::zeros((p, p));
415 let mut rhs = Array1::<f64>::zeros(p);
416
417 for i in 0..p {
418 for j in 0..p {
419 matrix[[i, j]] = r[i.abs_diff(j)];
420 }
421 rhs[i] = r[i + 1];
422 }
423
424 let coeffs = self.solve_linear_system(&matrix, &rhs)?;
426
427 Ok(coeffs)
428 }
429
430 fn solve_linear_system(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Vec<f64>> {
432 let at = a.t();
434 let ata = at.dot(a);
435 let atb = at.dot(b);
436
437 let n = ata.shape()[0];
440 let mut aug = Array2::<f64>::zeros((n, n + 1));
441
442 for i in 0..n {
443 for j in 0..n {
444 aug[[i, j]] = ata[[i, j]];
445 }
446 aug[[i, n]] = atb[i];
447 }
448
449 for i in 0..n {
451 let pivot = aug[[i, i]];
453 if pivot.abs() < 1e-10 {
454 continue;
455 }
456
457 for j in (i + 1)..n {
458 let factor = aug[[j, i]] / pivot;
459 for k in i..=n {
460 aug[[j, k]] -= factor * aug[[i, k]];
461 }
462 }
463 }
464
465 let mut x = vec![0.0; n];
467 for i in (0..n).rev() {
468 let mut sum = aug[[i, n]];
469 for j in (i + 1)..n {
470 sum -= aug[[i, j]] * x[j];
471 }
472 x[i] = sum / aug[[i, i]].max(1e-10);
473 }
474
475 Ok(x)
476 }
477
478 fn compute_fitted_values(&self, data: &Array1<f64>, coeffs: &[f64]) -> Result<(Vec<f64>, f64)> {
480 let n = data.len();
481 let p = coeffs.len();
482 let mean_val: f64 = data.mean();
483
484 let mut fitted: Vec<f64> = vec![mean_val; n];
485 let mut residuals: Vec<f64> = Vec::with_capacity(n);
486
487 for t in p..n {
488 let mut pred: f64 = mean_val;
489 for (j, &coeff) in coeffs.iter().enumerate() {
490 let val: f64 = data[t - j - 1];
491 pred += coeff * (val - mean_val);
492 }
493 fitted[t] = pred;
494 let actual: f64 = data[t];
495 residuals.push(actual - pred);
496 }
497
498 let residual_var = if residuals.is_empty() {
500 1.0
501 } else {
502 residuals.iter().map(|&r| r * r).sum::<f64>() / residuals.len() as f64
503 };
504
505 Ok((fitted, residual_var))
506 }
507
508 async fn train_arima_model(&mut self, data: &Array1<f64>) -> Result<()> {
510 let differenced = if self.config.differencing_order > 0 {
512 self.difference_series(data, self.config.differencing_order)?
513 } else {
514 data.to_vec()
515 };
516
517 let diff_array = Array1::from_vec(differenced);
518
519 self.train_ar_model(&diff_array).await?;
521
522 Ok(())
523 }
524
525 async fn train_exponential_smoothing(&mut self, data: &Array1<f64>) -> Result<()> {
527 let alpha = 0.3; let mut smoothed = Vec::with_capacity(data.len());
529
530 smoothed.push(data[0]);
531 for i in 1..data.len() {
532 let s = alpha * data[i] + (1.0 - alpha) * smoothed[i - 1];
533 smoothed.push(s);
534 }
535
536 let residuals: Vec<f64> = data.iter().zip(&smoothed).map(|(x, s)| x - s).collect();
537 let residual_var = residuals.iter().map(|&r| r * r).sum::<f64>() / residuals.len() as f64;
538
539 let mut params = self.model_params.write().await;
540 params.ar_coeffs = vec![alpha];
541 params.fitted_values = smoothed;
542 params.residual_variance = residual_var;
543 params.intercept = data[0];
544
545 Ok(())
546 }
547
548 async fn train_holt_winters(&mut self, data: &Array1<f64>) -> Result<()> {
550 let alpha = 0.3; let beta = 0.1; let gamma = 0.2; let seasonal_period = self.config.seasonal_period.unwrap_or(12);
555
556 if data.len() < 2 * seasonal_period {
557 return Err(anyhow!("Insufficient data for Holt-Winters"));
558 }
559
560 let mut level = data[0];
562 let mut trend = (data[seasonal_period] - data[0]) / seasonal_period as f64;
563 let mut seasonal = vec![1.0; seasonal_period];
564
565 for i in 0..seasonal_period {
567 seasonal[i] =
568 data[i] / (data.iter().take(seasonal_period).sum::<f64>() / seasonal_period as f64);
569 }
570
571 let mut fitted = Vec::with_capacity(data.len());
572
573 for (t, &value) in data.iter().enumerate() {
574 let season_idx = t % seasonal_period;
575 let forecast = (level + trend) * seasonal[season_idx];
576 fitted.push(forecast);
577
578 let old_level = level;
580 level = alpha * (value / seasonal[season_idx]) + (1.0 - alpha) * (level + trend);
581 trend = beta * (level - old_level) + (1.0 - beta) * trend;
582 seasonal[season_idx] = gamma * (value / level) + (1.0 - gamma) * seasonal[season_idx];
583 }
584
585 let residuals: Vec<f64> = data.iter().zip(&fitted).map(|(x, f)| x - f).collect();
586 let residual_var = residuals.iter().map(|&r| r * r).sum::<f64>() / residuals.len() as f64;
587
588 let mut params = self.model_params.write().await;
589 params.trend_coeffs = vec![level, trend];
590 params.seasonal_components = seasonal;
591 params.fitted_values = fitted;
592 params.residual_variance = residual_var;
593
594 Ok(())
595 }
596
597 async fn train_moving_average(&mut self, data: &Array1<f64>) -> Result<()> {
599 let window = self.config.max_ma_terms.min(data.len() / 2);
600
601 let mut fitted = Vec::with_capacity(data.len());
602 for i in 0..data.len() {
603 let start = i.saturating_sub(window);
604 let avg = data.slice(s![start..=i]).mean();
605 fitted.push(avg);
606 }
607
608 let residuals: Vec<f64> = data.iter().zip(&fitted).map(|(x, f)| x - f).collect();
609 let residual_var = residuals.iter().map(|&r| r * r).sum::<f64>() / residuals.len() as f64;
610
611 let mut params = self.model_params.write().await;
612 params.ma_coeffs = vec![1.0 / window as f64; window];
613 params.fitted_values = fitted;
614 params.residual_variance = residual_var;
615
616 Ok(())
617 }
618
619 fn difference_series(&self, data: &Array1<f64>, order: usize) -> Result<Vec<f64>> {
621 let mut result = data.to_vec();
622
623 for _ in 0..order {
624 result = result.windows(2).map(|w| w[1] - w[0]).collect();
625 }
626
627 Ok(result)
628 }
629
630 pub async fn forecast(&mut self, steps: usize) -> Result<ForecastResult> {
632 let params = self.model_params.read().await;
633 let data = self.data.read().await;
634
635 if data.len() < self.config.min_data_points {
636 return Err(anyhow!("Insufficient data for forecasting"));
637 }
638
639 let data_vec: Vec<f64> = data.iter().copied().collect();
640 drop(data);
641
642 let predictions = match self.config.algorithm {
643 ForecastAlgorithm::AutoRegressive => self.forecast_ar(&data_vec, ¶ms, steps)?,
644 ForecastAlgorithm::ExponentialSmoothing => {
645 self.forecast_exponential_smoothing(&data_vec, ¶ms, steps)?
646 }
647 ForecastAlgorithm::HoltWinters => self.forecast_holt_winters(¶ms, steps)?,
648 _ => self.forecast_ar(&data_vec, ¶ms, steps)?,
649 };
650
651 let std_dev = params.residual_variance.sqrt();
653 let z_score = 1.96; let lower_bound: Vec<f64> = predictions
656 .iter()
657 .enumerate()
658 .map(|(i, &p)| p - z_score * std_dev * ((i + 1) as f64).sqrt())
659 .collect();
660
661 let upper_bound: Vec<f64> = predictions
662 .iter()
663 .enumerate()
664 .map(|(i, &p)| p + z_score * std_dev * ((i + 1) as f64).sqrt())
665 .collect();
666
667 let trend = self.detect_trend(&predictions);
669
670 let seasonality = if self.config.enable_seasonality_detection {
672 self.detect_seasonality(&data_vec)?
673 } else {
674 SeasonalityType::None
675 };
676
677 let accuracy_metrics = self.compute_accuracy_metrics(&data_vec, ¶ms.fitted_values)?;
679
680 let mut stats = self.stats.write().await;
682 stats.total_forecasts += 1;
683 stats.last_forecast_time = Some(chrono::Utc::now());
684 stats.avg_accuracy = (stats.avg_accuracy * (stats.total_forecasts - 1) as f64
685 + accuracy_metrics.r_squared)
686 / stats.total_forecasts as f64;
687 stats.model_params_count = params.ar_coeffs.len() + params.ma_coeffs.len();
688
689 Ok(ForecastResult {
690 predictions,
691 lower_bound,
692 upper_bound,
693 steps_ahead: (1..=steps).collect(),
694 confidence_level: self.config.confidence_level,
695 trend,
696 seasonality,
697 accuracy_metrics,
698 })
699 }
700
701 fn forecast_ar(
703 &self,
704 data: &[f64],
705 params: &ModelParameters,
706 steps: usize,
707 ) -> Result<Vec<f64>> {
708 let mut predictions = Vec::with_capacity(steps);
709 let mut history = data.to_vec();
710 let mean = params.intercept;
711
712 for _ in 0..steps {
713 let mut pred = mean;
714 for (j, &coeff) in params.ar_coeffs.iter().enumerate() {
715 if j < history.len() {
716 pred += coeff * (history[history.len() - j - 1] - mean);
717 }
718 }
719 predictions.push(pred);
720 history.push(pred);
721 }
722
723 Ok(predictions)
724 }
725
726 fn forecast_exponential_smoothing(
728 &self,
729 data: &[f64],
730 params: &ModelParameters,
731 steps: usize,
732 ) -> Result<Vec<f64>> {
733 let last_smooth = params
734 .fitted_values
735 .last()
736 .copied()
737 .unwrap_or(data.last().copied().unwrap_or(0.0));
738 Ok(vec![last_smooth; steps])
739 }
740
741 fn forecast_holt_winters(&self, params: &ModelParameters, steps: usize) -> Result<Vec<f64>> {
743 if params.trend_coeffs.len() < 2 {
744 return Err(anyhow!("Holt-Winters model not trained"));
745 }
746
747 let level = params.trend_coeffs[0];
748 let trend = params.trend_coeffs[1];
749 let seasonal_period = params.seasonal_components.len();
750
751 let predictions: Vec<f64> = (0..steps)
752 .map(|h| {
753 let season_idx = h % seasonal_period;
754 (level + (h + 1) as f64 * trend) * params.seasonal_components[season_idx]
755 })
756 .collect();
757
758 Ok(predictions)
759 }
760
761 fn detect_trend(&self, values: &[f64]) -> TrendDirection {
763 if values.len() < 2 {
764 return TrendDirection::Stable;
765 }
766
767 let increases = values.windows(2).filter(|w| w[1] > w[0]).count();
768 let decreases = values.windows(2).filter(|w| w[1] < w[0]).count();
769
770 if increases == 0 && decreases == 0 {
772 return TrendDirection::Stable;
773 }
774
775 let ratio = increases as f64 / (increases + decreases) as f64;
776
777 if ratio > 0.7 {
778 TrendDirection::Increasing
779 } else if ratio < 0.3 {
780 TrendDirection::Decreasing
781 } else if (ratio - 0.5).abs() < 0.1 {
782 TrendDirection::Oscillating
783 } else {
784 TrendDirection::Stable
785 }
786 }
787
788 fn detect_seasonality(&self, data: &[f64]) -> Result<SeasonalityType> {
790 if let Some(period) = self.config.seasonal_period {
791 if data.len() < 2 * period {
792 return Ok(SeasonalityType::None);
793 }
794
795 let seasonal_diff: Vec<f64> = data
797 .iter()
798 .skip(period)
799 .zip(data.iter())
800 .map(|(x2, x1)| x2 - x1)
801 .collect();
802
803 let regular_diff: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
804
805 let seasonal_var =
806 seasonal_diff.iter().map(|&x| x * x).sum::<f64>() / seasonal_diff.len() as f64;
807 let regular_var =
808 regular_diff.iter().map(|&x| x * x).sum::<f64>() / regular_diff.len() as f64;
809
810 if seasonal_var < 0.5 * regular_var {
811 Ok(SeasonalityType::Additive)
812 } else {
813 Ok(SeasonalityType::None)
814 }
815 } else {
816 Ok(SeasonalityType::None)
817 }
818 }
819
820 fn compute_accuracy_metrics(&self, actual: &[f64], fitted: &[f64]) -> Result<AccuracyMetrics> {
822 let n = actual.len().min(fitted.len());
823
824 if n == 0 {
825 return Ok(AccuracyMetrics::default());
826 }
827
828 let errors: Vec<f64> = actual
829 .iter()
830 .zip(fitted.iter())
831 .map(|(a, f)| a - f)
832 .collect();
833
834 let abs_errors: Vec<f64> = errors.iter().map(|e| e.abs()).collect();
835 let squared_errors: Vec<f64> = errors.iter().map(|e| e * e).collect();
836
837 let mae = abs_errors.iter().sum::<f64>() / n as f64;
838 let mse = squared_errors.iter().sum::<f64>() / n as f64;
839 let rmse = mse.sqrt();
840
841 let mape = abs_errors
842 .iter()
843 .zip(actual.iter())
844 .map(|(ae, a)| if a.abs() > 1e-10 { ae / a.abs() } else { 0.0 })
845 .sum::<f64>()
846 / n as f64
847 * 100.0;
848
849 let actual_mean = actual.iter().sum::<f64>() / n as f64;
850 let ss_tot: f64 = actual.iter().map(|a| (a - actual_mean).powi(2)).sum();
851 let ss_res: f64 = squared_errors.iter().sum();
852
853 let r_squared = if ss_tot > 1e-10 {
854 1.0 - (ss_res / ss_tot)
855 } else {
856 0.0
857 };
858
859 let bias = errors.iter().sum::<f64>() / n as f64;
860
861 Ok(AccuracyMetrics {
862 mae,
863 mse,
864 rmse,
865 mape,
866 r_squared,
867 bias,
868 })
869 }
870
871 pub async fn get_stats(&self) -> PredictiveStats {
873 self.stats.read().await.clone()
874 }
875
876 pub async fn reset(&mut self) -> Result<()> {
878 self.data.write().await.clear();
879 *self.model_params.write().await = ModelParameters::default();
880 *self.stats.write().await = PredictiveStats::default();
881 Ok(())
882 }
883}
884
885trait ArrayMean {
887 fn mean(&self) -> f64;
888}
889
890impl ArrayMean for Array1<f64> {
891 fn mean(&self) -> f64 {
892 if self.is_empty() {
893 0.0
894 } else {
895 self.sum() / self.len() as f64
896 }
897 }
898}
899
900impl ArrayMean for ArrayView1<'_, f64> {
901 fn mean(&self) -> f64 {
902 if self.is_empty() {
903 0.0
904 } else {
905 self.sum() / self.len() as f64
906 }
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 #[tokio::test]
915 async fn test_predictive_analytics_creation() {
916 let config = ForecastingConfig::default();
917 let analytics = PredictiveAnalytics::new(config);
918 assert!(analytics.is_ok());
919 }
920
921 #[tokio::test]
922 async fn test_ar_model_training() {
923 let config = ForecastingConfig {
924 algorithm: ForecastAlgorithm::AutoRegressive,
925 min_data_points: 10,
926 max_ar_terms: 3,
927 ..Default::default()
928 };
929
930 let mut analytics = PredictiveAnalytics::new(config).unwrap();
931
932 let data: Vec<f64> = (0..50).map(|i| i as f64 + (i as f64 * 0.1).sin()).collect();
934
935 let result = analytics.train(&data).await;
936 assert!(result.is_ok());
937
938 let stats = analytics.get_stats().await;
939 assert_eq!(stats.retraining_count, 1);
940 assert_eq!(stats.training_data_size, 50);
941 }
942
943 #[tokio::test]
944 async fn test_forecasting() {
945 let config = ForecastingConfig {
946 algorithm: ForecastAlgorithm::AutoRegressive,
947 horizon: 10,
948 min_data_points: 20,
949 ..Default::default()
950 };
951
952 let mut analytics = PredictiveAnalytics::new(config).unwrap();
953
954 let data: Vec<f64> = (0..30).map(|i| i as f64 * 2.0).collect();
956 analytics.train(&data).await.unwrap();
957
958 let forecast = analytics.forecast(10).await;
959 assert!(forecast.is_ok());
960
961 let result = forecast.unwrap();
962 assert_eq!(result.predictions.len(), 10);
963 assert_eq!(result.lower_bound.len(), 10);
964 assert_eq!(result.upper_bound.len(), 10);
965
966 let last_data = data.last().copied().unwrap_or(0.0);
967 let first_pred = result.predictions[0];
968
969 assert!(
971 (first_pred - last_data).abs() < 10.0,
972 "Prediction {} is too far from last data point {}",
973 first_pred,
974 last_data
975 );
976 }
977
978 #[tokio::test]
979 async fn test_exponential_smoothing() {
980 let config = ForecastingConfig {
981 algorithm: ForecastAlgorithm::ExponentialSmoothing,
982 min_data_points: 10,
983 ..Default::default()
984 };
985
986 let mut analytics = PredictiveAnalytics::new(config).unwrap();
987
988 let data: Vec<f64> = vec![10.0, 12.0, 11.0, 13.0, 14.0, 13.5, 15.0, 16.0, 15.5, 17.0];
989 analytics.train(&data).await.unwrap();
990
991 let forecast = analytics.forecast(5).await;
992 assert!(forecast.is_ok());
993 }
994
995 #[tokio::test]
996 async fn test_trend_detection() {
997 let config = ForecastingConfig::default();
998 let analytics = PredictiveAnalytics::new(config).unwrap();
999
1000 let increasing = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1001 assert_eq!(
1002 analytics.detect_trend(&increasing),
1003 TrendDirection::Increasing
1004 );
1005
1006 let decreasing = vec![5.0, 4.0, 3.0, 2.0, 1.0];
1007 assert_eq!(
1008 analytics.detect_trend(&decreasing),
1009 TrendDirection::Decreasing
1010 );
1011
1012 let stable = vec![5.0, 5.0, 5.0, 5.0, 5.0];
1013 assert_eq!(analytics.detect_trend(&stable), TrendDirection::Stable);
1014 }
1015
1016 #[tokio::test]
1017 async fn test_add_data_point() {
1018 let config = ForecastingConfig {
1019 min_data_points: 5,
1020 auto_retrain: false,
1021 ..Default::default()
1022 };
1023
1024 let mut analytics = PredictiveAnalytics::new(config).unwrap();
1025
1026 for i in 0..10 {
1027 let result = analytics.add_data_point(i as f64).await;
1028 assert!(result.is_ok());
1029 }
1030
1031 let data = analytics.data.read().await;
1032 assert_eq!(data.len(), 10);
1033 }
1034
1035 #[tokio::test]
1036 async fn test_holt_winters() {
1037 let config = ForecastingConfig {
1038 algorithm: ForecastAlgorithm::HoltWinters,
1039 seasonal_period: Some(4),
1040 min_data_points: 20,
1041 ..Default::default()
1042 };
1043
1044 let mut analytics = PredictiveAnalytics::new(config).unwrap();
1045
1046 let mut data = Vec::new();
1048 for i in 0..24 {
1049 let base = (i / 4) as f64;
1050 let seasonal = (i % 4) as f64 * 2.0;
1051 data.push(base + seasonal);
1052 }
1053
1054 analytics.train(&data).await.unwrap();
1055 let forecast = analytics.forecast(8).await;
1056 assert!(forecast.is_ok());
1057 }
1058
1059 #[tokio::test]
1060 async fn test_accuracy_metrics() {
1061 let config = ForecastingConfig::default();
1062 let analytics = PredictiveAnalytics::new(config).unwrap();
1063
1064 let actual = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1065 let fitted = vec![1.1, 2.1, 2.9, 3.9, 5.1];
1066
1067 let metrics = analytics.compute_accuracy_metrics(&actual, &fitted);
1068 assert!(metrics.is_ok());
1069
1070 let m = metrics.unwrap();
1071 assert!(m.mae > 0.0);
1072 assert!(m.rmse > 0.0);
1073 assert!(m.r_squared > 0.9); }
1075
1076 #[tokio::test]
1077 async fn test_confidence_intervals() {
1078 let config = ForecastingConfig {
1079 confidence_level: 0.95,
1080 ..Default::default()
1081 };
1082
1083 let mut analytics = PredictiveAnalytics::new(config).unwrap();
1084
1085 let data: Vec<f64> = (0..30).map(|i| i as f64 + fastrand::f64() * 2.0).collect();
1086 analytics.train(&data).await.unwrap();
1087
1088 let forecast = analytics.forecast(5).await.unwrap();
1089
1090 for i in 0..forecast.predictions.len() - 1 {
1092 let width_i = forecast.upper_bound[i] - forecast.lower_bound[i];
1093 let width_next = forecast.upper_bound[i + 1] - forecast.lower_bound[i + 1];
1094 assert!(width_next >= width_i - 1e-6); }
1096 }
1097
1098 #[tokio::test]
1099 async fn test_reset() {
1100 let config = ForecastingConfig::default();
1101 let mut analytics = PredictiveAnalytics::new(config).unwrap();
1102
1103 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
1104 analytics.train(&data).await.unwrap();
1105
1106 analytics.reset().await.unwrap();
1107
1108 let stats = analytics.get_stats().await;
1109 assert_eq!(stats.training_data_size, 0);
1110 assert_eq!(stats.total_forecasts, 0);
1111 }
1112}