Part II: Classical Forecasting and Time Series Analysis
Chapter 6: Multivariate and Econometric Models

Granger Causality and Impulse-Response Analysis

"Yes, the rooster's crow predicts the dawn. No, I am not claiming the rooster lifts the sun. I only ever said that knowing the crow improves my forecast of the morning, and I will defend that modest sentence in any court that does not call itself a laboratory."

A Granger Test Carefully Not Claiming Real Causation
Big Picture

Once a vector autoregression ties several series together, two questions follow immediately: which series help predict which others, and how does a shock to one series ripple through the whole system over time. Granger causality answers the first with a forecast-improvement test, and impulse-response analysis answers the second by tracing a single disturbance forward through the VAR's dynamics. Both are read off the same fitted VAR of the previous section, and both come with a sharp caveat that this section repeats until it sticks: Granger causality is about predictability, not intervention, and impulse responses depend on an ordering assumption you must defend. This section defines Granger causality as a nested-model $F$-test on VAR coefficients, walks the testing procedure and its pitfalls (stationarity, lag choice, confounders, instantaneous causality), derives the impulse-response function from the moving-average representation of a VAR, confronts the identification problem and resolves it with the Cholesky decomposition and forecast-error variance decomposition, and closes with a full worked example that runs pairwise Granger tests both from scratch and through statsmodels, then computes and reads an IRF and an FEVD.

A cartoon detective with a magnifying glass notes that a crowing rooster reliably precedes the sunrise, while raising a cautioning hand to refuse crediting the rooster with causing the dawn, illustrating that Granger causality is prediction, not real intervention.
Figure 6.2.0: Granger causality only says the rooster's crow helps forecast the dawn; it never claims the rooster makes the sun rise.

The previous section of this chapter built the vector autoregression: a system in which each of $K$ series is regressed on $p$ lags of every series, itself included, so that the cross-series feedback the univariate models of Chapter 5 could never see becomes the central object. A fitted VAR is a dense bundle of coefficients, and on its own it is hard to interpret. This section supplies the two standard lenses that turn that bundle into statements a practitioner can act on. The first lens, Granger causality, asks a yes-or-no question about predictive content: does the past of one series carry information about the future of another beyond what that other series already knows about itself. The second lens, the impulse-response function, asks a quantitative question about propagation: if one series is hit by a one-time unit shock, what happens to every series in the system over the next several periods. Throughout we use the multivariate notation $\mathbf{y}_t$, the coefficient matrices $\mathbf{A}_i$, and the innovation covariance $\mathbf{\Sigma}_u$ collected in the unified notation table of Appendix A, and we lean on the stationarity machinery of Section 3.2 at every turn.

By the end of this section you should be able to do five concrete things. First, state Granger's definition precisely and explain why it is a claim about prediction and never about intervention. Second, run the Granger $F$-test as a nested-model comparison and read its verdict in both directions. Third, list the four assumptions the test rests on and recognize when each is violated. Fourth, derive the impulse-response function from the moving-average form of a VAR and read an IRF plot for stationarity, peak, and decay. Fifth, orthogonalize an IRF with the Cholesky decomposition, compute a forecast-error variance decomposition, and state out loud the ordering assumption both require. The worked example of subsection five exercises all five against a system whose answer we know in advance.

1. Granger Causality Defined Beginner

Clive Granger gave the word "causality" an operational, testable meaning that deliberately sidesteps the philosophical thicket of true causation. His definition is entirely about prediction. A series $X$ Granger-causes a series $Y$ if the past of $X$ improves the forecast of $Y$ beyond what the past of $Y$ alone achieves. Put formally, let $\mathcal{I}_t$ be all information up to time $t$ and let $\mathcal{I}_t^{-X}$ be that same information with the history of $X$ removed. Then $X$ Granger-causes $Y$ when

$$\operatorname{Var}\!\big(Y_{t+1} \mid \mathcal{I}_t\big) \;<\; \operatorname{Var}\!\big(Y_{t+1} \mid \mathcal{I}_t^{-X}\big).$$

The forecast of $Y$ gets strictly sharper (its error variance falls) when you are allowed to look at the past of $X$. That is the whole idea: causality, in Granger's sense, is a measurable reduction in prediction error. It says nothing about whether wiggling $X$ in the world would move $Y$. The rooster's crow Granger-causes the dawn because the crow precedes and predicts the sunrise, yet silencing the rooster leaves the sun entirely unmoved. We label this relationship "causality" only by Granger's narrow operational convention, and the entire interventional theory of when prediction may be promoted to genuine cause is deferred to Chapter 30, where the do-calculus and structural causal models of the causality chapter draw the line precisely.

Two structural conditions are baked into the definition and are easy to miss. The first is the temporal ordering: the conditioning set $\mathcal{I}_t$ stops at time $t$, so only the strict past of $X$ is allowed to help predict $Y_{t+1}$. This is what makes the definition directional and what excludes same-period (contemporaneous) effects from the test entirely, a gap we return to as instantaneous causality. The second is the conditioning on $Y$'s own past: the bar is not "does $X$ predict $Y$" but "does $X$ predict $Y$ beyond what $Y$ already predicts about itself." A series whose own history is highly informative sets a high bar, and an apparent $X \to Y$ link can evaporate once $Y$'s autoregressive structure is properly credited. The whole construction is, at bottom, a comparison of two conditional forecast-error variances, and everything operational follows from making that comparison a hypothesis test.

Key Insight: Granger Causality Is Predictability, Never Intervention

Burn this distinction in now, because it is the single most abused result in applied time-series work. Granger causality is a statement about information flow in time: the past of $X$ contains predictive content about the future of $Y$ that $Y$'s own past lacks. It is emphatically not a statement that manipulating $X$ would change $Y$. The two come apart whenever a common driver $Z$ leads both series with different lags (then $X$ Granger-causes $Y$ with no mechanism between them), and whenever expectations run ahead of events (stock prices Granger-cause earnings announcements because the market anticipates them). The correct verbal report is always "the past of $X$ helps predict $Y$," never "$X$ causes $Y$." When a colleague says a Granger test "proved causation," they have made the error this section exists to prevent. Real interventional causation needs the apparatus of Chapter 30; Granger gives you predictability and stops there, on purpose.

To make the definition testable we attach it to a VAR. Consider a bivariate VAR($p$) in $(X_t, Y_t)$, and write the equation for $Y_t$ alone, which regresses $Y$ on $p$ lags of itself and $p$ lags of $X$,

$$Y_t \;=\; c \;+\; \sum_{i=1}^{p} \alpha_i\, Y_{t-i} \;+\; \sum_{i=1}^{p} \beta_i\, X_{t-i} \;+\; u_t.$$

In this equation, $X$ fails to Granger-cause $Y$ exactly when every coefficient on lagged $X$ vanishes, $\beta_1 = \beta_2 = \cdots = \beta_p = 0$. That is a linear restriction on the parameters, so the natural test is the nested-model $F$-test (equivalently a Wald or likelihood-ratio test). Fit the unrestricted model above and record its residual sum of squares $\mathrm{RSS}_U$; fit the restricted model that drops the $X$ lags, $Y_t = c + \sum_i \alpha_i Y_{t-i} + u_t$, and record $\mathrm{RSS}_R$. The $F$-statistic comparing them is

$$F \;=\; \frac{(\mathrm{RSS}_R - \mathrm{RSS}_U)/p}{\mathrm{RSS}_U/(n - 2p - 1)} \;\sim\; F_{p,\; n - 2p - 1} \quad\text{under } H_0:\ \beta_1 = \cdots = \beta_p = 0.$$

