README.md

qpmR

R-CMD-check test-coverage Lifecycle: stable

Quarterly Projection Models for Monetary Policy Analysis in R.

qpmR builds, solves, filters, estimates and reports the semi-structural quarterly projection models (QPM) used in central-bank Forecasting and Policy Analysis Systems (FPAS): output gap, Phillips curve, forward-looking policy rule, and exchange-rate block, solved under model-consistent expectations.

The goal is the workflow, not just the equations:

data -> filtering -> gaps -> model -> baseline -> judgment -> scenarios -> report

Every stage of that chain is implemented, exercised end to end on a real quarterly dataset for Czechia that ships with the package, and cross-checked against Dynare. Documentation: https://mustapha-wasseja.github.io/qpmR/.

Installation

install.packages("qpmR")            # from CRAN, once accepted

# development version
# install.packages("pak")
pak::pak("Mustapha-Wasseja/qpmR")

Quickstart

library(qpmR)

m <- qpm_template("bkl")          # canonical small open economy QPM
summary(m)

sol <- qpm_solve(m)               # QZ solution + Blanchard-Kahn check
sol

ir <- irf(sol, shock = "eps_i")   # 100bp-style policy tightening
plot(ir, vars = c("pi", "y_gap", "i", "q"))

histq <- simulate(sol, nsim = 48, seed = 7, burn = 20)
fc <- qpm_forecast(sol, from = histq, horizon = 12)
plot(fc, vars = c("pi", "i", "y_gap", "q"))

# transmission experiment: double exchange-rate pass-through
m2 <- qpm_calibrate(m, b3 = 0.2)
plot(irf(qpm_solve(m2), shock = "eps_q"), vars = c("pi", "i"))

The vignettes walk through the whole chain: vignette("qpmR-quickstart") takes a forecast round from data to report, and vignette("qpmR-estimation") covers priors, posteriors, identification and Bayes factors.

What it does

The model layer

Filtering

mcz <- qpm_calibrate(qpm_template("bkl", trends = "rw"),
                     pi_tar = 2, istar_ss = 2, pistar_ss = 2, prem_ss = 1)
cz <- czechia[czechia$period >= "1999",
              c("period", "pi4", "i", "q", "dy_obs", "istar", "pistar")]
fit <- qpm_filter(mcz, cz)
plot(fit, vars = c("y_gap", "dy_bar", "r_bar", "q_gap"))
plot(qpm_decompose(fit), var = "pi4")

Forecasting and policy analysis

base <- qpm_forecast(qpm_solve(mcz), from = fit, horizon = 12)
hold <- qpm_condition(base, i = c("2026-Q3" = 3.5, "2026-Q4" = 3.5),
                      anticipated = TRUE, instruments = "eps_i")

rA <- qpm_round("2026-Q1 March", mcz, cz_to_2025Q4, horizon = 12)
rB <- add_judgment(qpm_round("2026-Q3 September", mcz, cz, horizon = 12),
                   pi4 = c("2027-Q1" = 0.4), author = "prices desk",
                   rationale = "announced energy-tariff increase")
compare_rounds(rA, rB)

Estimation

est <- qpm_estimate(mcz, cz, priors(
  b1 = beta(0.70, 0.10), b2 = gamma(0.25, 0.10), b3 = gamma(0.10, 0.05),
  c1 = beta(0.70, 0.10), c2 = truncate(normal(1.5, 0.25), lower = 1),
  eps_pi = invgamma(1, 0.5)
), iter = 4000, chains = 2)
est
plot(est)                                   # prior vs posterior
plot(posterior_forecast(est, horizon = 12)) # parameter-uncertainty fans

Country adaptation

m <- add_block(qpm_template("bkl"), block_food_cpi(weight = 0.45))
plot(irf(qpm_solve(m), shock = "eps_pifood"),
     vars = c("pi_food", "pi", "pi_core", "i"))
qpm_diff(qpm_template("bkl"), m)

