TBATS: trigonometric seasonal exponential smoothing with Box-Cox and ARMA errors.
Classes
T\ rigonometric seasonality, B\ ox-Cox transform, A\ RMA errors, T\ rend and S\ easonal components -- an exponential smoothing state-space model for series with complex, multiple, high frequency or non-integer seasonal periods.
seasonal_periods=[7, 365.25] both representable and cheap.__init__( self, seasonal_periods: Optional[Sequence[float] | float] = None, n_harmonics: Optional[Sequence[int] | int] = None, use_trend: bool = True, damped_trend: bool = True, box_cox: bool = False, box_cox_lambda: Optional[float] = None, use_arma_errors: bool = True, arma_order: Tuple[int, int] = (), maxiter: int = 40, tol: float = 1e-06, )
Overview
Fitting proceeds as follows:
- Optionally Box-Cox transform the series to stabilise the variance,
- Seed the level, trend and seasonal states by ordinary least squares
[1, t, cos/sin harmonics], which gives the recursion a starting point already close to the data.
- Run the state-space smoothing recursion, updating level, damped trend
- Minimise the sum of squared one-step errors over the smoothing
- Fit ARMA errors to the residual series and add their forecast.
- Forecast by iterating the recursion with zero errors and inverting
Theory
With y_t^{(\lambda)} the Box-Cox transformed observation, the model is
where \phi is the damping parameter and d_t is an ARMA error process. Each seasonal component is the sum of k_i harmonic pairs that rotate at the seasonal frequencies \lambda^{(i)}_j = 2\pi j / m_i:
with s^{(i)}_t = \sum_{j=1}^{k_i} s^{(i)}_{j,t}. Because m_i enters only through the angle 2\pi j/m_i, it need not be an integer -- 365.25 is as valid as 12. The Box-Cox transform is
Parameters
seasonal_periods
365.25) and there may be several ([7, 365.25]). None disables seasonality.
n_harmonics
min(floor(m / 2), 5) for each period, which keeps the state small for very long periods.
use_trend
damped_trend
use_trend is False.
box_cox
box_cox_lambda
box_cox is True, :math:`\\lambda` is chosen from a small grid by profile likelihood.
use_arma_errors
arma_order
(p, q) order of the residual ARMA model.
maxiter
tol
Attributes
params_
alpha, beta, phi and the per-period gamma1/gamma2.
lambda_
box_cox is False.
harmonics_
level_
trend_
use_trend is False).
seasonal_
ar_params_
ma_params_
fitted_values_
resid_
sse_
aic_
sse_.
n_obs_
fit.
Notes
Complexity:
- Training: O(\text{maxiter} \cdot k \cdot n K) where
- Prediction: O(h K) for h steps.
- Multiple simultaneous seasonalities -- daily plus weekly plus yearly.
- Non-integer periods such as 365.25 (leap years) or 52.18 (weeks
- High-frequency seasonality where one state per seasonal index
- Multiplicative-looking variance that a Box-Cox transform can tame.
- The ARMA error stage is estimated in a second pass (Hannan-Rissanen
- Model selection over the discrete choices (trend on/off, damping
- Estimation minimises the sum of squared errors rather than the exact
Seasonality here is additive on the (optionally Box-Cox transformed) scale, which is the standard TBATS formulation -- multiplicative behaviour is obtained through the transform, not through a separate multiplicative seasonal form.
References
See Also
>>> import numpy as np
>>> from tuiml.algorithms.timeseries.tbats import TBATS
>>> t = np.arange(120)
>>> y = 10.0 + 0.05 * t + 3 * np.sin(2 * np.pi * t / 12)
>>> model = TBATS(seasonal_periods=[12]).fit(y)
>>> forecast = model.predict(steps=12)
>>> truth = 10.0 + 0.05 * np.arange(120, 132) + 3 * np.sin(
... 2 * np.pi * np.arange(120, 132) / 12)
>>> bool(np.mean(np.abs(forecast - truth)) < 0.2)
True
A non-integer period is handled exactly like an integer one:
>>> t = np.arange(200)
>>> y = 5.0 + 2 * np.sin(2 * np.pi * t / 52.18)
>>> model = TBATS(seasonal_periods=52.18).fit(y)
>>> model.predict(steps=4).shape
(4,)
>>> model.harmonics_
[5]
Methods
fit
(self, y: np.ndarray, X: Optional[np.ndarray]=None) -> 'TBATS'
fit
(self, y: np.ndarray, X: Optional[np.ndarray]=None) -> 'TBATS'
Fit the TBATS model to a time series.
Parameters
y
X
SARIMAX when exogenous regressors are needed.
Returns
self
predict
(self, steps: int=1, X: Optional[np.ndarray]=None) -> np.ndarray
predict
(self, steps: int=1, X: Optional[np.ndarray]=None) -> np.ndarray
Forecast future values.
Parameters
steps
X
Returns
forecast