datarekha
Time Series June 7, 2026

Decomposition: reading trend, seasonality, and residual

A practical guide to time series decomposition — separating trend, seasonality, and residual to reveal what a signal is actually doing.

11 min read · by datarekha · time seriesdecompositionseasonalitytrendstl

Every December, electricity demand climbs. Every Sunday, website traffic drops. Every pre-holiday week, retail sales spike so hard they look like outliers — until you realise the spike happens every year on almost the same calendar date. These rhythms are not accidents. They are structure baked into the data, and if you do not separate them from the genuine long-run signal you are trying to understand, you will mistake seasonal noise for trend and trend for anomaly.

Time series decomposition is the tool that does that separation. It breaks an observed series into three additive (or multiplicative) components — trend, seasonality, and residual — so each can be read and used on its own terms. Understanding decomposition is foundational to any serious time series work; it sits at the core of time series components and decomposition and explains a large part of why time series is different from cross-sectional analysis.

The two models: additive and multiplicative

Before you decompose anything, you have to pick a model. The choice is not cosmetic — it changes the mathematics and the meaning of every component.

Additive model: the observed value is the sum of the three components.

Y(t) = T(t) + S(t) + R(t)

Multiplicative model: the observed value is the product.

Y(t) = T(t) × S(t) × R(t)

In the additive model, the seasonal swing is measured in the same units as the series. A retailer whose weekly revenue seasonality peaks by £50 000 above the trend has that same £50 000 peak whether annual revenue is £1 million or £10 million. In the multiplicative model, the seasonal swing is proportional to the level: a 20% uplift means a £200 000 bump at £1 million revenue but a £2 million bump at £10 million revenue.

The diagnostic is visual. Plot the raw series and ask: does the height of the seasonal swing stay roughly constant as the trend rises (or falls)? If yes, additive. Does the swing grow (or shrink) with the level? If yes, multiplicative.

The diagram below shows the contrast in a simulated retail-sales series. In the additive panel the seasonal humps have the same absolute height throughout. In the multiplicative panel the humps visibly widen as the trend climbs — a hallmark of proportional seasonality.

Additive seasonalityMultiplicative seasonalitytimevalueswing stays constant → additivetimevalueswing grows with level → multiplicativeobservedtrend
Fig. 1 — Additive seasonality (left) keeps the wave amplitude constant; multiplicative seasonality (right) scales the amplitude with the trend level.

Classical decomposition via moving averages

The oldest formal method uses a centred moving average to estimate the trend component, subtracts (or divides) it out to isolate seasonality, and calls whatever is left the residual.

For a series with period m (e.g. m = 12 for monthly data), a centred moving average of order m smooths out exactly one full seasonal cycle at each point, leaving only the trend signal. Once you have the trend estimate T(t), the seasonal component is obtained by averaging the de-trended values at each seasonal position — all Januaries together, all Februaries together, and so on.

In Python the statsmodels library exposes this directly:

from statsmodels.tsa.seasonal import seasonal_decompose
import pandas as pd

result = seasonal_decompose(series, model="additive", period=12)
result.plot()

The four-panel output — observed, trend, seasonal, residual — is the classic decomposition view, shown schematically in the diagram below.

OBSERVEDTRENDSEASONALRESIDUALtTtime
Fig. 2 — Classic four-panel decomposition: observed series, smooth trend, repeating seasonal pattern, and residual noise.

Classical decomposition has two significant weaknesses that limit its use on real data.

Fixed seasonality. The seasonal component is assumed identical every year. If consumer shopping patterns shift — as they did dramatically during pandemic lockdowns — the classical method cannot follow. It averages across all years and smears the old pattern into periods where it no longer applies.

Missing endpoints. Because the centred moving average cannot be computed at the start or end of the series (there are not enough neighbours on both sides), the trend estimate is undefined for the first and last m/2 observations. On a one-year series this means you lose roughly six months of trend at each end — exactly the most recent data you usually care about most.

STL: the modern default

STL — Seasonal-Trend decomposition using Loess — was introduced by Cleveland, Cleveland, McRae, and Terpenning in 1990 and remains the workhorse of production decomposition because it addresses both weaknesses of the classical method.

The key ideas are:

  • Loess smoothing instead of moving averages. Each point’s trend and seasonal estimate is a locally weighted regression over its neighbours, so the trend can bend and the seasonality can shift over time.
  • Iterative inner and outer loops. The algorithm alternates between updating the seasonal and trend estimates until they converge, and an outer robust-reweighting loop downweights extreme residuals so that a single spike (a flash sale, a data-entry error) does not corrupt the component estimates.
  • Configurable smoothing windows. You control how much the seasonal pattern is allowed to change (s_window) and how smooth the trend is (t_window). Setting s_window="periodic" recovers classical fixed seasonality when you want it; setting a finite integer lets seasonality evolve.
from statsmodels.tsa.seasonal import STL

stl = STL(series, period=12, robust=True)
result = stl.fit()

trend    = result.trend
seasonal = result.seasonal
residual = result.resid

