Quant Terminal
D1-03D1·intermediate·~20 min

Stationarity and unit roots — when statistics on a time series mean anything

time-seriesstationarityunit-rootadf-testspurious-regression

▸ Pretest — guess, even if you don't know

You regress Stock A's price on Stock B's price over five years of daily data and get R-squared 0.85 with a tiny p-value. What have you learned?

Stationarity: the license to estimate

Every statistic you compute on a time series — a mean, a volatility, an autocorrelation (the correlation of a series with its own past), a regression beta — is an average over time. Averaging over time only estimates something meaningful if the thing being estimated holds still. That requirement has a name. A series is weakly stationary if:

  1. its mean is constant over time,
  2. its variance is constant (and finite) over time,
  3. the covariance between xtx_t and xtkx_{t-k} depends only on the lag kk, not on tt.

Reading the notation in condition 3: xtx_t ("x sub t") is the series' value at time tt, and xtkx_{t-k} ("x sub t minus k") is its value kk steps earlier. The lag kk is how many steps back in time you look. So condition 3 says: how today relates to kk days ago is the same relationship in 2015 as in 2025 — it depends on the gap kk, not on the calendar date tt.

The trading question stationarity answers: is the past statistically like the future? If yes, history is a sample you can learn from. If no, your carefully estimated parameters describe a regime — a market era with its own behavior — that no longer exists. A model fit on such a series produces garbage forecasts with confident-looking standard errors.

Prices are not stationary; returns roughly are

From D1-02, a random walk's variance after tt steps is tσ2t\sigma^2 — "t times sigma squared," the variance of one step multiplied by the number of steps. It grows without bound, violating condition 2, and any drift (a steady tilt up or down) violates condition 1. Prices, being approximately random walks, are non-stationary. Their first difference, though — the series of changes, each value minus the one before, which for prices means returns — behaves much better. Returns have roughly constant mean near zero, roughly constant unconditional variance (unconditional — measured over the whole sample, ignoring what just happened), and autocorrelations that depend only on lag. Returns are approximately weakly stationary.

This is the general recipe. A series whose differences are stationary is called integrated of order one — meaning you must difference it exactly once to make it stationary. And differencing (np.diff, or log-price to log-return) is the standard transformation that turns it into something you may legitimately compute statistics on.

Two honest caveats. Volatility clustering (D1-01) means the conditional variance of returns — the variance given what just happened — moves around, so returns are not iid (independent and identically distributed). Weak stationarity is a workable approximation, not a law. And markets have regimes: a mean and volatility estimated in 2017 described 2017. Stationarity is always an assumption you argue for, never a fact you observe.

Spurious regression: the classic trap

Spurious means fake — an artifact of the method, not a real relationship. Why exactly does regressing price on price blow up? Both series wander; that is what random walks do. Over any given window, two wandering series will drift in some loosely shared way — up-up, down-down, or up-down — so the regression finds a strong "relationship" in-sample. Worse, the regression residuals — the leftover errors after the fit — are themselves non-stationary. That violates the assumptions behind OLS standard errors (OLS — ordinary least squares, the standard regression from A2-06). The t-statistics and p-values are simply wrong, and R-squared — the share of variation the regression claims to explain — stays high no matter how much data you add. Granger and Newbold's simulated independent walks produced t>2|t| > 2 (a t-statistic beyond the usual "significant" bar) about 75% of the time.

The trap wears many trading costumes: "my indicator is 0.9 correlated with the S&P," "these two coins move together." Computed on levels — the raw prices themselves rather than their changes — all of it is meaningless. The fix is mechanical: difference first. Regress returns on returns, changes on changes. (There is one important exception. Sometimes a combination of two non-stationary prices is genuinely stationary. That is called cointegration, and it is the respectable foundation of pairs trading. D1-04 builds the model for that spread.)

Unit roots and the ADF test

Write the suspicion formally with next lesson's model:

xt=ϕxt1+εtx_t = \phi x_{t-1} + \varepsilon_t

