Cointegration: two random walks that cannot lose sight of each other
Why a regression between two non-stationary series almost always lies — and the one precise case where it tells the truth. Interactive simulators, the Engle–Granger test, error correction models, and Python code that runs directly in this page.
Why this matters
Two curves have been rising together for ten years. A commodity price and a sector index, electricity consumption and average temperature, two stocks on the same market. The temptation is irresistible: there must be a link, and you can trade on it. A regression confirms it, with a flattering \(R^2\) and a highly significant coefficient.
Except that this reasoning is wrong roughly three times out of four. Between two series that drift freely — two random walks with no relationship whatsoever — the t statistic crosses the significance threshold in more than 80% of cases instead of the advertised 5%. And it gets worse as the sample grows: the more data you have, the more solid the false relationship looks. This is the result that earned Granger a Nobel prize, and it is probably the most expensive statistical mistake ever made in finance.
Cointegration is the answer: the exact criterion separating real relationships from mirages. It says there exists a combination of the two series that does not drift — a gap that always returns to its mean, like a dog held on a leash by its walker. If that leash exists, you can bet on reversion to equilibrium. If it doesn't, you are betting on nothing.
On this page you will draw your own pairs of independent series and watch convincing "relationships" appear before your eyes. Then you will cut the leash on a genuinely linked pair, by dragging a slider, and watch the test verdict flip live. The code driving all of it is right here, editable and runnable.
The problem: spurious regression
Take two series with nothing in common. Two pure random walks, produced by two independent generators:
Regress the first on the second. Least squares theory says the slope should be zero, the \(R^2\) nil, the t statistic small. It is not. Granger and Newbold (1974) showed it by simulation, Phillips (1986) proved it: under a unit root the \(t\) statistic diverges at rate \(\sqrt{n}\) instead of converging to a normal law. The longer the sample, the more significant the false relationship looks.
The simulator below draws two independent random walks and regresses one on the other. Run it a few times: convincing "relationships" will appear. Then launch the Monte Carlo experiment to measure the true rejection rate of a test whose nominal level is 5%.
Order of integration
Vocabulary first. A series \(X_t\) is integrated of order \(d\), written \(X_t \sim I(d)\), if it must be differenced \(d\) times to become stationary:
White noise is \(I(0)\); a random walk is \(I(1)\), since its first difference is white noise. In the multivariate VARMA formalism this corresponds to the diagonal operator \(\lambda(B) = \operatorname{diag}\!\big((1-B)^{d_1}, \dots, (1-B)^{d_m}\big)\) of the VARIMA model \(\Phi(B)\lambda(B)(\mathbf{X}_t - \boldsymbol\mu) = \Theta(B)\mathbf{a}_t\), where each component may carry its own differencing order \(d_j\).
Two arithmetic rules give the intuition for everything that follows. If \(X_t \sim I(0)\) and \(Y_t \sim I(0)\), any linear combination is \(I(0)\). If \(X_t \sim I(1)\) and \(Y_t \sim I(1)\), then in general \(aX_t + bY_t \sim I(1)\): the non-stationarity does not cancel. Cointegration is precisely the exception to that rule.
Defining cointegration
The components of the vector \(\mathbf{X}_t = (X_{t1}, \dots, X_{tm})\'\) are said to be cointegrated of order \((d, b)\), written \(\mathbf{X}_t \sim CI(d,b)\), if:
(i) every component is \(I(d)\); (ii) there exists a non-zero vector \(\boldsymbol\beta = (\beta_1,\dots,\beta_m)\'\) such that
\[z_t = \boldsymbol\beta\' \mathbf{X}_t \sim I(d-b), \qquad b > 0.\]The case that matters in practice is \(CI(1,1)\): every series is \(I(1)\), but one particular combination is \(I(0)\), hence stationary. The vector \(\boldsymbol\beta\) is called the cointegrating vector, and \(z_t\) the equilibrium error: the momentary departure from a long-run relationship \(\boldsymbol\beta\' \mathbf{X}_t = 0\).
Note that \(\boldsymbol\beta\) is only defined up to a multiplicative constant: if \(\boldsymbol\beta\' \mathbf{X}_t\) is stationary, so is \(2\boldsymbol\beta\' \mathbf{X}_t\). The first coefficient is therefore normalised to 1 by convention, which in the bivariate case gives \(z_t = X_{t1} - \beta X_{t2}\).
Simulator: the leash and the dog
The second simulator builds a pair whose leash you control. The series \(X_{t2}\) is a pure random walk — the common stochastic trend. The equilibrium error follows an AR(1) with parameter \(\phi = 1 - \alpha\), and \(X_{t1}\) is rebuilt as \(X_{t1} = \beta X_{t2} + z_t\):
The parameter \(\alpha\) is the speed of adjustment. When \(\alpha = 0\) the leash is cut: \(z_t\) becomes a random walk and the pair is no longer cointegrated, even though the two curves may still look very much alike. As \(\alpha\) grows, the gap is pulled back to zero faster and faster. Drag the \(\alpha\) slider towards 0 and watch the test statistic flip.
Three things in the readout are worth watching. First, the least squares estimate of \(\beta\) is excellent even when the noise is enormous: this is superconsistency, \(\hat\beta\) converges at rate \(n\) rather than \(\sqrt{n}\). Second, the half-life \(\ln(0.5)/\ln(\hat\phi)\) gives the number of periods needed to absorb half a shock: it is the first quantity a pairs trader looks at. Third, the test statistic is compared with \(-3.34\) and not with \(-1.96\); the next section explains why.
The Engle–Granger test
The two-step procedure is straightforward. First estimate the long-run relationship by ordinary least squares, then test whether the residual is stationary:
Under \(H_0\), \(\hat z_t\) carries a unit root: the two series drift apart independently. Under \(H_1\), \(\rho < 0\) and the gap is pulled back towards zero. We reject \(H_0\) when the \(t\) statistic attached to \(\hat\rho\) is sufficiently negative.
| Test | Statistic | 5% critical value |
|---|---|---|
| Ordinary t test (invalid here) | \(t\) | −1.96 |
| Augmented Dickey–Fuller, observed series, with constant | \(t_{\rho}\) | ≈ −2.86 |
| Engle–Granger, residual, 2 variables | \(t_{\rho}\) | ≈ −3.34 |
| Engle–Granger, residual, 3 variables | \(t_{\rho}\) | ≈ −3.74 |
| Engle–Granger, residual, 4 variables | \(t_{\rho}\) | ≈ −4.10 |
Approximate asymptotic values from MacKinnon (1991) for the case with a constant and no deterministic trend. Using −1.96 multiplies the false discovery rate by five or six.
Checking all this by simulation
The code below really runs in your browser (Python compiled to WebAssembly). Edit the values and re-run: the first launch takes a few seconds while Python loads.
Error correction: the representation theorem
The deepest result in the theory is not the test but an equivalence. The Granger representation theorem states that two \(I(1)\) series are cointegrated if and only if they admit an error correction representation:
Each equation is perfectly legitimate in the least squares sense: every variable is stationary, since we work in differences and \(z_{t-1}\) is \(I(0)\) by assumption. The coefficient \(\alpha_1\) measures the fraction of the disequilibrium corrected each period; it must be negative for the system to be stable. At least one \(\alpha_j\) must be non-zero, otherwise nothing pulls the system back to equilibrium.
In VAR notation the reformulation is illuminating. A VAR(\(p\)) in levels \(\mathbf{X}_t = \Phi_1\mathbf{X}_{t-1} + \dots + \Phi_p \mathbf{X}_{t-p} + \mathbf{a}_t\) can be rewritten exactly as:
Everything then hinges on the rank of the \(m \times m\) matrix \(\Pi\):
| rank(\(\Pi\)) = r | Interpretation | What to fit |
|---|---|---|
| \(r = 0\) | \(\Pi = 0\): no long-run relationship | VAR in first differences |
| \(0 < r < m\) | \(\Pi = \boldsymbol\alpha \boldsymbol\beta\'\), \(r\) cointegrating relations | VECM of rank \(r\) |
| \(r = m\) | \(\Pi\) has full rank: \(\mathbf{X}_t\) was stationary all along | VAR in levels |
The factorisation \(\Pi = \boldsymbol\alpha\boldsymbol\beta\'\) cleanly separates the two natures: \(\boldsymbol\beta\) (of size \(m \times r\)) holds the long-run equilibrium relations, \(\boldsymbol\alpha\) (of size \(m \times r\)) the short-run adjustment speeds.
Beyond two series: Johansen
Engle–Granger has two weaknesses as soon as you go past two variables. First, the result depends on which variable you put on the left of the equals sign: regressing \(X_1\) on \(X_2\) or \(X_2\) on \(X_1\) does not give exactly the same verdict in finite samples. Second, with \(m\) series there can be up to \(m-1\) independent cointegrating relations, and a single regression reveals at best one combination of them.
The Johansen procedure (1988, 1991) attacks the problem directly as a question of rank. The VECM is estimated by maximum likelihood and the rank of \(\Pi\) is tested from the eigenvalues \(\hat\lambda_1 \geq \dots \geq \hat\lambda_m\) of a canonical correlation problem between \(\Delta\mathbf{X}_t\) and \(\mathbf{X}_{t-1}\), corrected for the lags:
The test is run sequentially for \(r = 0\), then \(r = 1\), and so on, up to the first non-rejection — which gives the retained rank. The maximum eigenvalue statistic \(-n\ln(1-\hat\lambda_{r+1})\) provides a complementary test. Here too the limiting laws are non-standard and tabulated separately according to the deterministic terms included (constant inside the cointegrating relation, trend, and so on) — six classic cases, and picking the wrong one is a common source of error.
In practice (R and Python)
Nobody codes a Johansen test by hand in production. Here are the standard paths, with the detail that matters on each line.
# ─── R: tseries + urca + vars ─── library(tseries); library(urca); library(vars) # 0. check that EVERY series really is I(1) first adf.test(x1) # expect no rejection in levels adf.test(diff(x1)) # expect rejection in differences # 1. Engle-Granger, two steps eg <- lm(x1 ~ x2) adf.test(residuals(eg)) # WARNING: the p-value is invalid here po.test(cbind(x1, x2)) # Phillips-Ouliaris: correct critical values # 2. Johansen: cointegration rank jo <- ca.jo(cbind(x1, x2), type = "trace", ecdet = "const", K = 2) summary(jo) # compare the trace stat with the critical values # 3. fitted VECM, then conversion to a VAR in levels for forecasting vecm <- cajorls(jo, r = 1) var_levels <- vec2var(jo, r = 1) predict(var_levels, n.ahead = 25)
# ─── Python: statsmodels ─── from statsmodels.tsa.stattools import adfuller, coint from statsmodels.tsa.vector_ar.vecm import coint_johansen, VECM adfuller(x1) # I(1)? in levels, then in differences coint(x1, x2) # Engle-Granger, correct critical values jres = coint_johansen(X, det_order=0, k_ar_diff=1) print(jres.lr1, jres.cvt) # trace vs 90/95/99% critical values model = VECM(X, k_ar_diff=1, coint_rank=1, deterministic="ci").fit() print(model.beta, model.alpha) # long run, adjustment speeds model.predict(steps=25)
adf.test(residuals(eg)) trap. The function will return a p-value,
and that p-value will be wrong — too small — because it is computed for an observed series, not for
a least squares residual. Use po.test() in R or coint() in Python, which
apply MacKinnon critical values adapted to the number of regressors.
Limits and pitfalls
| Assumption or pitfall | Consequence → remedy |
|---|---|
| Series must share the same order \(I(1)\) | Mixing \(I(0)\) and \(I(1)\) makes the test uninterpretable → test each series separately first |
| Structural break in the relationship | The test fails to reject \(H_0\) although cointegration holds piecewise → Gregory–Hansen tests |
| Low power in small samples | With \(n < 100\) and slow adjustment almost nothing is detected → lengthen the sample rather than raising the frequency |
| Choice of the number of lags \(p\) in the ADF | Too few: autocorrelated residuals; too many: loss of power → AIC or the Schwert rule |
| Deterministic terms (constant, trend) | Each case has its own critical values → decide before looking at the data |
| Cointegration ≠ causality | An equilibrium relation does not say who adjusts to whom → read the \(\alpha_j\) and test weak exogeneity |
| Relationship stable over time | In finance, pairs "de-cointegrate" → re-estimate on a rolling window and monitor the half-life |
References
- Engle, R. F. & Granger, C. W. J. (1987). Co-integration and error correction: representation, estimation, and testing, Econometrica 55(2) — the founding paper, Nobel 2003.
- Granger, C. W. J. & Newbold, P. (1974). Spurious regressions in econometrics, Journal of Econometrics 2 — the original warning, by simulation.
- Phillips, P. C. B. (1986). Understanding spurious regressions in econometrics, Journal of Econometrics 33 — the asymptotic theory behind it.
- Johansen, S. (1991). Estimation and hypothesis testing of cointegration vectors in Gaussian vector autoregressive models, Econometrica 59(6).
- MacKinnon, J. G. (1991). Critical values for cointegration tests, in Engle & Granger (eds.), Long-Run Economic Relationships, Oxford University Press.
- Woodward, W. A., Gray, H. L. & Elliott, A. C. (2017). Applied Time Series Analysis with R, 2nd ed., CRC Press — chapter 10, the VAR/VARMA framework and tests of association between series.
- Murray, M. P. (1994). A drunk and her dog: an illustration of cointegration and error correction, The American Statistician 48(1) — the leash metaphor.
The simulators on this page are written in plain JavaScript/Canvas with no external dependency; the Python cells run through Pyodide. The source is available on GitHub.