# Fixed-specification test helpers for STA 542 Workbooks 9 and 10.
# Base R only. Inputs must be finite observations at equally spaced times;
# missing observations are rejected, never silently removed.
# These are approximate tests under their stated model assumptions, not
# universal tests of weak stationarity. Choose deterministic terms, ADF lags,
# and KPSS bandwidth before inspecting the resulting rejection decisions.
# ADF assumes the chosen lag augmentation adequately describes short-run
# dynamics under a unit-root null. EG additionally presumes two I(1) series
# and a specified deterministic part in the cointegrating regression.
# KPSS uses its conventional short-memory stationary null, positive long-run
# variance, and large-sample calibration. A fixed classroom bandwidth is a
# sensitivity choice, not a proof that a consistent LRV estimate was obtained.
# No automatic lag selection and no interpolated or clipped p-values.
#
# MacKinnon (2010), "Critical Values for Cointegration Tests", QED WP 1227:
# https://www.econ.queensu.ca/research/working-papers/1227
# Coefficient arrays and response-surface convention below are transcribed
# from statsmodels 0.14.5, statsmodels/tsa/adfvalues.py (N = 1 and N = 2 only):
# https://github.com/statsmodels/statsmodels/blob/v0.14.5/statsmodels/tsa/adfvalues.py
# Exception: statsmodels attributes the no-constant ADF array to MacKinnon
# (1996); that case was not updated in 2010. All constant/trend arrays used
# here are the 2010 response surfaces. Their values are approximations.
# Its BSD 3-clause notices apply to the transcribed arrays:
# Copyright (C) 2006, Jonathan E. Taylor. All rights reserved.
# Copyright (c) 2006-2008 Scipy Developers. All rights reserved.
# Copyright (c) 2009-2018 statsmodels Developers. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# a. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# b. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# c. Neither the name of statsmodels nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL STATSMODELS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
.wb_series <- function(x, name = "x") {
if (!is.numeric(x) || is.complex(x) || !is.null(dim(x)) ||
length(x) < 4L || any(!is.finite(x))) {
stop(name, " must be a finite numeric vector with at least four observations.")
}
as.numeric(x)
}
.wb_integer <- function(x, name, upper) {
if (length(x) != 1L || !is.numeric(x) || !is.finite(x) ||
x < 0 || x != floor(x) || x > upper) {
stop(name, " must be a nonnegative integer no larger than ", upper, ".")
}
as.integer(x)
}
.wb_ols <- function(y, design) {
fit <- lm.fit(design, y)
k <- ncol(design)
df <- length(y) - k
if (fit$rank < k || df <= 0L) stop("Regression is rank deficient or has no residual degrees of freedom.")
sigma2 <- sum(fit$residuals^2) / df
if (!is.finite(sigma2) || sigma2 <= 0) stop("Regression has zero or invalid residual variance.")
se_pivot <- sqrt(diag(chol2inv(qr.R(fit$qr))) * sigma2)
se <- numeric(k)
se[fit$qr$pivot] <- se_pivot
names(se) <- colnames(design)
list(coefficients = fit$coefficients, se = se,
residuals = fit$residuals, fitted = fit$fitted.values,
df = df, sigma2 = sigma2)
}
.wb_mackinnon <- function(nobs, deterministic, N = 1L) {
# Rows: 1%, 5%, 10%. Columns: a0, a1, a2, a3.
# Critical value = a0 + a1/nobs + a2/nobs^2 + a3/nobs^3.
key <- paste(N, deterministic, sep = "_")
coefficients <- switch(key,
"1_none" = c(-2.56574, -2.2358, -3.627, 0,
-1.94100, -0.2686, -3.365, 31.223,
-1.61682, 0.2656, -2.714, 25.364),
"1_constant" = c(-3.43035, -6.5393, -16.786, -79.433,
-2.86154, -2.8903, -4.234, -40.040,
-2.56677, -1.5384, -2.809, 0),
"1_trend" = c(-3.95877, -9.0531, -28.428, -134.155,
-3.41049, -4.3904, -9.036, -45.374,
-3.12705, -2.5856, -3.925, -22.380),
"2_constant" = c(-3.89644, -10.9519, -33.527, 0,
-3.33613, -6.1101, -6.823, 0,
-3.04445, -4.2412, -2.720, 0),
"2_trend" = c(-4.32762, -15.4387, -35.679, 0,
-3.78057, -9.5106, -12.074, 0,
-3.49631, -7.0815, -7.538, 21.892),
stop("Unsupported MacKinnon specification."))
a <- matrix(coefficients, nrow = 3, byrow = TRUE)
critical <- as.numeric(a %*% (1 / nobs)^(0:3))
setNames(critical, c("1%", "5%", "10%"))
}
wb_adf <- function(x, lags = 0, deterministic = c("none", "constant", "trend")) {
deterministic <- match.arg(deterministic)
x <- .wb_series(x)
n <- length(x)
ndet <- switch(deterministic, none = 0L, constant = 1L, trend = 2L)
lags <- .wb_integer(lags, "lags", floor(n / 2) - ndet - 1L)
if (max(x) == min(x)) stop("ADF is undefined for a constant series.")
dx <- diff(x)
t <- seq.int(lags + 2L, n)
design <- matrix(x[t - 1L], ncol = 1L, dimnames = list(NULL, "level"))
if (lags > 0L) {
lagged <- vapply(seq_len(lags), function(j) dx[t - j - 1L], numeric(length(t)))
colnames(lagged) <- paste0("difference_lag", seq_len(lags))
design <- cbind(design, lagged)
}
if (ndet >= 1L) design <- cbind(design, constant = 1)
if (ndet == 2L) design <- cbind(design, trend = seq_along(t))
fit <- .wb_ols(dx[t - 1L], design)
if (sum(fit$residuals^2) <= 100 * .Machine$double.eps^2 * sum(dx[t - 1L]^2)) {
stop("The ADF regression is essentially exact; its statistic is unreliable.")
}
statistic <- unname(fit$coefficients["level"] / fit$se["level"])
critical <- .wb_mackinnon(length(t), deterministic, N = 1L)
list(statistic = statistic, stat = statistic, nobs = length(t), n = n,
critical_nobs = length(t), lags = lags, deterministic = deterministic,
critical = critical, reject = statistic < critical,
null = "Unit root", alternative = "Stationary relative to the specified deterministic terms",
coefficients = fit$coefficients, se = fit$se,
residuals = fit$residuals, fitted = fit$fitted, df = fit$df)
}
wb_eg <- function(y, x, lags = 0, deterministic = c("constant", "trend")) {
deterministic <- match.arg(deterministic)
y <- .wb_series(y, "y")
x <- .wb_series(x, "x")
if (length(y) != length(x)) stop("x and y must have the same length and aligned dates.")
n <- length(y)
design <- cbind(x = x, constant = 1)
if (deterministic == "trend") design <- cbind(design, trend = seq_len(n))
levels <- .wb_ols(y, design)
if (sum(levels$residuals^2) < 100 * .Machine$double.eps * sum((y - mean(y))^2)) {
stop("The levels relation is nearly exact; the residual test is numerically unreliable.")
}
adf <- wb_adf(levels$residuals, lags = lags, deterministic = "none")
# EG uses N = 2 and the levels regression's deterministic specification.
# Match statsmodels.coint: critical-value nobs is n - 1 regardless of lags;
# adf$nobs = n - 1 - lags records the actual second-stage regression size.
critical <- .wb_mackinnon(n - 1L, deterministic, N = 2L)
list(statistic = adf$statistic, stat = adf$statistic,
nobs = adf$nobs, n = n, critical_nobs = n - 1L,
lags = adf$lags, deterministic = deterministic,
critical = critical, reject = adf$statistic < critical,
null = "No cointegration between the two I(1) series",
alternative = "Cointegration with the specified deterministic terms",
coefficients = levels$coefficients, residuals = levels$residuals,
fitted = levels$fitted, adf = adf)
}
wb_kpss <- function(x, bandwidth, deterministic = c("constant", "trend", "none")) {
deterministic <- match.arg(deterministic)
x <- .wb_series(x)
n <- length(x)
if (deterministic != "none" && max(x) == min(x)) stop("KPSS is undefined for a constant series.")
bandwidth <- .wb_integer(bandwidth, "bandwidth", n - 1L)
if (deterministic == "none") {
# Known population mean zero: do not estimate or subtract a sample mean.
e <- x
coefficients <- numeric(0)
} else {
design <- matrix(1, nrow = n, ncol = 1L, dimnames = list(NULL, "constant"))
if (deterministic == "trend") design <- cbind(design, trend = seq_len(n))
fit <- .wb_ols(x, design)
e <- fit$residuals
coefficients <- fit$coefficients
if (sum(e^2) <= 100 * .Machine$double.eps^2 * sum(x^2)) {
stop("The fitted deterministic part is essentially exact; KPSS is undefined.")
}
}
partial_sums <- cumsum(e)
gamma <- numeric(bandwidth + 1L)
gamma[1L] <- sum(e^2) / n
if (bandwidth > 0L) {
for (h in seq_len(bandwidth)) gamma[h + 1L] <- sum(e[(h + 1L):n] * e[1L:(n - h)]) / n
}
# bandwidth is the maximum included lag L, with weight 1 - h/(L+1).
lrv <- gamma[1L]
if (bandwidth > 0L) lrv <- lrv + 2 * sum((1 - seq_len(bandwidth) / (bandwidth + 1)) * gamma[-1L])
if (!is.finite(lrv) || lrv <= 0) stop("KPSS requires a positive estimated long-run variance.")
statistic <- sum(partial_sums^2) / (n^2 * lrv)
# Fitted cases: KPSS (1992) Table 1, also used by tseries::kpss.test.
# Known mean zero: Kagalwala (2022), Table 2, Bartlett KPSS, rounded.
# https://doi.org/10.1177/1536867X221106371
# Its limit is integral_0^1 W(r)^2 dr, not the demeaned Brownian bridge law.
critical <- switch(deterministic, none = c(2.79, 1.66, 1.20),
constant = c(0.739, 0.463, 0.347), trend = c(0.216, 0.146, 0.119))
names(critical) <- c("1%", "5%", "10%")
list(statistic = statistic, stat = statistic, nobs = n, n = n,
bandwidth = bandwidth, deterministic = deterministic,
critical = critical, reject = statistic > critical,
null = switch(deterministic, none = "Stationarity with known mean zero",
constant = "Level stationarity", trend = "Trend stationarity"),
alternative = "A stochastic trend under the KPSS model",
lrv = lrv, residuals = e, partial_sums = partial_sums,
autocovariances = gamma, coefficients = coefficients)
}