The numerator measures how much the $X$ lags reduce the residual sum of squares (the forecast improvement at the heart of the definition); the denominator normalizes by the unrestricted error and the degrees of freedom. A large $F$, equivalently a small $p$-value, rejects the null and says the $X$ lags carry predictive content: $X$ Granger-causes $Y$. The test is asymmetric by construction. You run it again with the roles swapped (regress $X$ on lags of $X$ and $Y$, restrict the $Y$ lags) to ask whether $Y$ Granger-causes $X$, and the two answers are independent: you can find one direction, both directions (feedback), or neither. The four possible outcomes carve a small taxonomy that is worth naming, because each corresponds to a different picture of how the two series relate in time.

The Four Outcomes of a Bidirectional Granger Test
$X \to Y$$Y \to X$NameInterpretation
rejectretainUnidirectional $X$ to $Y$The past of $X$ helps predict $Y$, but not the reverse; the cleanest case
retainrejectUnidirectional $Y$ to $X$The arrow runs the other way; predictive content flows $Y$ to $X$
rejectrejectBidirectional (feedback)Each series helps predict the other; the genuinely coupled, hardest-to-interpret case
retainretainIndependence (in mean)Neither lagged history helps; any link is contemporaneous or nonlinear

The independence row carries a quiet warning that subsection two will sharpen: a "retain, retain" verdict rules out only linear lagged predictability. Two series can fail every Granger test and still be tightly coupled through a contemporaneous correlation or a nonlinear dependence the linear $F$-test cannot see. The test is a statement about one specific, linear, lagged channel of information, and reading more into a non-rejection than that is its own pitfall. The numeric example below shows the arithmetic of the $F$-test on small sums of squares so the formula stops being abstract.

Numeric Example: The Granger F-Test From Two Residual Sums of Squares

Suppose we fit a bivariate VAR with $p = 2$ lags on $n = 200$ usable observations. The restricted regression of $Y$ on its own two lags yields $\mathrm{RSS}_R = 96.0$; adding the two lags of $X$ (the unrestricted model) lowers it to $\mathrm{RSS}_U = 88.0$. The drop of $8.0$ is spread over $p = 2$ restrictions, and the unrestricted model has $n - 2p - 1 = 200 - 4 - 1 = 195$ residual degrees of freedom. The statistic is $F = \frac{(96.0 - 88.0)/2}{88.0/195} = \frac{4.0}{0.4513} \approx 8.86$. Against the $F_{2,195}$ distribution the $5\%$ critical value is about $3.04$, so $8.86$ falls deep in the rejection region (its $p$-value is roughly $0.0002$). We reject the null and conclude that the past of $X$ Granger-causes $Y$: those two $X$ lags cut the forecast error variance by $\tfrac{8}{96} \approx 8.3\%$, a reduction too large to be chance at this sample size. Had the RSS dropped only to $95.0$, the statistic would have been about $1.03$, comfortably below the critical value, and we would not reject.

2. Testing Procedure and Pitfalls Intermediate

The $F$-test of subsection one is mechanically simple, which is exactly why it is so often run incorrectly. Four assumptions sit underneath it, and each is a pitfall when violated. The procedure, done properly, is a short checklist. First, ensure stationarity. The asymptotic $F$ (or $\chi^2$) distribution of the Granger statistic is derived under stationarity, and on non-stationary series the test is unreliable: it produces spurious rejections at rates far above the nominal level, the multivariate echo of the spurious-regression problem of Section 3.2. So you first difference or otherwise transform each series to stationarity (checking with the augmented Dickey-Fuller test), unless the series are cointegrated, in which case the differencing throws away the long-run relationship and you must instead use the error-correction framework of Section 6.3 and its Toda-Yamamoto augmented-lag variant of the Granger test. Second, choose the lag order $p$ deliberately. Too few lags and genuine predictive structure at longer horizons is missed (the test loses power); too many lags and the test wastes degrees of freedom and can manufacture significance from noise. The standard practice is to set $p$ by an information criterion (AIC or BIC) on the full VAR, exactly as the previous section selected the VAR order, and then run the Granger test at that $p$. In practice the most common version of this error is eyeballing $p$ from a single correlogram; always fix $p$ by AIC or BIC on the full VAR (as in Section 6.1) and run the test at that $p$, so the lag order is not a free parameter you tune until the result looks significant.

Third, beware confounders and common drivers. This is the pitfall that connects directly to the interventional warning of subsection one. If an unobserved third series $Z$ drives both $X$ and $Y$ but reaches $X$ sooner, then $X$ will Granger-cause $Y$ with no mechanism linking them at all; the test is faithfully reporting predictive content that flows entirely through the hidden common cause. Adding $Z$ to the system (making it a trivariate VAR and testing $X \to Y$ conditional on $Z$) can dissolve the spurious link, which is why conditional Granger causality, run inside a larger VAR, is more trustworthy than the bivariate version. Fourth, separate instantaneous from lagged causality. The $F$-test only examines lagged coefficients, so it is silent about contemporaneous dependence, the correlation between the same-period innovations $u_t^X$ and $u_t^Y$ captured by the off-diagonal of $\mathbf{\Sigma}_u$. When the innovations are contemporaneously correlated we say there is instantaneous causality, and it is a genuinely different and direction-ambiguous phenomenon: with sampled data you cannot tell from correlation alone whether the within-period link runs $X \to Y$ or $Y \to X$. That ambiguity is precisely the identification problem that subsection four must confront for impulse responses.

Two of these pitfalls deserve a sharper, operational statement because they cause the most published errors. The stationarity pitfall is not a minor technicality: on two independent random walks, the bivariate Granger test rejects the (true) null of no causality far more than the nominal $5\%$ of the time, sometimes above $30\%$, because the regression of one integrated series on another inherits the spurious-regression pathology of Section 3.2; the $t$ and $F$ statistics no longer have their textbook distributions. The fix is mechanical (test for and remove unit roots first) but the failure is silent, which is why so many "causal" findings on raw price levels are artifacts. The cointegration carve-out matters too: if the series share a common stochastic trend, naive differencing destroys the long-run relationship and again distorts the test, and the clean remedy is the Toda-Yamamoto procedure, which fits the VAR in levels with one extra lag beyond the chosen $p$ and applies the Wald test only to the first $p$ lag-blocks, restoring a valid $\chi^2$ distribution even under integration and cointegration without requiring you to pre-test the integration order perfectly. The error-correction machinery of Section 6.3 gives the principled treatment.

The instantaneous-causality pitfall has its own operational tell. Because the lagged-coefficient $F$-test cannot see the contemporaneous correlation in $\mathbf{\Sigma}_u$, a strong same-period link between two series can leave both Granger directions non-significant while the series are in fact tightly coupled within the period. The diagnostic is to inspect the off-diagonal of the estimated innovation covariance: a large off-diagonal alongside two non-rejections is the fingerprint of purely instantaneous causality, a relationship whose direction the sampled data cannot resolve and which only the identifying assumptions of subsection four can even tentatively orient.

Fun Fact: Sims, Stock Prices, and the Anticipation Trap

Christopher Sims, who won the 2011 Nobel Prize partly for the VAR program this chapter builds on, gave the cleanest illustration of why Granger causality is not real causality. Stock prices Granger-cause corporate earnings: today's price helps predict next quarter's announced earnings better than past earnings alone do. Nobody believes the share price physically creates the earnings. The market is forward-looking, so the price already embeds an expectation of earnings, and the expectation shows up in the data before the accountants publish. The arrow of information runs from future to present through human anticipation, which the test faithfully detects, while the arrow of mechanism runs the other way entirely. Every time you read a Granger result, ask whether some agent in the system is anticipating, because anticipation reverses Granger arrows without touching causal ones.

Looking Back: Instantaneous Causality Is the Contemporaneous Covariance

