Seasonal Decomposition of Time Series
Why decompose a time series at all
Most real-world time series — retail sales, electricity demand, website traffic — are not a single clean signal. They're a mixture of several simpler patterns layered on top of each other: a long-run trend, a repeating seasonal pattern tied to the calendar, and leftover residual noise that neither explains. Seasonal decomposition is the practice of splitting a series $y_t$ into these components so you can study, forecast, or adjust each one independently.
The classic additive model is $y_t = T_t + S_t + R_t$, where $T_t$ is trend, $S_t$ is seasonality, and $R_t$ is the residual. When the seasonal swings grow proportionally with the level of the series (e.g., holiday sales spikes that get bigger as the business grows), a multiplicative model fits better: $y_t = T_t \times S_t \times R_t$. A common trick is to log-transform the series first, turning a multiplicative problem into an additive one, since $\log(T_t \times S_t \times R_t) = \log T_t + \log S_t + \log R_t$.
Classical decomposition: moving averages
The oldest approach estimates trend with a centered moving average whose window matches the seasonal period (e.g., a 12-month window for monthly data with yearly seasonality). Subtracting (or dividing by) this trend leaves the seasonal-plus-residual component. Averaging the detrended values within each seasonal position (all Januaries, all Februaries, etc.) gives a stable estimate of $S_t$ that repeats identically every cycle. What's left after removing both trend and seasonality is the residual.
This method is simple and interpretable but has real limitations: it assumes the seasonal pattern is perfectly fixed across the whole series, it can't handle the first and last half-period of data (the moving average needs values on both sides), and it's sensitive to outliers.
STL: Seasonal-Trend decomposition using LOESS
STL, developed by Cleveland, Cleveland, McRae, and Terpenning (1990), improves on classical decomposition by using LOESS (locally estimated scatterplot smoothing) to extract trend and seasonal components iteratively. STL alternates between two smoothing passes: it estimates the trend by smoothing the deseasonalized data, then estimates seasonality by smoothing the detrended data within each seasonal sub-series, repeating until both stabilize.
STL's key advantages: the seasonal component is allowed to change slowly over time (unlike the rigid repeating pattern from classical decomposition), it's robust to outliers when you enable the robust fitting option, and it handles missing values and irregular series more gracefully. The tradeoff is more hyperparameters to tune — the seasonal window, trend window, and the number of robustness iterations all affect the result.
import pandas as pd
from statsmodels.tsa.seasonal import STL, seasonal_decompose
# Load a monthly series with a DatetimeIndex
df = pd.read_csv("retail_sales.csv", index_col="date", parse_dates=True)
series = df["sales"].asfreq("MS") # ensure a regular monthly frequency
# --- Classical moving-average decomposition ---
classical = seasonal_decompose(series, model="additive", period=12)
print(classical.trend.tail())
print(classical.seasonal.tail())
# --- STL decomposition (robust to outliers) ---
stl = STL(series, period=12, robust=True)
result = stl.fit()
trend = result.trend
seasonal = result.seasonal
resid = result.resid
# Residuals should look like noise -- no leftover structure
print(resid.describe())
Decomposition is a diagnostic tool, not just a forecasting step
Multiple seasonalities and MSTL
Many real series have more than one seasonal cycle at once — hourly electricity demand has both a daily cycle and a weekly cycle; retail data can have weekly and yearly cycles simultaneously. MSTL (Multiple STL) extends STL to extract several seasonal components in sequence, each with its own period, by iterating: extract the shortest-period seasonality first, remove it, then extract the next, and so on, refining each pass. This is especially useful before feeding a deseasonalized series into models like ARIMA, which otherwise assume a single seasonal period.
Classical decomposition vs. STL vs. MSTL
| Method | Seasonal pattern | Outlier robustness | Multiple seasonalities |
|---|---|---|---|
| Classical (moving average) | Fixed, repeats exactly | Low | No |
| STL | Slowly evolving | High (robust mode) | No (single period) |
| MSTL | Slowly evolving, per period | High | Yes |
Decomposition can leak the future if you're not careful
What's next
Decomposition gives you a clean seasonal and trend signal to work with, but it doesn't forecast anything on its own — you still need a model to project the trend forward and repeat the seasonal pattern into the future. That naturally leads into dedicated forecasting methods built to combine trend, seasonality, and uncertainty into an actual prediction.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.