0.2 + 0.6 * 2 + 0.4 * 1 + 0.2 * 0.5[1] 1.9
Econ 6376 — Applied Time Series Econometrics · AI Learning Pack
Everything runs as shipped, offline. Your work is to predict, run, modify and explain. Record predictions before 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 whole lab section and its variations. No AI is needed to use the lab.
We follow the deck’s sequence: pictures → exogeneity → controlled experiment → dynamic responses → forecasting calendar → real data. The notes are canonical for facts, notation and the Core boundary. Sections 2–7 are the main path; sections 8–9 are optional. The model expands the mixing board with observed predictors, retaining outcome lags and switching off explicit MA terms in the baseline. A fitted value is ; a forecast is .
Open this file from the pack folder and run chunks in order. In RStudio, choose Session → Set Working Directory → To Source File Location before running individual chunks; getwd() should show ai_packs/module_08 and dir.exists("data") should be TRUE. Quarto’s file render uses the lab’s directory automatically. Installed R packages are ggplot2, forecast, urca, knitr and rmarkdown; rendering also needs Quarto. Render this file by path, not the whole folder.
An ARMA remembers outcomes and innovations. An ARDL brings an observed second series into the room. Predict: if current is unavailable, which route must a forecast replace or omit: (a) outcome memory, (b) current- input, or (c) every lagged- input? Draw the restriction before reading on.
These are posited dependencies, not identified causal effects. Unrolling time keeps and separate and lets feedback across dates remain acyclic.
The ARDL equation is
Generalize to outcome lags and predictor lags only after you can point to each term. Here is maximum predictor lag; ARMA uses for MA order. Setting removes current , not its history. Setting all gives an AR. All gives a distributed-lag regression.
With and , compute the conditional mean:
0.2 + 0.6 * 2 + 0.4 * 1 + 0.2 * 0.5[1] 1.9
Calling this a conditional mean requires the disturbance to have mean zero given those inputs. A realization also contains ; an estimated equation replaces the coefficients with hats and the disturbance with .
A software translation to check. Regression with AR(1) disturbances, , , implies
It is a restricted ARDL. xreg does not by itself tell you which specification was fitted. Modify on paper: allow ; then set . Explain which paths survive and which information a one-step forecast needs.
Think of two coworkers reacting to the same emergency in The Office fire drill. One moves and the other moves; both may be reacting to the alarm. The diagram makes that omitted route visible without needing a clip or a network connection. The reference is an analogy for common news, not a claim that the scene is a controlled experiment.
Let contain the relevant histories. Our sufficient conditional-mean restriction is
It is about the whole conditioning information set, not just a sequence of pairwise correlations. Alongside suitable sampling, stability, finite moments and full rank, it supports consistency. It does not promise exact finite-sample unbiasedness or exact dynamic-regression inference.
Predict: which situation directly threatens the structural coefficient’s OLS interpretation: (a) is correlated with , (b) current reveals part of the structural disturbance, or (c) uses already-conditioned-on and the current shocks are independent of each other and the past?
The current-shock and feedback stories differ. In the feedback case, the path reveals why future can contain information about today’s disturbance while the sequential restriction still holds. Strict exogeneity over the entire path is stronger.
White noise need not be unrelated to current . Lagging a variable does not remove every persistent omitted driver. A clean residual ACF and robust standard errors do not certify structural exogeneity. Keep three questions separate: conditional prediction, response along a stated input path, and causal intervention effect.
The pair of equations is
The shock pairs are independent across dates, jointly Gaussian with unit variances and current correlation . Compare , , and . These three configurations have stable joint dynamics. In a general feedback system, checking only would be insufficient.
Predict before running this section: with more data, does the shared-shock estimate of the current- coefficient approach (a) 0.4, (b) 1.1, or (c) zero? What changes when the only new feature is lagged feedback?
ardl_simulator <- function(n = 400, alpha = 0.2, phi = 0.6,
beta = c(0.4, 0.2), rho_x = 0.5,
feedback = 0, shock_corr = 0, burn_in = 200) {
stopifnot(n >= 2, n == as.integer(n), burn_in >= 0,
burn_in == as.integer(burn_in), length(beta) == 2,
all(is.finite(c(alpha, phi, beta, rho_x, feedback, shock_corr))),
abs(shock_corr) < 1)
total <- n + burn_in + 1L
epsilon_x <- rnorm(total)
epsilon <- shock_corr * epsilon_x +
sqrt(1 - shock_corr^2) * rnorm(total)
x <- y <- numeric(total)
for (t in 2:total) {
x[t] <- rho_x * x[t - 1] + feedback * y[t - 1] + epsilon_x[t]
y[t] <- alpha + phi * y[t - 1] + beta[1] * x[t] +
beta[2] * x[t - 1] + epsilon[t]
}
keep <- (burn_in + 2L):total
data.frame(t = seq_len(n), y = y[keep], x = x[keep],
epsilon = epsilon[keep], epsilon_x = epsilon_x[keep])
}The for-loop is the DGP: compute first, then . Burn-in reduces the effect of arbitrary initial values in a stable system; it cannot create stability. This function and the multiplier function below are built inline, with exact canonical copies in R/ardl_utils.R for reuse.
case_names <- c("Baseline", "Shared current shocks", "Lagged feedback only")
case_corr <- c(0, 0.7, 0)
case_feedback <- c(0, 0, 0.2)
case_data <- case_fits <- vector("list", 3)
single_estimates <- matrix(NA_real_, 3, 4,
dimnames = list(case_names,
c("alpha", "phi_1", "beta_0", "beta_1")))
for (s in seq_along(case_names)) {
set.seed(637608) # Same underlying independent draws across the three cases.
d <- ardl_simulator(shock_corr = case_corr[s], feedback = case_feedback[s])
d$y_lag1 <- c(NA, head(d$y, -1))
d$x_lag1 <- c(NA, head(d$x, -1))
fit <- lm(y ~ y_lag1 + x + x_lag1, data = d)
case_data[[s]] <- d
case_fits[[s]] <- fit
single_estimates[s, ] <- coef(fit)
}
knitr::kable(rbind("DGP coefficients" = c(0.2, 0.6, 0.4, 0.2),
single_estimates), digits = 3)| alpha | phi_1 | beta_0 | beta_1 | |
|---|---|---|---|---|
| DGP coefficients | 0.200 | 0.600 | 0.400 | 0.200 |
| Baseline | 0.221 | 0.555 | 0.352 | 0.276 |
| Shared current shocks | 0.217 | 0.565 | 1.065 | -0.068 |
| Lagged feedback only | 0.265 | 0.552 | 0.349 | 0.259 |
Lags are explicit shifted columns. stats::lag() on a numeric vector is not a substitute for checking those columns against their dates. One sample is not the distribution; repeat the experiment serially with a fixed seed.
set.seed(637609)
B <- 200
mc <- array(NA_real_, c(B, 4, 3),
dimnames = list(NULL, colnames(single_estimates), case_names))
for (b in seq_len(B)) {
for (s in seq_along(case_names)) {
d <- ardl_simulator(shock_corr = case_corr[s], feedback = case_feedback[s])
d$y_lag1 <- c(NA, head(d$y, -1))
d$x_lag1 <- c(NA, head(d$x, -1))
mc[b, , s] <- coef(lm(y ~ y_lag1 + x + x_lag1, data = d))
}
}
mc_table <- data.frame()
truth <- c(0.2, 0.6, 0.4, 0.2)
for (s in seq_along(case_names)) {
for (j in seq_along(truth)) {
mc_table <- rbind(mc_table, data.frame(
case = case_names[s], coefficient = colnames(single_estimates)[j],
DGP = truth[j], mean_estimate = mean(mc[, j, s]),
simulation_sd = sd(mc[, j, s]),
MCSE_of_mean = sd(mc[, j, s]) / sqrt(B)))
}
}
knitr::kable(mc_table, digits = 3)| case | coefficient | DGP | mean_estimate | simulation_sd | MCSE_of_mean |
|---|---|---|---|---|---|
| Baseline | alpha | 0.2 | 0.204 | 0.053 | 0.004 |
| Baseline | phi_1 | 0.6 | 0.599 | 0.033 | 0.002 |
| Baseline | beta_0 | 0.4 | 0.396 | 0.053 | 0.004 |
| Baseline | beta_1 | 0.2 | 0.206 | 0.055 | 0.004 |
| Shared current shocks | alpha | 0.2 | 0.205 | 0.039 | 0.003 |
| Shared current shocks | phi_1 | 0.6 | 0.597 | 0.028 | 0.002 |
| Shared current shocks | beta_0 | 0.4 | 1.097 | 0.036 | 0.003 |
| Shared current shocks | beta_1 | 0.2 | -0.144 | 0.055 | 0.004 |
| Lagged feedback only | alpha | 0.2 | 0.206 | 0.058 | 0.004 |
| Lagged feedback only | phi_1 | 0.6 | 0.595 | 0.034 | 0.002 |
| Lagged feedback only | beta_0 | 0.4 | 0.403 | 0.042 | 0.003 |
| Lagged feedback only | beta_1 | 0.2 | 0.200 | 0.053 | 0.004 |
mc_plot <- data.frame(estimate = as.vector(mc[, "beta_0", ]),
case = rep(case_names, each = B))
ggplot(mc_plot, aes(x = estimate, fill = case)) +
geom_histogram(bins = 25, color = "white") +
geom_vline(xintercept = 0.4, linetype = 2, linewidth = 0.8) +
facet_wrap(~ case, ncol = 1, scales = "free_y") +
labs(x = "Estimated contemporaneous coefficient", y = "Replications",
title = "Same structural beta_0; different regressor–disturbance relations",
subtitle = "Dashed line: structural DGP coefficient 0.4") +
guides(fill = "none")The Monte Carlo SD measures dispersion across samples. Its MCSE measures precision of the simulation mean, not uncertainty about the structural interpretation. Inspect the table before declaring which discrepancy is sampling error and which persists with sample size.
In the shared-shock case, and . Substitution gives
The known structural coefficients are still 0.4 and 0.2. OLS is estimating a different conditional relationship. Here is a structural disturbance, not an innovation relative to current and the history. The remaining conditional disturbance is .
forecast::ggAcf(residuals(case_fits[[2]]), lag.max = 20) +
labs(title = "Residual ACF: the shared-shock equation can look well specified",
subtitle = "A residual diagnostic cannot certify a structural interpretation")Modify: raise n to 2,000 in ardl_simulator() calls and repeat the comparison, then restore shock_corr = 0 and use feedback = 0.2. Explain why one change sharpens a different target and the other preserves the structural target under the stated independent-shock assumptions.
Hold initial conditions and outcome innovations fixed. A pulse raises one by one then returns to its baseline path. A step raises all future values by one. The direct lagged coefficient is only one of tomorrow’s routes:
Predict: with , is tomorrow’s imposed-pulse response (a) 0.2, (b) 0.44, or (c) 0.84? Which number belongs to the permanent step instead?
ardl_multipliers <- function(phi, beta, horizon = 20) {
stopifnot(is.numeric(phi), is.numeric(beta), length(beta) >= 1,
all(is.finite(c(phi, beta))), horizon >= 0,
horizon == as.integer(horizon))
pulse <- numeric(horizon + 1L)
for (h in 0:horizon) {
direct <- if (h < length(beta)) beta[h + 1L] else 0
inherited <- 0
if (h > 0 && length(phi) > 0) {
for (j in seq_len(min(length(phi), h))) {
inherited <- inherited + phi[j] * pulse[h - j + 1L]
}
}
pulse[h + 1L] <- direct + inherited
}
data.frame(h = 0:horizon, pulse = pulse, step = cumsum(pulse))
}response <- ardl_multipliers(phi = 0.6, beta = c(0.4, 0.2), horizon = 20)
knitr::kable(head(response, 5), digits = 4)| h | pulse | step |
|---|---|---|
| 0 | 0.4000 | 0.4000 |
| 1 | 0.4400 | 0.8400 |
| 2 | 0.2640 | 1.1040 |
| 3 | 0.1584 | 1.2624 |
| 4 | 0.0950 | 1.3574 |
response_long <- rbind(
data.frame(h = response$h, response = response$pulse, experiment = "Pulse"),
data.frame(h = response$h, response = response$step, experiment = "Permanent step"))
ggplot(response_long, aes(h, response, color = experiment)) +
geom_line(linewidth = 0.9) + geom_point(size = 1.7) +
labs(x = "Periods since the imposed change", y = "Difference in y",
color = "Input path", title = "One equation, two different input experiments") +
theme(legend.position = "bottom")The cumulative pulse response equals the step response in this linear, time-invariant equation. Under stable outcome dynamics its limit is
This is a change, not the mean of . Estimated counterparts need hats and uncertainty; a near-zero denominator magnifies coefficient uncertainty.
Before running: does a finite algebraic ratio imply convergence when ? Follow the response, not just the denominator.
stable <- ardl_multipliers(0.6, c(0.4, 0.2), horizon = 20)
unit_root <- ardl_multipliers(1, c(0.4, 0.2), horizon = 20)
unstable <- ardl_multipliers(1.05, c(0.4, 0.2), horizon = 20)
break_plot <- rbind(
data.frame(h = stable$h, step = stable$step, phi = "phi = 0.6"),
data.frame(h = unit_root$h, step = unit_root$step, phi = "phi = 1"),
data.frame(h = unstable$h, step = unstable$step, phi = "phi = 1.05"))
ggplot(break_plot, aes(h, step, color = phi)) +
geom_line(linewidth = 1) +
labs(x = "Periods since permanent step", y = "Difference in y",
color = NULL, title = "A formula needs its stability condition") +
theme(legend.position = "bottom")The unit-root step accumulates without bound; the explosive step also fails to converge. The finite ratio is not an eventual response. The helper intentionally permits unstable inputs so this failure is visible.
Now use the baseline with independent current shocks, no lagged feedback, and fixed outcome innovations. An isolated innovation in the predictor equation generates differences , not a one-period pulse. Compute the outcome-path differences directly:
x_difference <- 0.5^(0:20)
y_difference <- numeric(length(x_difference))
for (k in seq_along(x_difference)) {
inherited <- if (k == 1) 0 else 0.6 * y_difference[k - 1]
lagged_input <- if (k == 1) 0 else 0.2 * x_difference[k - 1]
y_difference[k] <- inherited + 0.4 * x_difference[k] + lagged_input
}
knitr::kable(data.frame(h = 0:3, imposed_pulse = response$pulse[1:4],
x_innovation_path = y_difference[1:4]), digits = 4)| h | imposed_pulse | x_innovation_path |
|---|---|---|
| 0 | 0.4000 | 0.4000 |
| 1 | 0.4400 | 0.6400 |
| 2 | 0.2640 | 0.5840 |
| 3 | 0.1584 | 0.4504 |
At horizon one the innovation-induced response is 0.64, compared with 0.44 for an imposed pulse. With shared shocks, conditioning on news can change the expected outcome disturbance too. With feedback, follow both equations. None of these calculations alone identifies an intervention in observational data.
Modify: use in the pulse experiment. Explain the cancellation at horizon one by following both paths. Then compare a stable value near one with the explosive value and say why a large multiplier needs uncertainty.
A current predictor in an outcome equation becomes a future predictor in a forecast. Under the maintained model,
The predictor can be forecast, stipulated as a scenario, or observed early enough for a nowcast. Each changes the information set. Conditional scenario uncertainty omits predictor-path uncertainty by design. An unconditional forecast must account for predictor forecast errors and any relevant covariance with outcome shocks, as well as coefficient uncertainty.
Predict: does the method allowed to observe the target-period predictor answer the same forecasting question as a method that must forecast it?
set.seed(637610)
forecast_data <- ardl_simulator(n = 800)
forecast_data$y_lag1 <- c(NA, head(forecast_data$y, -1))
forecast_data$x_lag1 <- c(NA, head(forecast_data$x, -1))
train <- forecast_data[2:600, ]
holdout <- forecast_data[601:800, ]
forecast_fit <- lm(y ~ y_lag1 + x + x_lag1, data = train)
x_fit <- lm(x ~ x_lag1, data = train)
feasible_inputs <- holdout
feasible_inputs$x <- predict(x_fit, newdata = holdout)
feasible <- predict(forecast_fit, newdata = feasible_inputs)
oracle <- predict(forecast_fit, newdata = holdout) # Knows target-period x.
knitr::kable(data.frame(
information = c("Forecast x from its observed lag", "Oracle: observe target-period x"),
RMSE = c(sqrt(mean((holdout$y - feasible)^2)),
sqrt(mean((holdout$y - oracle)^2)))), digits = 3)| information | RMSE |
|---|---|
| Forecast x from its observed lag | 1.039 |
| Oracle: observe target-period x | 0.950 |
Both methods freeze training coefficients and roll observed lags. The oracle has future information. Its score could be a legitimate nowcast only if that predictor is actually released before the target at the stated issue date. The current contents of a CSV do not answer that question.
Modify the design on paper: move the issue date earlier than the previous month’s unemployment release. List the lag columns that are no longer observed. Explain what must be forecast or omitted before any new scoring.
Let be the monthly unemployment rate in percent. Let be the mean of weekly initial claims whose week-ending Saturdays fall in month . Both are seasonally adjusted. Model
is in percentage points; is approximately percentage growth in the mean weekly claims count. A monthly mean is not a monthly total. A permanent step in this would mean permanently higher claims growth, not one fixed increase in the level of claims. We use the empirical model for prediction and diagnostics, not a causal claims intervention.
The origin contract: wait until the previous month’s unemployment and its final claims Saturday have both been released, before target-month unemployment is released. Ignore partial target-month claims. This is one step ahead by monthly observation index and ordinarily a nowcast of the current calendar month. Write the information set as .
The caches are revised historical values collected on different dates. This is revised-data pseudo-out-of-sample evaluation, not a real-time-vintage experiment. Actual deployment requires checking the actual release calendar.
app_u <- read.csv(file.path(data_dir, "UNRATE.csv"))
app_c <- read.csv(file.path(data_dir, "ICSA.csv"))
app_u$observation_date <- as.Date(app_u$observation_date)
app_c$observation_date <- as.Date(app_c$observation_date)
app_u <- app_u[app_u$observation_date >= as.Date("1988-12-01") &
app_u$observation_date <= as.Date("2019-12-01"), ]
app_c <- app_c[order(app_c$observation_date), ]
stopifnot(
!anyNA(app_u), !anyNA(app_c), all(app_c$ICSA > 0),
!anyDuplicated(app_c$observation_date),
all(as.POSIXlt(app_c$observation_date)$wday == 6),
identical(app_c$observation_date,
seq(as.Date("1989-01-07"), as.Date("2019-12-28"), by = "week")),
identical(app_u$observation_date,
seq(as.Date("1988-12-01"), as.Date("2019-12-01"), by = "month"))
)
# December 1988 unemployment supplies January 1989's change;
# this makes twelve outcome lags available by January 1990.
app_u$y <- c(NA_real_, diff(app_u$UNRATE))
app_u$U_previous <- c(NA_real_, head(app_u$UNRATE, -1L))
app_u <- app_u[app_u$observation_date >= as.Date("1989-01-01"), ]
app_c$month <- as.Date(format(app_c$observation_date, "%Y-%m-01"))
app_month_c <- aggregate(ICSA ~ month, data = app_c, FUN = mean)
app_nweeks <- table(app_c$month)
stopifnot(all(app_nweeks %in% c(4L, 5L)))
app_data <- merge(app_u, app_month_c,
by.x = "observation_date", by.y = "month", all = TRUE)
names(app_data)[match(c("observation_date", "UNRATE", "ICSA"),
names(app_data))] <- c("date", "U", "C")
stopifnot(nrow(app_data) == 372L, !anyNA(app_data))
app_data$x <- c(NA_real_, 100 * diff(log(app_data$C)))
for (j in 1:12) {
app_data[[paste0("y_l", j)]] <-
c(rep(NA_real_, j), head(app_data$y, -j))
if (j <= 3) {
app_data[[paste0("x_l", j)]] <-
c(rep(NA_real_, j), head(app_data$x, -j))
}
}
app_data <- app_data[app_data$date >= as.Date("1990-01-01"), ]
stopifnot(nrow(app_data) == 360L, !anyNA(app_data))
app_train <- app_data[app_data$date <= as.Date("2017-12-01"), ]
app_test <- app_data[app_data$date >= as.Date("2018-01-01"), ]
stopifnot(nrow(app_train) == 336L, nrow(app_test) == 24L)Training has 336 monthly observations, January 1990–December 2017. Hold out the 24 months of 2018–2019. Padding from 1989 makes all lag menus use the same training outcomes. The code checks complete calendars before it merges or computes changes; missing weeks are not zero and must not be averaged away.
app_paths <- rbind(
data.frame(date = app_train$date, value = app_train$U,
series = "Unemployment rate (percent)"),
data.frame(date = app_train$date, value = app_train$C / 1000,
series = "Mean weekly claims (thousands)"),
data.frame(date = app_train$date, value = app_train$y,
series = "y: change in unemployment (percentage points)"),
data.frame(date = app_train$date, value = app_train$x,
series = "x: 100 times log change in mean weekly claims")
)
ggplot(app_paths, aes(date, value)) +
geom_line(colour = "#16697A", linewidth = 0.4) +
facet_wrap(~ series, scales = "free_y", ncol = 1) +
labs(x = NULL, y = NULL) + theme_bw()Predict from the transformed plots: will the evidence support a stationary working baseline? The ADF null is a unit root; the KPSS null here is stationarity around a constant. The settings below are fixed in advance.
app_stationarity <- data.frame()
for (app_name in c("y", "x")) {
app_adf <- urca::ur.df(app_train[[app_name]], type = "drift",
lags = 12, selectlags = "BIC")
app_kpss <- urca::ur.kpss(app_train[[app_name]], type = "mu", use.lag = 12)
app_stationarity <- rbind(app_stationarity, data.frame(
Series = app_name,
ADF_statistic = unname(app_adf@teststat[1, "tau2"]),
ADF_5pct = unname(app_adf@cval["tau2", "5pct"]),
KPSS_statistic = unname(app_kpss@teststat),
KPSS_5pct = unname(app_kpss@cval[1, "5pct"])
))
}
knitr::kable(app_stationarity, digits = 3)| Series | ADF_statistic | ADF_5pct | KPSS_statistic | KPSS_5pct |
|---|---|---|---|---|
| y | -3.692 | -2.87 | 0.149 | 0.463 |
| x | -12.795 | -2.87 | 0.105 | 0.463 |
Here both ADF tests reject at 5% and neither KPSS test rejects. This supports a working specification. It does not prove stationarity, constant variance, exogeneity or stability over future regimes.
Fit a lag-only ARDL, setting , beside an AR. The initial menu has 1–3 own lags and 1–3 claims lags. In this code q = 0 labels the AR benchmark with no claims term; it is not the theoretical ARDL with current . BIC chooses within each family on the same training rows. The holdout chooses neither the lag orders nor the coefficient estimates.
app_grid <- expand.grid(p = 1:3, q = 0:3)
app_grid$family <- ifelse(app_grid$q == 0, "AR", "ARDL")
app_grid$AIC <- app_grid$BIC <- NA_real_
app_fits <- vector("list", nrow(app_grid))
for (i in seq_len(nrow(app_grid))) {
app_terms <- paste0("y_l", seq_len(app_grid$p[i]))
if (app_grid$q[i] > 0) {
app_terms <- c(app_terms, paste0("x_l", seq_len(app_grid$q[i])))
}
app_fits[[i]] <- lm(reformulate(app_terms, response = "y"),
data = app_train, na.action = na.fail)
app_grid$AIC[i] <- AIC(app_fits[[i]])
app_grid$BIC[i] <- BIC(app_fits[[i]])
stopifnot(nobs(app_fits[[i]]) == nrow(app_train))
}
app_ar_rows <- which(app_grid$family == "AR")
app_ardl_rows <- which(app_grid$family == "ARDL")
app_ar_row <- app_ar_rows[which.min(app_grid$BIC[app_ar_rows])]
app_ardl_row <- app_ardl_rows[which.min(app_grid$BIC[app_ardl_rows])]
app_ar <- app_fits[[app_ar_row]]
app_ardl <- app_fits[[app_ardl_row]]
app_grid$selected <- seq_len(nrow(app_grid)) %in%
c(app_ar_row, app_ardl_row)
knitr::kable(app_grid[order(app_grid$family, app_grid$BIC),
c("family", "p", "q", "AIC", "BIC", "selected")],
digits = 2, row.names = FALSE)| family | p | q | AIC | BIC | selected |
|---|---|---|---|---|---|
| AR | 3 | 0 | -326.58 | -307.50 | TRUE |
| AR | 2 | 0 | -311.87 | -296.60 | FALSE |
| AR | 1 | 0 | -295.81 | -284.36 | FALSE |
| ARDL | 3 | 3 | -362.42 | -331.88 | TRUE |
| ARDL | 2 | 3 | -351.75 | -325.03 | FALSE |
| ARDL | 1 | 3 | -347.93 | -325.02 | FALSE |
| ARDL | 3 | 2 | -343.53 | -316.81 | FALSE |
| ARDL | 3 | 1 | -332.16 | -309.25 | FALSE |
| ARDL | 2 | 2 | -329.17 | -306.27 | FALSE |
| ARDL | 1 | 2 | -320.36 | -301.27 | FALSE |
| ARDL | 2 | 1 | -315.68 | -296.59 | FALSE |
| ARDL | 1 | 1 | -302.14 | -286.87 | FALSE |
app_residuals <- data.frame(date = app_train$date, e = residuals(app_ardl))
ggplot(app_residuals, aes(date, e)) +
geom_hline(yintercept = 0, colour = "grey65") +
geom_line(colour = "#16697A", linewidth = 0.4) +
labs(x = NULL, y = "Residual (percentage points)") + theme_bw()forecast::ggAcf(app_residuals$e, lag.max = 24) + theme_bw()The Breusch–Godfrey-style auxiliary regression includes the original regressors and 12 residual lags, using the same auxiliary sample after dropping the first 12 rows. Its chi-squared reference is asymptotic and relies on maintained regression conditions, including the appropriate homoskedastic variance restriction; it is not robust to arbitrary conditional heteroskedasticity. Selection also makes this an exploratory diagnostic. It is a specification warning, not an exogeneity test.
app_bg_data <- app_train
app_bg_data$e <- as.numeric(residuals(app_ardl))
for (j in 1:12) {
app_bg_data[[paste0("e_l", j)]] <-
c(rep(NA_real_, j), head(app_bg_data$e, -j))
}
app_bg_data <- app_bg_data[13:nrow(app_bg_data), ]
app_bg_terms <- c(attr(terms(app_ardl), "term.labels"), paste0("e_l", 1:12))
app_bg_fit <- lm(reformulate(app_bg_terms, response = "e"), data = app_bg_data)
app_bg_stat <- nobs(app_bg_fit) * summary(app_bg_fit)$r.squared
app_bg_p <- pchisq(app_bg_stat, df = 12, lower.tail = FALSE)
knitr::kable(data.frame(Test = "BG, residual lags 1 through 12",
Statistic = app_bg_stat, df = 12,
p_value = format.pval(app_bg_p, digits = 3)),
digits = 4)| Test | Statistic | df | p_value |
|---|---|---|---|
| BG, residual lags 1 through 12 | 45.4938 | 12 | 8.48e-06 |
The short specification selects the edge of the lag menu and leaves serial dependence. Make one training-only revision: allow up to 12 own lags in both families, keeping at most 3 claims lags and the same 336 training outcomes.
app_short_ardl <- app_ardl
app_short_bg_p <- app_bg_p
app_expanded <- expand.grid(p = 1:12, q = 0:3)
app_expanded$family <- ifelse(app_expanded$q == 0, "AR", "ARDL")
app_expanded$BIC <- NA_real_
app_expanded_fits <- vector("list", nrow(app_expanded))
for (i in seq_len(nrow(app_expanded))) {
app_terms <- paste0("y_l", seq_len(app_expanded$p[i]))
if (app_expanded$q[i] > 0) {
app_terms <- c(app_terms, paste0("x_l", seq_len(app_expanded$q[i])))
}
app_expanded_fits[[i]] <- lm(reformulate(app_terms, response = "y"),
data = app_train, na.action = na.fail)
app_expanded$BIC[i] <- BIC(app_expanded_fits[[i]])
stopifnot(nobs(app_expanded_fits[[i]]) == nrow(app_train))
}
app_exp_ar_rows <- which(app_expanded$family == "AR")
app_exp_ardl_rows <- which(app_expanded$family == "ARDL")
app_exp_ar_row <- app_exp_ar_rows[which.min(app_expanded$BIC[app_exp_ar_rows])]
app_exp_ardl_row <- app_exp_ardl_rows[which.min(app_expanded$BIC[app_exp_ardl_rows])]
app_ar <- app_expanded_fits[[app_exp_ar_row]]
app_ardl <- app_expanded_fits[[app_exp_ardl_row]]
# Show each family's three smallest training BIC values.
app_show_rows <- c(
head(app_exp_ar_rows[order(app_expanded$BIC[app_exp_ar_rows])], 3),
head(app_exp_ardl_rows[order(app_expanded$BIC[app_exp_ardl_rows])], 3)
)
knitr::kable(app_expanded[app_show_rows, c("family", "p", "q", "BIC")],
digits = 2, row.names = FALSE)| family | p | q | BIC |
|---|---|---|---|
| AR | 6 | 0 | -314.45 |
| AR | 5 | 0 | -312.45 |
| AR | 7 | 0 | -308.75 |
| ARDL | 6 | 3 | -356.51 |
| ARDL | 7 | 3 | -355.72 |
| ARDL | 8 | 3 | -350.93 |
app_bg_data$e <- as.numeric(residuals(app_ardl))[13:nrow(app_train)]
for (j in 1:12) {
app_bg_data[[paste0("e_l", j)]] <-
c(rep(NA_real_, j), head(as.numeric(residuals(app_ardl)), -j))[
13:nrow(app_train)]
}
app_bg_terms <- c(attr(terms(app_ardl), "term.labels"), paste0("e_l", 1:12))
app_bg_fit <- lm(reformulate(app_bg_terms, response = "e"), data = app_bg_data)
app_bg_stat <- nobs(app_bg_fit) * summary(app_bg_fit)$r.squared
app_bg_p <- pchisq(app_bg_stat, df = 12, lower.tail = FALSE)
app_phi <- coef(app_ardl)[grep("^y_l", names(coef(app_ardl)))]
app_min_root <- min(Mod(polyroot(c(1, -app_phi))))
knitr::kable(data.frame(
Diagnostic = c("Initial short ARDL: BG p-value", "Revised ARDL: BG p-value",
"Revised ARDL: smallest AR root modulus"),
Value = c(format.pval(app_short_bg_p, digits = 3),
format.pval(app_bg_p, digits = 3),
format(round(app_min_root, 4), nsmall = 4))
), digits = 4)| Diagnostic | Value |
|---|---|
| Initial short ARDL: BG p-value | 8.48e-06 |
| Revised ARDL: BG p-value | 0.0031 |
| Revised ARDL: smallest AR root modulus | 1.1412 |
forecast::ggAcf(residuals(app_ardl), lag.max = 24) + theme_bw()The choices are AR(6) and lag-only ARDL ( 6, 3). The ARDL’s smallest lag-polynomial root modulus is 1.141. Its revised BG -value is 0.0031; serial dependence remains detectable. Report that limitation and stop this illustration’s revision here. Do not keep searching until a diagnostic passes. HAC standard errors would not repair invalid moments or a misspecified mean.
app_revised_coef <- data.frame(Term = names(coef(app_ardl)),
Estimate = unname(coef(app_ardl)))
knitr::kable(app_revised_coef, digits = 4, row.names = FALSE)| Term | Estimate |
|---|---|
| (Intercept) | -0.0001 |
| y_l1 | -0.1646 |
| y_l2 | -0.0029 |
| y_l3 | 0.0933 |
| y_l4 | 0.0976 |
| y_l5 | 0.2197 |
| y_l6 | 0.2092 |
| x_l1 | 0.0083 |
| x_l2 | 0.0108 |
| x_l3 | 0.0101 |
The x_l1 coefficient is an association between approximately one percentage point faster prior-month claims growth and the current unemployment-rate change, in percentage points, holding the other included lags fixed. It is not the effect of forcing people to file claims. Different conditioning sets can give different coefficient estimates.
Predict: does the ARDL have to beat the AR or no-change benchmark on this short holdout just because it has extra information? Commit before scoring.
Predict only from released lag columns. Reconstruct each level using . Do not accumulate all 24 changes from the December 2017 level; that would be a different multi-step exercise. No change in the level means a zero prediction for .
app_pred <- data.frame(
AR = as.numeric(predict(app_ar, newdata = app_test)),
ARDL = as.numeric(predict(app_ardl, newdata = app_test)),
No_change = rep(0, nrow(app_test))
)
app_scores <- data.frame()
app_forecast_paths <- data.frame()
for (app_model in names(app_pred)) {
app_error <- app_test$y - app_pred[[app_model]]
app_level_forecast <- app_test$U_previous + app_pred[[app_model]]
stopifnot(isTRUE(all.equal(app_error,
app_test$U - app_level_forecast)))
app_scores <- rbind(app_scores, data.frame(
Model = app_model, N = length(app_error),
RMSE = sqrt(mean(app_error^2)), MAE = mean(abs(app_error))
))
app_forecast_paths <- rbind(app_forecast_paths, data.frame(
date = app_test$date, U = app_level_forecast, Model = app_model
))
}
knitr::kable(app_scores, digits = 4, row.names = FALSE)| Model | N | RMSE | MAE |
|---|---|---|---|
| AR | 24 | 0.1129 | 0.0929 |
| ARDL | 24 | 0.1057 | 0.0831 |
| No_change | 24 | 0.1099 | 0.0875 |
ggplot(app_forecast_paths, aes(date, U, colour = Model, linetype = Model)) +
geom_line(linewidth = 0.6) +
geom_line(data = app_test, aes(date, U), inherit.aes = FALSE,
colour = "black", linewidth = 0.8) +
geom_point(data = app_test, aes(date, U), inherit.aes = FALSE,
colour = "black", size = 1.2) +
scale_colour_manual(values = c(AR = "#16697A", ARDL = "#D1495B",
No_change = "#8A7B37")) +
scale_x_date(breaks = as.Date(c("2018-01-01", "2018-07-01",
"2019-01-01", "2019-07-01")),
date_labels = "%Y-%m") +
labs(x = NULL, y = "Unemployment rate (percent)",
subtitle = "Black line and points: actual unemployment rate") +
theme_bw() + theme(legend.position = "bottom")On these dates the ARDL’s RMSE is 0.1057 percentage points; the AR’s is 0.1129 and the no-change benchmark’s is 0.1099. Change and reconstructed-level errors coincide because they add the same observed prior level to both sides. MAE is also in percentage points; MAPE is unsuitable for changes that can be zero or negative.
This ranking describes the chosen specifications and these dates. It does not establish exogeneity, causal effects, or a universal advantage of adding predictors. Do not retune on this holdout and still call it untouched.
Modify: state a different lag menu before running it, keep common samples and the same issue dates, and report the result as an additional exploratory comparison. Explain why it does not overturn the existing specification warning merely because the score improves.
These previews are outside Module 8’s assessable Core and do not add a new required simulation/application cycle.
Ask the tutor to build any of these on request, labelled as a preview.
These investigations are outside the assessable surface. They are starting points, not extra required questions or official assessment answers.
n = 100, 400, and 2000. Plot dispersion and mean coefficient separately. Which discrepancy shrinks, and which target is the mean approaching?feedback modestly around zero. Derive the joint transition matrix first and check its eigenvalues before claiming stationarity. Measure how relates to and reconcile with sequential exogeneity.Read CHECKS.md for the scope of these six checks. They verify shipped examples and invariants; they cannot certify a causal interpretation or every student modification. Running them in a private environment leaves your inline functions and data unchanged.
source("R/checks.R", local = new.env())Module 8 checks
---------------
PASS 1. Complete calendars, mean weekly aggregation, dated lags, 336 training and 24 holdout rows [weeks=1617; training=336; holdout=24]
PASS 2. Simulator equations, seed reproducibility, burn_in=0 length and joint feedback stability [max recursion gap=2.2e-16; largest eigenvalue=0.8868]
PASS 3. Structural targets in independent-shock cases; distinct conditional targets with shared shocks [max target gap=0.0052; shared beta0 estimate=1.1037; remainder ACF1=-0.0029]
PASS 4. Pulse recursion, cumulative step, stable limit and unstable nonconvergence; persistent-x distinction [pulse h1=0.44; persistent-x h1=0.64; stable limit=1.50]
PASS 5. Unavailable-predictor error contains beta0 epsilon_x; oracle changes the information set [feasible-minus-oracle MSE=0.1535; population gap=0.16]
PASS 6. Common-sample AR(6)/ARDL(6,3), frozen lag-only predictions, matched errors and remaining BG warning [RMSE=0.112942/0.105747/0.109924; BG p=0.003095; min root=1.1412]
---------------
6/6 checks passed.
Finish your private learning-log entry. Record your revised reasoning and remaining uncertainty in your own words. If your prediction was wrong, say which assumption, information set, or input experiment changed your mind.