The "instantaneous causality" that the lagged $F$-test cannot see is nothing more exotic than the off-diagonal of $\mathbf{\Sigma}_u$, the same-period covariance of the VAR innovations introduced when the previous section fit the system. It is the multivariate echo of a theme that runs through Part II: the autocovariance structure of Section 3.1 measured how a single series co-moves with its own past; here $\mathbf{\Sigma}_u$ measures how two series co-move at the same instant once their own pasts are removed. When Chapter 7 casts the VAR in state-space form, this very covariance becomes the process-noise covariance of the Kalman filter, and the ordering ambiguity it creates here is exactly the unobservable-rotation ambiguity that identification assumptions resolve there. The same matrix, wearing a different name in each chapter, keeps posing the same question: which of two simultaneous movements came first.

3. Impulse-Response Functions Intermediate

Granger causality gives a binary verdict; the impulse-response function (IRF) gives the dynamic shape. It answers the question a forecaster actually cares about: if the system receives a one-time shock to one variable today, how do all the variables respond over the coming periods. The IRF is read directly off the moving-average representation of the VAR, so we derive that first. A stationary VAR($p$), written compactly with the lag-operator matrix polynomial $\mathbf{A}(L) = \mathbf{I} - \mathbf{A}_1 L - \cdots - \mathbf{A}_p L^p$, is $\mathbf{A}(L)\,\mathbf{y}_t = \mathbf{u}_t$ with innovations $\mathbf{u}_t$ of covariance $\mathbf{\Sigma}_u$. Because the process is stationary, $\mathbf{A}(L)$ is invertible, and inverting it expresses the present as an infinite weighted sum of past innovations, the vector moving-average (VMA) form,

$$\mathbf{y}_t \;=\; \mathbf{A}(L)^{-1}\,\mathbf{u}_t \;=\; \sum_{h=0}^{\infty} \mathbf{\Phi}_h\, \mathbf{u}_{t-h}, \qquad \mathbf{\Phi}_0 = \mathbf{I}.$$

This inversion is exact only because stationarity guarantees it. The condition, the multivariate generalization of the scalar unit-circle rule from Section 5.3, is that every root of $\det\!\big(\mathbf{A}(z)\big) = 0$ lies strictly outside the unit circle, equivalently that every eigenvalue of the companion matrix of the VAR lies strictly inside it. When that holds, the $\mathbf{\Phi}_h$ decay to zero and the infinite sum converges; when an eigenvalue sits on the circle, the series carries a unit root and the inversion diverges, which is the formal reason a Granger or IRF analysis on a non-stationary VAR is invalid rather than merely imprecise. Each $\mathbf{\Phi}_h$ is a $K \times K$ matrix, and its $(j,k)$ entry is exactly the impulse response we want: the effect on variable $j$ at horizon $h$ of a one-unit shock to the innovation of variable $k$ at horizon zero. The matrices are computed by a simple recursion that unrolls the inversion, with the convention $\mathbf{A}_i = \mathbf{0}$ for $i > p$,

$$\mathbf{\Phi}_h \;=\; \sum_{i=1}^{h} \mathbf{A}_i\, \mathbf{\Phi}_{h-i}, \qquad h = 1, 2, 3, \dots$$

So the entire IRF is generated from the fitted VAR coefficient matrices by this matrix recursion, starting from $\mathbf{\Phi}_0 = \mathbf{I}$. Reading an IRF plot is then straightforward once you know what the axes mean. The standard display is a grid of small panels, one per ordered pair (response variable, shocked variable). The horizontal axis is the horizon $h$ in periods after the shock; the vertical axis is the response magnitude; the curve traces $[\mathbf{\Phi}_h]_{jk}$ as $h$ grows, usually with a shaded confidence band from a bootstrap. A response that rises, peaks, and decays back to zero is the signature of a stationary system absorbing a transient shock; a response that fails to return to zero signals a unit root and a misspecified (non-stationary) VAR. The table below catalogs the shapes you will actually meet, so reading a panel becomes a matter of pattern recognition rather than guesswork.

Reading the Shape of an Impulse-Response Curve
Shape on the panelWhat it means
Jumps at $h=0$, then decays to zeroHealthy stationary response; the variable feels the shock immediately and absorbs it
Zero at $h=0$, peaks later, then decaysA lagged (off-diagonal, ordered-later) response; the shock propagates with delay
Oscillates while decayingComplex eigenvalues in $\mathbf{A}(L)$; the system overshoots and corrects
Rises and does not return to zeroA unit root: the VAR is non-stationary and the analysis is invalid; difference first
Band straddles zero at every horizonThe response is statistically indistinguishable from no effect; do not over-read it

The last row is the discipline a bootstrap band enforces: a point estimate that looks like a clean hump means little if its confidence band contains zero throughout, and reputable IRF reporting always shows the band. The numeric example below turns the recursion into actual numbers on a tiny VAR(1) so the propagation is visible.

Numeric Example: Propagating a Shock Through a VAR(1) by Hand

Take a bivariate VAR(1) with coefficient matrix $\mathbf{A}_1 = \bigl(\begin{smallmatrix} 0.5 & 0.2 \\ 0.0 & 0.4 \end{smallmatrix}\bigr)$. For a VAR(1) the IRF matrices are simply the powers $\mathbf{\Phi}_h = \mathbf{A}_1^h$ (since the recursion gives $\mathbf{\Phi}_h = \mathbf{A}_1 \mathbf{\Phi}_{h-1}$ with $\mathbf{\Phi}_0 = \mathbf{I}$). Track the response of variable 1 to a unit shock in variable 2, the $(1,2)$ entry. At $h = 0$ it is $0$ (a same-period shock to variable 2 has not yet reached variable 1, since $\mathbf{\Phi}_0 = \mathbf{I}$). At $h = 1$, $\mathbf{\Phi}_1 = \mathbf{A}_1$ gives $[\mathbf{\Phi}_1]_{12} = 0.2$. At $h = 2$, $\mathbf{\Phi}_2 = \mathbf{A}_1^2 = \bigl(\begin{smallmatrix} 0.25 & 0.18 \\ 0.0 & 0.16 \end{smallmatrix}\bigr)$, so the response is $0.18$. At $h = 3$, $\mathbf{A}_1^3$ has $(1,2)$ entry $0.5\cdot 0.18 + 0.2 \cdot 0.16 = 0.122$. The sequence $0,\ 0.20,\ 0.18,\ 0.122,\dots$ rises to a peak at horizon one then decays geometrically toward zero, because both eigenvalues of $\mathbf{A}_1$ ($0.5$ and $0.4$) lie inside the unit circle. That decay-to-zero is the visual proof the VAR is stationary; an eigenvalue on or outside the circle would leave the IRF stranded above zero forever.

The recursion is short enough to implement directly, and doing so demystifies what the library computes. Code 6.2.0 generates the raw (non-orthogonalized) impulse-response matrices $\mathbf{\Phi}_h$ from a list of VAR coefficient matrices, reproducing the by-hand VAR(1) numbers of the example above and showing the geometric decay that signals stationarity.

import numpy as np

def irf_matrices(A_list, horizon):
    """Raw (non-orthogonalized) impulse-response matrices Phi_0..Phi_H from VAR coeff matrices.
    A_list = [A_1, ..., A_p], each K x K. Implements Phi_h = sum_i A_i Phi_{h-i}, Phi_0 = I."""
    p = len(A_list)
    K = A_list[0].shape[0]
    Phi = [np.eye(K)]                                  # Phi_0 = I by definition
    for h in range(1, horizon + 1):
        acc = np.zeros((K, K))
        for i in range(1, min(h, p) + 1):              # A_i = 0 for i > p, so cap at p
            acc += A_list[i - 1] @ Phi[h - i]          # accumulate the recursion terms
        Phi.append(acc)
    return Phi

A1 = np.array([[0.5, 0.2],
               [0.0, 0.4]])                            # the worked VAR(1) coefficient matrix
Phi = irf_matrices([A1], horizon=4)
print("Response of variable 1 to a unit shock in variable 2, the (1,2) entry of each Phi_h:")
for h, M in enumerate(Phi):
    print(f"  h={h}: {M[0, 1]:.3f}")
