Bayesian econometrics without PyMC¶
StatsPAI has two Bayesian toolkits.
statspai.bayes holds the causal designs (difference in differences,
regression discontinuity, instrumental variables, synthetic control,
marginal treatment effects). They are written in PyMC and need the bayes
extra.
statspai.mcmc holds the models of a first course in Bayesian
econometrics. They are plain NumPy samplers and need nothing beyond the
core install. This guide covers them. It follows the chapters of
Ramirez-Hassan, Introduction to Bayesian Econometrics (2026), whose
examples use the R packages MCMCpack, bayesm, coda and BMA.
Chapter by chapter¶
| chapter | the book runs | StatsPAI |
|---|---|---|
| 3 | conjugate normal / inverse-gamma regression, marginal likelihood, Bayes factor | sp.bayes_regress(model='conjugate'), .log_marginal_likelihood(), sp.bayes_factor |
| 4 | Gibbs, Metropolis-Hastings, convergence diagnostics | the samplers below; sp.geweke_diag, sp.raftery_diag, sp.heidel_diag, sp.gelman_rubin, sp.mcmc_ess |
| 6 | MCMCregress |
sp.bayes_regress(model='normal') |
| 6 | MCMClogit, rbprobitGibbs |
model='logit', model='probit' |
| 6 | rordprobitGibbs |
model='oprobit' |
| 6 | negative binomial by Metropolis-Hastings | model='negbin', also model='poisson' |
| 6 | multinomial logit by Metropolis-Hastings | model='mlogit' |
| 6 | MCMCtobit |
model='tobit', lower=, upper= |
| 6 | MCMCquantreg |
model='quantile', quantile= |
| 6 | heteroskedastic errors by a scale mixture (exercise) | model='t', dof= |
| 6 | Bayesian bootstrap | sp.bayes_bootstrap |
| 8 | dlm::dlmModReg, dlmMLE, dlmFilter, dlmSmooth, dlmGibbsDIG |
sp.dlm("y ~ x", df), method='mle' or 'gibbs' |
| 8 | Minnesota-prior VAR | sp.bvar |
| 8 | ARMA processes | sp.bayes_arima(y, order=(p, d, q), horizon=) |
| 8 | stochastic volatility (stochvol::svsample) |
sp.stochvol(returns) |
| 7 | MCMCregress on several outcomes, bayesm::rsurGibbs |
sp.bayes_sur(["y1 ~ x", "y2 ~ z"], df) |
| 7 | bayesm::rmvpGibbs (multivariate probit) |
sp.bayes_mvprobit(["y1 ~ x", "y2 ~ x"], df) |
| 6 | bayesm::rmnpGibbs (multinomial probit) |
sp.bayes_mnprobit("choice ~ x", df) |
| 11 | finite mixtures, Dirichlet process mixtures | sp.bayes_mixture("y ~ 1", df, components=3 / 'dp') |
| 7, 13 | bayesm::rivGibbs |
sp.bayes_ivreg("y ~ x + (d ~ z)", df) |
| 9 | MCMChregress, hierarchical logit and Poisson |
sp.bayes_mixed(family='normal' / 'logit' / 'poisson') |
| 10 | BMA::bicreg, BMA::bic.glm |
sp.bma(method='bic') |
| 10 | g-prior model averaging, MC3 | sp.bma(method='gprior') |
| 10 | Savage-Dickey, Chib, Gelfand-Dey | sp.savage_dickey; .log_marginal_likelihood(method=) |
| 12 | Bayesian lasso, stochastic search variable selection | sp.bayes_shrink(prior='lasso' / 'ssvs') |
| 12 | BART | sp.bart("y ~ x1 + x2", df), family='binary' |
| 12 | Gaussian process regression | sp.gp_regress("y ~ x", df) |
| 14 | approximate Bayesian computation, synthetic likelihood | sp.abc(simulate, s_obs, prior, method='rejection' / 'synthetic') |
| 14 | variational Bayes for the linear model | sp.bayes_regress(inference='vb') |
| 13 | Bayesian IV, DiD, RD | sp.bayes_iv, sp.bayes_did, sp.bayes_rd, sp.bayes_fuzzy_rd (PyMC) |
Not covered: more than one endogenous regressor in sp.bayes_ivreg,
multinomial probit with regressors that vary across alternatives,
Bayesian splines, INLA, and chapter 13's exponentially tilted empirical
likelihood and general Bayes posteriors.
How each sampler is checked¶
A Markov chain cannot be compared digit by digit with another package, so each sampler is tested against a posterior obtained without it.
- A small model whose posterior is integrated on a grid, with the
integrand written separately from
scipy.stats(all ofsp.bayes_regress,sp.bayes_mixed,sp.bayes_ivreg,sp.bayes_sur,sp.bayes_shrink,sp.bayes_arima). - Enumeration: all 4,140 partitions of eight observations for
sp.bayes_mixture, all five trees of a two-cutpoint problem forsp.bart. - Importance sampling from the prior for the probit systems.
- The joint-distribution test, for samplers with latent states
(
sp.stochvol, the probit systems,sp.bart). Each sweep is followed by a fresh draw of the data given the current state. If every conditional is right, the parameters are then distributed as the prior. - Deterministic pieces against a reference to 1e-9 or better:
coda,BMA,BMS,dlm, scikit-learn.
Comparisons with long runs of MCMCpack, bayesm, stochvol and dbarts are screens. Both sides are simulation output.
sp.bart_causal (the Bayesian causal forest, method='bcf', and a BART
response surface, method='bart') reuses the sp.bart engine, whose
backfitting kernel now takes precision weights; with unit weights every
sp.bart draw is bit for bit what it was. Its evidence is known truth
(credible intervals cover the true ATE / ATT, the effect forest recovers
the heterogeneity) and an identity (constant weights c equal noise
sigma^2 / c). Against R bcf and bartCause it is a seed screen,
eight seeds a side: BCF's posterior mean of the ATE is 0.06 posterior SDs
from bcf's and the posterior SDs agree to 1.2%; the BART version is 0.21
posterior SDs from bartCause's (more than seed noise; the two BART
specifications differ in a detail that has not been located) with SDs
within 3.3%. Since 1.41 sp.bcf is sp.bart_causal(method='bcf') with
sp.bcf's argument names (n_trees_mu, n_trees_tau, alpha,
random_state); through 1.40 it was a random-forest / boosting
approximation, which sp.BayesianCausalForest(...).fit() still gives and
which warns that it is one.
A regression¶
fit = sp.bayes_regress(
"lv ~ Perf + Age + Age2 + NatTeam + Goals + Exp + Exp2", players,
prior_var=1000, sigma2_prior=(0.001, 0.001), draws=20000, seed=1,
)
print(fit.summary())
fit.prob("NatTeam > 0") # posterior probability of a statement
fit.conf_int(kind="hpd") # highest posterior density intervals
fit.predict(new, what="interval")
fit.diagnostics() # Geweke, Heidelberger-Welch, Raftery-Lewis
fit.plot() # traces and densities
The table has the posterior mean and standard deviation, the Monte Carlo standard error of the mean, the effective sample size, the credible interval and the posterior probability that the parameter is positive.
draws is the number of draws kept. Each chain runs
burnin + draws * thin iterations. chains=4 starts four chains from
dispersed values and adds the Gelman-Rubin factor to fit.diagnostics().
Priors¶
Coefficients have a normal prior, prior_mean and prior_var. The
variance can be one number, one per coefficient or a full matrix.
The default variance is 1000 (100 for logit, probit and ordered probit,
whose coefficients live on a bounded scale). That is vague only when the
regressors are on a moderate scale. A wage regression with income in
dollars, or a regressor measured in thousandths, can have a coefficient in
the hundreds, and then a standard deviation of 31.6 is an informative prior
that pulls the estimate to zero. StatsPAI checks this after sampling and
warns when the default prior is not vague for a coefficient. Set
prior_var or rescale the regressor.
The error variance has the prior sigma2 ~ InvGamma(alpha0 / 2, delta0 /
2), given as sigma2_prior=(alpha0, delta0). This is the convention of
the book and of MCMCpack (c0, d0).
Other models add one prior each.
| model | extra parameter | prior argument |
|---|---|---|
negbin |
alpha, with variance mu + alpha mu^2 |
size_prior=(shape, rate) on 1 / alpha |
quantile |
scale sigma of the asymmetric Laplace |
scale_prior=(n0, s0), or fix it with scale= |
oprobit |
cutpoints | cut_prior_var on the log distance between cutpoints |
t |
none, dof is fixed |
Things that differ from the R packages¶
Quantile regression estimates the scale. MCMCpack::MCMCquantreg
fixes the scale of the asymmetric Laplace likelihood at one. The posterior
spread then depends on the units of the outcome. Multiply y by ten and
the credible intervals do not grow by ten. sp.bayes_regress estimates the
scale by default (Kozumi and Kobayashi 2011). scale=1.0 reproduces
MCMCquantreg.
The ordered probit always has free cutpoints. bayesm::rordprobitGibbs
fixes the first cutpoint at zero and expects a constant in the design. The
book's example passes a design without a constant, which forces the first
threshold to zero and moves every coefficient. sp.bayes_regress reports
the slopes and J - 1 cutpoints, the parameterisation of sp.oprobit and
Stata, and cannot be given the restricted model by accident.
The hierarchical logit and Poisson have no extra error term.
MCMChlogit and MCMChpoisson add an observation-level normal error to
the linear index. sp.bayes_mixed fits the usual generalised linear mixed
model, the one sp.melogit and Stata's melogit fit by maximum
likelihood.
The fixed effects of a hierarchical model have their marginal
spread. On the public-capital panel MCMChregress reports a posterior
sd of 0.008 for the intercept. With 48 states and a state-effect variance
of 0.106 the intercept cannot be known better than
sqrt(0.106 / 48) = 0.047. sp.bayes_mixed reports 0.17 under the same
prior, and agrees with lme4 under its own default prior.
sp.bma finds every model inside Occam's window. bicreg keeps the
best 150 models of each size and then applies the window. sp.bma runs an
exact branch and bound for the window itself. The two agree whenever
bicreg's cap does not bind. bicreg also computes BIC from an R-squared
rounded to five decimals, so its probabilities agree with the exact ones
to about three digits.
The multivariate Gelman-Rubin factor follows the paper. coda puts the
number of parameters where Brooks and Gelman (1998) have the number of
chains. The univariate factors are identical.
Comparing models¶
small = sp.bayes_regress("y ~ x1", df, model="conjugate", prior_var=4)
large = sp.bayes_regress("y ~ x1 + x2", df, model="conjugate", prior_var=4)
sp.bayes_factor(small, large)
sp.savage_dickey(large, "x2")
Four estimators of the log marginal likelihood are available through
fit.log_marginal_likelihood(method=).
| method | models | what it is |
|---|---|---|
'exact' |
conjugate | closed form |
'chib' |
normal, probit | Chib (1995), from the Gibbs output |
'gelfand-dey' |
all | Gelfand and Dey (1994) with Geweke's truncated normal |
'laplace' |
all but quantile | Laplace approximation at the posterior mode |
A Bayes factor needs proper priors and depends on them. With a prior
variance of c on an extra coefficient, the Bayes factor in favour of the
smaller model grows like the square root of c. A "vague" default prior
therefore favours the smaller model by an arbitrary amount. Choose the
prior variance of the coefficients under test on purpose, and report how
the Bayes factor moves when it changes.
Under the conjugate prior the Savage-Dickey ratio compares with a
restricted model whose prior for sigma2 has one more degree of freedom.
The docstring gives the exact statement.
Model averaging¶
out = sp.bma("y ~ x1 + x2 + x3 + x4 + x5 + x6", df) # BIC
out = sp.bma("d ~ x1 + x2 + x3", df, family="binomial") # logit
out = sp.bma("y ~ x1 + x2 + x3", df, method="gprior") # g-prior
out = sp.bma("y ~ treat + x1 + x2 + x3", df, always=["treat"]) # controls only
out.table # inclusion probabilities, averaged coefficients
out.models # the models and their probabilities
always= keeps a term in every model, which is how to ask how an estimate
moves across sets of controls. A posterior inclusion probability is a
statement about prediction. It does not say that a control is a valid
adjustment variable.
Panel data¶
fit = sp.bayes_mixed("lgsp ~ lpcap + lpc + lemp + unemp", df, group="id")
fit = sp.bayes_mixed("y ~ x", df, group="id", random=["x"]) # random slope
fit = sp.bayes_mixed("d ~ x", df, group="id", family="logit")
fit.random_effects # posterior mean and sd of every group's effects
fit.re_cov # posterior mean of their covariance matrix
The random effects are assumed independent of the regressors. When that is
in doubt and the target is a within-unit effect, use sp.panel(...,
method='fe').
The covariance of the random effects has the prior
InvWishart(df, df * scale). The textbook and MCMCpack use an identity
scale, which says the random effects have a variance near one. For a log
outcome that is usually far too large. In the book's public-capital panel
(48 states, log gross state product) the prior InvWishart(5, 5) supplies
91 percent of the sum of squares behind the variance of the state
effects, and the posterior mean of that variance is 0.106 against a
restricted maximum likelihood estimate of 0.0076.
The default of sp.bayes_mixed is therefore different: df = q + 1 and
scale = 0.02 / df. In the full conditional of the covariance matrix the
prior then adds 0.02 to the sum of squares of each random effect, which
is negligible on any ordinary scale. On the same panel the default gives
the lme4 estimates and standard errors to two digits.
re_prior=(q + 2, 1.0) is the textbook prior.
Whatever the prior, fit.model_info['re_prior_share'] is the share of
each variance component that comes from it, and a warning is raised above
25 percent. That happens with the default too when the effects are tiny
or the groups very few. Report how the variance components move when
re_prior changes.
Instrumental variables¶
fit = sp.bayes_ivreg("y ~ x + (d ~ z1 + z2)", df, seed=1)
fit.conf_int().loc["d"] # the effect
fit.prob("rho > 0") # is the regressor endogenous, and which way
fit.model_info["first_stage_F"]
sp.bayes_ivreg samples the joint normal model of the first stage and
the structural equation by Gibbs, with the formula syntax of sp.ivreg.
rho is the correlation of the two errors. Zero means the regressor is
exogenous, so its posterior is the Bayesian counterpart of a Hausman
test.
With a strong instrument the posterior of the effect is close to normal around 2SLS with the 2SLS standard error. With a weak instrument it is wide and skewed and depends on the priors, and the fit warns when the first-stage F is below 10.
sp.bayes_iv fits the same model in PyMC, with half-normal priors on the
error scales. The two agree. bayesm::rivGibbs uses an identity scale for
the inverse-Wishart prior of the error covariance; the default here adds
0.02 to the sums of squared residuals, and sigma_prior=(3, 1.0) is the
bayesm prior.
Time-varying coefficients¶
fit = sp.dlm("y ~ x", df) # variances by maximum likelihood
fit = sp.dlm("y ~ x", df, method="gibbs", constant=["Intercept"], seed=1)
fit.smoothed # coefficient paths given the whole sample, with intervals
fit.filtered # given the data up to each date
fit.variances # observation variance and one state variance per term
fit.forecast(4, new)
fit.plot()
Each coefficient follows a random walk. "y ~ 1" is the local level
model, and constant= holds a coefficient fixed. A state variance
estimated at zero says the data do not ask for that coefficient to move.
The filter, smoother and likelihood are those of the R package dlm and
agree with it to nine digits; dlm::dlmLL is the negative log likelihood
without the 2 pi constant.
Diagnostics for any chain¶
The diagnostic functions take any array or DataFrame of draws, including draws produced elsewhere.
sp.mcmc_summary(draws) # mean, sd, naive and time-series SE, ESS, quantiles
sp.geweke_diag(draws) # early mean against late mean
sp.heidel_diag(draws) # stationarity, then precision of the mean
sp.raftery_diag(draws) # how long a chain a quantile needs
sp.gelman_rubin([c1, c2]) # several chains, or one chain with split=True
sp.hpd_interval(draws)
They return the same numbers as coda to nine digits or more.
What the evidence is¶
MCMC output cannot be compared digit for digit across programs. Two correct samplers differ by Monte Carlo error. The checks are therefore of three kinds.
Deterministic functions are compared with R on committed files. This
covers the diagnostics (coda), model averaging by BIC (BMA) and by
g-prior (BMS).
Every sampler is compared with the exact posterior of a small model. With two coefficients and one auxiliary parameter the posterior can be integrated on a grid without simulation. The posterior means must be within four Monte Carlo standard errors of the exact ones, the standard deviations within five percent, and the marginal likelihood estimators must reproduce the exact normalising constant. The hierarchical models are checked the same way, with the random effects integrated out analytically or by Gauss-Hermite quadrature, and the IV sampler with the error covariance integrated out in closed form.
The book's examples are rerun on its own data against long runs of
MCMCpack and bayesm. This is a screen, not a parity claim.