Model card — Phillips-Perron and Phillips-Ouliaris tests¶
phillips_perron · phillips_ouliaris
Two semiparametric tests built on the same idea: instead of adding lagged
differences to soak up serial correlation (the augmentation in ADF and
Engle-Granger), estimate a simple regression by OLS and then correct the test
statistic for the residual's long-run variance with a nonparametric (Bartlett)
kernel. phillips_perron is the unit-root test — a drop-in alternative to
adf; phillips_ouliaris is its cointegration analog — a
residual-based alternative to johansen for a single
cointegrating relationship. Both are companions to the confirmatory stationarity
workflow, not replacements for reading ADF and KPSS together.
| Function | Null hypothesis | The analog it complements |
|---|---|---|
phillips_perron |
the series has a unit root | adf (semiparametric, no lag augmentation) |
phillips_ouliaris |
the regressors are not cointegrated with y |
Engle-Granger; johansen (single-equation route) |
phillips_perron — semiparametric unit-root test¶
What it estimates. The Phillips-Perron (1988) \(Z_\tau\) (default) or \(Z_\alpha\) statistic for a unit root. It runs the Dickey-Fuller level regression \(y_t = \mu + \delta t + \rho\, y_{t-1} + u_t\) by OLS with no lagged differences, then corrects the raw \(t\)-statistic (or the \(T(\hat\rho-1)\) statistic) for serial correlation using a Bartlett kernel estimate of the residual long-run variance. Same nonstandard Dickey-Fuller null distribution as ADF, so the MacKinnon (1996, 2010) response-surface p-values apply.
Assumptions. The only nonstationarity is a unit root (a deterministic trend
must be modeled through regression="ct"). The nonparametric correction handles
serial correlation and heteroskedasticity of unknown form — its strength — but
the test is known to have size distortions when the series has a large negative
MA root (a shared weakness with ADF), and low power near the unit-root boundary
(why you still pair it with KPSS).
When to use (and when not). Use as an ADF alternative when you would rather
not choose an augmentation lag length, or as a robustness cross-check on an ADF
verdict — agreement between the two is reassuring. Do not treat a failure to
reject as evidence of a unit root (low power); do not read it alone — run
check_stationarity or pair it with kpss for the
confirmatory quadrant. Prefer ADF when a strong negative MA component is
suspected.
Key arguments and defaults (and why). regression: "n" (no
deterministics), "c" (constant; default), "ct" (constant + trend) — the same
"match the deterministics to the stationary alternative" choice that dominates
ADF. test_type: "tau" (the \(Z_\tau\) \(t\)-form; default) or "rho" (the
\(Z_\alpha\) coefficient form). lags: the Bartlett bandwidth; None uses the
\(\lceil 12\,(n/100)^{1/4}\rceil\) rule (arch's default).
How to read the output. stat (the requested statistic), pvalue
(MacKinnon), crit (the 1/5/10% critical values), lags (the bandwidth used),
nobs, plus both ztau and zalpha for convenience. Small pvalue ⇒ reject
the unit root (the series looks stationary). Quote the bandwidth: a PP result
without its lags is not reproducible.
Failure modes. Reading PP alone (same trap as ADF alone); size distortion
under a large negative MA root; mistaking a deterministic trend for a root by
leaving regression="c" when "ct" is called for.
Validated against. arch.unitroot.PhillipsPerron (Sheppard) for both \(Z_\tau\)
and \(Z_\alpha\) — an independent package — to < 1e-10, with MacKinnon
response-surface p-values (phillips.json,
phillips_golden.rs).
See the validation matrix.
References. Phillips & Perron (1988); MacKinnon (1996, 2010); Newey & West (1987, the long-run-variance kernel).
import numpy as np, tsecon
rng = np.random.default_rng(0)
walk = np.cumsum(rng.standard_normal(300)) # a random walk (unit root)
stat = rng.standard_normal(300) # i.i.d. (stationary)
pp = tsecon.phillips_perron(walk, regression="c", test_type="tau")
print("PP(walk) Z-tau:", round(pp["stat"], 4), " p:", round(pp["pvalue"], 4),
" bandwidth:", pp["lags"])
print(" 5% critical value:", round(pp["crit"]["5%"], 3))
print("PP(stationary) Z-tau:", round(tsecon.phillips_perron(stat)["stat"], 4),
" p:", round(tsecon.phillips_perron(stat)["pvalue"], 4))
PP(walk) Z-tau: -0.7675 p: 0.8285 bandwidth: 16
5% critical value: -2.871
PP(stationary) Z-tau: -18.7697 p: 0.0
The random walk cannot reject the unit root (p ≈ 0.83, statistic well above the
−2.87 critical value); the i.i.d. series rejects overwhelmingly. These match
arch.unitroot.PhillipsPerron to machine precision.
phillips_ouliaris — residual cointegration test¶
What it estimates. The Phillips-Ouliaris (1990) \(Z_t\) (default) or \(Z_\alpha\)
residual test for cointegration. It regresses y on the stochastic regressors
x (plus the chosen deterministics) by OLS, then applies the Phillips-Perron
correction to a unit-root test on the regression residual. Under the null of
no cointegration that residual has a unit root; a cointegrating relationship
makes it stationary, so a large negative statistic rejects "no cointegration".
Assumptions. The variables in [y, x] are each I(1); a single cointegrating
vector is the alternative (this is a single-equation test — for the number of
cointegrating relations use johansen). The null
distribution depends on the number of regressors, so the critical values are
indexed by \(N = 1 + \dim(x)\).
When to use (and when not). Use for a quick, single-equation cointegration
check when one series is a natural dependent variable (a spread, an arbitrage
relation) — the Engle-Granger workflow, with the semiparametric correction.
Do not add your own constant column to x (deterministics come from trend);
do not use it to count cointegrating vectors (that is Johansen's job); remember
the test is not invariant to which variable you place on the left.
Key arguments and defaults (and why). x is a 2-D (T, m) matrix of the m
stochastic regressors, used as-is. trend: "n", "c" (default), "ct" — the
deterministics in the cointegrating regression. test_type: "Zt" (default) or
"Za". bandwidth: the Bartlett bandwidth of the residual AR(1); None uses
the \(\lfloor 4((T-1)/100)^{2/9}\rfloor\) rule.
How to read the output. stat, pvalue, crit, lags (bandwidth),
nobs, n_vars (\(N = 1 + m\)). Small pvalue ⇒ reject no cointegration (the
series move together in the long run). Zt p-values and critical values use the
MacKinnon N-surfaces (the statsmodels coint route); Za is statistic-only
(pvalue/crit are None) because the library deliberately declines to ship
arch's proprietary \(Z_\alpha\) simulation surface.
Failure modes. Adding a redundant constant column to x (double-counts the
deterministic); reading it as a rank test; swapping the dependent variable and
getting a different verdict (a known non-invariance of single-equation tests).
Validated against. arch.unitroot.cointegration.phillips_ouliaris for the
statistics — an independent package — with Zt p-values/critical values from the
statsmodels MacKinnon cointegration N-surfaces
(phillips.json,
phillips_golden.rs).
References. Phillips & Ouliaris (1990); Engle & Granger (1987); MacKinnon (1996, 2010).
import numpy as np, tsecon
rng = np.random.default_rng(0)
T = 300
x = np.cumsum(rng.standard_normal(T)) # an I(1) regressor
y = 1.5 * x + rng.standard_normal(T) # cointegrated with x
Xreg = x.reshape(-1, 1) # (T, 1) — no constant column
po = tsecon.phillips_ouliaris(y, Xreg, trend="c", test_type="Zt")
print("PO(cointegrated) Zt:", round(po["stat"], 4), " p:", round(po["pvalue"], 4),
" N:", po["n_vars"])
y2 = np.cumsum(rng.standard_normal(T)) # an independent random walk
po2 = tsecon.phillips_ouliaris(y2, Xreg, trend="c", test_type="Zt")
print("PO(independent) Zt:", round(po2["stat"], 4), " p:", round(po2["pvalue"], 4))
The genuinely cointegrated pair rejects "no cointegration" decisively; two
independent random walks do not (p ≈ 0.11) — the spurious-regression trap the
test exists to catch. Because it is single-equation, use johansen when you need
to know how many cointegrating relations a larger system supports.