This vignette walks through the full estimation pipeline of
acpbvar on a small simulated VAR for which the
data-generating process is known. The objective is twofold: to
illustrate the public interface end to end, and to verify that the
package recovers a known structural impact matrix from a
sign-restriction identification.
The helper simulate_var_example() returns a simulated
sample of three variables (output, inflation, interest rate) generated
by a structural VAR(1) with three structural shocks (supply, demand,
monetary policy). The structural impact matrix is calibrated to deliver
the canonical macroeconomic sign pattern: a positive supply shock raises
output and lowers inflation; a positive demand shock raises both; a
positive monetary-policy shock raises the interest rate while lowering
output and inflation.
sim <- simulate_var_example(T = 200, seed = 42)
Y <- sim$Y
head(Y)
#> output inflation interest_rate
#> [1,] 1.5999687 0.009553379 0.56087474
#> [2,] 1.4689501 -0.665480622 0.54289572
#> [3,] 0.7485236 0.607942746 0.08197592
#> [4,] 1.6380594 -0.570413529 0.07332373
#> [5,] 0.8958783 0.331286665 -0.40549007
#> [6,] 0.6700644 0.152115603 -0.67038980The true structural impact matrix, against which posterior estimates will be compared, is:
The A-form representation requires equation-by-equation response vectors and regressor matrices. The regressors include the lagged variables, an intercept, and (for equations beyond the first) the contemporaneous variables ordered before the current equation, entered with a negative sign so that the impact matrix is lower triangular by construction.
p <- 1L
dat <- build_yX(Y, p = p)
length(dat$y_list)
#> [1] 3
sapply(dat$X_list, dim)
#> [,1] [,2] [,3]
#> [1,] 199 199 199
#> [2,] 4 5 6Univariate AR fits supply the residual variances that enter the asymmetric Minnesota prior as scaling factors.
optim_hyper() selects the own-lag and cross-lag
tightness parameters \((\kappa_1,
\kappa_2)\) by maximising the closed-form log marginal likelihood
under the asymmetric Minnesota prior. Nelder-Mead optimisation runs in
log-space to guarantee positivity.
post_sim() wraps the full pipeline: it prepares the
data, fits the AR variances, runs Empirical Bayes for the tightness,
builds the asymmetric prior, and draws R independent
posterior values of the structural coefficients and variances, equation
by equation (exact Normal-Inverse-Gamma draws, no Markov chain).
The sign-restriction matrix encodes the three identifying patterns.
Rows index shocks (supply, demand, monetary); columns index variables
(output, inflation, interest rate). A value of +1 requires
a positive response on impact, -1 a negative response, and
0 leaves the response unrestricted.
S <- rbind(
supply = c( 1, -1, 0),
demand = c( 1, 1, 0),
monetary = c(-1, -1, 1)
)
colnames(S) <- sim$var_names
S
#> output inflation interest_rate
#> supply 1 -1 0
#> demand 1 1 0
#> monetary -1 -1 1sign_restrict() implements the Rubio-Ramirez, Waggoner
and Zha (2010) rotation algorithm: for each posterior draw of the
reduced form, candidate rotations are drawn from the Haar measure and
screened against all 2^n_id sign flips; the first accepted
rotation is stored.
accepted <- sign_restrict(mcmc, S, n_id = 3L, max_tries = 1000L, verbose = FALSE)
length(accepted)
#> [1] 1000The diagnostic vector attached to the result records, for each
accepted draw, how many Q candidates were screened before
acceptance.
n_tries <- attr(accepted, "n_tries")
summary(n_tries)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 1.00 8.00 19.00 29.55 41.00 192.00Partial identification means that only a subset of the structural
shocks is identified. The matrix S has fewer rows than the
number of variables. The columns of the rotation beyond
n_id are left unrestricted and carry no label; the default
n_id = nrow(S) is all that is needed.
S_partial <- S[c("supply", "demand"), ]
S_partial
#> output inflation interest_rate
#> supply 1 -1 0
#> demand 1 1 0
accepted_partial <- sign_restrict(mcmc, S_partial, max_tries = 1000L, verbose = FALSE)
length(accepted_partial)
#> [1] 1000If a row of zeros is passed, it is rejected: it would declare an identified shock without restrictions, whose sign would then be fixed by the order in which sign flips are tried rather than by an economic hypothesis.
So far the signs have been imposed on impact only (h = 0, the default
horizons = 0). The argument horizons = K
extends them to horizons 0, …, K, in two possible ways. The first is the
same signs at every horizon: the same S is imposed on all
horizons 0, …, K, as in Uhlig (2005). The responses at the various
horizons are computed for each candidate rotation, so more rotations are
screened per draw.
accepted_h3 <- sign_restrict(mcmc, S, horizons = 3L, max_tries = 5000L, verbose = FALSE)
#> Warning: 1 of 1000 posterior draws dropped: no rotation found within max_tries
#> = 5000. If max(attr(., 'n_tries')) is close to max_tries, increase it.
length(accepted_h3)
#> [1] 999
summary(attr(accepted_h3, "n_tries"))
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 1.0 22.0 57.0 109.6 130.0 3291.0The second is different signs at different horizons: S
is an array of size n_id x n_var x (horizons + 1), one
matrix per horizon; a slice of zeros leaves that horizon unrestricted,
and each shock must carry at least one restriction somewhere. In the
example below there are signs for all restricted variables at impact,
then only the interest rate at h = 1 and only output at h = 2.
S_array <- array(0, dim = c(3, 3, 3),
dimnames = list(sim$shock_names, sim$var_names, paste0("h", 0:2)))
S_array[, , "h0"] <- S
S_array["monetary", "interest_rate", "h1"] <- 1
S_array["supply", "output", "h2"] <- 1
S_array
#> , , h0
#>
#> output inflation interest_rate
#> supply 1 -1 0
#> demand 1 1 0
#> monetary -1 -1 1
#>
#> , , h1
#>
#> output inflation interest_rate
#> supply 0 0 0
#> demand 0 0 0
#> monetary 0 0 1
#>
#> , , h2
#>
#> output inflation interest_rate
#> supply 1 0 0
#> demand 0 0 0
#> monetary 0 0 0
accepted_arr <- sign_restrict(mcmc, S_array, horizons = 2L, max_tries = 5000L, verbose = FALSE)
length(accepted_arr)
#> [1] 1000Partial identification works the same way: the matrix or array
S has n_id rows, and the columns beyond
n_id remain free at each horizon.
The default is method = "per_draw", which searches up to
max_tries rotations for each draw from the posterior and
keeps the first one that satisfies the constraints. In this case the
reduced-form posterior is left untouched, and Q is uniform within the
set identified by each posterior draw. The alternative is
method = "joint", which draws a single rotation per sample
and discards the draw if that rotation does not pass. As in the
replication code of Chan (2022), the reduced-form draws are implicitly
reweighted by the size of their identified set. In this case far fewer
draws are accepted, so a larger R is required. Chan (2022,
footnote 13) describes both implementations.
After a run, three numbers are worth checking before looking at any impulse response: the convergence code of the hyperparameter search, the draws that found no valid rotation, and how close the search came to its limit. They are printed in this order below.
convergence is the code returned by optim()
in the search for the hyperparameters \(\kappa_1\) and \(\kappa_2\). If convergence is
0, convergence has been reached; otherwise the \(\kappa\) values stored in spec
are the point at which the optimiser stopped, and
post_sim() has already issued a warning.
failed_draws contains the indices of the draws for which
no rotation satisfied the sign constraints within
max_tries. If the identified set of those draws is empty,
it makes sense to discard them. The problem arises only when the limit
bites before a valid rotation is found, which the next number tells.
n_tries records, for each accepted draw, the attempt at
which the rotation satisfying the signs was found;
max_tries is 10000 by default. Comparing
max(n_tries) with max_tries allows you to
determine whether the rejected draws were rejected by the model or by
the limit. If max(n_tries) is close to
max_tries, it is advisable to raise the limit and run
again.
collect_irfs() stacks the per-draw impulse responses
into a single four-dimensional array; plot_irfs()
summarises the posterior into a median path and a credible band.
The impact responses (horizon zero) carry the sign restrictions by construction: the supply column is positive on output and negative on inflation; the demand column is positive on both; the monetary column is negative on output and inflation, positive on the interest rate.
irfs is a 4-dimensional array: variable × horizon ×
shock × draw. The h-th horizon is located at position h + 1 because the
first element represents the impact at h = 0. A summary at a fixed
horizon is obtained by slicing the second index and taking quantiles
over the last one, the draws: below, the posterior median and the 16-84
band at h = 4.
h <- 4L
resp <- irfs[, h + 1, , ] # variable x shock x draw
med <- apply(resp, c(1, 2), stats::median)
lo <- apply(resp, c(1, 2), stats::quantile, probs = 0.16)
hi <- apply(resp, c(1, 2), stats::quantile, probs = 0.84)
dimnames(med) <- dimnames(lo) <- dimnames(hi) <- list(sim$var_names, sim$shock_names)
round(med, 3) # posterior median at horizon 4
#> supply demand monetary
#> output 0.039 0.116 -0.051
#> inflation -0.006 0.057 -0.021
#> interest_rate -0.043 0.099 0.079
round(lo, 3) # 16th percentile
#> supply demand monetary
#> output -0.001 0.074 -0.111
#> inflation -0.028 0.027 -0.054
#> interest_rate -0.078 0.041 0.019
round(hi, 3) # 84th percentile
#> supply demand monetary
#> output 0.082 0.157 0.007
#> inflation 0.020 0.087 0.012
#> interest_rate -0.008 0.145 0.136By fixing the variable and the shock instead of the horizon, we obtain the entire trajectory of a response.
path <- apply(irfs[1, , 3, ], 1, stats::quantile, probs = c(0.16, 0.5, 0.84))
colnames(path) <- paste0("h", 0:H)
round(path, 3) # output after a monetary shock
#> h0 h1 h2 h3 h4 h5 h6 h7 h8 h9
#> 16% -0.351 -0.251 -0.188 -0.143 -0.111 -0.085 -0.067 -0.052 -0.041 -0.032
#> 50% -0.185 -0.145 -0.108 -0.075 -0.051 -0.034 -0.023 -0.015 -0.009 -0.006
#> 84% -0.050 -0.054 -0.029 -0.008 0.007 0.017 0.022 0.023 0.023 0.022
#> h10 h11 h12
#> 16% -0.025 -0.021 -0.017
#> 50% -0.004 -0.002 -0.001
#> 84% 0.020 0.018 0.016Averaging the impact column across accepted draws gives a posterior estimate of the structural impact matrix, which can be compared against the true matrix used in the simulation.
impact_post <- Reduce("+", lapply(accepted, `[[`, "impact")) / length(accepted)
dimnames(impact_post) <- list(sim$var_names, sim$shock_names)
round(impact_post, 3)
#> supply demand monetary
#> output 0.468 0.177 -0.199
#> inflation -0.514 0.190 -0.204
#> interest_rate -0.038 0.159 0.309round(impact_true, 3)
#> supply demand monetary
#> output 0.5 0.2 -0.2
#> inflation -0.6 0.2 -0.2
#> interest_rate 0.0 0.2 0.4The posterior recovers the sign pattern of the true impact matrix on every entry, and the magnitudes are in the same neighbourhood, with the expected sampling and identification uncertainty. Sign restrictions identify the impact matrix only up to an orthogonal transformation within the admissible set, so exact numerical recovery is not the relevant standard; the recovery of the qualitative pattern, and of the order of magnitude of the entries, is.
Every exported function first checks its arguments and stops,
displaying a message that identifies the incorrect argument and says
what is expected. The chunks below are executed with
error = TRUE, so the messages are shown instead of
interrupting the vignette.
The data must be a numeric matrix, not a data frame:
post_sim(as.data.frame(Y), p = 1L, R = 100L)
#> Error:
#> ! `Y` must be a numeric matrix (T x n), one column per variable.The lag order must be a whole number:
post_sim(Y, p = 1.5, R = 100L)
#> Error:
#> ! `p` must be a single integer with 1 <= p <= 199; got 1.5.S must have one column per variable, and entries in {-1,
0, 1}:
S_bad <- S; S_bad[1, 1] <- 2
sign_restrict(mcmc, S_bad)
#> Error:
#> ! `S` must be a numeric matrix (n_id x n_var) or a 3-d array (n_id x n_var x (horizons + 1)) with entries in {-1, 0, 1}.n must match the number of variables in the accepted
draws: