[FEAT] StatsForecast SeasonalNaive forecasting model (#3340)

* feat: add seasonalnaive model

* feat: update seasonalnaive usage example

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

View File

@@ -56,6 +56,7 @@ from openbb_terminal.forecast import (
forecast_model,
forecast_view,
autoets_view,
seasonalnaive_view,
expo_model,
expo_view,
linregr_view,
@@ -111,6 +112,7 @@ class ForecastController(BaseController):
"atr",
"signal",
"autoets",
"seasonalnaive",
"expo",
"theta",
"rnn",
@@ -249,6 +251,7 @@ class ForecastController(BaseController):
"combine",
"rename",
"autoets",
"seasonalnaive",
"expo",
"theta",
"rnn",
@@ -328,6 +331,7 @@ class ForecastController(BaseController):
mt.add_raw("\n")
mt.add_info("_tsforecasting_")
mt.add_cmd("autoets", self.files)
mt.add_cmd("seasonalnaive", self.files)
mt.add_cmd("expo", self.files)
mt.add_cmd("theta", self.files)
mt.add_cmd("linregr", self.files)
@@ -1742,6 +1746,61 @@ class ForecastController(BaseController):
export_pred_raw=ns_parser.export_pred_raw,
)
# SeasonalNaive Model
@log_start_end(log=logger)
def call_seasonalnaive(self, other_args: List[str]):
"""Process seasonalnaive command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
prog="seasonalnaive",
description="""
Perform SeasonalNaive forecasting:
https://nixtla.github.io/statsforecast/models.html#seasonalnaive
""",
)
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
seasonalnaive_view.display_seasonalnaive_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,
)
# EXPO Model
@log_start_end(log=logger)
def call_expo(self, other_args: List[str]):

View File

@@ -0,0 +1,146 @@
# pylint: disable=too-many-arguments
"""Seasonal Naive 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 SeasonalNaive
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_seasonalnaive_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 Seasonal Naive forecasting
This is a wrapper around StatsForecast Seasonal Naive;
we refer to this link for the original and more complete documentation of the parameters.
https://nixtla.github.io/statsforecast/models.html#seasonalnaive
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 SeasonalNaive 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 = SeasonalNaive(
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["SeasonalNaive"].values
precision = helpers.mean_absolute_percentage_error(y_true, y_hat)
console.print(f"SeasonalNaive 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={"SeasonalNaive": target_column}),
target_column,
is_scaler=use_scalers,
time_col="ds",
)
_, historical_fcast = helpers.get_series(
historical_fcast.groupby("ds")
.head(1)
.rename(columns={"SeasonalNaive": 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 @@
"""SeasonalNaive 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 seasonalnaive_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_seasonalnaive_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 SeasonalNaive 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,
) = seasonalnaive_model.get_seasonalnaive_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="SeasonalNaive",
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

@@ -1050,6 +1050,7 @@ en:
forecast/rnn: Probabilistic Recurrent Neural Network (RNN, LSTM, GRU)
forecast/brnn: Block Recurrent Neural Network (RNN, LSTM, GRU) (feat. Past covariates)
forecast/nbeats: Neural Bayesian Estimation (feat. Past covariates)
forecast/seasonalnaive: Seasonal Naive Model
forecast/tcn: Temporal Convolutional Neural Network (feat. Past covariates)
forecast/trans: Transformer Network (feat. Past covariates)
forecast/tft: Temporal Fusion Transformer Network(feat. Past covariates)

View File

@@ -1993,6 +1993,10 @@ forecast_extras = {
"model": "openbb_terminal.forecast.autoets_model.get_autoets_data",
"view": "openbb_terminal.forecast.autoets_view.display_autoets_forecast",
},
"forecast.seasonalnaive": {
"model": "openbb_terminal.forecast.seasonalnaive_model.get_seasonalnaive_data",
"view": "openbb_terminal.forecast.seasonalnaive_view.display_seasonalnaive_forecast",
},
"forecast.expo": {
"model": "openbb_terminal.forecast.expo_model.get_expo_data",
"view": "openbb_terminal.forecast.expo_view.display_expo_forecast",

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
```
usage: seasonalnaive [--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 SeasonalNaive forecasting: https://nixtla.github.io/statsforecast/models.html#seasonalnaive
```
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 seasonalnaive' to access the related guide.
```
Example:
```
2022 Nov 07, 19:24 (🦋) /stocks/ $ forecast
2022 Nov 07, 19:24 (🦋) /forecast/ $ seasonalnaive AAPL
Cross Validation Time Series 1: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 115/115 [00:02<00:00, 47.75it/s]
Forecast: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 14873.42it/s]
SeasonalNaive obtains MAPE: 4.26%
Actual price: 138.38
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Datetime ┃ Prediction ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 2022-11-07 │ 138.38 │
├────────────┼────────────┤
│ 2022-11-08 │ 144.80 │
├────────────┼────────────┤
│ 2022-11-09 │ 155.74 │
├────────────┼────────────┤
│ 2022-11-10 │ 153.34 │
├────────────┼────────────┤
│ 2022-11-11 │ 150.65 │
└────────────┴────────────┘
```
<img width="1363" alt="image" src="https://user-images.githubusercontent.com/10517170/200444390-4a7b2420-37bc-4f01-83d0-d89d2ca74fd7.png">

View File

@@ -791,6 +791,8 @@ main:
ref: terminal/forecast/rsi
- name: season
ref: terminal/forecast/season
- name: seasonalnaive
ref: terminal/forecast/seasonalnaive
- name: show
ref: terminal/forecast/show
- name: signal