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,

\[\{B_{k}\}_{k=0}^{K}, \qquad \Sigma\]

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

rise.engine.var.lptools.estimate

the projections; returns B and Sigma, or beta and beta_se when given a measured shock

rise.engine.var.lptools.identified_set

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 = 0 the left-hand side sits on the right-hand side too, so the fit is exact and B{r}{1} = I. It is set, not estimated.

  • Therefore Sigma comes from the k = 1 residuals, not from k = 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.

est.mode

When

What comes out

'lpvar'

default

B — reduced-form responses. A structural shock exists only after you rotate; see below.

'shock'

you pass shock

beta, beta_se — already structural. No rotation.

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.