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,
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)