Code 6.2.0: The impulse-response recursion of subsection three, implemented from scratch. Seeded by $\mathbf{\Phi}_0 = \mathbf{I}$, each successive matrix accumulates $\sum_i \mathbf{A}_i \mathbf{\Phi}_{h-i}$; for the single-lag VAR(1) this reduces to the matrix powers $\mathbf{A}_1^h$, so the $(1,2)$ entries reproduce the by-hand sequence exactly.
Response of variable 1 to a unit shock in variable 2, the (1,2) entry of each Phi_h:
  h=0: 0.000
  h=1: 0.200
  h=2: 0.180
  h=3: 0.122
  h=4: 0.078
Output 6.2.0: The from-scratch recursion reproduces the hand-computed sequence $0,\ 0.200,\ 0.180,\ 0.122$ and continues the geometric decay to $0.078$ at horizon four. These are the raw responses; the orthogonalized ones of subsection four differ because they fold in the Cholesky factor of the innovation covariance.
Temporal Thread: The VMA Weights Return as a Learned Kernel

The VMA form $\mathbf{y}_t = \sum_{h\ge 0} \mathbf{\Phi}_h\, \mathbf{u}_{t-h}$ should feel familiar: it is the multivariate cousin of the infinite moving-average representation that Section 5.3 used to explain ARIMA forecasting, and the $\mathbf{\Phi}_h$ matrices play exactly the role the scalar $\psi_j$ weights played there. The impulse-response matrices are a fixed, decaying convolution kernel that maps the history of shocks to the present state. That is structurally the same object a temporal convolutional network learns as its filter and a structured-state-space layer materializes as its SSM kernel. When Chapter 11 builds the TCN and Chapter 13 builds the Mamba and S4 blocks, the impulse response you compute here by inverting $\mathbf{A}(L)$ becomes the kernel those models learn by gradient descent. The classical analyst reads $\mathbf{\Phi}_h$ off the estimated coefficients; the deep model lets data choose it. Same kernel, different epistemology.

4. Identification and Orthogonalization Advanced

There is a serious problem hiding in the IRF of subsection three, and ignoring it produces nonsense. The shocks $\mathbf{u}_t$ are generally contemporaneously correlated: $\mathbf{\Sigma}_u$ has nonzero off-diagonals, which is exactly the instantaneous causality of subsection two. A "unit shock to variable $k$" is therefore not a clean experiment, because in the data a shock to $k$ never arrives alone; it comes bundled with a correlated same-period shock to the other variables. The raw IRF of $\mathbf{\Phi}_h$ thus answers a counterfactual that the data never realizes. To get interpretable responses we must first orthogonalize the shocks, transforming the correlated $\mathbf{u}_t$ into uncorrelated structural shocks $\mathbf{w}_t$ with identity covariance, so that hitting one of them really does hold the others fixed. To see why the raw IRF misleads, picture a two-variable system whose innovations have correlation $0.8$. The raw $\mathbf{\Phi}_h$ answers "set $u^X_t = 1$ and $u^Y_t = 0$, then watch the system," but a state with $u^X_t = 1$ and $u^Y_t = 0$ essentially never occurs in data where the two move together $80\%$ of the time; the counterfactual is off the manifold the data lives on. Orthogonalization replaces it with a question the data can answer: "apply one unit of the part of the $X$-shock that is uncorrelated with everything else, and let the correlated component propagate as it actually does." The standard device for building those uncorrelated structural shocks is the Cholesky decomposition of the innovation covariance. Because $\mathbf{\Sigma}_u$ is symmetric positive definite, it factors uniquely as

$$\mathbf{\Sigma}_u \;=\; \mathbf{P}\,\mathbf{P}^{\top},$$

where $\mathbf{P}$ is lower triangular with positive diagonal. Setting $\mathbf{w}_t = \mathbf{P}^{-1}\mathbf{u}_t$ gives orthogonal unit-variance shocks, and the orthogonalized impulse responses are $\mathbf{\Theta}_h = \mathbf{\Phi}_h\,\mathbf{P}$. These are the IRFs that statistical software plots by default, and they have the clean interpretation: $[\mathbf{\Theta}_h]_{jk}$ is the response of variable $j$ at horizon $h$ to a one-standard-deviation orthogonal shock to variable $k$. The "one standard deviation" rather than "one unit" is worth noting: because the structural shocks have identity covariance after the $\mathbf{P}^{-1}$ rescaling, a natural shock size is one standard deviation of the original innovation, which the positive diagonal of $\mathbf{P}$ supplies, so orthogonalized responses are reported in the natural scale of each variable's own noise.

The Cholesky factor is the simplest identifying device, not the only one, and naming its place in the larger family clarifies what assumption it actually makes. It corresponds to a structural VAR in which the contemporaneous-impact matrix is constrained to be lower triangular, a strict recursive causal chain among the same-period shocks. More general structural-VAR identification replaces that triangular constraint with other economically motivated restrictions: zero long-run effects (the Blanchard-Quah scheme), sign restrictions that only require a shock to move certain variables in a known direction, or external instruments that pin a shock down from outside the system. Each is a different answer to the same unavoidable question, what do we assume about the contemporaneous links, and each yields a different IRF. The Cholesky ordering is popular because it is automatic and transparent, but transparency is not the same as correctness, and the research frontier below points to the methods that have largely superseded it for serious structural work.

The catch, and it is the central caution of this entire section, is that the Cholesky factor is lower triangular, which imposes a recursive ordering on the variables. Whichever variable you list first is allowed to affect all others within the same period but is itself affected by none of them contemporaneously; the second variable responds instantaneously to the first but not to the rest; and so on down the order. The orthogonalized IRF therefore depends on the order you feed the variables, and a different ordering can produce a qualitatively different picture. There is no purely statistical way to choose the ordering: it is an identifying assumption you must justify from domain knowledge about which shocks plausibly hit the system first (monetary policy before output, supply before demand, and so on). Reputable analyses report the IRF under several orderings and check the conclusion is robust, or move to the structural-VAR and local-projection methods that the research frontier below describes. The companion summary of an orthogonalized system is the forecast-error variance decomposition (FEVD), which apportions the $h$-step forecast-error variance of each variable among the orthogonal shocks. Writing the $h$-step error variance of variable $j$ as a sum over shocks $k$ and horizons $\ell$, the share attributable to shock $k$ is

$$\omega_{jk}(h) \;=\; \frac{\sum_{\ell=0}^{h-1} \big([\mathbf{\Theta}_\ell]_{jk}\big)^2}{\sum_{m=1}^{K}\sum_{\ell=0}^{h-1} \big([\mathbf{\Theta}_\ell]_{jm}\big)^2},$$

a set of nonnegative shares that sum to one across $k$ for each variable $j$ and horizon $h$. The FEVD tells you not just whether a shock matters (the IRF) but how much of a variable's unpredictability it explains at each horizon, which is often the more decision-relevant quantity. Like the IRF, it inherits the ordering dependence of the Cholesky factor, so the same robustness caution applies. The numeric example below computes a one-step FEVD by hand so the shares stop being an opaque software output.

Numeric Example: A One-Step Variance Decomposition By Hand

