Atlas
statminds
Time Series (Unit Root Model)The underlying model family class (e.g. GLM, linear model, categorical matrix, log-linear).Parametric ReferenceStatistical methods that assume a specific probability distribution family (typically normal).12-stage workflow

Augmented Dickey-Fuller (ADF)

The engine for Stationarity Discovery. The ADF test audits the presence of a 'Unit Root', reveal if your time series data is stable enough for valid temporal modeling or if it is drifting in an unpredictable random walk.

Model familyTime Series (Unit Root Model)
Hypothesishypothesis_test
AliasesADF Test · Unit Root Test · Stationarity Audit
G1
Stationarity Audit
Determine if the mean and variance of your data remain constant over the study window.
G2
Unit Root Neutralization
Identify if the data needs 'Differencing' to strip away unstable temporal dependencies.
G3
Modeling Integrity Shield
Protect discovery in ARIMA and VAR modeling by ensuring the foundational data basis is stable.
Visual Overview Dashboard
1

What is it?

Augmented Dickey-Fuller (ADF) analyzes sequences of data points ordered chronologically over time to extract patterns, model trends, and make forecasts.

The engine for Stationarity Discovery. The ADF test audits the presence of a 'Unit Root', reveal if your time series data is stable enough for valid temporal modeling or if it is drifting in an unpredictable random walk.

2

Goals & Indications

  • Stationarity Audit: Determine if the mean and variance of your data remain constant over the study window.
  • Unit Root Neutralization: Identify if the data needs 'Differencing' to strip away unstable temporal dependencies.
  • Modeling Integrity Shield: Protect discovery in ARIMA and VAR modeling by ensuring the foundational data basis is stable.
3

Core Idea Diagram

Stationary (ρ = 0.7)Unit Root (ρ = 1.0)
4

Claims tested

H₀: H₀: Unit root is present (series is non-stationary, has stochastic trend, γ = 0 in regression)
Hₐ: Hₐ: No unit root (series is stationary around deterministic trend or constant, γ < 0)
5

How it works

  1. Formulate Dickey-Fuller regression equation including lags of differences.
  2. Estimate parameters using OLS regression on the time series differences.
  3. Calculate t-statistic for the lagged level variable coefficient.
  4. Compare with Dickey-Fuller critical values to check for a unit root.
6

Assumptions

Linear data generating process: Time series follows linear autoregressive structure with possible unit root
Appropriate lag length selection: Sufficient lags included to remove autocorrelation in test equation residuals
Correct specification of deterministic components: Model includes appropriate constant and/or trend terms
7

Important Note