The robust=True flag activates the outer loop, which is almost always worth enabling on real-world data. Economic series, sensor readings, and web traffic all contain occasional spikes that classical decomposition would absorb into the seasonal or trend components — STL with robust fitting isolates them in the residual where they belong.

What each component tells you

Trend is the long-run direction after all cyclical rhythm is removed. A retailer whose raw weekly revenue oscillates wildly can read the trend component to know whether the business is actually growing, flat, or declining — without seasonal noise obscuring the answer. Trend changes (inflections, slope changes) are often leading indicators of structural shifts that would take months to see in raw data.

Seasonal is the repeating calendar pattern. For most consumer businesses this is the most actionable component: it tells you exactly how much of this week’s revenue is simply the seasonal contribution and how much is genuine performance. An electricity utility uses the seasonal component to set baseline demand forecasts before any weather or economic adjustment. See time series fundamentals for the distinction between seasonality and cyclicality (which is non-repeating).

Residual is what neither model captured. Ideally it looks like white noise — no autocorrelation, no heteroscedasticity, no obvious patterns. In practice the residual is where anomalies live. A large positive residual on a single day means something happened that was not in the trend or the seasonal rhythm: a product launch, a viral moment, a competitor’s outage. A large negative residual means the opposite — a lost hour, a failed deployment, an unusually quiet news cycle. Anomaly detection on residuals is far more sensitive than anomaly detection on raw values because the predictable structure has been removed.

Using decomposition in practice

Deseasonalising. Subtract (additive) or divide out (multiplicative) the seasonal component and you have the seasonally adjusted series: Y_adj(t) = Y(t) - S(t). Analysts, investors, and policymakers almost always prefer seasonally adjusted figures when comparing periods — they reveal whether conditions truly improved or merely reflected the expected seasonal pattern.

Anomaly detection on residuals. Fit STL, extract the residual, and flag any point whose residual exceeds three standard deviations (or a robust equivalent like the interquartile range scaled by 1.5). Because the seasonal and trend contributions have been removed, a genuine outlier stands out cleanly. This approach outperforms threshold rules on raw data, which fire constantly near seasonal peaks and miss anomalies in quiet periods.

Residual diagnostics. After decomposition, inspect the residual with an ACF plot. If there is significant autocorrelation left at any lag, the decomposition has not fully captured the structure — the seasonal period may be wrong, there may be a second seasonal cycle (daily data often has both a weekly and an annual cycle), or the trend may need to be more flexible. Unexplained structure in the residual is a model warning, not a data problem.

Forecasting baseline. Many production forecasting pipelines decompose first, forecast each component separately (trend by extrapolation, seasonal by a calendar model, residual as zero or a small ARIMA correction), then recompose. This is faster and more interpretable than end-to-end black-box forecasting for series with stable seasonal structure.

import numpy as np

stl    = STL(train_series, period=52, robust=True).fit()
trend  = stl.trend
season = stl.seasonal

trend_slope  = np.polyfit(np.arange(len(trend)), trend, 1)
trend_future = np.polyval(trend_slope, np.arange(len(trend), len(trend) + horizon))

season_cycle = season[-52:]
season_future = np.tile(season_cycle, int(np.ceil(horizon / 52)))[:horizon]

forecast = trend_future + season_future

This skeleton extrapolates the trend linearly and tiles the most recent seasonal cycle forward — a useful baseline before you layer in more sophisticated corrections.

Frequently asked questions

Q: How do I choose the right seasonal period?

Start from domain knowledge: 7 for daily data with weekly patterns, 12 for monthly with annual patterns, 52 for weekly with annual patterns. If you are unsure, inspect an ACF plot — the lag at which the first large positive spike appears is the dominant period. For data with multiple seasonal cycles (hourly data that has both daily and weekly structure), consider MSTL (statsmodels.tsa.seasonal.MSTL), the multi-seasonal extension of STL.

Q: When does decomposition fail?

Decomposition assumes at least two or three complete seasonal cycles of history to estimate the pattern reliably. It also struggles when the series has a structural break — a permanent regime change, like a product being discontinued — because the trend and seasonal components are assumed to evolve smoothly. In those cases, split the series at the break point and decompose each segment independently.

Q: STL or classical — when should I use classical decomposition?

Almost never for production work. Classical decomposition is useful for teaching and for a quick sanity check, but its fixed-seasonality assumption and missing endpoints make it inappropriate for most real series. Use STL by default. If your regulator or audit process specifically requires the X-13 or SEATS methodology, use those, but they are more complex to configure without adding meaningful accuracy for most business series.

Q: How do I know if my residual is clean enough?

Run three checks. First, an ACF plot of the residual: no lag outside the confidence bands is the goal. Second, a Ljung-Box test — a p-value above 0.05 means you cannot reject the null of no autocorrelation at the tested lags. Third, a time plot of the residual: it should look like static, with no expanding variance, no drifts, and no seasonal humps. If any check fails, revisit your choice of period, your additive-vs-multiplicative decision, or whether there is a second seasonal frequency you missed.

Skip to content