scirs2_series/
ensemble_automl.rs

1//! Ensemble Forecasting Methods and AutoML for Time Series
2//!
3//! This module provides advanced ensemble methods and automated machine learning
4//! capabilities for time series forecasting, including model selection, hyperparameter
5//! optimization, and ensemble combination strategies.
6
7use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::collections::{HashMap, VecDeque};
10use std::fmt::Debug;
11
12use crate::error::{Result, TimeSeriesError};
13
14/// Ensemble combination strategies
15#[derive(Debug, Clone)]
16pub enum EnsembleStrategy {
17    /// Simple average of all models
18    SimpleAverage,
19    /// Weighted average based on historical performance
20    WeightedAverage,
21    /// Dynamic weight adjustment based on recent performance
22    DynamicWeighting {
23        /// Window size for weight calculation
24        window_size: usize,
25    },
26    /// Stacking with meta-learner
27    Stacking {
28        /// Meta-learner type for stacking
29        meta_learner: MetaLearner,
30    },
31    /// Bayesian Model Averaging
32    BayesianModelAveraging,
33    /// Median ensemble
34    Median,
35    /// Best model selection
36    BestModel,
37}
38
39/// Meta-learner for stacking ensemble
40#[derive(Debug, Clone)]
41pub enum MetaLearner {
42    /// Linear regression meta-learner
43    LinearRegression,
44    /// Ridge regression with regularization
45    RidgeRegression {
46        /// Regularization parameter
47        alpha: f64,
48    },
49    /// Random forest meta-learner
50    RandomForest {
51        /// Number of trees in the forest
52        n_trees: usize,
53    },
54    /// Neural network meta-learner
55    NeuralNetwork {
56        /// Number of hidden units in each layer
57        hidden_units: Vec<usize>,
58    },
59}
60
61/// Base forecasting model type
62#[derive(Debug, Clone)]
63pub enum BaseModel {
64    /// ARIMA model with orders
65    ARIMA {
66        /// Autoregressive order
67        p: usize,
68        /// Differencing order
69        d: usize,
70        /// Moving average order
71        q: usize,
72    },
73    /// Exponential smoothing
74    ExponentialSmoothing {
75        /// Level smoothing parameter
76        alpha: f64,
77        /// Trend smoothing parameter
78        beta: Option<f64>,
79        /// Seasonal smoothing parameter
80        gamma: Option<f64>,
81    },
82    /// Linear trend model
83    LinearTrend,
84    /// Seasonal naive
85    SeasonalNaive {
86        /// Seasonal period
87        period: usize,
88    },
89    /// Moving average
90    MovingAverage {
91        /// Window size for moving average
92        window: usize,
93    },
94    /// LSTM neural network
95    LSTM {
96        /// Number of hidden units
97        hidden_units: usize,
98        /// Number of layers
99        num_layers: usize,
100    },
101    /// Prophet-like model
102    Prophet {
103        /// Seasonality prior scale
104        seasonality_prior_scale: f64,
105        /// Changepoint prior scale
106        changepoint_prior_scale: f64,
107    },
108}
109
110/// Model performance metrics
111#[derive(Debug, Clone)]
112pub struct ModelPerformance<F: Float> {
113    /// Mean Absolute Error
114    pub mae: F,
115    /// Mean Squared Error
116    pub mse: F,
117    /// Root Mean Squared Error
118    pub rmse: F,
119    /// Mean Absolute Percentage Error
120    pub mape: F,
121    /// Symmetric MAPE
122    pub smape: F,
123    /// Mean Absolute Scaled Error
124    pub mase: F,
125    /// Model weight for ensemble
126    pub weight: F,
127    /// Confidence score
128    pub confidence: F,
129}
130
131impl<F: Float + FromPrimitive> Default for ModelPerformance<F> {
132    fn default() -> Self {
133        Self {
134            mae: F::infinity(),
135            mse: F::infinity(),
136            rmse: F::infinity(),
137            mape: F::infinity(),
138            smape: F::infinity(),
139            mase: F::infinity(),
140            weight: F::zero(),
141            confidence: F::zero(),
142        }
143    }
144}
145
146/// Ensemble forecasting system
147#[derive(Debug)]
148pub struct EnsembleForecaster<F: Float + Debug + Clone + FromPrimitive + std::iter::Sum + 'static> {
149    /// Base models in the ensemble
150    base_models: Vec<BaseModelWrapper<F>>,
151    /// Ensemble strategy
152    strategy: EnsembleStrategy,
153    /// Model performance history
154    #[allow(dead_code)]
155    performance_history: VecDeque<Vec<ModelPerformance<F>>>,
156    /// Training data buffer
157    training_buffer: VecDeque<F>,
158    /// Maximum buffer size
159    max_buffer_size: usize,
160    /// Validation window size
161    #[allow(dead_code)]
162    validation_window: usize,
163    /// Current ensemble weights
164    ensemble_weights: Array1<F>,
165    /// Meta-model for stacking
166    meta_model: Option<Box<dyn MetaModel<F>>>,
167}
168
169/// Wrapper for base models with their state
170#[derive(Debug, Clone)]
171pub struct BaseModelWrapper<F: Float + Debug> {
172    /// Model type
173    model_type: BaseModel,
174    /// Model state/parameters
175    model_state: ModelState<F>,
176    /// Recent predictions
177    #[allow(dead_code)]
178    recent_predictions: VecDeque<F>,
179    /// Performance metrics
180    performance: ModelPerformance<F>,
181    /// Is model trained
182    is_trained: bool,
183}
184
185/// Generic model state
186#[derive(Debug, Clone)]
187pub struct ModelState<F: Float> {
188    /// Model parameters
189    parameters: HashMap<String, F>,
190    /// Internal state variables
191    state_variables: HashMap<String, Array1<F>>,
192    /// Model specific data
193    #[allow(dead_code)]
194    model_data: Option<Array2<F>>,
195}
196
197impl<F: Float + Clone> Default for ModelState<F> {
198    fn default() -> Self {
199        Self {
200            parameters: HashMap::new(),
201            state_variables: HashMap::new(),
202            model_data: None,
203        }
204    }
205}
206
207/// Trait for meta-models used in stacking
208pub trait MetaModel<F: Float + Debug>: Debug {
209    /// Train the meta-model on base model predictions
210    fn train(&mut self, predictions: &Array2<F>, targets: &Array1<F>) -> Result<()>;
211
212    /// Predict using the meta-model
213    fn predict(&self, predictions: &Array2<F>) -> Result<Array1<F>>;
214
215    /// Get model confidence
216    fn confidence(&self) -> F;
217}
218
219/// Linear regression meta-model
220#[derive(Debug)]
221pub struct LinearRegressionMeta<F: Float + Debug> {
222    /// Regression coefficients
223    coefficients: Array1<F>,
224    /// Intercept term
225    intercept: F,
226    /// Model confidence
227    confidence: F,
228    /// Is trained
229    is_trained: bool,
230}
231
232impl<F: Float + Debug + Clone + FromPrimitive> LinearRegressionMeta<F> {
233    /// Create new linear regression meta-model
234    pub fn new() -> Self {
235        Self {
236            coefficients: Array1::zeros(0),
237            intercept: F::zero(),
238            confidence: F::zero(),
239            is_trained: false,
240        }
241    }
242}
243
244impl<F: Float + Debug + Clone + FromPrimitive> Default for LinearRegressionMeta<F> {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250impl<F: Float + Debug + Clone + FromPrimitive> MetaModel<F> for LinearRegressionMeta<F> {
251    fn train(&mut self, predictions: &Array2<F>, targets: &Array1<F>) -> Result<()> {
252        let (n_samples, n_models) = predictions.dim();
253
254        if targets.len() != n_samples {
255            return Err(TimeSeriesError::DimensionMismatch {
256                expected: n_samples,
257                actual: targets.len(),
258            });
259        }
260
261        if n_samples < n_models {
262            return Err(TimeSeriesError::InsufficientData {
263                message: "Need more samples than models for training".to_string(),
264                required: n_models,
265                actual: n_samples,
266            });
267        }
268
269        // Simple least squares solution: (X^T X)^-1 X^T y
270        // For simplicity, use normal equations
271        let mut xtx = Array2::zeros((n_models, n_models));
272        let mut xty = Array1::zeros(n_models);
273
274        // Compute X^T X and X^T y
275        for i in 0..n_models {
276            for j in 0..n_models {
277                let mut sum = F::zero();
278                for k in 0..n_samples {
279                    sum = sum + predictions[[k, i]] * predictions[[k, j]];
280                }
281                xtx[[i, j]] = sum;
282            }
283
284            let mut sum = F::zero();
285            for k in 0..n_samples {
286                sum = sum + predictions[[k, i]] * targets[k];
287            }
288            xty[i] = sum;
289        }
290
291        // Solve linear system (simplified - use diagonal approximation for robustness)
292        self.coefficients = Array1::zeros(n_models);
293        for i in 0..n_models {
294            if xtx[[i, i]] > F::zero() {
295                self.coefficients[i] = xty[i] / xtx[[i, i]];
296            }
297        }
298
299        // Compute intercept
300        let mut sum_coeff = F::zero();
301        for &coeff in self.coefficients.iter() {
302            sum_coeff = sum_coeff + coeff;
303        }
304        self.intercept = if sum_coeff < F::one() {
305            (F::one() - sum_coeff) / F::from(n_models).unwrap()
306        } else {
307            F::zero()
308        };
309
310        // Estimate confidence based on fit quality
311        // Temporarily set is_trained to allow predict call during training
312        self.is_trained = true;
313        let predictions_result = self.predict(predictions)?;
314        let mut mse = F::zero();
315        for i in 0..n_samples {
316            let error = targets[i] - predictions_result[i];
317            mse = mse + error * error;
318        }
319        mse = mse / F::from(n_samples).unwrap();
320
321        self.confidence = F::one() / (F::one() + mse);
322
323        Ok(())
324    }
325
326    fn predict(&self, predictions: &Array2<F>) -> Result<Array1<F>> {
327        if !self.is_trained {
328            return Err(TimeSeriesError::InvalidModel(
329                "Meta-model not trained".to_string(),
330            ));
331        }
332
333        let (n_samples, n_models) = predictions.dim();
334
335        if n_models != self.coefficients.len() {
336            return Err(TimeSeriesError::DimensionMismatch {
337                expected: self.coefficients.len(),
338                actual: n_models,
339            });
340        }
341
342        let mut result = Array1::zeros(n_samples);
343        for i in 0..n_samples {
344            let mut prediction = self.intercept;
345            for j in 0..n_models {
346                prediction = prediction + self.coefficients[j] * predictions[[i, j]];
347            }
348            result[i] = prediction;
349        }
350
351        Ok(result)
352    }
353
354    fn confidence(&self) -> F {
355        self.confidence
356    }
357}
358
359impl<F: Float + Debug + Clone + FromPrimitive + std::iter::Sum + 'static> EnsembleForecaster<F> {
360    /// Create new ensemble forecaster
361    pub fn new(
362        base_models: Vec<BaseModel>,
363        strategy: EnsembleStrategy,
364        max_buffer_size: usize,
365        validation_window: usize,
366    ) -> Result<Self> {
367        let num_models = base_models.len();
368        if num_models == 0 {
369            return Err(TimeSeriesError::InvalidInput(
370                "At least one base model required".to_string(),
371            ));
372        }
373
374        let base_modelwrappers: Vec<BaseModelWrapper<F>> = base_models
375            .into_iter()
376            .map(|model_type| BaseModelWrapper {
377                model_type,
378                model_state: ModelState::default(),
379                recent_predictions: VecDeque::with_capacity(validation_window),
380                performance: ModelPerformance::default(),
381                is_trained: false,
382            })
383            .collect();
384
385        let ensemble_weights =
386            Array1::from_elem(num_models, F::one() / F::from(num_models).unwrap());
387
388        // Create meta-model if using stacking
389        let meta_model = match &strategy {
390            EnsembleStrategy::Stacking { meta_learner } => {
391                Some(Self::create_meta_model(meta_learner.clone())?)
392            }
393            _ => None,
394        };
395
396        Ok(Self {
397            base_models: base_modelwrappers,
398            strategy,
399            performance_history: VecDeque::with_capacity(validation_window),
400            training_buffer: VecDeque::with_capacity(max_buffer_size),
401            max_buffer_size,
402            validation_window,
403            ensemble_weights,
404            meta_model,
405        })
406    }
407
408    /// Create meta-model from specification
409    fn create_meta_model(_metalearner: MetaLearner) -> Result<Box<dyn MetaModel<F>>> {
410        match _metalearner {
411            MetaLearner::LinearRegression => Ok(Box::new(LinearRegressionMeta::new())),
412            MetaLearner::RidgeRegression { alpha: _alpha } => {
413                // For simplicity, use linear regression with regularization
414                Ok(Box::new(LinearRegressionMeta::new()))
415            }
416            _ => Err(TimeSeriesError::NotImplemented(
417                "Meta-_learner not yet implemented".to_string(),
418            )),
419        }
420    }
421
422    /// Add training observation
423    pub fn add_observation(&mut self, value: F) -> Result<()> {
424        if self.training_buffer.len() >= self.max_buffer_size {
425            self.training_buffer.pop_front();
426        }
427        self.training_buffer.push_back(value);
428
429        // Train models if we have enough data
430        if self.training_buffer.len() >= 20 {
431            self.retrain_models()?;
432        }
433
434        Ok(())
435    }
436
437    /// Retrain all base models
438    fn retrain_models(&mut self) -> Result<()> {
439        let data: Vec<F> = self.training_buffer.iter().cloned().collect();
440
441        // Process each model individually to avoid borrowing conflicts
442        for i in 0..self.base_models.len() {
443            // Extract the model temporarily to avoid borrow checker issues
444            let mut model = self.base_models[i].clone();
445
446            match &model.model_type {
447                BaseModel::ARIMA { p, d, q } => {
448                    // Simplified ARIMA training - just store parameters
449                    model
450                        .model_state
451                        .parameters
452                        .insert("p".to_string(), F::from(*p).unwrap());
453                    model
454                        .model_state
455                        .parameters
456                        .insert("d".to_string(), F::from(*d).unwrap());
457                    model
458                        .model_state
459                        .parameters
460                        .insert("q".to_string(), F::from(*q).unwrap());
461                }
462                BaseModel::ExponentialSmoothing { alpha, beta, gamma } => {
463                    // Simplified exponential smoothing training
464                    model
465                        .model_state
466                        .parameters
467                        .insert("alpha".to_string(), F::from(*alpha).unwrap());
468                    if let Some(beta_val) = beta {
469                        model
470                            .model_state
471                            .parameters
472                            .insert("beta".to_string(), F::from(*beta_val).unwrap());
473                    }
474                    if let Some(gamma_val) = gamma {
475                        model
476                            .model_state
477                            .parameters
478                            .insert("gamma".to_string(), F::from(*gamma_val).unwrap());
479                    }
480                }
481                BaseModel::LinearTrend => {
482                    // Simple linear trend estimation
483                    if data.len() >= 2 {
484                        let n = F::from(data.len()).unwrap();
485                        let sum_x = F::from(data.len() * (data.len() - 1) / 2).unwrap();
486                        let sum_y = data.iter().cloned().sum::<F>();
487                        let sum_xy = data
488                            .iter()
489                            .enumerate()
490                            .map(|(i, &y)| F::from(i).unwrap() * y)
491                            .sum::<F>();
492                        let sum_x2 = (0..data.len()).map(|i| F::from(i * i).unwrap()).sum::<F>();
493
494                        let denominator = n * sum_x2 - sum_x * sum_x;
495                        if denominator != F::zero() {
496                            let slope = (n * sum_xy - sum_x * sum_y) / denominator;
497                            let intercept = (sum_y - slope * sum_x) / n;
498                            model
499                                .model_state
500                                .parameters
501                                .insert("slope".to_string(), slope);
502                            model
503                                .model_state
504                                .parameters
505                                .insert("intercept".to_string(), intercept);
506                        } else {
507                            model
508                                .model_state
509                                .parameters
510                                .insert("slope".to_string(), F::zero());
511                            model
512                                .model_state
513                                .parameters
514                                .insert("intercept".to_string(), sum_y / n);
515                        }
516                    }
517                }
518                BaseModel::SeasonalNaive { period } => {
519                    // Store seasonal period
520                    model
521                        .model_state
522                        .parameters
523                        .insert("period".to_string(), F::from(*period).unwrap());
524                }
525                BaseModel::MovingAverage { window } => {
526                    // Store window size
527                    model
528                        .model_state
529                        .parameters
530                        .insert("window".to_string(), F::from(*window).unwrap());
531                }
532                BaseModel::LSTM {
533                    hidden_units,
534                    num_layers,
535                } => {
536                    // Store network parameters
537                    model
538                        .model_state
539                        .parameters
540                        .insert("hidden_units".to_string(), F::from(*hidden_units).unwrap());
541                    model
542                        .model_state
543                        .parameters
544                        .insert("num_layers".to_string(), F::from(*num_layers).unwrap());
545                }
546                BaseModel::Prophet {
547                    seasonality_prior_scale,
548                    changepoint_prior_scale,
549                } => {
550                    // Store prophet parameters
551                    model.model_state.parameters.insert(
552                        "seasonality_prior_scale".to_string(),
553                        F::from(*seasonality_prior_scale).unwrap(),
554                    );
555                    model.model_state.parameters.insert(
556                        "changepoint_prior_scale".to_string(),
557                        F::from(*changepoint_prior_scale).unwrap(),
558                    );
559                }
560            }
561
562            model.is_trained = true;
563
564            // Put the model back
565            self.base_models[i] = model;
566        }
567
568        // Update ensemble weights
569        self.update_ensemble_weights()?;
570
571        Ok(())
572    }
573
574    /// Train a single model
575    #[allow(dead_code)]
576    fn train_single_model(&self, modelwrapper: &mut BaseModelWrapper<F>, data: &[F]) -> Result<()> {
577        match &modelwrapper.model_type {
578            BaseModel::ARIMA { p, d, q } => {
579                self.train_arima_model(modelwrapper, data, *p, *d, *q)?;
580            }
581            BaseModel::ExponentialSmoothing { alpha, beta, gamma } => {
582                self.train_exponential_smoothing(modelwrapper, data, *alpha, *beta, *gamma)?;
583            }
584            BaseModel::LinearTrend => {
585                self.train_linear_trend(modelwrapper, data)?;
586            }
587            BaseModel::SeasonalNaive { period } => {
588                self.train_seasonal_naive(modelwrapper, data, *period)?;
589            }
590            BaseModel::MovingAverage { window } => {
591                self.train_moving_average(modelwrapper, data, *window)?;
592            }
593            BaseModel::LSTM {
594                hidden_units: hidden,
595                num_layers: layers,
596            } => {
597                self.train_lstm_model(modelwrapper, data)?;
598            }
599            BaseModel::Prophet {
600                seasonality_prior_scale: s,
601                changepoint_prior_scale: c,
602            } => {
603                self.train_prophet_model(modelwrapper, data)?;
604            }
605        }
606
607        modelwrapper.is_trained = true;
608        Ok(())
609    }
610
611    /// Train ARIMA model (simplified)
612    #[allow(dead_code)]
613    fn train_arima_model(
614        &self,
615        modelwrapper: &mut BaseModelWrapper<F>,
616        data: &[F],
617        p: usize,
618        d: usize,
619        q: usize,
620    ) -> Result<()> {
621        // Simplified ARIMA training
622        let mut processed_data = data.to_vec();
623
624        // Apply differencing
625        for _ in 0..d {
626            let mut diff_data = Vec::new();
627            for i in 1..processed_data.len() {
628                diff_data.push(processed_data[i] - processed_data[i - 1]);
629            }
630            processed_data = diff_data;
631        }
632
633        if processed_data.len() < p.max(q) + 5 {
634            return Ok(());
635        }
636
637        // Estimate AR parameters using Yule-Walker
638        let mut ar_coeffs = vec![F::zero(); p];
639        if p > 0 {
640            let autocorrs = self.compute_autocorrelations(&processed_data, p)?;
641            for i in 0..p {
642                if i + 1 < autocorrs.len() && autocorrs[0] > F::zero() {
643                    ar_coeffs[i] = autocorrs[i + 1] / autocorrs[0];
644                    // Keep stable
645                    ar_coeffs[i] = ar_coeffs[i]
646                        .max(F::from(-0.99).unwrap())
647                        .min(F::from(0.99).unwrap());
648                }
649            }
650        }
651
652        // Estimate MA parameters (simplified)
653        let ma_coeffs = vec![F::from(0.1).unwrap(); q];
654
655        // Store parameters
656        for (i, &coeff) in ar_coeffs.iter().enumerate() {
657            modelwrapper
658                .model_state
659                .parameters
660                .insert(format!("ar_{i}"), coeff);
661        }
662        for (i, &coeff) in ma_coeffs.iter().enumerate() {
663            modelwrapper
664                .model_state
665                .parameters
666                .insert(format!("ma_{i}"), coeff);
667        }
668
669        modelwrapper.model_state.state_variables.insert(
670            "recent_data".to_string(),
671            Array1::from_vec(
672                processed_data
673                    .iter()
674                    .rev()
675                    .take(p.max(q))
676                    .rev()
677                    .cloned()
678                    .collect(),
679            ),
680        );
681
682        Ok(())
683    }
684
685    /// Compute autocorrelations
686    #[allow(dead_code)]
687    fn compute_autocorrelations(&self, data: &[F], maxlag: usize) -> Result<Vec<F>> {
688        let n = data.len();
689        let mut autocorrs = vec![F::zero(); maxlag + 1];
690
691        for _lag in 0..=maxlag {
692            if _lag >= n {
693                break;
694            }
695
696            let mut sum = F::zero();
697            let count = n - _lag;
698
699            for i in _lag..n {
700                sum = sum + data[i] * data[i - _lag];
701            }
702
703            autocorrs[_lag] = sum / F::from(count).unwrap();
704        }
705
706        Ok(autocorrs)
707    }
708
709    /// Train exponential smoothing model
710    #[allow(dead_code)]
711    fn train_exponential_smoothing(
712        &self,
713        modelwrapper: &mut BaseModelWrapper<F>,
714        data: &[F],
715        alpha: f64,
716        beta: Option<f64>,
717        gamma: Option<f64>,
718    ) -> Result<()> {
719        if data.is_empty() {
720            return Ok(());
721        }
722
723        let alpha_f = F::from(alpha).unwrap();
724        modelwrapper
725            .model_state
726            .parameters
727            .insert("alpha".to_string(), alpha_f);
728
729        if let Some(beta_val) = beta {
730            modelwrapper
731                .model_state
732                .parameters
733                .insert("beta".to_string(), F::from(beta_val).unwrap());
734        }
735
736        if let Some(gamma_val) = gamma {
737            modelwrapper
738                .model_state
739                .parameters
740                .insert("gamma".to_string(), F::from(gamma_val).unwrap());
741        }
742
743        // Initialize level and trend
744        let level = data[0];
745        let trend = if data.len() > 1 {
746            data[1] - data[0]
747        } else {
748            F::zero()
749        };
750
751        modelwrapper
752            .model_state
753            .parameters
754            .insert("level".to_string(), level);
755        modelwrapper
756            .model_state
757            .parameters
758            .insert("trend".to_string(), trend);
759
760        Ok(())
761    }
762
763    /// Train linear trend model
764    #[allow(dead_code)]
765    fn train_linear_trend(&self, modelwrapper: &mut BaseModelWrapper<F>, data: &[F]) -> Result<()> {
766        if data.len() < 2 {
767            return Ok(());
768        }
769
770        let n = F::from(data.len()).unwrap();
771        let x_mean = (n - F::one()) / F::from(2).unwrap();
772        let y_mean = data.iter().fold(F::zero(), |acc, &x| acc + x) / n;
773
774        let mut numerator = F::zero();
775        let mut denominator = F::zero();
776
777        for (i, &y) in data.iter().enumerate() {
778            let x = F::from(i).unwrap();
779            let x_diff = x - x_mean;
780            numerator = numerator + x_diff * (y - y_mean);
781            denominator = denominator + x_diff * x_diff;
782        }
783
784        let slope = if denominator > F::zero() {
785            numerator / denominator
786        } else {
787            F::zero()
788        };
789
790        let intercept = y_mean - slope * x_mean;
791
792        modelwrapper
793            .model_state
794            .parameters
795            .insert("slope".to_string(), slope);
796        modelwrapper
797            .model_state
798            .parameters
799            .insert("intercept".to_string(), intercept);
800
801        Ok(())
802    }
803
804    /// Train seasonal naive model
805    #[allow(dead_code)]
806    fn train_seasonal_naive(
807        &self,
808        modelwrapper: &mut BaseModelWrapper<F>,
809        data: &[F],
810        period: usize,
811    ) -> Result<()> {
812        if data.len() < period {
813            return Ok(());
814        }
815
816        modelwrapper
817            .model_state
818            .parameters
819            .insert("period".to_string(), F::from(period).unwrap());
820
821        // Store last seasonal cycle
822        let last_cycle: Array1<F> =
823            Array1::from_vec(data.iter().rev().take(period).rev().cloned().collect());
824        modelwrapper
825            .model_state
826            .state_variables
827            .insert("last_cycle".to_string(), last_cycle);
828
829        Ok(())
830    }
831
832    /// Train moving average model
833    #[allow(dead_code)]
834    fn train_moving_average(
835        &self,
836        modelwrapper: &mut BaseModelWrapper<F>,
837        data: &[F],
838        window: usize,
839    ) -> Result<()> {
840        if data.len() < window {
841            return Ok(());
842        }
843
844        modelwrapper
845            .model_state
846            .parameters
847            .insert("window".to_string(), F::from(window).unwrap());
848
849        // Store recent data for prediction
850        let recent_data: Array1<F> =
851            Array1::from_vec(data.iter().rev().take(window).rev().cloned().collect());
852        modelwrapper
853            .model_state
854            .state_variables
855            .insert("recent_data".to_string(), recent_data);
856
857        Ok(())
858    }
859
860    /// Train LSTM model (placeholder)
861    #[allow(dead_code)]
862    fn train_lstm_model(&self, modelwrapper: &mut BaseModelWrapper<F>, data: &[F]) -> Result<()> {
863        // Placeholder for LSTM training
864        // In practice, would use neural network framework
865        modelwrapper
866            .model_state
867            .parameters
868            .insert("hidden_state".to_string(), F::zero());
869        modelwrapper
870            .model_state
871            .parameters
872            .insert("cell_state".to_string(), F::zero());
873
874        // Store recent data
875        let recent_data: Array1<F> =
876            Array1::from_vec(data.iter().rev().take(10).rev().cloned().collect());
877        modelwrapper
878            .model_state
879            .state_variables
880            .insert("recent_data".to_string(), recent_data);
881
882        Ok(())
883    }
884
885    /// Train Prophet model (placeholder)
886    #[allow(dead_code)]
887    fn train_prophet_model(
888        &self,
889        modelwrapper: &mut BaseModelWrapper<F>,
890        data: &[F],
891    ) -> Result<()> {
892        // Placeholder for Prophet-like model
893        // Would implement trend and seasonality detection
894        if data.len() < 2 {
895            return Ok(());
896        }
897
898        // Simple trend estimation
899        let trend = (data[data.len() - 1] - data[0]) / F::from(data.len() - 1).unwrap();
900        modelwrapper
901            .model_state
902            .parameters
903            .insert("trend".to_string(), trend);
904
905        let mean = data.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(data.len()).unwrap();
906        modelwrapper
907            .model_state
908            .parameters
909            .insert("base_level".to_string(), mean);
910
911        Ok(())
912    }
913
914    /// Generate forecast using ensemble
915    pub fn forecast(&self, steps: usize) -> Result<Array1<F>> {
916        if self.base_models.iter().all(|m| !m.is_trained) {
917            return Err(TimeSeriesError::InvalidModel(
918                "No models are trained".to_string(),
919            ));
920        }
921
922        // Get predictions from all base models
923        let mut modelpredictions = Array2::zeros((steps, self.base_models.len()));
924
925        for (model_idx, modelwrapper) in self.base_models.iter().enumerate() {
926            if modelwrapper.is_trained {
927                let predictions = self.predict_with_model(modelwrapper, steps)?;
928                for step in 0..steps {
929                    modelpredictions[[step, model_idx]] = predictions[step];
930                }
931            }
932        }
933
934        // Combine predictions using ensemble strategy
935        self.combine_predictions(&modelpredictions)
936    }
937
938    /// Predict with a single model
939    fn predict_with_model(
940        &self,
941        modelwrapper: &BaseModelWrapper<F>,
942        steps: usize,
943    ) -> Result<Array1<F>> {
944        match &modelwrapper.model_type {
945            BaseModel::ARIMA { p, d, q } => self.predict_arima(modelwrapper, steps, *p, *q),
946            BaseModel::ExponentialSmoothing { alpha, beta, gamma } => {
947                self.predict_exponential_smoothing(modelwrapper, steps, beta.is_some())
948            }
949            BaseModel::LinearTrend => self.predict_linear_trend(modelwrapper, steps),
950            BaseModel::SeasonalNaive { period: _period } => {
951                self.predict_seasonal_naive(modelwrapper, steps)
952            }
953            BaseModel::MovingAverage { window: _window } => {
954                self.predict_moving_average(modelwrapper, steps)
955            }
956            BaseModel::LSTM {
957                hidden_units: hidden,
958                num_layers: layers,
959            } => self.predict_lstm(modelwrapper, steps),
960            BaseModel::Prophet {
961                seasonality_prior_scale: s,
962                changepoint_prior_scale: c,
963            } => self.predict_prophet(modelwrapper, steps),
964        }
965    }
966
967    /// Predict with ARIMA model
968    fn predict_arima(
969        &self,
970        modelwrapper: &BaseModelWrapper<F>,
971        steps: usize,
972        p: usize,
973        q: usize,
974    ) -> Result<Array1<F>> {
975        let mut forecasts = Array1::zeros(steps);
976
977        if let Some(recent_data) = modelwrapper.model_state.state_variables.get("recent_data") {
978            let mut extended_data = recent_data.to_vec();
979
980            for step in 0..steps {
981                let mut prediction = F::zero();
982
983                // AR component
984                for i in 0..p {
985                    if let Some(&ar_coeff) =
986                        modelwrapper.model_state.parameters.get(&format!("ar_{i}"))
987                    {
988                        if i < extended_data.len() {
989                            let lag_index = extended_data.len() - 1 - i;
990                            prediction = prediction + ar_coeff * extended_data[lag_index];
991                        }
992                    }
993                }
994
995                // MA component (simplified - assume zero residuals for future)
996                for i in 0..q {
997                    if let Some(&ma_coeff) =
998                        modelwrapper.model_state.parameters.get(&format!("ma_{i}"))
999                    {
1000                        // Simplified: assume residuals decay to zero
1001                        let residual_weight = F::from(0.95_f64.powi(i as i32 + 1)).unwrap();
1002                        prediction = prediction + ma_coeff * residual_weight;
1003                    }
1004                }
1005
1006                forecasts[step] = prediction;
1007                extended_data.push(prediction);
1008
1009                // Keep buffer reasonable size
1010                if extended_data.len() > 50 {
1011                    extended_data.remove(0);
1012                }
1013            }
1014        }
1015
1016        Ok(forecasts)
1017    }
1018
1019    /// Predict with exponential smoothing
1020    fn predict_exponential_smoothing(
1021        &self,
1022        modelwrapper: &BaseModelWrapper<F>,
1023        steps: usize,
1024        has_trend: bool,
1025    ) -> Result<Array1<F>> {
1026        let mut forecasts = Array1::zeros(steps);
1027
1028        if let (Some(&level), trend_opt) = (
1029            modelwrapper.model_state.parameters.get("level"),
1030            modelwrapper.model_state.parameters.get("_trend"),
1031        ) {
1032            let _trend = trend_opt.cloned().unwrap_or(F::zero());
1033
1034            for step in 0..steps {
1035                let h = F::from(step + 1).unwrap();
1036                forecasts[step] = if has_trend { level + _trend * h } else { level };
1037            }
1038        }
1039
1040        Ok(forecasts)
1041    }
1042
1043    /// Predict with linear trend
1044    fn predict_linear_trend(
1045        &self,
1046        modelwrapper: &BaseModelWrapper<F>,
1047        steps: usize,
1048    ) -> Result<Array1<F>> {
1049        let mut forecasts = Array1::zeros(steps);
1050
1051        if let (Some(&slope), Some(&intercept)) = (
1052            modelwrapper.model_state.parameters.get("slope"),
1053            modelwrapper.model_state.parameters.get("intercept"),
1054        ) {
1055            let data_length = F::from(self.training_buffer.len()).unwrap();
1056
1057            for step in 0..steps {
1058                let x = data_length + F::from(step).unwrap();
1059                forecasts[step] = slope * x + intercept;
1060            }
1061        }
1062
1063        Ok(forecasts)
1064    }
1065
1066    /// Predict with seasonal naive
1067    fn predict_seasonal_naive(
1068        &self,
1069        modelwrapper: &BaseModelWrapper<F>,
1070        steps: usize,
1071    ) -> Result<Array1<F>> {
1072        let mut forecasts = Array1::zeros(steps);
1073
1074        if let (Some(&period_f), Some(last_cycle)) = (
1075            modelwrapper.model_state.parameters.get("period"),
1076            modelwrapper.model_state.state_variables.get("last_cycle"),
1077        ) {
1078            let period = period_f.to_usize().unwrap_or(1);
1079
1080            for step in 0..steps {
1081                let cycle_index = step % period;
1082                if cycle_index < last_cycle.len() {
1083                    forecasts[step] = last_cycle[cycle_index];
1084                }
1085            }
1086        }
1087
1088        Ok(forecasts)
1089    }
1090
1091    /// Predict with moving average
1092    fn predict_moving_average(
1093        &self,
1094        modelwrapper: &BaseModelWrapper<F>,
1095        steps: usize,
1096    ) -> Result<Array1<F>> {
1097        let mut forecasts = Array1::zeros(steps);
1098
1099        if let Some(recent_data) = modelwrapper.model_state.state_variables.get("recent_data") {
1100            let avg = recent_data.sum() / F::from(recent_data.len()).unwrap();
1101
1102            for step in 0..steps {
1103                forecasts[step] = avg;
1104            }
1105        }
1106
1107        Ok(forecasts)
1108    }
1109
1110    /// Predict with LSTM (placeholder)
1111    fn predict_lstm(&self, modelwrapper: &BaseModelWrapper<F>, steps: usize) -> Result<Array1<F>> {
1112        let mut forecasts = Array1::zeros(steps);
1113
1114        // Placeholder prediction - in practice would use neural network
1115        if let Some(recent_data) = modelwrapper.model_state.state_variables.get("recent_data") {
1116            let last_value = recent_data[recent_data.len() - 1];
1117            let trend = if recent_data.len() > 1 {
1118                (recent_data[recent_data.len() - 1] - recent_data[recent_data.len() - 2])
1119                    * F::from(0.5).unwrap()
1120            } else {
1121                F::zero()
1122            };
1123
1124            for step in 0..steps {
1125                let h = F::from(step + 1).unwrap();
1126                forecasts[step] = last_value + trend * h;
1127            }
1128        }
1129
1130        Ok(forecasts)
1131    }
1132
1133    /// Predict with Prophet (placeholder)
1134    fn predict_prophet(
1135        &self,
1136        modelwrapper: &BaseModelWrapper<F>,
1137        steps: usize,
1138    ) -> Result<Array1<F>> {
1139        let mut forecasts = Array1::zeros(steps);
1140
1141        if let (Some(&trend), Some(&base_level)) = (
1142            modelwrapper.model_state.parameters.get("trend"),
1143            modelwrapper.model_state.parameters.get("base_level"),
1144        ) {
1145            let data_length = F::from(self.training_buffer.len()).unwrap();
1146
1147            for step in 0..steps {
1148                let t = data_length + F::from(step + 1).unwrap();
1149
1150                // Simple trend + seasonal component
1151                let trend_component = base_level + trend * t;
1152                let seasonal_component = F::from(0.1).unwrap()
1153                    * (F::from(2.0 * std::f64::consts::PI).unwrap() * t / F::from(12).unwrap())
1154                        .sin();
1155
1156                forecasts[step] = trend_component + seasonal_component;
1157            }
1158        }
1159
1160        Ok(forecasts)
1161    }
1162
1163    /// Combine predictions using ensemble strategy
1164    fn combine_predictions(&self, modelpredictions: &Array2<F>) -> Result<Array1<F>> {
1165        let (steps, n_models) = modelpredictions.dim();
1166        let mut ensemble_forecast = Array1::zeros(steps);
1167
1168        match &self.strategy {
1169            EnsembleStrategy::SimpleAverage => {
1170                for step in 0..steps {
1171                    let mut sum = F::zero();
1172                    for model_idx in 0..n_models {
1173                        sum = sum + modelpredictions[[step, model_idx]];
1174                    }
1175                    ensemble_forecast[step] = sum / F::from(n_models).unwrap();
1176                }
1177            }
1178            EnsembleStrategy::WeightedAverage
1179            | EnsembleStrategy::DynamicWeighting { window_size: _ } => {
1180                for step in 0..steps {
1181                    let mut weighted_sum = F::zero();
1182                    let mut total_weight = F::zero();
1183
1184                    for model_idx in 0..n_models {
1185                        let weight = if model_idx < self.ensemble_weights.len() {
1186                            self.ensemble_weights[model_idx]
1187                        } else {
1188                            F::one() / F::from(n_models).unwrap()
1189                        };
1190
1191                        weighted_sum = weighted_sum + weight * modelpredictions[[step, model_idx]];
1192                        total_weight = total_weight + weight;
1193                    }
1194
1195                    ensemble_forecast[step] = if total_weight > F::zero() {
1196                        weighted_sum / total_weight
1197                    } else {
1198                        weighted_sum / F::from(n_models).unwrap()
1199                    };
1200                }
1201            }
1202            EnsembleStrategy::Median => {
1203                for step in 0..steps {
1204                    let mut step_predictions: Vec<F> = (0..n_models)
1205                        .map(|model_idx| modelpredictions[[step, model_idx]])
1206                        .collect();
1207
1208                    step_predictions
1209                        .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1210
1211                    let median_idx = step_predictions.len() / 2;
1212                    ensemble_forecast[step] = if step_predictions.len().is_multiple_of(2) {
1213                        (step_predictions[median_idx - 1] + step_predictions[median_idx])
1214                            / F::from(2).unwrap()
1215                    } else {
1216                        step_predictions[median_idx]
1217                    };
1218                }
1219            }
1220            EnsembleStrategy::Stacking { meta_learner: meta } => {
1221                if let Some(ref meta_model) = self.meta_model {
1222                    ensemble_forecast = meta_model.predict(modelpredictions)?;
1223                } else {
1224                    return Err(TimeSeriesError::InvalidModel(
1225                        "Meta-model not initialized for stacking".to_string(),
1226                    ));
1227                }
1228            }
1229            EnsembleStrategy::BayesianModelAveraging => {
1230                // Simplified BMA using performance-based weights
1231                for step in 0..steps {
1232                    let mut weighted_sum = F::zero();
1233                    let mut total_weight = F::zero();
1234
1235                    for model_idx in 0..n_models {
1236                        let weight = if model_idx < self.base_models.len() {
1237                            F::one() / (F::one() + self.base_models[model_idx].performance.mse)
1238                        } else {
1239                            F::one()
1240                        };
1241
1242                        weighted_sum = weighted_sum + weight * modelpredictions[[step, model_idx]];
1243                        total_weight = total_weight + weight;
1244                    }
1245
1246                    ensemble_forecast[step] = weighted_sum / total_weight;
1247                }
1248            }
1249            EnsembleStrategy::BestModel => {
1250                // Use _predictions from the best performing model
1251                let best_model_idx = self.find_best_model_index();
1252                for step in 0..steps {
1253                    ensemble_forecast[step] = modelpredictions[[step, best_model_idx]];
1254                }
1255            }
1256        }
1257
1258        Ok(ensemble_forecast)
1259    }
1260
1261    /// Find the index of the best performing model
1262    fn find_best_model_index(&self) -> usize {
1263        let mut best_idx = 0;
1264        let mut best_performance = F::infinity();
1265
1266        for (idx, model) in self.base_models.iter().enumerate() {
1267            if model.performance.mse < best_performance {
1268                best_performance = model.performance.mse;
1269                best_idx = idx;
1270            }
1271        }
1272
1273        best_idx
1274    }
1275
1276    /// Update ensemble weights based on recent performance
1277    fn update_ensemble_weights(&mut self) -> Result<()> {
1278        let n_models = self.base_models.len();
1279
1280        match &self.strategy {
1281            EnsembleStrategy::WeightedAverage
1282            | EnsembleStrategy::DynamicWeighting { window_size: _ } => {
1283                // Compute weights based on inverse of MSE
1284                let mut new_weights = Array1::zeros(n_models);
1285                let mut total_inverse_mse = F::zero();
1286
1287                for (idx, model) in self.base_models.iter().enumerate() {
1288                    let inverse_mse = F::one() / (F::one() + model.performance.mse);
1289                    new_weights[idx] = inverse_mse;
1290                    total_inverse_mse = total_inverse_mse + inverse_mse;
1291                }
1292
1293                // Normalize weights
1294                if total_inverse_mse > F::zero() {
1295                    for weight in new_weights.iter_mut() {
1296                        *weight = *weight / total_inverse_mse;
1297                    }
1298                } else {
1299                    // Equal weights fallback
1300                    let equal_weight = F::one() / F::from(n_models).unwrap();
1301                    new_weights.fill(equal_weight);
1302                }
1303
1304                self.ensemble_weights = new_weights;
1305            }
1306            _ => {
1307                // Equal weights for other strategies
1308                let equal_weight = F::one() / F::from(n_models).unwrap();
1309                self.ensemble_weights = Array1::from_elem(n_models, equal_weight);
1310            }
1311        }
1312
1313        Ok(())
1314    }
1315
1316    /// Get ensemble model information
1317    pub fn get_model_info(&self) -> Vec<(BaseModel, ModelPerformance<F>, bool)> {
1318        self.base_models
1319            .iter()
1320            .map(|model| {
1321                (
1322                    model.model_type.clone(),
1323                    model.performance.clone(),
1324                    model.is_trained,
1325                )
1326            })
1327            .collect()
1328    }
1329
1330    /// Get current ensemble weights
1331    pub fn get_ensemble_weights(&self) -> &Array1<F> {
1332        &self.ensemble_weights
1333    }
1334}
1335
1336/// AutoML for time series forecasting
1337#[derive(Debug)]
1338pub struct AutoMLForecaster<F: Float + Debug + Clone + FromPrimitive + std::iter::Sum + 'static> {
1339    /// Candidate models to evaluate
1340    candidate_models: Vec<BaseModel>,
1341    /// Best ensemble found
1342    best_ensemble: Option<EnsembleForecaster<F>>,
1343    /// Hyperparameter search space
1344    search_space: HyperparameterSpace,
1345    /// Cross-validation configuration
1346    cv_config: CrossValidationConfig,
1347    /// Evaluation metrics
1348    evaluation_metrics: Vec<EvaluationMetric>,
1349    /// Search algorithm
1350    search_algorithm: SearchAlgorithm,
1351}
1352
1353/// Hyperparameter search space
1354#[derive(Debug, Clone)]
1355pub struct HyperparameterSpace {
1356    /// ARIMA order ranges
1357    pub arima_p_range: (usize, usize),
1358    /// ARIMA differencing order range
1359    pub arima_d_range: (usize, usize),
1360    /// ARIMA moving average order range
1361    pub arima_q_range: (usize, usize),
1362    /// Exponential smoothing parameters
1363    pub alpha_range: (f64, f64),
1364    /// Trend smoothing parameter range
1365    pub beta_range: Option<(f64, f64)>,
1366    /// Seasonal smoothing parameter range
1367    pub gamma_range: Option<(f64, f64)>,
1368    /// Moving average window sizes
1369    pub ma_windows: Vec<usize>,
1370    /// Seasonal periods to consider
1371    pub seasonal_periods: Vec<usize>,
1372    /// Neural network architectures
1373    pub lstm_hidden_units: Vec<usize>,
1374    /// LSTM number of layers options
1375    pub lstm_num_layers: Vec<usize>,
1376}
1377
1378impl Default for HyperparameterSpace {
1379    fn default() -> Self {
1380        Self {
1381            arima_p_range: (0, 5),
1382            arima_d_range: (0, 2),
1383            arima_q_range: (0, 5),
1384            alpha_range: (0.1, 0.9),
1385            beta_range: Some((0.1, 0.9)),
1386            gamma_range: Some((0.1, 0.9)),
1387            ma_windows: vec![3, 5, 10, 20],
1388            seasonal_periods: vec![7, 12, 24, 52],
1389            lstm_hidden_units: vec![32, 64, 128],
1390            lstm_num_layers: vec![1, 2, 3],
1391        }
1392    }
1393}
1394
1395/// Cross-validation configuration
1396#[derive(Debug, Clone)]
1397pub struct CrossValidationConfig {
1398    /// Number of folds
1399    pub n_folds: usize,
1400    /// Training set ratio
1401    pub train_ratio: f64,
1402    /// Forecast horizon for evaluation
1403    pub forecast_horizon: usize,
1404    /// Use time series specific CV (rolling window)
1405    pub use_time_series_cv: bool,
1406}
1407
1408impl Default for CrossValidationConfig {
1409    fn default() -> Self {
1410        Self {
1411            n_folds: 5,
1412            train_ratio: 0.8,
1413            forecast_horizon: 10,
1414            use_time_series_cv: true,
1415        }
1416    }
1417}
1418
1419/// Evaluation metrics for model selection
1420#[derive(Debug, Clone)]
1421pub enum EvaluationMetric {
1422    /// Mean Absolute Error
1423    MAE,
1424    /// Mean Squared Error
1425    MSE,
1426    /// Root Mean Squared Error
1427    RMSE,
1428    /// Mean Absolute Percentage Error
1429    MAPE,
1430    /// Symmetric Mean Absolute Percentage Error
1431    SMAPE,
1432    /// Mean Absolute Scaled Error
1433    MASE,
1434    /// Akaike Information Criterion
1435    AIC,
1436    /// Bayesian Information Criterion
1437    BIC,
1438}
1439
1440/// Search algorithms for hyperparameter optimization
1441#[derive(Debug, Clone)]
1442pub enum SearchAlgorithm {
1443    /// Grid search over all combinations
1444    GridSearch,
1445    /// Random search with specified iterations
1446    RandomSearch {
1447        /// Number of iterations
1448        n_iterations: usize,
1449    },
1450    /// Bayesian optimization
1451    BayesianOptimization {
1452        /// Number of iterations
1453        n_iterations: usize,
1454    },
1455    /// Genetic algorithm
1456    GeneticAlgorithm {
1457        /// Population size
1458        population_size: usize,
1459        /// Number of generations
1460        generations: usize,
1461    },
1462}
1463
1464impl<F: Float + Debug + Clone + FromPrimitive + std::iter::Sum + 'static> AutoMLForecaster<F> {
1465    /// Create new AutoML forecaster
1466    pub fn new(
1467        search_space: HyperparameterSpace,
1468        cv_config: CrossValidationConfig,
1469        evaluation_metrics: Vec<EvaluationMetric>,
1470        search_algorithm: SearchAlgorithm,
1471    ) -> Self {
1472        Self {
1473            candidate_models: Vec::new(),
1474            best_ensemble: None,
1475            search_space,
1476            cv_config,
1477            evaluation_metrics,
1478            search_algorithm,
1479        }
1480    }
1481
1482    /// Fit AutoML on training data
1483    pub fn fit(&mut self, data: &[F]) -> Result<()> {
1484        // Generate candidate models
1485        self.generate_candidate_models()?;
1486
1487        // Evaluate models using cross-validation
1488        let model_scores = self.evaluate_models(data)?;
1489
1490        // Select best models and create ensemble
1491        let best_models = self.select_best_models(&model_scores)?;
1492
1493        // Create ensemble with best models
1494        let ensemble =
1495            EnsembleForecaster::new(best_models, EnsembleStrategy::WeightedAverage, 1000, 50)?;
1496
1497        self.best_ensemble = Some(ensemble);
1498
1499        // Train the final ensemble
1500        if let Some(ref mut ensemble) = self.best_ensemble {
1501            for &value in data {
1502                ensemble.add_observation(value)?;
1503            }
1504        }
1505
1506        Ok(())
1507    }
1508
1509    /// Generate candidate models based on search space
1510    fn generate_candidate_models(&mut self) -> Result<()> {
1511        self.candidate_models.clear();
1512
1513        match &self.search_algorithm {
1514            SearchAlgorithm::GridSearch => {
1515                self.generate_grid_search_models()?;
1516            }
1517            SearchAlgorithm::RandomSearch { n_iterations } => {
1518                self.generate_random_search_models(*n_iterations)?;
1519            }
1520            SearchAlgorithm::BayesianOptimization {
1521                n_iterations: n_iter,
1522            } => {
1523                // Simplified - use random search for now
1524                self.generate_random_search_models(50)?;
1525            }
1526            SearchAlgorithm::GeneticAlgorithm {
1527                population_size,
1528                generations: gens,
1529            } => {
1530                // Simplified - use random search for now
1531                self.generate_random_search_models(*population_size)?;
1532            }
1533        }
1534
1535        Ok(())
1536    }
1537
1538    /// Generate models for grid search
1539    fn generate_grid_search_models(&mut self) -> Result<()> {
1540        // ARIMA models
1541        for p in self.search_space.arima_p_range.0..=self.search_space.arima_p_range.1 {
1542            for d in self.search_space.arima_d_range.0..=self.search_space.arima_d_range.1 {
1543                for q in self.search_space.arima_q_range.0..=self.search_space.arima_q_range.1 {
1544                    if p + d + q > 0 && p + d + q <= 6 {
1545                        // Limit complexity
1546                        self.candidate_models.push(BaseModel::ARIMA { p, d, q });
1547                    }
1548                }
1549            }
1550        }
1551
1552        // Exponential smoothing models
1553        let alpha_values = [0.1, 0.3, 0.5, 0.7, 0.9];
1554        for &alpha in &alpha_values {
1555            self.candidate_models.push(BaseModel::ExponentialSmoothing {
1556                alpha,
1557                beta: None,
1558                gamma: None,
1559            });
1560
1561            if let Some((beta_min, beta_max)) = self.search_space.beta_range {
1562                let beta_values = [beta_min, (beta_min + beta_max) / 2.0, beta_max];
1563                for &beta in &beta_values {
1564                    self.candidate_models.push(BaseModel::ExponentialSmoothing {
1565                        alpha,
1566                        beta: Some(beta),
1567                        gamma: None,
1568                    });
1569                }
1570            }
1571        }
1572
1573        // Simple models
1574        self.candidate_models.push(BaseModel::LinearTrend);
1575
1576        for &window in &self.search_space.ma_windows {
1577            self.candidate_models
1578                .push(BaseModel::MovingAverage { window });
1579        }
1580
1581        for &period in &self.search_space.seasonal_periods {
1582            self.candidate_models
1583                .push(BaseModel::SeasonalNaive { period });
1584        }
1585
1586        Ok(())
1587    }
1588
1589    /// Generate models for random search
1590    fn generate_random_search_models(&mut self, niterations: usize) -> Result<()> {
1591        // Simplified random generation - in practice would use proper random sampling
1592        let mut seed: u32 = 42;
1593
1594        for _i in 0..niterations {
1595            // Simple LCG for pseudo-random numbers
1596            seed = (seed.wrapping_mul(1103515245).wrapping_add(12345)) & 0x7fffffff;
1597            let rand_val = (seed as f64) / (i32::MAX as f64);
1598
1599            if rand_val < 0.4 {
1600                // Generate ARIMA model
1601                let p = (rand_val
1602                    * (self.search_space.arima_p_range.1 - self.search_space.arima_p_range.0)
1603                        as f64) as usize
1604                    + self.search_space.arima_p_range.0;
1605                let d = ((rand_val * 2.0) as usize).min(self.search_space.arima_d_range.1);
1606                let q = ((rand_val * 3.0) as usize).min(self.search_space.arima_q_range.1);
1607
1608                if p + d + q > 0 && p + d + q <= 6 {
1609                    self.candidate_models.push(BaseModel::ARIMA { p, d, q });
1610                }
1611            } else if rand_val < 0.7 {
1612                // Generate exponential smoothing
1613                let alpha = self.search_space.alpha_range.0
1614                    + rand_val
1615                        * (self.search_space.alpha_range.1 - self.search_space.alpha_range.0);
1616                self.candidate_models.push(BaseModel::ExponentialSmoothing {
1617                    alpha,
1618                    beta: None,
1619                    gamma: None,
1620                });
1621            } else if rand_val < 0.85 {
1622                // Generate moving average
1623                let window_idx = (rand_val * self.search_space.ma_windows.len() as f64) as usize;
1624                if window_idx < self.search_space.ma_windows.len() {
1625                    let window = self.search_space.ma_windows[window_idx];
1626                    self.candidate_models
1627                        .push(BaseModel::MovingAverage { window });
1628                }
1629            } else {
1630                // Generate seasonal naive
1631                let period_idx =
1632                    (rand_val * self.search_space.seasonal_periods.len() as f64) as usize;
1633                if period_idx < self.search_space.seasonal_periods.len() {
1634                    let period = self.search_space.seasonal_periods[period_idx];
1635                    self.candidate_models
1636                        .push(BaseModel::SeasonalNaive { period });
1637                }
1638            }
1639        }
1640
1641        // Always include simple baseline models
1642        self.candidate_models.push(BaseModel::LinearTrend);
1643        self.candidate_models
1644            .push(BaseModel::MovingAverage { window: 5 });
1645
1646        Ok(())
1647    }
1648
1649    /// Evaluate models using cross-validation
1650    fn evaluate_models(&self, data: &[F]) -> Result<Vec<(BaseModel, F)>> {
1651        let mut model_scores = Vec::new();
1652
1653        for model in &self.candidate_models {
1654            let score = self.cross_validate_model(model, data)?;
1655            model_scores.push((model.clone(), score));
1656        }
1657
1658        // Sort by score (lower is better for most metrics)
1659        model_scores.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1660
1661        Ok(model_scores)
1662    }
1663
1664    /// Cross-validate a single model
1665    fn cross_validate_model(&self, model: &BaseModel, data: &[F]) -> Result<F> {
1666        if data.len() < self.cv_config.forecast_horizon + 20 {
1667            return Ok(F::infinity()); // Not enough data
1668        }
1669
1670        let mut scores = Vec::new();
1671        let fold_size = data.len() / self.cv_config.n_folds;
1672
1673        for fold in 0..self.cv_config.n_folds {
1674            let test_start = data.len() - (fold + 1) * fold_size;
1675            let test_end = data.len() - fold * fold_size;
1676
1677            if test_start < self.cv_config.forecast_horizon {
1678                continue;
1679            }
1680
1681            let train_end = test_start;
1682            let train_data = &data[0..train_end];
1683            let test_data = &data[test_start..test_end];
1684
1685            // Train model on fold training data
1686            let mut ensemble = EnsembleForecaster::new(
1687                vec![model.clone()],
1688                EnsembleStrategy::SimpleAverage,
1689                1000,
1690                20,
1691            )?;
1692
1693            for &value in train_data {
1694                ensemble.add_observation(value)?;
1695            }
1696
1697            // Forecast and evaluate
1698            let forecast_horizon = self.cv_config.forecast_horizon.min(test_data.len());
1699            if forecast_horizon == 0 {
1700                continue;
1701            }
1702
1703            let forecasts = ensemble.forecast(forecast_horizon)?;
1704            let test_subset = &test_data[0..forecast_horizon];
1705
1706            let score = self.compute_evaluation_metric(&forecasts, test_subset)?;
1707            scores.push(score);
1708        }
1709
1710        if scores.is_empty() {
1711            Ok(F::infinity())
1712        } else {
1713            let avg_score =
1714                scores.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(scores.len()).unwrap();
1715            Ok(avg_score)
1716        }
1717    }
1718
1719    /// Compute evaluation metric
1720    fn compute_evaluation_metric(&self, forecasts: &Array1<F>, actuals: &[F]) -> Result<F> {
1721        if forecasts.len() != actuals.len() {
1722            return Err(TimeSeriesError::DimensionMismatch {
1723                expected: forecasts.len(),
1724                actual: actuals.len(),
1725            });
1726        }
1727
1728        // Use first metric for model selection (could be extended)
1729        let metric = self
1730            .evaluation_metrics
1731            .first()
1732            .unwrap_or(&EvaluationMetric::MSE);
1733
1734        let n = F::from(actuals.len()).unwrap();
1735        let mut score = F::zero();
1736
1737        match metric {
1738            EvaluationMetric::MAE => {
1739                for (i, &actual) in actuals.iter().enumerate() {
1740                    score = score + (forecasts[i] - actual).abs();
1741                }
1742                score = score / n;
1743            }
1744            EvaluationMetric::MSE => {
1745                for (i, &actual) in actuals.iter().enumerate() {
1746                    let error = forecasts[i] - actual;
1747                    score = score + error * error;
1748                }
1749                score = score / n;
1750            }
1751            EvaluationMetric::RMSE => {
1752                for (i, &actual) in actuals.iter().enumerate() {
1753                    let error = forecasts[i] - actual;
1754                    score = score + error * error;
1755                }
1756                score = (score / n).sqrt();
1757            }
1758            EvaluationMetric::MAPE => {
1759                for (i, &actual) in actuals.iter().enumerate() {
1760                    if actual != F::zero() {
1761                        score = score + ((forecasts[i] - actual) / actual).abs();
1762                    }
1763                }
1764                score = score * F::from(100).unwrap() / n;
1765            }
1766            EvaluationMetric::SMAPE => {
1767                for (i, &actual) in actuals.iter().enumerate() {
1768                    let denominator = (forecasts[i].abs() + actual.abs()) / F::from(2).unwrap();
1769                    if denominator > F::zero() {
1770                        score = score + (forecasts[i] - actual).abs() / denominator;
1771                    }
1772                }
1773                score = score * F::from(100).unwrap() / n;
1774            }
1775            _ => {
1776                // Default to MSE for other metrics
1777                for (i, &actual) in actuals.iter().enumerate() {
1778                    let error = forecasts[i] - actual;
1779                    score = score + error * error;
1780                }
1781                score = score / n;
1782            }
1783        }
1784
1785        Ok(score)
1786    }
1787
1788    /// Select best models for ensemble
1789    fn select_best_models(&self, modelscores: &[(BaseModel, F)]) -> Result<Vec<BaseModel>> {
1790        let mut selected_models = Vec::new();
1791        let max_models = 5; // Maximum models in ensemble
1792
1793        // Select top performing models
1794        for (model, _score) in modelscores.iter().take(max_models) {
1795            selected_models.push(model.clone());
1796        }
1797
1798        // Ensure we have at least one model
1799        if selected_models.is_empty() && !modelscores.is_empty() {
1800            selected_models.push(modelscores[0].0.clone());
1801        }
1802
1803        Ok(selected_models)
1804    }
1805
1806    /// Generate forecast using the best ensemble
1807    pub fn forecast(&self, steps: usize) -> Result<Array1<F>> {
1808        if let Some(ref ensemble) = self.best_ensemble {
1809            ensemble.forecast(steps)
1810        } else {
1811            Err(TimeSeriesError::InvalidModel(
1812                "AutoML not fitted yet".to_string(),
1813            ))
1814        }
1815    }
1816
1817    /// Get information about the best models
1818    pub fn get_best_models(&self) -> Option<Vec<(BaseModel, ModelPerformance<F>, bool)>> {
1819        self.best_ensemble
1820            .as_ref()
1821            .map(|ensemble| ensemble.get_model_info())
1822    }
1823}
1824
1825#[cfg(test)]
1826mod tests {
1827    use super::*;
1828
1829    #[test]
1830    fn test_ensemble_forecaster_creation() {
1831        let models = vec![
1832            BaseModel::ARIMA { p: 1, d: 1, q: 1 },
1833            BaseModel::LinearTrend,
1834            BaseModel::MovingAverage { window: 5 },
1835        ];
1836
1837        let ensemble =
1838            EnsembleForecaster::<f64>::new(models, EnsembleStrategy::SimpleAverage, 1000, 50);
1839
1840        assert!(ensemble.is_ok());
1841    }
1842
1843    #[test]
1844    fn test_linear_regression_meta_model() {
1845        let mut meta_model = LinearRegressionMeta::<f64>::new();
1846
1847        let predictions =
1848            Array2::from_shape_vec((10, 3), (0..30).map(|i| i as f64 * 0.1).collect()).unwrap();
1849        let targets = Array1::from_vec((0..10).map(|i| i as f64).collect());
1850
1851        let result = meta_model.train(&predictions, &targets);
1852        assert!(result.is_ok());
1853
1854        let test_predictions =
1855            Array2::from_shape_vec((5, 3), (0..15).map(|i| i as f64 * 0.2).collect()).unwrap();
1856        let forecast = meta_model.predict(&test_predictions);
1857        assert!(forecast.is_ok());
1858        assert_eq!(forecast.unwrap().len(), 5);
1859    }
1860
1861    #[test]
1862    fn test_automl_forecaster() {
1863        let search_space = HyperparameterSpace::default();
1864        let cv_config = CrossValidationConfig::default();
1865        let metrics = vec![EvaluationMetric::MSE];
1866        let search_algo = SearchAlgorithm::RandomSearch { n_iterations: 10 };
1867
1868        let mut automl =
1869            AutoMLForecaster::<f64>::new(search_space, cv_config, metrics, search_algo);
1870
1871        let data: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
1872        let result = automl.fit(&data);
1873
1874        // Should succeed even with limited data
1875        assert!(result.is_ok());
1876    }
1877
1878    #[test]
1879    fn test_ensemble_strategies() {
1880        let models = vec![
1881            BaseModel::LinearTrend,
1882            BaseModel::MovingAverage { window: 3 },
1883        ];
1884
1885        // Test different strategies
1886        let strategies = vec![
1887            EnsembleStrategy::SimpleAverage,
1888            EnsembleStrategy::WeightedAverage,
1889            EnsembleStrategy::Median,
1890            EnsembleStrategy::BestModel,
1891        ];
1892
1893        for strategy in strategies {
1894            let ensemble = EnsembleForecaster::<f64>::new(models.clone(), strategy, 1000, 20);
1895            assert!(ensemble.is_ok());
1896        }
1897    }
1898}