In words: the next value equals ϕ\phi times the current value, plus a fresh shock. ϕ\phi is the Greek letter phi — a number that says how much of today carries into tomorrow. εt\varepsilon_t ("epsilon sub t") is the shock — fresh random noise arriving at each step, as in D1-02. Concrete: with ϕ=0.9\phi = 0.9 and today's value 10, tomorrow's expected value is 9 — the series gets pulled back toward zero.

If ϕ=1\phi = 1, this is exactly a random walk: today carries over in full, shocks never decay, and the series is said to have a unit root ("unit" = the value one; the root of the equation sits exactly at 1) — it is non-stationary. If ϕ<1|\phi| < 1, shocks decay and the series is stationary. The Augmented Dickey–Fuller (ADF) test asks which world you are in. Read it carefully, because the direction trips people up:

In practice it is one line (shown for orientation — the exercise runtime here is numpy-only, so run this locally):

from statsmodels.tsa.stattools import adfuller
stat, pvalue, *_ = adfuller(x)   # H0: unit root (non-stationary)

Typical output: SPY prices, p around 0.6 — cannot reject the unit root. SPY returns, p far below 0.01 — stationary. One warning: the ADF test has low power near the boundary — low power meaning it often fails to detect a difference even when one exists. In practice it often cannot tell ϕ=0.98\phi = 0.98 (slowly mean-reverting, tradeable) from ϕ=1\phi = 1 (random walk, not tradeable). Failing to reject is absence of evidence, not proof of a unit root — and that ambiguity is exactly where many pairs trades go to die.

Try it

A crude but instructive stationarity check: a series whose mean drifts fails the most basic requirement.

▮ EXERCISE · d1-03-ex1

Implement two functions. difference(x) returns the first difference of x, using np.diff. is_mean_stable(x) is a crude stationarity check: split x into a first half of len(x) // 2 points and a second half with the rest, and return True when the absolute gap between the two half means is at most 0.5 times the sample standard deviation of the whole series, np.std with ddof=1. Real work uses the ADF test; this builds the intuition that a stationary series has no drifting mean.

⧉ Review card
State the three conditions of weak stationarity and why they matter.
Constant mean, constant finite variance, and autocovariance between x_t and x_t-k depending only on the lag k. They are the license to estimate: any statistic averaged over time only means something if the quantity holds still over time.
⧉ Review card
Why are prices non-stationary while returns are roughly stationary?
Prices are approximately random walks: their variance grows like t times sigma squared, unbounded, and drift moves the mean. Returns — the first difference — have roughly constant mean and unconditional variance. Caveat: volatility clustering means returns are stationary only as a working approximation.
⧉ Review card
What is spurious regression and what is the rule that prevents it?
Regressing one non-stationary series on another produces high R-squared and significant t-statistics even for independent random walks (Granger-Newbold 1974), because the residuals are non-stationary and OLS standard errors break. Rule: difference first — regress returns on returns, never prices on prices (unless testing cointegration).
⧉ Review card
What are the null hypothesis and reading of the ADF test?
Null: the series has a unit root, i.e. is non-stationary like a random walk. Small p-value rejects the null — evidence of stationarity. Large p-value means you failed to reject, not that a unit root is proven; the test has low power near phi close to 1.

Teach it

Teach spurious regression to an imaginary junior analyst who just proudly showed you a levels-on-levels regression with R-squared 0.9. In your own words: why do two independent random walks look related, what exactly breaks in the OLS standard errors, and what one-line rule should they follow from now on? Two minutes, out loud.

Predict before the next lesson

Next: autoregressive models (D1-04). Before then, predict: you want a model of a series that gets pulled back toward a long-run mean instead of wandering forever. How would you write next value as a function of the current value plus noise — and what coefficient value would turn your model back into a random walk?

◈ Calibration check

Could you define weak stationarity, explain to a colleague why regressing prices on prices is invalid, and state exactly what the ADF null hypothesis is?

1 = guessing · 5 = could teach it

⏻ End of lesson

Mark it read to book its 4 review cards into your deck.

Sources & further reading