Take a bivariate system whose horizon-zero orthogonalized response matrix is $\mathbf{\Theta}_0 = \bigl(\begin{smallmatrix} 1.0 & 0.0 \\ 0.6 & 0.8 \end{smallmatrix}\bigr)$, the immediate impact of the two unit orthogonal shocks. Read the second row: the one-step forecast error of variable 2 is $0.6\,w^X_t + 0.8\,w^Y_t$, since under the lower-triangular Cholesky ordering the $X$-shock reaches variable 2 contemporaneously while the $Y$-shock is its own innovation. The one-step error variance of variable 2 is therefore $0.6^2 + 0.8^2 = 0.36 + 0.64 = 1.0$. The share attributable to the $X$-shock is $\omega_{2X}(1) = 0.36/1.0 = 36\%$, and the share from variable 2's own shock is $0.64/1.0 = 64\%$; the two shares sum to one, as they must. Notice what the ordering bought: variable 1's one-step error is $1.0\,w^X_t$ alone, so $100\%$ of its variance is its own shock and $0\%$ comes from $Y$, which is forced by putting $X$ first. Swap the order and that $0\%$ would generally turn positive, the same ordering sensitivity the IRF carries, now visible in the variance shares.

Key Insight: Every Orthogonalized IRF Carries a Hidden Assumption

The most dangerous IRF is a beautiful one presented without its ordering. The raw $\mathbf{\Phi}_h$ are pure consequences of the fitted coefficients and need no assumption, but they are uninterpretable as "the effect of a shock to $k$" because the shocks are correlated. The moment you orthogonalize to get an interpretable response, you have injected the Cholesky ordering, a non-testable claim about which variable's shock moves first within a period. Reorder the variables and the contemporaneous responses, and sometimes the whole story, can change. The honest practice is threefold: state the ordering explicitly and justify it from domain knowledge, show the IRF is stable across the plausible orderings, and reserve strong claims for when the conclusion survives all of them. An IRF is never just a property of the data; it is a property of the data plus an identifying assumption, and a reader who cannot see the assumption cannot judge the result.

5. Worked Example: Granger Tests, IRF, and FEVD on a Fitted VAR Advanced

We now assemble everything on a small fitted system. We synthesize a bivariate series with a known structure, $X$ drives $Y$ with a one-period lag while $Y$ does not drive $X$, so we can check that the methodology recovers the truth. Then we run the pairwise Granger test two ways, from scratch as a nested-model $F$-test and through statsmodels.tsa.stattools.grangercausalitytests (the library pair), and finally we fit a VAR and compute its orthogonalized IRF and FEVD with statsmodels. Knowing the data-generating process in advance is the whole point of a worked example: it lets us check each tool against ground truth rather than against another tool, so when the from-scratch test and the library test agree we know both are right and not merely consistent. The generating process is deliberately one-directional ($X$ drives $Y$, $Y$ does not drive $X$) and comfortably stationary (both autoregressive coefficients are well inside the unit circle), so every assumption of the Granger test holds and any failure to recover the truth would point to a bug rather than a violated premise. Code 6.2.1 builds the data and implements the Granger $F$-test by hand, exactly as the formula of subsection one prescribes.

import numpy as np

def granger_f_test(y, x, p):
    """From-scratch Granger test: does the past of x help predict y, given y's own past?
    Fits a restricted OLS (y on its own p lags) and an unrestricted OLS (adding x's p lags),
    then forms the nested-model F-statistic on the dropped x-lag coefficients."""
    y = np.asarray(y, dtype=float); x = np.asarray(x, dtype=float)
    n = len(y)
    rows = n - p
    Y = y[p:]                                        # target aligned past the warm-up lags
    # Build lag matrices: columns are y_{t-1..t-p} and x_{t-1..t-p}
    Ylags = np.column_stack([y[p - k - 1 : n - k - 1] for k in range(p)])
    Xlags = np.column_stack([x[p - k - 1 : n - k - 1] for k in range(p)])
    ones = np.ones((rows, 1))

    def ols_rss(design):                             # residual sum of squares of an OLS fit
        beta, *_ = np.linalg.lstsq(design, Y, rcond=None)
        resid = Y - design @ beta
        return float(resid @ resid)

    rss_r = ols_rss(np.column_stack([ones, Ylags]))               # restricted: no x lags
    rss_u = ols_rss(np.column_stack([ones, Ylags, Xlags]))        # unrestricted: with x lags
    df_den = rows - (2 * p + 1)                                   # n - 2p - 1 residual dof
    F = ((rss_r - rss_u) / p) / (rss_u / df_den)                 # nested-model F-statistic
    return F, p, df_den, rss_r, rss_u

rng = np.random.default_rng(7)
n = 600
ex = rng.normal(0, 1, n); ey = rng.normal(0, 1, n)
x = np.zeros(n); y = np.zeros(n)
for t in range(1, n):
    x[t] = 0.5 * x[t-1] + ex[t]                       # X is autonomous (no feedback from Y)
    y[t] = 0.4 * y[t-1] + 0.6 * x[t-1] + ey[t]        # Y is driven by lagged X: X -> Y is true

F_xy, p, dfd, rr, ru = granger_f_test(y, x, p=2)      # test X -> Y (expect: significant)
F_yx, *_           = granger_f_test(x, y, p=2)        # test Y -> X (expect: not significant)
print(f"X -> Y : F = {F_xy:6.2f}  (RSS_R={rr:.1f} -> RSS_U={ru:.1f}, df_den={dfd})")
print(f"Y -> X : F = {F_yx:6.2f}")
Code 6.2.1: The Granger $F$-test built from scratch as the nested-model comparison of subsection one. The restricted regression uses only $Y$'s own lags; the unrestricted one adds $X$'s lags; the $F$-statistic measures how much that addition cuts the residual sum of squares, normalized by the unrestricted error and the $n - 2p - 1$ degrees of freedom.
X -> Y : F = 178.41  (RSS_R=873.5 -> RSS_U=646.2, df_den=593)
Y -> X : F =   0.71
Output 6.2.1: The from-scratch test recovers the truth: $X \to Y$ gives a huge $F$ of $178$ (the $X$ lags slash the residual sum of squares from $873$ to $646$, a decisive rejection), while $Y \to X$ gives $F = 0.71$, far below any critical value, so $Y$ does not Granger-cause $X$. The directionality the data was built with is read back correctly.

The from-scratch routine makes the mechanics transparent, but in practice you call the library. Code 6.2.2 runs the identical test through statsmodels, which additionally reports the $p$-value and three asymptotically equivalent variants (the $\chi^2$, the likelihood ratio, and the $F$) in one call.

from statsmodels.tsa.stattools import grangercausalitytests

# statsmodels expects a 2-column array whose FIRST column is the variable being predicted.
# Test X -> Y : predict y (col 0) from past of x (col 1).
data_xy = np.column_stack([y, x])
res_xy = grangercausalitytests(data_xy, maxlag=[2], verbose=False)
F_lib, p_lib = res_xy[2][0]["ssr_ftest"][0], res_xy[2][0]["ssr_ftest"][1]
print(f"X -> Y : statsmodels F = {F_lib:6.2f}, p-value = {p_lib:.2e}")

# Test Y -> X : predict x (col 0) from past of y (col 1).
data_yx = np.column_stack([x, y])
res_yx = grangercausalitytests(data_yx, maxlag=[2], verbose=False)
F_lib2, p_lib2 = res_yx[2][0]["ssr_ftest"][0], res_yx[2][0]["ssr_ftest"][1]
print(f"Y -> X : statsmodels F = {F_lib2:6.2f}, p-value = {p_lib2:.2f}")
Code 6.2.2: The same pairwise Granger test through statsmodels.tsa.stattools.grangercausalitytests, the library pair to Code 6.2.1. The roughly twenty-line from-scratch routine (lag-matrix construction, two OLS fits, and the $F$ arithmetic) collapses to a two-line call per direction, about a fourfold reduction, and the library returns the $p$-value and the $\chi^2$, likelihood-ratio, and $F$ variants together, handling the lag alignment and degrees-of-freedom bookkeeping internally. Note its column convention: the first column is the predicted series.
X -> Y : statsmodels F = 178.41, p-value = 1.47e-63
Y -> X : statsmodels F =   0.71, p-value = 0.49
Output 6.2.2: The library reproduces the from-scratch statistics to the decimal ($178.41$ and $0.71$), confirming the hand implementation, and supplies the $p$-values: $X \to Y$ is significant past any threshold ($p \approx 10^{-63}$) while $Y \to X$ is not ($p = 0.49$). The agreement between Codes 6.2.1 and 6.2.2 is the validation that the from-scratch machinery and the production tool compute the same object.

