Toolbox

CausalPy in Python: Bayesian Quasi-Experimental Causal Inference

1 What Problem Does CausalPy Solve?

Most quasi-experimental causal inference tools— did, rdrobust, synthdid— produce point estimates with frequentist confidence intervals. While these are often appropriate, there are settings where a Bayesian approach offers meaningful advantages:  

  • Small samples: Prior information can regularise estimates when data are limited.  
  • Uncertainty propagation: The posterior predictive distribution provides a natural and interpretable representation of counterfactual uncertainty.  
  • Model comparison: Bayesian information criteria and posterior predictive checks allow formal comparison of model specifications.  
  • Complex model structures: Bayesian computation via MCMC handles hierarchical models, non-Gaussian likelihoods, and regression kinks naturally.  

CausalPy [Martin et al., 2023] is an open-source Python package developed by PyMC Labs that implements Bayesian versions of the most common quasi-experimental designs: interrupted time series (ITS), difference-in-differences (DiD), regression discontinuity (RD), regression kink design (RKD), and synthetic control. It uses PyMC [Salvatier et al., 2016] for MCMC sampling and supports both MCMC and variational inference (ADVI) backends. As of version 0.8.0 (March 2026), CausalPy supports staggered adoption DiD and multiple treated units in synthetic control.  

2 Installation and Setup

# Listing 1: Installation # Via pip (recommended) pip install causalpy # Or via conda conda install -c conda-forge causalpy # Key dependencies installed automatically: # pymc >= 5.0, arviz, pandas, matplotlib, scikit-learn

CausalPy requires Python 3.9+ and PyMC 5.x. MCMC sampling benefits from having a C compiler (JAX or C++) but will run on CPU without special hardware.  

3 Interrupted Time Series: A Minimal Example

Interrupted time series is used when treatment is administered to a single unit at a known point in time, with pre- and post-treatment observations but no comparison unit. A government introduces a health policy in month 25 of a 48-month series; we want to estimate the change in level and trend.  

# Listing 2: Bayesian ITS with CausalPy import causalpy as cp import pandas as pd import numpy as np # Simulate an ITS dataset np.random.seed(42) T = 48 t = np.arange(T) treatment_time = 25 y = 2 + 0.5 * t + (t >= treatment_time) * 5 + np.random.normal(0, 1.5, T) df = pd.DataFrame({"t": t, "y": y, "treated": (t >= treatment_time).astype(int)}) # Fit Bayesian ITS model result = cp.InterruptedTimeSeries( data=df, outcome_variable="y", running_variable="t", model=cp.pymc_models.LinearRegression( sample_kwargs={"draws": 2000, "tune": 1000} ), treatment_threshold=treatment_time ) # Summary of posterior result.summary(hdi_prob=0.95) # Plot observed vs counterfactual with credible interval result.plot()

The default model fits a linear trend before treatment and extrapolates the counterfactual post-treatment trajectory. The posterior predictive distribution is shown as a shaded band around the counterfactual, giving an immediate visual sense of uncertainty.  

4 Difference-in-Differences

# Listing 3: Bayesian DiD with CausalPy # DiD with two groups, one treatment period df_did = pd.DataFrame({ "y": [3.1, 3.5, 3.3, # control pre 6.0, 6.2, 5.8, # control post 4.0, 4.2, 3.9, # treated pre 8.5, 8.8, 8.3], # treated post "post": [0]*6 + [1]*6, "treated": ([0]*3 + [1]*3) * 2 }) df_did["post_x_treated"] = df_did["post"] * df_did["treated"] result_did = cp.DifferenceInDifferences( data=df_did, formula="y ~ 1 + post + treated + post_x_treated", outcome_variable="y", model=cp.pymc_models.LinearRegression( sample_kwargs={"draws": 2000, "tune": 1000, "target_accept": 0.9} ) ) print(result_did.summary()) # The coefficient on post_x_treated is the DiD ATT estimate

The Bayesian DiD model returns a full posterior for the interaction coefficient (the DiD ATT estimate), including credible intervals. Unlike frequentist point estimates with asymptotic confidence intervals, the credible interval has an exact probability interpretation: there is a 95% posterior probability that the true ATT lies in the reported interval, given the model and data.  

5 Regression Discontinuity

