HomeArticlesData Science

Time Series Decomposition: Pulling Trend and Seasonality Apart

Moving averages, cycle-averaged seasonal indices, STL's robust Loess smoothing, and a periodogram to find the period you didn't know to look for.

mysimulator teamUpdated June 2026≈ 8 min read▶ Open the simulation

Splitting a signal into three stories

A time series is rarely one clean process — it is usually a slow-moving trend, a repeating seasonal pattern locked to the calendar or clock, and whatever is left over after removing both, the residual (or irregular component). Classical decomposition assumes the observed series Y is built from these three pieces in one of two simple ways:

additive:        Y(t) = Trend(t) + Seasonal(t) + Residual(t)
                 use when the seasonal swing stays roughly constant in absolute size

multiplicative:  Y(t) = Trend(t) × Seasonal(t) × Residual(t)
                 use when the seasonal swing grows/shrinks proportionally to
                 the trend level (common in sales, traffic, anything that
                 compounds) — equivalent to an additive model on log(Y)
live demo · a synthetic series with trend, seasonality and noise● LIVE

Extracting the trend: the moving average

The most robust way to pull out the trend without assuming its functional form is a centred moving average whose window exactly matches the seasonal period — averaging over a full cycle cancels the seasonal component (it sums to roughly zero over one period, by construction) and smooths out the noise, leaving the slower-moving trend behind:

for period p (e.g. p = 12 for monthly data with yearly seasonality):

  Trend(t) = (1/p) · Σ_{i=−p/2}^{p/2} Y(t+i)      (centred; a small tweak
                                                    handles even p by
                                                    averaging two overlapping
                                                    windows so each point
                                                    gets an unbiased weight)

This is exactly why the window length matters so much: too short and seasonal ripple leaks through into what you call "trend"; too long and you smear away real turning points in the underlying trend. The unavoidable cost of a centred window is that it cannot be computed for the first and last p/2 points of the series — a moving average trend estimate is always missing an endpoint, which STL (below) fixes with a more careful extrapolation.

Extracting seasonality: average by phase

With the trend removed (subtracted for additive, divided out for multiplicative), what is left is seasonality plus noise. To isolate the seasonal component, group every observation by its position within the cycle — every January together, every February together, and so on for monthly data — and average across all the cycles present in the series:

detrended(t) = Y(t) − Trend(t)                      (additive case)
seasonal_index[m] = mean of detrended(t) over all t with phase(t) = m
Seasonal(t) = seasonal_index[phase(t)]              (normalised to sum/average to 0)

Averaging across cycles is what makes this robust to noise in any single cycle: a single unusually cold January does not distort the estimated January seasonal index much once averaged with several other Januaries. It also means the classical method needs at least two, and ideally many, full cycles of data before its seasonal estimate is trustworthy — with only one cycle observed, seasonality and residual are indistinguishable.

What's left: the residual, and what it is for

Residual(t) = Y(t) − Trend(t) − Seasonal(t) is everything the model could not explain — genuine noise, but also real signal the trend/seasonal model is too simple to capture: one-off events, structural breaks, holidays that move around the calendar (which a fixed monthly seasonal index cannot represent), or a seasonal pattern that is itself slowly changing over the years. The residual is where anomaly detection lives: once trend and seasonality are removed, a point that is still many standard deviations from zero is genuinely unusual, in a way it would not obviously be if you only looked at the raw series (a big number in December might be entirely normal seasonal behaviour; the same absolute jump in the residual after decomposition is not).

STL: a more careful, more robust decomposition

STL (Seasonal-Trend decomposition using Loess, Cleveland et al. 1990) refines the classical recipe in three ways. It replaces the fixed-window moving average with Loess — local weighted polynomial regression — for both the trend and the seasonal smoothing, which handles the missing-endpoint problem gracefully and lets the seasonal pattern itself drift slowly over the years instead of being locked to one fixed shape. It iterates: estimate trend, then seasonal from the detrended series, then trend again from the deseasonalised series, converging over a handful of passes rather than doing each step once. And it supports robust weighting, down-weighting outlying observations across iterations so that a handful of anomalous points do not drag the whole trend or seasonal estimate off course — a real advantage over the classical method's plain averaging, which treats every observation as equally trustworthy.

Finding the period itself: the periodogram

Everything above assumes you already know the seasonal period p. When you do not — or want to check for periodicities you were not expecting — the discrete Fourier transform answers the question directly, by re-expressing the series as a sum of sines and cosines at a discrete set of frequencies and reporting how much power (squared amplitude) sits at each:

X(f) = Σ_t Y(t) · exp(−2πi f t)          discrete Fourier transform
Power(f) = |X(f)|²                        the periodogram
period = 1 / f  at the frequency where Power(f) peaks

A live periodogram makes seasonality visually obvious in a way the raw time series often does not: a true periodic component shows up as a sharp, tall spike at its frequency (and often smaller spikes at its harmonics, if the seasonal shape is not a pure sine wave), while pure noise spreads its power roughly evenly across all frequencies. The one hard limit is the Nyquist frequency: sampled at one point per unit time, the DFT can only resolve periodicities down to two samples per cycle — any true periodicity faster than that folds back and masquerades as a lower, spurious frequency (aliasing), so the sampling rate has to be chosen with the fastest cycle you actually care about in mind.

Frequently asked questions

How do I know whether to use additive or multiplicative decomposition?

Look at whether the size of the seasonal swings stays roughly constant over time or grows with the trend level. If a series with an upward trend also shows seasonal peaks that get proportionally bigger as the trend rises, use a multiplicative model (or, equivalently, take the log of the series and decompose that additively). If the seasonal swing stays a roughly fixed absolute size regardless of trend level, additive is the right choice.

Why does moving-average trend extraction always cut off some data at the start and end?

A centred moving average of window length p needs p/2 points on either side of the point being estimated, so it simply has no valid window for the first and last p/2 points of the series. STL improves on this by using local regression (Loess), which can extrapolate more gracefully near the edges, but even STL's trend estimate is generally less certain right at the boundaries of the data.

What does a spike in the periodogram actually tell me?

It tells you that a substantial fraction of the series' total variance is concentrated at that specific frequency — strong evidence of a real periodic component with period 1/f, rather than random noise, which would spread its power roughly evenly across all frequencies instead of concentrating it. It does not, by itself, tell you the shape of that periodic component; smaller spikes at integer multiples of the fundamental frequency usually mean the seasonal cycle is not a pure sine wave.

Try it live

Everything above runs in your browser — open Time Series Decomposition and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.

▶ Open Time Series Decomposition simulation

What did you find?

Add reproduction steps (optional)