cultivars.multivariate.structural.perturbation#

Likelihood estimation of perturbation-solved DSGE models.

A DSGE model is a set of equilibrium conditions \(\mathbb{E}_t f(y_{t+1}, y_t, x_{t+1}, x_t; \theta) = 0\) closed by a shock process, and its solution is a pair of policy functions

\[y_t = g(x_t; \theta), \qquad x_{t+1} = h(x_t; \theta) + \eta(\theta)\, \varepsilon_{t+1},\]

which is a state space whose parameters are the structural parameters. Perturbation (Schmitt-Grohé and Uribe 2004) expands \(g\) and \(h\) around the deterministic steady state: the first-order terms solve a generalized eigenproblem under the Blanchard-Kahn condition, and the second-order terms solve a linear system in the policy Hessians whose risk correction \(g_{\sigma\sigma}, h_{\sigma\sigma}\) is the first place the model’s attitude to uncertainty appears. Stopping at first order throws that away and, with it, everything the likelihood knows about risk; Fernández-Villaverde and Rubio-Ramírez (2007) made the case that the second-order solution keeps the leading term and the particle filter evaluates its likelihood without further approximation. The second-order system is pruned (Andreasen, Fernández-Villaverde and Rubio-Ramírez 2018) so that it has finite moments and cannot explode along a sample path, which is what makes its likelihood well defined.

That is the pipeline here. The specification supplies the equations, the steady state, the shock loading, the measurement equation and a log prior, all as functions of \(\theta\); the engine solves the model at any point by numerical differentiation of the equations, emits the solution as a state space on the substrate its order calls for – exact linear-Gaussian at first order, the pruned nonlinear system at second – and estimates by whichever likelihood that substrate offers. Under filter="kalman" the criterion is exact and available at first order only, the Smets-Wouters tradition. Under "extended" or "unscented" it is a Gaussian approximation on the pruned second-order system, deterministic and cheap: a good optimizer target and a poor final answer. Under "particle" it is a bootstrap-filter estimate under common random numbers, maximized by Nelder-Mead, or – through PerturbationDSGE.sample() – the exact posterior by particle marginal Metropolis-Hastings with one filter per draw. A first-order solution is the second-order pruned system with its quadratic blocks zero, so the same specification at order=1 and order=2 can be compared on the same particle likelihood.

Two commitments shape the surface. First, the model is the user’s and only the user’s. The equations are a plain callable differentiated numerically, so any model that can be written in numpy is admissible – in logs for a log-linear approximation, in levels otherwise – and the engine never reorders, renames or rescales what it is given; parameters travel through the optimizer in an unconstrained transform of the declared bounds and come back in the model’s own space. Second, the criterion is named rather than blurred. A result records which filter produced its value, the summary says in one sentence what that value is, and values across filters are not presented as comparable, because they are not: a particle estimate with too few particles is biased downward, severely so when measurement noise is small relative to the state innovations, and the deterministic filters are approximations whose error the module does not pretend to bound.

Layout. PerturbationDSGE is a _PerturbationModel from _internals against the _PerturbationModelSpecification protocol there; the solver is _solve_perturbation in _internals over _first_order and _second_order in _core, the substrate is emitted by _pruned_state_space, the point estimate comes from _maximize_likelihood on a _PerturbationObjective or a _ParticleLikelihoodObjective, and the posterior from _ParticleMarginalChain. PerturbationDSGEResult carries the estimate and the solution and re-emits the latter through _impulse_responses and _simulate_perturbation. The filters themselves live in linear_gaussian and nonlinear.

References

Fernández-Villaverde, J., & Rubio-Ramírez, J. F. (2007). Estimating macroeconomic models: A likelihood approach. Review of Economic Studies, 74(4), 1059-1087.

Schmitt-Grohé, S., & Uribe, M. (2004). Solving dynamic general equilibrium models using a second-order approximation to the policy function. Journal of Economic Dynamics and Control, 28(4), 755-775.

Andreasen, M. M., Fernández-Villaverde, J., & Rubio-Ramírez, J. F. (2018). The pruned state-space system for non-linear DSGE models: Theory and empirical applications. Review of Economic Studies, 85(1), 1-49.

An, S., & Schorfheide, F. (2007). Bayesian analysis of DSGE models. Econometric Reviews, 26(2-4), 113-172.

Example

The same latent AR(1) model solved at both orders. The first-order exact likelihood and the second-order Gaussian approximation agree on a linear model, the first-order fit is the second-order fit, and the second-order solution’s quadratic and risk blocks are zero:

>>> import numpy as np
>>> class Latent:
...     parameter_names, bounds = ("rho", "sigma"), ((0.0, 0.99), (0.01, 1.0))
...     n_states, n_controls = 1, 1
...     def equations(self, th, y_next, y, x_next, x):
...         return np.array([y[0] - x[0], x_next[0] - th[0] * x[0]])
...     def steady_state(self, th):
...         return np.zeros(1), np.zeros(1)
...     def shock_loading(self, th):
...         return np.array([[th[1]]])
...     def observation(self, th):
...         return np.array([[0.0, 1.0]]), np.zeros(1), np.array([[1e-4]])
...     def log_prior(self, th):
...         return 0.0
>>> rng = np.random.default_rng(0)
>>> x = np.zeros(200)
>>> for t in range(1, 200):
...     x[t] = 0.8 * x[t - 1] + 0.5 * rng.standard_normal()
>>> first = PerturbationDSGE(Latent(), x, order=1).fit([0.5, 0.3], filter="kalman")
>>> second = PerturbationDSGE(Latent(), x, order=2).fit([0.5, 0.3])
>>> first.filter, second.filter, bool(abs(first.llf - second.llf) < 1.0)
('kalman', 'unscented', True)
>>> bool(np.allclose(first.theta, second.theta, atol=1e-2))
True
>>> quadratic = second.solution
>>> bool(np.allclose(quadratic.h_xx, 0.0)), bool(np.allclose(quadratic.h_ss, 0.0))
(True, True)

Classes#

PerturbationDSGE

A perturbation-solved DSGE model, estimated by likelihood.

PerturbationDSGEPosterior

PerturbationDSGEResult

A point-estimated perturbation model.

PerturbationDSGESolution

PerturbationModelSpecification