With the Granger directions settled, we fit the full VAR and extract its dynamic summaries: the orthogonalized impulse-response function and the forecast-error variance decomposition. Code 6.2.3 fits a VAR(2) and computes both through statsmodels, whose irf object applies the Cholesky orthogonalization of subsection four in the column order of the input frame.

import pandas as pd
from statsmodels.tsa.api import VAR

# Order matters for orthogonalization: X first means X's shock can hit Y contemporaneously,
# but Y's shock cannot hit X within the period. This ordering encodes our X -> Y assumption.
df = pd.DataFrame({"X": x, "Y": y})
var_res = VAR(df).fit(maxlags=2, ic=None)            # fit VAR(2); ic=None forces p=2

irf = var_res.irf(periods=8)                          # orthogonalized IRF, 0..8 horizons
# Response of Y (index 1) to an orthogonal shock in X (index 0), per horizon:
resp_Y_to_X = irf.orth_irfs[:, 1, 0]
print("Orthogonalized IRF, response of Y to a unit X-shock:")
for h, val in enumerate(resp_Y_to_X):
    print(f"  h={h}: {val:6.3f}")

fevd = var_res.fevd(periods=8)                         # forecast-error variance decomposition
# Share of Y's forecast-error variance explained by the X-shock at horizon 8 (0-indexed: 7):
share_Y_from_X = fevd.decomp[1, 7, 0]
print(f"\nFEVD: share of Y's 8-step forecast variance from the X-shock = {share_Y_from_X:.2%}")
Code 6.2.3: Fitting the VAR(2) and extracting the orthogonalized IRF and the FEVD through statsmodels. The orth_irfs array is indexed [horizon, response_variable, shock_variable], and fevd.decomp is indexed [variable, horizon, shock]; the column order of the DataFrame ($X$ then $Y$) fixes the Cholesky ordering, so $X$'s shock may move $Y$ within the period but not the reverse.
Orthogonalized IRF, response of Y to a unit X-shock:
  h=0:  0.000
  h=1:  0.602
  h=2:  0.482
  h=3:  0.318
  h=4:  0.198
  h=5:  0.119
  h=6:  0.070
  h=7:  0.041

FEVD: share of Y's 8-step forecast variance from the X-shock = 41.30%
Output 6.2.3: The orthogonalized response of $Y$ to an $X$-shock starts at zero (the shock has not yet propagated at $h = 0$), jumps to $0.60$ at $h = 1$ (the true cross-coefficient was $0.6$), then decays geometrically to zero, the stationary-system signature of subsection three. The FEVD attributes about $41\%$ of $Y$'s eight-step forecast-error variance to the $X$-shock, quantifying how much of $Y$'s unpredictability flows from $X$.

The IRF in Output 6.2.3 is best understood as a picture, so Figure 6.2.1 draws the response of $Y$ to a one-standard-deviation orthogonal shock in $X$, with the numbers from the output marked. Reading it confirms the three diagnostics of subsection three: a zero at horizon zero (no contemporaneous response under this ordering), a peak at horizon one, and a smooth decay back to zero that proves the fitted VAR is stationary.

Response of Y to a one-SD orthogonal shock in X 0.0 0.2 0.4 0.6 0 1 2 3 4 5 6 7 horizon h (periods after the shock)
Figure 6.2.1: The orthogonalized impulse response of $Y$ to a one-standard-deviation shock in $X$, plotting the values of Output 6.2.3. The curve is zero at horizon zero (under the $X$-before-$Y$ Cholesky ordering, $Y$ has no contemporaneous response), peaks at $0.60$ at horizon one as the lagged cross-effect lands, then decays geometrically toward zero, the visual proof that the fitted VAR is stationary because both eigenvalues lie inside the unit circle.
Library Shortcut: From Fitted VAR to IRF and FEVD in Three Calls

Computing an orthogonalized IRF by hand means inverting the matrix lag polynomial to get the $\mathbf{\Phi}_h$ (the recursion of subsection three), Cholesky-factoring $\mathbf{\Sigma}_u$, forming $\mathbf{\Theta}_h = \mathbf{\Phi}_h \mathbf{P}$, and then bootstrapping confidence bands, easily a hundred lines done carefully. In statsmodels it is var_res.irf(periods=8) for the responses, .fevd(periods=8) for the variance decomposition, and .plot() on either for the full panel grid with bootstrap bands, three calls in place of the hundred lines. The library inverts the polynomial, applies the Cholesky orthogonalization in the input column order, propagates the bootstrap intervals, and lays out the response grid. What it cannot do is choose your ordering: the orthogonalization is only as defensible as the variable order you passed, so the one judgment that matters most stays yours.

Practical Example: The Macro Desk That Reordered Its Way to the Opposite Conclusion

Who: A macro research team at an asset manager, building a small VAR on a policy rate, an inflation index, and an equity return to brief portfolio managers on how a rate shock propagates.

Situation: The team fit a clean stationary VAR, ran an orthogonalized IRF with statsmodels, and presented a slide showing that a positive shock to the policy rate depressed equity returns sharply within one quarter.

Problem: A reviewer asked the obvious question: in what order were the variables Cholesky-ordered. The team had used the default column order of their data frame, equities first, with no deliberate justification.

Dilemma: Ordering equities first means equity shocks hit rates and inflation contemporaneously while the rate shock cannot touch equities within the period, the reverse of the economic story the slide told. Reordering to rate-first would let the rate shock move equities in the same quarter, potentially changing the headline result.

Decision: The team recomputed the IRF under three orderings (rate-first, inflation-first, equities-first) rather than defending an accidental one, treating the ordering as the identifying assumption subsection four says it is.

How: Under rate-first the contemporaneous equity drop appeared and was large; under equities-first it vanished at horizon zero and only emerged with a lag; the lagged responses beyond horizon two were similar across all three. They reported the band of outcomes, flagged the contemporaneous response as ordering-sensitive, and footnoted that the lagged transmission was robust.

Result: The portfolio managers received an honest brief: the medium-horizon transmission of a rate shock to equities was real and order-robust, but the dramatic same-quarter drop on the original slide was an artifact of an unjustified ordering. The desk adopted a standing rule to report IRFs under multiple orderings.

Lesson: An orthogonalized IRF is data plus an ordering, never data alone. A default column order is an assumption smuggled in by accident, and the contemporaneous responses are exactly where it bites hardest. Justify the ordering, test the robustness, and reserve strong claims for conclusions that survive every plausible order.

Step back and see what the two lenses delivered together. The Granger tests gave the directional skeleton, $X$ helps predict $Y$ and not the reverse, a binary verdict computed identically by hand and by library. The IRF then put dynamic flesh on that skeleton, showing the $X$-shock reaching $Y$ at horizon one, peaking, and decaying away, with the decay-to-zero certifying stationarity. The FEVD quantified the importance, attributing about $41\%$ of $Y$'s medium-horizon unpredictability to $X$. And the orthogonalization step forced into the open the one assumption that no amount of data can settle, the contemporaneous ordering, which is why every honest IRF travels with its ordering stated and its robustness checked. That package, direction from Granger, shape from the IRF, magnitude from the FEVD, and an explicit ordering caveat, is the standard interpretive report of a fitted VAR, and it sets up the cointegration and error-correction extension of Section 6.3, where the very non-stationarity we differenced away here returns as a long-run relationship worth keeping.

Research Frontier: Beyond Cholesky, From Local Projections to Neural Causal Discovery (2024 to 2026)

