Skip to content

Commit c86d391

Browse files
authored
[ENH] Add recursive and direct methods to forecasting base class (#2899)
* base class methods * direct * recursive * base * direct_forecasting * recursive_forecasting * recursive_forecasting * recursive_forecasting * direct notebook * direct notebook * direct notebook * switch to iterative * switch to iterative * iterative * iterative notebook * append change * append change * fix regression bug * comment
1 parent 53ddbe5 commit c86d391

File tree

7 files changed

+857
-4
lines changed

7 files changed

+857
-4
lines changed

aeon/forecasting/_regression.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ def _fit(self, y, exog=None):
7373
X = np.lib.stride_tricks.sliding_window_view(y, window_shape=self.window)
7474
# Ignore the final horizon values: need to store these for pred with empty y
7575
X = X[: -self.horizon]
76-
# Extract y
77-
y = y[self.window + self.horizon - 1 :]
76+
# Extract y_train
77+
y_train = y[self.window + self.horizon - 1 :]
7878
self.last_ = y[-self.window :]
7979
self.last_ = self.last_.reshape(1, -1)
80-
self.regressor_.fit(X=X, y=y)
80+
self.regressor_.fit(X=X, y=y_train)
8181
return self
8282

8383
def _predict(self, y=None, exog=None):

aeon/forecasting/base.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
__all__ = ["BaseForecaster"]
99

1010
from abc import abstractmethod
11+
from typing import final
1112

1213
import numpy as np
1314
import pandas as pd
@@ -46,6 +47,7 @@ def __init__(self, horizon: int, axis: int):
4647
self.meta_ = None # Meta data related to y on the last fit
4748
super().__init__(axis)
4849

50+
@final
4951
def fit(self, y, exog=None):
5052
"""Fit forecaster to series y.
5153
@@ -89,6 +91,7 @@ def fit(self, y, exog=None):
8991
@abstractmethod
9092
def _fit(self, y, exog=None): ...
9193

94+
@final
9295
def predict(self, y=None, exog=None):
9396
"""Predict the next horizon steps ahead.
9497
@@ -117,6 +120,7 @@ def predict(self, y=None, exog=None):
117120
@abstractmethod
118121
def _predict(self, y=None, exog=None): ...
119122

123+
@final
120124
def forecast(self, y, exog=None):
121125
"""Forecast the next horizon steps ahead.
122126
@@ -144,6 +148,106 @@ def _forecast(self, y, exog=None):
144148
self.fit(y, exog)
145149
return self._predict(y, exog)
146150

151+
@final
152+
def direct_forecast(self, y, prediction_horizon):
153+
"""
154+
Make ``prediction_horizon`` ahead forecasts using a fit for each horizon.
155+
156+
This is commonly called the direct strategy. The forecaster is trained to
157+
predict one ahead, then retrained to fit two ahead etc. Not all forecasters
158+
are capable of being used with direct forecasting. The ability to
159+
forecast on horizons greater than 1 is indicated by the tag
160+
"capability:horizon". If this tag is false this function raises a value
161+
error. This method cannot be overridden.
162+
163+
Parameters
164+
----------
165+
y : np.ndarray
166+
The time series to make forecasts about.
167+
prediction_horizon : int
168+
The number of future time steps to forecast.
169+
170+
predictions : np.ndarray
171+
An array of shape `(prediction_horizon,)` containing the forecasts for
172+
each horizon.
173+
174+
Raises
175+
------
176+
ValueError
177+
if ``"capability:horizon`` is False or `prediction_horizon` less than 1.
178+
179+
Examples
180+
--------
181+
>>> from aeon.forecasting import RegressionForecaster
182+
>>> y = np.array([1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 2.0, 3.0, 4.0])
183+
>>> f = RegressionForecaster(window=3)
184+
>>> f.direct_forecast(y,2)
185+
array([3., 2.])
186+
"""
187+
horizon = self.get_tag("capability:horizon")
188+
if not horizon:
189+
raise ValueError(
190+
"This forecaster cannot be used with the direct strategy "
191+
"because it cannot be trained with a horizon > 1."
192+
)
193+
if prediction_horizon < 1:
194+
raise ValueError(
195+
"The `prediction_horizon` must be greater than or equal to 1."
196+
)
197+
198+
preds = np.zeros(prediction_horizon)
199+
for i in range(0, prediction_horizon):
200+
self.horizon = i + 1
201+
preds[i] = self.forecast(y)
202+
return preds
203+
204+
def iterative_forecast(self, y, prediction_horizon):
205+
"""
206+
Forecast ``prediction_horizon`` prediction using a single model from `y`.
207+
208+
This function implements the iterative forecasting strategy (also called
209+
recursive or iterated). This involves a single model fit on y which is then
210+
used to make ``prediction_horizon`` ahead using its own predictions as
211+
inputs for future forecasts. This is done by taking
212+
the prediction at step ``i`` and feeding it back into the model to help
213+
predict for step ``i+1``. The basic contract of
214+
`iterative_forecast` is that `fit` is only ever called once.
215+
216+
y : np.ndarray
217+
The time series to make forecasts about.
218+
prediction_horizon : int
219+
The number of future time steps to forecast.
220+
221+
Returns
222+
-------
223+
predictions : np.ndarray
224+
An array of shape `(prediction_horizon,)` containing the forecasts for
225+
each horizon.
226+
227+
Raises
228+
------
229+
ValueError
230+
if prediction_horizon` less than 1.
231+
232+
Examples
233+
--------
234+
>>> from aeon.forecasting import RegressionForecaster
235+
>>> y = np.array([1.0, 2.0, 3.0, 4.0, 3.0, 2.0, 1.0, 2.0, 3.0, 4.0])
236+
>>> f = RegressionForecaster(window=3)
237+
>>> f.iterative_forecast(y,2)
238+
array([3., 2.])
239+
"""
240+
if prediction_horizon < 1:
241+
raise ValueError(
242+
"The `prediction_horizon` must be greater than or equal to 1."
243+
)
244+
preds = np.zeros(prediction_horizon)
245+
self.fit(y)
246+
for i in range(0, prediction_horizon):
247+
preds[i] = self.predict(y)
248+
y = np.append(y, preds[i])
249+
return preds
250+
147251
def _convert_y(self, y: VALID_SERIES_INNER_TYPES, axis: int):
148252
"""Convert y to self.get_tag("y_inner_type")."""
149253
if axis > 1 or axis < 0:

aeon/forecasting/tests/test_base.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pandas as pd
55
import pytest
66

7-
from aeon.forecasting import NaiveForecaster
7+
from aeon.forecasting import NaiveForecaster, RegressionForecaster
88

99

1010
def test_base_forecaster():
@@ -39,3 +39,29 @@ def test_convert_y():
3939
f.set_tags(**{"y_inner_type": "pd.Series"})
4040
with pytest.raises(ValueError, match="Unsupported inner type"):
4141
f._convert_y(y, axis=1)
42+
43+
44+
def test_direct_forecast():
45+
"""Test direct forecasting."""
46+
y = np.random.rand(50)
47+
f = RegressionForecaster(window=10)
48+
# Direct should be the same as setting horizon manually.
49+
preds = f.direct_forecast(y, prediction_horizon=10)
50+
assert isinstance(preds, np.ndarray) and len(preds) == 10
51+
for i in range(0, 10):
52+
f = RegressionForecaster(window=10, horizon=i + 1)
53+
p = f.forecast(y)
54+
assert p == preds[i]
55+
56+
57+
def test_recursive_forecast():
58+
"""Test recursive forecasting."""
59+
y = np.random.rand(50)
60+
f = RegressionForecaster(window=4)
61+
preds = f.iterative_forecast(y, prediction_horizon=10)
62+
assert isinstance(preds, np.ndarray) and len(preds) == 10
63+
f.fit(y)
64+
for i in range(0, 10):
65+
p = f.predict(y)
66+
assert p == preds[i]
67+
y = np.append(y, p)

examples/forecasting/direct.ipynb

Lines changed: 385 additions & 0 deletions
Large diffs are not rendered by default.

examples/forecasting/img/direct.png

86.4 KB
Loading
86.1 KB
Loading

examples/forecasting/iterative.ipynb

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)