Skip to content

[ENH] Forecasting code #2895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 57 additions & 119 deletions aeon/forecasting/_ets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
import numpy as np
from numba import njit

from aeon.forecasting.base import BaseForecaster
from aeon.forecasting.base import BaseForecaster, DirectForecastingMixin

ADDITIVE = "additive"
MULTIPLICATIVE = "multiplicative"


class ETSForecaster(BaseForecaster):
class ETSForecaster(BaseForecaster, DirectForecastingMixin):
"""Exponential Smoothing (ETS) forecaster.

Implements the ETS (Error, Trend, Seasonality) forecaster, supporting additive
Expand All @@ -45,29 +45,6 @@ class ETSForecaster(BaseForecaster):
phi : float, default=0.99
Trend damping parameter (used only for damped trend models).

Attributes
----------
forecast_val_ : float
Forecast value for the given horizon.
level_ : float
Estimated level component.
trend_ : float
Estimated trend component.
seasonality_ : array-like or None
Estimated seasonal components.
aic_ : float
Akaike Information Criterion of the fitted model.
avg_mean_sq_err_ : float
Average mean squared error of the fitted model.
residuals_ : list of float
Residuals from the fitted model.
fitted_values_ : list of float
Fitted values for the training data.
liklihood_ : float
Log-likelihood of the fitted model.
n_timepoints_ : int
Number of time points in the training series.

References
----------
.. [1] R. J. Hyndman and G. Athanasopoulos,
Expand All @@ -90,6 +67,7 @@ class ETSForecaster(BaseForecaster):

_tags = {
"capability:horizon": False,
"fit_is_empty": True,
}

def __init__(
Expand All @@ -103,46 +81,33 @@ def __init__(
gamma: float = 0.01,
phi: float = 0.99,
):
self.alpha = alpha
self.beta = beta
self.gamma = gamma
self.phi = phi
self.forecast_val_ = 0.0
self.level_ = 0.0
self.trend_ = 0.0
self.seasonality_ = None
self._beta = beta
self._gamma = gamma
self.error_type = error_type
self.trend_type = trend_type
self.seasonality_type = seasonality_type
self.seasonal_period = seasonal_period
self._seasonal_period = seasonal_period
self.n_timepoints_ = 0
self.avg_mean_sq_err_ = 0
self.liklihood_ = 0
self.k_ = 0
self.aic_ = 0
self.residuals_ = []
self.fitted_values_ = []
super().__init__(horizon=1, axis=1)
self.alpha = alpha
self.beta = beta
self.gamma = gamma
self.phi = phi

def _fit(self, y, exog=None):
"""Fit Exponential Smoothing forecaster to series y.
super().__init__(horizon=1, axis=1)

Fit a forecaster to predict self.horizon steps ahead using y.
def _predict(self, y=None, exog=None):
"""
Predict the next horizon steps ahead.

Parameters
----------
y : np.ndarray
A time series on which to learn a forecaster to predict horizon ahead
y : np.ndarray, default = None
A time series to predict the next horizon value for. If None,
predict the next horizon value after series seen in fit.
exog : np.ndarray, default =None
Optional exogenous time series data assumed to be aligned with y

Returns
-------
self
Fitted ETSForecaster.
float
single prediction self.horizon steps ahead of y.
"""
_validate_parameter(self.error_type, False)
_validate_parameter(self.seasonality_type, True)
Expand All @@ -158,87 +123,60 @@ def _get_int(x):
return 2
return x

self._error_type = _get_int(self.error_type)
self._seasonality_type = _get_int(self.seasonality_type)
self._trend_type = _get_int(self.trend_type)
if self._seasonal_period < 1 or self._seasonality_type == 0:
self._seasonal_period = 1
error_type = _get_int(self.error_type)
seasonality_type = _get_int(self.seasonality_type)
trend_type = _get_int(self.trend_type)

seasonal_period = self.seasonal_period
if self.seasonal_period < 1 or seasonality_type == 0:
seasonal_period = 1