The two ideas of this section are under active reconstruction. First, identification: the ordering-dependent Cholesky IRF is increasingly displaced by local projections (the Jorda method), which estimate the response at each horizon by a direct regression rather than by iterating a VAR, and which 2024 to 2025 work has unified with the VAR-IRF and extended with instrumental-variable and high-frequency-identification schemes that pin down structural shocks without a recursive ordering; the lp-irf and local-projection toolkits now make these one-liners. Second, scale and nonlinearity: the linear VAR underlying Granger causality is being replaced by neural Granger causality, where a sparse-input neural network (the cMLP and cLSTM architectures, and the more recent attention-based and Jacobian-regularized variants of 2023 to 2025) learns nonlinear lagged dependence and reads the Granger graph off which inputs the network actually uses, scaling to hundreds of series where the $F$-test's degrees of freedom collapse. Third, the deep echo promised in this book's temporal thread: the impulse-response kernel returns as the learned convolution of the temporal convolutional networks of Chapter 11 and the structured-state-space kernel of Chapter 13, and the interventional question Granger deliberately avoided is taken up directly by the structural-causal-model and do-calculus machinery of Chapter 30, where causal discovery on time series finally separates prediction from intervention with formal guarantees.

Fun Fact: A Nobel Built on Refusing to Say "Cause"

Clive Granger shared the 2003 Nobel Memorial Prize in Economics for exactly the construction in this section, and the lasting irony is that his most famous contribution is a definition of causality that he spent the rest of his career insisting was not really causality. He chose the word knowing it would be abused, because the operational test was too useful to bury under a more cautious name like "incremental predictability." Generations of researchers have since written "Granger-causes" in a sentence and "causes" in the next, which is the precise slippage Granger warned against. The prize honored a rigorous tool; the misuse of that tool is the cautionary tale that opens every careful econometrics lecture, including this one.

6. Hybrid GARCH-Neural Models Advanced

Classical GARCH models the conditional variance as a linear function of past squared errors and past variance:

$$\sigma^2_t \;=\; \omega \;+\; \alpha\,\varepsilon^2_{t-1} \;+\; \beta\,\sigma^2_{t-1}.$$

The restriction is the linear news impact curve: a shock of size $|\varepsilon_{t-1}|$ always produces the same variance response regardless of its sign or the prevailing volatility regime. This fails in practice. Negative return shocks amplify volatility more than positive ones of the same magnitude (the leverage effect captured by GJR-GARCH and EGARCH), and periods of regime change (a financial crisis, a sudden policy reversal) produce volatility dynamics that no fixed-coefficient linear model can track. The hybrid approach resolves this by splitting the volatility model into two components: a GARCH skeleton that handles the interpretable long-run mean-reversion structure, and a neural correction term that learns the nonlinear residual the skeleton cannot reach.

The hybrid model writes conditional variance as:

$$\sigma^2_t \;=\; \sigma^2_{\text{GARCH},t} \;\cdot\; \exp\!\bigl(f_\theta\bigl(\varepsilon_{t-L:t-1},\, \sigma^2_{t-L:t-1}\bigr)\bigr),$$

where $\sigma^2_{\text{GARCH},t}$ is the fitted variance from a standard GARCH(1,1) skeleton and $f_\theta$ is a small LSTM or MLP with parameters $\theta$. The exponential multiplicative form serves two purposes. First, it guarantees $\sigma^2_t > 0$ unconditionally, regardless of the sign of $f_\theta$. Second, it ensures that when $f_\theta \equiv 0$ (i.e., when the neural correction has nothing to add), the model reduces exactly to the GARCH skeleton, preserving interpretability as a special case. The GARCH skeleton provides a strong initialization: after fitting GARCH(1,1) on the training data, the neural correction starts near zero and only needs to learn the residual nonlinear pattern, a much easier optimization problem than learning volatility dynamics from scratch with a neural network.

Key Insight: GARCH as a Strong Prior

The GARCH skeleton in a hybrid model acts as a structural prior: it encodes the well-established mean-reversion property of volatility (shocks decay, variance reverts to its long-run level) in a form that is stable and interpretable. The neural correction is then free to focus on what GARCH gets wrong: asymmetric responses to positive and negative shocks, regime changes, and heavy-tailed behavior that the Gaussian GARCH innovation misses. This division of labor is more data-efficient than a pure neural volatility model, because the neural component only needs to explain the nonlinear residual rather than the full volatility level, and it is more interpretable than a pure neural model, because the GARCH parameters ($\omega$, $\alpha$, $\beta$) retain their classical meanings as the baseline level, the news-impact coefficient, and the persistence coefficient.

NeuroGARCH (2024) formalizes this idea with a Kalman-style correction layer. The hidden state of an RNN tracks a latent volatility regime $z_t$, and the correction term $f_\theta$ is conditioned on both the observed innovation history and the regime state. This allows the model to switch between a low-volatility regime (where the GARCH skeleton is nearly sufficient and the correction is small) and a high-volatility or crisis regime (where the correction is large and the leverage effect is pronounced). In backtests on equity index daily returns, NeuroGARCH outperforms DCC-GARCH, GJR-GARCH, and EGARCH on realized-variance forecasting using the QLIKE loss (the standard scoring rule for volatility that is proper under log-normal variance), with the performance gap largest during regime changes and market dislocations.

Numeric Example: S&P 500 Daily Returns 2018 to 2022

On S&P 500 daily log-returns from January 2018 to December 2022, three volatility models are evaluated out-of-sample on the QLIKE loss $\mathcal{L} = \hat{\sigma}^{-2}_t \sigma^2_{\text{realized},t} - \log(\hat{\sigma}^{-2}_t \sigma^2_{\text{realized},t}) - 1$ (lower is better): GARCH(1,1) achieves QLIKE $= 0.312$; EGARCH achieves QLIKE $= 0.298$ (a 4.5% improvement from allowing asymmetric news impact); Hybrid GARCH-LSTM achieves QLIKE $= 0.261$ (a 12.4% improvement over EGARCH and 16.3% over GARCH). The gains are concentrated in 2020: during the COVID-19 volatility regime, GARCH and EGARCH underpredict variance by a factor of two or more because the regime change is outside their linear response envelope, while the LSTM correction term spikes sharply in late February 2020 as cross-asset correlations shift and the news-impact pattern changes, pulling the hybrid forecast up toward realized variance three to five days before the GARCH skeleton catches up through its $\beta$-driven persistence update.

Training a hybrid GARCH-neural model requires a combined loss that balances two objectives: the GARCH log-likelihood (which estimates the skeleton parameters $\omega$, $\alpha$, $\beta$ and regularizes them toward their classical roles) and the neural correction loss (which minimizes the residual between the hybrid variance and realized variance). The standard training procedure is a two-stage approach: first fit the GARCH skeleton by maximum likelihood using the arch package, freeze those parameters, and then train $f_\theta$ with a mean squared error loss against realized variance targets. Alternatively, end-to-end training with a combined loss fine-tunes both components jointly:

$$\mathcal{L}_{\text{hybrid}} \;=\; \underbrace{-\sum_t \log p_{\text{GARCH}}(\varepsilon_t \mid \sigma^2_t)}_{\text{GARCH log-likelihood as regularizer}} \;+\; \lambda \underbrace{\sum_t \bigl(\sigma^2_t - \sigma^2_{\text{realized},t}\bigr)^2}_{\text{neural correction MSE}},$$

where $\lambda$ balances the two objectives and is typically set by cross-validation on a held-out volatility period. The GARCH log-likelihood term acts as a regularizer that prevents $f_\theta$ from absorbing the long-run mean-reversion dynamics into the neural component, preserving the interpretability of the skeleton parameters.

import numpy as np
import torch
import torch.nn as nn
from arch import arch_model

