ADF Test in Python: Are Prices or Returns Stationary?
Lesson 12 of 16 · Data Manipulation
The ADF Test: Prices, Returns and the Unit Root
Scene: the data chapter of your dissertation. Your methodology section says "all series are stationary", and your examiner asks how you know. The honest answer is a test, and in finance the default is the augmented Dickey-Fuller (ADF) test. It takes one line in statsmodels. The trouble is reading it: half the students who run it read the p-value backwards. In this lesson you run it on prices, returns and a mean-reverting spread, and you learn to read it the right way round.
What it is: A series is stationary when its mean, variance and autocorrelations do not drift over time, so the past is a fair guide to the future. A unit root is the classic way to fail: a random walk, where each shock is permanent and the series wanders with no level to return to. Stock prices behave like this; their returns do not.
Why it matters
Regress one random walk on another, independent one, and you routinely get a "significant" slope and a high R-squared that mean nothing. Granger and Newbold called this spurious regression in 1974. Testing for a unit root first, and working with returns (differences) when you find one, is what keeps a thesis out of that trap.
The test
Δy_t = a + γ·y_(t-1)
+ δ_1·Δy_(t-1) + ... + δ_p·Δy_(t-p) + e_t
H0: γ = 0 unit root: NON-stationary
H1: γ < 0 pulled back to a level: stationary
ADF statistic = γ̂ / std err(γ̂)
The extra lagged differences (the "augmented" part) soak up short-term autocorrelation; statsmodels chooses how many by AIC. The statistic looks like a t-statistic, but under the null it does not follow a t distribution: its critical values are more negative. With a constant and a long sample, the 5% critical value is about -2.86, not -1.65.
Reading the p-value: the null is the unit root
| p-value | Decision | Plain English |
|---|---|---|
| below 0.05 | reject H0 | stationary: no unit root |
| 0.05 or above | cannot reject H0 | non-stationary: treat it as having a unit root |
A large p-value never proves a unit root; it means the data cannot rule one out. That asymmetry is why careful work also runs the KPSS test, whose null is the opposite (stationarity).
What this lesson's data give
Three synthetic series of 750 daily observations: a price that is a random walk with drift (100 rising to about 154), its log returns, and a spread that reverts to zero with persistence 0.9.
| series | ADF stat | p-value | 5% crit | verdict |
|---|---|---|---|---|
| price | -0.53 | 0.886 | -2.87 | non-stationary |
| returns | -27.25 | 0.000 | -2.87 | stationary |
| spread | -6.60 | 0.000 | -2.87 | stationary |
The spread is the instructive one: it is slow-moving and a chart of it looks lazy, yet the test rejects a unit root firmly. Stationary does not mean flat; it means tethered.
AIC picks 0 extra lags for the price, so its ADF statistic is plain Dickey-Fuller: regress the daily change in price on yesterday's price. The code does that by hand: γ̂ = -0.00131 with t = -0.53, exactly the ADF statistic.
Your Task
The starter runs the test on all three series and prints a verdict for each. The numbers are right, but YOUR TURN marks the verdict rule, which reads the p-value backwards: it calls the random-walk price "stationary".
Predict before you run: which of the three series will flip verdict once the rule is fixed, and which will not? Then correct the comparison in verdict(p).
Related terms in the glossary
Further reading
Where to go deeper. Free means a full, legal copy is online.
Chapter 11 on highly persistent series and random walks, and Chapter 18 on testing for unit roots and spurious regression.