cultivars.multivariate.structural.heteroskedacity#
Identification through heteroskedasticity: the data identify, the user labels.
Every other scheme in this package buys identification with an economic restriction someone must defend. This one buys it with a statistical fact. If the structural shocks’ variances shift across declared regimes while the impact matrix stays constant,
with \(\Lambda_m\) diagonal, then the regime covariances jointly pin down \(B\) up to column order and sign, and no zero, no sign pattern, no instrument is needed (Rigobon 2003). Normalizing \(\Lambda_1 = I\) and writing \(\Sigma_1 = L L'\), the whitened second-regime covariance \(L^{-1} \Sigma_2 L^{-\top} = Q \Lambda_2 Q'\) is a symmetric eigenproblem whose eigenvectors are the rotation and whose eigenvalues are the variance ratios, and \(B = L Q\). With two regimes that is the whole estimator. With more, \(B\) is over-identified: no single rotation exactly diagonalizes every regime in sample, so the estimate comes from joint approximate diagonalization warm-started at the two-regime solution, and the residual off-diagonal energy is reported as the data’s verdict on the constant-impact assumption.
Two commitments shape the surface. First, what the data cannot supply is
not invented. The recovered shocks are labelled by their variance behaviour
– unit variance in the first regime, ordered by descending ratio in the
second, named shock1 ... shockk unless the user says otherwise – and
attaching a name like “monetary” to one of them is a claim the user must
argue from outside the model, which the restriction note on every summary
says plainly. Second, the identification condition is checked, not assumed.
Identification needs the variance ratios to be distinct: two shocks whose
variances shift by the same factor are indistinguishable within their span,
so the minimum ratio separation is reported, a separation within sampling
noise is flagged WEAK, and an exact tie is a refusal rather than an
arbitrary rotation.
Layout. HeteroskedasticSVAR is an _IdentificationModel from
_internals; the regime assignment is validated by _validate_regimes
in _core, the first regime’s factor comes from _lower_cholesky in
_core, the multi-regime refinement minimizes the shared
_CoDiagonalObjective from _internals with _solve, and the
rotation is packaged into SVARResult from
zero_restrictions with
scheme="heteroskedasticity". The same idea with the variance path
estimated rather than declared is
stochastic_volatility;
identification from higher moments on the same diagonalization surface is
non_gaussian.
References
Rigobon, R. (2003). Identification through heteroskedasticity. Review of Economics and Statistics, 85(4), 777-792.
Lanne, M., & Lütkepohl, H. (2008). Identifying monetary policy shocks via changes in volatility. Journal of Money, Credit and Banking, 40(6), 1131-1149.
Lewis, D. J. (2021). Identifying shocks via time-varying volatility. Review of Economic Studies, 88(6), 3086-3124.
Example
One shock triples its standard deviation halfway through the sample, the other does not. Declaring the split recovers both impact columns from the variance shift alone, and the diagnostics carry the ratios that did the identifying:
>>> import numpy as np
>>> from cultivars.multivariate.reduced_form.vector_autoregression import VAR
>>> rng = np.random.default_rng(0)
>>> B = np.array([[1.0, 0.5], [0.3, 1.0]])
>>> eps = rng.standard_normal((402, 2))
>>> eps[201:, 0] *= 3.0
>>> y = np.zeros((402, 2))
>>> for t in range(1, 402):
... y[t] = 0.4 * y[t - 1] + B @ eps[t]
>>> res = VAR(y, order=1).fit()
>>> labels = ["calm"] * 200 + ["volatile"] * 201
>>> svar = HeteroskedasticSVAR(res, labels, shock_names=("shifted", "steady")).identify()
>>> svar.shock_names, dict(svar.diagnostics)["Identification"]
(('shifted', 'steady'), 'distinct ratios')
>>> line = dict(svar.diagnostics)["Variances in 'volatile' vs 'calm'"]
>>> ratios = [float(v) for v in line.split(", ")]
>>> bool(8.0 < ratios[0] < 11.0), bool(0.8 < ratios[1] < 1.2)
(True, True)
>>> unit = lambda v: v / np.linalg.norm(v)
>>> bool(np.abs(unit(svar.impact[:, 1]) - unit(B[:, 1])).max() < 0.05)
True
Classes#
Identification from declared variance regimes, Rigobon (2003). |