walkers <- function(shocks, adjust_h = 0.1, adjust_m = 0.1, burn = 500L) {
stopifnot(is.matrix(shocks), ncol(shocks) == 3L,
all(is.finite(shocks)), burn >= 0L, burn == as.integer(burn),
nrow(shocks) > burn + 2L,
adjust_h >= 0, adjust_m >= 0, adjust_h + adjust_m < 2)
n <- nrow(shocks)
h <- m <- k <- numeric(n + 1L)
for (t in seq_len(n)) {
gap <- m[t] - h[t]
h[t + 1L] <- h[t] + adjust_h * gap + shocks[t, 1L]
m[t + 1L] <- m[t] - adjust_m * gap + shocks[t, 2L]
k[t + 1L] <- k[t] + shocks[t, 3L]
}
keep <- (burn + 2L):(n + 1L)
data.frame(t = seq_along(keep), Harry = h[keep], Mouse = m[keep],
Karen = k[keep], gap = m[keep] - h[keep])
}
set.seed(637610)
shocks <- matrix(rnorm(3 * 1240), ncol = 3)
connected <- walkers(shocks, adjust_h = 0.1, adjust_m = 0.1, burn = 1000)Module 10 Lab: Cointegration and Error Correction
Econ 6376 — Applied Time Series Econometrics · AI Learning Pack
How to use this lab
Everything runs as shipped, offline. Your work is to predict, run, modify and explain. Record predictions before running the corresponding chunks, in your private LEARNING_LOG.md, an R comment, or on paper. With an AI tutor, the prediction ask takes one turn; “just run it” releases it. Printed subprompts are optional self-study opportunities: they do not re-arm the tutor’s prediction ask within one work unit, such as a section and its variations. No AI is needed to use the lab.
We follow the deck’s sequence: story and simulation → definition and weights → testing → error correction → economics and reporting. The notes are canonical for facts, notation and the Core boundary. Sections 2–7 are the main path; sections 8–9 are optional. We keep the ARDL mixing board with outcome memory and observed predictors. Explicit MA terms and a deterministic trend are off in our baseline relation. The new question is whether a stable combination connects wandering levels.
Read Murray (1994), pp. 37–38, alongside the story; see TEXTBOOK_MAP.md for the bibliographic pointer. The article and lecture artwork are not bundled. This R lab stands alone without the deck’s interactive widget.
1. Setup
Run chunks in order from this pack’s root. In RStudio, choose Session → Set Working Directory → To Source File Location; getwd() should end in ai_packs/module_10. Installed R packages are ggplot2, urca, knitr, and rmarkdown; rendering also needs Quarto. Once installed, no network or credentials are needed. Render this file by path, never the whole folder.
2. Harry and Mouse leave the bar
Harry and his puppy Mouse meet Karen at the bar. When they leave, Harry and Karen wander; Mouse follows each new scent. If all three walked independently, knowing one location would not supply a correcting mechanism for the others. But Mouse belongs to Harry. Harry notices Mouse getting ahead and calls; Mouse responds. Karen continues on her own.
Murray’s story gives us a statistical relationship, not a physical tether. There is no leash. The separation can occasionally be large, and neither an exact reunion nor a five-meter bound is promised.
Predict: which object should be more stable: (a) Harry’s location, (b) Mouse’s location, or (c) Mouse minus Harry? Commit before the plots.
Make the story a generating rule
Let denote their locations and write the signed gap as :
The innovations are independent standard normal across characters and time. If Mouse is ahead, Harry’s correction is positive and Mouse’s is negative. New shocks can still pull them apart. Once correction is active, Harry and Mouse are generally no longer literal random walks with independent increments: their previous separation helps predict their next steps.
The loop is the equation. We supply the shocks so later experiments can change only the rule. This is a course adaptation of Murray’s mechanism, not a reproduction of his figure. The same function is stamped in R/cointegration_utils.R for reuse; we build it inline here.
We keep 240 observations after a 1,000-step initialization period. Initialization reduces the effect of the starting gap in this stable case; it cannot make a random walk stationary. Do not generate new shocks when comparing correction settings.
Locations, changes, and separation
locations <- data.frame(t = rep(connected$t, 3),
location = c(connected$Harry, connected$Mouse, connected$Karen),
walker = rep(c("Harry", "Mouse", "Karen"), each = nrow(connected)))
ggplot(locations, aes(t, location, colour = walker)) +
geom_line(linewidth = 0.65) + scale_colour_manual(values = walker_colours) +
labs(x = "Retained observation", y = "Location", colour = NULL)changes <- data.frame(t = rep(connected$t[-1], 2),
change = c(diff(connected$Harry), diff(connected$Mouse)),
walker = rep(c("Harry", "Mouse"), each = nrow(connected) - 1))
ggplot(changes, aes(t, change, colour = walker)) +
geom_line(linewidth = 0.55) + facet_wrap(~walker, ncol = 1) +
scale_colour_manual(values = walker_colours) + guides(colour = "none") +
labs(x = "Retained observation", y = "One-period change")Differencing is a valid transformation. The question is which predictors we keep: a model containing only changes does not explicitly say how far Mouse has moved from Harry. Now examine that accumulated separation.
gaps <- data.frame(t = rep(connected$t, 2),
gap = c(connected$gap, connected$Karen - connected$Harry),
pair = factor(rep(c("Mouse - Harry", "Karen - Harry"), each = nrow(connected)),
levels = c("Mouse - Harry", "Karen - Harry")))
ggplot(gaps, aes(t, gap)) + geom_hline(yintercept = 0, colour = "grey65") +
geom_line(colour = "#176B87", linewidth = 0.65) + facet_wrap(~pair, ncol = 1) +
labs(x = "Retained observation", y = "Signed separation")The generating rules make the Mouse–Harry gap stable while Karen contributes an independent stochastic trend. A finite path can be deceptive; the plots alone do not establish these population properties. Reconcile your prediction by naming the object whose uncertainty is stabilized.
3. Name the stationary object
The baseline definition is
The individual levels wander; a fixed, nontrivial weighted combination is stationary. Here , , , and . The long-run weights identify the combination; multiplying both by a nonzero constant gives the same relationship. Normalizing the coefficient on to one does not identify causal direction. A stable deviation need not be white noise, small, or always zero.
Predict: if and is I(0), which is stationary: (a) , (b) , or (c) both? The code re-expresses the existing simulated paths; it draws no new innovations.
x <- connected$Harry
y <- 2 + 3 * x + connected$gap
weighted <- data.frame(t = rep(connected$t, 2),
value = c(y - x, y - 2 - 3 * x),
combination = rep(c("Unweighted: y - x", "Correct weights: y - 2 - 3x"),
each = nrow(connected)))
stopifnot(max(abs((y - 2 - 3 * x) - connected$gap)) < 1e-12)
ggplot(weighted, aes(t, value)) + geom_hline(yintercept = 0, colour = "grey65") +
geom_line(colour = "#176B87") + facet_wrap(~combination, ncol = 1) +
labs(x = "Retained observation", y = "Combination")The correct combination recovers the known gap; the raw difference retains . This algebra tells us more than whether the plot looks tidy. Modify: replace the slope by a negative value and adjust the candidate combination accordingly. Murray’s teeter-totter analogy makes the same point: a weighted sum can be stable. Actual seesaw heights are bounded; the analogy illustrates weights, not a literal I(1) physical system.
High correlation, matching upward trends, or ordinary levels-regression stars do not establish cointegration. Two trend-stationary series can cancel a deterministic trend without meeting this I(1) setup. Nor does a stationary relationship distinguish behavioral adjustment, a common driver, accounting, or other economic mechanisms. Exogeneity and causal interpretation require additional assumptions.
4. Estimate the relationship, then test its deviation
We know the population gap in the simulation. An analyst usually does not. Fit and save the estimated deviation . Make the same comparison for Karen on Harry.
Predict: does fitting an intercept and slope guarantee (a) stationary residuals, (b) residuals centered near zero, or (c) both? Commit before fitting.
mouse_levels <- lm(Mouse ~ Harry, data = connected)
karen_levels <- lm(Karen ~ Harry, data = connected)
pair_residuals <- data.frame(t = rep(connected$t, 2),
residual = c(residuals(mouse_levels), residuals(karen_levels)),
pair = factor(rep(c("Mouse on Harry", "Karen on Harry"), each = nrow(connected)),
levels = c("Mouse on Harry", "Karen on Harry")))
ggplot(pair_residuals, aes(t, residual)) +
geom_hline(yintercept = 0, colour = "grey65") +
geom_line(colour = "#176B87") + facet_wrap(~pair, ncol = 1) +
labs(x = "Retained observation", y = "Fitted levels residual")The intercept mechanically centers both residual vectors. It does not make them stationary. Keep three objects separate: the known population deviation , the estimated levels residual , and the innovation .
Engle–Granger uses the fitted deviation and its own reference values
Under a plausible I(1) working setup, estimate the levels relationship, then fit
The null is no cointegration. Sufficiently negative supports a stationary combination. Lagged differences address short-run dependence. Our residual regression adds no intercept or trend after the first-stage intercept has been projected out. That does not make ordinary no-intercept ADF critical values appropriate: we estimated the combination from these same observations.
This helper deliberately covers only two variables, an intercept without a trend in the levels relation, and a 5% decision. The MacKinnon (2010) response surface uses for levels observations. It is an approximation, not exact finite-sample size control. The test-regression row count is separately . Inputs must already be complete, aligned, and equally spaced; deleting missing values separately from each series is unsafe.
eg_cv5 <- function(n_levels) {
# MacKinnon (2010), Table 2: N=2, tau_c, 5%; n_c is not the ADF row count.
stopifnot(length(n_levels) == 1L, is.finite(n_levels),
n_levels == as.integer(n_levels), n_levels >= 30L)
n_cv <- n_levels - 1L
-3.33613 - 6.1101 / n_cv - 6.823 / n_cv^2
}
eg_test <- function(y, x, p_adf = 4L) {
stopifnot(is.numeric(y), is.numeric(x), length(y) == length(x),
all(is.finite(y)), all(is.finite(x)), sd(x) > 0,
length(p_adf) == 1L, is.finite(p_adf), p_adf >= 0L,
p_adf == as.integer(p_adf), length(y) > 2L * p_adf + 5L)
n <- length(y)
levels_fit <- lm(y ~ x)
e <- residuals(levels_fit)
block <- embed(diff(e), p_adf + 1L)
adf_data <- data.frame(de = block[, 1L], e_lag = e[(p_adf + 1L):(n - 1L)])
if (p_adf > 0L) {
lag_names <- paste0("de_lag", seq_len(p_adf))
adf_data[lag_names] <- as.data.frame(block[, -1L, drop = FALSE])
}
adf_fit <- lm(de ~ . - 1, data = adf_data)
tau <- unname(coef(summary(adf_fit))["e_lag", "t value"])
cv <- eg_cv5(n)
list(levels = levels_fit, residuals = e, adf = adf_fit,
statistic = tau, cv5 = cv, reject5 = tau < cv,
p_adf = p_adf, n_levels = n, n_adf = nrow(adf_data))
}sim_eg <- eg_test(connected$Mouse, connected$Harry, p_adf = 1)
karen_eg <- eg_test(connected$Karen, connected$Harry, p_adf = 1)
pair_table <- data.frame(Relation = c("Mouse on Harry", "Karen on Harry"),
Statistic = c(sim_eg$statistic, karen_eg$statistic),
Critical_5pct = c(sim_eg$cv5, karen_eg$cv5),
Reject_no_cointegration = c(sim_eg$reject5, karen_eg$reject5))
knitr::kable(pair_table, digits = 3, row.names = FALSE)| Relation | Statistic | Critical_5pct | Reject_no_cointegration |
|---|---|---|---|
| Mouse on Harry | -4.767 | -3.362 | TRUE |
| Karen on Harry | -0.598 | -3.362 | FALSE |
Both tests use 240 levels, one lagged residual difference, and 238 test rows. The critical-value sample argument is 239. Mouse on Harry rejects in this sample; Karen on Harry does not. Rejection supports cointegration under the maintained specification. Nonrejection is insufficient evidence, not proof of absence. Ordinary lm() p-values and the ordinary reference values printed by urca::ur.df() must not replace the matching Engle–Granger reference value.
Modify: change only p_adf to two. Print levels count, test rows, and critical value together. Explain which count changes and why. A levels residual can be autocorrelated even when stationary; the test regression’s remaining residual dependence is a separate diagnostic question.
5. Turn down correction, then turn it off
Here is the assumption break. Reuse exactly the same shocks and retained window. First make both responses slow; then switch both off.
Predict: which change destroys the stationary relationship in the DGP: (a) slowing each response to 0.005, (b) setting both to zero, or (c) both? Can a finite-sample test distinguish every case reliably?
slow <- walkers(shocks, adjust_h = 0.005, adjust_m = 0.005, burn = 1000)
unconnected <- walkers(shocks, adjust_h = 0, adjust_m = 0, burn = 1000)
sim_pairs <- list(Connected = connected, Slow = slow, Disconnected = unconnected)
gap_cases <- data.frame(t = rep(connected$t, 3),
gap = c(connected$gap, slow$gap, unconnected$gap),
case = factor(rep(c("Connected: a + b = 0.20", "Slow: a + b = 0.01",
"Disconnected: a + b = 0"), each = nrow(connected)),
levels = c("Connected: a + b = 0.20", "Slow: a + b = 0.01",
"Disconnected: a + b = 0")))
ggplot(gap_cases, aes(t, gap)) + geom_hline(yintercept = 0, colour = "grey65") +
geom_line(colour = "#176B87") + facet_wrap(~case, ncol = 1) +
labs(x = "Retained observation", y = "Mouse - Harry")sim_tests <- lapply(sim_pairs, function(d) eg_test(d$Mouse, d$Harry, p_adf = 1))
sim_table <- data.frame(World = names(sim_tests),
Cointegrated_in_DGP = c(TRUE, TRUE, FALSE),
Statistic = vapply(sim_tests, `[[`, numeric(1), "statistic"),
Critical_5pct = vapply(sim_tests, `[[`, numeric(1), "cv5"),
Reject_no_cointegration = vapply(sim_tests, `[[`, logical(1), "reject5"))
knitr::kable(sim_table, digits = 3, row.names = FALSE)| World | Cointegrated_in_DGP | Statistic | Critical_5pct | Reject_no_cointegration |
|---|---|---|---|---|
| Connected | TRUE | -4.767 | -3.362 | TRUE |
| Slow | TRUE | -2.731 | -3.362 | FALSE |
| Disconnected | FALSE | -2.357 | -3.362 | FALSE |
The connected and slow worlds have stable gaps; the disconnected gap is a random walk. The slow test does not reject here. That is a power problem in this sample, not a change in the population property. A rejection in a disconnected world would instead be a false positive. We did not change the seed to get convenient outcomes.
Modify: set Harry’s response to zero and Mouse’s to 0.2. Reuse the shocks. Predict the gap first. Then compare the first 60 retained observations with all 240. Who adjusts, how much evidence is available, and whether a stable relationship exists are distinct questions. The supplied helper only builds the unit-slope story; it is not a general weighted-system simulator.
6. OK, now what? Error correction
We identified a relationship between Harry and Mouse. How does it help explain the next step? The story’s call and response says yesterday’s separation contributes to today’s movement. A regression containing only changes has no explicit variable for that accumulated deviation.
Predict: if Mouse’s previous gap is and its adjustment coefficient is , does the correction contribute (a) , (b) , or (c) a guaranteed 10% reduction in the observed gap? Hold other terms separate.
Rearrange the ARDL, keeping the statistical question visible
Starting from Module 8’s ARDL(1,1), subtract and substitute :
For , let , , and . Then
is adjustment; is the long-run slope; remains the ordinary equation intercept. A negative is corrective under this gap normalization. Reversing the gap’s sign reverses the coefficient without changing the contribution. Algebra alone does not establish cointegration, and one negative coefficient is not a general test of dynamic-system stability.
Estimate Mouse’s conditional equation
The population Mouse response is . Our constructed example has independent current innovations; current Harry news therefore does not carry an omitted current Mouse shock. Economic data do not inherit that assumption. The regression substitutes the estimated levels residual for the unknown population deviation.
n_sim <- nrow(connected)
ecm_data <- data.frame(dm = diff(connected$Mouse),
dh = diff(connected$Harry), e_lag = sim_eg$residuals[-n_sim])
ecm_fit <- lm(dm ~ e_lag + dh, data = ecm_data)
knitr::kable(data.frame(Term = c("Intercept", "Previous estimated gap",
"Current change in Harry"),
Estimate = unname(coef(ecm_fit))),
digits = 3, row.names = FALSE)| Term | Estimate |
|---|---|
| Intercept | 0.014 |
| Previous estimated gap | -0.151 |
| Current change in Harry | -0.060 |
The 239 rows align each current change with the preceding estimated gap. The fitted equation uses as a predictor; its own remaining disturbance is an ECM residual, . The estimated adjustment is -0.151, not necessarily the generating value: the path is finite and the relationship was estimated. Ordinary regression stars do not substitute for the cointegration analysis.
correction <- data.frame(gap = ecm_data$e_lag,
contribution = coef(ecm_fit)["e_lag"] * ecm_data$e_lag)
ggplot(correction, aes(gap, contribution)) +
geom_hline(yintercept = 0, colour = "grey65") +
geom_vline(xintercept = 0, colour = "grey65") +
geom_point(colour = "#E4572E", alpha = 0.6) +
labs(x = "Previous estimated gap", y = "Fitted contribution to change in Mouse")Harry also moves and new shocks arrive. The fitted coefficient does not promise a fixed percentage closure of the whole observed gap. Including current also differs from Module 9’s past-only Granger information set. Conditional estimation and interpretation require suitable contemporaneous-dependence and exogeneity assumptions; predictive adjustment is not automatically a policy effect.
Modify: change the predictor to -e_lag and refit. Compare the coefficient and the product with the predictor. Explain what changed and what did not. For the one-adjusting-party world, a weak response in Harry’s equation would not refute the relationship Mouse helps maintain.
7. Back to economics
Economists use cointegration to assess a proposed relationship, estimate its weights, and retain the deviation in a dynamic model when warranted. Engle–Granger (1987, pp. 271–274) report evidence in their historical consumption/income and yield examples. Hamilton’s U.S.–Italy PPP example (pp. 598–600) finds little evidence. Variable definitions, periods, regimes, and procedures matter; the name of an economic relationship guarantees no test outcome.
Finding some is also weaker than establishing the weights a theory requires. Cointegrated yields need not have a stationary unit-weight spread. Ordinary OLS slope t-tests cannot validate these restrictions. Our one full application asks whether consumption and income share a stable log relation.
Predict: could two log levels have excellent fit while their Engle–Granger test does not reject no cointegration: (a) yes or (b) no? Commit before fitting.
Data first: definitions, calendar, and vintage
The CSV contains aggregate total real personal consumption expenditures and real disposable income, in billions of chained 2005 dollars at seasonally adjusted annual rates. It has 203 complete quarters, 1959Q1–2009Q3, from a December 2009 FRED archive distributed as public-domain statsmodels macrodata. See data/README.md. This is neither today’s vintage nor a replication of Engle–Granger’s real per-capita nondurables sample.
ci <- read.csv("data/module_10_consumption_income.csv")
stopifnot(nrow(ci) == 203, all(complete.cases(ci)), all(ci$realcons > 0),
all(ci$realdpi > 0), all(diff(4 * ci$year + ci$quarter) == 1))
ci$date <- as.Date(sprintf("%d-%02d-01", ci$year, 3 * ci$quarter - 2))
ci$lc <- log(ci$realcons)
ci$ly <- log(ci$realdpi)
ci_plot <- data.frame(date = rep(ci$date, 2), level = c(ci$lc, ci$ly),
series = rep(c("Consumption", "Disposable income"), each = nrow(ci)))
ggplot(ci_plot, aes(date, level, colour = series)) + geom_line(linewidth = 0.65) +
labs(x = NULL, y = "Natural log of real level", colour = NULL)Taking logs makes the slope describe proportional association. It does not remove a stochastic trend, yield a dollar marginal propensity to consume, or identify a causal elasticity.
Assess whether I(1) is a reasonable working description
The individual log-level ADF tests include an intercept and trend, allowing a trend-stationary alternative. The first-difference tests use an intercept. Each includes four lagged differences, a fixed quarterly teaching choice, not a universal rule.
unit_tests <- list(
"Log consumption: level" = urca::ur.df(ci$lc, type = "trend", lags = 4),
"Log income: level" = urca::ur.df(ci$ly, type = "trend", lags = 4),
"Log consumption: change" = urca::ur.df(diff(ci$lc), type = "drift", lags = 4),
"Log income: change" = urca::ur.df(diff(ci$ly), type = "drift", lags = 4))
unit_table <- data.frame(Series = names(unit_tests),
ADF_statistic = vapply(unit_tests, function(u) as.numeric(u@teststat[1]), numeric(1)),
Critical_5pct = vapply(unit_tests, function(u) as.numeric(u@cval[1, "5pct"]), numeric(1)))
unit_table$Reject_unit_root <- unit_table$ADF_statistic < unit_table$Critical_5pct
knitr::kable(unit_table, digits = 3, row.names = FALSE)| Series | ADF_statistic | Critical_5pct | Reject_unit_root |
|---|---|---|---|
| Log consumption: level | -2.435 | -3.43 | FALSE |
| Log income: level | -1.908 | -3.43 | FALSE |
| Log consumption: change | -5.082 | -2.88 | TRUE |
| Log income: change | -6.351 | -2.88 | TRUE |
Levels do not reject and changes do. This supports a working I(1) assessment, not proof: power, breaks, and deterministic specification remain relevant. The ordinary univariate ADF critical values fit these individual-series tests; they do not calibrate a test of estimated cointegrating residuals. If the series were I(0), I(2), or mismatched in integration order, this baseline procedure would require reconsideration.
Estimate, test, and inspect the specification
The proposed long-run relation contains an intercept and no time trend. The trend in the individual-series tests addressed a different question. Freeze the four-lag residual-ADF baseline before looking at its result.
ci_eg <- eg_test(ci$lc, ci$ly, p_adf = 4)
level_summary <- data.frame(
Quantity = c("Estimated intercept", "Estimated slope", "Levels R-squared",
"Levels observations", "Residual-ADF observations"),
Value = c(coef(ci_eg$levels), summary(ci_eg$levels)$r.squared,
ci_eg$n_levels, ci_eg$n_adf))
knitr::kable(level_summary, digits = 4, row.names = FALSE)| Quantity | Value |
|---|---|
| Estimated intercept | -0.3758 |
| Estimated slope | 1.0320 |
| Levels R-squared | 0.9984 |
| Levels observations | 203.0000 |
| Residual-ADF observations | 198.0000 |
ggplot(data.frame(date = ci$date, residual = ci_eg$residuals), aes(date, residual)) +
geom_hline(yintercept = 0, colour = "grey65") + geom_line(colour = "#176B87") +
labs(x = NULL, y = "Estimated log consumption-income deviation")The estimated slope is 1.032 and is 0.9984. The Engle–Granger statistic is -2.589 against the matching 5% critical value -3.367. It is not sufficiently negative: the baseline does not reject no cointegration. High fit has not settled the question.
Now examine a declared set of augmentation choices on the same levels sample. Test rows differ because the required lag history differs; these are sensitivity specifications, not a nested RSS comparison.
lag_set <- c(0, 2, 4, 8)
sensitivity_fits <- lapply(lag_set, function(p) eg_test(ci$lc, ci$ly, p_adf = p))
sensitivity <- data.frame(Lagged_differences = lag_set,
Test_rows = vapply(sensitivity_fits, `[[`, integer(1), "n_adf"),
Statistic = vapply(sensitivity_fits, `[[`, numeric(1), "statistic"),
Critical_5pct = vapply(sensitivity_fits, `[[`, numeric(1), "cv5"),
Reject_no_cointegration = vapply(sensitivity_fits, `[[`, logical(1), "reject5"))
knitr::kable(sensitivity, digits = 3, row.names = FALSE)| Lagged_differences | Test_rows | Statistic | Critical_5pct | Reject_no_cointegration |
|---|---|---|---|---|
| 0 | 202 | -3.397 | -3.367 | TRUE |
| 2 | 200 | -2.898 | -3.367 | FALSE |
| 4 | 198 | -2.589 | -3.367 | FALSE |
| 8 | 194 | -2.032 | -3.367 | FALSE |
reverse_eg <- eg_test(ci$ly, ci$lc, p_adf = 4)Only the zero-lag version rejects, narrowly. The reverse normalization at four lags gives -2.626 against the same critical value and does not reject. Normalization sensitivity in finite samples is a specification question, not causal direction.
The next plot concerns test-regression residuals, distinct from the first-stage deviation. A stationary deviation may be persistent, while substantial dependence left after augmentation can undermine the test’s specification.
acf_zero <- acf(residuals(sensitivity_fits[[1]]$adf), lag.max = 12, plot = FALSE)
acf_four <- acf(residuals(ci_eg$adf), lag.max = 12, plot = FALSE)
diagnostic <- data.frame(lag = rep(1:12, 2),
acf = c(as.numeric(acf_zero$acf)[-1], as.numeric(acf_four$acf)[-1]),
model = factor(rep(c("Zero lags", "Four lags"), each = 12),
levels = c("Zero lags", "Four lags")))
bands <- data.frame(model = factor(c("Zero lags", "Four lags"),
levels = c("Zero lags", "Four lags")),
band = 1.96 / sqrt(c(nobs(sensitivity_fits[[1]]$adf), nobs(ci_eg$adf))))
ggplot(diagnostic, aes(lag, acf)) + geom_hline(yintercept = 0, colour = "grey65") +
geom_hline(data = bands, aes(yintercept = band), linetype = "dashed", colour = "grey50") +
geom_hline(data = bands, aes(yintercept = -band), linetype = "dashed", colour = "grey50") +
geom_segment(aes(xend = lag, yend = 0), colour = "#176B87", linewidth = 0.9) +
facet_wrap(~model, nrow = 1) + scale_x_continuous(breaks = c(1, 2, 4, 6, 8, 10, 12)) +
labs(x = "Lag", y = "Test-regression residual ACF")At lags one and two the zero-lag test’s residual autocorrelations are -0.280 and 0.202, outside its approximate ±0.138 bands. The four-lag values are -0.007 and -0.004. The zero-lag rejection has weaker diagnostic support; this does not prove the four-lag residuals are white noise.
Report what the evidence warrants
The simulated ECM had a relationship justified by its generating rule. This historical economic sample has not earned a stable long-run relation as an established feature. We therefore stop short of presenting a fitted economic ECM as validated. Changes-based modeling remains a candidate for further analysis while the long-run question stays open. Nonrejection proves neither that consumption and income are unrelated nor that adjustment is zero.
Reconcile: write a short report in your own words. Include definitions, units, dates and vintage; the working I(1) assessment; normalization, deterministics, lags and matching critical value; baseline decision and diagnostics; sensitivity and remaining uncertainty. Keep existence, estimation, and economic interpretation separate. Do not promote the zero-lag result to the headline because it rejects.
Modify: choose a defensible additional lag specification before running it. Report the added choice alongside all existing results. Different definitions, breaks, deterministic terms, or additional variables may change the economic question and must be documented as further work. Searching for the first rejection is not that workflow.
8. Looking Ahead — optional
This preview is outside Module 10’s assessable Core. Module 11 gives each variable an equation in a VAR system. Module 13 returns to error correction within that system. With several I(1) variables, cointegrating rank counts independent stationary relationships; Johansen’s method asks about that rank. The scalar ECM is the entry point. Matrix estimation, trace/max-eigenvalue testing, and ca.jo() are later machinery, not requirements of this lab. Ask the tutor to build them on request, labelled as a preview.
9. Explorations — optional, guided to open
These are starting points for curiosity, outside the assessable surface. They are not official assessment questions or answers.
- Only Mouse adjusts. Reuse the shock matrix with
adjust_h = 0andadjust_m = 0.2. Compare known gaps, fitted residuals, and adjustment estimates in the two equations. Explain why a relationship need not give both variables nonzero adjustment. - Window versus truth. Use the first 60, 120, and 240 observations of each existing world, keeping one residual-ADF lag. Compare critical values, statistics, and decisions. Which objects changed, and which population properties did not? A few windows do not estimate test power.
- Weights and signs. Re-express the existing paths using slopes , , and , retaining the known stationary gap. Plot the right and wrong combinations. Separate the weight from adjustment ; do not treat the unit-slope helper as a general weighted model.
- Where does stability come from? Read the optional gap-dynamics derivation in notes §10.6. Subtract the walker recursions, derive the gap’s AR coefficient, and examine under-, over-, and zero-correction. State why the helper excludes combined correction of two or more.
- A changing relationship. Propose a dated change in the long-run intercept or slope in an artificial world. Write the old and new relationships before coding; preserve the innovation draws. Compare what a fixed full-sample relationship leaves in its residuals. Do not claim that the simple Engle–Granger helper is a break-robust test.
10. Run the referee and reconcile
Read CHECKS.md for the scope of the six checks. They verify the shipped examples, indexing, and calibration; they do not certify arbitrary modifications, universal finite-sample test size, or causal interpretation. The private environment keeps the checks from replacing your lab objects.
source("R/checks.R", local = new.env())Module 10 checks
----------------
PASS 1. Archive schema, positive levels and complete quarterly calendar [203 rows; 1959Q1 to 2009Q3]
PASS 2. Supplied-shock replay, simulator recursion and burn indexing [max recursion discrepancy=0.0e+00]
PASS 3. Same shocks: stable, slow and disconnected gap constructions [gap coefficients=0.80/0.99/1.00; max identity discrepancy=1.7e-14]
PASS 4. EG statistic, lag rows and bivariate intercept/no-trend calibration [max independent statistic discrepancy=2.7e-15; CV(n=240)=-3.361815]
PASS 5. ARDL-to-ECM identity, previous-gap alignment and shipped adjustment [ECM rows=239; estimated adjustment=-0.15083077]
PASS 6. Archive baseline, declared lag sensitivity and remaining-dependence warning [baseline tau=-2.589009; zero-lag residual ACF=-0.280/0.202; four-lag=-0.007/-0.004]
----------------
6/6 checks passed.
Finish your private learning-log entry. Record your revised reasoning and remaining uncertainty in your own words. If your prediction changed, say whether the surprise came from the generating rule, the estimated relationship, the reference distribution, or one sample’s evidence.