import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from prophetverse.datasets.loaders import load_tourism
Hierarchical Bayesian Model
In this example, we will show how to forecast panel timeseries with the Prophetverse
model.
The univariate Prophetverse model can seamlessly handle hierarchical timeseries due to the package’s compatibility with sktime.
Import dataset
Here we use the tourism dataset with purpose-level aggregation.
= load_tourism(groupby="Purpose")
y display(y)
Trips | ||
---|---|---|
Purpose | Quarter | |
Business | 1998Q1 | 7391.962068 |
1998Q2 | 7701.153191 | |
1998Q3 | 8911.852065 | |
1998Q4 | 7777.766525 | |
1999Q1 | 6917.257864 | |
... | ... | ... |
__total | 2015Q4 | 51518.858354 |
2016Q1 | 54984.720748 | |
2016Q2 | 49583.595515 | |
2016Q3 | 49392.159616 | |
2016Q4 | 54034.155613 |
380 rows × 1 columns
We define the helper function below to plot the predictions and the observations.
= y.index.get_level_values(0).unique()
LEVELS
def plot_preds(y=None, preds={}, axs=None):
if axs is None:
= plt.subplots(
fig, axs =(12, 8), nrows=int(np.ceil(len(LEVELS) / 2)), ncols=2
figsize
)= iter(axs.flatten())
ax_generator for level in LEVELS:
= next(ax_generator)
ax if y is not None:
0].rename("Observation").plot(
y.loc[level].iloc[:, =ax, label="truth", color="black"
ax
)for name, _preds in preds.items():
0].rename(name).plot(ax=ax, legend=True)
_preds.loc[level].iloc[:,
ax.set_title(level)
# Tight layout
plt.tight_layout()return ax
Automatic upcasting
Because of sktime’s amazing interface, we can use the univariate Prophet seamlessly with hierarchical data.
import jax.numpy as jnp
from prophetverse.effects import LinearFourierSeasonality
from prophetverse.effects.trend import PiecewiseLinearTrend, PiecewiseLogisticTrend
from prophetverse.engine import MAPInferenceEngine, MCMCInferenceEngine
from prophetverse.sktime.univariate import Prophetverse
from prophetverse.utils import no_input_columns
from prophetverse.engine.optimizer import LBFGSSolver
= Prophetverse(
model =PiecewiseLogisticTrend(
trend=0.1,
changepoint_prior_scale=8,
changepoint_interval=-8,
changepoint_range
),=[
exogenous_effects
("seasonality",
LinearFourierSeasonality(=["YE"],
sp_list=[1],
fourier_terms_list="Q",
freq=0.1,
prior_scale="multiplicative",
effect_mode
),
no_input_columns,
)
],=MCMCInferenceEngine(
inference_engine=500,
num_warmup=1000,
num_samples
),
)=y) model.fit(y
Prophetverse(exogenous_effects=[('seasonality', LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', prior_scale=0.1, sp_list=['YE']), '^$')], inference_engine=MCMCInferenceEngine(num_warmup=500), trend=PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8))Please rerun this cell to show the HTML repr or trust the notebook.
Prophetverse(exogenous_effects=[('seasonality', LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', prior_scale=0.1, sp_list=['YE']), '^$')], inference_engine=MCMCInferenceEngine(num_warmup=500), trend=PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8))
PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8)
LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', prior_scale=0.1, sp_list=['YE'])
MCMCInferenceEngine(num_warmup=500)
We can see how, internally, sktime creates clones of the model for each timeseries instance:
model.forecasters_
forecasters | |
---|---|
Business | Prophetverse(exogenous_effects=[('seasonality'... |
Holiday | Prophetverse(exogenous_effects=[('seasonality'... |
Other | Prophetverse(exogenous_effects=[('seasonality'... |
Visiting | Prophetverse(exogenous_effects=[('seasonality'... |
__total | Prophetverse(exogenous_effects=[('seasonality'... |
To call the same methods we used in the univariate case, we do not need to change a single line of code. The only difference is that the output will be a pd.DataFrame with more rows and index levels.
= pd.period_range("1997Q1",
forecast_horizon "2020Q4",
="Q")
freq= model.predict(fh=forecast_horizon)
preds
display(preds.head())
# Plot
"Prophet": preds})
plot_preds(y, { plt.show()
Trips | ||
---|---|---|
Purpose | Quarter | |
Business | 1997Q1 | 7072.360840 |
1997Q2 | 8001.782715 | |
1997Q3 | 8919.611328 | |
1997Q4 | 7981.878418 | |
1998Q1 | 7072.360840 |
The same applies to the decomposition method:
= model.predict_components(fh=forecast_horizon)
decomposition decomposition.head()
mean | obs | seasonality | trend | ||
---|---|---|---|---|---|
Business | 1997Q1 | 7072.360840 | 7078.845703 | -923.445984 | 7995.806641 |
1997Q2 | 8001.782715 | 7961.663086 | 5.976065 | 7995.806641 | |
1997Q3 | 8919.611328 | 8937.083008 | 923.805542 | 7995.806641 | |
1997Q4 | 7981.878418 | 7955.804199 | -13.926888 | 7995.806641 | |
1998Q1 | 7072.360840 | 7057.951660 | -923.445984 | 7995.806641 |
Hierarchical Bayesian model
Sometimes, we want to capture patterns shared between the different series, such as seasonality or trend. In this case, we can use a Bayesian Hierarchical Model.
A Bayesian Hierarchical Model sets hyperpriors: global priors over the priors for each timeseries. These hyperpriors allow us to share information across the different series, which can lead to better forecasts, especially when some series have very few observations.
To do that, we just need to use PanelBHLinearEffect
as the seasonality__linear_effect
parameter in the model. This effect will automatically create a hierarchical model for the linear effects, allowing us to share information across the different series. Also, let us set the broadcast_mode
to “off” to use a single model to all the series.
from prophetverse.effects.linear import PanelBHLinearEffect
from numpyro import distributions as dist
= model.clone()
model_hier
model_hier.set_params(=PanelBHLinearEffect(
seasonality__linear_effect=dist.HalfNormal(0.1)
scale_hyperprior
),="effect",
broadcast_mode
)=y) model_hier.fit(y
Prophetverse(broadcast_mode='effect', exogenous_effects=[('seasonality', LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', linear_effect=PanelBHLinearEffect(scale_hyperprior=<numpyro.distributions.continuous.HalfNormal object at 0x7f967441dc50 with batch shape () and event shape ()>), prior_scale=0.1, sp_list=['YE']), '^$')], inference_engine=MCMCInferenceEngine(num_warmup=500), trend=PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8))Please rerun this cell to show the HTML repr or trust the notebook.
Prophetverse(broadcast_mode='effect', exogenous_effects=[('seasonality', LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', linear_effect=PanelBHLinearEffect(scale_hyperprior=<numpyro.distributions.continuous.HalfNormal object at 0x7f967441dc50 with batch shape () and event shape ()>), prior_scale=0.1, sp_list=['YE']), '^$')], inference_engine=MCMCInferenceEngine(num_warmup=500), trend=PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8))
PiecewiseLogisticTrend(changepoint_interval=8, changepoint_prior_scale=0.1, changepoint_range=-8)
LinearFourierSeasonality(effect_mode='multiplicative', fourier_terms_list=[1], freq='Q', linear_effect=PanelBHLinearEffect(scale_hyperprior=<numpyro.distributions.continuous.HalfNormal object at 0x7f967441dc50 with batch shape () and event shape ()>), prior_scale=0.1, sp_list=['YE'])
PanelBHLinearEffect(scale_hyperprior=<numpyro.distributions.continuous.HalfNormal object at 0x7f967441dc50 with batch shape () and event shape ()>)
PanelBHLinearEffect(scale_hyperprior=<numpyro.distributions.continuous.HalfNormal object at 0x7f967441dc50 with batch shape () and event shape ()>)
MCMCInferenceEngine(num_warmup=500)
= model_hier.predict(fh=forecast_horizon)
preds_hier
plot_preds(
y,={
preds"Prophet": preds,
"HierarchicalProphet": preds_hier,
}, )