Causal Inference with PyMC
PyMC supports structural causal models via pm.do and pm.observe.
pm.do (Interventions)
Apply do-calculus interventions to set variables to fixed values, breaking incoming causal edges:
with pm.Model() as causal_model:
x = pm.Normal("x", 0, 1)
y = pm.Normal("y", x, 1)
z = pm.Normal("z", y, 1)
# Intervene: set x = 2 (breaks incoming edges to x)
with pm.do(causal_model, {"x": 2}) as intervention_model:
idata = pm.sample_prior_predictive()
# Samples from P(y, z | do(x=2))pm.observe (Conditioning)
Condition on observed values without breaking causal structure:
# Condition: observe y = 1 (doesn't break causal structure)
with pm.observe(causal_model, {"y": 1}) as conditioned_model:
idata = pm.sample(nuts_sampler="nutpie")
# Samples from P(x, z | y=1)Combining do and observe
# Intervention + observation for causal queries
with pm.do(causal_model, {"x": 2}) as m1:
with pm.observe(m1, {"z": 0}) as m2:
idata = pm.sample(nuts_sampler="nutpie")
# P(y | do(x=2), z=0)Causal Effect Estimation
# Average causal effect via do-calculus
with pm.do(causal_model, {"treatment": 1}) as treated:
idata_treated = pm.sample_prior_predictive(draws=2000)
with pm.do(causal_model, {"treatment": 0}) as control:
idata_control = pm.sample_prior_predictive(draws=2000)
# ATE = E[Y | do(T=1)] - E[Y | do(T=0)]
ate = (idata_treated.prior_predictive["outcome"].mean() -
idata_control.prior_predictive["outcome"].mean())Key Considerations
pm.doreplaces the variable’s distribution with a constant — all upstream causal paths are severedpm.observeadds observed data — the variable remains stochastic but conditioned on the value- For backdoor adjustment, condition on confounders using
pm.observeor include them in the model directly - For front-door adjustment or instrumental variables, combine
pm.dowith appropriate model structure