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.
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.
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.
Core Idea Diagram
Claims tested
How it works
- Formulate Dickey-Fuller regression equation including lags of differences.
- Estimate parameters using OLS regression on the time series differences.
- Calculate t-statistic for the lagged level variable coefficient.
- Compare with Dickey-Fuller critical values to check for a unit root.
Assumptions
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.
Worked Example
| Lag | ADF Stat | 5% Crit | p-value | Stationary? |
|---|---|---|---|---|
| Lag 1 | -3.42 | -2.89 | 0.011 | Yes |
| Lag 2 | -2.15 | -2.89 | 0.224 | No |
Dickey-Fuller Unit Root Laboratory
Slide the autoregressive coefficient ρ. If ρ = 1.00, the process contains a unit root (random walk), leading to non-stationarity.
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).
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: Unit root is present (series is non-stationary, has stochastic trend, γ = 0 in regression)
Hₐ: No unit root (series is stationary around deterministic trend or constant, γ < 0)
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.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Lag length audit using AIC/BIC to ensure residual white noise in the test model.
- Comparison of 'Intercept', 'Trend', and 'None' specifications.
- T-ADF statistic vs. Dickey-Fuller specialized critical values.
- Residual serial correlation check (Ljung-Box) on the ADF regression residuals.
- Differencing audit: Checking d=0 vs. d=1 to confirm unit root removal.
- KPSS test comparison to reverse the burden of proof (Null = Stationary).
- Phillips-Perron (PP) test for robustness against weak heteroskedasticity.
- Zivot-Andrews test if structural breaks are suspected of mimicking a unit root.
- Visual pulse-reversion analysis: mapping the speed of decay after a random shock.
- Sensitivity audit to the lag-selection method (e.g., Schwert benchmark).
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
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
# ============================================================================Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- First-Difference Strike (d=1) — Mathematically level the series to achieve stationarity.
- Log-Transformation — Neutralize exponential drift in the temporal mean.
- 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.
- Phillips-Perron (PP) Test — A more robust alternative that accounts for serial correlation and variance shifts automatically.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
- 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
ADF tests for unit root (non-stationarity). Post-hoc tests are not applicable.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
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 Size | Parameters | Required n |
|---|---|---|
| Small Effect | High Persistence (Rho=0.95) | n ≈ 250 |
| Medium Effect | Moderate Persistence (Rho=0.80) | n ≈ 100 |
| Large Effect | Low Persistence (Rho=0.50) | n ≈ 50 |
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.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Variable | ADF Statistic | Critical Value (5%) | p-value | Status |
|---|---|---|---|---|
| Raw Price | -1.24 | -2.86 | .652 | Non-Stationary |
| Log Return | -12.45 | -2.86 | < .001 | Stationary |
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.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 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')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)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.