What Makes a Time Series Tick
A time series is a sequence of observations indexed in time order: $y_1, y_2, \ldots, y_T$. Unlike cross-sectional data, adjacent observations are statistically dependent — the value today carries information about the value tomorrow. Classical forecasting methods exploit this autocorrelation structure explicitly.
Before fitting any model, you must understand the structure of your series along four axes:
- Trend: a persistent long-run direction (upward, downward, or flat).
- Seasonality: a repeating pattern tied to the calendar (weekly, monthly, annual).
- Cyclicality: longer, irregular fluctuations not tied to fixed calendar periods.
- Irregular / noise: the unpredictable remainder.
Additive vs. Multiplicative Decomposition
An additive decomposition assumes the components sum:
A multiplicative decomposition assumes they multiply:
Multiplicative decomposition is appropriate when the amplitude of seasonal swings grows with the level of the series — a telltale sign is a cone-shaped seasonal pattern on a raw plot. Taking logs converts multiplicative to additive, which is why log-transforming is a ubiquitous first step.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# Load AirPassengers (classic benchmark series)
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
df = pd.read_csv(url, parse_dates=["Month"], index_col="Month")
df.index.freq = "MS" # monthly start
# Multiplicative decomposition (seasonal amplitude grows with level)
result = seasonal_decompose(df["Passengers"], model="multiplicative", period=12)
result.plot()
plt.tight_layout()
plt.show()Stationarity
Most classical models — in particular ARIMA — require weak stationarity: a constant mean, constant variance, and autocovariances that depend only on the lag, not on absolute time. A series with a trend or growing variance is non-stationary.
Two formal tests are standard:
- Augmented Dickey-Fuller (ADF): the null hypothesis is a unit root (non-stationary). Rejecting (low p-value) implies stationarity.
- KPSS: the null hypothesis is stationarity. Rejecting implies non-stationarity.
Using both together is more reliable because they have complementary power.
from statsmodels.tsa.stattools import adfuller, kpss
log_series = np.log(df["Passengers"])
adf_stat, adf_p, *_ = adfuller(log_series, autolag="AIC")
print(f"ADF stat={adf_stat:.3f}, p={adf_p:.4f}")
kpss_stat, kpss_p, *_ = kpss(log_series, regression="c", nlags="auto")
print(f"KPSS stat={kpss_stat:.3f}, p={kpss_p:.4f}")
# ADF fails to reject -> non-stationary; KPSS rejects -> confirms non-stationary
# Solution: first-difference the log seriesThe standard remedies for non-stationarity are differencing (subtract consecutive values) and log transformation. The order of differencing $d$ is a key parameter of ARIMA.
The ACF and PACF: Your Diagnostic Compass
The autocorrelation function (ACF) at lag $k$ measures the linear correlation between $y_t$ and $y_{t-k}$, accounting for all intermediate lags:
The partial autocorrelation function (PACF) measures the correlation between $y_t$ and $y_{t-k}$ after removing the effect of $y_{t-1}, \ldots, y_{t-k+1}$. PACF isolates the direct linear contribution of each lag.
The classical pattern-matching rules for model identification:
- AR(p): PACF cuts off after lag <!--MATHBLOCK11-->; ACF decays geometrically.
- MA(q): ACF cuts off after lag <!--MATHBLOCK12-->; PACF decays geometrically.
- ARMA(p,q): both tail off gradually.
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
diff_log = np.log(df["Passengers"]).diff().dropna()
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
plot_acf(diff_log, lags=40, ax=axes[0], title="ACF — First-Differenced Log")
plot_pacf(diff_log, lags=40, ax=axes[1], title="PACF — First-Differenced Log")
plt.tight_layout()
plt.show()Spikes at seasonal lags (12, 24, 36 for monthly data) in the ACF signal unresolved seasonal autocorrelation and indicate that a seasonal differencing step or SARIMA component is needed.