Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ The sum of the moving-average coefficients of a stable reduced-form VAR, `C(1) =
**Cumulative MA impact matrix (Θ(1))**:
The structural counterpart, `Θ(1) = C(1) P`: the total effect of each structural shock on each variable's level. Where `Cholesky` and `SignRestriction` constrain the impact matrix `Θ(0) = P`, `LongRunRestriction` constrains `Θ(1)` to be lower-triangular in a stated variable ordering, with positive diagonal (shock `j` raises variable `j`'s long-run level).
_Avoid_: "Blanchard-Quah identification" as an API name — the scheme is named for what it restricts, not for its first users (the prose citation is fine). "Permanent shock" for anything but the first column: only the shock restricted nowhere is unambiguously permanent.
A rule for recovering structural shocks from reduced-form covariance, implemented as adapters of the `IdentificationScheme` Protocol (`Cholesky`, `SignRestriction`, `ZeroSignRestriction`, `ProxySVAR`). The scheme is a pure function: it consumes a Cholesky factor `L` and produces a structural shock matrix `B = identify(L)`. It does not own time iteration.
_Avoid_: "identification strategy" (used colloquially; the Protocol is named "scheme").

**Zero-and-sign restrictions (ARW construction)**:
Identification combining exact zeros on the impact matrix with sign restrictions on impulse responses, via the recursive orthogonalisation of Arias, Rubio-Ramírez & Waggoner (2018). With `P = L Q`, a zero is a *linear* condition on one column of `Q` (`Z_j L q_j = 0`), so columns are built one at a time — each drawn uniformly from the unit sphere of the null space of the stacked zero conditions and the already-drawn columns. The zeros hold by construction; only the signs are accept/reject. Admissibility is the Rubio-Ramírez, Waggoner & Zha (2010) counting condition `z_j <= n - j` on shocks sorted by zero count, checked before sampling. Equality throughout is exact identification and reproduces the Cholesky factor; anything looser is set identification. Draws are *unweighted* — no volume-element correction for the ARW uniform-conditional prior — which is documented rather than silent. Failed draws are `NaN`, never a fallback to `L`, because a fallback would violate the zeros the scheme exists to impose.
_Avoid_: "penalty-function zeros" — that is a different (Uhlig-style loss-minimising) construction that only approximates the zeros; these are exact.

**Volatility process**:
The seam that owns how the structural-shock covariance Σ_t is constructed (in PyMC), evolved over time, and queried. Concrete adapters of the `VolatilityProcess` Protocol: `Constant` (homoscedastic Σ; the default) and `StochasticVolatility` (time-varying). The volatility process owns its downstream computation — forecast covariance paths, time-`t` Cholesky query, per-variable volatility paths — so the pipeline never branches on adapter type.
Expand Down
49 changes: 49 additions & 0 deletions docs/explanation/identification.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,52 @@ $C(1)$ is the long-run multiplier on the levels of the variables *as modelled*.
Triangularity is $n(n-1)/2$ restrictions, exactly the number needed for point identification. With two variables that is the one restriction you wanted. With three it asserts three zeros at once, which is a much stronger joint claim than it looks. Arbitrary (non-recursive) long-run zero patterns are not supported.

Two things can go wrong, and Impulso reports them separately. If $I - \sum_j A_j$ is close to singular, $C(1)$ is numerically undefined and those draws are blanked (or, with `on_undefined="raise"`, refused). If a draw is explosive — companion spectral radius above one — the arithmetic is fine but $\sum_h \Phi_h$ diverges, so "long-run effect" has no meaning for it; those draws are always reported and never blanked, because posteriors near a unit root routinely contain them.
## Zero and sign restrictions together

Cholesky and sign restrictions sit at opposite ends of one spectrum. Cholesky imposes $n(n-1)/2$ zeros, which is exactly enough to pin a single answer. Sign restrictions impose no zeros at all and leave a set of answers. Most credible identification schemes sit in between: a few zeros you can defend on institutional or physical grounds, plus signs on the responses you have a firm prior about.

`ZeroSignRestriction` covers that middle ground, following {cite:t}`ariasRubioRamirezWaggoner2018`:

```python
from impulso.identification import ZeroSignRestriction