if self._trend_type == 0:
beta = self.beta
if trend_type == 0:
# Required for the equations in _update_states to work correctly
self._beta = 0
if self._seasonality_type == 0:
beta = 0

gamma = self.gamma
if seasonality_type == 0:
# Required for the equations in _update_states to work correctly
self._gamma = 0
gamma = 0

data = y.squeeze()
(
self.level_,
self.trend_,
self.seasonality_,
self.n_timepoints_,
self.residuals_,
self.fitted_values_,
self.avg_mean_sq_err_,
self.liklihood_,
self.k_,
self.aic_,
level_,
trend_,
seasonality_,
n_timepoints_,
residuals_,
fitted_values_,
avg_mean_sq_err_,
liklihood_,
k_,
aic_,
) = _numba_fit(
data,
self._error_type,
self._trend_type,
self._seasonality_type,
self._seasonal_period,
error_type,
trend_type,
seasonality_type,
seasonal_period,
self.alpha,
self._beta,
self._gamma,
beta,
gamma,
self.phi,
)
self.forecast_ = _predict(
self._trend_type,
self._seasonality_type,
self.level_,
self.trend_,
self.seasonality_,

fitted_value = _predict(
trend_type,
seasonality_type,
level_,
trend_,
seasonality_,
self.phi,
self.horizon,
self.n_timepoints_,
self._seasonal_period,
)

return self

def _predict(self, y, exog=None):
"""
Predict the next horizon steps ahead.

Parameters
----------
y : np.ndarray, default = None
A time series to predict the next horizon value for. If None,
predict the next horizon value after series seen in fit.
exog : np.ndarray, default =None
Optional exogenous time series data assumed to be aligned with y

Returns
-------
float
single prediction self.horizon steps ahead of y.
"""
return self.forecast_

def _initialise(self, data):
"""
Initialize level, trend, and seasonality values for the ETS model.

Parameters
----------
data : array-like
The time series data
(should contain at least two full seasons if seasonality is specified)
"""
self.level_, self.trend_, self.seasonality_ = _initialise(
self._trend_type, self._seasonality_type, self._seasonal_period, data
n_timepoints_,
seasonal_period,
)
return fitted_value


@njit(fastmath=True, cache=True)
Expand Down
22 changes: 4 additions & 18 deletions aeon/forecasting/_naive.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,16 @@ class NaiveForecaster(BaseForecaster):
Only relevant for "seasonal_last".
"""

_tags = {
"fit_is_empty": True,
}

def __init__(self, strategy="last", seasonal_period=1, horizon=1):
self.strategy = strategy
self.seasonal_period = seasonal_period

super().__init__(horizon=horizon, axis=1)

def _fit(self, y, exog=None):
y_squeezed = y.squeeze()

if self.strategy == "last":
self.forecast_ = y_squeezed[-1]
elif self.strategy == "mean":
self.forecast_ = np.mean(y_squeezed)
elif self.strategy == "seasonal_last":
season = y_squeezed[-self.seasonal_period :]
idx = (self.horizon - 1) % self.seasonal_period
self.forecast_ = season[idx]
else:
raise ValueError(
f"Unknown strategy: {self.strategy}. "
"Valid strategies are 'last', 'mean', 'seasonal_last'."
)
return self

def _predict(self, y, exog=None):
y_squeezed = y.squeeze()

Expand Down
10 changes: 8 additions & 2 deletions aeon/forecasting/_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@
import numpy as np
from sklearn.linear_model import LinearRegression

from aeon.forecasting.base import BaseForecaster
from aeon.forecasting.base import (
BaseForecaster,
DirectForecastingMixin,
IterativeForecastingMixin,
)


class RegressionForecaster(BaseForecaster):
class RegressionForecaster(
BaseForecaster, DirectForecastingMixin, IterativeForecastingMixin
):
"""
Regression based forecasting.

Expand Down
Loading
Loading