Large and high-dimensional systems#
An unrestricted VAR on \(k\) series with \(p\) lags has
\(p k^2\) slope coefficients, and past a handful of variables a
macroeconomic sample cannot pin them down. cultivars.multivariate.large_dim
offers three answers: shrink every coefficient toward a prior (the
Bayesian VARs, documented on Bayesian VARs), select a sparse
support by penalized estimation, or factor the panel into a few common
components and model those. This page covers the selection and factor
models and the Diebold-Yilmaz spillover view that reads any closed
reduced form as a network. It is not the place for small systems
(Vector autoregressions) or for identification (Structural identification).
The family at a glance#
Class |
Module |
Model in one line |
Use when |
|---|---|---|---|
Principal components of the standardized panel, then a VAR on the factor scores (Stock-Watson two-step). |
Forecasting or summarizing a wide panel driven by a few common factors. |
||
A VAR on factors plus observed variables, with an observation equation back to every panel series (Bernanke, Boivin and Eliasz 2005). |
Responses of hundreds of series to a shock identified in a small system. |
||
Static factor model with stochastic volatility on every factor and idiosyncratic error; Gibbs sampler. |
A time-varying covariance or correlation matrix for a panel of returns. |
||
Penalized VAR: lasso, adaptive lasso, SCAD, MCP or lag-group. |
The zero pattern is the finding: a directed Granger network. |
||
Lasso VAR plus a sparse residual precision by nodewise regression (Barigozzi and Brownlees 2019). |
You also want the contemporaneous partial-correlation network. |
||
Not an estimator: the generalized variance decomposition of a fitted closed reduced form as a connectedness table. |
Diebold-Yilmaz total, directional and net spillovers. |
||
|
|
Shrinkage toward a prior, with posterior draws. |
See Bayesian VARs. |
Choosing among them#
Forecast every series, few common drivers:
DFM. It is one SVD and one least-squares VAR, fast at any width.Structural shock, panel-wide responses:
FAVAR, thenFactorAugmentedSVAR.Who Granger-causes whom among many series:
SparseVAR; addGraphicalVARfor the within-period network.Moving covariances of many returns:
FactorSV.Connectedness of any of the above:
Spilloveron a closed reduced form (VARResult,SparseVARResult,GraphicalVARResult, thefactorsof a DFM or FAVAR).A wide VAR with bands on everything and no sparsity claim:
BVAR(Bayesian VARs).
Dynamic factor model#
DFM estimates
on the standardized panel \(x_t = (y_t - \bar y) / s\). The first \(r\) principal components estimate the factor space (Stock and Watson 2002), and a VAR without deterministic terms is fitted to the scores. Left unspecified, \(r\) minimizes the Bai and Ng (2002) criterion
import numpy as np
from cultivars.multivariate.large_dim.dynamic_factor import DFM
rng = np.random.default_rng(1)
n, N = 240, 40
f = np.zeros((n, 2))
for t in range(1, n):
f[t] = np.array([[0.7, 0.1], [0.0, 0.5]]) @ f[t - 1] + rng.standard_normal(2)
loadings = rng.standard_normal((N, 2))
panel = 10.0 + f @ loadings.T + rng.standard_normal((n, N))
dfm = DFM(panel, order=1).fit()
print(dfm.n_factors, dfm.criterion_values.round(2))
print(dfm.variance_shares.round(3), dfm.factors.names)
print(dfm.loadings.shape, dfm.common_component.shape)
print(dfm.forecast(4).shape)
2 [-0.38 -0.84 -0.81 -0.79 -0.74 -0.7 -0.66 -0.62]
[0.386 0.266] ('f1', 'f2')
(40, 2) (240, 40)
(4, 40)
Specification choices#
order(required)Lag order of the factor VAR, at least 0.
n_factors(defaultNone)Fix \(r\), or leave
Noneto minimize \(IC_{p2}\) over1 .. max_factors.max_factors(default 8)Upper end of the search; silently capped at
min(nobs - 1, N).series_names(defaultx1 ... xN)Labels used by
r_squared()and the summary.
What the result gives you#
DFMResult:
factors is a full
VARResult
on the scores, with variables f1 ... fr (irf, fevd,
forecast, identify all run there). loadings are the
(N, r) orthonormal loadings on the standardized panel;
means and scales undo the standardization; idiosyncratic is
the standardized remainder; variance_shares the share of standardized
variance each component explains; criterion_values the
\(IC_{p2}\) path (empty when n_factors was fixed). The property
common_component and the method forecast(steps) are in original
units; r_squared() returns a {series: R^2} dict.
40 0.652
True
Factors are identified only up to rotation, so f1 and f2 carry no
structural names; the common component, the R^2 values and the panel
forecast are rotation invariant. The panel forecast maps the factor
forecast through the loadings with the idiosyncratic part at zero, so it
decays to the series means at the factor VAR’s rate.
Factor-augmented VAR#
FAVAR puts
observed variables \(Y_t\) (a policy rate, say) beside the factors,
Principal components of the full panel absorb the observed block’s
contemporaneous influence. Declaring the slow series (those that do
not react within the period) runs the Bernanke-Boivin-Eliasz cleaning
\(\hat F_t = C_t - \hat b_Y' Y_t\), which a recursive identification
with the observed block ordered last needs.
from cultivars.multivariate.large_dim.factor_augmented import FAVAR
from cultivars.multivariate.structural.factor_augmented import FactorAugmentedSVAR
rng = np.random.default_rng(0)
n = 300
factor = np.zeros(n)
for t in range(1, n):
factor[t] = 0.7 * factor[t - 1] + rng.standard_normal()
shock = rng.standard_normal(n)
rate = 0.5 * factor + shock
lam = rng.uniform(0.5, 1.5, 30)
slow = np.outer(factor, lam[:15]) + 0.5 * rng.standard_normal((n, 15))
fast = np.outer(factor, lam[15:]) + 0.5 * rng.standard_normal((n, 15))
fast += 0.8 * np.outer(shock, rng.uniform(0.5, 1.5, 15))
info = np.column_stack([slow, fast])
labels = tuple(f"s{i}" for i in range(15)) + tuple(f"q{i}" for i in range(15))
favar = FAVAR(info, rate, n_factors=1, order=1, panel_names=labels,
slow=labels[:15], observed_names=("rate",)).fit()
print(favar.cleaned, favar.factors.names, favar.loadings.shape)
print(round(float(favar.r2.mean()), 3))
svar = FactorAugmentedSVAR(favar).identify()
print(svar.shock_names, svar.irf(8).shape)
True ('f1', 'rate') (30, 2)
0.897
('f1', 'rate') (9, 30, 2)
FAVAR(panel, observed, *, n_factors, order, slow=None, trend="c",
observed_names=None, panel_names=None): n_factors is required
(no criterion search here), and slow needs at least
n_factors + 1 names. The result carries factors (a VARResult
on [F, Y]), loadings (N, r + m), scores (T, r),
r2 (an array), explained_variance (before cleaning), cleaned,
and the method common_component(). Identification happens afterwards:
FactorAugmentedSVAR
factorizes factors recursively with the observed block last by
default, or maps any point-identified structural result on
favar.factors, and returns responses of all \(N\) panel series;
see Structural identification.
Factor stochastic volatility#
FactorSV
models a demeaned panel as
with a log-AR(1) variance on each of the \(r\) factors and \(k\)
idiosyncratic errors, so \(\Sigma_t = \Lambda H_t \Lambda' + D_t\)
moves through \(k + r\) paths (Pitt and Shephard 1999; Chib, Nardari
and Shephard 2006). Estimation is a Gibbs sampler with Kim-Shephard-Chib
mixture blocks. \(\Lambda\) is lower triangular with a unit diagonal
in its first \(r\) rows, so the first n_factors columns anchor
the factors’ scale and sign.
from cultivars.multivariate.large_dim.factor_volatility import FactorSV
rng = np.random.default_rng(2)
n = 200
h = np.zeros(n)
for t in range(1, n):
h[t] = 0.95 * h[t - 1] + 0.3 * rng.standard_normal()
common = np.exp(h / 2) * rng.standard_normal(n)
returns = np.outer(common, [1.0, 0.8, 0.6, 0.4]) + 0.5 * rng.standard_normal((n, 4))
fsv = FactorSV(returns, n_factors=1).fit(n_draws=300, n_burn=100, seed=0)
print(fsv.n_kept, fsv.loadings[:, 0].round(2))
print(fsv.communality().round(2))
print(fsv.factor_volatility(0).shape, fsv.conditional_correlation().shape)
200 [1. 0.84 0.63 0.45]
[0.86 0.71 0.62 0.45]
(3, 200) (200, 4, 4)
FactorSV(panel, *, n_factors, series_names=None) requires at least 50
rows and n_factors < k. fit(n_draws=3000, n_burn=1000, thin=1,
prior=None, loading_prior_precision=1.0, seed=None) counts n_draws
including burn-in; prior is a
VolatilityPrior for the log-variance
laws. The result stores loading_draws, factor_draws,
h_factor_draws, h_idio_draws and the AR parameters’ draws;
loadings and factors are posterior-mean properties;
factor_volatility(j) and idiosyncratic_volatility(series) return
(len(quantiles), T) quantiles of the standard deviation;
conditional_covariance() and conditional_correlation() are
(T, k, k) posterior means; communality() is the time-averaged
factor share per series. The sampler does not use ancillarity-sufficiency
interweaving (Kastner, Fruehwirth-Schnatter and Lopes 2017), so loadings
and volatility paths mix slowly; read convergence() before the point
summaries.
Sparse VAR#
SparseVAR solves, on the
standardized panel,
with \(P\) the lasso (Basu and Michailidis 2015), the adaptive lasso
(ridge-pilot weights), SCAD or MCP (local linear approximation, Zou and
Li 2008), or the lag-group penalty
\(\sum_l \sqrt{k^2}\,\lVert A_l \rVert_F\) that keeps or drops whole
lag matrices (Nicholson, Matteson and Bien 2017). Deterministic terms are
never penalized. When lam is not given it is chosen along a
geometric path by rolling-origin one-step forecast cross-validation with a
refit at every origin.
from cultivars.multivariate.large_dim.sparse import SparseVAR
rng = np.random.default_rng(3)
k, n = 8, 300
A = 0.4 * np.eye(k)
A[1, 0], A[2, 0], A[5, 4] = 0.3, 0.3, -0.3
y = np.zeros((n, k))
for t in range(1, n):
y[t] = A @ y[t - 1] + rng.standard_normal(k)
lasso = SparseVAR(y, order=1).fit(penalty="lasso")
print(round(lasso.lam, 4), lasso.n_nonzero, round(lasso.sparsity, 3))
print(lasso.lambda_path.shape, lasso.cv_errors.shape)
edges = lasso.granger_adjacency(threshold=0.1)
print([(i, j) for i in range(k) for j in range(k) if edges[i, j]])
0.0357 35 0.453
(20,) (20,)
[(1, 0), (2, 0), (5, 4)]
granger_adjacency(threshold) returns a (k, k) boolean matrix with
[i, j] true when some lag of variable j enters equation i
above the threshold; the diagonal is always false. At the default
threshold=0.0 every surviving coefficient is an edge, and the
forecast-optimal penalty keeps many small ones.
fit(penalty="lasso", lam=None, n_lambdas=20, lambda_min_ratio=1e-3);
penalty is one of "lasso", "adaptive", "scad", "mcp",
"group". The result has coefficients (p, k, k),
deterministic, sigma_u (residual scatter over nobs minus the
average active regressors per equation), resid, fittedvalues,
penalty, lam (on the standardized scale), lambda_path and
cv_errors (both empty when lam was given), n_nonzero,
sparsity and ma_representation(horizon). It reports no
likelihood, information criteria or standard errors: post-selection
inference is not attempted.
0.0455 [64, 64]
[64, 0]
Each lag block is either entirely zero or entirely free. The cross-validated level (0.0455) keeps the second lag although the simulated system has only one; a slightly larger stated level, 0.05, drops it.
Graphical VAR#
GraphicalVAR adds the
second network. Stage one is the lasso VAR above; stage two estimates the
precision of the residuals by Meinshausen-Buhlmann nodewise lasso
regressions, each with its own penalty chosen by K-fold
cross-validation, symmetrized by averaging. A zero in the precision is a
zero partial correlation: conditional independence of two innovations
given the others.
from cultivars.multivariate.large_dim.graphical import GraphicalVAR
rng = np.random.default_rng(0)
k, n = 5, 400
theta = np.eye(k)
theta[0, 1] = theta[1, 0] = -0.5
theta[2, 3] = theta[3, 2] = 0.5
chol = np.linalg.cholesky(np.linalg.inv(theta))
coef = 0.5 * np.eye(k)
coef[1, 0] = coef[2, 1] = 0.4
z = np.zeros((n, k))
for t in range(1, n):
z[t] = coef @ z[t - 1] + chol @ rng.standard_normal(k)
graph = GraphicalVAR(z, order=1).fit(seed=0)
within = graph.contemporaneous_adjacency(threshold=0.3)
print([(i, j) for i in range(k) for j in range(i + 1, k) if within[i, j]])
across = graph.granger_adjacency(threshold=0.2)
print([(i, j) for i in range(k) for j in range(k) if across[i, j]])
print(graph.partial_correlations.round(2)[0, 1], graph.lam_nodes.shape)
[(0, 1), (2, 3)]
[(1, 0), (2, 1)]
0.47 (5,)
fit(lam=None, n_lambdas=15, lambda_min_ratio=1e-3, n_folds=5,
seed=None); the first stage is always the lasso (there is no
penalty argument), and seed drives the fold shuffle of the
second stage. The result is a SparseVARResult plus precision,
partial_correlations, lam_nodes and
contemporaneous_adjacency(threshold=0.05). Neither network is a
causal graph or a test.
Spillover tables#
Spillover reads the
generalized forecast-error variance decomposition (Koop, Pesaran and
Potter 1996; Pesaran and Shin 1998) of a fitted closed reduced form as a
directed network (Diebold and Yilmaz 2009, 2012):
It needs only names, k_endog, sigma_u and
ma_representation, so it takes a VARResult (shrunk or not), a
SparseVARResult, a GraphicalVARResult, or the factors of a
DFM or FAVAR. Running it on the lasso VAR is the Demirer, Diebold, Liu
and Yilmaz (2018) pipeline:
from cultivars.multivariate.large_dim.spillover import Spillover
table = Spillover(lasso, horizon=10).compute()
print(round(table.total, 2), table.transmitters())
print(table.table.shape, np.allclose(table.table.sum(axis=1), 100.0))
print(table.net.round(1))
print(round(Spillover(dfm.factors, horizon=10).compute().total, 2))
6.26 ('y1', 'y5', 'y8')
(8, 8) True
[ 22.6 -13.6 -10.5 -1.2 9.4 -5.2 -1.5 0.1]
1.13
Spillover(result, *, horizon=10) then compute(). horizon is the
largest moving-average lead, so \(H + 1\) terms are summed and the
table equals 100 * result.generalized_fevd(horizon)[-1] on a
VARResult. SpilloverResult
has table (rows sum to 100; entry [i, j] is the share of i’s
variance due to shocks to j), total (average off-diagonal mass,
in percent), to_others (column sums), from_others (row sums),
net, net_pairwise and transmitters() (names with positive
net, largest first). Because generalized shocks are correlated the
table is a relative attribution, not an additive decomposition. For the
rolling-window picture, refit on each window and call Spillover
again; there is no built-in rolling helper.
How this relates to the Bayesian VARs#
The three answers differ in what they claim and what they return.
Shrink ( |
Select ( |
Factor ( |
|
|---|---|---|---|
Claim |
Coefficients are near a prior mean. |
Most coefficients are exactly zero. |
A few factors drive the comovement. |
Uncertainty |
Posterior draws, 68% bands. |
None reported. |
Two-step: none for the factor step; |
Closed system for |
No (except |
Yes. |
Yes, through |
Model comparison |
Marginal likelihood ( |
Cross-validation errors on the result. |
\(IC_{p2}\) (DFM). |
A BVAR result propagates through its own draws and does not satisfy the
closed-system protocol, so Spillover refuses it. For a shrunk
network, fit the point-estimate path with the same prior:
from cultivars.bayes.priors import NormalInverseWishartPrior
from cultivars.exceptions import SpecificationError
from cultivars.multivariate.large_dim.bayesian import BVAR
from cultivars.multivariate.reduced_form.vector_autoregression import VAR
prior = NormalInverseWishartPrior(persistence=0.0)
posterior = BVAR(y, order=1, prior=prior).fit(n_draws=50, seed=0)
try:
Spillover(posterior)
except SpecificationError as err:
print(str(err)[:60])
shrunk = VAR(y, order=1, prior=prior).fit()
print(np.allclose(shrunk.coefficients, posterior.coefficients))
print(round(Spillover(shrunk, horizon=10).compute().total, 2))
Spillover reads a fitted closed reduced-form result exposing
True
7.92
How the pieces connect#
Factor VARs are VARs.
DFMResult.factorsandFAVARResult.factorsare ordinaryVARResultobjects: forecast,irf,fevd, Granger tests, and every identification model on Structural identification apply.FactorAugmentedSVARmaps the identified factor-space shocks onto all panel series.Closed-system contract.
SparseVARResultandGraphicalVARResultexposenames,k_endog,sigma_uandma_representation, which is whatSpilloverand the structural layer read.Bayesian tools.
FactorSVResult.convergence()is the same diagnostic as on the Bayesian VARs (The Bayesian workflow). The penalized and two-step factor models have no posterior and no marginal likelihood.Forecasting.
DFMResult.forecastgives point paths for the whole panel; the factor VAR’sforecastgives the factor paths. For predictive densities on a wide system useBVAR.forecast_paths(Bayesian VARs, Forecasting and forecast evaluation).
Nuances and gotchas#
SparseVAR and GraphicalVAR have no forecast or irf. Their results offer only
ma_representationamong the propagation methods; build forecasts fromcoefficientsanddeterministic, or impulse responses fromma_representationandsigma_u.FAVAR’s observation equation has no intercept.
loadingscome from regressing the standardized (mean-zero) panel on[F, Y]without a constant, whileYis used as supplied. Shifting the observed block by a constant changesloadings,r2andcommon_component(). Demean the observed variables before passing them if you read the observation equation.Inconsistent accessors across the factor results.
DFMResult.common_componentis a property andr_squared()a method returning a dict;FAVARResult.common_component()is a method andr2an array attribute. The panel labels areseries_namesonDFMandFactorSVandpanel_namesonFAVAR.Factor labels are a basis. In
DFMandFAVARthe factors are principal components, identified up to rotation; inFactorSVthey are pinned by the triangular convention, so reordering the columns of the panel changes the posterior.The Bai-Ng count can overselect when the common component explains nearly all of the panel; fix
n_factorsif the selected count looks generous next tovariance_shares.Selected support is not inference. Edges depend on
lamand onthreshold. The cross-validated level is prediction-optimal and keeps more than the true support; the nonconvex penalties are designed for support recovery, but on any one sample they can keep spurious links too. Inspectlambda_pathandcv_errors.lam is not comparable across penalties. It is on the standardized scale and each penalty’s path starts from its own zeroing level.
Spillover horizon counts from zero.
horizon=10sums \(\Psi_0, \ldots, \Psi_{10}\), eleven terms.Run time.
SparseVARrefits at every rolling origin for every path point. With defaults, the 8-variable example above takes about 0.2 s at one lag and about 1.5 s at two lags;penalty="mcp"roughly doubles that.FactorSVat 300 iterations on four series takes a few seconds; real applications need thousands.
See also#
Bayesian VARs – the shrinkage answer: BVAR and its relatives.
Vector autoregressions – the reduced-form VAR that factor models and spillover tables build on.
Structural identification – identification for closed systems, including
FactorAugmentedSVAR.Volatility models – univariate stochastic volatility, the law each
FactorSVpath follows.Forecasting and forecast evaluation – evaluating panel forecasts.
cultivars.multivariate.large_dim.dynamic_factor,cultivars.multivariate.large_dim.factor_augmented,cultivars.multivariate.large_dim.factor_volatility,cultivars.multivariate.large_dim.sparse,cultivars.multivariate.large_dim.graphical,cultivars.multivariate.large_dim.spillover