scheme = ZeroSignRestriction(
shock_names=["supply", "demand", "policy"],
zero_restrictions={"gdp": ["policy"]},
sign_restrictions={
"gdp": {"supply": "+", "demand": "+"},
"inflation": {"supply": "-", "demand": "+", "policy": "-"},
},
)
```

This says output does not respond to a policy shock within the period, on top of the usual signs.

### How the construction works

Write the structural impact matrix as $P = LQ$, where $L$ is the Cholesky factor of the residual covariance and $Q$ is orthogonal. The response of variable $i$ to shock $j$ on impact is $e_i' L q_j$, so a zero restriction is a *linear* condition on the $j$-th column of $Q$:

$$Z_j L q_j = 0$$ (eq-zero-condition)

where $Z_j$ selects the rows carrying zeros on shock $j$. Because {eq}`eq-zero-condition` is linear, it does not have to be searched for. Columns are built one at a time, in decreasing order of how many zeros they carry. At step $k$ the column must satisfy its own zero conditions *and* be orthogonal to the $k-1$ columns already drawn:

$$R_k = \begin{pmatrix} Z_k L \\ q_1' \\ \vdots \\ q_{k-1}' \end{pmatrix}, \qquad q_k \sim \text{Uniform}\!\left(\mathcal{S} \cap \mathcal{N}(R_k)\right)$$ (eq-arw-recursion)

with $\mathcal{N}(R_k)$ the null space of $R_k$ and $\mathcal{S}$ the unit sphere. Drawing a standard Gaussian in an orthonormal basis of that null space and normalising gives the uniform draw. The zeros then hold to the precision of the singular value decomposition (SVD) used to find the basis, and orthogonality holds by construction, so the accept/reject step only ever has to test the *signs*.

### The rank condition

$R_k$ has $z_k + (k-1)$ rows, where $z_k$ is the number of zeros on the $k$-th shock, so its null space is non-trivial only when

$$z_j \le n - j, \qquad j = 1, \dots, n$$ (eq-rwz-rank)

with shocks indexed in decreasing order of $z_j$. This is the counting condition of {cite:t}`rubioRamirezWaggonerZha2010`. It depends on the restriction pattern alone, so Impulso checks it once before any sampling and raises a `ValueError` naming the offending shock. Equality throughout — $z_j = n - j$ for every shock — makes every null space one-dimensional, the answer unique up to column signs, and reproduces the Cholesky factor. That is the exactly-identified end of the spectrum; anything looser leaves a set.

:::{admonition} Draws are unweighted
:class: warning
Accepted draws are kept as the recursion produced them, with no importance weight. {cite:t}`ariasRubioRamirezWaggoner2018` derive such a weight — a volume-element correction for the manifold the zero restrictions carve out — which their uniform-conditional prior over the identified set requires. Without it, a set-identified posterior from `ZeroSignRestriction` is not that prior exactly.

Two cases are unaffected. With no zero restrictions the recursion reduces to Gram-Schmidt on Gaussian columns, which is exactly the Haar measure. With zeros that exactly identify the system, the answer is a point up to column signs, and no weight can move a point. In between, treat the spread across draws as reflecting the restrictions and the parameter posterior, not a calibrated prior over rotations.

Two further differences from the paper are worth stating. Impulso retries rotations *within* each posterior draw, as `SignRestriction` does, rather than rejecting the joint $(\theta, Q)$ pair; and it draws from the orthogonal group $O(n)$ rather than the special orthogonal group $SO(n)$, which is immaterial whenever at least one column's sign is left unpinned.
:::
1 change: 1 addition & 0 deletions docs/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ climate-pitfalls
sign-restrictions
long-run-restrictions
heavy-tailed-errors
zero-sign-restrictions
```
74 changes: 74 additions & 0 deletions docs/how-to/zero-sign-restrictions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Combining Zero and Sign Restrictions

`ZeroSignRestriction` imposes exact zeros on the impact matrix alongside sign restrictions on impulse responses. Use it when part of your identification rests on a timing or exclusion argument you can defend outright, and the rest on the direction a response should take.

## A worked example

Take a three-variable system: a temperature anomaly, a measure of economic activity, and emissions. Economic activity moves emissions immediately, and emissions eventually move temperature, but the thermal inertia of the ocean means an activity shock cannot register in this year's temperature anomaly. That is a zero, not a sign.

```python
from impulso.identification import ZeroSignRestriction

scheme = ZeroSignRestriction(
shock_names=["climate", "activity", "emissions"],
zero_restrictions={
# An activity shock has no contemporaneous effect on temperature:
# the physical lag between emissions and warming is far longer
# than the sampling frequency.
"temperature": ["activity"],
},
sign_restrictions={
"temperature": {"climate": "+"},
"activity": {"climate": "-", "activity": "+"},
"emissions": {"activity": "+", "emissions": "+"},
},
random_seed=42,
)

identified = fitted.set_identification_strategy(scheme)
irf = identified.impulse_response(horizon=20)
```

## Specifying restrictions

- `shock_names` fixes the column order of the returned structural matrix. Name fewer shocks than you have variables and the rest are labelled `unidentified_1`, `unidentified_2`, ... — those columns carry no restrictions and are rotation-arbitrary, so `fevd()` masks their shares.
- `zero_restrictions` maps a variable to the shocks that do not move it on impact. Zeros bind at horizon 0 only; long-run zeros are not supported.
- `sign_restrictions` uses the same format as `SignRestriction`: variable → shock → `"+"` or `"-"`.
- `restriction_horizon=H` imposes the *signs* at horizons `0..H`. The zeros stay at impact.

