| Title: | Bayesian Structural VARs with Asymmetric Conjugate Priors |
|---|---|
| Description: | Implements Bayesian estimation of structural Vector Autoregressions in the asymmetric A-form of Chan (2022) <doi:10.3982/QE1381>, with an asymmetric Minnesota prior whose own-lag and cross-lag hyperparameters are selected by empirical Bayes via the closed-form marginal likelihood. Exact equation-by-equation posterior simulation in the structural form (independent Normal-Inverse-Gamma draws) provides posterior draws of the reduced-form parameters; structural identification through sign restrictions follows the algorithm of Rubio-Ramirez, Waggoner and Zha (2010) <doi:10.1111/j.1467-937X.2009.00578.x>. Impulse responses, posterior bands, and acceptance diagnostics are computed from the accepted draws. |
| Authors: | Federica Ursillo [aut, cre] |
| Maintainer: | Federica Ursillo <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0 |
| Built: | 2026-11-01 11:47:22 UTC |
| Source: | https://github.com/ursillofederica/acpbvar |
post_sim() returns an object of class "acpbvar_post",
sign_restrict() an object of class "acpbvar_signs" and
collect_irfs() an object of class "acpbvar_irf". All three remain the
plain list or array documented in the respective function (subsetting an
"acpbvar_irf" with [ gives a plain array); the classes only add the
methods below.
## S3 method for class 'acpbvar_post' print(x, ...) ## S3 method for class 'acpbvar_signs' print(x, ...) ## S3 method for class 'acpbvar_irf' print(x, ...) ## S3 method for class 'acpbvar_irf' summary( object, horizons = NULL, probs = c(0.16, 0.5, 0.84), vars = NULL, shocks = NULL, ... ) ## S3 method for class 'summary.acpbvar_irf' print(x, digits = 3, ...) ## S3 method for class 'acpbvar_irf' plot(x, vars = NULL, shocks = NULL, probs = c(0.16, 0.84), ...)## S3 method for class 'acpbvar_post' print(x, ...) ## S3 method for class 'acpbvar_signs' print(x, ...) ## S3 method for class 'acpbvar_irf' print(x, ...) ## S3 method for class 'acpbvar_irf' summary( object, horizons = NULL, probs = c(0.16, 0.5, 0.84), vars = NULL, shocks = NULL, ... ) ## S3 method for class 'summary.acpbvar_irf' print(x, digits = 3, ...) ## S3 method for class 'acpbvar_irf' plot(x, vars = NULL, shocks = NULL, probs = c(0.16, 0.84), ...)
x, object
|
An object of the corresponding class. |
... |
For |
horizons |
Integer vector of horizons to summarise (default all). |
probs |
Quantiles to report (summary, default |
vars, shocks
|
Variables and shocks to summarise or plot: integer
indices or names. Default |
digits |
Number of digits when printing a summary. |
print methods return their argument invisibly. summary returns
an object of class "summary.acpbvar_irf": a list, one element per
selected shock, each a list of probs x horizons matrices, one per
selected variable. plot returns NULL invisibly.
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L, var_names = sim$var_names) mcmc S <- rbind(supply = c(1, -1, 0), demand = c(1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) acc irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) irfs summary(irfs, vars = "output", shocks = "monetary", horizons = c(0, 4, 8)) plot(irfs, shocks = "monetary", mfrow = c(1, 3))sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L, var_names = sim$var_names) mcmc S <- rbind(supply = c(1, -1, 0), demand = c(1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) acc irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) irfs summary(irfs, vars = "output", shocks = "monetary", horizons = c(0, 4, 8)) plot(irfs, shocks = "monetary", mfrow = c(1, 3))
For each variable, fits an AR(ar.lags) with intercept by ordinary least
squares and returns the mean of the squared residuals, as in the
replication code of Chan (2022). The resulting variances enter the
asymmetric Minnesota prior as scaling factors. Series too short for the
fit (fewer than ar.lags + 3 observations) get their sample variance.
build_s2(y_list, ar.lags = 4)build_s2(y_list, ar.lags = 4)
y_list |
A list of |
ar.lags |
Integer; order of the univariate autoregression. Default |
A list with elements:
s2Numeric vector of length n; residual variances.
SDiagonal matrix diag(s2).
sim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) s2 <- build_s2(dat$y_list)$s2 s2 # AR(4) residual variances, one per variable; # they scale the Minnesota priorsim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) s2 <- build_s2(dat$y_list)$s2 s2 # AR(4) residual variances, one per variable; # they scale the Minnesota prior
Constructs the equation-by-equation response vectors and regressor matrices
required by the asymmetric A-form representation of a structural VAR. For
equation i, the regressors include the lagged values of all variables, an
intercept, and (for i > 1) the contemporaneous values of variables 1
through i - 1 entered with a negative sign, so that the impact matrix is
lower triangular by construction.
build_yX(Y, p)build_yX(Y, p)
Y |
A |
p |
The VAR lag order (positive integer). |
A list with the following elements:
y_listA list of length n; y_list[[i]] is the post-lag
response vector for equation i, of length TT = T - p.
X_listA list of length n; X_list[[i]] is the regressor
matrix for equation i, with TT rows and (i - 1) + 1 + n * p
columns (contemporaneous block, intercept, lagged variables).
nNumber of endogenous variables.
pVAR lag order.
TTEffective sample size after lag truncation.
sim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) length(dat$y_list) # one response vector per equation sapply(dat$X_list, ncol) # 4, 5, 6: intercept + n*p lags, plus the # contemporaneous variables ordered before # the equation (entered with a minus sign)sim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) length(dat$y_list) # one response vector per equation sapply(dat$X_list, ncol) # 4, 5, 6: intercept + n*p lags, plus the # contemporaneous variables ordered before # the equation (entered with a minus sign)
Loops compute_irf() over the list of accepted rotations returned by
sign_restrict() and stacks the resulting impulse-response arrays along a
fourth dimension indexing the draw.
collect_irfs(accepted, n, p, H)collect_irfs(accepted, n, p, H)
accepted |
The list of accepted draws returned by |
n, p, H
|
As in |
An object of class "acpbvar_irf": a four-dimensional array of
dimension n x (H + 1) x n x n_accepted (variable x horizon x shock x
draw), with variable and shock names as dimnames, and print,
summary and plot methods. Subsetting with [ returns a plain array.
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) S <- rbind(c(1, -1, 0), c(1, 1, 0), c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) dim(irfs) # variable x horizon x shock x accepted draw # Posterior median and 68% band of variable 1 to shock 3, horizons 0..8 apply(irfs[1, , 3, ], 1, quantile, probs = c(0.16, 0.5, 0.84))sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) S <- rbind(c(1, -1, 0), c(1, 1, 0), c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) dim(irfs) # variable x horizon x shock x accepted draw # Posterior median and 68% band of variable 1 to shock 3, horizons 0..8 apply(irfs[1, , 3, ], 1, quantile, probs = c(0.16, 0.5, 0.84))
Computes the structural impulse responses at horizons 0, ..., H for the
given impact matrix and reduced-form coefficient matrix, through the VMA
recursion on the n x n lag matrices.
compute_irf(impact, B_rf, n, p, H)compute_irf(impact, B_rf, n, p, H)
impact |
The |
B_rf |
Reduced-form coefficient matrix as returned by |
n, p
|
Number of variables and VAR lag order. |
H |
Maximum impulse-response horizon. |
A three-dimensional array of dimension n x (H + 1) x n, indexed
by [variable, horizon, shock].
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 50L) S <- rbind(c(1, -1, 0), c(1, 1, 0), c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irf1 <- compute_irf(acc[[1]]$impact, acc[[1]]$B_rf, n = 3L, p = 1L, H = 8L) dim(irf1) # variable x horizon (0..H) x shock irf1[1, , 3] # response of variable 1 to shock 3 over horizonssim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 50L) S <- rbind(c(1, -1, 0), c(1, 1, 0), c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irf1 <- compute_irf(acc[[1]]$impact, acc[[1]]$B_rf, n = 3L, p = 1L, H = 8L) dim(irf1) # variable x horizon (0..H) x shock irf1[1, , 3] # response of variable 1 to shock 3 over horizons
Selects the own-lag and cross-lag tightness parameters
by maximising the closed-form log marginal
likelihood. Optimisation is performed by Nelder-Mead in log-space, with the
transformation kappa = exp(log_kappa) ensuring positivity. The marginal
likelihood is evaluated under the prior selected by elicit, so that the
tightness is optimised for the prior actually used in estimation.
optim_hyper( y_list, X_list, p, s2, kappa3 = 100, unit_root_mean = FALSE, elicit = c("structural", "reduced"), init = c(0.04, 0.004) )optim_hyper( y_list, X_list, p, s2, kappa3 = 100, unit_root_mean = FALSE, elicit = c("structural", "reduced"), init = c(0.04, 0.004) )
y_list, p, s2
|
As produced by |
X_list |
Per-equation regressor matrices as produced by |
kappa3 |
Intercept variance (positive scalar). Default |
unit_root_mean |
Logical; if |
elicit |
Where the Minnesota prior on the lag coefficients is
elicited: |
init |
Numeric vector of length 2; starting values for
|
A list with kappa1, kappa2, the optimised log_ml, and the
convergence code returned by stats::optim().
sim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) s2 <- build_s2(dat$y_list)$s2 opt <- optim_hyper(dat$y_list, dat$X_list, p = 1L, s2 = s2) opt$kappa1 # own-lag tightness (larger = looser) opt$kappa2 # cross-lag tightness, usually much smaller opt$convergence # 0 means the optimiser convergedsim <- simulate_var_example(T = 120, seed = 1) dat <- build_yX(sim$Y, p = 1L) s2 <- build_s2(dat$y_list)$s2 opt <- optim_hyper(dat$y_list, dat$X_list, p = 1L, s2 = s2) opt$kappa1 # own-lag tightness (larger = looser) opt$kappa2 # cross-lag tightness, usually much smaller opt$convergence # 0 means the optimiser converged
Produces a grid of base-R plots displaying the posterior median impulse
response of each selected variable to each selected shock, together with a
shaded credible band defined by the quantiles in probs. A second set of
responses (ref), for example from another package or the true responses
of a simulated process, can be overlaid for comparison.
plot_irfs( irfs, vars = NULL, shocks = NULL, var_names = NULL, shock_names = NULL, probs = c(0.16, 0.84), ref = NULL, col_med = "black", col_band = "steelblue", col_ref = "black", alpha_band = 0.3, xlab = "", ylab = "", main_cex = 0.9, mfrow = NULL )plot_irfs( irfs, vars = NULL, shocks = NULL, var_names = NULL, shock_names = NULL, probs = c(0.16, 0.84), ref = NULL, col_med = "black", col_band = "steelblue", col_ref = "black", alpha_band = 0.3, xlab = "", ylab = "", main_cex = 0.9, mfrow = NULL )
irfs |
A four-dimensional array of impulse responses as returned by
|
vars, shocks
|
Which variables and shocks to plot: integer indices or
names matching the dimnames of |
var_names |
Optional character vector of variable names of length
|
shock_names |
Optional character vector of shock names of length
|
probs |
Two-element numeric vector of quantiles for the credible
band. Default |
ref |
Optional responses to overlay (dashed median, dotted band at
the same |
col_med |
Colour of the posterior median line. Default |
col_band |
Colour of the credible band. Default |
col_ref |
Colour of the overlaid responses. Default |
alpha_band |
Transparency of the credible band. Default |
xlab, ylab
|
Axis labels. Defaults |
main_cex |
Scaling factor for panel titles. Default |
mfrow |
Panel layout passed to |
Called for its side effect (a grid of base-R plots). Invisibly
returns NULL.
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) S <- rbind(supply = c(1, -1, 0), demand = c(1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) plot_irfs(irfs, probs = c(0.16, 0.84)) # all panels, 68% band plot_irfs(irfs, shocks = "monetary", mfrow = c(1, 3)) # one shock, one row # overlay the true responses of the simulated process irf_true <- compute_irf(sim$impact_true, cbind(sim$intercept_true, sim$B_true), n = 3L, p = 1L, H = 8L) plot_irfs(irfs, ref = array(irf_true, c(dim(irf_true), 1)))sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) S <- rbind(supply = c(1, -1, 0), demand = c(1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) irfs <- collect_irfs(acc, n = 3L, p = 1L, H = 8L) plot_irfs(irfs, probs = c(0.16, 0.84)) # all panels, 68% band plot_irfs(irfs, shocks = "monetary", mfrow = c(1, 3)) # one shock, one row # overlay the true responses of the simulated process irf_true <- compute_irf(sim$impact_true, cbind(sim$intercept_true, sim$B_true), n = 3L, p = 1L, H = 8L) plot_irfs(irfs, ref = array(irf_true, c(dim(irf_true), 1)))
End-to-end wrapper: prepares the equation-by-equation responses and
regressors, fits univariate AR variance estimates, optimises the Minnesota
tightness via Empirical Bayes, constructs the asymmetric prior, and runs
draws R independent posterior samples, equation by equation.
post_sim( Y, p, R, kappa3 = 100, unit_root_mean = FALSE, elicit = c("structural", "reduced"), var_names = NULL, seed = NULL )post_sim( Y, p, R, kappa3 = 100, unit_root_mean = FALSE, elicit = c("structural", "reduced"), var_names = NULL, seed = NULL )
Y |
A |
p |
VAR lag order. |
R |
Number of posterior draws to retain. |
kappa3 |
Intercept prior variance, passed to |
unit_root_mean |
Logical; whether to centre the first own-lag prior
at one. Default |
elicit |
Where the Minnesota prior on the lag coefficients is
elicited: |
var_names |
Optional character vector of variable names of length
|
seed |
Optional integer seed passed to |
An object of class "acpbvar_post" (a list with print method) with samples (the posterior draws of theta and
sigma2), spec (sampler configuration, optimised tightness, log
marginal likelihood, the convergence code returned by stats::optim()
for the hyperparameter search, residual variances, variable names), and dat
(the prepared response/regressor objects).
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) mcmc$spec$kappa # tightness chosen by Empirical Bayes mcmc$spec$convergence # 0 = optimiser converged dim(mcmc$samples$sigma2) # R draws x n equations length(mcmc$samples$theta) # R draws, each a list of n coefficient vectorssim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 200L) mcmc$spec$kappa # tightness chosen by Empirical Bayes mcmc$spec$convergence # 0 = optimiser converged dim(mcmc$samples$sigma2) # R draws x n equations length(mcmc$samples$theta) # R draws, each a list of n coefficient vectors
Given a posterior draw of the structural coefficients and variances,
reconstructs the contemporaneous impact matrix A, its inverse, the
reduced-form coefficient matrix B_rf = A^{-1} B_struct, and the reduced-
form innovation covariance Sigma_rf = A^{-1} diag(sigma2) A^{-1}'.
recover_rf(theta_draw, sigma2_draw)recover_rf(theta_draw, sigma2_draw)
theta_draw |
A list of length |
sigma2_draw |
Numeric vector of length |
A list with A, A_inv, B_struct, B_rf, and Sigma_rf.
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 50L) rf <- recover_rf(mcmc$samples$theta[[1]], mcmc$samples$sigma2[1, ]) rf$A # lower triangular, ones on the diagonal rf$B_rf # reduced-form intercept and lag coefficients rf$Sigma_rf # reduced-form innovation covariancesim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 50L) rf <- recover_rf(mcmc$samples$theta[[1]], mcmc$samples$sigma2[1, ]) rf$A # lower triangular, ones on the diagonal rf$B_rf # reduced-form intercept and lag coefficients rf$Sigma_rf # reduced-form innovation covariance
Implements the Rubio-Ramirez, Waggoner and Zha (2010, Theorem 9)
rotation algorithm for sign-restriction identification. For each retained
posterior draw of the reduced form, candidate rotations Q are drawn from
the Haar measure (via QR decomposition of a standard normal matrix, with
sign normalisation on the diagonal of R); for each candidate, all
2^n_id sign flips on the columns are checked against the restrictions,
and the first accepted rotation is stored.
sign_restrict( mcmc, S, n_id = nrow(S), horizons = 0L, method = c("per_draw", "joint"), max_tries = 10000, verbose = TRUE )sign_restrict( mcmc, S, n_id = nrow(S), horizons = 0L, method = c("per_draw", "joint"), max_tries = 10000, verbose = TRUE )
mcmc |
A posterior object produced by |
S |
Sign restrictions, with entries |
n_id |
Number of identified shocks (defaults to |
horizons |
Non-negative integer. Sign restrictions are imposed on the
impulse responses at horizons |
method |
|
max_tries |
Maximum number of |
verbose |
Logical; print a progress bar and acceptance summary. |
A list of length equal to the number of accepted draws; each entry
contains draw_id, impact, Q, B_rf, Sigma_rf.
The result carries two attributes: "n_tries", the number of Q
candidates needed for each accepted draw, and "failed_draws", the
indices of the posterior draws dropped after max_tries.
sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 100L) # Rows: identified shocks. Columns: variables (output, inflation, rate). # +1 / -1 restrict the sign of the impact response, 0 leaves it free. S <- rbind(supply = c( 1, -1, 0), demand = c( 1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) length(acc) # accepted draws (one rotation each) acc[[1]]$impact # impact matrix of the first draw summary(attr(acc, "n_tries")) # rotations screened per acceptance; # compare the max with max_tries attr(acc, "failed_draws") # draws dropped after max_tries # Partial identification: two shocks, the third column is left free S2 <- S[1:2, ] acc2 <- sign_restrict(mcmc, S2, max_tries = 500L, verbose = FALSE) # Same signs imposed on impact and on the next horizon (Uhlig 2005 uses # several): more demanding, so more rotations are screened per draw acc3 <- sign_restrict(mcmc, S, horizons = 1L, max_tries = 1000L, verbose = FALSE) summary(attr(acc3, "n_tries")) # Different signs at different horizons: one matrix per horizon A <- array(0, c(3, 3, 3)) A[, , 1] <- S # impact: full pattern A[3, 3, 2] <- 1 # h = 1: interest rate still up after a monetary shock A[1, 1, 3] <- 1 # h = 2: output still up after a supply shock accA <- sign_restrict(mcmc, A, horizons = 2L, max_tries = 2000L, verbose = FALSE)sim <- simulate_var_example(T = 120, seed = 1) mcmc <- post_sim(sim$Y, p = 1L, R = 100L) # Rows: identified shocks. Columns: variables (output, inflation, rate). # +1 / -1 restrict the sign of the impact response, 0 leaves it free. S <- rbind(supply = c( 1, -1, 0), demand = c( 1, 1, 0), monetary = c(-1, -1, 1)) acc <- sign_restrict(mcmc, S, max_tries = 500L, verbose = FALSE) length(acc) # accepted draws (one rotation each) acc[[1]]$impact # impact matrix of the first draw summary(attr(acc, "n_tries")) # rotations screened per acceptance; # compare the max with max_tries attr(acc, "failed_draws") # draws dropped after max_tries # Partial identification: two shocks, the third column is left free S2 <- S[1:2, ] acc2 <- sign_restrict(mcmc, S2, max_tries = 500L, verbose = FALSE) # Same signs imposed on impact and on the next horizon (Uhlig 2005 uses # several): more demanding, so more rotations are screened per draw acc3 <- sign_restrict(mcmc, S, horizons = 1L, max_tries = 1000L, verbose = FALSE) summary(attr(acc3, "n_tries")) # Different signs at different horizons: one matrix per horizon A <- array(0, c(3, 3, 3)) A[, , 1] <- S # impact: full pattern A[3, 3, 2] <- 1 # h = 1: interest rate still up after a monetary shock A[1, 1, 3] <- 1 # h = 2: output still up after a supply shock accA <- sign_restrict(mcmc, A, horizons = 2L, max_tries = 2000L, verbose = FALSE)
Generates a three-variable structural VAR(1) calibrated to deliver impulse responses with the canonical macroeconomic sign pattern: a positive supply shock raises output and lowers inflation; a positive demand shock raises both output and inflation; a positive monetary-policy shock lowers output and inflation while raising the interest rate. The function returns the simulated sample together with the true structural impact matrix and the true reduced-form coefficients, so that recovery can be validated against the data-generating process.
simulate_var_example(T = 200, seed = NULL)simulate_var_example(T = 200, seed = NULL)
T |
Integer; number of post-burn-in observations to return.
Default |
seed |
Optional integer seed passed to |
The simulation discards a burn-in of 100 observations to wash out the influence of the zero initial condition.
A list with the following elements:
YT x 3 matrix of simulated observations on output,
inflation, and the interest rate.
impact_trueThe 3 x 3 structural impact matrix used in the
DGP; columns index shocks (supply, demand, monetary).
B_trueThe 3 x 3 reduced-form coefficient matrix on the
first lag.
intercept_trueThe 3-vector of intercepts in the DGP.
var_namesVariable names.
shock_namesShock names corresponding to the columns of
impact_true.
sim <- simulate_var_example(T = 200, seed = 42) head(sim$Y) sim$impact_truesim <- simulate_var_example(T = 200, seed = 42) head(sim$Y) sim$impact_true