Stationarity Testing
Stationarity testing formally verifies whether a time series has stable statistical properties over time — a prerequisite for ARIMA and most classical forecasting models, and the first diagnostic step in any time series analysis.
Why Does This Exist?
ARIMA, VAR, and most classical time series models assume the series is stationary — that the mean and variance don't change over time. Fitting these models to a non-stationary series (one with a trend or explosive variance) produces unreliable, often completely wrong estimates.
The Augmented Dickey-Fuller (ADF) and KPSS tests provide formal statistical procedures to decide whether a series needs to be transformed (via differencing or log transformation) before modeling.
Think of It Like This
Testing whether a car is level before aligning the wheels
Wheel alignment assumes the car is on a level surface. If the floor is tilted, the measurements are meaningless. Similarly, time series models assume the series is stationary. Testing for stationarity is checking whether the statistical "floor" is level before running any analysis on top of it.
How It Actually Works
What stationarity means
A strictly stationary series has the same joint distribution for any lag — all statistical properties are time-invariant. In practice, we use the weaker weak (covariance) stationarity:
- Constant mean:
- Constant variance:
- Autocovariance depends only on lag:
A trending series violates condition 1. A series with increasing volatility violates condition 2.
Augmented Dickey-Fuller (ADF) test
The ADF test is the most widely used stationarity test. It tests for a unit root — the formal cause of non-stationarity in most financial and economic series.
Null hypothesis (): The series has a unit root (is non-stationary).
Alternative (): The series is stationary.
The test regresses and tests .
Interpretation:
- p-value < 0.05: Reject → series is stationary (no unit root)
- p-value ≥ 0.05: Fail to reject → series likely has a unit root (non-stationary)
KPSS test (complementary)
The KPSS test reverses the null and alternative:
Null hypothesis (): The series IS stationary.
Alternative (): The series has a unit root.
Used alongside ADF to distinguish "has a unit root" from "trend-stationary":
| ADF result | KPSS result | Conclusion |
|---|---|---|
| Reject | Fail to reject | Stationary ✓ |
| Fail to reject | Reject | Unit root (difference to fix) |
| Fail to reject | Fail to reject | Trend-stationary (detrend to fix) |
| Reject | Reject | Insufficient data / structural break |
Making a series stationary
| Problem | Transformation |
|---|---|
| Linear trend | First difference: |
| Exponential trend | Log then first difference: |
| Seasonal non-stationarity | Seasonal difference: |
| Increasing variance | Log or Box-Cox transform |
The order of integration is the number of differences needed to achieve stationarity. = already stationary; = needs 1 difference (most economic series); = needs 2 differences (rare).
Code
import numpy as npimport pandas as pdfrom statsmodels.tsa.stattools import adfuller, kpss
# ── Generate non-stationary and stationary series ─────────────────────────────np.random.seed(42)n = 200
# Non-stationary: random walk (unit root)y_nonstat = np.cumsum(np.random.randn(n))
# Stationary: mean-reverting AR(1) with |φ| < 1y_stat = np.zeros(n)for t in range(1, n): y_stat[t] = 0.7 * y_stat[t-1] + np.random.randn()
def run_stationarity_tests(series, name): print(f"\n{'='*50}") print(f"Series: {name}") print(f"Mean: {series.mean():.3f}, Std: {series.std():.3f}") # ── ADF test ────────────────────────────────────────────────────────────── adf_result = adfuller(series, autolag='AIC') print(f"\nADF Test:") print(f" Test statistic: {adf_result[0]:.4f}") print(f" p-value: {adf_result[1]:.4f}") print(f" Lags used: {adf_result[2]}") print(f" Critical values: 1%={adf_result[4]['1%']:.3f} " f"5%={adf_result[4]['5%']:.3f} 10%={adf_result[4]['10%']:.3f}") adf_conclusion = "STATIONARY" if adf_result[1] < 0.05 else "NON-STATIONARY (unit root)" print(f" Conclusion: {adf_conclusion}") # ── KPSS test ───────────────────────────────────────────────────────────── kpss_result = kpss(series, regression='c', nlags='auto') print(f"\nKPSS Test:") print(f" Test statistic: {kpss_result[0]:.4f}") print(f" p-value: {kpss_result[1]:.4f}") kpss_conclusion = "NON-STATIONARY" if kpss_result[1] < 0.05 else "STATIONARY" print(f" Conclusion: {kpss_conclusion}") # ── Differencing if needed ──────────────────────────────────────────────── if adf_result[1] >= 0.05: series_diff = np.diff(series) adf_diff = adfuller(series_diff, autolag='AIC') print(f"\nAfter 1st differencing:") print(f" ADF p-value: {adf_diff[1]:.4f} → " f"{'STATIONARY ✓' if adf_diff[1] < 0.05 else 'still non-stationary'}")
run_stationarity_tests(y_nonstat, "Random Walk (non-stationary)")run_stationarity_tests(y_stat, "AR(1) φ=0.7 (stationary)")
# ── Real-world example: test then difference ──────────────────────────────────print("\n--- ADF decision function ---")def ensure_stationary(series, max_diffs=3): """Difference until stationary, return transformed series and order.""" for d in range(max_diffs + 1): result = adfuller(series, autolag='AIC') if result[1] < 0.05: print(f" Stationary at d={d} (p={result[1]:.4f})") return series, d print(f" d={d}: p={result[1]:.4f} → applying difference") series = np.diff(series) print(" WARNING: could not achieve stationarity in {max_diffs} differences") return series, max_diffs
_, integration_order = ensure_stationary(y_nonstat)Watch Out For
Conflating stationarity tests with certainty
ADF and KPSS are hypothesis tests with Type I and Type II errors. For short series (< 100 observations), both tests have low power — they may fail to detect non-stationarity even when it's present. Supplement formal tests with visual inspection of rolling mean and variance, and with the ACF correlogram.
Over-differencing
Differencing once too many times (applying to an already-stationary series) introduces spurious moving-average components and makes the series harder to model. Use the minimum number of differences needed to pass the ADF test. If ADF already rejects at , the series is already stationary — do not difference.
The Quick Version
- Stationarity = constant mean, variance, and autocovariance structure over time.
- ADF test: = unit root (non-stationary); reject (p < 0.05) → stationary.
- KPSS test: = stationary; reject (p < 0.05) → non-stationary.
- Use both tests together — they help distinguish unit-root non-stationarity from trend-stationarity.
- Transform to achieve stationarity: first difference for linear trends, log for exponential growth, seasonal difference for seasonal patterns.