A cell cannot be restricted to zero and to a sign at once — that is a contradiction at horizon 0, and construction fails with a `ValueError`.

## How many zeros are admissible

Sort the shocks by how many zeros they carry, most first. The shock in position `j` may carry at most `n - j` zeros. Break that and identification is impossible for *any* orthogonal matrix, so `identify()` raises before sampling starts rather than burning through rotations. At the limit — `n - 1`, `n - 2`, ..., `0` — the zeros exactly identify the system and reproduce the Cholesky factor.

## When draws fail

A draw fails when no candidate satisfies the sign restrictions within `n_rotations` attempts.

:::{admonition} Failed draws become NaN, not Cholesky
:class: warning
`ZeroSignRestriction` fills failed draws with `NaN` and warns once with the count and fraction. It deliberately does **not** fall back to the unrotated factor the way `SignRestriction` does: that fallback would silently break the zero restrictions, which are the whole point of the scheme.

`NaN` draws propagate into impulse-response and variance-decomposition summaries and are rejected by the scenario methods, so treat a non-trivial failure fraction as a result to act on, not a warning to suppress. Raise `n_rotations`, relax the signs, or set `on_failure="raise"` to stop at the first failure.
:::

## Tuning

- Each unpinned column sign is effectively a coin flip, so a scheme with `k` sign-restricted shocks accepts roughly one candidate in `2**k` even when the restrictions are otherwise easy. Budget `n_rotations` accordingly.
- Read the acceptance rate off the shock matrix:

```python
attrs = identified.shock_matrix().attrs
attrs["zero_sign_acceptance_rate"] # fraction of draws identified
attrs["zero_sign_mean_attempts"] # candidates drawn per draw
attrs["zero_sign_max_zero_violation"] # largest |zero cell|, expect ~1e-14
```

- Set `random_seed` for reproducibility.
- Naming a shock without giving it any sign restriction leaves its column sign unidentified, so posterior summaries average over both directions. Impulso warns when this happens.

:::{admonition} Draws are unweighted
:class: note
No importance weight corrects for the volume element of the zero-restricted manifold, so set-identified results are not the uniform-conditional prior of Arias, Rubio-Ramírez and Waggoner (2018). See [the identification explanation](../explanation/identification.md) for what this does and does not affect.
:::
1 change: 1 addition & 0 deletions docs/reference/identification.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
SignRestriction
LongRunRestriction
ProxySVAR
ZeroSignRestriction
```
20 changes: 20 additions & 0 deletions docs/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,23 @@ @article{blanchardQuah1989
number = {4},
pages = {655--673},
}

@article{ariasRubioRamirezWaggoner2018,
author = {Arias, Jonas E. and Rubio-Ram{\'i}rez, Juan F. and Waggoner, Daniel F.},
title = {Inference Based on Structural Vector Autoregressions Identified with Sign and Zero Restrictions: Theory and Applications},
journal = {Econometrica},
year = {2018},
volume = {86},
number = {2},
pages = {685--720},
}

@article{rubioRamirezWaggonerZha2010,
author = {Rubio-Ram{\'i}rez, Juan F. and Waggoner, Daniel F. and Zha, Tao},
title = {Structural Vector Autoregressions: Theory of Identification and Algorithms for Inference},
journal = {The Review of Economic Studies},
year = {2010},
volume = {77},
number = {2},
pages = {665--696},
}
4 changes: 3 additions & 1 deletion src/impulso/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from impulso.conjugate_volatility import ConjugateVolatility, PandemicBreak
from impulso.evidence import EvidenceComparison, ModelEvidence, compare_evidence
from impulso.fitted import FittedVAR
from impulso.identification import Cholesky, LongRunRestriction, ProxySVAR, SignRestriction
from impulso.identification import Cholesky, LongRunRestriction, ProxySVAR, SignRestriction, ZeroSignRestriction
from impulso.identified import IdentifiedVAR
from impulso.observation import Gaussian, StudentT
from impulso.priors import MinnesotaPrior, NIWPrior
Expand Down Expand Up @@ -89,6 +89,7 @@
"VariablePath",
"VolatilityProcess",
"VolatilityResult",
"ZeroSignRestriction",
"adf_test",
"compare_evidence",
"compute_ma_phi",
Expand All @@ -108,6 +109,7 @@
"LongRunRestriction": "impulso.identification",
"ProxySVAR": "impulso.identification",
"SignRestriction": "impulso.identification",
"ZeroSignRestriction": "impulso.identification",
"MinnesotaPrior": "impulso.priors",
"NIWPrior": "impulso.priors",
"ConjugateVAR": "impulso.conjugate",
Expand Down
Loading
Loading