Machine Learning ML-11.01
Time series fundamentals
Take a time series apart: trend, seasonality and noise, classical and STL decomposition, and reading the autocorrelation function.
The big idea
A time seriesA sequence of measurements of one quantity taken at successive points in time, usually at a regular interval. is a set of measurements in time order: visitors per day, temperature per hour, CO2 per month. Unlike a table of customers, the order is the information. Yesterday's sales tell you a lot about today's, and last December tells you about this December.
Think of the water level at a harbour. Over decades it creeps up (sea level rise). Twice a day it rises and falls with the tide. On top of that, wind and waves add a jitter nobody can predict. Most series you will meet have the same three layers: a slow drift, a repeating rhythm and noise. Separating them is the first thing to do with any series, because each layer needs a different kind of model, and the noise tells you how well you can ever hope to forecast.
How it works
Step 1: Three components
The textbook Forecasting: Principles and Practice (Hyndman and Athanasopoulos, third edition) gives clean definitions:
- Trend: "a long-term increase or decrease in the data. It does not have to be linear." (They usually call it the trend-cycleThe slowly changing level of a time series: its long-run increase or decrease, including slow cycles that are not of fixed length., since slow cycles get mixed in.)
- Seasonality: a pattern driven by the calendar, such as the time of year or the day of the week. SeasonalityA repeating pattern of fixed, known period, such as day of week or month of year. "is always of a fixed and known period".
- Cycles: rises and falls "that are not of a fixed frequency", such as business cycles. Do not confuse them with seasonality: you know when next Monday is, but not when the next recession starts.
What remains after removing trend and seasonality is the remainderWhat is left of a time series after the estimated trend and seasonal components are subtracted; ideally unpredictable noise., also called residual or noise.
- Observed Four years of a monthly series. It rises and wiggles, and it is hard to say by eye how much of each wiggle is rhythm and how much is chance.
- Trend Smooth away the wiggles and a steady climb is left: the trend.
- Seasonal Subtract the trend and the same 12-month wave appears every year, peaking at the same point in each period (dashed lines mark the years).
- Remainder Subtract the seasonal wave too and only small, irregular movements are left: the remainder. Add the three lower curves and you get the top one back.
Step 2: Additive or multiplicative
The additive modelWriting a time series as trend plus seasonal plus remainder, appropriate when the seasonal swings stay about the same size as the level changes. is
It fits when the seasonal swing stays about the same size as the level changes. If the swing grows with the level, say December sales are always 30 percent above average whether the shop sells 1,000 or 10,000 items, use the multiplicative form . Taking logarithms turns it back into an additive one, , so a common recipe is: log, decompose additively, transform back.
Step 3: Classical decomposition
The oldest recipe takes four steps, for a season of length (12 for monthly data with a yearly pattern, 7 for daily data with a weekly one):
- Trend: a centred moving averageThe average of the values in a window around each point; a window of exactly one season length cancels the seasonal pattern. over one full season. For odd average values; for even , use a "2 x m" moving average, which weights the two end points by half, so the window stays centred.
- Detrend: .
- Seasonal: average the detrended values for each season (all Januaries, all Februaries, ...) and shift them so they sum to zero.
- Remainder: .
Why exactly one season? A window covering all 12 months contains every month once, so the seasonal ups and downs cancel and only the level remains. Worked example with a 4-quarter season: values 10, 14, 12, 8 (season sums to zero around a level of 11). Any window of 4 consecutive quarters averages exactly 11, so the trend is flat at 11 and the seasonal effects come out as -1, +3, +1, -3.
Classical decomposition has known weaknesses: there is no trend estimate for the first and last half-season of data, it assumes the seasonal pattern repeats identically every year, it over-smooths sharp rises and falls, and it is not robust to unusual values.
Step 4: STL, the modern default
STLSeasonal and Trend decomposition using Loess: a robust, flexible decomposition that lets the seasonal pattern change slowly over time. stands for "Seasonal and Trend decomposition using Loess". It replaces the moving averages with local regression (loess), handles any season length, lets the seasonal shape change slowly over time at a rate you choose, and has a robust option so a few outliers do not distort the trend and season. It only does additive decompositions, so take logs first for multiplicative data.
Step 5: Autocorrelation
Correlation ( Exploratory data analysis ) measures how two variables move together. AutocorrelationThe correlation between a time series and a copy of itself shifted by k steps (lag k); plotted for many lags it is the ACF. correlates a series with itself shifted by steps, the lag:
Plotting for gives the autocorrelation function (ACF), and it is a fingerprint of the series:
- Trend: small lags are large and positive, decaying slowly, because nearby points have nearby values.
- Seasonality: peaks at multiples of the season length (lag 12, 24, ... for monthly data).
- White noise: no autocorrelation. For a series of length , about 95 percent of the spikes should fall within . With 100 points that band is about .
The ACF of the remainder is your check on the decomposition. If it still shows spikes, there is structure left that a model could use.
Try it
- On the CO2 series, set the window to 12. The violet trend is smooth and the green seasonal wave swings about 6.5 ppm from peak to trough. Now set the window to 7: the trend starts to wiggle with the yearly cycle, and the seasonal wave shrinks to about 4 ppm, because part of the season has leaked into the trend. A window of 24 is smooth again but reacts more slowly, so the remainder grows.
- Set the window back to 12 and switch to autocorrelation. The series' ACF starts at 0.96 and decays slowly, with bumps at lags 12, 24 and 36 riding on top: trend plus a yearly season. The remainder's bars are much smaller, but lag 1 (about 0.28) still sticks out of the white-noise band. Classical decomposition left some short-term structure behind.
- Pick the sales series, set noise to 0 and the window to 7: the remainder is flat, because a 7-day average removes a weekly pattern exactly. Raise the noise to 3: the seasonal estimate gets less regular, because each weekday's average now mixes the weekly effect with noise.
Going deeper
Code: decomposing 44 years of CO2
import numpy as np
from statsmodels.datasets import co2
from statsmodels.tsa.seasonal import seasonal_decompose, STL
from statsmodels.tsa.stattools import acf
# Weekly CO2 at Mauna Loa (public domain, in statsmodels) -> monthly means, small gaps filled
y = co2.load_pandas().data["co2"].resample("MS").mean().interpolate()
print(f"{len(y)} months, {y.index[0]:%Y-%m} to {y.index[-1]:%Y-%m}, {y.iloc[0]:.1f} to {y.iloc[-1]:.1f} ppm")
# Classical additive decomposition: 2x12 moving average for the trend, monthly averages for the season
d = seasonal_decompose(y, model="additive", period=12)
print(f"trend rose {d.trend.dropna().iloc[-1] - d.trend.dropna().iloc[0]:.1f} ppm; "
f"first {d.trend.isna().idxmin():%Y-%m}: trend is missing for the first and last 6 months")
season = d.seasonal.groupby(d.seasonal.index.month).first() # one value per calendar month
print("seasonal effect Jan..Dec (ppm):", " ".join(f"{v:+.1f}" for v in season.values + 0.0))
names = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()
print(f"peak {names[season.idxmax() - 1]}, low {names[season.idxmin() - 1]}, swing {season.max() - season.min():.1f} ppm")
r = d.resid.dropna()
print(f"remainder sd {r.std():.2f} ppm vs series sd {y.std():.2f} ppm")
# STL lets the seasonal shape drift over the years
s = STL(y, period=12, robust=True).fit()
amp = s.seasonal.groupby(s.seasonal.index.year).agg(lambda v: v.max() - v.min())
print(f"STL seasonal swing: {amp.iloc[1]:.1f} ppm in {amp.index[1]}, {amp.iloc[-1]:.1f} ppm in {amp.index[-1]}")
# Autocorrelation: the raw series is dominated by the trend; the remainder is not
band = 1.96 / np.sqrt(len(r))
print("ACF of the series, lags 1, 6, 12, 24:", " ".join(f"{v:.2f}" for v in acf(y, nlags=24)[[1, 6, 12, 24]]))
print("ACF of the detrended series, lags 1, 6, 12:", " ".join(f"{v:+.2f}" for v in acf((y - d.trend).dropna(), nlags=12)[[1, 6, 12]]))
print(f"ACF of the remainder, lag 1: {acf(r, nlags=1)[1]:+.2f} (white-noise band +-{band:.2f})")Output
526 months, 1958-03 to 2001-12, 316.1 to 371.0 ppm
trend rose 55.3 ppm; first 1958-09: trend is missing for the first and last 6 months
seasonal effect Jan..Dec (ppm): -0.0 +0.6 +1.4 +2.5 +2.9 +2.3 +0.7 -1.3 -3.0 -3.1 -2.0 -0.9
peak May, low Oct, swing 6.1 ppm
remainder sd 0.28 ppm vs series sd 17.12 ppm
STL seasonal swing: 4.9 ppm in 1959, 6.3 ppm in 2001
ACF of the series, lags 1, 6, 12, 24: 0.99 0.95 0.94 0.87
ACF of the detrended series, lags 1, 6, 12: +0.83 -0.85 +0.97
ACF of the remainder, lag 1: +0.40 (white-noise band +-0.09) The data are weekly CO2 readings from Mauna Loa, Hawaii, from 1958 to 2001 (Keeling and Whorf, public domain, shipped with statsmodels), averaged to 526 months. seasonal_decompose runs the classical recipe with a 2 x 12 moving average. The trend rises 55.3 ppm over the record, and the seasonal effect peaks in May and bottoms out in October with a swing of 6.1 ppm. The remainder's standard deviation is 0.28 ppm against 17.12 for the raw series: trend and season explain nearly all of the movement.
STL tells a finer story: its seasonal swing grows from 4.9 ppm in 1959 to 6.3 ppm in 2001, something classical decomposition cannot show because it forces one seasonal shape for all years. The ACF lines confirm the fingerprints: the raw series has and stays high at every lag (trend); the detrended series swings from +0.83 at lag 1 to -0.85 at lag 6 and back to +0.97 at lag 12 (a yearly cycle); and the classical remainder still has , well outside the white-noise band of plus or minus 0.09.
Why this matters for ML
- Never shuffle a time series into random train and test sets. Neighbouring points are correlated, so a shuffled test set is full of near-copies of training points and the score is inflated. Split by time ( Backtesting and forecast evaluation ).
- A seasonal naive forecast ("same as one season ago") is a strong baseline for seasonal data. Beat it before celebrating.
- Decomposition gives features: the trend, the seasonal index, and lags at 1 and steps are some of the most useful inputs for the ML forecasters in ML and deep learning for forecasting .
Trade-offs
| Option | Wins when | Loses when |
|---|---|---|
| Classical decomposition | Simple, transparent, a few lines of code | Fixed seasonal shape; no trend at the edges; outliers distort it |
| STL | Seasonal shape can drift; robust option; any season length | Additive only; more knobs to tune |
| Multiplicative (or log + additive) | Seasonal swings that scale with the level | Needs positive values; harder to read in original units |
Common mistakes
- Using a moving-average window that is not a whole number of seasons, so the trend picks up seasonal ripples.
- Choosing additive decomposition when the seasonal swing clearly grows with the level.
- Calling any up-and-down pattern "seasonal". Seasonality has a fixed, known period; irregular cycles do not.
- Reading a slowly decaying ACF as "strong seasonality" when it is just a trend.
- Shuffling time-ordered data into random cross-validation folds.
In the wild
Retailers decompose daily sales into trend, day-of-week and holiday effects before planning stock; utilities do the same with electricity demand, which has daily, weekly and yearly cycles at once. Statistical agencies publish "seasonally adjusted" unemployment and retail figures, meaning the seasonal component has been removed so month-to-month changes reflect the trend. In Python, statsmodels provides seasonal_decompose, STL and acf; in R, the fable and feasts packages accompany the Hyndman and Athanasopoulos textbook.
Check yourself
-
Which pattern has a fixed and known period?
Show answer
B. Seasonality Seasonality is tied to the calendar (day of week, month of year). Cycles rise and fall without a fixed frequency, and a trend is a long-run drift.
-
Monthly sales swing by about 30 percent around the level, and the level has tripled over the years. Which decomposition fits?
Show answer
B. Multiplicative, or additive on the log of the values When the seasonal swing grows with the level, the components multiply. Logs turn products into sums, so log then additive decomposition works.
-
For monthly data with a yearly pattern, why use a 12-month (2 x 12) moving average for the trend?
Show answer
B. A window of one full season contains each month once, so the seasonal effects cancel Averaging over exactly one season leaves only the level. Other window lengths let part of the seasonal pattern leak into the trend.
-
An ACF plot of monthly data shows tall spikes at lags 12, 24 and 36. What does it suggest?
Show answer
B. A yearly seasonal pattern Autocorrelations are largest at multiples of the season length when a series is seasonal.
-
The remainder of your decomposition has an ACF at lag 1 of 0.4, with a white-noise band of plus or minus 0.09. What should you conclude?
Show answer
B. There is short-term structure left that a model could still exploit White noise would stay inside the band. A clear lag-1 correlation means consecutive remainders are related, which models such as ARIMA can use (ML-11.02).
Recap
- Most time series combine a trend, seasonality of fixed period, and an unpredictable remainder.
- Additive decomposition sums the parts; use multiplicative (or logs) when seasonal swings grow with the level.
- Classical decomposition uses a one-season moving average and per-season averages; STL is more flexible and robust.
- The ACF is a fingerprint: slow decay means trend, peaks at seasonal lags mean seasonality, flat within plus or minus 1.96 / sqrt(T) means white noise.
- Never shuffle time series data; split by time and compare against a seasonal naive baseline.