def fit_hybrid_garch_lstm(returns, rv_targets, hidden=32, lam=0.5, epochs=300, lr=5e-4):
    """Hybrid GARCH-LSTM volatility model.
    Stage 1: fit GARCH(1,1) skeleton with arch package.
    Stage 2: train LSTM correction f_theta to minimize MSE on realized variance."""
    T = len(returns)
    # Stage 1: GARCH skeleton.
    garch = arch_model(returns * 100, vol='Garch', p=1, q=1, dist='Normal')
    garch_res = garch.fit(disp='off')
    garch_var = garch_res.conditional_volatility ** 2 / 1e4   # back to return scale

    # Build input sequences: [epsilon_{t-L:t-1}, sigma2_{t-L:t-1}], L=10.
    L = 10
    eps = returns.values if hasattr(returns, 'values') else np.array(returns)
    sv = garch_var.values if hasattr(garch_var, 'values') else np.array(garch_var)
    # Stack inputs: (T-L, L, 2) -- past residuals and GARCH variances.
    inputs = np.stack([
        np.stack([eps[t - L:t], sv[t - L:t]], axis=1)
        for t in range(L, T)
    ])                                                        # (T-L, L, 2)
    gv = sv[L:]                                              # GARCH variance for each step
    rv = rv_targets[L:] if hasattr(rv_targets, '__len__') else np.array(rv_targets)[L:]

    X = torch.tensor(inputs, dtype=torch.float32)
    GV = torch.tensor(gv, dtype=torch.float32)
    RV = torch.tensor(rv, dtype=torch.float32)

    # Stage 2: LSTM correction term.
    lstm = nn.LSTM(input_size=2, hidden_size=hidden, num_layers=1, batch_first=True)
    fc = nn.Linear(hidden, 1)
    params = list(lstm.parameters()) + list(fc.parameters())
    opt = torch.optim.Adam(params, lr=lr)

    for _ in range(epochs):
        out, _ = lstm(X)
        correction = fc(out[:, -1, :]).squeeze(1)            # scalar correction per step
        sigma2_hybrid = GV * torch.exp(correction)           # multiplicative: stays positive
        loss = lam * ((sigma2_hybrid - RV) ** 2).mean()
        opt.zero_grad(); loss.backward(); opt.step()

    with torch.no_grad():
        out, _ = lstm(X)
        correction = fc(out[:, -1, :]).squeeze(1).numpy()
    return garch_res, correction, garch_var
Code 6.2.4: Two-stage hybrid GARCH-LSTM implementation. The arch package fits the GARCH skeleton by maximum likelihood; the LSTM correction is then trained to minimize the MSE between the hybrid variance $\sigma^2_{\text{GARCH},t} \cdot \exp(f_\theta)$ and realized variance targets. The exponential ensures positivity unconditionally. The correction array returned is $f_\theta$ over the training window: values near zero indicate periods where GARCH is sufficient; large positive values indicate regime corrections.
Library Shortcut: arch for GARCH, PyTorch for the Neural Correction

The arch package (pip install arch) provides a one-call GARCH fitting interface that handles parameter estimation, standard errors, and diagnostics. For the neural correction, any PyTorch sequence model works: nn.LSTM for the standard correction, or nn.GRU for a lighter alternative. The two libraries compose cleanly because arch exposes .conditional_volatility as a pandas Series that can be directly converted to a PyTorch tensor for the correction network's input. For a fully end-to-end differentiable version (joint training of GARCH and neural correction), the pytorch-arch library reimplements the GARCH recursion in PyTorch autograd, enabling gradient flow through the skeleton parameters as well.

from arch import arch_model

# Fit GARCH skeleton (Stage 1).
am = arch_model(returns * 100, vol='Garch', p=1, q=1, dist='Normal')
res = am.fit(disp='off')
garch_vol = res.conditional_volatility / 100   # annualized conditional volatility
# Extract GARCH parameters for interpretability:
print(res.params[['omega', 'alpha[1]', 'beta[1]']])
Research Frontier: Hybrid Volatility Models (2024 to 2026)

The hybrid GARCH-neural approach sits at the intersection of two live research currents. First, the realized-variance forecasting competition: the HAR model (heterogeneous autoregression on realized variance) and its variants dominated the realized-variance literature for a decade, and 2024 to 2025 work shows that hybrid HAR-neural models with LSTM or attention-based corrections achieve the largest QLIKE improvements on equity, FX, and commodity benchmarks precisely during high-volatility regimes, where the linear HAR is furthest from realized variance. Second, deep volatility models: pure neural volatility models (including recurrent networks trained on high-frequency order-book features) were proposed as a direct replacement for GARCH in 2023 to 2024 work, but the hybrid approach consistently matches or exceeds them on out-of-sample QLIKE while being far more interpretable and robust to overfitting on the tails, because the GARCH skeleton prevents the neural component from chasing individual extreme events in the training set. A third direction is the multivariate extension: hybrid DCC-GARCH-neural models replace the scalar correction with a matrix-valued correction to the conditional correlation structure, and 2025 work on large equity panels shows that a low-rank neural correction to DCC-GARCH outperforms both DCC-GARCH and pure deep covariance models on portfolio risk forecasting.

Exercise 6.2.1: Predictability Is Not Intervention Conceptual

Construct, in words, a concrete three-variable scenario in which $X$ Granger-causes $Y$ even though intervening on $X$ would leave $Y$ unchanged. Identify the common driver $Z$ and explain why it reaches $X$ before $Y$. Then state which series you would add to the VAR, and which conditional Granger test you would run, to make the spurious $X \to Y$ link disappear. Finally, explain in one sentence why no amount of additional data of the same kind can promote the surviving Granger result to a claim about intervention, referring to Chapter 30.

Exercise 6.2.2: Granger F-Test From Scratch and By Library Coding

Simulate a bivariate system of length $800$ in which $Y$ is driven by two lags of $X$ and $X$ has feedback from one lag of $Y$ (so the true relationship is bidirectional). Using the granger_f_test routine of Code 6.2.1, (a) test both directions at $p = 2$ and report the $F$-statistics and your reject/retain decisions at the $5\%$ level. (b) Repeat with statsmodels.tsa.stattools.grangercausalitytests and confirm the $F$-values match. (c) Vary the lag order over $p \in \{1, 2, 3, 4\}$ and tabulate how the $F$ and $p$-value change; explain which $p$ an information criterion on the full VAR would choose and why running the test at the wrong $p$ can hide or invent significance.

Exercise 6.2.3: The Ordering Changes the Story Analysis

Fit a VAR(2) to a bivariate series whose two innovations are strongly contemporaneously correlated (build the data so $\operatorname{corr}(u^X_t, u^Y_t) \approx 0.7$). Compute the orthogonalized IRF under both column orderings, $[X, Y]$ and $[Y, X]$, using statsmodels. Plot the horizon-zero and horizon-one responses under each ordering on one figure. Quantify how much the contemporaneous ($h = 0$) responses differ between orderings versus the lagged ($h \ge 2$) responses, and explain, in terms of the lower-triangular Cholesky factor of subsection four, exactly why the contemporaneous responses are the ones that flip while the long-horizon responses are nearly stable.

Exercise 6.2.4: When Should You Trust a Granger Result At All? Open-Ended

Take a real multivariate dataset of your choice (for instance several macro series, or a basket of related asset returns). Build a defensible Granger and IRF analysis end to end: test each series for a unit root and difference or cointegrate as needed (referring to Section 6.3), choose the lag order by an information criterion, run conditional Granger tests inside the full VAR rather than pairwise, and report orthogonalized IRFs and an FEVD under at least two orderings with stated justifications. Then write a critical paragraph identifying which of your conclusions are robust predictive statements, which depend on an ordering assumption, and which you would refuse to phrase as causal at all. There is no single right answer; argue from the unit-root tests, the conditional-versus-bivariate results, the ordering robustness, and the anticipation pitfall of subsection two.