# Listing 4: Bayesian RD with CausalPy import causalpy as cp # Load example RD dataset (simulated election data) df_rd = cp.load_data("regression_discontinuity") result_rd = cp.RegressionDiscontinuity( data=df_rd, formula="outcome ~ 1 + running + treated + running:treated", outcome_variable="outcome", running_variable_name="running", treatment_threshold=0.0, model=cp.pymc_models.LinearRegression( sample_kwargs={"draws": 2000, "tune": 1000} ), bandwidth=0.3 ) # Posterior of the discontinuity (jump at threshold) result_rd.plot() print(result_rd.summary())

The Bayesian RD model estimates a jump at the threshold as the difference in the posterior predictive means just above and below the cutoff. The posterior predictive is particularly useful here because it allows visualization of the full distribution of plausible treatment effects, not just a point estimate with standard errors. CausalPy also supports polynomial fits, kernel-weighted local regression, and bandwidth sensitivity analyses within the Bayesian framework.  

6 Posterior Predictive Checks

A key advantage of the Bayesian framework is the ability to run posterior predictive checks (PPCs): simulating new datasets from the posterior and comparing them to the observed data. If the model is well-specified, simulated data should look like the observed data.  

# Listing 5: Posterior predictive checks import arviz as az # Generate posterior predictive samples post_pred = result_rd.idata.posterior_predictive # Plot observed vs posterior predictive az.plot_ppc(result_rd.idata, num_pp_samples=200)

In frequentist quasi-experimental tools, model diagnostics typically focus on pre-trend tests and covariate balance. Bayesian PPCs add a complementary check: does the model's predicted distribution for the outcome match its observed distribution?  

7 Synthetic Control

# Listing 6: Bayesian synthetic control # Bayesian synthetic control: treatment of California tobacco df_sc = cp.load_data("sc_synthetic_control") result_sc = cp.SyntheticControl( data=df_sc, time_variable_name="year", unit_variable_name="state", treated_unit_name="California", outcome_variable="cigsale", treatment_time=1989, model=cp.pymc_models.WeightedSumFitter( sample_kwargs={"draws": 2000, "tune": 1000} ) ) result_sc.plot()

The Bayesian synthetic control uses a Dirichlet prior on donor weights, which automatically enforces non-negativity and the sum-to-one constraint. The posterior distribution over donor weights can be inspected to understand uncertainty about which donors are most informative.  

8 Comparison with Frequentist Tools

Feature CausalPy Frequentist (R) Notes
Uncertainty Posterior distribution CIs (asymptotic) Bayes gives exact finite-sample
Small samples Prior regularisation None Bayes advantage
Model comparison WAIC/LOO-CV AIC/BIC Formal model selection
Staggered DiD v0.8.0 (2026) did, fixest New in CausalPy
Synthetic control Yes Synth, augsynth Bayesian weights
Speed Slower (MCMC) Fast ADVI option for speed
Table 1: CausalPy vs Frequentist Quasi-Experimental Tools

9 Key Options and Pitfalls

  • Tune MCMC carefully: Set target_accept = 0.9 for complex models; increase draws and tune for high posterior curvature.  
  • Check convergence: Use az.plot_trace() and az.summary() to verify R̂ < 1.01 and effective sample size > 400 for all parameters.  
  • Prior sensitivity: The default priors are weakly informative. If results change substantially under reasonable alternative priors, investigate why.  
  • ADVI for exploration: Use cp.pymc_models.LinearRegressionWithADVI() for rapid exploration; switch to MCMC for final inference.  
  • Bandwidth in RD: CausalPy does not automatically select the optimal bandwidth (unlike rdrobust). Use a grid search over bandwidths and check sensitivity.  

References

  1. Abadie, A., Diamond, A., and Hainmueller, J. (2010). Synthetic control methods for comparative case studies. Journal of the American Statistical Association, 105(490), 493-505.  
  2. Martin, O., Kumar, R., and Lao, J. (2023). CausalPy: A Python package for Bayesian causal inference in quasi-experiments. PyMC Labs. https://github.com/pymc-labs/CausalPy.  
  3. Salvatier, J., Wiecki, T. V., and Fonnesbeck, C. (2016). Probabilistic programming in Python using PyMC3. PeerJ Computer Science, 2, e55.  

Continue Reading

Browse All Sections →
Home
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.

Article Title