Reporting, audit and policy experiments

verify_round(r)                       # does the archive still reproduce?
chart_pack(r, "chart_pack.pdf")
qpm_report(r, "mpr.html", compare_to = previous_round)

Does it agree with Dynare?

Yes — and you can check rather than take it on trust. write_dynare() exports any model as a .mod file, and the test suite pins qpmR's impulse responses to golden files produced by Dynare 6.0:

write_dynare(qpm_template("bkl"), "bkl.mod")

Across 4080 impulse-response points (12 shocks x 17 variables x 20 quarters) the largest discrepancy is 1.4e-14, with steady states agreeing to 8e-14. The food-block model agrees to 1.7e-14, which also validates add_block() independently — Dynare parses the original equations and builds its own auxiliary variables, so the two implementations agree only if both handle long leads and lags correctly. Regenerate the golden files with data-raw/dynare_golden.R.

Speed

The Kalman filter and the stationary-covariance solve are compiled (RcppArmadillo). Both keep reference implementations in R, and the test suite pins the compiled paths to them to machine precision — that agreement is what makes the fast path trustworthy. Use options(qpmR.use_cpp = FALSE) to fall back to R.

A posterior draw on the Czech model (22 states, 110 quarters) costs 27 ms, against 181 ms before this work — so a 6000-draw estimate takes under three minutes rather than eighteen. Two of the three gains were not the filter:

| | before | after | |---|---|---| | model assembly (build_first_order) | 27.5 ms | 1.7 ms | | stationary covariance (Lyapunov) | 39 ms | 1 ms | | Kalman filter | 55 ms | 16 ms |

The structure of the first-order system does not depend on the parameters, so it is computed once per model and cached; and the Lyapunov equation is solved by O(N^3) squaring rather than the O(N^6) Kronecker system, which also speeds up model_properties(), qpm_rule_eval() and qpm_identify().

Testing

More than 800 assertions across 28 files, at about 90% line coverage — measured on every push by the test-coverage workflow. The suite pins the solver to analytic solutions (AR(1), hybrid roots, brute-force perfect foresight), the Kalman filter to the exact closed-form Gaussian likelihood and the compiled filter to its R reference, the revision decomposition to exact telescoping, and the whole solver to Dynare (above).

Design commitments

  1. Everything has an escape hatch. sol$P, sol$Q, eigen_table() and state_space() expose the actual matrices; nothing is hidden in closures.
  2. Errors teach. A Blanchard–Kahn failure names the economics that usually causes it, not just the rank condition it violates.
  3. Verification is cheap. The test suite pins the solver to analytic solutions and to Dynare for the shipped templates, and every compiled path to its R reference implementation.

Roadmap

| Version | Focus | |---|---| | 0.1 | Model DSL, QZ solver, BK diagnostics, IRFs, simulation, forecasts, BKL template | | 0.2 | Kalman filter/smoother, shock decompositions, unit-root trends with diffuse initialization, real country dataset (czechia) | | 0.3 | Conditional forecasts (anticipated vs unanticipated), scenarios, judgment ledger, forecast rounds, round store, revision decomposition | | 0.4 | Bayesian estimation (priors, adaptive RWM, R-hat/ESS), identification diagnostics, marginal likelihood, posterior fans, estimation vignette | | 1.0 | Country adaptation (extension blocks, qpm_diff()), reporting (qpm_report(), chart_pack()) and audit (verify_round()) | | 1.1 | Compiled Kalman filter and Lyapunov solver, fevd(), model_properties(), balance of risks, rule evaluation, counterfactuals, model comparison, temporal disaggregation, standard R generics, Dynare cross-check, pkgdown site; CRAN submission | | next | Exact Durbin-Koopman diffuse initialization |

References

License

MIT.



Try the qpmR package in your browser

Any scripts or data that you put into this service are public.

qpmR documentation built on Sept. 29, 2026, 5:10 p.m.