Module 9 Lab: Granger Causality in Dynamic Regression
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 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: the predictive question → the joint test → simulations with a known mechanism → claims and unemployment → uses and limits. The notes are canonical for facts, notation and the Core boundary. Sections 2–7 are the main path; sections 8–9 are optional.
Where are we on the mixing board? No new slider. We keep Module 8’s ARDL board — outcome lags on, observed-predictor lags on, explicit MA terms and deterministic trend off — and ask whether switching off the lagged- block costs us predictive information. The current- term is left out of both regressions in the past-only comparison.
1. Setup
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_09 and dir.exists("data") should be TRUE. Quarto’s file render uses the lab’s directory automatically. Installed R packages are ggplot2, urca, knitr and rmarkdown; rendering also needs Quarto. Render this file by path, not the whole folder.
The canonical course helpers granger_nested_test() and granger_bg_test() live in R/granger_utils.R. We build the F statistic by hand first, then load the helpers in section 4 and make them agree with our arithmetic.
2. The predictive question
Keep three questions apart:
Question
What an answer would tell us
Do and move together?
Association.
Does the past of improve prediction of beyond the history already used?
Additional predictive information — the Granger question.
Would intervening on change ?
A causal intervention effect, under an identification argument.
The past of Granger-causes if it contains information useful for predicting beyond the past of and the other information specified in the comparison. Three words do the work: incremental (with versus without ’s history), directional ( helping predict is a different question from helping predict ), and conditional (say what else is known). In shorthand the null is and the alternative ; the arrows stand for a joint restriction on a whole included lag block, conditional on the stated own lags and other information.
Hamilton’s thesis is the module’s thesis: these are tests of whether one series helps forecast another, not of whether it causes it; they bear on the causal question only together with further assumptions (Hamilton §11.2; see the notes for his oil-price example). Claims can be an early signal of a labor-demand deterioration without being a lever: changing the number in the claims database would not change employment.
Within-period effects are a different question
Add a same-period term to the working ARDL:
The Granger null concerns only . A same-period link is what Granger (1969) called instantaneous causality. Our past-only comparison leaves out of both regressions; it does not test .
Take the simplest case: independent over time and . Changing changes in this DGP.
Predict: in a past-only comparison ( versus plus ), will the test (a) reject decisively because clearly affects , (b) typically not reject because yesterday’s carries nothing about today’s , or (c) fail to run?
set.seed(6376093)n_wp <-600x_all <-rnorm(n_wp +1L)y_all <-0.8* x_all +rnorm(n_wp +1L)wp <-data.frame(y = y_all[-1], x = x_all[-1],y_l1 = y_all[-(n_wp +1L)], x_l1 = x_all[-(n_wp +1L)])wp_r <-lm(y ~ y_l1, data = wp, na.action = na.fail)wp_u <-lm(y ~ y_l1 + x_l1, data = wp, na.action = na.fail)wp_past_p <-anova(wp_r, wp_u)[2, "Pr(>F)"]wp_beta0 <-unname(coef(lm(y ~ y_l1 + x + x_l1, data = wp))["x"])# Residuals from the two past-only equations: same-period dependence.wp_rx <-lm(x ~ x_l1 + y_l1, data = wp, na.action = na.fail)wp_cor <-cor(residuals(wp_u), residuals(wp_rx))knitr::kable(data.frame(Quantity =c("Past-only Granger p-value (x lag in y equation)","Estimated beta_0 when current x is included","Correlation of the two equations' residuals"),Value =c(format.pval(wp_past_p, digits =3), round(wp_beta0, 3),round(wp_cor, 3))))
Quantity
Value
Past-only Granger p-value (x lag in y equation)
0.18
Estimated beta_0 when current x is included
0.842
Correlation of the two equations’ residuals
0.647
The past-only p-value is 0.18: lagged carries no information here, so the test has nothing to find. Yet 0.842 when current enters, close to the DGP value 0.8, and the two equations’ residuals correlate at 0.647. That residual correlation is symmetric — it is the same number whichever variable you call the outcome — so by itself it cannot say which moved first within the period.
Modify: put current x in both regressions and re-test x_l1. That asks whether earlier adds information given current — a different question again. Putting xinside the tested block instead would make a joint test of instantaneous and lagged terms. Neither is our past-only comparison.
Expectations can reverse the apparent story
In Hamilton’s present-value example (§11.2), stock prices are set by expected future dividends and do not affect dividends. Yet past prices help predict dividends while past dividends do not help predict prices: the price already contains investors’ news. The predictive direction runs opposite to the structural one. Forward-looking behavior is a central reason to keep “helps forecast” and “causes” apart.
3. Turn the question into a joint test
The unrestricted and restricted regressions are
with against “at least one .” Here is the largest predictor lag, not an MA order. Re-estimate everything in the restricted model; do not freeze the unrestricted estimates. The null is the whole block: one insignificant is not this test, and neither is , because offsetting lag effects can still change predictions.
With and from the two fits on the same observations, and counting every unrestricted coefficient including the intercept,
The reference is an approximation under maintained dynamic- regression conditions, not an exact finite-sample law.
Predict: ; the unrestricted model has an intercept, two own lags and three lags; , . Count and first. Is closer to (a) 0.2, (b) 7.6, or (c) 18?
F numerator_df denominator_df p_value
1 7.6 3 114 0.0001118079
Now write the statistic as a function of two fitted models. It is deliberately naive: it trusts its inputs.
f_by_hand <-function(restricted, unrestricted) { rss_r <-sum(residuals(restricted)^2) rss_u <-sum(residuals(unrestricted)^2) n <-nobs(unrestricted) k_u <-length(coef(unrestricted)) q <- k_u -length(coef(restricted)) F_stat <- ((rss_r - rss_u) / q) / (rss_u / (n - k_u))data.frame(n_R =nobs(restricted), n_U = n, q = q, k_U = k_u,RSS_R = rss_r, RSS_U = rss_u, F = F_stat,p_value =pf(F_stat, q, n - k_u, lower.tail =FALSE))}
On a common sample the unrestricted model can reproduce the restricted fit by setting its extra coefficients to zero, so . A negative RSS reduction means the models or rows are wrong, not that you found something.
Reverse the direction with another equation. To ask whether helps predict , make the outcome with its own lags and test the added -lag block. That is a new regression and a new null.
An in-sample test of a predictive idea
The F-test judges a predictive idea by in-sample fit. Ashley, Granger and Schmalensee (1980) argued that credible Granger testing should lean on out-of-sample forecasting. The trade-off: in-sample fit can reward overfitting, while a pre-chosen holdout spends data and power. Ashley and Tsang (2014) cross-sample validation splits at , fits each side and predicts the other, so every prediction uses coefficients from data not used to make it. Cornwall, Mills, Sauley and Weng (2019) hold out one block at a time and compare the restricted and unrestricted models’ held-out losses.
Predict: in-sample, always on a common sample. On held-out data, can the unrestricted model predict worse? Hold your answer for section 6.
4. Simulate a known one-way relationship
with mutually independent standard normal innovations. The equation has lagged ; the equation has no lagged . The for-loop makes the timing literal.
Predict: which direction has population predictive content? Must every finite sample produce exactly one rejection and one nonrejection — (a) yes, (b) no, sampling variation can produce either error?
Lag columns come from the pre-sample burn-in, so both regressions use the same 600 rows. Now confirm the arithmetic against R’s own nested-model F test and the course helper:
source("R/granger_utils.R")anova(sim_y_r, sim_y_u, test ="F")
Analysis of Variance Table
Model 1: y ~ y_l1
Model 2: y ~ y_l1 + x_l1
Res.Df RSS Df Sum of Sq F Pr(>F)
1 598 853.87
2 597 595.27 1 258.6 259.35 < 2.2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
The cross-lag estimate is 0.600 against the DGP value 0.6 — agreement after rounding, not exact recovery. The reverse-direction p-value is 0.303. A p-value is evidence from one sample, not the definition of the mechanism.
ggplot(data.frame(t = sim$t, e =residuals(sim_y_u)), aes(t, e)) +geom_line(colour ="#315375") +labs(x ="Observation", y ="Fitted residual",title ="Known one-way DGP: residuals from the unrestricted outcome equation")
Break the comparison: unequal rows
Build lag columns by padding inside the retained sample instead, and let lm() drop incomplete rows silently. The restricted model uses one own lag; the unrestricted model adds three lags.
Predict: will f_by_hand() (a) refuse, (b) quietly return a number, or (c) return exactly the same F as a correct common-sample comparison?
[1] "refused: identical(rownames(r), rownames(u)) is not TRUE"
The naive function returned a number from 599 restricted rows and 597 unrestricted rows: the RSS difference mixes a model change with a data change. Here only 2 rows differ, so the naive F (88.49) happens to sit near the common-rows F (88.45) — closeness in one draw is luck, not validity. With twelve own lags in one model and three in the other, or a missing month in the middle, the gap need not be small. The helper checks matching outcomes, rows and nested design columns and refuses. The repair is to build the complete lagged sample before fitting either model and use na.action = na.fail so nothing is dropped silently.
Modify and reconcile: keep the seed and innovations, replace 0.6 by zero in the loop, regenerate and re-test both directions. The null is now true in both. A p-value below 0.05 would be a false rejection, not a hidden effect. Then shorten the retained sample and explain why weaker evidence does not mean a weaker DGP coefficient.
5. A useful signal need not be a lever
Unobserved news arrives independently each period. The recorded signal is noisy news, and responds to yesterday’s news:
, and are mutually independent standard normal draws. There is no term in the structural equation for .
Predict: (1) Does help predict when is omitted — (a) yes, (b) no? (2) Once is included, is the sample test (a) guaranteed not to reject, or (b) able to reject even though the population cross-lag coefficient is zero?
The first comparison finds genuine predictive information: the signal reveals part of yesterday’s news. It is not a false positive merely because does not drive . The second asks whether the signal helps once the actual news is known; its population coefficient is zero. In this draw its p-value is 0.01566 — a false rejection in this sample. We keep this draw on purpose. Adding the driver removes ’s population content; it does not guarantee a sample nonrejection.
Now impose a change on recorded , holding , the initial outcome and its innovations fixed. The equation contains no , so the path does not move. Informative in observational data; not a driver under intervention.
How often does a true null get rejected?
One false rejection is one draw. Repeat the conditional comparison, whose null is true by construction.
Predict: at the 5% level, across 200 independent samples, will the rejection rate be closer to (a) 0%, (b) 5%, or (c) 25%?
4.5% of these null-true samples reject at 5%. That is the size working as designed, within Monte Carlo error. Our kept draw is one of the roughly one-in-twenty.
6. Application: claims and unemployment
is the monthly unemployment rate (percent). is the mean of the weekly initial-claims observations whose week-ending Saturdays fall in month — not a monthly total. Both are seasonally adjusted. As in Module 8,
is in percentage points; is approximately percentage growth of the monthly claims mean.
Dates on the data and dates on the clock
The forecast origin waits until both previous-month measures are released, before the target month’s unemployment release: usually a nowcast of the current calendar month, written with information . The calendar is messier than the index:
Frequency and aggregation. refers to the CPS reference week (the week containing the 12th) and is released early in month . averages weeks before and after that reference week, so the monthly “lag” is not a clean calendar lead: the late weeks inside fall within the period measures. Aggregation can create or hide predictive direction, and can push a one-week response into the same month.
Seasonal adjustment. BLS and the Department of Labor adjust the series separately; adjusted history is revised with later data, so a past-only comparison may not be strictly past-only.
Release timing. Weekly claims come out each Thursday. By the time is released, claims covering month ’s reference week have been public for about two weeks. Our past-only design leaves them out on purpose, so it understates claims’ practical nowcasting value. Using them would be the same-period question of section 2.
The caches are revised values collected on different dates: this is a revised-data pseudo-out-of-sample exercise, not a real-time vintage study.
Load and check the data
Predict: if one week’s claims were missing, should the monthly mean be (a) computed from the remaining weeks, or (b) treated as an error?
Training is January 1990–December 2017 (336 rows); January 2018–December 2019 (24 rows) is reserved for evaluation. 1989 supplies differences and lags, so every specification below uses the same training outcomes. The window avoids the pandemic, which also limits what the results say about later periods.
paths <-rbind(data.frame(date = train$date, value = train$y,Series ="Unemployment change (percentage points)"),data.frame(date = train$date, value = train$x,Series ="Claims: 100 x log change"))ggplot(paths, aes(date, value)) +geom_line(colour ="#315375") +facet_wrap(~ Series, ncol =1, scales ="free_y") +labs(x =NULL, y =NULL, title ="Transformed training data, 1990-2017")
The transformations are a stationary working specification. Reuse Module 2: ADF has a unit-root null; KPSS has a level-stationarity null. The ADF lag choice below does not choose the ARDL’s lag orders.
Both ADF statistics fall below their 5% critical values and neither KPSS statistic exceeds its own. That supports the working specification; it does not prove a time-invariant DGP.
Fix the comparison, then diagnose it
Main specification in each direction: six own lags and three lags of the other variable. For unemployment this carries forward the specification reached in Module 8’s exploratory training work; it is not a fresh prespecified confirmatory test. The reverse equation reuses the lag lengths for comparability.
Forward null: conditional on six lags of unemployment changes, the three claims-growth lag coefficients are all zero. The reverse null exchanges roles.
Predict: for the forward equation, count , , and the residual degrees of freedom before running. Then: if the nominal F rejects but the Breusch–Godfrey diagnostic also rejects, do you (a) report a Granger-causality verdict, (b) report the nominal result and the limitation, or (c) add lags until the diagnostic passes?
The Breusch–Godfrey (BG) diagnostic regresses the fitted residual on the original regressors and its own lags 1–12 (dropping the first twelve rows), and compares with an asymptotic . Its null is no residual serial correlation through lag 12. It is a specification warning, not an exogeneity test.
Forward: nominal F p-value 1.71e-12, BG(12) p-value 0.0031. Reverse: nominal p-value 0.146, BG(12) p-value 0.255. The forward equation fails the residual-dynamics check at 5%. Its tiny nominal p-value is therefore not a clean basis for a validated Granger-causality verdict. Report the arithmetic and the limitation, and stop the illustration here. The reverse nonrejection is “not detected with these lags,” not “unemployment changes carry no information about claims.”
residual_paths <-rbind(data.frame(date = train$date, e =residuals(y_u), Equation ="Unemployment-change outcome"),data.frame(date = train$date, e =residuals(x_u), Equation ="Claims-growth outcome"))ggplot(residual_paths, aes(date, e)) +geom_line(colour ="#315375") +facet_wrap(~ Equation, ncol =1, scales ="free_y") +labs(x =NULL, y ="Fitted residual", title ="Residual behavior is part of the evidence")
A limited sensitivity table, not a search for significance
Three specifications fixed in advance, , with the outcome’s own lags and the other variable’s lags, all on the same 336 rows.
Predict: does the reverse-direction decision at 5% (a) stay the same across all three rows, or (b) depend on the lag menu?
menu <-data.frame(p =c(3, 6, 6), q =c(1, 1, 3))sensitivity <-data.frame()for (outcome inc("y", "x")) { predictor <-if (outcome =="y") "x"else"y"for (i inseq_len(nrow(menu))) { own <-paste0(outcome, "_l", seq_len(menu$p[i])) other <-paste0(predictor, "_l", seq_len(menu$q[i])) r <-lm(reformulate(own, response = outcome), data = train, na.action = na.fail) u_fit <-lm(reformulate(c(own, other), response = outcome), data = train, na.action = na.fail) result <-granger_nested_test(r, u_fit) diagnostic <-granger_bg_test(u_fit, order =12) sensitivity <-rbind(sensitivity, data.frame(Outcome = outcome, p = menu$p[i], q = menu$q[i], n = result$n,F = result$F, Nominal_F_p = result$p_value, BG_12_p = diagnostic$p_value)) }}knitr::kable(transform(sensitivity,Nominal_F_p =format.pval(Nominal_F_p, digits =3, eps =1e-6),BG_12_p =format.pval(BG_12_p, digits =3, eps =1e-6)), digits =3)
Reverse direction, nominal p-values: 0.00518 at (3,1), 0.0286 at (6,1), and 0.146 at the main (6,3). Whether unemployment history appears to help predict claims depends on the finite lag menu. In the forward direction every row rejects, but the BG(12) p-values are 1.09e-13, 3.5e-07 and 0.0031: the shorter specifications fail the residual check far more decisively.
Hamilton (§11.2) reports the same lag-length sensitivity in the money–income literature, and Enders (Ch. 5 §8) frames the trade-off: too few lags misspecify, too many waste degrees of freedom. The remedy is to fix lags in advance by a stated rule on a common sample — not to report the row you like. Stop here. If residual dependence remains, revisit the economic specification as a separate modeling exercise.
Compare forecasts separately
Freeze the training coefficients of the restricted (6 own lags) and unrestricted (6 own, 3 claims lags) unemployment models. At each 2018–2019 origin, newly observed previous-month values supply the lags: a sequence of one-step predictions, not a 24-step path from December 2017.
Predict: recall section 3’s question. Must the claims model win this 24-month holdout because in training — (a) yes, (b) no?
ggplot(forecast_paths, aes(date, U, colour = Model, linetype = Model)) +geom_line() +geom_line(data = test, aes(date, U), inherit.aes =FALSE,linewidth =0.9, colour ="black") +labs(x =NULL, y ="Unemployment rate (percent)",title ="One-step index predictions at post-release origins, 2018-2019",subtitle ="Black line: observed unemployment; coefficients fixed after training") +scale_x_date(breaks =as.Date(c("2018-01-01", "2018-07-01", "2019-01-01", "2019-07-01")),date_labels ="%b %Y") +theme(legend.position ="bottom")
The level prediction is , so change and level errors are identical, and RMSE and MAE are in percentage points. The added-claims RMSE is 0.1057 against 0.1129 for own history and 0.1099 for no change: a modest realized improvement on 24 particular months. That is descriptive. It does not repair the training diagnostics, identify an intervention effect, or promise a future gain, and we attach no ordinary Diebold–Mariano p-value to these nested estimated models.
Keep three conclusions separate: the nominal restriction tests (arithmetic plus limitations), the diagnostics and sensitivity (what the regressions leave and how much results lean on lags), and the held-out scores (realized accuracy on revised data at stated origins).
Write your report. Using the notes’ reporting template (§9.10), draft a short paragraph for the forward direction in your own words: transformed outcome and units, frequency and dates, lags and information, the excluded block, , F and degrees of freedom, nominal p-value, diagnostics, a bounded conclusion, and the separate holdout result. “We computed the test but cannot give it the usual interpretation under this specification” is a legitimate finding.
7. Uses and limits
Integrated levels. Our clean baseline is a stationary dynamic regression. With integrated levels, lag-exclusion statistics can have nonstandard reference distributions, so a routine p-value does not settle anything. “Difference everything” is not a universal remedy either: two wandering levels may share a stationary long-run relationship whose adjustment carries predictive information. Recognizing this is Core; the machinery is Module 10’s.
Economics and beyond. Sims (1972) asked whether money helps predict income and the reverse. Friedman and Kuttner’s evidence, discussed by Enders, shows the money–income result changing across sample periods under a fixed specification. In brain-signal research the same ARDL comparison asks whether region A’s past helps predict region B; slow, indirect fMRI measurement complicates reading a predictive direction as neural influence. In each case, state the series, the outcome, the other histories, the lags and the period.
Research connection. Cornwall, Mills, Sauley and Weng (2019) find that some in-sample Phillips-curve rejections coexist with weak held-out predictive evidence. Ask what was predicted, against which benchmark, on which observations, with what loss and what information.
8. Looking Ahead — optional, outside Core
Starting from an ARDL with current , , subtracting and collecting terms gives, for ,
with , and . This is algebra, not proof of cointegration; Module 10 develops the evidence. Ask the tutor to build any preview on request, labelled as such.
9. Explorations — optional, guided to open
These are outside the assessable surface: starting points, not required questions or official answers.
Size and sample length. Rerun section 5’s null-rejection loop with the retained sample cut to 60 and 150 observations. Predict first: does the 5% rejection rate drift? Report Monte Carlo standard errors.
Power curve. In section 4, replace 0.6 by 0.05, 0.1 and 0.2 and repeat each over a few hundred seeded samples at two sample sizes. Plot rejection rate against the coefficient. Which matters more here, coefficient or ?
Aggregation moves the lag. Simulate a weekly process in which affects with a one-week lag. Average both to four-week “months.” Run the past-only monthly test and a monthly regression that includes current . Where did the effect go? Connect your answer to instantaneous causality.
A persistent hidden driver. Make an AR(1) in section 5. Does including only still remove ’s predictive content with one lag? State the population target before running.
Levels versus changes. Simulate two independent random walks, run a Granger F-test in levels with own lags, and record the 5% rejection rate over many seeds. Then repeat in differences. Label this a Module 10 preview.
Cross-sample validation, one split. On the training data, pick one , fit the restricted and unrestricted unemployment models on each side and predict the other side. Sum the squared prediction errors and form the Ashley–Tsang pseudo-F. Can it be negative? Try several and explain why a single split is arbitrary.
10. Run the referee and reconcile
Read CHECKS.md for the scope of these checks. They verify shipped examples and invariants; they cannot certify a causal interpretation or every modification. Running them in a private environment leaves your objects unchanged.
source("R/checks.R", local =new.env())
Module 9 checks
---------------
PASS 1. Complete Saturday calendar, 4-5 weeks per month, mean weekly aggregation, dated lags, 336 training and 24 evaluation rows [weeks=1617; months merged=372; training=336; evaluation=24]
PASS 2. Manual F = anova F = helper F on common rows; q=3, k_U=10, df=326; RSS_U <= RSS_R; unequal rows refused [manual=21.058129; anova=21.058129; helper=21.058129]
PASS 3. Known one-way DGP: lab draw's cross-lag estimate rounds to 0.600 and rejects; large sample near 0.6 [lab estimate=0.6001; large-n estimate=0.6040]
PASS 4. Common driver: pairwise signal rejects; kept conditional draw is a false rejection (p near 0.0157); population targets 0.4 and 0 [conditional p=0.01566; large-n pairwise=0.4018; conditional=0.0139]
PASS 5. Application: forward F and BG(12) failure, reverse nonrejection, reverse decision depends on lags [forward F=21.0581; BG p=0.00310; reverse p=0.1463]
PASS 6. Frozen one-step predictions use only lagged inputs; change and level errors match; holdout RMSEs reproduce [RMSE=0.112942/0.105747/0.109924]
---------------
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 information set, lag choice, or timing assumption changed your mind.