7. Local Projections
7.1. Description
A local projection estimates impulse responses by regressing a lead of a variable on current and lagged variables, one regression per horizon (Jordà, 2005):
y_{i,t+k} = b'_{i,k,0} y_t + sum_l b'_{i,k,l} y_{t-l} + C'_{i,k} w_t + u_{i,k,t}
It is not a VAR. There is no dynamic system, no state space and no
likelihood: it is a stack of ordinary least-squares regressions, and
nothing about it is sampled. It also is not a univariate model — at each
horizon the same design matrix serves every equation, so the horizon-k
coefficients form an n x n block.
That block is what makes the object structurally useful: with y_t on the
right-hand side, the coefficient block on y_t at horizon k is the
reduced-form impulse response matrix at that horizon (Plagborg-Møller and
Wolf, 2021). So a local projection produces exactly the pair that a
rotation-based identification scheme consumes,
and everything structural happens afterwards. This is why RISE ships local projections as two functions rather than as a model class: there is no model object to build.
Function |
Does |
|---|---|
|
the projections; returns |
|
sign-restricted identified sets by random rotations |
7.2. Quick-start
endo = {'FFR','GDP','PCEG','SPARE','TIGHT','IMPP'};
est = rise.engine.var.lptools.estimate(db, endo, ...
horizons = 0:12, ...
lag_length = 1, ...
constant_term = true, ...
deterministic_vars = {'trend'});
db is a struct with one field per series. est.B{1}{k+1} is the
n x n response matrix at horizon k; est.Sigma is the innovation
covariance.
Two properties of the specification are worth knowing before reading any output:
Horizon 0 is the identity by construction. At
k = 0the left-hand side sits on the right-hand side too, so the fit is exact andB{r}{1} = I. It is set, not estimated.Therefore
Sigmacomes from thek = 1residuals, not fromk = 0.
7.3. State-dependent projections
Pass a state and the regressors are interacted with an indicator
(Ramey and Zubairy, 2018):
est = rise.engine.var.lptools.estimate(db, endo, ...
horizons = 0:6, ...
state = struct('variable','ACR', ...
'value','median', ... % or a number
'delay',1));
The indicator is \(I_{t-1} = \mathbb{1}\{z_{t-d} > \bar z\}\) with
delay giving d. est.B{1} is the block where the indicator is
one; est.B{2} where it is zero. value = 'median' uses the sample
median of the state variable.
Important
With B_{r,0} = I in both states and a single pooled Sigma,
a state-dependent local projection can only reveal state-dependence
that lives in the propagation coefficients, at k >= 1. It is
structurally blind to state-dependence that lives only in the impact
covariance. If that is where your mechanism is, use a switching VAR
instead (see Reduced-form VAR Modeling).
7.4. Two modes: measured shock, or rotation
There are two quite different things called a local projection, and
estimate does both. Which one you are running is recorded in
est.mode.
|
When |
What comes out |
|---|---|---|
|
default |
|
|
you pass |
|
lpvar mode is the Plagborg-Møller & Wolf form used above: y_t is a
regressor and the horizon-k block on it is the reduced-form impulse
response matrix, the same object a VAR’s IRF is built from. Its entries
answer “if \(y_j\) moves unexpectedly by one unit today, where is
\(y_i\) in k periods?” — a forecast-revision question. Since the
innovations are correlated, “\(y_j\) moves alone” is not something the
system can do, which is exactly why a structural reading requires the
rotation step.
shock mode is the Jordà (2005) / Ramey–Zubairy (2018) form. You supply a measured shock series — narrative, high-frequency surprise, proxy — and read its coefficient directly:
est = rise.engine.var.lptools.estimate(db, endo, ...
horizons = 0:12, ...
lag_length = 1, ...
shock = 'mp_surprise', ...
shock_lags = 4);
est.beta{1}(i,k+1) % response of variable i at horizon k
est.beta_se{1}(i,k+1) % Newey-West standard error
\(\beta_{i,k}\) is the structural response — no rotation, no identified set. The entire identifying assumption lives in the shock series, imported from outside the regression.
Important
shock mode drops the contemporaneous block, and must. In shock mode
y_t is not a regressor; only lags of y are controls. This is
not an oversight. \(s_t\) causes \(y_t\), so \(y_t\)
mediates the whole effect being measured — conditioning on it closes the
channel and drives \(\beta\) toward zero. It is a bad control, and a
silent one: you would get small, tidy, badly wrong coefficients.
Because there is no contemporaneous block, B is empty in shock mode
and identified_set refuses such an estimate outright.
Note also that beta at k = 0 is a genuine estimate — the impact
response — whereas in lpvar mode B{r}{1} is the identity by
construction and carries no information.
Warning
Use the HAC standard errors. Overlapping horizons make the horizon-k
residual serially correlated of order roughly k, so OLS standard
errors are wrong and get worse as the horizon grows. beta_se is
Newey–West, truncated at k + 1 by default; override with
hac_lags. lpvar mode reports no standard errors at all — its
uncertainty is an identified set, not a band.
7.5. Identification
This section applies to lpvar mode only; a shock-mode estimate is already structural.
Restrictions use the same syntax as the VAR family — they are parsed by
the same routine — so 'GDP{0:2}@MP' means here what it means in
identify:
restr = { 'FFR{0:2}@MP' , '+' ; ...
'SPARE{0:2}@MP', '+' ; ...
'GDP{0:2}@MP' , '-' ; ...
'PCEG{0:2}@MP' , '-' };
SET = rise.engine.var.lptools.identified_set(est, restr, ...
draws = 1e5, ...
elasticity = {'GDP','FFR',10}, ... % |GDP(0)| <= 10 |FFR(0)|
normalize = {'FFR',0.05}); % 5bp impact rise
SET.lo and SET.hi are n x (K+1) x nstate; SET.naccept
records how many draws survived in each state.
elasticity bounds the ratio of two impact responses. normalize
rescales every accepted draw so the named variable moves by the requested
amount on impact — with it, that variable’s set collapses to a point at
k = 0.
Lag-structure restrictions are rejected: a projection has no lag polynomial to restrict.
Warning
A set is not a band. Every draw satisfying the restrictions is
equally admissible, so [lo, hi] is the range of responses
consistent with the restrictions at fixed coefficients. It carries no
parameter uncertainty and is not a credible interval. Reporting one as
the other is the standard set-identification error.
Read the width before the midpoint. If the two states’ sets overlap
almost entirely, a difference in their midpoints is not evidence of
anything. And check whether a bound is pinned by a restriction rather
than by the data: under normalize = {'FFR',0.05} and
elasticity = {'GDP','FFR',10}, a GDP set of exactly
[-0.5, 0] is the restriction talking, since
\(10 \times 0.05 = 0.5\).
7.6. Running it the other way: validating against a model
Everything above runs data → projection → structure. The other direction is how you find out whether the specification can recover a mechanism at all: parameterise a model you trust, simulate from it, project on the artificial data, and check you get the model’s own impulse responses back. A specification that cannot recover a mechanism known to be in the data is worthless on real data.
With a recursive scheme the comparison has a hard target:
est = rise.engine.var.lptools.estimate(db, endo, horizons = 0:4);
Ahat = chol(est.Sigma, 'lower'); % recovers a triangular impact matrix
lp_irf_k = est.B{1}{k+1} * Ahat; % compare against the model's own IRF
Warning
This check is only meaningful when the shock is recoverable from the observables you hand over. If it is not — news shocks, fiscal foresight, fewer observables than shocks — the projection converges to the wrong answer and gives no sign of it.
The textbook case: \(y_t = \varepsilon_t + \theta\varepsilon_{t-1}\)
with \(\theta > 1\) is non-fundamental. Its second moments are
identical to those of the invertible
\(y_t = u_t + \theta^{-1}u_{t-1}\) with
\(\mathrm{var}(u)=\theta^2\), so nothing reading only \(y\)’s
history can separate them. At \(\theta = 2\) and
\(T = 200{,}000\) the projection returns 0.502 where the truth
is 2 — it reports the effect decaying where it actually grows,
and more data never helps.
This is not a weakness of local projections specifically. Because LP and VARs estimate the same object, LP inherits the invertibility problem in full; a VAR on that data is wrong in exactly the same way. A local projection is not a robustness fix for non-fundamentalness.
The way out is information, not a different estimator: supply a shock
series (see shock mode above) and the same data returns [1, 2]
correctly. That is the practical argument for narrative and
high-frequency shock measures.
One further gap worth knowing when comparing against a DSGE: a DSGE’s state-space solution, projected onto a subset of observables, is generically VARMA, not a finite-order VAR. So agreement is only asymptotic in the lag length. At finite lags both LP and VAR are misspecified, but differently — a VAR iterates its estimated coefficients forward, so short-lag error compounds across horizons, while a projection re-estimates each horizon and it does not. That is the substantive argument for LP at long horizons, paid for in variance.
7.7. Why it is cheap
Everything here is OLS and linear algebra. On seven variables, seven horizons and two states, estimation takes a small fraction of a second and 100,000 rotations take a couple of seconds. There is no posterior, so none of the mixing questions that attend a switching VAR arise.
That cheapness is the main practical argument for running a local projection alongside a switching VAR rather than instead of it: the two make different assumptions and are informative about different things, and the projection costs almost nothing to add.