-
Notifications
You must be signed in to change notification settings - Fork 329
[ENH] Working draft PR for forecasting #2244
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
Closed
Closed
Changes from 18 commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
aa76e2b
forecaster base and dummy
TonyBagnall 3e026e2
Merge branch 'main' into ajb/forecasting
TonyBagnall 5c231ed
Merge branch 'main' into ajb/forecasting
TonyBagnall a5dbc28
forecasting tests
TonyBagnall 7fee274
forecasting tests
TonyBagnall c4628ca
forecasting tests
TonyBagnall e154078
forecasting tests
TonyBagnall a6043d6
regression
TonyBagnall 5db044b
notebook
TonyBagnall b99af60
Merge branch 'main' into ajb/forecasting
TonyBagnall c39db43
Merge branch 'main' into ajb/forecasting
TonyBagnall 12dc180
Merge branch 'main' into ajb/forecasting
TonyBagnall 7d932a0
regressor
TonyBagnall b89f165
regressor
TonyBagnall 72fe631
regressor
TonyBagnall 80f8bd5
tags
TonyBagnall f4f828f
tags
TonyBagnall aeb8bbb
requires_y
TonyBagnall b1e1f32
Merge branch 'main' into ajb/forecasting
TonyBagnall f820984
forecasting notebook
TonyBagnall 5f37757
forecasting notebook
TonyBagnall 560f664
remove tags
TonyBagnall 8b7555b
Merge branch 'main' into ajb/forecasting
TonyBagnall 765eacd
Merge remote-tracking branch 'origin/main' into ajb/forecasting
MatthewMiddlehurst 413bd28
Merge remote-tracking branch 'origin/main' into ajb/forecasting
MatthewMiddlehurst 26aec53
fix forecasting testing (they still fail though)
MatthewMiddlehurst da1c970
Merge remote-tracking branch 'origin/ajb/forecasting' into ajb/foreca…
MatthewMiddlehurst 47f0559
Merge branch 'main' into ajb/forecasting
TonyBagnall ea385a5
Merge remote-tracking branch 'origin/ajb/forecasting' into ajb/foreca…
MatthewMiddlehurst 54743a0
_is_fitted -> is_fitted
TonyBagnall b9bba0f
_is_fitted -> is_fitted
TonyBagnall 9f8bed6
_forecast
TonyBagnall 63d22ff
notebook
TonyBagnall 107cab7
is_fitted
TonyBagnall 4474593
y_fitted
TonyBagnall 2d28b3a
ETS forecaster
TonyBagnall 87c5969
add y checks and conversion
TonyBagnall afde1f5
add tag
TonyBagnall 5bf2828
tidy
TonyBagnall c292f5d
_check_is_fitted()
TonyBagnall 608558d
_check_is_fitted()
TonyBagnall 5f0a3f5
Add fully functional ETS Forecaster. Modify base to not set default y…
alexbanwell1 d78e695
Merge remote-tracking branch 'origin/main' into ajb/forecasting
MatthewMiddlehurst a8c00e2
Ajb/forecasting (#2357)
alexbanwell1 2f891cf
Merge branch 'main' into ajb/forecasting
TonyBagnall File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """Forecasters.""" | ||
|
|
||
| __all__ = ["DummyForecaster", "BaseForecaster", "RegressionForecaster"] | ||
|
|
||
| from aeon.forecasting._dummy import DummyForecaster | ||
| from aeon.forecasting._regression import RegressionForecaster | ||
| from aeon.forecasting.base import BaseForecaster |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """DummyForecaster always predicts the last value seen in training.""" | ||
|
|
||
| from aeon.forecasting.base import BaseForecaster | ||
|
|
||
|
|
||
| class DummyForecaster(BaseForecaster): | ||
| """Dummy forecaster always predicts the last value seen in training.""" | ||
|
|
||
| def __init__(self): | ||
| """Initialize DummyForecaster.""" | ||
| self.last_value_ = None | ||
| super().__init__() | ||
|
|
||
| def _fit(self, y, exog=None): | ||
| """Fit dummy forecaster.""" | ||
| y = y.squeeze() | ||
| self.last_value_ = y[-1] | ||
| return self | ||
|
|
||
| def _predict(self, y=None, exog=None): | ||
| """Predict using dummy forecaster.""" | ||
| return self.last_value_ | ||
|
|
||
| def _forecast(self, y, exog=None): | ||
| """Forecast using dummy forecaster.""" | ||
| y = y.squeeze() | ||
| return y[-1] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| """Window-based regression forecaster. | ||
|
|
||
| General purpose forecaster to use with any scikit learn or aeon compatible | ||
| regressor. Simply forms a collection of windows from the time series and trains to | ||
| predict the next | ||
| """ | ||
|
|
||
| import numpy as np | ||
| from sklearn.linear_model import LinearRegression | ||
|
|
||
| from aeon.forecasting.base import BaseForecaster | ||
|
|
||
|
|
||
| class RegressionForecaster(BaseForecaster): | ||
| """ | ||
| Regression based forecasting. | ||
|
|
||
| Container for forecaster that reduces forecasting to regression through a | ||
| window. Form a collection of sub series of length `window` through a sliding | ||
| winodw to form X, take `horizon` points ahead to form `y`, then apply an aeon or | ||
| sklearn regressor. | ||
|
|
||
|
|
||
| Parameters | ||
| ---------- | ||
| window : int | ||
| The window prior to the current time point to use in forecasting. So if | ||
| horizon is one, forecaster will train using points $i$ to $window+i-1$ to | ||
| predict value $window+i$. If horizon is 4, forecaster will used points $i$ | ||
| to $window+i-1$ to predict value $window+i+3$. If None, the algorithm will | ||
| internally determine what data to use to predict `horizon` steps ahead. | ||
| horizon : int, default =1 | ||
| The number of time steps ahead to forecast. If horizon is one, the forecaster | ||
| will learn to predict one point ahead | ||
| regressor : object, default =None | ||
| Regression estimator that implements BaseRegressor or is otherwise compatible | ||
| with sklearn regressors. | ||
| """ | ||
|
|
||
| def __init__(self, window, horizon=1, regressor=None): | ||
| self.regressor = regressor | ||
| super().__init__(horizon, axis=1) | ||
|
|
||
| def _fit(self, y, exog=None): | ||
| """Fit forecaster to time series. | ||
|
|
||
| Split X into windows of length window and train the forecaster on each window | ||
| to predict the horizon ahead. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| X : Time series on which to learn a forecaster | ||
|
|
||
| Returns | ||
| ------- | ||
| self | ||
| Fitted estimator | ||
| """ | ||
| # Window data | ||
| if self.regressor is None: | ||
| self.regressor_ = LinearRegression() | ||
| else: | ||
| self.regressor_ = self.regressor | ||
| y = y.squeeze() | ||
| X = np.lib.stride_tricks.sliding_window_view(y, window_shape=self.window) | ||
| # Ignore the final horizon values: need to store these for pred with empty y | ||
| X = X[: -self.horizon] | ||
| # Extract y | ||
| y = y[self.window + self.horizon - 1 :] | ||
| self.last_ = y[-self.window :] | ||
| self.last_ = self.last_.reshape(1, -1) | ||
| self.regressor_.fit(X=X, y=y) | ||
| return self | ||
|
|
||
| def _predict(self, y=None, exog=None): | ||
| """Predict values for time series X.""" | ||
| if y is None: | ||
| return self.regressor_.predict(self.last_) | ||
| last = y[:, -self.window :] | ||
| return self.regressor_.predict(last) | ||
|
|
||
| def _forecast(self, y, exog=None): | ||
| """Forecast values for time series X. | ||
|
|
||
| NOTE: deal with horizons | ||
| """ | ||
| self.fit(y, exog) | ||
| return self.predict(y) | ||
|
|
||
| @classmethod | ||
| def _get_test_params(cls, parameter_set="default"): | ||
| """Return testing parameter settings for the estimator. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| parameter_set : str, default='default' | ||
| Name of the parameter set to return. | ||
|
|
||
| Returns | ||
| ------- | ||
| dict | ||
| Dictionary of testing parameter settings. | ||
| """ | ||
| return {"window": 4} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| """BaseForecaster class. | ||
|
|
||
| A simplified first base class for foreacasting models. The focus here is on a | ||
| specific form of forecasting: longer series, long winodws and single step forecasting. | ||
|
|
||
| aeon enhancement proposal | ||
| https://github.com/aeon-toolkit/aeon-admin/pull/14 | ||
|
|
||
| """ | ||
|
|
||
| from abc import abstractmethod | ||
|
|
||
| from aeon.base import BaseSeriesEstimator | ||
|
|
||
|
|
||
| class BaseForecaster(BaseSeriesEstimator): | ||
| """ | ||
| Abstract base class for time series forecasters. | ||
|
|
||
| The base forecaster specifies the methods and method signatures that all | ||
| forecasters have to implement. Attributes with an underscore suffix are set in the | ||
| method fit. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| horizon : int, default =1 | ||
| The number of time steps ahead to forecast. If horizon is one, the forecaster | ||
| will learn to predict one point ahead | ||
| """ | ||
|
|
||
| _tags = { | ||
| "capability:univariate": True, | ||
| "capability:multivariate": False, | ||
| "capability:missing_values": False, | ||
| "y_inner_type": "np.ndarray", | ||
| "fit_is_empty": False, | ||
| "requires_y": True, | ||
| } | ||
|
|
||
| def __init__(self, horizon=1, axis=1): | ||
| self.horizon = horizon | ||
| self._is_fitted = False | ||
| super().__init__(axis) | ||
|
|
||
| def fit(self, y, exog=None): | ||
| """Fit forecaster to series y. | ||
|
|
||
| Fit a forecaster to predict self.horizon steps ahead using y. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| y : np.ndarray | ||
| A time series on which to learn a forecaster to predict horizon ahead | ||
| exog : np.ndarray, default =None | ||
| Optional exogenous time series data assumed to be aligned with y | ||
|
|
||
| Returns | ||
| ------- | ||
| self | ||
| Fitted BaseForecaster. | ||
| """ | ||
| # Validate y | ||
|
|
||
| # Convert if necessary | ||
| y = self._preprocess_series(y, axis=self.axis, store_metadata=False) | ||
| if exog is not None: | ||
| raise NotImplementedError("Exogenous variables not yet supported") | ||
| # Validate exog | ||
| self._is_fitted = True | ||
| return self._fit(y, exog) | ||
|
|
||
| @abstractmethod | ||
| def _fit(self, y, exog=None): ... | ||
|
|
||
| def predict(self, y=None, 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. | ||
| """ | ||
| if y is not None: | ||
| y = self._preprocess_series(y, axis=self.axis, store_metadata=False) | ||
| if not self._is_fitted: | ||
| raise ValueError("Forecaster must be fitted before predicting") | ||
| if exog is not None: | ||
| raise NotImplementedError("Exogenous variables not yet supported") | ||
| # Validate exog | ||
| self._is_fitted = True | ||
| return self._predict(y, exog) | ||
|
|
||
| @abstractmethod | ||
| def _predict(self, y=None, exog=None): ... | ||
|
|
||
| def forecast(self, y, X=None): | ||
| """ | ||
|
|
||
| Forecast basically fit_predict. | ||
|
|
||
| Returns | ||
| ------- | ||
| np.ndarray | ||
| single prediction directly after the last point in X. | ||
| """ | ||
| y = self._preprocess_series(y, axis=self.axis, store_metadata=False) | ||
| return self._forecast(y, X) | ||
|
|
||
| @abstractmethod | ||
| def _forecast(self, y=None, exog=None): ... | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Forecaster tests.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """Test base forecaster.""" | ||
|
|
||
| import numpy as np | ||
|
|
||
| from aeon.forecasting import DummyForecaster | ||
|
|
||
|
|
||
| def test_base_forecaster(): | ||
| """Test base forecaster functionality.""" | ||
| f = DummyForecaster() | ||
| y = np.random.rand(50) | ||
| f.fit(y) | ||
| p1 = f.predict() | ||
| assert p1 == y[-1] | ||
| p2 = f.forecast(y) | ||
| assert p2 == p1 |
52 changes: 52 additions & 0 deletions
52
aeon/testing/estimator_checking/_yield_forecasting_checks.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| """Tests for all forecasters.""" | ||
|
|
||
| from functools import partial | ||
|
|
||
| import numpy as np | ||
|
|
||
| from aeon.base._base import _clone_estimator | ||
| from aeon.base._base_series import VALID_INNER_TYPES | ||
|
|
||
|
|
||
| def _yield_forecasting_checks(estimator_class, estimator_instances, datatypes): | ||
| """Yield all forecasting checks for an aeon forecaster.""" | ||
| # only class required | ||
| yield partial(check_forecasting_base_functionality, estimator_class=estimator_class) | ||
|
|
||
| # test class instances | ||
| for _, estimator in enumerate(estimator_instances): | ||
| # no data needed | ||
| yield partial(check_forecaster_instance, estimator=estimator) | ||
|
|
||
|
|
||
| def check_forecasting_base_functionality(estimator_class): | ||
| """Test compliance with the base class contract.""" | ||
| # Test they dont override final methods, because python does not enforce this | ||
| assert "fit" not in estimator_class.__dict__ | ||
| assert "predict" not in estimator_class.__dict__ | ||
| assert "forecast" not in estimator_class.__dict__ | ||
| fit_is_empty = estimator_class.get_class_tag(tag_name="fit_is_empty") | ||
| assert not fit_is_empty == "_fit" not in estimator_class.__dict__ | ||
| # Test valid tag for X_inner_type | ||
| X_inner_type = estimator_class.get_class_tag(tag_name="X_inner_type") | ||
| assert X_inner_type in VALID_INNER_TYPES | ||
| # Must have at least one set to True | ||
| multi = estimator_class.get_class_tag(tag_name="capability:multivariate") | ||
| uni = estimator_class.get_class_tag(tag_name="capability:univariate") | ||
| assert multi or uni | ||
|
|
||
|
|
||
| def check_forecaster_instance(estimator): | ||
| """Test forecasters.""" | ||
| estimator = _clone_estimator(estimator) | ||
| pass | ||
| # Sort | ||
| # Check output correct: predict should return a float | ||
| y = np.array([0.5, 0.7, 0.8, 0.9, 1.0]) | ||
| estimator.fit(y) | ||
| p = estimator.predict() | ||
| assert isinstance(p, float) | ||
| # forecast should return a float equal to fit/predict | ||
| p2 = estimator.forecast(y) | ||
| assert p == p2 | ||
| # Add other tests when |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Mock forecasters useful for testing and debugging. | ||
|
|
||
| Used in tests for the forecasting base class. | ||
| """ | ||
|
|
||
| from aeon.forecasting.base import BaseForecaster | ||
|
|
||
|
|
||
| class MockForecaster(BaseForecaster): | ||
| """Mock segmenter for testing.""" | ||
|
TonyBagnall marked this conversation as resolved.
Outdated
|
||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
|
|
||
| def _fit(self, y, X=None): | ||
| return self | ||
|
|
||
| def _predict(self, y): | ||
| return 1.0 | ||
|
|
||
| def _forecast(self, y, X=None): | ||
| return 1.0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.