Using Long-Run Restrictions#

Long-run restrictions identify structural shocks by what they do eventually, not on impact: a shock is defined by having no permanent effect on the level of some variable. This is the Blanchard-Quah scheme.

Define the restriction#

Give the variables in order, most restricted first, and name the shocks:

from impulso.identification import LongRunRestriction

scheme = LongRunRestriction(
    ordering=["output_growth", "unemployment"],
    shock_names=["supply", "demand"],
)

That single zero says the demand shock has no permanent effect on the level of output. Shock j has no long-run effect on any variable ordered before it, so the ordering is the restriction — with two variables there is exactly one.

If naming the zeros directly is clearer than reasoning about an ordering, use the alternative constructor. It recovers both orderings from the pattern — the variables’ and the shocks’, so neither list has to be given in order — and refuses patterns that no ordering can produce:

scheme = LongRunRestriction.from_zero_restrictions(
    restrictions={"output_growth": ["demand"]},
    var_names=["output_growth", "unemployment"],
    shock_names=["supply", "demand"],
)

Your variables must be differenced

The restriction is on the long-run level of the variables as they enter the model. “Demand has no permanent effect on output” therefore requires output to enter as a growth rate — the level then accumulates, and a zero cumulative effect on the growth rate is a zero permanent effect on the level. If you pass output in levels, the restriction says the demand shock has no permanent effect on the growth rate, which is a much weaker and rarely intended claim. Impulso cannot detect the difference.

Apply it#

identified = fitted.set_identification_strategy(scheme)
irf = identified.impulse_response(horizon=40)

The scheme needs the posterior lag coefficients to build the long-run multiplier; set_identification_strategy passes them through automatically.

Rows of the structural shock matrix stay in the data’s variable order, whatever ordering says. Only the shock columns follow shock_names.

Read the diagnostics#

Identification succeeds or fails per posterior draw, so the diagnostics are posterior quantities. Summary statistics ride along on the shock matrix:

P = identified.shock_matrix()
P.attrs["long_run_singular_draws"]      # draws where C(1) is undefined
P.attrs["long_run_explosive_draws"]     # draws with spectral radius > 1
P.attrs["long_run_condition_q95"]       # conditioning of I - sum_j A_j
P.attrs["long_run_spectral_radius_max"]

For the full per-draw picture:

diagnostics = scheme.long_run_diagnostics(fitted.idata.posterior)
diagnostics["condition"]         # shape (chains, draws)
diagnostics["spectral_radius"]   # shape (chains, draws)

A spectral radius above one means that draw’s moving-average sum diverges, so the long run does not exist for it. Impulso warns and reports those draws but keeps them: posteriors on persistent data routinely put mass above one, and dropping them would quietly condition the posterior on stability.

When draws are undefined#

The long-run multiplier is C(1) = (I - sum_j A_j)^-1. When that matrix is close to singular, the inverse is numerically meaningless. Those draws are blanked:

scheme = LongRunRestriction(
    ordering=["output_growth", "unemployment"],
    shock_names=["supply", "demand"],
    on_undefined="nan",     # default; "raise" errors instead
    max_condition=1e8,      # condition number above which C(1) is refused
)

NaN draws propagate: impulse responses and forecast error variance decompositions (FEVD) report NaN for them rather than a misleading zero, and highest-density intervals over a mixed set of draws may come back NaN too. The scenario methods (counterfactual, structural_scenario) reject them outright. If any of that is in your plans, run with on_undefined="raise" so the problem surfaces at identification time instead of three steps later.

A climate example#

The scheme is not specific to macroeconomics. Take global-mean surface temperature and an El Niño-Southern Oscillation (ENSO) index, with temperature differenced:

scheme = LongRunRestriction(
    ordering=["temperature_change", "enso_index"],
    shock_names=["forced", "internal"],
)
identified = fitted.set_identification_strategy(scheme)

The restriction: internal variability has no permanent effect on the level of global-mean temperature, while forced variability may. That is one assumption, stated plainly, and it is doing all the identifying work — so be clear about what it commits you to:

  • Temperature must enter as a change, not a level, or the restriction means something else.

  • With two variables this is exactly one restriction. Adding a third variable would assert three zeros at once, which is a far stronger joint claim.

  • The zero is exact and permanent. If internal variability has a small but genuinely permanent effect, the scheme will attribute it to the forced shock.