From 9a2cce516b084d9369f8bd5c80727d8fb8e178d1 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 2 Jul 2025 06:12:15 +0200 Subject: [PATCH 1/3] add auto naive forecaster --- aeon/forecasting/_auto_naive.py | 76 +++++++++++++++++++++++++++++++++ aeon/forecasting/_naive.py | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 aeon/forecasting/_auto_naive.py diff --git a/aeon/forecasting/_auto_naive.py b/aeon/forecasting/_auto_naive.py new file mode 100644 index 0000000000..5c493d20e7 --- /dev/null +++ b/aeon/forecasting/_auto_naive.py @@ -0,0 +1,76 @@ +"""Naive forecaster with parameters set on the training data.""" + +"""Naive forecaster with multiple strategies.""" + +__maintainer__ = ["TonyBagnall"] +__all__ = ["AutoNaiveForecaster"] + + +import numpy as np + +from aeon.forecasting.base import BaseForecaster +from aeon.forecasting._naive import NaiveForecaster + +class AutoNaiveForecaster(BaseForecaster): + """ + Naive forecaster with strategy set based on minimising error. + + Searches options, "last", "mean", and "seasonal_last", with season in range [2,max_season]. + If max_season is not passed to the constructor, it will be set to series length/2. + + Simple first implementation, splits the train series into 70% train and 30% validation split + and minimises RMSE on the validation set. + + Parameters + ---------- + max_season : int or None, default=None + The maximum season to consider in the parameter search. In None, set as quarter the length of the series + passed in `fit`. + + Examples + -------- + >>> import aeon as ae + """ + + def __init__(self, max_season=None, horizon=1): + self.max_season = max_season + super().__init__(horizon=horizon, axis=1) + + def _fit(self, y, exog=None): + y = y.squeeze() + l = len(y) + + y_train = y[:int(0.7*l)] + y_test = y[int(0.7*l):] + # Eval last first + last = y_train[-1] + mean = np.mean(y_train) + # measure error and pick one + seasons = y_train[-self.max_season:] # Get all the fixed values + + return self + + def _predict(self, y=None, exog=None): + if y is None: + if self.strategy == "last" or self.strategy == "mean": + return self._fitted_scalar_value + + # For "seasonal_last" strategy + prediction_index = (self.horizon - 1) % self.seasonal_period + return self._fitted_last_season[prediction_index] + else: + y_squeezed = y.squeeze() + + if self.strategy == "last": + return y_squeezed[-1] + elif self.strategy == "mean": + return np.mean(y_squeezed) + elif self.strategy == "seasonal_last": + period = y_squeezed[-self.seasonal_period :] + idx = (self.horizon - 1) % self.seasonal_period + return period[idx] + else: + raise ValueError( + f"Unknown strategy: {self.strategy}. " + "Valid strategies are 'last', 'mean', 'seasonal_last'." + ) diff --git a/aeon/forecasting/_naive.py b/aeon/forecasting/_naive.py index da242018e2..5149f3908a 100644 --- a/aeon/forecasting/_naive.py +++ b/aeon/forecasting/_naive.py @@ -1,6 +1,6 @@ """Naive forecaster with multiple strategies.""" -__maintainer__ = [] +__maintainer__ = ["TonyBagnall"] __all__ = ["NaiveForecaster"] From a716daa29014aeb0b2abea756ba5a155bd16e021 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 2 Jul 2025 20:22:43 +0200 Subject: [PATCH 2/3] add auto naive forecaster --- aeon/forecasting/_auto_naive.py | 75 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/aeon/forecasting/_auto_naive.py b/aeon/forecasting/_auto_naive.py index 5c493d20e7..7ff6b32166 100644 --- a/aeon/forecasting/_auto_naive.py +++ b/aeon/forecasting/_auto_naive.py @@ -7,9 +7,9 @@ import numpy as np - +from enum import Enum from aeon.forecasting.base import BaseForecaster -from aeon.forecasting._naive import NaiveForecaster + class AutoNaiveForecaster(BaseForecaster): """ @@ -34,43 +34,50 @@ class AutoNaiveForecaster(BaseForecaster): def __init__(self, max_season=None, horizon=1): self.max_season = max_season + self.strategy_ = "last" super().__init__(horizon=horizon, axis=1) def _fit(self, y, exog=None): y = y.squeeze() - l = len(y) - - y_train = y[:int(0.7*l)] - y_test = y[int(0.7*l):] - # Eval last first - last = y_train[-1] - mean = np.mean(y_train) - # measure error and pick one - seasons = y_train[-self.max_season:] # Get all the fixed values + # last strategy + mse_last = np.mean((y[1:] - y[:-1]) ** 2) + + # series mean strategy, in sample + mse_mean = np.mean((y - np.mean(y)) ** 2) + + # seasonal strategy, in sample + max_season = self.max_season + if self.max_season is None: + max_season = len(y)/4 + best_s = None + best_seasonal = np.inf + for s in range(1, max_season + 1): + # Predict y[t] = y[t - s] + y_true = y[s:] + y_pred = y[:-s] + mse = np.mean((y_true - y_pred) ** 2) + + if mse < best_seasonal: + best_seasonal = mse + best_s = s + self.best_mse_ = mse_last + self._fitted_scalar_value = y[:-1] + + if mse_mean < mse_last: + self.strategy_ = "mean" + self.best_mse_ = mse_mean + self._fitted_scalar_value = np.mean(y) + + if self.best_mse_ < best_seasonal: + self.strategy_ = "seasonal" + self.season = best_s + self.best_mse_ = best_seasonal return self - def _predict(self, y=None, exog=None): - if y is None: - if self.strategy == "last" or self.strategy == "mean": - return self._fitted_scalar_value - + def _predict(self, y, exog=None): + if self.strategy_ == "last" or self.strategy_ == "mean": + return self._fitted_scalar_value # For "seasonal_last" strategy - prediction_index = (self.horizon - 1) % self.seasonal_period - return self._fitted_last_season[prediction_index] - else: - y_squeezed = y.squeeze() - - if self.strategy == "last": - return y_squeezed[-1] - elif self.strategy == "mean": - return np.mean(y_squeezed) - elif self.strategy == "seasonal_last": - period = y_squeezed[-self.seasonal_period :] - idx = (self.horizon - 1) % self.seasonal_period - return period[idx] - else: - raise ValueError( - f"Unknown strategy: {self.strategy}. " - "Valid strategies are 'last', 'mean', 'seasonal_last'." - ) + prediction_index = (self.horizon - 1) % self.seasonal_period + return self._fitted_last_season[prediction_index] From e101eda4249450d69f4a4b24c427de33ab693af8 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Wed, 2 Jul 2025 20:43:28 +0200 Subject: [PATCH 3/3] add auto naive forecaster --- aeon/forecasting/_auto_naive.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aeon/forecasting/_auto_naive.py b/aeon/forecasting/_auto_naive.py index 7ff6b32166..928fa530b2 100644 --- a/aeon/forecasting/_auto_naive.py +++ b/aeon/forecasting/_auto_naive.py @@ -39,6 +39,7 @@ def __init__(self, max_season=None, horizon=1): def _fit(self, y, exog=None): y = y.squeeze() + self._y = y # last strategy mse_last = np.mean((y[1:] - y[:-1]) ** 2) @@ -70,7 +71,7 @@ def _fit(self, y, exog=None): if self.best_mse_ < best_seasonal: self.strategy_ = "seasonal" - self.season = best_s + self.season_ = best_s self.best_mse_ = best_seasonal return self @@ -79,5 +80,7 @@ def _predict(self, y, exog=None): if self.strategy_ == "last" or self.strategy_ == "mean": return self._fitted_scalar_value # For "seasonal_last" strategy - prediction_index = (self.horizon - 1) % self.seasonal_period - return self._fitted_last_season[prediction_index] + prediction_index = (self.horizon - 1) % self.season_ + return self.self._y [prediction_index] + +