TBATS: Trigonometric Seasonal Exponential Smoothing
TBATS (Trigonometric seasonality, Box-Cox transformation, ARMA errors, Trend, and Seasonal components) extends BATS by using a Fourier representation for seasonal components, enabling the model to handle:
- Non-integer seasonal periods (e.g., 52.18 weeks per year)
- Very long seasonal cycles (hundreds or thousands of periods)
- Multiple complex seasonalities (daily + weekly + yearly)
- Dual calendar effects (e.g., Hijri + Gregorian calendars)
This implementation is a pure Julia port of the R forecast::tbats function based on De Livera, Hyndman & Snyder (2011).
Mathematical Framework
1. Box-Cox Transformation
The model begins with an optional Box-Cox transformation to stabilize variance:
\[y_t^{(\omega)} = \begin{cases} \frac{y_t^\omega - 1}{\omega}, & \omega \neq 0 \\[6pt] \ln(y_t), & \omega = 0 \end{cases}\]
where $\omega \in [0, 1]$ is the Box-Cox parameter estimated from the data.
2. TBATS State Space Model
The TBATS model is represented in innovations form with the following components:
Observation Equation
\[y_t^{(\omega)} = \ell_{t-1} + \phi b_{t-1} + \sum_{i=1}^{T} s_{i,t-1} + d_t\]
where:
\[\ell_t\]
is the local level\[b_t\]
is the trend component\[\phi\]
is the damping parameter ($0 < \phi \leq 1$)\[s_{i,t}\]
is the $i$-th seasonal component\[d_t\]
is the ARMA error term\[T\]
is the number of seasonal periods
State Equations
Local Level:
\[\ell_t = \ell_{t-1} + \phi b_{t-1} + \alpha d_t\]
where $\alpha$ is the level smoothing parameter.
Trend Component:
\[b_t = \phi b_{t-1} + \beta d_t\]
where $\beta$ is the trend smoothing parameter and $\phi$ controls damping.
Trigonometric Seasonal Components:
For each seasonal period $m_i$ with $k_i$ Fourier harmonics:
\[s_{i,t} = \sum_{j=1}^{k_i} \left[ s_{i,j,t}^{(1)} \cos(\lambda_{i,j} t) + s_{i,j,t}^{(2)} \sin(\lambda_{i,j} t) \right]\]
where $\lambda_{i,j} = \frac{2\pi j}{m_i}$ is the $j$-th harmonic frequency.
Harmonic State Evolution:
Each harmonic pair evolves via a rotation matrix:
\[\begin{pmatrix} s_{i,j,t}^{(1)} \\ s_{i,j,t}^{(2)} \end{pmatrix} = \begin{pmatrix} \cos\lambda_{i,j} & \sin\lambda_{i,j} \\ -\sin\lambda_{i,j} & \cos\lambda_{i,j} \end{pmatrix} \begin{pmatrix} s_{i,j,t-1}^{(1)} \\ s_{i,j,t-1}^{(2)} \end{pmatrix} + \begin{pmatrix} \gamma_{1,i} \\ \gamma_{2,i} \end{pmatrix} d_t\]
where $\gamma_{1,i}$ and $\gamma_{2,i}$ are smoothing parameters for the seasonal component.
ARMA Error Component:
\[d_t = \sum_{k=1}^p \varphi_k d_{t-k} + \varepsilon_t + \sum_{\ell=1}^q \theta_\ell \varepsilon_{t-\ell}\]
where:
\[p\]
is the AR order\[q\]
is the MA order\[\varphi_k\]
are AR coefficients\[\theta_\ell\]
are MA coefficients\[\varepsilon_t \sim \mathcal{N}(0, \sigma^2)\]
3. Key Advantages Over BATS
| Feature | BATS | TBATS |
|---|---|---|
| Seasonal periods | Integer only | Non-integer allowed |
| State dimension | $m_i$ states per season | $2k_i$ states per season |
| Max seasonal period | ~350 (computational limit) | Unlimited (via Fourier) |
| Dual calendars | Not feasible | Fully supported |
| Storage | $O(m)$ | $O(k)$, typically $k \ll m$ |
Example: For weekly seasonality ($m = 52$), BATS needs 52 states, while TBATS with $k=2$ harmonics needs only 4 states.
4. Forecasting
Future states evolve deterministically (errors set to zero):
Level:
\[\ell_{t+h} = \ell_t + \sum_{u=1}^h \phi^{u-1} b_t\]
Trend:
\[b_{t+h} = \phi^h b_t\]
Seasonal Components:
The harmonic pairs rotate forward via matrix powers:
\[\begin{pmatrix} s_{i,j,t+h}^{(1)} \\ s_{i,j,t+h}^{(2)} \end{pmatrix} = R(\lambda_{i,j})^h \begin{pmatrix} s_{i,j,t}^{(1)} \\ s_{i,j,t}^{(2)} \end{pmatrix}\]
where $R(\lambda)$ is the rotation matrix.
Point Forecast (Transformed Scale):
\[\hat{y}_{t+h}^{(\omega)} = \ell_{t+h} + \phi b_{t+h} + \sum_{i=1}^T s_{i,t+h}\]
The forecast is then back-transformed via inverse Box-Cox:
\[\hat{y}_{t+h} = \begin{cases} (\omega \hat{y}_{t+h}^{(\omega)} + 1)^{1/\omega}, & \omega \neq 0 \\[6pt] \exp(\hat{y}_{t+h}^{(\omega)}), & \omega = 0 \end{cases}\]
Forecast Variance:
The $h$-step ahead forecast variance is:
\[\text{Var}(\hat{y}_{t+h}) = \sigma^2 \sum_{j=0}^{h-1} c_j^2\]
where $c_j$ depends on the model's transition matrix $F$ and error vector $g$.
Julia Implementation
Basic Usage
using Durbyn
y = rand(100)
m = [7, 365.25]
model = tbats(y, m)
fc = forecast(model, h=20)Function Signature
tbats(
y::AbstractVector{<:Real},
m::Union{Vector{Int}, Nothing} = nothing;
use_box_cox::Union{Bool, AbstractVector{Bool}, Nothing} = nothing,
use_trend::Union{Bool, AbstractVector{Bool}, Nothing} = nothing,
use_damped_trend::Union{Bool, AbstractVector{Bool}, Nothing} = nothing,
use_arma_errors::Bool = true,
bc_lower::Real = 0.0,
bc_upper::Real = 1.0,
biasadj::Bool = false,
model = nothing
)Parameters
y: Univariate time series (1D vector)m: Vector of seasonal periods (can be non-integer). Usenothingfor non-seasonal models.use_box_cox: Whether to use Box-Cox transformationnothing(default): tries both and selects by AICtrue/false: forces the choice- Vector of bools: tries each option
use_trend: Whether to include trend componentnothing(default): tries bothtrue/false: forces the choice
use_damped_trend: Whether to damp the trendnothing(default): tries bothtrue/false: forces the choice- Ignored if
use_trend=false
use_arma_errors: Whether to model residuals with ARMAbc_lower,bc_upper: Bounds for Box-Cox parameter searchbiasadj: Use bias-adjusted back-transformation for forecastsmodel: Previously fitted TBATS model to refit
Returns
A TBATSModel struct containing:
lambda: Box-Cox parameter (ornothing)alpha: Level smoothing parameterbeta: Trend smoothing parameter (ornothing)damping_parameter: Damping parameter φ (ornothing)gamma_one_values: First seasonal smoothing parametersgamma_two_values: Second seasonal smoothing parametersar_coefficients: AR coefficients (ornothing)ma_coefficients: MA coefficients (ornothing)seasonal_periods: Vector of seasonal periodsk_vector: Vector of Fourier orders per seasonal periodfitted_values: In-sample fitted valueserrors: Residualsx: State matrixseed_states: Initial statesvariance: Residual varianceAIC: Akaike Information Criterionlikelihood: Log-likelihoody: Original time seriesmethod: Model descriptor string
Model Selection
The implementation automatically selects:
Fourier orders ($k_i$) for each seasonal period via AIC
- Starts with $k=1$
- For small periods ($m \leq 12$): searches downward from $k = \lfloor(m-1)/2\rfloor$
- For large periods ($m > 12$): uses step-up/step-down search around $k \in \{5,6,7\}$
ARMA orders using
auto_arimaon residualsBox-Cox parameter via profile likelihood
Trend and damping via AIC comparison
The descriptor format matches R's forecast::tbats:
TBATS(omega, {p,q}, phi, <m1,k1>, <m2,k2>, ...)Example: TBATS(0.001, {0,0}, 0.98, <7,3>, <365.25,10>)
Examples
Example 1: Weekly Data with Non-Integer Seasonality
using Durbyn
y = randn(520)
m = 52.18
model = tbats(y, m)
println(model)
fc = forecast(model, h=52)Output:
TBATS(0.112, {0,0}, -, <52.18,5>)
Parameters:
Lambda: 0.1120
Alpha: 0.0823
Gamma-1: 0.0045, 0.0032, 0.0019, 0.0011, 0.0007
Gamma-2: 0.0051, 0.0036, 0.0024, 0.0015, 0.0009
Sigma: 0.9876
AIC: 1523.45Example 2: Multiple Seasonalities (Daily + Weekly + Yearly)
using Durbyn, Dates
n = 365 * 3
t = 1:n
y = 100 .+ 10 .* sin.(2π .* t ./ 365.25) .+ # yearly
5 .* sin.(2π .* t ./ 7) .+ # weekly
2 .* randn(n) # noise
model = tbats(y, [7, 365.25])
println(model)
fc = forecast(model, h=30)Example 3: Dual Calendar Effects (Gregorian + Hijri)
using Durbyn
m_gregorian = 365.25
m_hijri = 354.37
y = load_data() # Data with both calendar effects
model = tbats(y, [m_gregorian, m_hijri])
println(model)Output:
TBATS(0.234, {1,1}, 0.975, <365.25,8>, <354.37,7>)Example 4: Forcing Model Components
using Durbyn
y = randn(200)
m = [7, 30]
model = tbats(
y, m;
use_box_cox = false, # No transformation
use_trend = true, # Force trend
use_damped_trend = true, # Force damping
use_arma_errors = false # No ARMA errors
)Example 5: High-Frequency Data (5-minute intervals)
using Durbyn
minutes_per_day = 288 # 24 * 60 / 5
minutes_per_week = 2016 # 7 * 288
y = load_call_center_data()
model = tbats(y, [minutes_per_day, minutes_per_week])
println("State dimension: ", size(model.x, 1))
println("Seasonal periods: ", model.seasonal_periods)
println("Fourier orders: ", model.k_vector)Forecasting
Basic Forecasting
using Durbyn
model = tbats(y, [7, 365.25])
fc = forecast(model, h=30)
println("Point forecasts: ", fc.mean)
println("80% CI: ", fc.lower[:, 1], " to ", fc.upper[:, 1])
println("95% CI: ", fc.lower[:, 2], " to ", fc.upper[:, 2])Forecast Options
fc = forecast(
model;
h = 50, # Forecast horizon
level = [80, 95], # Confidence levels
fan = false, # Fan chart levels
biasadj = nothing # Bias adjustment (inherits from model)
)Fan Charts
fc = forecast(model, h=30, fan=true)
println("Confidence levels: ", fc.level) # [51, 54, 57, ..., 99]Model Diagnostics
Fitted Values and Residuals
using Durbyn
model = tbats(y, m)
fitted_vals = fitted(model)
residuals_vals = residuals(model)
using Statistics
println("RMSE: ", sqrt(mean(residuals_vals .^ 2)))
println("MAE: ", mean(abs.(residuals_vals)))Checking ARMA Adequacy
using StatsPlots, Statistics
model = tbats(y, m, use_arma_errors=true)
resid = residuals(model)
autocor(resid, 1:20) |> plotInformation Criteria
model = tbats(y, m)
println("AIC: ", model.AIC)
println("Log-likelihood: ", model.likelihood)
println("Parameters: ", length(model.parameters[:vect]))
println("States: ", size(model.seed_states, 1))Computational Considerations
State Space Dimension
For a TBATS model with:
- Trend: 2 states ($\ell_t$, $b_t$)
\[T\]
seasonal components with Fourier orders $k_1, \ldots, k_T$: $2\sum_{i=1}^T k_i$ states- ARMA($p$, $q$): $p + q$ states
Total states: $2 + 2\sum k_i + p + q$
Example:
- Daily ($m=288$, $k=5$): 10 states
- Weekly ($m=2016$, $k=7$): 14 states
- Total: $2 + 10 + 14 = 26$ states
Compare to BATS: $2 + 288 + 2016 = 2306$ states!
Choosing Fourier Orders
The implementation automatically selects $k_i$ via AIC, but general guidelines:
- Short periods ($m < 12$): use $k \approx m/2$
- Medium periods ($m = 12$ to $100$): use $k \in [3, 10]$
- Long periods ($m > 100$): use $k \in [5, 15]$
- Very long periods ($m > 1000$): use $k \in [10, 20]$
Higher $k$ captures more complex seasonal shapes but increases computation.
Empirical Results (From De Livera et al. 2011)
1. Weekly U.S. Gasoline Demand
- Seasonal period: 52.18 weeks/year
- Result: TBATS outperforms BATS because seasonality is non-integer
- MASE improvement: 8.3%
2. Call Center Arrivals (5-minute intervals)
- Seasonal periods: 169 (daily), 845 (weekly)
- BATS states: 1014
- TBATS states: 24 (with $k_1=5$, $k_2=7$)
- Result: 40× reduction in state dimension with better forecasts
3. Turkish Electricity Consumption
- Calendars: Gregorian (365.25) + Hijri (354.37)
- Result: BATS cannot handle non-integer dual calendars; TBATS is the only feasible model
- MASE improvement over ETS: 23.1%
Comparison: BATS vs TBATS
| Scenario | Use BATS | Use TBATS |
|---|---|---|
| Integer seasonal period | ✓ | ✓ |
| Non-integer seasonal period | ✗ | ✓ |
| Short seasonal period ($m < 50$) | ✓ | ✓ |
| Long seasonal period ($m > 350$) | ✗ | ✓ |
| Multiple integer seasons | ✓ | ✓ |
| Dual calendars | ✗ | ✓ |
| Memory-constrained | ✗ | ✓ |
Rule of thumb: Use TBATS when:
- Any seasonal period is non-integer
- Any seasonal period exceeds 100
- You have dual calendar effects
- Memory/computation is limited
References
De Livera, A.M., Hyndman, R.J., & Snyder, R.D. (2011). Forecasting time series with complex seasonal patterns using exponential smoothing. Journal of the American Statistical Association, 106(496), 1513-1527.
See Also
- BATS Documentation - Integer seasonal periods only