[FEAT] StatsForecast AutoCES forecasting model (#3337)

* feat: add autoces model

* feat: update autoces example

* fix: remove unused file

Co-authored-by: martinb-bb <105685594+martinb-bb@users.noreply.github.com>
This commit is contained in:
fede
2022-11-08 14:27:12 -06:00
committed by GitHub
parent de0125076e
commit a4e5e436d5
9 changed files with 419 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
# pylint: disable=too-many-arguments
"""Automatic Comples Exponential Smoothing Model"""
__docformat__ = "numpy"
import logging
from typing import Any, Union, Optional, List, Tuple
import warnings
import numpy as np
import pandas as pd
from statsforecast.models import AutoCES
from statsforecast.core import StatsForecast
from openbb_terminal.decorators import log_start_end
from openbb_terminal.rich_config import console
from openbb_terminal.forecast import helpers
warnings.simplefilter("ignore")
logger = logging.getLogger(__name__)
# pylint: disable=E1123
@log_start_end(log=logger)
def get_autoces_data(
data: Union[pd.Series, pd.DataFrame],
target_column: str = "close",
seasonal_periods: int = 7,
n_predict: int = 5,
start_window: float = 0.85,
forecast_horizon: int = 5,
) -> Tuple[list[np.ndarray], List[np.ndarray], List[np.ndarray], Optional[float], Any]:
"""Performs Automatic Complex Exponential Smoothing forecasting
This is a wrapper around StatsForecast AutoCES;
we refer to this link for the original and more complete documentation of the parameters.
https://nixtla.github.io/statsforecast/models.html#autoces
Parameters
----------
data : Union[pd.Series, np.ndarray]
Input data.
target_column (str, optional):
Target column to forecast. Defaults to "close".
seasonal_periods: int
Number of seasonal periods in a year (7 for daily data)
If not set, inferred from frequency of the series.
n_predict: int
Number of days to forecast
start_window: float
Size of sliding window from start of timeseries and onwards
forecast_horizon: int
Number of days to forecast when backtesting and retraining historical
Returns
-------
list[float]
Adjusted Data series
list[float]
List of historical fcast values
list[float]
List of predicted fcast values
Optional[float]
precision
Any
Fit CES model object.
"""
use_scalers = False
# statsforecast preprocessing
# when including more time series
# the preprocessing is similar
_, ticker_series = helpers.get_series(data, target_column, is_scaler=use_scalers)
freq = ticker_series.freq_str
ticker_series = ticker_series.pd_dataframe().reset_index()
ticker_series.columns = ["ds", "y"]
ticker_series.insert(0, "unique_id", target_column)
try:
# Model Init
model = AutoCES(
season_length=int(seasonal_periods),
)
fcst = StatsForecast(df=ticker_series, models=[model], freq=freq, verbose=True)
except Exception as e: # noqa
error = str(e)
if "got an unexpected keyword argument" in error:
console.print(
"[red]Please update statsforecast to version 1.1.3 or higher.[/red]"
)
else:
console.print(f"[red]{error}[/red]")
return [], [], [], None, None
# Historical backtesting
last_training_point = int((len(ticker_series) - 1) * start_window)
historical_fcast = fcst.cross_validation(
h=int(forecast_horizon),
test_size=len(ticker_series) - last_training_point,
n_windows=None,
input_size=min(10 * forecast_horizon, len(ticker_series)),
)
# train new model on entire timeseries to provide best current forecast
# we have the historical fcast, now lets predict.
forecast = fcst.forecast(int(n_predict))
y_true = historical_fcast["y"].values
y_hat = historical_fcast["CES"].values
precision = helpers.mean_absolute_percentage_error(y_true, y_hat)
console.print(f"AutoCES obtains MAPE: {precision:.2f}% \n")
# transform outputs to make them compatible with
# plots
use_scalers = False
_, ticker_series = helpers.get_series(
ticker_series.rename(columns={"y": target_column}),
target_column,
is_scaler=use_scalers,
time_col="ds",
)
_, forecast = helpers.get_series(
forecast.rename(columns={"CES": target_column}),
target_column,
is_scaler=use_scalers,
time_col="ds",
)
_, historical_fcast = helpers.get_series(
historical_fcast.groupby("ds").head(1).rename(columns={"CES": target_column}),
target_column,
is_scaler=use_scalers,
time_col="ds",
)
return (
ticker_series,
historical_fcast,
forecast,
precision,
fcst,
)

View File

@@ -0,0 +1,117 @@
"""Automatic Complex Exponential Smoothing View"""
__docformat__ = "numpy"
import logging
from typing import Union, Optional, List
from datetime import datetime
import pandas as pd
import matplotlib.pyplot as plt
from openbb_terminal.forecast import autoces_model
from openbb_terminal.decorators import log_start_end
from openbb_terminal.forecast import helpers
logger = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
@log_start_end(log=logger)
def display_autoces_forecast(
data: Union[pd.DataFrame, pd.Series],
target_column: str = "close",
dataset_name: str = "",
seasonal_periods: int = 7,
n_predict: int = 5,
start_window: float = 0.85,
forecast_horizon: int = 5,
export: str = "",
residuals: bool = False,
forecast_only: bool = False,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
naive: bool = False,
export_pred_raw: bool = False,
external_axes: Optional[List[plt.axes]] = None,
):
"""Display Automatic Complex Exponential Smoothing Model
Parameters
----------
data : Union[pd.Series, np.array]
Data to forecast
dataset_name str
The name of the ticker to be predicted
target_column (str, optional):
Target column to forecast. Defaults to "close".
seasonal_periods: int
Number of seasonal periods in a year
If not set, inferred from frequency of the series.
n_predict: int
Number of days to forecast
start_window: float
Size of sliding window from start of timeseries and onwards
forecast_horizon: int
Number of days to forecast when backtesting and retraining historical
export: str
Format to export data
residuals: bool
Whether to show residuals for the model. Defaults to False.
forecast_only: bool
Whether to only show dates in the forecasting range. Defaults to False.
start_date: Optional[datetime]
The starting date to perform analysis, data before this is trimmed. Defaults to None.
end_date: Optional[datetime]
The ending date to perform analysis, data after this is trimmed. Defaults to None.
naive: bool
Whether to show the naive baseline. This just assumes the closing price will be the same
as the previous day's closing price. Defaults to False.
external_axes:Optional[List[plt.axes]]
External axes to plot on
"""
data = helpers.clean_data(data, start_date, end_date, target_column, None)
if not helpers.check_data(data, target_column, None):
return
(
ticker_series,
historical_fcast,
predicted_values,
precision,
_model,
) = autoces_model.get_autoces_data(
data=data,
target_column=target_column,
seasonal_periods=seasonal_periods,
n_predict=n_predict,
start_window=start_window,
forecast_horizon=forecast_horizon,
)
if ticker_series == []:
return
probabilistic = False
helpers.plot_forecast(
name="AutoCES",
target_col=target_column,
historical_fcast=historical_fcast,
predicted_values=predicted_values,
ticker_series=ticker_series,
ticker_name=dataset_name,
data=data,
n_predict=n_predict,
forecast_horizon=forecast_horizon,
past_covariates=None,
precision=precision,
probabilistic=probabilistic,
export=export,
forecast_only=forecast_only,
naive=naive,
export_pred_raw=export_pred_raw,
external_axes=external_axes,
)
if residuals:
helpers.plot_residuals(
_model, None, ticker_series, forecast_horizon=forecast_horizon
)

View File

@@ -55,6 +55,7 @@ from openbb_terminal.rich_config import console, MenuText
from openbb_terminal.forecast import (
forecast_model,
forecast_view,
autoces_view,
autoets_view,
seasonalnaive_view,
expo_model,
@@ -111,6 +112,7 @@ class ForecastController(BaseController):
"delta",
"atr",
"signal",
"autoces",
"autoets",
"seasonalnaive",
"expo",
@@ -250,6 +252,7 @@ class ForecastController(BaseController):
"signal",
"combine",
"rename",
"autoces",
"autoets",
"seasonalnaive",
"expo",
@@ -330,6 +333,7 @@ class ForecastController(BaseController):
mt.add_cmd("signal", self.files)
mt.add_raw("\n")
mt.add_info("_tsforecasting_")
mt.add_cmd("autoces", self.files)
mt.add_cmd("autoets", self.files)
mt.add_cmd("seasonalnaive", self.files)
mt.add_cmd("expo", self.files)
@@ -1691,6 +1695,61 @@ class ForecastController(BaseController):
ns_parser.target_dataset,
)
# AutoCES Model
@log_start_end(log=logger)
def call_autoces(self, other_args: List[str]):
"""Process autoces command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
prog="autoces",
description="""
Perform Automatic Complex Exponential Smoothing forecast:
https://nixtla.github.io/statsforecast/models.html#autoces
""",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "--target-dataset")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
export_allowed=EXPORT_ONLY_FIGURES_ALLOWED,
target_dataset=True,
target_column=True,
n_days=True,
seasonal="A",
periods=True,
window=True,
residuals=True,
forecast_only=True,
start=True,
end=True,
naive=True,
export_pred_raw=True,
)
# TODO Convert this to multi series
if ns_parser:
if not helpers.check_parser_input(ns_parser, self.datasets):
return
autoces_view.display_autoces_forecast(
data=self.datasets[ns_parser.target_dataset],
dataset_name=ns_parser.target_dataset,
n_predict=ns_parser.n_days,
target_column=ns_parser.target_column,
seasonal_periods=ns_parser.seasonal_periods,
start_window=ns_parser.start_window,
forecast_horizon=ns_parser.n_days,
export=ns_parser.export,
residuals=ns_parser.residuals,
forecast_only=ns_parser.forecast_only,
start_date=ns_parser.s_start_date,
end_date=ns_parser.s_end_date,
naive=ns_parser.naive,
export_pred_raw=ns_parser.export_pred_raw,
)
# AutoETS Model
@log_start_end(log=logger)
def call_autoets(self, other_args: List[str]):

View File

@@ -1041,6 +1041,7 @@ en:
forecast/atr: Add Average True Range
forecast/signal: Add Price Signal (short vs. long term)
forecast/_tsforecasting_: TimeSeries Forecasting
forecast/autoces: Automatic Complex Exponential Smoothing Model
forecast/autoets: Automatic ETS (Error, Trend, Seasonality) Model
forecast/arima: Arima (Non-darts)
forecast/expo: Probabilistic Exponential Smoothing

View File

@@ -1989,6 +1989,10 @@ forecast_extras = {
"forecast.roc": {"model": "openbb_terminal.forecast.forecast_model.add_roc"},
"forecast.mom": {"model": "openbb_terminal.forecast.forecast_model.add_momentum"},
"forecast.delta": {"model": "openbb_terminal.forecast.forecast_model.add_delta"},
"forecast.autoces": {
"model": "openbb_terminal.forecast.autoces_model.get_autoces_data",
"view": "openbb_terminal.forecast.autoces_view.display_autoces_forecast",
},
"forecast.autoets": {
"model": "openbb_terminal.forecast.autoets_model.get_autoets_data",
"view": "openbb_terminal.forecast.autoets_view.display_autoets_forecast",

View File

@@ -0,0 +1,11 @@
import pytest
from tests.openbb_terminal.forecast import conftest
try:
from openbb_terminal.forecast import autoces_model
except ImportError:
pytest.skip(allow_module_level=True)
def test_get_autoces_model(tsla_csv):
conftest.test_model(autoces_model.get_autoces_data, tsla_csv)

View File

@@ -0,0 +1,18 @@
import pytest
try:
from openbb_terminal.forecast import autoces_view
except ImportError:
pytest.skip(allow_module_level=True)
def test_display_ces_forecast(tsla_csv):
with pytest.raises(AttributeError):
autoces_view.display_autoces_forecast(
tsla_csv,
target_column="close",
seasonal_periods=3,
n_predict=1,
start_window=0.5,
forecast_horizon=1,
)

View File

@@ -0,0 +1,63 @@
```
usage: autoces [--naive] [-d {AAPL}] [-c TARGET_COLUMN] [-n N_DAYS] [-s {N,A,M}] [-p SEASONAL_PERIODS] [-w START_WINDOW] [--end S_END_DATE] [--start S_START_DATE] [--residuals] [--forecast-only]
[--export-pred-raw] [-h] [--export EXPORT]
```
Perform Automatic Complex Exponential Smoothing forecast: https://nixtla.github.io/statsforecast/models.html#autoces
```
optional arguments:
--naive Show the naive baseline for a model. (default: False)
-d {AAPL}, --target-dataset {AAPL}
The name of the dataset you want to select (default: None)
-c TARGET_COLUMN, --target-column TARGET_COLUMN
The name of the specific column you want to use (default: close)
-n N_DAYS, --n-days N_DAYS
prediction days. (default: 5)
-s {N,A,M}, --seasonal {N,A,M}
Seasonality: N: None, A: Additive, M: Multiplicative. (default: A)
-p SEASONAL_PERIODS, --periods SEASONAL_PERIODS
Seasonal periods: 4: Quarterly, 7: Daily (default: 7)
-w START_WINDOW, --window START_WINDOW
Start point for rolling training and forecast window. 0.0-1.0 (default: 0.85)
--end S_END_DATE The end date (format YYYY-MM-DD) to select for testing (default: None)
--start S_START_DATE The start date (format YYYY-MM-DD) to select for testing (default: None)
--residuals Show the residuals for the model. (default: False)
--forecast-only Do not plot the hisotorical data without forecasts. (default: False)
--export-pred-raw Export predictions to a csv file. (default: False)
-h, --help show this help message (default: False)
--export EXPORT Export figure into png, jpg, pdf, svg (default: )
For more information and examples, use 'about autoces' to access the related guide.
```
Example:
```
2022 Nov 07, 16:42 (🦋) /stocks/ $ forecast
2022 Nov 07, 16:42 (🦋) /forecast/ $ autoces AAPL
Cross Validation Time Series 1: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 115/115 [00:13<00:00, 8.70it/s]
Forecast: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 3.23it/s]
AutoCES obtains MAPE: 3.09%
Actual price: 138.38
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Datetime ┃ Prediction ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 2022-11-07 │ 138.70 │
├────────────┼────────────┤
│ 2022-11-08 │ 138.53 │
├────────────┼────────────┤
│ 2022-11-09 │ 139.16 │
├────────────┼────────────┤
│ 2022-11-10 │ 138.91 │
├────────────┼────────────┤
│ 2022-11-11 │ 139.01 │
└────────────┴────────────┘
```
<img width="1370" alt="image" src="https://user-images.githubusercontent.com/10517170/200422580-549f6a02-4869-4d80-bb78-72c2f9d45d79.png">

View File

@@ -745,6 +745,8 @@ main:
sub:
- name: atr
ref: terminal/forecast/atr
- name: autoces
ref: terminal/forecast/autoces
- name: autoets
ref: terminal/forecast/autoets
- name: brnn