CRITICAL INTERPRETATION: ADF test has OPPOSITE interpretation from most tests. Low p-value (p < 0.05) means REJECT H₀ → series is STATIONARY (good for ARIMA). High p-value (p ≥ 0.05) means FAIL TO REJECT H₀ → series is NON-STATIONARY (need differencing). This confuses many analysts who expect p<0.05 to indicate a problem. The test is based on regression: ∆y_t = α + βt + γy_{t-1} + Σφ_i∆y_{t-i} + ε_t. Three variants: (1) No constant, no trend: tests pure random walk, (2) Constant, no trend: tests random walk with drift, (3) Constant and trend: tests trend-stationary vs difference-stationary. Test statistic γ_hat/SE(γ_hat) follows non-standard distribution (Dickey-Fuller distribution, not t-distribution) with critical values depending on sample size and model specification. MacKinnon (1996) provides numerical approximation for p-values. Key distinction from KPSS test: ADF has H₀=non-stationary, KPSS has H₀=stationary (use both for confirmation: ADF rejects + KPSS doesn't reject = definitely stationary). Key distinction from Phillips-Perron: ADF uses parametric correction for autocorrelation (lag augmentation), PP uses non-parametric correction (robust to heteroscedasticity and autocorrelation). Lag length selection critical: too few lags → residual autocorrelation (size distortion), too many lags → loss of power. Information criteria (AIC, BIC, HQIC) or significance-based methods (t-stat approach) used for lag selection.

8

Worked Example

LagADF Stat5% Critp-valueStationary?
Lag 1-3.42-2.890.011Yes
Lag 2-2.15-2.890.224No
Interactive Sandbox

Dickey-Fuller Unit Root Laboratory

Slide the autoregressive coefficient ρ. If ρ = 1.00, the process contains a unit root (random walk), leading to non-stationarity.

Autoregressive Coefficient (ρ)0.95
ρ = 1.00 is a pure Unit Root. ρ < 1.00 is mean-reverting stationary.
Deterministic Drift (d)0.3
Time Steps (N)100
DF Regression Estimates
Estimated γ (ρ - 1): -0.1247
Estimated Intercept: 0.7569
t-statistic: -2.839
Critical Values (Constant Only):
1%: -3.51
5%: -2.89
10%: -2.58

Verdict:Non-Stationary (Fail to Reject H₀)
Simulated Time Series path
9.0-1.00Time (t) →
Interpretation

The test statistic (-2.84) is not more negative than the critical value (-2.89). We fail to reject the null hypothesis. The series has a unit root (non-stationary random walk).

The 12-Stage Precision Workflow
01Unit Root Null
Hypotheses
We test the Null (Unit Root exists = Non-stationary) against the discovery of a stable, Mean-Reverting process.
02Temporal Order
Assumptions
Ensuring the data is sequenced correctly by time—the foundational requirement for unit root authority.
03Lag Calibration
Diagnostics
Determining the optimal 'Lag Length'—ensuring enough previous points are included to neutralize any residual serial correlation.
04focus
Testing if daily FlowMotion app downloads are a stable stream or a drifting trend that requires differencing.
05KPSS Pivot
Alternatives
Knowing when to switch to the KPSS test if you want to reverse the burden of proof (Null = Stationary).
06The ADF Strike
Significance
Calculating the t-statistic against specialized Dickey-Fuller critical values—standard t-tables are invalid for unit root discovery.
07The Rho Coefficient
Effect Size
Interpreting the 'Persistence' of the data—quantifying how quickly the series returns to its mean after a random shock.
08Stability Buffer
Sample Size
Ensuring a minimum of 50 timepoints to provide the ADF math enough temporal 'History' to detect a unit root.
09The Stationarity Verdict
Reporting
Reporting the t-ADF, lag count, and p-value clearly: 'The series was non-stationary, t(df) = X.XX, p = .XXX.'
10tseries / urca Logic
Software
Executing 'adf.test()' or 'ur.df()' commands, ensuring the 'Constant' and 'Trend' components are correctly specified.
11focus
The fatal error of confusing a 'Deterministic Trend' (predictable growth) with a 'Unit Root' (random drift).
12focus
Tracing the logic back to Dickey and Fuller (1979) and the foundational evolution of modern econometric forensics.
01Hypothesis test logic

Hypotheses

Pragmatic null and alternative hypotheses defined in mathematical notation.

A hypothesis is a question sharpened to a point. Ambiguity is the enemy of inference.
Logic Core
Null · H₀

H₀: Unit root is present (series is non-stationary, has stochastic trend, γ = 0 in regression)

Alternative · Hₐ

Hₐ: No unit root (series is stationary around deterministic trend or constant, γ < 0)

Why it matters hypothesis_test

CRITICAL INTERPRETATION: ADF test has OPPOSITE interpretation from most tests. Low p-value (p < 0.05) means REJECT H₀ → series is STATIONARY (good for ARIMA). High p-value (p ≥ 0.05) means FAIL TO REJECT H₀ → series is NON-STATIONARY (need differencing). This confuses many analysts who expect p<0.05 to indicate a problem. The test is based on regression: ∆y_t = α + βt + γy_{t-1} + Σφ_i∆y_{t-i} + ε_t. Three variants: (1) No constant, no trend: tests pure random walk, (2) Constant, no trend: tests random walk with drift, (3) Constant and trend: tests trend-stationary vs difference-stationary. Test statistic γ_hat/SE(γ_hat) follows non-standard distribution (Dickey-Fuller distribution, not t-distribution) with critical values depending on sample size and model specification. MacKinnon (1996) provides numerical approximation for p-values. Key distinction from KPSS test: ADF has H₀=non-stationary, KPSS has H₀=stationary (use both for confirmation: ADF rejects + KPSS doesn't reject = definitely stationary). Key distinction from Phillips-Perron: ADF uses parametric correction for autocorrelation (lag augmentation), PP uses non-parametric correction (robust to heteroscedasticity and autocorrelation). Lag length selection critical: too few lags → residual autocorrelation (size distortion), too many lags → loss of power. Information criteria (AIC, BIC, HQIC) or significance-based methods (t-stat approach) used for lag selection.

02Model diagnostics

Assumptions

The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.

Stationarity is the prerequisite of prediction. You cannot forecast a system that is fundamentally changing its rules.
Integrity Shield
6
Assumptions
6
Critical / High Severity
How to check
Quick
Plot time series: linear processes show smooth trends or mean reversion. Non-linear: sudden jumps, threshold effects, regime changes. Plot first differences: should appear stationary if linear. If series shows exponential growth: log transform first, then test. Check for obvious non-linear patterns: cycles with changing amplitude, asymmetric responses
Rigorous
Test for non-linearity: BDS test (tests for non-linear dependence), Teräsvirta test for smooth transition, TAR test for threshold effects. Plot recursive residuals from ADF regression: should be stable. Compare ADF results with non-parametric tests. If non-linear suspected: transform data (logs, Box-Cox), or use non-linear unit root tests. Estimate ADF equation, check residuals for patterns suggesting non-linearity
If violated
Transform data: log transformation for exponential growth, Box-Cox for stabilizing variance. Difference series to remove trend, then test differences. Use non-linear unit root tests: Kapetanios-Snell-Shin (KSS) test for ESTAR non-linearity, threshold unit root tests. Consider regime-switching models or structural time series models if breaks present. For strong seasonality: seasonal adjustment before testing, or use seasonal unit root tests (HEGY test)
How to check
Quick
Start with automatic lag selection via information criteria: AIC (tends to select more lags), BIC (more parsimonious), HQIC (intermediate). Default rule: max lags = int(12*(n/100)^0.25) for monthly data. Check if test results stable across nearby lag lengths. Most software provides automatic selection
Rigorous
Test residual autocorrelation at each lag length: compute ADF at p=0,1,2,... lags, perform Ljung-Box test on residuals. If Ljung-Box p<0.05, increase lags. Compare AIC/BIC/HQIC values: select lag minimizing criterion. Use t-stat approach: start with max lag p_max, drop if last lag insignificant (t<1.6), repeat until significant. Check robustness: test at p±1, p±2 lags; if results drastically different (one rejects, one doesn't), inconclusive. Plot ADF statistic vs lag length
If violated
Too few lags (residual autocorrelation): increase lag length until Ljung-Box p>0.05, use information criteria, check ACF of residuals. Too many lags (loss of power, overfitting): reduce lags, use BIC instead of AIC (penalizes more), ensure each lag coefficient significant. If uncertain: test at multiple lag lengths, report range. Use sequential testing (t-stat) or modified information criteria (MAIC). For very autocorrelated series: Phillips-Perron test may be better (non-parametric autocorrelation correction)
How to check
Quick
Visual inspection: if series wanders around zero with no trend → no constant/trend. If mean ≠ 0 but no trend → include constant. If clear linear trend → include constant and trend. Default: start with constant + trend (most general), then test down. Plot series: does it have upward/downward trend? Does it fluctuate around non-zero level?
Rigorous
Sequential testing procedure: (1) Test with constant + trend, check if trend coefficient significant (t-test), (2) If trend insignificant: re-test with constant only, (3) If constant insignificant: re-test with neither. Use information criteria to compare specifications. Check if results consistent across specifications: if constant+trend gives stationary but constant-only gives non-stationary, indicates trend-stationary process. Examine series visually alongside test results. Elder & Kennedy (2001) provide decision rules
If violated
Over-specification (including trend when not needed): reduces test power (harder to reject unit root, more Type II errors). Fix: test without trend if series has no obvious trend. Under-specification (omitting needed trend): test statistic biased, invalid inference. Fix: include trend if visual inspection shows trend. If uncertain: report results for multiple specifications (constant only, constant+trend), check robustness. Use sequential testing to determine appropriate specification. Common practice: test with constant+trend for most economic/financial series
How to check
Quick
Plot time series: look for sudden level shifts (mean changes), slope changes (trend breaks), variance changes. Identify major events (policy changes, crises, regime shifts) that may cause breaks. Visual inspection often sufficient for obvious breaks. If break suspected: standard ADF invalid
Rigorous
Formal tests for breaks: Zivot-Andrews test (endogenously determines one break point), Perron test (tests unit root allowing for known break), Clemente-Montañés-Reyes test (two breaks), Bai-Perron test (multiple breaks). CUSUM and CUSUM-of-squares tests for parameter stability. Plot recursive residuals and coefficients from ADF regression. If break detected: use unit root tests robust to breaks (Zivot-Andrews, Perron), or model break explicitly with dummy variables, or split sample and test separately
If violated
If structural break present: standard ADF biased toward non-rejection (fails to detect stationarity around broken trend). Fixes: (1) Use Zivot-Andrews test (allows one break at unknown date), (2) Use Perron test if break date known, (3) Include break dummies in ADF regression: dummy for level shift, trend break, or both, (4) Split sample at break point, test each subsample separately, (5) Use regime-switching models. Report break date and nature (level, trend, variance). Note: multiple breaks require specialized tests
How to check
Quick
If using software with automatic lag selection via information criteria: residuals should be approximately white noise. Quick check: examine ACF of residuals, look for spikes beyond confidence bands. Most important at low lags (1-5). If residuals autocorrelated, need more lags
Rigorous
After fitting ADF equation at chosen lag length p: extract residuals, perform Ljung-Box test for autocorrelation at multiple lags (typically 10-20). H₀=no autocorrelation, p>0.05 indicates white noise (good). Plot ACF and PACF of residuals: all lags should be within confidence bands. Check for heteroscedasticity: plot squared residuals, ARCH test. If autocorrelation remains: increase lag length. Portmanteau tests: Box-Pierce, Ljung-Box
If violated
If residuals show autocorrelation: (1) Increase lag length p until Ljung-Box p>0.05, (2) Use information criteria (AIC/BIC) to select lags automatically, (3) Check if maximum lag sufficient: try higher max lag. If heteroscedasticity detected: use robust standard errors, or Phillips-Perron test (robust to heteroscedasticity). If residuals non-normal: ADF still valid (based on asymptotic theory), but small-sample power may be affected. Never proceed with autocorrelated residuals: test invalid
How to check
Quick
Check n: minimum 50 observations recommended, preferably 100+. For n<50: test has low power (may fail to reject even when stationary), critical values less accurate. Larger samples → more reliable inference. Monthly data: ≥5 years. Quarterly: ≥20 years. Annual: often insufficient for ADF unless very long span
Rigorous
Consider effective sample size: n_eff = n - p - k where p=lags, k=deterministic components. Need n_eff ≥ 50. For seasonal data: need multiple seasons (≥3 years monthly, ≥10 years quarterly). Power analysis: small samples have low power to reject unit root (high Type II error). Bootstrap critical values for small samples. Check if lag length p not too large relative to n: rule of thumb p ≤ n/10. Compare to Monte Carlo critical values for exact sample size
If violated
If n<50: (1) Acknowledge low power in interpretation, (2) Use finite-sample critical values (not asymptotic), (3) Consider DF-GLS test (better small-sample power), (4) Bootstrap critical values, (5) Combine with KPSS test and visual inspection, (6) Use fewer lags (reduces degrees of freedom loss), (7) Report results cautiously: failure to reject may reflect low power, not true unit root. If n<30: ADF very unreliable; rely more on visual inspection and alternative evidence. Consider collecting more data
03Residual Forensics

Diagnostics

Checking residual plots and indices to examine model deviations and ensure standard error integrity.

White noise is the goal. If your residuals have a pattern, you have left information on the table.
System Health
Essential checks
  1. Lag length audit using AIC/BIC to ensure residual white noise in the test model.
  2. Comparison of 'Intercept', 'Trend', and 'None' specifications.
  3. T-ADF statistic vs. Dickey-Fuller specialized critical values.
  4. Residual serial correlation check (Ljung-Box) on the ADF regression residuals.
  5. Differencing audit: Checking d=0 vs. d=1 to confirm unit root removal.
Recommended checks
  1. KPSS test comparison to reverse the burden of proof (Null = Stationary).
  2. Phillips-Perron (PP) test for robustness against weak heteroskedasticity.
  3. Zivot-Andrews test if structural breaks are suspected of mimicking a unit root.
  4. Visual pulse-reversion analysis: mapping the speed of decay after a random shock.
  5. Sensitivity audit to the lag-selection method (e.g., Schwert benchmark).
04Live Instances

Applied Minds

Review concrete study examples, data layout guidelines, and copy executable syntax scripts.

Theory is the map. Practice is the terrain. Simulation bridges the gap.
Applied Wisdom
Example 01

Testing Stationarity of Stock Prices vs Returns

Apply ADF test to stock price levels (expected non-stationary) and returns (expected stationary) to demonstrate classic unit root behavior. Generate n=500 daily observations: prices follow random walk (unit root), returns are stationary. Test with different specifications (constant, constant+trend, none), demonstrate lag selection via AIC, compare to KPSS test for confirmation, visualize data and interpret conflicting results. Shows why raw prices are non-stationary (need differencing for ARIMA) while returns are stationary (can model directly). Includes comprehensive diagnostics, critical value comparisons, and common interpretation pitfalls.

# ============================================================================
# AUGMENTED DICKEY-FULLER TEST: Stock Prices vs Returns Stationarity
# ============================================================================
# Demonstrates: ADF test on non-stationary (prices) and stationary (returns)
# Data: 500 daily stock prices (random walk) and returns (stationary)
# Key: Shows OPPOSITE interpretation (p<0.05 = stationary, NOT a problem)
# ============================================================================

# Load required packages
library(tseries)       # adf.test function
library(urca)          # ur.df for detailed ADF, KPSS test
library(ggplot2)       # visualization
library(gridExtra)     # multiple plots
library(forecast)      # Acf, Pacf functions

set.seed(42)

# ============================================================================
# 1. DATA GENERATION: Random Walk (Non-Stationary) and Returns (Stationary)
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("AUGMENTED DICKEY-FULLER TEST: Testing Stationarity\n")
cat("=", rep("=", 78), "\n\n", sep="")

# Generate stock price as random walk (non-stationary)
n <- 500  # 500 daily observations (~2 years of trading days)
initial_price <- 100
returns_raw <- rnorm(n, mean = 0.0005, sd = 0.02)  # mean ~0.05% daily return

# Price as random walk: P_t = P_{t-1} + ε_t (unit root process)
prices <- numeric(n + 1)
prices[1] <- initial_price
for (t in 2:(n + 1)) {
  prices[t] <- prices[t - 1] * (1 + returns_raw[t - 1])
}
prices <- prices[-1]  # Remove initial value

# Calculate returns: R_t = (P_t - P_{t-1}) / P_{t-1}
returns <- diff(log(prices))  # Log returns (stationary)

cat("Data generated:\n")
cat("  Stock prices: n =", n, "observations(random walk, NON-STATIONARY)\n")
cat("  Returns: n =", length(returns), "observations(should be STATIONARY)\n\n")

cat("Price summary:\n")
cat("  Start price:", round(prices[1], 2), "\n")
cat("  End price:", round(prices[n], 2), "\n")
cat("  Mean price:", round(mean(prices), 2), "\n")
cat("  SD price:", round(sd(prices), 2), "\n\n")

cat("Return summary:\n")
cat("  Mean return:", round(mean(returns), 6), "(~0 for stationary)\n")
cat("  SD return:", round(sd(returns), 4), "\n\n")

# ============================================================================
# 2. VISUAL INSPECTION: First Step in Stationarity Assessment
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("VISUAL INSPECTION\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("Visual characteristics of non-stationary vs stationary:\n")
cat("  Non-stationary(prices): wandering mean, no tendency to revert\n")
cat("  Stationary(returns): constant mean(~0), constant variance, mean-reverting\n\n")

# Create visualization
par(mfrow = c(3, 2), mar = c(4, 4, 3, 2))

# Prices time series
plot(prices, type = "l", col = "steelblue", lwd = 2,
     main = "Stock Prices(Non-Stationary)",
     xlab = "Time(days)", ylab = "Price")
grid()

# Returns time series
plot(returns, type = "l", col = "darkgreen", lwd = 1,
     main = "Log Returns(Stationary)",
     xlab = "Time(days)", ylab = "Return")
abline(h = 0, col = "red", lty = 2, lwd = 2)
grid()

# ACF of prices (slow decay indicates non-stationarity)
Acf(prices, lag.max = 50, main = "ACF: Prices", col = "steelblue", lwd = 2)

# ACF of returns (rapid decay indicates stationarity)
Acf(returns, lag.max = 50, main = "ACF: Returns", col = "darkgreen", lwd = 2)

# Histogram of prices
hist(prices, breaks = 30, col = "lightblue", border = "white",
     main = "Distribution: Prices", xlab = "Price", freq = FALSE)
lines(density(prices), col = "darkblue", lwd = 2)

# Histogram of returns
hist(returns, breaks = 30, col = "lightgreen", border = "white",
     main = "Distribution: Returns", xlab = "Return", freq = FALSE)
curve(dnorm(x, mean(returns), sd(returns)), add = TRUE, col = "darkred", lwd = 2)

par(mfrow = c(1, 1))

cat("Observations from visual inspection:\n")
cat("  Prices: Wandering pattern, ACF decays very slowly → NON-STATIONARY\n")
cat("  Returns: Mean-reverting around 0, ACF decays quickly → STATIONARY\n\n")

# ============================================================================
# 3. ADF TEST ON PRICES: Expect to FAIL TO REJECT (Non-Stationary)
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("ADF TEST: STOCK PRICES(Non-Stationary Expected)\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("CRITICAL INTERPRETATION NOTE:\n")
cat("  H₀: Unit root present(NON-STATIONARY)\n")
cat("  Hₐ: No unit root(STATIONARY)\n")
cat("  → p < 0.05: REJECT H₀ → Series IS stationary(GOOD for ARIMA)\n")
cat("  → p ≥ 0.05: FAIL to reject H₀ → Series is NON-stationary(need differencing)\n")
cat("  THIS IS OPPOSITE of typical hypothesis tests!\n\n")

# Simple ADF test using tseries package
cat("----- Simple ADF Test(tseries::adf.test) -----\n\n")
adf_prices_simple <- adf.test(prices, alternative = "stationary")
print(adf_prices_simple)

cat("\nInterpretation:\n")
if (adf_prices_simple$p.value >= 0.05) {
  cat("  p-value =", round(adf_prices_simple$p.value, 4), "≥ 0.05\n")
  cat("  → FAIL to reject H₀\n")
  cat("  → Series is NON-STATIONARY(has unit root)\n")
  cat("  → Need to difference before using ARIMA\n\n")
} else {
  cat("  p-value =", round(adf_prices_simple$p.value, 4), "< 0.05\n")
  cat("  → REJECT H₀\n")
  cat("  → Series is STATIONARY\n\n")
}

# Detailed ADF test using urca package (more control)
cat("----- Detailed ADF Test(urca::ur.df) -----\n\n")
cat("Testing three specifications:\n")
cat("  1. 'none': No constant, no trend(pure random walk test)\n")
cat("  2. 'drift': Constant, no trend(random walk with drift)\n")
cat("  3. 'trend': Constant and trend(trend-stationary vs difference-stationary)\n\n")

# Test with constant and trend (most common)
cat("Specification: Constant + Trend\n")
cat("Model: Δy_t = α + βt + γy_{t-1} + Σφ_iΔy_{t-i} + ε_t\n\n")

adf_prices_trend <- ur.df(prices, type = "trend", lags = 10, selectlags = "AIC")
summary(adf_prices_trend)

cat("\nSelected lag length(by AIC):", adf_prices_trend@lags, "\n")
cat("Test statistic(tau3):", round(adf_prices_trend@teststat[1], 4), "\n")
cat("Critical values:\n")
print(adf_prices_trend@cval)

cat("\nInterpretation:\n")
if (adf_prices_trend@teststat[1] > adf_prices_trend@cval["5pct", "tau3"]) {
  cat("  Test statistic(", round(adf_prices_trend@teststat[1], 4), 
      ") > critical value(", adf_prices_trend@cval["5pct", "tau3"], ")\n", sep="")
  cat("  → FAIL to reject H₀\n")
  cat("  → Series is NON-STATIONARY\n\n")
} else {
  cat("  Test statistic < critical value\n")
  cat("  → REJECT H₀\n")
  cat("  → Series is STATIONARY\n\n")
}

# Test with constant only (no trend)
cat("----- Specification: Constant only(no trend) -----\n\n")
adf_prices_drift <- ur.df(prices, type = "drift", lags = 10, selectlags = "AIC")
cat("Test statistic(tau2):", round(adf_prices_drift@teststat[1], 4), "\n")
cat("Critical value(5%):", adf_prices_drift@cval["5pct", "tau2"], "\n")

if (adf_prices_drift@teststat[1] > adf_prices_drift@cval["5pct", "tau2"]) {
  cat("→ FAIL to reject H₀ (NON-STATIONARY)\n\n")
} else {
  cat("→ REJECT H₀ (STATIONARY)\n\n")
}

# Test without constant or trend
cat("----- Specification: No constant, no trend -----\n\n")
adf_prices_none <- ur.df(prices, type = "none", lags = 10, selectlags = "AIC")
cat("Test statistic(tau1):", round(adf_prices_none@teststat[1], 4), "\n")
cat("Critical value(5%):", adf_prices_none@cval["5pct", "tau1"], "\n")

if (adf_prices_none@teststat[1] > adf_prices_none@cval["5pct", "tau1"]) {
  cat("→ FAIL to reject H₀ (NON-STATIONARY)\n\n")
} else {
  cat("→ REJECT H₀ (STATIONARY)\n\n")
}

cat("CONCLUSION FOR PRICES:\n")
cat("  All three specifications fail to reject unit root\n")
cat("  → Stock prices are NON-STATIONARY(random walk)\n")
cat("  → Must difference before applying ARIMA or other stationary methods\n\n")

# ============================================================================
# 4. ADF TEST ON RETURNS: Expect to REJECT (Stationary)
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("ADF TEST: RETURNS(Stationary Expected)\n")
cat("=", rep("=", 78), "\n\n", sep="")

# Simple ADF test
cat("----- Simple ADF Test(tseries::adf.test) -----\n\n")
adf_returns_simple <- adf.test(returns, alternative = "stationary")
print(adf_returns_simple)

cat("\nInterpretation:\n")
if (adf_returns_simple$p.value < 0.05) {
  cat("  p-value =", round(adf_returns_simple$p.value, 4), "< 0.05\n")
  cat("  → REJECT H₀\n")
  cat("  → Series is STATIONARY(no unit root)\n")
  cat("  → Can use returns directly for modeling(ARMA, regression, etc.)\n\n")
} else {
  cat("  p-value =", round(adf_returns_simple$p.value, 4), "≥ 0.05\n")
  cat("  → FAIL to reject H₀\n")
  cat("  → Series appears NON-STATIONARY(unexpected for returns)\n\n")
}

# Detailed ADF test with constant (typical for returns)
cat("----- Detailed ADF Test: Constant only -----\n\n")
adf_returns_drift <- ur.df(returns, type = "drift", lags = 10, selectlags = "AIC")
summary(adf_returns_drift)

cat("\nSelected lag length:", adf_returns_drift@lags, "\n")
cat("Test statistic(tau2):", round(adf_returns_drift@teststat[1], 4), "\n")
cat("Critical values:\n")
print(adf_returns_drift@cval)

cat("\nInterpretation:\n")
if (adf_returns_drift@teststat[1] < adf_returns_drift@cval["5pct", "tau2"]) {
  cat("  Test statistic(", round(adf_returns_drift@teststat[1], 4), 
      ") < critical value(", adf_returns_drift@cval["5pct", "tau2"], ")\n", sep="")
  cat("  → REJECT H₀\n")
  cat("  → Returns are STATIONARY\n\n")
} else {
  cat("  Test statistic > critical value\n")
  cat("  → FAIL to reject H₀\n")
  cat("  → Returns appear NON-STATIONARY(unusual)\n\n")
}

cat("CONCLUSION FOR RETURNS:\n")
cat("  Strong rejection of unit root hypothesis\n")
cat("  → Returns are STATIONARY\n")
cat("  → Differencing prices(taking returns) achieved stationarity\n")
cat("  → This is the standard finding: prices I(1), returns I(0)\n\n")

# ============================================================================
# 5. LAG LENGTH SELECTION: Sensitivity Analysis
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("LAG LENGTH SELECTION SENSITIVITY\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("Testing how lag length affects ADF results for prices...\n\n")

# Test prices at different lag lengths
lag_range <- 0:15
test_stats <- numeric(length(lag_range))
aic_values <- numeric(length(lag_range))
bic_values <- numeric(length(lag_range))

for (i in seq_along(lag_range)) {
  p <- lag_range[i]
  adf_temp <- ur.df(prices, type = "drift", lags = p)
  test_stats[i] <- adf_temp@teststat[1]
  # Extract residuals to compute AIC/BIC
  resid <- residuals(adf_temp@testreg)
  n_eff <- length(resid)
  k <- p + 2  # lags + constant + lagged level
  aic_values[i] <- n_eff * log(sum(resid^2) / n_eff) + 2 * k
  bic_values[i] <- n_eff * log(sum(resid^2) / n_eff) + k * log(n_eff)
}

cat("Lag  Test Stat     AIC        BIC\n")
cat("----------------------------------------\n")
for (i in seq_along(lag_range)) {
  cat(sprintf("%3d  %9.4f  %9.2f  %9.2f", lag_range[i], test_stats[i], 
              aic_values[i], bic_values[i]))
  if (i == which.min(aic_values)) cat("  <- AIC minimum")
  if (i == which.min(bic_values)) cat("  <- BIC minimum")
  cat("\n")
}

optimal_lag_aic <- lag_range[which.min(aic_values)]
optimal_lag_bic <- lag_range[which.min(bic_values)]

cat("\nOptimal lag by AIC:", optimal_lag_aic, "\n")
cat("Optimal lag by BIC:", optimal_lag_bic, "\n")
cat("\nNote: BIC tends to select fewer lags(more parsimonious)\n")
cat("      AIC tends to select more lags(better fit)\n\n")

# Plot test statistic vs lag length
par(mfrow = c(1, 2))
plot(lag_range, test_stats, type = "b", col = "darkblue", lwd = 2,
     main = "ADF Test Statistic vs Lag Length",
     xlab = "Lag Length", ylab = "Test Statistic",
     ylim = c(min(test_stats, -3.5), max(test_stats)))
abline(h = -2.86, col = "red", lty = 2, lwd = 2)  # Approximate 5% critical value
text(max(lag_range) * 0.7, -2.86, "5% Critical Value", pos = 3, col = "red")
grid()

plot(lag_range, aic_values, type = "b", col = "darkgreen", lwd = 2,
     main = "AIC vs Lag Length",
     xlab = "Lag Length", ylab = "AIC")
points(optimal_lag_aic, aic_values[optimal_lag_aic + 1], 
       col = "red", pch = 19, cex = 2)
text(optimal_lag_aic, aic_values[optimal_lag_aic + 1], 
     paste("Min =", optimal_lag_aic), pos = 4, col = "red")
grid()

par(mfrow = c(1, 1))

# ============================================================================
# 6. KPSS TEST: Complementary Test (H₀ = Stationary)
# ============================================================================

cat("\n", rep("=", 79), "\n", sep="")
cat("KPSS TEST: Confirmation via Complementary Test\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("KPSS Test has OPPOSITE null hypothesis:\n")
cat("  H₀: Series is STATIONARY(level or trend stationary)\n")
cat("  Hₐ: Series is NON-STATIONARY(unit root)\n")
cat("  → p < 0.05: REJECT H₀ → NON-stationary\n")
cat("  → p ≥ 0.05: FAIL to reject H₀ → Stationary\n\n")

cat("Combined interpretation:\n")
cat("  ADF rejects + KPSS doesn't reject → Definitely STATIONARY\n")
cat("  ADF doesn't reject + KPSS rejects → Definitely NON-STATIONARY\n")
cat("  Both reject or both don't reject → Inconclusive\n\n")

# KPSS test on prices
cat("----- KPSS Test: Prices -----\n\n")
kpss_prices <- kpss.test(prices, null = "Trend")
print(kpss_prices)

cat("\nInterpretation for prices:\n")
if (kpss_prices$p.value < 0.05) {
  cat("  KPSS p-value < 0.05 → REJECT H₀ → NON-STATIONARY\n")
  cat("  Combined with ADF(failed to reject) → Prices are NON-STATIONARY\n\n")
} else {
  cat("  KPSS p-value ≥ 0.05 → FAIL to reject H₀ → STATIONARY\n")
  cat("  Conflicts with ADF result → Inconclusive\n\n")
}

# KPSS test on returns
cat("----- KPSS Test: Returns -----\n\n")
kpss_returns <- kpss.test(returns, null = "Level")
print(kpss_returns)

cat("\nInterpretation for returns:\n")
if (kpss_returns$p.value >= 0.05) {
  cat("  KPSS p-value ≥ 0.05 → FAIL to reject H₀ → STATIONARY\n")
  cat("  Combined with ADF(rejected) → Returns are STATIONARY\n\n")
} else {
  cat("  KPSS p-value < 0.05 → REJECT H₀ → NON-STATIONARY\n")
  cat("  Conflicts with ADF result → Inconclusive\n\n")
}

# ============================================================================
# 7. PHILLIPS-PERRON TEST: Alternative to ADF
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("PHILLIPS-PERRON TEST: Robust to Heteroscedasticity\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("PP test vs ADF:\n")
cat("  - Both test for unit root(same hypotheses)\n")
cat("  - ADF: parametric(includes lags to correct autocorrelation)\n")
cat("  - PP: non-parametric(uses Newey-West correction)\n")
cat("  - PP: more robust to heteroscedasticity and autocorrelation\n\n")

# PP test on prices
cat("----- PP Test: Prices -----\n\n")
pp_prices <- PP.test(prices)
print(pp_prices)

if (pp_prices$p.value >= 0.05) {
  cat("  → NON-STATIONARY(consistent with ADF)\n\n")
} else {
  cat("  → STATIONARY(differs from ADF)\n\n")
}

# PP test on returns
cat("----- PP Test: Returns -----\n\n")
pp_returns <- PP.test(returns)
print(pp_returns)

if (pp_returns$p.value < 0.05) {
  cat("  → STATIONARY(consistent with ADF)\n\n")
} else {
  cat("  → NON-STATIONARY(differs from ADF)\n\n")
}

# ============================================================================
# 8. DIFFERENCING TO ACHIEVE STATIONARITY
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("ACHIEVING STATIONARITY VIA DIFFERENCING\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("Since prices are non-stationary, apply first difference:\n")
cat("  First difference: Δy_t = y_t - y_{t-1}\n")
cat("  For prices: this gives(approximately) returns\n\n")

# First difference of prices (approximately returns)
prices_diff1 <- diff(prices)

cat("Testing first-differenced prices...\n\n")
adf_diff1 <- adf.test(prices_diff1)
print(adf_diff1)

cat("\nInterpretation:\n")
if (adf_diff1$p.value < 0.05) {
  cat("  p-value < 0.05 → First difference is STATIONARY\n")
  cat("  → Prices are I(1): integrated of order 1\n")
  cat("  → One difference sufficient to achieve stationarity\n")
  cat("  → For ARIMA: use d=1\n\n")
} else {
  cat("  p-value ≥ 0.05 → First difference still non-stationary\n")
  cat("  → May need second difference(rare)\n\n")
}

# Visualize before and after differencing
par(mfrow = c(2, 2))
plot(prices, type = "l", col = "steelblue", lwd = 2,
     main = "Original Prices(Non-Stationary)",
     xlab = "Time", ylab = "Price")

Acf(prices, lag.max = 40, main = "ACF: Prices(Slow Decay)", col = "steelblue")

plot(prices_diff1, type = "l", col = "darkgreen", lwd = 1,
     main = "First Difference(Stationary)",
     xlab = "Time", ylab = "Differenced Price")
abline(h = 0, col = "red", lty = 2)

Acf(prices_diff1, lag.max = 40, main = "ACF: Differenced(Rapid Decay)", 
    col = "darkgreen")

par(mfrow = c(1, 1))

cat("Visual confirmation: Differencing removes trend, ACF decays rapidly\n\n")

# ============================================================================
# 9. SUMMARY TABLE: All Tests
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("COMPREHENSIVE SUMMARY: All Stationarity Tests\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("Test Results Summary:\n")
cat("---------------------------------------------------\n")
cat("Series         Test      Statistic   p-value   Conclusion\n")
cat("---------------------------------------------------\n")
cat(sprintf("Prices         ADF       %8.4f    %6.4f    %s\n",
            adf_prices_simple$statistic, adf_prices_simple$p.value,
            ifelse(adf_prices_simple$p.value < 0.05, "Stationary", "Non-Stationary")))
cat(sprintf("Prices         KPSS      %8.4f    %6.4f    %s\n",
            kpss_prices$statistic, kpss_prices$p.value,
            ifelse(kpss_prices$p.value < 0.05, "Non-Stationary", "Stationary")))
cat(sprintf("Prices         PP        %8.4f    %6.4f    %s\n",
            pp_prices$statistic, pp_prices$p.value,
            ifelse(pp_prices$p.value < 0.05, "Stationary", "Non-Stationary")))
cat("---------------------------------------------------\n")
cat(sprintf("Returns        ADF       %8.4f    %6.4f    %s\n",
            adf_returns_simple$statistic, adf_returns_simple$p.value,
            ifelse(adf_returns_simple$p.value < 0.05, "Stationary", "Non-Stationary")))
cat(sprintf("Returns        KPSS      %8.4f    %6.4f    %s\n",
            kpss_returns$statistic, kpss_returns$p.value,
            ifelse(kpss_returns$p.value < 0.05, "Non-Stationary", "Stationary")))
cat(sprintf("Returns        PP        %8.4f    %6.4f    %s\n",
            pp_returns$statistic, pp_returns$p.value,
            ifelse(pp_returns$p.value < 0.05, "Stationary", "Non-Stationary")))
cat("---------------------------------------------------\n")
cat(sprintf("Diff Prices    ADF       %8.4f    %6.4f    %s\n",
            adf_diff1$statistic, adf_diff1$p.value,
            ifelse(adf_diff1$p.value < 0.05, "Stationary", "Non-Stationary")))
cat("---------------------------------------------------\n\n")

# ============================================================================
# 10. FINAL INTERPRETATION AND RECOMMENDATIONS
# ============================================================================

cat("=", rep("=", 78), "\n", sep="")
cat("FINAL INTERPRETATION\n")
cat("=", rep("=", 78), "\n\n", sep="")

cat("KEY FINDINGS:\n")
cat("\n1. STOCK PRICES(Level):\n")
cat("   - ADF test: FAIL to reject unit root(p ≥ 0.05)\n")
cat("   - KPSS test: REJECT stationarity(p < 0.05)\n")
cat("   - Conclusion: Prices are NON-STATIONARY(random walk)\n")
cat("   - Implication: Cannot use in regression/ARIMA without differencing\n")
cat("   - Why: Prices have stochastic trend(no mean reversion)\n\n")

cat("2. RETURNS(First Difference of Prices):\n")
cat("   - ADF test: REJECT unit root(p < 0.05)\n")
cat("   - KPSS test: FAIL to reject stationarity(p ≥ 0.05)\n")
cat("   - Conclusion: Returns are STATIONARY\n")
cat("   - Implication: Can use returns directly for modeling\n")
cat("   - Why: Returns fluctuate around constant mean(mean-reverting)\n\n")

cat("3. INTEGRATION ORDER:\n")
cat("   - Prices are I(1): integrated of order 1\n")
cat("   - Returns are I(0): stationary in levels\n")
cat("   - One difference transforms I(1) → I(0)\n\n")

cat("PRACTICAL RECOMMENDATIONS:\n")
cat("\n1. For ARIMA modeling:\n")
cat("   - Use returns directly: ARMA(p,q) model\n")
cat("   - OR use prices with d=1: ARIMA(p,1,q) model\n")
cat("   - Both approaches equivalent for forecasting returns\n\n")

cat("2. For regression analysis:\n")
cat("   - Do NOT regress non-stationary prices on non-stationary predictors\n")
cat("   - Leads to spurious regression(Granger & Newbold, 1974)\n")
cat("   - Use stationary returns, or test for cointegration\n\n")

cat("3. For risk modeling:\n")
cat("   - Returns are appropriate(stationary, mean ~0)\n")
cat("   - Volatility models(GARCH) use returns\n")
cat("   - Value-at-Risk calculations use return distribution\n\n")

cat("COMMON INTERPRETATION PITFALL:\n")
cat("  ✗ WRONG: 'ADF p-value < 0.05, so there's a problem'\n")
cat("  ✓ RIGHT: 'ADF p-value < 0.05, so series IS stationary(good!)'\n")
cat("  Remember: ADF tests H₀=non-stationary, so reject = stationary\n\n")

cat("=", rep("=", 78), "\n", sep="")
cat("ADF TEST ANALYSIS COMPLETE\n")
cat("=", rep("=", 78), "\n")

# ============================================================================
# END OF ADF TEST EXAMPLE
# ============================================================================
05Tactical Pivots

Alternatives

Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.

When the path is blocked, pivot. Rigor is not rigidity; it is the intelligent adaptation to reality.
Adaptive Strategy
Measurement Precision Ladder Ideal · Continuous Sequential Matrix
Ratio
Maintain ADF logic. Essential for auditing the stationarity pulse of high-fidelity markers.
Peak Signal
Interval
Ideal for Primary Audit. Ensure the 'Temporal Density' is sufficient to observe mean-reversion.
Standard Precision
Nominal / Ordinal
Abandon Unit-Root Tests. Use Markov-Chain audits or Runs tests to model categorical randomness.
Information Suicide
Temporal Trajectory Audit Static Stationarity Audit
Static Pulse
Single unit root.
Stay with ADF. Verify if the series is stable or drifting.
Multi-Unit Pulse
Clustered stationarity.
Pivot to Panel Unit Root tests (Levin-Lin-Chu) to audit stability across multiple subjects simultaneously.
Adaptive Technical Safeguards · adaptive safeguards
non stationary confirmed
  • First-Difference Strike (d=1) — Mathematically level the series to achieve stationarity.
  • Log-Transformation — Neutralize exponential drift in the temporal mean.
structural breaks present
  • Zivot-Andrews Test — Switch if a 'Policy Shift' or 'Clinical Event' is mimicking a unit root.
  • Chow Test — Audit the stability of parameters across different temporal epochs.
weak heteroscedasticity
  • Phillips-Perron (PP) Test — A more robust alternative that accounts for serial correlation and variance shifts automatically.
06Adjusted Comparisons

Post-hoc

Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.

The omnibus test opens the door; post-hoc analysis explores the room.
Forensic Detail
Adjusted Comparisons
  • Vary lag length using AIC/BIC or t-sig method
  • Compare with Phillips-Perron test (robust to heteroskedasticity)
  • Compare with KPSS test (null is stationarity - opposite null)
  • Test with/without trend and constant terms
  • Use Zivot-Andrews test if structural break suspected
Interpretation Guidelines

ADF tests for unit root (non-stationarity). Post-hoc tests are not applicable.

07Standardized scale impact

Effect Size

Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.

Significance is noise. Magnitude is the signal. Measure the impact, not just the probability.
Impact Magnitude
N/A
Recommended Measure
08Statistical Power

Sample Size

Guidelines for minimum sample requirements and power analysis parameters.

An underpowered study is an ethical failure. Respect the data by collecting enough of it.
Power Protocol
Floor Requirements

The 'Stability Buffer': A minimum of 50 timepoints is required. Unit root tests (ADF) lose significant power and reliability if the temporal history is too shallow to observe the 'Mean-Reversion'.

Effect SizeParametersRequired n
Small EffectHigh Persistence (Rho=0.95)n ≈ 250
Medium EffectModerate Persistence (Rho=0.80)n ≈ 100
Large EffectLow Persistence (Rho=0.50)n ≈ 50
Key considerations

The 'Lag Penalty': Every lag added to the ADF model to neutralize serial correlation consumes a degree of freedom. If your data is highly correlated, you need a 30% larger sample to maintain the same stationarity-detection power.

G*Power StrategyBenchmark: Unit root testing (ADF). Parameters: Rho (Persistence), Trend type, α = .05, Power = .80. Note: ADF power depends on how close the data is to a 'Random Walk' (Rho = 1.0).
09APA narrative blueprint

Reporting

How to compile statistical results into publication prose matching APA and journal style guides.

Data does not speak for itself. It requires a translator. Be clear, be precise, be honest.
Narrative Arc
10Exhibit Builder

Manuscript Lab

Copy standard summary tables and forensic reporting grids to outline analysis details.

Table 1: Augmented Dickey-Fuller (ADF) Test for Unit Roots
VariableADF StatisticCritical Value (5%)p-valueStatus
Raw Price-1.24-2.86.652Non-Stationary
Log Return-12.45-2.86< .001Stationary
Note. Null Hypothesis: Series is Non-Stationary (Drifting).
p = .652 (Raw)Confirms 'Random Walk'. Raw prices are drifting and cannot be modeled directly without differencing.
p < .001 (Return)Identifies Mean-Reverting Signal. By calculating returns, we've achieved a stable series ready for forecasting.
Header glossary

The Stability Score. More negative values indicate a stronger rejection of the unit root (more stationary).

Predictable Variance. Means the mean and variance of the series are constant over time—a requirement for ARIMA.

11Algorithmic Logic

Command Center

Syntax libraries and function parameters for executing calculations in stats packages.

Code is the modern laboratory. Clean execution ensures reproducible discovery.
Execution Engine
# 1. Execute ADF Test
tseries::adf.test(ts_data)

# 2. Advanced ADF (Drift and Trend Audit)
urca::ur.df(ts_data, type = 'trend', selectlags = 'AIC')
Library stack
R
tseriesurca
Python
statsmodels.tsa.stattools
Elite Forensic Strike

If ADF says 'Non-Stationary', don't panic. Difference the data ($y_t - y_{t-1}$) and test again. This is the 'I' in ARIMA.

# Automated Differencing Audit
forecast::ndiffs(ts_data)
12The Over-adjustment Trap

Common Mistakes

Analytical caveats and corrections to maintain modeling integrity.

Wisdom is learning from the failures of others. Anticipate the error before it occurs.
Defensive Logic
Why it's wrong
ADF test is OPPOSITE of typical tests. H₀=non-stationary, so p<0.05 means REJECT non-stationarity → series IS stationary (which is GOOD for ARIMA). Many analysts misinterpret, thinking p<0.05 indicates a problem. This backwards interpretation leads to wrong conclusions: treating stationary series as non-stationary (unnecessary differencing) or vice versa
The correction
Remember: ADF H₀ = unit root (non-stationary). Low p-value (p<0.05) = reject H₀ = NO unit root = STATIONARY = good for modeling. High p-value (p≥0.05) = fail to reject = unit root present = NON-STATIONARY = need differencing. Helpful mnemonic: 'ADF rejection = stationary'. Always state interpretation explicitly: 'p<0.05, reject H₀, series is stationary'. Compare to KPSS (H₀=stationary) to avoid confusion
Why it's wrong
Including trend in ADF regression when series has no trend reduces test power (harder to reject unit root, more Type II errors, fail to detect stationarity when present). Conversely, omitting trend when needed causes size distortion (spurious rejection). Over-specification (constant+trend for mean-zero series) wastes degrees of freedom. Under-specification (no constant for series with non-zero mean) biases test statistic
The correction
Visual inspection first: plot series. If wanders around zero with no trend → use 'none' or 'c'. If non-zero mean but no trend → use 'c' (constant). If clear linear trend → use 'ct' (constant+trend). Default for most economic/financial data: 'c' or 'ct'. Use sequential testing: start with most general (ct), test trend significance, drop if insignificant. Report results for multiple specifications to check robustness. If results differ drastically across specs: inconclusive, need more investigation
Why it's wrong
If too few lags included in ADF regression, residuals show autocorrelation. This violates test assumptions, causes size distortion (test rejects too often under null, spurious stationarity findings). Residual autocorrelation means test equation inadequate: hasn't fully captured dynamics. Standard errors wrong, test statistic biased. Particularly problematic for high-frequency data (daily, weekly) with strong autocorrelation
The correction
Use automatic lag selection: AIC (includes more lags, better fit), BIC (fewer lags, more parsimonious), HQIC (intermediate). After testing, check residuals: Ljung-Box test for autocorrelation (p>0.05 desired). If Ljung-Box p<0.05: increase lags. Plot ACF of residuals: should be within confidence bands. Rule of thumb for max lag: 12*(n/100)^0.25 for monthly, sqrt(n) for annual. Robust approach: test at multiple lag lengths, check consistency
Why it's wrong
Including too many lags wastes degrees of freedom, reduces effective sample size, decreases test power (harder to reject unit root even when stationary), increases parameter estimation uncertainty. Over-parameterized model: some lag coefficients insignificant. Particularly problematic for small samples: few observations left after lags. Can make stationary series appear non-stationary (Type II error)
The correction
Use BIC for lag selection (penalizes complexity more than AIC, selects fewer lags). Check if individual lag coefficients significant: if last few lags all insignificant, reduce lag length. Sequential testing: start with max lag p_max, test if pth lag significant (t>1.6 in absolute value), if not drop and re-test at p-1, continue until significant. Balance: enough lags to remove autocorrelation (check Ljung-Box), but not so many as to lose power. Typical: p≤10 for monthly data, p≤4 for quarterly
Why it's wrong
Relying solely on ADF can be misleading: test has low power in small samples (fails to reject even when stationary), sensitive to specification, may give inconclusive results. Without confirmation, uncertain if failure to reject reflects true unit root or just low power. One test insufficient for definitive conclusion about stationarity. Different tests have different power against different alternatives
The correction
ALWAYS use KPSS as complementary test. KPSS has opposite null (H₀=stationary), so combined interpretation stronger: (1) ADF rejects + KPSS doesn't reject = definitely stationary. (2) ADF doesn't reject + KPSS rejects = definitely non-stationary. (3) Both reject = inconclusive (series neither I(0) nor I(1), possibly fractionally integrated). (4) Both don't reject = inconclusive (low power). Also consider: Phillips-Perron test (robust to heteroscedasticity), visual inspection (plot series, ACF), variance ratio test
Why it's wrong
Structural breaks (sudden shifts in mean, trend, or variance) bias ADF test toward non-rejection. Standard ADF has low power when breaks present: fails to reject unit root even when series is stationary around broken trend. Perron (1989) showed most macroeconomic series become stationary when breaks accounted for. Treating break as unit root leads to over-differencing, model misspecification, poor forecasts. Break makes series look non-stationary when actually trend-stationary with break
The correction
Visually inspect for breaks: plot series, look for sudden level shifts (mean changes), slope changes (trend breaks), variance changes. Identify major events (recessions, policy changes, crises). If break suspected: use break-robust tests: (1) Zivot-Andrews test (endogenously determines break date), (2) Perron test (if break date known), (3) Lee-Strazicich test (two breaks), (4) Clemente-Montañés-Reyes test. Or include break dummies in ADF regression: impulse dummy (additive outlier), step dummy (level shift), trend break. Report break date and nature
Why it's wrong
ADF test requires adequate sample size for reliable inference. Small samples (n<50): test has very low power (almost never rejects unit root, even for stationary series), critical values less accurate (asymptotic approximation poor), high Type II error rate. For n<30: ADF extremely unreliable, dominated by finite-sample issues. Economic significance: may need decades of annual data, 5+ years of monthly data. Seasonal models need multiple seasons
The correction
Check sample size: n≥50 minimum, n≥100 preferred. For small samples: (1) Use DF-GLS test (better small-sample power than ADF), (2) Use finite-sample critical values (not asymptotic), (3) Bootstrap critical values, (4) Rely more on KPSS + visual inspection, (5) Report cautiously: 'failure to reject may reflect low power, not true unit root'. For n<30: acknowledge test unreliable. Monthly data: ≥5 years. Quarterly: ≥15 years. Annual: often insufficient unless very long span. Consider collecting more data
Why it's wrong
Many economic/financial series better analyzed after transformation: exponential growth (GDP, stock prices) should be logged first. Testing levels when growth is exponential: non-stationarity from increasing variance, not just mean. Variance non-stationarity violates test assumptions. Log transform linearizes exponential trend, stabilizes variance. Testing untransformed data: misleading conclusions, poor model fit, heteroscedastic residuals
The correction
Theory-driven transformation: if series shows exponential growth (GDP, prices, population), use log transform before testing. Log advantages: (1) variance stabilization, (2) linearizes exponential trend, (3) log differences = growth rates (stationary). For non-negative series with changing variance: Box-Cox transformation. Test transformed series: ADF on log(y) rather than y. Then interpret: if log(y) is I(1), growth rate is I(0). Visual check: does log transform make series look more stationary? Plot variance over time
Why it's wrong
After finding non-stationarity in levels, blindly assuming first difference is stationary without testing. But what if series is I(2) (needs two differences)? Or first difference still has structural issues? Using I(2) series with d=1 in ARIMA: residuals non-stationary, invalid inference, poor forecasts. Over-differencing (d=2 when d=1 sufficient): inflates variance, creates artificial MA structure, degrades forecasts. Always verify differencing achieved stationarity
The correction
After finding levels non-stationary: (1) Take first difference: Δy_t = y_t - y_{t-1}. (2) Test first difference with ADF: should reject unit root (p<0.05). (3) If still non-stationary: try second difference (rare), or check for breaks/outliers. (4) Check ACF of differenced series: should decay rapidly. (5) KPSS test on difference for confirmation. (6) Visual inspection: differenced series should look stationary. Most economic series I(1), so first difference usually sufficient. Second difference (d=2) very rare; if needed, investigate data quality
13Academic Lineage

References

Scholarly lineage and citation keys grounding the statistical framework.

We stand on the shoulders of giants. Honor the source of the method.
Academic Lineage
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
If the starting line is constantly moving, the race has no meaning. Audit the stationarity first, or you are simply measuring the drift of a random walk.
The Interpretive Rigor Directive
statminds · AugmentedMind reference · v2.2 · updated 2026-01-1715 of 15 sections