cultivars.multivariate.large_dim.dynamic_factor#

The dynamic factor model: a large panel explained by a small VAR.

The premise (Stock and Watson; Forni, Hallin, Lippi and Reichlin) is that a hundred macroeconomic series comove because a handful of common factors drive them,

\[x_t = \Lambda F_t + e_t, \qquad F_t = \sum_{i=1}^{p} A_i F_{t-i} + u_t,\]

each standardized series a loading on \(r \ll N\) factors plus an idiosyncratic remainder, and the factors themselves a VAR. Estimation here is the Stock-Watson two-step, deliberately: the first \(r\) principal components of the standardized panel estimate the factor space consistently as both \(N\) and \(T\) grow, a least-squares VAR on the scores captures the common dynamics, and the whole procedure is one singular-value decomposition and one regression – transparent, fast at any \(N\), and free of the convergence pathologies an EM pass can hide. The factor VAR is a full VARResult, so everything a VAR result can do – forecast, impulse responses, variance decompositions, spillover tables – runs on the factor system directly, and the panel’s own forecast is the factor forecast mapped back through the loadings with the idiosyncratic components at their zero mean.

Two commitments shape the surface. First, the factor count is a modelling choice the model makes only by a stated criterion: left unspecified it minimizes Bai and Ng’s \(IC_{p2}\), the standard consistent selector, and the criterion values are kept on the result so the choice can be audited rather than trusted; when the caller fixes the count the audit trail is empty, because nothing was decided. Second, the two identification facts are stated rather than hidden. Factors are identified only up to rotation – principal components pin the space, not the basis – so individual factors carry no structural names and the summary says so; anything worth interpreting is read from the common component, the loading patterns or the panel forecast, all of which are rotation-invariant. And the two-step ignores estimation error in the factors when fitting their VAR, which is asymptotically negligible for large panels (Bai and Ng, 2006) and is stated on the summary for small ones.

Layout. DFM validates the panel through validate_endog_matrix from _core, standardizes it, selects the count with its own _bai_ng over principal_components from _core, and fits the scores with VAR under trend="n". DFMResult carries the loadings, the standardization and the remainders, and takes the summary from _SummaryMixin. The factors augmented with observed variables are factor_augmented; the factor model whose variances move is factor_volatility; the connectedness of the factor system is spillover.

References

Stock, J. H., & Watson, M. W. (2002). Forecasting using principal components from a large number of predictors. Journal of the American Statistical Association, 97(460), 1167-1179.

Forni, M., Hallin, M., Lippi, M., & Reichlin, L. (2000). The generalized dynamic-factor model: Identification and estimation. Review of Economics and Statistics, 82(4), 540-554.

Bai, J., & Ng, S. (2002). Determining the number of factors in approximate factor models. Econometrica, 70(1), 191-221.

Bai, J., & Ng, S. (2006). Confidence intervals for diffusion index forecasts and inference for factor-augmented regressions. Econometrica, 74(4), 1133-1150.

Example

Forty series driven by two factors: the count is selected, the common component is rotation-invariant although the factor labels are not, and the factor VAR feeds the spillover table directly:

>>> import numpy as np
>>> from cultivars.multivariate.large_dim.spillover import Spillover
>>> rng = np.random.default_rng(0)
>>> f = np.zeros((300, 2))
>>> for t in range(1, 300):
...     f[t] = np.array([[0.6, 0.2], [0.0, 0.5]]) @ f[t - 1] + rng.standard_normal(2)
>>> lam = rng.standard_normal((40, 2))
>>> panel = f @ lam.T + rng.standard_normal((300, 40))
>>> res = DFM(panel, order=1).fit()
>>> res.n_factors, res.factors.is_stable
(2, True)
>>> theta = 0.7
>>> rotation = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
>>> rotated = (res.factors.endog @ rotation) @ (res.loadings @ rotation).T
>>> bool(np.allclose(rotated * res.scales + res.means, res.common_component))
True
>>> table = Spillover(res.factors, horizon=10).compute()
>>> table.names, bool(0.0 < table.total < 50.0)
(('f1', 'f2'), True)

Classes#

DFM

Dynamic factor model, Stock-Watson two-step.

DFMResult

A fitted dynamic factor model: loadings, a factor VAR, and the split.