Bayesian marginal effects — posterior draws via from_posterior¶
Every inference path in pymargins ultimately needs a distribution
over the parameter vector β. The delta method assumes that
distribution is normal with covariance vcov; Krinsky–Robb
simulates draws from that same normal; the bootstrap resamples it.
But if you fit the model in a Bayesian framework — PyMC, NumPyro,
Stan, bambi — you already have the posterior as a bank of MCMC
draws, and there is no reason to throw it away and re-approximate it
as normal.
Margins.from_posterior takes those draws directly. It runs the
simulation inference path, but instead of drawing β from
N(β̂, V̂) it uses your draws as-is. The marginal effect is computed
once per draw, and the spread of those values is the posterior of
the marginal effect — a genuine credible interval on the outcome
scale, with no delta-method linearization anywhere.
This demo uses the Mroz female-labor-force data and a logit. To keep
the documentation build dependency-free, we generate the posterior
draws with a Laplace approximation — a multivariate normal centred
at the MLE with the model’s estimated covariance. Real MCMC draws
from PyMC or NumPyro are an array of exactly the same shape
(n_draws, n_params) and plug into the same call; the only thing that
changes is where the draws come from. See the closing note for the
drop-in PyMC/NumPyro snippet.
import jax
jax.config.update("jax_enable_x64", True)
import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
from linearmodels.datasets import mroz
from pymargins import Margins
cols = ["inlf", "nwifeinc", "educ", "exper", "age", "kidslt6", "kidsge6"]
df = mroz.load()[cols].dropna().copy()
df["expersq"] = df["exper"] ** 2
fit = smf.glm(
"inlf ~ nwifeinc + educ + exper + expersq + age + kidslt6 + kidsge6",
data=df,
family=sm.families.Binomial(),
).fit()
print(fit.summary().tables[1])
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
Intercept 0.4255 0.860 0.495 0.621 -1.261 2.112
nwifeinc -0.0213 0.008 -2.535 0.011 -0.038 -0.005
educ 0.2212 0.043 5.091 0.000 0.136 0.306
exper 0.2059 0.032 6.422 0.000 0.143 0.269
expersq -0.0032 0.001 -3.104 0.002 -0.005 -0.001
age -0.0880 0.015 -6.040 0.000 -0.117 -0.059
kidslt6 -1.4434 0.204 -7.090 0.000 -1.842 -1.044
kidsge6 0.0601 0.075 0.804 0.422 -0.086 0.207
==============================================================================
1. Build the posterior draw bank¶
The Laplace posterior is N(β̂, V̂). We draw 4 000 samples; the
columns are in the same order as fit.params, which is exactly the
order the adapter expects.
beta_hat = np.asarray(fit.params)
V_hat = np.asarray(fit.cov_params())
rng = np.random.default_rng(0)
posterior_draws = rng.multivariate_normal(beta_hat, V_hat, size=4000)
print("draw bank shape:", posterior_draws.shape) # (n_draws, n_params)
draw bank shape: (4000, 8)
2. Open a posterior session¶
from_posterior defaults method="simulation" and n_sim to the
number of draws. The point estimate defaults to the posterior mean;
pass point_estimate= to report a different summary (e.g. the MLE or
the posterior median).
m_bayes = Margins.from_posterior(fit, posterior_draws, at="overall")
print("method:", m_bayes.method, " draws:", m_bayes.n_sim)
method: simulation draws: 4000
3. AME credible interval vs delta-method interval¶
Ask the posterior session for the average marginal effect of education on participation. The interval it returns is a 95 % credible interval — the 2.5th and 97.5th percentiles of the AME across the posterior draws. We put it side by side with the ordinary delta-method confidence interval from a standard session.
m_delta = Margins.linear_scale(fit, at="overall")
ame_bayes = m_bayes.dydx("educ")
ame_delta = m_delta.dydx("educ")
print("Posterior (credible interval):")
print(ame_bayes.summary())
print("\nDelta method (confidence interval):")
print(ame_delta.summary())
Posterior (credible interval):
===========================================================
Margins Result (simulation, level=0.95)
===========================================================
estimate std err statistic P>|z| [95% Conf. Int.]
-----------------------------------------------------------
educ 0.0395 0.0072 0.0395 0.000 0.0247, 0.0528
===========================================================
n = 753
κ: 0.055
Delta method (confidence interval):
========================================================
Margins Result (delta, level=0.95)
========================================================
estimate std err z P>|z| [95% Conf. Int.]
--------------------------------------------------------
educ 0.0395 0.0073 5.4144 0.000 0.0252, 0.0538
========================================================
n = 753
κ: 0.054
Delta-vs-sim disagreement: 2.267%
Under a flat prior the Laplace posterior is, by construction, the same
normal the delta method assumes — so the two intervals here agree to
within Monte-Carlo error. That agreement is the point: it shows the
posterior path is calibrated against the analytic one in the case
where they should coincide. The value of from_posterior shows up
when they should not coincide — an informative or skewed prior, a
small sample where the posterior is non-normal, or a nonlinear
estimand whose posterior is asymmetric. There the credible interval
follows the true posterior shape and the delta interval cannot.
4. The full posterior of an effect, not just an interval¶
Because every draw produces a complete marginal effect, you are not limited to a symmetric interval — you can report any posterior summary. Here is the AME of an additional young child (a discrete contrast) as a posterior, with its probability of being negative.
from pymargins import pairwise
scen, w = pairwise("kidslt6", [1, 0])
child_effect = m_bayes.contrasts(scenarios=scen, contrasts=w)
print(child_effect.summary())
draws = np.asarray(child_effect.draws)
print(f"\nPosterior mean : {draws.mean():.4f}")
print(f"P(effect < 0) : {(draws < 0).mean():.3f}")
print(f"90% credible : [{np.percentile(draws, 5):.4f}, "
f"{np.percentile(draws, 95):.4f}]")
================================================================
Margins Result (simulation, level=0.95)
================================================================
estimate std err statistic P>|z| [95% Conf. Int.]
----------------------------------------------------------------
kidslt6=1 -0.2697 0.0347 -0.2697 0.000 -0.3321, -0.1982
================================================================
n = 753
κ: 0.057
Posterior mean : -0.2668
P(effect < 0) : 1.000
90% credible : [-0.3228, -0.2100]
P(effect < 0) is a direct posterior probability — the kind of
statement a Bayesian analysis is supposed to deliver and that a
p-value cannot. It comes for free once the estimand is evaluated on
each draw.
Plugging in real MCMC draws¶
Nothing above is specific to the Laplace approximation. If you fit the
same logit in PyMC or NumPyro, you obtain a posterior array of shape
(n_draws, n_params) and pass it to the same constructor:
# PyMC
import pymc as pm
with pm.Model() as model:
# ... priors and likelihood for the same design matrix ...
idata = pm.sample()
# stack chains × draws into (n_draws, n_params), columns in fit.params order
posterior_draws = idata.posterior[coef_names].to_array() \
.transpose("chain", "draw", "variable") \
.values.reshape(-1, len(coef_names))
m_bayes = Margins.from_posterior(fit, posterior_draws, at="overall")
m_bayes.dydx("educ") # AME with a real posterior credible interval
The only requirements are that the draw columns line up with the
adapter’s coefficient order (the order of fit.params) and that
fit is the same specification you sampled. Everything downstream —
predict, dydx, contrasts, evaluate, subgroups, plotting —
behaves identically to any other session.
Where to next¶
Delta vs simulation vs bootstrap — how the simulation path relates to the delta method and the bootstrap, and why
from_posteriorslots into the same machinery.Mroz (1987) — Female labor force participation — the same dataset analysed with the standard delta-method and bootstrap paths.
Inference — delta, simulation, bootstrap — switching inference methods on a single session.