Stationarity in Time Series
What stationarity means
A time series is stationary if its statistical properties — mean, variance, and autocorrelation structure — don't change over time. Formally, a series $y_t$ is weakly (covariance) stationary if:
$E[y_t] = \mu$ for all $t$ (constant mean)
$\text{Var}(y_t) = \sigma^2$ for all $t$ (constant variance)
$\text{Cov}(y_t, y_{t+k})$ depends only on the lag $k$, not on $t$ itself (autocovariance depends only on lag)
Stationarity matters because most classical forecasting models — ARIMA, exponential smoothing in its simplest forms, and many statistical tests — are built on the assumption that the process generating the data is stable through time. A non-stationary series (one with a trend, a changing variance, or seasonal drift) will make these models produce unreliable forecasts and spurious statistical relationships.
What non-stationarity looks like
Three common sources of non-stationarity:
Trend: the mean drifts up or down over time (e.g. GDP growing year over year).
Seasonality: the mean or variance cycles predictably (e.g. retail sales spiking every December).
Changing variance (heteroskedasticity): the spread of the series widens or narrows over time (e.g. stock returns becoming more volatile during a crisis).
A classic trap is spurious correlation: two unrelated non-stationary series (say, cumulative rainfall in one country and a stock index in another) can show a strong correlation purely because they both trend upward, with no real causal link. This is why checking stationarity — or at least understanding why it's violated — comes before fitting most time series models, not after.
import pandas as pd
from statsmodels.tsa.stattools import adfuller
# series: a pandas Series of observations ordered by time
result = adfuller(series.dropna())
adf_statistic, p_value, used_lag, n_obs, critical_values, icbest = result
print(f"ADF statistic: {adf_statistic:.4f}")
print(f"p-value: {p_value:.4f}")
print("Critical values:", critical_values)
if p_value < 0.05:
print("Reject null hypothesis -> series is likely stationary")
else:
print("Fail to reject null -> series likely has a unit root (non-stationary)")The Augmented Dickey-Fuller (ADF) test
Making a series stationary: differencing and transforms
The most common fix for a trending series is differencing: instead of modeling $y_t$ directly, model the change $y_t - y_{t-1}$. This removes a linear trend because a constant upward drift becomes a roughly constant difference. If one differencing pass isn't enough, a second difference, $(y_t - y_{t-1}) - (y_{t-1} - y_{t-2})$, can remove quadratic trends — though differencing more than twice is rarely needed and starts to amplify noise.
For seasonality, seasonal differencing subtracts the value from one full seasonal period earlier: $y_t - y_{t-m}$, where $m$ is the season length (12 for monthly data with yearly seasonality). For changing variance, a log transform (or Box-Cox transform more generally) often stabilizes variance before differencing is applied to the mean.
import numpy as np
from statsmodels.tsa.stattools import adfuller
def adf_pvalue(s):
return adfuller(s.dropna())[1]
print("Original series p-value:", adf_pvalue(series))
# First difference removes a linear trend
diff1 = series.diff()
print("First difference p-value:", adf_pvalue(diff1))
# Seasonal difference (e.g. monthly data, yearly seasonality -> lag 12)
seasonal_diff = series.diff(12)
print("Seasonal difference p-value:", adf_pvalue(seasonal_diff))
# Log transform to stabilize variance before differencing the mean
log_diff = np.log(series).diff()
print("Log-differenced p-value:", adf_pvalue(log_diff))Over-differencing has a cost
What's Next?
Once a series has been made stationary — through differencing, transforms, or both — it becomes suitable input for the classical forecasting models built around the stationarity assumption. The most widely used of these combines autoregression, differencing, and a moving-average error structure into a single framework worth understanding in detail.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.