Bayesian Model-Based Potential Outcomes in NumPyro
Learn how Bayesian model-based potential outcomes turn causal inference into a missing-data problem, using NumPyro and the Lalonde job-training experiment.
causal
bayesian
Author
Pau Pereira
Published
March 11, 2025
Modified
August 2, 2026
Model-based potential outcomes (MBPO) takes the missing-data formulation of causal inference literally. Each unit has two potential outcomes, one under each treatment, but we observe only one. We specify a probability model for the complete schedule of potential outcomes, which Imbens and Rubin call the Science, keep it separate from the treatment-assignment mechanism, and use the observed data to obtain a posterior distribution for what is missing. The neat part is that the causal estimand need not be a parameter of the model: once a posterior draw completes the schedule, we can calculate any scientifically meaningful function of it, from an average or quantile effect to a policy-relevant decision. What I like about the framework is that it puts the outcome-generating process at the center of the analysis. Substantive knowledge can shape that model, while Bayesian inference carries uncertainty from its parameters, through the missing potential outcomes, to the causal quantities we ultimately care about.1
In most applications, I do not want to treat the outcome model as a nuisance that merely has to be flexible enough for an estimator to work. I think that iff a causal effect is worth measuring, the process that generates the outcome is usually worth understanding. A good science model should respect the support of the outcome, explain features such as zeros and tails, and make its substantive assumptions visible. Within the program-evaluation literature, MBPO comes closest to that view. Bayesian inference then adds a coherent way to propagate uncertainty through the model, the missing potential outcomes, and the causal estimands.
Ignorability is not required by the framework, but it makes inference particularly convenient. Under an ignorable assignment mechanism, the assignment model drops out of the posterior distribution of the missing potential outcomes. This does not make the design of the study or overlap between treatment groups unimportant, but it does allow the outcome analysis to focus entirely on the science. When assignment is nonignorable, the same framework still applies, but the assignment mechanism must be modeled jointly with the potential outcomes. At that point, identification requires additional economic or statistical structure, such as a model of selection, an exclusion restriction, or strong distributional assumptions.
The framework is deliberately agnostic about what the science should look like. At one extreme, we can use a flexible or fully nonparametric model for the potential outcomes. At the other, we can derive the potential outcomes from a substantive model of behaviour or technology. The framework itself does not make a model structural. For that, the science must be built from economic primitives that remain meaningful under the counterfactual policies we want to study. Still, MBPO feels to me like reduced-form causal inference with one foot on the line of structural econometrics. It gives us the familiar potential-outcomes estimands while leaving the door open to a structural model of how those outcomes are generated.
The Roy model is an interesting example of this structural flavour. Each worker has potential earnings in two sectors, and a selection rule determines both the sector the worker enters and the potential earnings we observe. The ingredients look remarkably similar to those of MBPO: a joint distribution of potential outcomes and a mechanism selecting which outcome is revealed. The crucial difference is that selection in the classical Roy model depends directly on the potential outcomes. Its assignment mechanism is therefore not ignorable, and the science and assignment mechanism must be modeled together. This example shows why separating the two components is useful even when one cannot ultimately be ignored.
I first encountered MBPO in Imbens and Rubin (2015). The idea is direct: for each unit, one potential outcome is observed and the other is missing. MBPO takes that formulation literally: specify a probability model for the potential outcomes, condition on what was observed, and impute what was not. Bayesian inference is a natural fit because it yields a posterior distribution for the complete potential-outcome schedule.
Once we have that posterior distribution, we can compute the posterior distribution of almost any causal estimand we care about. We are not limited to an average treatment effect. We can study quantiles, ratios, growth rates, heterogeneous effects, or, more generally, functions of the form \tau(\boldsymbol{Y}(0),\boldsymbol{Y}(1)).2 We can also connect the analysis directly to decision-making. If treatments have costs and outcomes have economic value, we can calculate the expected utility of different policies and construct treatment rules that maximize it. This is much closer to the question we usually care about: not merely whether an effect is different from zero, but what we should actually do.
The framework goes back to Rubin’s early work on potential outcomes and Bayesian causal inference, Rubin (1974) and Rubin (1978). Given Rubin’s parallel work on missing-data problems, the connection is a natural one.
In this article, I give a hands-on introduction to MBPO using NumPyro, a powerful Python library for probabilistic programming. I apply the framework to the well-known Lalonde study of a job-training program, LaLonde (1986), and show how to impute the missing potential outcomes and obtain posterior distributions for several treatment-effect estimands. The application follows an example from chapter 8 of Imbens and Rubin (2015). A related implementation using Stan can be found in Lee et al. (2018). Other useful introductions include Ding and Li (2018), Mealli et al. (2023), and Fabrizia Mealli’s video lecture.
The theory
The goal of this section is to go over the theory of how to derive the posterior distribution of the missing potential outcomes from two primitives: a model for the Science and a model for the assignment mechanism.
Setup and estimands
Consider N units indexed by i=1,\ldots,N. For each unit, \mathbf{X}_i is a vector of pretreatment covariates, W_i\in\{0,1\} indicates treatment assignment, and Y_i(0) and Y_i(1) are the potential outcomes under control and treatment.
I assume the Stable Unit Treatment Value Assumption (SUTVA). This combines two restrictions. First, there is no interference between units, so the potential outcome of unit i depends only on its own treatment:
Y_i(W_1,\ldots,W_N)=Y_i(W_i).
Second, there are no causally relevant hidden versions of either treatment. Together with consistency, this gives the observed outcome
Given W_i, there is a one-to-one mapping between \{Y_i(0),Y_i(1)\} and \{Y_i^{\mathrm{obs}},Y_i^{\mathrm{mis}}\}.
A finite-sample causal estimand is a function of the potential outcomes of the N units in the study. The most familiar example is the sample average treatment effect,
Because half of the potential outcomes are missing, \tau^S is only partially observed. In a Bayesian analysis, its posterior uncertainty comes from uncertainty about the missing entries in the realized potential-outcome schedule.
A superpopulation estimand instead refers to the population from which the units are sampled. For example,
The distinction matters because finite-sample and superpopulation estimands require different posterior calculations. It is also important that the causal estimand need not be a parameter of the probability model. The estimand is defined by the scientific question; the model is a device for learning about it from incomplete data. This distinction is central to the model-based approach in Imbens and Rubin (2015).
The Science and the assignment mechanism
Following Rubin, I will call the complete schedule
This is a modeling assumption, not a consequence of the potential-outcomes framework. Hierarchical, clustered, spatial, or network models would require a different factorization.
where \psi denotes any unknown parameters governing assignment. I keep this distribution at the vector level because assignments need not be independent across units. In a completely randomized experiment, for example, fixing the number of treated units induces dependence among the W_i’s.
The full joint distribution factors as
\begin{aligned}
&p\!\left\{\mathbf{Y}(0),\mathbf{Y}(1),\mathbf{W}\mid\mathbf{X},\theta,\psi\right\} \\
&\qquad = \underbrace{g_\psi\!\left\{\mathbf{W}\mid\mathbf{Y}(0),\mathbf{Y}(1),\mathbf{X}\right\}}_{\text{assignment mechanism}}\underbrace{f_\theta\!\left\{\mathbf{Y}(0),\mathbf{Y}(1)\mid\mathbf{X}\right\}}_{\text{model for the Science}}.
\end{aligned}
\tag{1}
This factorization is just the chain rule. What matters is that its two factors represent different objects and should be informed by different kinds of knowledge.
Combining them with a prior gives the general MBPO posterior:
For observational studies, this assumption is usually accompanied by positivity:
0 < \Pr(W_i=1\mid\mathbf{X}_i=\mathbf{x}) < 1
for all covariate values \mathbf{x} in the target population. Positivity is not required for the algebraic cancellation below, but it is required for nonparametric identification and for both potential-outcome surfaces to have empirical support over the target covariate distribution.
In a completely randomized experiment with N_1 treated units, the assignment mechanism is known:
If the parameters of the Science and the assignment mechanism are distinct and their prior factorizes,
p(\theta,\psi)=p(\theta)p(\psi),
then an ignorable assignment mechanism contains no information about \mathbf{Y}^{\text{miss}} or \theta once we condition on \mathbf{W} and \mathbf{X}. Equation 2 simplifies to
This is what it means to ignore the assignment mechanism in the Bayesian analysis. It does not mean that the design of the study is unimportant. Poor overlap forces the model for the Science to extrapolate into regions with little or no data, making the analysis correspondingly more sensitive to model specification.
More general assignment mechanisms can be ignorable in the missing-data sense if assignment depends on observed potential outcomes but not on the missing ones. That broader definition is useful for adaptive and sequential designs. For the one-time treatment considered here, conditional unconfoundedness is the cleaner assumption.
If assignment is nonignorable, the g_\psi term remains in Equation 2. Treatment status then carries information about the missing potential outcomes, and the Science and assignment mechanism must be modeled jointly. MBPO accommodates this case, but it does not make the identification problem disappear. Identification must come from additional structure, such as a structural selection rule, an exclusion restriction, instrumental variation, distributional restrictions, external data, or a sensitivity analysis.
Posterior imputation under ignorability
Under conditional independence across units, let f_{\theta,w}(y\mid\mathbf{x}) denote the marginal density of Y_i(w) implied by the joint model for the Science. The observed-data posterior for \theta is
The empirical distribution of the \tau^{(s)}’s is the posterior distribution of the estimand. Notice that the estimand enters only in the final step. We can therefore use the same posterior draws of the completed Science to calculate many different causal estimands.
It is important to use the same parameter draw for all units in a posterior iteration. Once \theta is integrated out, shared parameter uncertainty induces posterior dependence among their imputations.
Dependence between the potential outcomes
The observed likelihood in Equation 4 contains information about the two marginal distributions
The association parameter does not appear in Equation 7, which means that it cannot be updated by the data. If the prior also separates as p(\theta^m,\theta^a)=p(\theta^m)p(\theta^a), then
The posterior for \theta^a is its prior because the likelihood is flat in that direction. The fact that we get a proper posterior can conceal a lack of empirical identification. If the priors for \theta^m and \theta^a are dependent, the posterior for \theta^a may move indirectly, but that movement comes from prior dependence rather than information in the likelihood. This is the distinction between identified marginal parameters and an unidentified association parameter emphasized in Ding and Li (2018) and in Chapter 8 of Imbens and Rubin (2015).
Here \theta^a=\rho. The observed likelihood updates the two means and variances but not \rho. Nevertheless, \rho changes the conditional distributions used to impute the missing potential outcomes. For a fixed value of \rho,
Increasing \rho makes the observed outcome more informative about the missing outcome through the conditional mean and reduces the residual conditional variance. It does not change the fit to the observed data or the posterior for the marginal outcome parameters. It changes only the completion of each worker’s potential-outcome pair.
These are some of the consequences of this fact:
Population average effects, conditional average effects, and differences between marginal quantiles depend only on the two marginal distributions.
The variance or distribution of individual treatment effects, the probability that treatment benefits an individual, and other paired or cross-world quantities depend on the unidentified association.
The posterior distribution of a finite-sample estimand can also depend on that association because each missing outcome is imputed conditional on its observed counterpart.
The association should therefore be treated as a substantive assumption or sensitivity parameter, not as something estimated from the observed data. A flexible or nonparametric model for the marginal distributions does not recover the missing joint information. In Model 1 I set \rho=0 as a transparent baseline. The Model 1 sensitivity appendix repeats the imputation at fixed values of \rho, following the strategy described by Imbens and Rubin (2015) and Ding and Li (2018).
Finite-sample and superpopulation inference
For the finite-sample average effect, each completed posterior draw gives
where F_X is the covariate distribution of the target population. If F_X is modeled, Equation 9 can be evaluated for every posterior draw of its parameters and \theta.
Often we do not want to model the covariate distribution. We can instead standardize the conditional mean effects over the empirical distribution of the observed covariates:
As Ding and Li (2018) stresses, \tau^X is generally neither the realized sample average effect \tau^S nor the unconditional population average effect \tau^P. It is a model-based average conditional effect standardized to the covariate distribution of the study sample.
For superpopulation functionals such as Equation 9 and Equation 10, imputing the missing potential outcomes is often unnecessary. We can evaluate the functional directly for each posterior draw of \theta. Imputation is essential when the target is a function of the realized complete schedule, as with finite-sample or unit-level estimands.
Everything that follows is an implementation of this recipe. We specify progressively more credible models for the Science, draw from the posterior of their parameters, impute the missing potential outcomes, and transform the completed schedules into causal estimands.
Bayesian models with NumPyro
Readers familiar with regression adjustment or the outcome-regression part of AIPW will recognize the basic operation: estimate the two potential-outcome surfaces, predict both outcomes for each unit, and average the contrast of interest. MBPO puts probability models on the outcome distributions, carries parameter and imputation uncertainty into the estimand, and makes distributional and finite-sample estimands especially convenient.
A Bayesian model combines a prior, which records plausible values before seeing the outcomes, with a likelihood, which describes how the observed outcomes arise. Their combination is the posterior distribution. A posterior draw is one plausible setting of the model parameters after conditioning on the data; repeating a calculation over many such draws propagates parameter uncertainty. A credible interval summarizes that posterior distribution directly: conditional on the model and observed data, it contains the stated posterior probability.
The posterior predictive distribution adds a second layer. For each posterior parameter draw, it simulates outcomes from the sampling model. In a posterior predictive check, those simulations are replicated observed datasets used to criticize the fitted model. In causal imputation, they are the missing potential outcomes used to complete the Science. The same machinery serves both jobs, but the targets are different and should not be confused.
NumPyro expresses these ideas with a small vocabulary:
numpyro.sample introduces an unknown quantity or a simulated outcome; passing obs= conditions that site on observed data.
numpyro.plate marks repeated observations and, later, supports minibatching.
mask selects which potential outcome contributes to the observed-data likelihood.
numpyro.deterministic records a derived, non-random quantity, such as a causal contrast.
Predictive runs the fitted model forward for replication or imputation.
Predictive is a very usefuld NumPyro abstraction that use it for prior and posterior predictive checks, and for causal imputation. In the first case, parameters are drwawn from their prior distributions. For posterior checks, we pass samples from the posterior distribution (for example, from running MCMC) and ask the model to draw samples of the observed quantities (e.g. the observed potential outcomes). We do the same for causal imputation but in this case we use the model to draw samples of the unobserved potential outcomes.
NUTS, the sampler used below, targets the posterior distribution. Diagnostics such as divergences, \widehat R, and effective sample size diagnose the sampling computation; they do not establish that the science model fits the data. JAX supplies the numerical engine. Its pseudo-random number generator is explicit, so every stochastic call receives a key and keys are split before reuse. That small inconvenience makes the random computation reproducible and easier to reason about.
Python imports
import platformimport timeimport numpy as npimport jax import jax.numpy as jnpimport jax.random as jrimport jax.tree as jtimport arviz as azimport polars as plfrom polars import col, litimport polars.selectors as csimport numpyroimport numpyro.distributions as distimport numpyro.infer as inferfrom jax.example_libraries.optimizers import exponential_decayfrom numpyro.infer.autoguide import AutoNormalfrom numpyro.infer.initialization import init_to_medianimport matplotlib.pyplot as pltimport seaborn as snsfrom tqdm.notebook import tqdm%config InlineBackend.figure_format ='retina'numpyro.set_host_device_count(4)
Plotting helpers
COLORS = {"ink": "#172033","muted": "#64748B","prior": "#7C3AED","prior_fill": "#EDE9FE","posterior": "#0F766E","posterior_fill": "#CCFBF1","control": "#4F5D95","treated": "#E76F51","sample": "#2563EB","population": "#9333EA","baseline": "#64748B","expanded": "#0F766E","reference": "#94A3B8","grid": "#E8ECF1",}sns.set_theme( context="notebook", style="ticks", font="Source Sans 3", rc={"axes.axisbelow": True,"axes.edgecolor": "#CBD5E1","axes.linewidth": 0.8,"axes.spines.top": False,"axes.spines.right": False,"axes.titleweight": "semibold","axes.titlesize": 11,"axes.titlelocation": "left","axes.titlepad": 10,"axes.labelsize": 9.5,"axes.grid": True,"axes.grid.axis": "y","grid.color": COLORS["grid"],"grid.linewidth": 0.65,"grid.alpha": 0.75,"legend.frameon": False,"legend.fontsize": 8.5,"xtick.labelsize": 8.5,"ytick.labelsize": 8.5,"figure.figsize": (6.2, 3.6),"figure.constrained_layout.use": True, },)# General plotting primitivesdef add_reference_line(ax, x=0):"""Add a quiet vertical reference line and return the axes.""" ax.axvline( x, color=COLORS["reference"], linewidth=1, linestyle=(0, (3, 3)), zorder=0, )return axdef plot_histogram(values, *, ax, color, bins=28):"""Draw a clean histogram without repeated styling arguments."""return sns.histplot( values, bins=bins, color=color, edgecolor="none", alpha=0.82, ax=ax, )def plot_step_histogram(values, *, ax, color, label=None, bins=30):"""Draw an unsmoothed density estimate with a shared, legible style."""return sns.histplot( values, bins=bins, stat="density", element="step", fill=False, color=color, linewidth=1.8, label=label, ax=ax, )def plot_predictive_ecdf( draws, observed=None,*, stage="posterior", positive=False, grid=None, ax,):"""Plot an 80% predictive ECDF envelope and optional observed ECDF.""" valid_stages = ("prior", "posterior")if stage notin valid_stages:raiseValueError(f"`stage` must be one of {valid_stages}, got {stage!r}.") draws = np.asarray(draws)if draws.ndim notin (1, 2):raiseValueError("`draws` must be a 1D sample or a 2D draws-by-unit array.")if draws.size ==0:raiseValueError("`draws` must not be empty.") draws = draws[None, :] if draws.ndim ==1else drawsif draws.shape[0] ==0or draws.shape[1] ==0:raiseValueError("`draws` must contain at least one draw and one unit.")if observed isnotNone: observed = np.asarray(observed)if observed.ndim !=1or observed.size ==0:raiseValueError("`observed` must be a non-empty 1D array.")if positive: draw_mask = draws >0if np.any(draw_mask.sum(axis=1) ==0):raiseValueError("Every predictive draw must contain a positive value when `positive=True`." ) draw_values = draws[draw_mask]if observed isnotNone: observed = observed[observed >0]if observed.size ==0:raiseValueError("`observed` must contain a positive value when `positive=True`." )else: draw_mask = np.ones_like(draws, dtype=bool) draw_values = draws.ravel()if grid isnotNone: grid = np.asarray(grid)if grid.ndim !=1or grid.size ==0:raiseValueError("`grid` must be a non-empty 1D array.")if positive and np.any(grid <=0):raiseValueError("`grid` must be strictly positive when `positive=True`.")if grid isNone: pooled = draw_values if observed isNoneelse np.concatenate([draw_values, observed]) lower, upper = np.quantile(pooled, [0.005, 0.995])if observed isnotNone: lower, upper =min(lower, observed.min()), max(upper, observed.max()) grid = ( np.geomspace(max(lower, np.finfo(float).tiny), upper, 240)if positiveelse np.linspace(lower, upper, 240) ) counts = np.sum(draw_mask, axis=1, keepdims=True) cdfs = np.sum( (draws[:, :, None] <= grid) & draw_mask[:, :, None], axis=1, ) / counts cdf_lo, cdf_mid, cdf_hi = np.quantile(cdfs, [0.10, 0.50, 0.90], axis=0) line_color = COLORS[stage] fill_color = COLORS[f"{stage}_fill"] ax.fill_between( grid, cdf_lo, cdf_hi, color=fill_color, linewidth=0, label="80% predictive envelope", ) ax.plot(grid, cdf_mid, color=line_color, linewidth=2, label="Predictive median")if observed isnotNone: sns.ecdfplot( observed, color=COLORS["ink"], linewidth=2.2, label="Observed", ax=ax, )return ax# Tutorial-specific compositionsdef plot_effect_heterogeneity(values, *, ax):"""Plot conditional effects and mark their average.""" plot_histogram(values, color=COLORS["posterior"], ax=ax) ax.axvline( np.mean(values), color=COLORS["ink"], linewidth=2, linestyle=(0, (3, 3)), label="Average conditional effect", )return axdef plot_posterior_predictive_by_arm( observed, replicated, treated,*, axes, positive=False, title=None,):"""Draw matching control and treatment posterior predictive checks.""" observed = np.asarray(observed) replicated = np.asarray(replicated) treated = np.asarray(treated, dtype=bool) axes =tuple(axes)if observed.ndim !=1or treated.ndim !=1:raiseValueError("`observed` and `treated` must be 1D arrays.")if replicated.ndim !=2:raiseValueError("`replicated` must be a 2D draws-by-unit array.")if replicated.shape[1] != observed.size or treated.size != observed.size:raiseValueError("`observed`, `treated`, and the unit dimension of `replicated` must align." )ifnot np.any(treated) or np.all(treated):raiseValueError("`treated` must contain both control and treated units.")iflen(axes) !=2:raiseValueError("`axes` must contain exactly two Matplotlib axes.")for mask, arm, ax inzip((~treated, treated), ("Control", "Treated"), axes): plot_predictive_ecdf( replicated[:, mask], observed[mask], stage="posterior", positive=positive, ax=ax, )if positive: ax.set_xscale("log") ax.set( title=arm if title isNoneelse title, xlabel="Positive earnings ($000s, log scale)"if positive else"Earnings ($000s)", ylabel="Cumulative share", ) ax.legend(title=None)return axes
The Lalonde randomized job-training experiment
The data are the experimental National Supported Work sample analyzed by LaLonde (1986). Assignment to the job-training program was randomized: 185 of the 445 workers were assigned to treatment and 260 to control. This tutorial uses the experiment itself, not the nonexperimental comparison samples that made the Lalonde study famous.
The outcome is 1978 earnings. The demographic variables and 1974 and 1975 earnings were measured before treatment. These covariates are not needed to identify the average treatment effect in this randomized experiment. They can still improve prediction and precision, describe treatment-effect heterogeneity, and make the missing-outcome imputations responsive to known predictors of earnings. In a study that instead relies on conditional ignorability, the covariates would also be part of the identification and overlap argument.
The outcome is re78; treat records randomized assignment; all other columns are pretreatment covariates. The earnings variables are rescaled to thousands of dollars in the next cell. The columns are:
age: age in years
educ: years of schooling
nodegr: indicator variable for being a high school dropout
black: indicator variable for being African American
hisp: indicator variable for being Hispanic/Latino
married: indicator variable for being now or ever before married
re74: pre-training earnings in 1974
u74: an indicator for earnings in 1974 being zero
re75: pre-training earnings in 1975
u75: an indicator for earnings in 1975 being zero
re78: post-program labor market earnings in 1978
treat: randomized assignment to the job-training program
df = ( df .with_columns(# normalize the earnings columns to thousands cs.starts_with("re").truediv(1_000) ))
Figure 1: Observed earnings in the NSW experimental sample by treatment status. Positive earnings are shown on a logarithmic scale.
Model 1: start with the simplest science
We begin with a model that we already know is wrong.
Earnings cannot be negative, their distribution is strongly right-skewed, and a substantial fraction of workers report exactly zero earnings. A Normal distribution cannot reproduce any of these features. Still, it is a useful place to start. The model is simple enough that we can see every part of the MBPO machinery, from the observed-data likelihood to the imputation of missing potential outcomes. Its failures will also give us a concrete reason to build a better model.
For now, suppose that the potential outcomes follow two Normal distributions:
This is an assumption, not something we learn from the data. As discussed in the theory section on potential-outcome dependence, we never observe Y_i(0) and Y_i(1) for the same worker, so the data identify their marginal distributions but not their association. Setting \rho=0 gives us a convenient baseline. The sensitivity appendix repeats the Model 1 imputation at other fixed values of \rho.
Earnings are measured in thousands of dollars. I use the priors
which has a standard deviation of approximately 14.1, or $14,100. The prior permits large positive and negative effects without putting much mass on earnings scales that are completely detached from the application.
Writing the science in NumPyro
The following function handles three distinct operations:
fit evaluates the observed-data likelihood.
replicate generates a new dataset under the assignment actually observed in the experiment.
impute retains the factual outcomes and fills in the missing counterfactuals.
The masks are doing something important. During fitting, both y_0 and y_1 receive the observed earnings vector. The masks determine which elements contribute to the likelihood. The y_0 site contributes
The missing potential outcomes have already been integrated out. They do not enter the NUTS state. We could write the same likelihood with a single observed distribution whose location and scale are selected with jnp.where. The two forms are algebraically equivalent. I prefer the masked version here because it keeps the two potential-outcome distributions visible in the code. That is the science we are trying to model.
Prior predictive checks
Before fitting the model, we simulate earnings distributions from the prior. In replicate mode, the model generates both potential outcomes and then applies the observed assignment:
No observed earnings enter this calculation. At this stage I am asking two fairly narrow questions: does the prior put earnings on a remotely sensible scale, and what range of treatment effects does it imply before seeing the outcomes?
As Figure 2 shows, the prior predictive distribution is broad but remains on a recognizable earnings scale. It also exposes a problem that no reasonable choice of parameters can repair: the Normal model assigns positive probability to negative earnings. This is a failure of the outcome family, not simply a failure of the prior. Tightening the priors could reduce the number of negative draws, but it cannot make the support of the model agree with the support of the outcome.
Figure 2: Prior predictive implications under the Normal model. The first panel shows the median ECDF and central 80% envelope across 100 replicated datasets; the second uses 500 prior ATE draws.
Fitting the observed-data model
We fit the model with four NUTS chains. Because the missing potential outcomes have been marginalized, the sampler needs to explore only the four-dimensional parameter vector \theta.
The sampler behaves well. There are no divergent transitions, the rank-normalized \widehat R values are essentially one, and the effective sample sizes are large relative to the number of retained draws. The posterior means are approximately
The model estimates higher average earnings under treatment, together with substantially greater dispersion. The corresponding calculation in Chapter 8 gives the same broad result: an average effect around $1,800 and posterior uncertainty around $500 for the finite-sample estimand. I use that comparison as an implementation check rather than as a separate result.
Figure 3: Each color identifies one NUTS chain. Step histograms show unsmoothed marginal distributions; trace panels show 1,000 post-warmup draws per chain.
Posterior predictive checks
Good computational diagnostics tell us that NUTS successfully sampled from the posterior we specified. They do not tell us whether that posterior arises from a useful model of earnings. For that, we generate replicated datasets using mode="replicate".
The comparison should be made separately by treatment status, since the model assigns each arm a different distribution. A density overlay is useful, but it can hide the most important failures. I also compare the fraction of workers with exactly zero earnings,
Figure 4: Black lines show observed ECDFs; teal lines and mint bands show posterior predictive medians and central 80% envelopes across 100 replicated datasets.
The diagnosis is unambiguous. The observed data contain a large point mass at zero, while a continuous Normal distribution produces zero with probability zero. The replicated datasets contain negative earnings and are symmetric, while positive earnings are strongly right-skewed. The model can reproduce the arm-specific means and variances, but not the shape of either distribution.
The distinction between a prior and a posterior failure is useful here. Before fitting, we saw that the model could generate negative earnings. After fitting, we see that the problem has not disappeared and that the model also cannot reproduce the bunching at zero or the right tail.
The next step is not to tighten the priors. It is also not obvious that adding covariates would solve the problem. Neither change would repair the support of the outcome distribution. We need to revise the science model for earnings.
Imputing the missing potential outcomes
Posterior predictive replication asks whether the fitted model can reproduce data like those we observed. Causal imputation asks a different question: what does the fitted model imply about the potential outcomes we did not observe? We keep these two operations separate in the code.
Its posterior mean is approximately 1.78, or $1,780, with a posterior standard deviation of 0.69. The central 90 percent credible interval is approximately [0.66,2.90].
For the workers in the experiment, the finite-sample average treatment effect is
Its posterior mean is approximately 1.80, with a posterior standard deviation of 0.50. The population and sample averages are similar in this application, but they are different estimands and need not agree in general.
Figure 5: Unsmoothed density histograms pool the first 10 completed potential-outcome schedules from Model 1. Negative earnings remain possible under the Normal outcome family.
We can also calculate population quantile treatment effects. For quantile q,
\tau_q^{\mathrm{pop}}=Q_q\{Y(1)\}-Q_q\{Y(0)\}.
Because the two marginal distributions are Normal,
The posterior means at the first quartile, median, and third quartile are approximately 0.17, 1.78, and 3.40. For the completed experimental sample, the corresponding posterior means are approximately 0.62, 1.64, and 3.09.
The population and finite-sample QTEs differ because the latter retain the observed outcomes, including their large point mass at zero, and impute only the missing outcomes. The increasing population QTE is driven by the larger estimated variance under treatment. It should not yet receive a strong substantive interpretation. The Normal model is attempting to explain a point-mass, right-skewed outcome through changes in only its mean and variance.
These completed schedules use the baseline assumption \rho=0. The observed data cannot select that value. The sensitivity appendix shows how the finite-sample conclusions change when the same Model 1 marginals are paired using other fixed correlations.
Figure 6: Points are posterior medians and whiskers are central 90% intervals. Finite-sample QTEs use completed potential-outcome schedules; population QTEs use the fitted Normal marginals.
What we learned from Model 1
Model 1 has done useful work. It gave us a transparent implementation of MBPO, showed how masking produces the marginalized observed-data likelihood, and separated posterior predictive replication from causal imputation. It also produced a baseline estimate of the average treatment effect.
More importantly, it failed its predictive checks in ways that are directly connected to the economics of earnings. The data contain a positive-earnings margin, represented by the point mass at zero, and a highly skewed distribution among positive earners. A Normal model has neither feature.
The natural repair is a hurdle model with one process for whether earnings are positive and another for their level conditional on being positive. Before making that change, I first add covariates while holding the Normal likelihood fixed. This controlled comparison shows what covariates can and cannot repair.
Model 2: hold the Normal likelihood fixed and add covariates
The first revision changes only the conditional means. For w\in\{0,1\},
to vary with the observed covariate profile. This is a conditional mean contrast, not a realized individual treatment effect. The realized contrast Y_i(1)-Y_i(0) still depends on two potential outcomes that are never jointly observed.
I standardize age, educ, re74, and re75, and leave the five binary indicators black, married, nodegr, u74, and u75 as 0/1. These are the nine regressors in the Chapter 8 two-part specification; the intercept is a separate parameter. The design matrix therefore has nine columns, not ten, and does not include hisp.
Averaging the conditional mean contrasts over the observed covariate profiles gives the sample-standardized estimand \tau^X from Equation 10. Imputing the missing potential outcomes gives the realized finite-sample estimand \tau^S. Keeping those calculations separate prevents a regression contrast from being mislabeled as an individual causal effect.
I first run the model in replicate mode without conditioning on the outcomes. The same targeted statistics used for Model 1 reveal what the priors and the Normal likelihood imply jointly: the share at zero, the share below zero, the 90th percentile, and skewness, all calculated separately by treatment status. I also inspect the prior distribution of \tau^X.
Adding covariates does not change the support of the model. Prior predictive datasets can still contain negative earnings, and an exactly zero outcome still has probability zero. The point of this check is therefore not to confirm that the Normal model has become realistic. It is to verify that the regression priors imply plausible conditional mean variation before fitting.
fig, ax = plt.subplots(figsize=(5.6, 3.0))plot_histogram( np.asarray(prior_samples_m2["tau_x"]), color=COLORS["prior"], bins=25, ax=ax,)add_reference_line(ax)ax.set_xlabel(r"Sample-standardized effect $\tau^X$(\$000s)")ax.set_ylabel("Draws")ax.set_title("Covariates still permit large prior effects")plt.show()
Figure 7: The sample-standardized prior effect integrates Model 2’s covariate-specific contrasts over the observed covariate distribution. The histogram contains 500 prior draws.
Fitting the observed-data model
Fitting again uses the marginalized observed-data likelihood. The only additional unknowns are the arm-specific regression coefficients. I check divergences, \widehat R, and effective sample sizes before interpreting either the regression contrasts or the imputed potential outcomes.
The computation is well behaved: there are no divergent transitions, every reported \widehat R rounds to 1.00, and the effective sample sizes are comfortably large.
I now use the fitted model twice for two different purposes. In replicate mode, it generates new observed datasets for checking the Normal regression. In impute mode, it combines the factual outcomes with simulated counterfactuals to complete the potential-outcome schedule. The first operation diagnoses the model; the second propagates its assumptions into the causal estimands.
tau_cond_m2 = np.asarray(imputations_m2["tau_cond"])fig, ax = plt.subplots(figsize=(5.6, 3.0))posterior_mean_tau_cond_m2 = tau_cond_m2.mean(axis=0)plot_effect_heterogeneity(posterior_mean_tau_cond_m2, ax=ax)ax.set_xlabel("Posterior mean conditional contrast ($000s)")ax.set_ylabel("Workers")ax.set_title("Most posterior mean conditional contrasts are positive")ax.legend(title=None)plt.show()
Figure 8: The histogram shows the distribution across workers of posterior mean conditional contrasts under Model 2. The dashed line is their average over the observed workers.
Show Normal-model ATE comparison plot
ate_sample_m1 = np.asarray(imputations_m1["tau_sample"])ate_sample_m2 = np.asarray(imputations_m2["tau_sample"])ate_comparison_bins = np.histogram_bin_edges( np.concatenate([ate_sample_m1, ate_sample_m2]), bins=32,)fig, ax = plt.subplots(figsize=(5.6, 3.0))plot_step_histogram( ate_sample_m1, label="Model 1: no covariates", color=COLORS["baseline"], bins=ate_comparison_bins, ax=ax,)plot_step_histogram( ate_sample_m2, label="Model 2: with covariates", color=COLORS["expanded"], bins=ate_comparison_bins, ax=ax,)add_reference_line(ax)ax.set_xlabel("Finite-sample ATE ($000s)")ax.set_ylabel("Density")ax.set_title("Covariates modestly lower the ATE")ax.legend(title=None)plt.show()
Figure 9: Unsmoothed density histograms compare posterior finite-sample ATE draws under the two Normal models. Both models use the same bin edges; the vertical line marks no average effect.
What covariates repaired, and what they did not
Model 2 can explain systematic differences in conditional means and can express heterogeneous conditional mean effects. The sample-standardized effect \tau^X and the realized finite-sample ATE \tau^S both have posterior means of about $1.6 thousand here. Their numerical agreement is a result, not an identity: the first averages regression contrasts over the observed covariates, while the second averages contrasts from completed potential-outcome schedules.
The posterior predictive diagnosis is more consequential. About 35% of control outcomes and 24% of treated outcomes are exactly zero, while the replicated share is always zero. The model instead puts about 21% of each arm below zero. Replicated skewness is centered near zero, compared with observed skewness of 1.82 in the control arm and 2.72 in the treated arm. Those failures are almost unchanged because they come from the Normal outcome family, not from omitted predictors.
This is the point of the controlled comparison. Covariates may improve prediction and imputation, but they cannot repair impossible support or create a point mass. The next model changes the outcome family in direct response to those predictive failures.
Model 3: a hurdle LogNormal model
Earnings have two scientifically distinct parts: whether a worker has positive earnings and, if so, how much they earn. Let E_i(w) indicate positive earnings under treatment level w. The model is
This is a hurdle model, or equivalently a point-mass mixture at zero. I avoid the looser label “zero-inflated” because zero is not in the support of the LogNormal component. The likelihood for one outcome is
During NUTS, the binary positive-earnings indicator is marginalized and the closed-form log likelihood is added with numpyro.factor. This is statistically useful, not merely a coding trick: NUTS cannot sample a discrete latent gate. In prior and posterior predictive simulation, sampling the gate directly is fine.
The priors are calibrated on the original earnings scale through prior predictive checks:
The population mean is p_w\exp(\mu_w+\sigma_w^2/2). Population quantiles come from the full mixture: a quantile is zero when its probability level lies inside the point mass, and otherwise it is the appropriately rescaled LogNormal quantile. This matters because subtracting the conditional positive-earnings quantiles would not give the marginal population QTE.
I report the population ATE \tau^{\mathrm{pop}}, its population QTEs, and the corresponding finite-sample estimands from completed schedules. I also report two margin-specific contrasts: p_1-p_0 for the probability of positive earnings and \mathbb{E}\{Y_i(1)\mid Y_i(1)>0\}-\mathbb{E}\{Y_i(0)\mid Y_i(0)>0\}. The contrasts have different units and should not be added together.
The second contrast needs a careful label. It compares treatment-specific groups of positive earners, and those groups need not contain the same workers. It is therefore not a causal effect for a common population. A principal-stratification estimand could instead compare outcomes among workers with positive earnings under both treatments, but that target depends on the joint distribution of the two potential positive-earnings indicators and requires assumptions beyond randomization. I report the marginal contrast because it describes the two-part outcome distribution, not because it answers the principal-stratum question. A future tutorial will take up that principal-stratification problem directly.
The finite-sample intervals below use the conditional cross-world independence implied by the separate control and treatment sampling sites. Other couplings with the same fitted marginals would leave the observed-data fit unchanged but could alter uncertainty in \tau^S and the finite-sample QTEs.
From the mixture to the helper functions
The two hurdle helpers below are direct translations of the mixture distribution. Let g=P(Y=0)=1-p_w be the probability of the zero gate, and let f_{\mathrm{LN}} and F_{\mathrm{LN}} denote the LogNormal density and CDF. After marginalizing the binary indicator, one observation contributes
This is the calculation in hurdle_lognormal_logp. For a positive value, the marginal CDF is
F(y)=g+(1-g)F_{\mathrm{LN}}(y).
Hamiltonian Monte Carlo relies on gradients and therefore cannot move directly through discrete latent variables. For NUTS, we marginalize the gate analytically. NumPyro can enumerate some discrete latent variables, but the closed-form marginalization here is simpler and typically more efficient.
The second helper implements this rescaling. It is what makes the reported population QTEs quantiles of the full point-mass mixture rather than quantiles conditional on positive earnings.3
The prior predictive check evaluates the two parts of the science separately. For each arm, I compare the zero share and the median, 90th percentile, and 95th percentile among positive earners. I also inspect the population ATE, the marginal population QTEs, and the two margin-specific contrasts.
Unlike the Normal models, every prior predictive outcome is nonnegative and exact zeros occur with positive probability. The observed arm-level summaries fall inside the displayed prior predictive intervals. The treatment contrasts have medians near zero, while the positive-earnings distributions remain broad enough to cover the observed upper tails. On these dimensions, the prior predictive distribution is much better matched to the application.
Fitting the observed-data model
In fit mode, the gate is marginalized and numpyro.factor adds the closed-form hurdle likelihood for the observed treatment arm. NUTS therefore samples only the six continuous parameters. In replicate and impute modes, sampling the binary positive-earnings indicators is valid because those calls run outside NUTS.
The four NUTS chains completed without divergences, and the reported \widehat R values round to 1.00. I now call the fitted model twice. Replication asks whether the hurdle model reproduces the zero shares and the distribution of positive earnings in each arm. Imputation combines observed factual outcomes with simulated counterfactuals and produces completed potential-outcome schedules. Keeping the calls separate prevents a replicated observed dataset from being mistaken for a causal completion.
Figure 10: Black lines show observed positive-earnings ECDFs; teal lines and mint bands show posterior predictive medians and central 80% envelopes across 100 Model 3 replications.
What Model 3 repaired
Model 3 directly repairs the failures that covariates could not. Replicated earnings are never negative, exact zeros have positive probability, and the positive outcomes can be right-skewed. The zero shares are reproduced closely: the posterior predictive medians are 0.358 for controls and 0.249 for treated units, compared with observed shares of 0.354 and 0.243.
The positive-earnings medians are somewhat low, and the upper tail remains too heavy. The predictive medians of the positive-earnings 95th percentile are 24.9 and 28.5, compared with observed values of 16.8 and 22.2. The hurdle fixes the impossible support and zero mass, but the simple arm-level LogNormal component does not completely reproduce the earnings tail.
The posterior mean of the population ATE \tau^{\mathrm{pop}} is 1.72, with a central 90% interval from 0.17 to 3.43. The finite-sample ATE \tau^S has posterior mean 1.88 and a central 90% interval from 0.72 to 3.13. These estimands describe different targets, so their numerical similarity is a result rather than an identity.
The average probability of positive earnings increases by 0.107, with a central 90% interval from 0.037 to 0.177. The positive-earnings conditional contrast is 1.11, with a central 90% interval from -0.91 to 3.27. The clearer signal is on the extensive margin, while the conditional contrast remains uncertain and does not describe an effect for a common positive-earner population.
Model 3 still assigns the same hurdle probability and positive-earnings distribution to every unit in a treatment arm. Model 4 keeps the repaired outcome family and asks whether pre-treatment covariates explain systematic variation in both margins.
Model 4: let both hurdle components depend on covariates
Model 3 gives every unit in an arm the same probability of positive earnings and the same distribution of earnings among workers. The final model lets both parts vary with the pre-treatment covariates \mathbf{X}_i. For w\in\{0,1\},
The employment component describes the extensive margin. The positive-earnings component describes the intensive margin. Treatment can shift either component, and the model does not force the same covariate relationships in the two treatment arms.
I reuse the nine-column design matrix from Model 2: standardized age, educ, re74, and re75, followed by the binary indicators black, married, nodegr, u74, and u75. This matches the nine covariate rows in Table 8.7 of Imbens and Rubin, in addition to an intercept. hisp is available in the data but is not part of that specification.
The priors are weakly informative on the scales created by this design matrix. A broader first pass put too much prior mass on enormous earnings because the LogNormal transformation amplifies uncertainty on the log scale. The scales below retain substantial uncertainty while keeping the prior predictive outcomes in a useful range:
The contrast m_{i1}-m_{i0} is a conditional mean treatment effect, not a realized individual effect. I call its average over the observed covariate profiles \tau^X. The realized finite-sample ATE \tau^S still comes from completed potential-outcome schedules. I also report two sample-standardized component contrasts: the difference in positive-earnings probabilities and the difference between treatment-specific means conditional on positive earnings. These contrasts have different units and are not an additive decomposition of \tau^X; the second also compares treatment-specific conditioning groups, not a common population.
Prior predictive checks
Before fitting, I check the implied zero shares, the scale and upper tail of positive earnings, and the three treatment-effect summaries. This is especially important for a LogNormal regression because modest changes on the log scale can produce very large earnings on the original scale. The observed arm-level summaries fall inside the displayed prior predictive intervals, while the treatment-effect priors remain centered near zero.
As in Model 3, fitting marginalizes the discrete positive-earnings indicators and evaluates the hurdle likelihood with numpyro.factor. NUTS therefore samples only continuous parameters. The predictive modes sample those indicators directly because they run outside NUTS. The appendix in Section 7.2 uses the same model interface with minibatches and stochastic variational inference when a full-data NUTS fit is no longer practical.
The four NUTS chains completed without divergences. I again keep replication and imputation separate. Replicated observed datasets test whether the fitted model reproduces the arm-specific zero shares and positive-earnings distributions. Completed potential-outcome schedules answer the causal questions only after that model check.
# hide-code-pdf-source: paired-with-cell-classfig_control_m4, ax_control_m4 = plt.subplots(figsize=(3.1, 2.5))fig_treated_m4, ax_treated_m4 = plt.subplots(figsize=(3.1, 2.5))plot_posterior_predictive_by_arm( y_m4, y_rep_m4[:100], w_m4, axes=(ax_control_m4, ax_treated_m4), positive=True, title="Covariates do not repair the tail",)plt.show()
(a) Control.
(b) Treated.
Figure 11: Black lines show observed positive-earnings ECDFs; teal lines and mint bands show posterior predictive medians and central 80% envelopes across 100 Model 4 replications.
Show Model 4 effect-heterogeneity plot
tau_cond_m4 = np.asarray(imputations_m4["tau_cond"])fig, ax = plt.subplots(figsize=(5.6, 3.0))posterior_mean_tau_cond_m4 = tau_cond_m4.mean(axis=0)plot_effect_heterogeneity(posterior_mean_tau_cond_m4, ax=ax)ax.set_xlabel("Posterior mean conditional contrast ($000s)")ax.set_ylabel("Workers")ax.set_title("Posterior mean conditional contrasts vary across workers")ax.legend(title=None)plt.show()
Figure 12: The histogram shows the distribution across workers of posterior mean conditional contrasts under Model 4. The dashed line is their average over the observed workers.
What Model 4 adds
Model 4 preserves the support repair from Model 3 while allowing the extensive and intensive margins to vary systematically with pre-treatment characteristics. The posterior predictive checks therefore have two jobs: verify that the hurdle still reproduces zeros and positive-earnings tails, and check whether the additional regression structure has improved those predictions rather than merely producing more coefficients.
The zero shares are reproduced closely: the posterior predictive medians are 0.358 for controls and 0.249 for treated units, compared with observed shares of 0.354 and 0.243. The positive-earnings medians are somewhat low, and the upper tail remains too heavy. For example, the predictive medians of the positive-earnings 95th percentile are 25.7 and 30.3, compared with observed values of 16.8 and 22.2. The hurdle solves the support problem, but the LogNormal component still leaves a visible tail mismatch.
The posterior mean of the sample-standardized conditional mean effect \tau^X is 1.68, with a central 90% interval from 0.00 to 3.47. The finite-sample ATE \tau^S, based on completed potential-outcome schedules, has posterior mean 1.78 and a central 90% interval from 0.55 to 3.13. The average probability of positive earnings increases by 0.104, with a central 90% interval from 0.031 to 0.172. The positive-earnings conditional contrast is 1.05, with a central 90% interval from -1.22 to 3.47. The clearer signal is on the extensive margin; the conditional contrast remains uncertain and does not describe an effect for a common positive-earner population.
The vector tau_cond contains conditional mean contrasts m_{i1}-m_{i0} for the observed covariate profiles. It does not contain realized individual treatment effects. The latter still require a joint potential-outcome schedule and remain unidentified for every unit.
Posterior parameter table: a Table 8.7 analogue
Table 8.7 in Imbens and Rubin reports the control-arm coefficients and the treated-minus-control differences for the positive-earnings coefficients \beta and the employment coefficients \gamma. The table below follows that layout. Entries are posterior means with posterior standard deviations in parentheses.
This is an analogue rather than a direct reproduction for three reasons:
age, educ, re74, and re75 are standardized here, while the book uses their raw scales.
The priors here are weakly informative and chosen through prior predictive reasoning, while Chapter 8 describes its priors only as diffuse.
The comparison checks parameter ordering, units, transformations, and signs; it does not use numerical agreement as the organizing goal.
Both implementations model P(Y(w)>0\mid X) directly, so the employment coefficients have the same sign convention.
The table below compares the same causal estimand across all four models: the finite-sample ATE \tau^S calculated from completed potential-outcome schedules. The posterior predictive verdict is a separate assessment of whether each fitted model can reproduce important features of the observed earnings data. Similar causal estimates do not imply similar predictive fit.
Table 1: A common finite-sample estimand and posterior predictive verdict across the four models.
Model
Outcome family
Covariates
ATE mean
Central 90%
PPC verdict
Model 1
Normal
No
1.80
[0.97, 2.61]
Fails: negatives, no zeros, symmetric.
Model 2
Normal
Yes
1.59
[0.78, 2.39]
Covariates do not repair those failures.
Model 3
Hurdle LogNormal
No
1.88
[0.72, 3.13]
Support fixed; positive tail too heavy.
Model 4
Hurdle LogNormal
Yes
1.78
[0.55, 3.13]
Covariates added; positive tail heavy.
The sequence matters more than the final row. Model 1 establishes a useful baseline, but its Normal outcome family generates negative earnings, cannot generate exact zeros, and misses the strong right skew. Model 2 shows what changes when we add pre-treatment covariates while holding that outcome family fixed. The covariates describe conditional means and heterogeneity, but they cannot repair a likelihood with the wrong support and shape.
Model 3 changes the outcome family in direct response to those failures. Its hurdle separates the probability of positive earnings from the amount earned when earnings are positive. This removes negative outcomes, reproduces the point mass at zero, and allows right skew. The remaining positive-earnings upper tail is still too heavy. Model 4 then lets covariates enter both hurdle components. It adds systematic heterogeneity while preserving the support repair, but the tail mismatch does not disappear.
The finite-sample ATE is fairly stable across these models. That stability is a robustness observation about this estimand, not a posterior predictive check and not evidence that the Normal models fit the outcome distribution. In both hurdle models, the clearer signal is an increase in the probability of positive earnings. The positive-earnings conditional contrast remains uncertain and does not describe a causal effect for a common population.
Finally, conditional mean contrasts such as m_{i1}-m_{i0} are not realized individual treatment effects. A realized effect requires both potential outcomes for the same unit. Completing those schedules depends on an unidentified joint potential-outcome distribution. The sensitivity appendix illustrates that dependence using Model 1.
Conclusion: where Model 4 leaves us
Model 4 is a useful pedagogical stopping point, with qualifications. It repairs the outcome support, represents exact zeros and right skew, and lets pre-treatment covariates shape both the extensive and intensive margins. The checks in Figure 11 also show why it is not an unqualified winner: its positive-earnings distribution still places too much mass in the upper tail.
The stability of the finite-sample ATE across Table 1 is reassuring for that estimand. Under Model 4 its posterior mean is about 1.8 thousand, with a central 90% interval from about 0.6 to 3.1 thousand. The clearest component signal is the average increase of about 0.10 in the probability of positive earnings. The positive-earnings conditional contrast remains uncertain. Stable ATEs do not validate every causal summary: tail-sensitive estimands, including finite-sample QTEs, remain sensitive to the outcome family.
Individual effects and probabilities of benefit require the joint distribution of the two potential outcomes, not just their fitted marginal distributions. That cross-world dependence is not identified by the observed data (Ding and Li 2018; Mealli et al. 2023). The Model 1 appendix exercise shows the same issue for posterior uncertainty in a finite-sample estimand: changing \rho alters the completed schedules even though the observed-data fit is unchanged.
The most complex model is not automatically the best model. Following the iterative Bayesian workflow of Gelman et al. (2020), the reusable lesson is to keep the scientific question, model criticism, and causal imputation in the right order:
Define the science and the assignment mechanism.
Fit only the observed-data likelihood.
Check replicated observed data against the features that matter scientifically.
Repair predictive failures with scientifically motivated structure, changing one assumption at a time when possible.
Impute missing potential outcomes only after checking the fitted model.
Report each estimand with its target population and identifying assumptions.
The immediate controlled revision would be a hurdle Gamma regression: keep the two-part structure and covariate specification, but replace the LogNormal positive-earnings component and ask whether the upper-tail check improves. If a Gamma distribution is still too restrictive, a generalized Gamma or a mixture model would be the next candidate. Each revision should go through the same prior predictive checks, computational diagnostics, posterior predictive checks, and estimand and sensitivity comparisons rather than being accepted because it is more flexible.
Beyond that model revision, the next steps are to examine prior sensitivity, carry the cross-world sensitivity analysis into the richer hurdle model, use out-of-sample predictive evaluation when it serves the scientific question, and connect the posterior causal summaries to an explicit policy decision with costs and utilities. When data size makes full MCMC impractical, the SVI scaling appendix shows how stochastic variational inference can scale the same model structure, with an explicit tradeoff in posterior fidelity.
Model 4 is where this tutorial stops, not where the workflow ends.
Appendix
Sensitivity to dependence between potential outcomes
The theory section on potential-outcome dependence explains why the observed-data likelihood identifies the marginal outcome parameters but not the association between Y_i(0) and Y_i(1). Model 1 makes the resulting sensitivity especially transparent because its bivariate Normal Science uses a single association parameter,
The main Model 1 analysis fixes \rho=0. Here I leave its fitted marginal distributions unchanged, fix \rho at three illustrative values, and repeat only the causal-imputation step using Equation 8. This is a sensitivity analysis, not an attempt to estimate \rho. Under the independent prior parameterization described in the theory section, placing a prior on \rho would simply pass that prior into the posterior.
This limitation is not repaired by moving to a more flexible likelihood. Model 1 is used here because the correlation is easy to interpret; richer marginal models still require assumptions about how the two potential outcomes are paired for the same worker.
Show the Model 1 correlation sensitivity calculation
Table 2: Model 1 sensitivity to the fixed potential-outcome correlation. Earnings are measured in thousands of dollars; QTE entries are central 90% intervals.
Correlation
ATE mean
ATE SD
QTE 25%
QTE 50%
QTE 75%
−0.50
1.79
0.39
[0.0, 1.29]
[0.95, 2.47]
[2.19, 4.03]
0.00
1.79
0.51
[0.0, 1.19]
[0.73, 2.61]
[2.03, 4.17]
0.50
1.79
0.59
[0.0, 1.24]
[0.54, 2.67]
[1.89, 4.16]
The posterior mean of the finite-sample ATE remains close to 1.79 across Table 2, but its posterior standard deviation increases with \rho in this application. The finite-sample QTE intervals also change because every value of \rho implies a different way of completing the same 445 potential-outcome pairs. There is no general monotonicity result for arbitrary finite-sample estimands: \rho changes both the conditional means and conditional variances of the missing outcomes.
By contrast, the population ATE and the population QTEs are functions of the fitted marginal distributions and remain unchanged. The sensitivity shown here is a consequence of the causal target and the missing joint information, not a computational failure.
Figure 13: The observed-data likelihood is unchanged across the three fixed correlations. Only the completion of the missing potential outcomes changes.
Scaling Model 4 with stochastic variational inference
When it is computationally feasible, NUTS is a useful reference method because it generates correlated draws that target the specified posterior rather than restricting inference to a chosen approximation family. Diagnostics such as divergences, \widehat R, and effective sample size can reveal sampling problems, but clean diagnostics do not prove that the statistical model is well specified. Its computational cost can grow quickly with the number of observations and gradient evaluations.
SVI takes a different route: it turns posterior inference into an optimization problem. We choose a tractable family q_\phi(\theta), called the guide, and tune its parameters \phi to approximate the posterior p(\theta\mid y). The guide is a computational approximation, not a second model for the data. Here AutoNormal uses a mean-field guide: after mapping constrained parameters to an unconstrained space, it represents the latent components with independent Normal factors. This is inexpensive, but it cannot represent posterior correlations and may distort marginal uncertainty.
Because \log p(y) does not depend on \phi, maximizing the ELBO is equivalent to minimizing this KL divergence within the chosen guide family. NumPyro reports a stochastic estimate of the negative ELBO, which the optimizer minimizes. Here that estimate uses one guide draw and a fresh minibatch at each step, so the raw trace is expected to be noisy. For a statistical introduction, see Blei et al. (2017); the official NumPyro SVI documentation covers the API.
A loss trace that falls and stabilizes supports a narrow conclusion: the optimizer has reached a stable region of the chosen objective. It does not show that the optimizer found the global optimum, that the guide is flexible enough, or that posterior means and intervals are accurate. Recovery against known simulation truth, repeated fits from different seeds, posterior predictive checks, and comparison with NUTS on a manageable subset probe those separate questions.
To make scale and recovery observable rather than asserted, I simulate 100,000 workers from Model 4. The four continuous covariates are standard Normal and the five indicators have probabilities 0.45, 0.35, 0.65, 0.40, and 0.35. Treatment is randomized with probability one half. The data seed is fixed, and the true parameters below are deliberately on the same standardized scale as the tutorial’s design matrix.
The subsample_size argument added to model4 changes only fitting. Inside numpyro.plate, NumPyro selects 2,048 rows and automatically scales their log likelihood to represent all 100,000 observations; there is no manual likelihood multiplier. Replication and imputation continue to use every row and reject subsampling.
I optimize the one-particle ELBO for 6,000 steps. The clipped Adam learning rate starts at 10^{-2} and halves every 2,000 steps.
Table 3: Large-data SVI configuration. The fit reports its current runtime below; that number includes JAX compilation and is not a hardware-independent benchmark.
Configuration
Value
Data and minibatches
100,000 rows; 2,048-row minibatches
Optimization
6,000 steps; one-particle ELBO; 122.88 effective data passes
Figure 14: The raw negative ELBO per observation is noisy because each update uses a fresh minibatch. The 101-step moving average falls rapidly and then stabilizes, diagnosing optimization but not posterior accuracy.
The convergence trace in Figure 14 is an optimization diagnostic: it shows that the stochastic objective has stabilized. It does not establish that the variational family accurately represents the posterior. Approximation quality is a separate question, addressed in the optional approximation checks.
For posterior draw s, define the fitted probability of positive earnings and the fitted mean among positive earners as
Materializing the fitted values for all S=1{,}000 posterior draws and all N=100{,}000 workers would require intermediate arrays with memory proportional to O(SN). Instead, the code evaluates C=50 draws at a time, immediately averages over workers, and discards the unit-level values before moving to the next chunk. Peak intermediate memory is therefore O(CN), while the retained result has only 3S entries—one value of each estimand for every posterior draw. Chunking changes the memory footprint, not the estimands.
Draw from the guide and evaluate estimands in chunks
The scaling walkthrough establishes that the optimization ran and shows how to compute the target summaries without materializing every unit-by-draw prediction. It does not establish posterior fidelity. This optional section checks the approximation in two complementary ways: recovery against the known simulation truth, and comparison with NUTS on a subset small enough for MCMC. The checking code is available in folded blocks on the website; the results remain visible in both formats.
Recovery against simulation truth
Check optimization and causal-summary recovery
assert np.isfinite(svi_loss_per_observation).all()assert ( svi_loss_per_observation[-250:].mean()< svi_loss_per_observation[:250].mean())estimand_labels_svi = {"tau_x": "Sample-standardized mean effect (tau^X)","delta_p_positive": "Change in probability of positive earnings","delta_mean_positive": "Positive-earnings conditional contrast",}recovery_rows_svi = []for name, label in estimand_labels_svi.items(): draws = estimands_svi[name] truth =float(true_estimands_svi[name]) lower, upper = np.quantile(draws, [0.05, 0.95]) recovery_rows_svi.append( {"Estimand": label,"Truth": truth,"SVI mean": np.mean(draws),"Absolute error": abs(np.mean(draws) - truth),"Central 90% interval": f"[{lower:.3f}, {upper:.3f}]","Covers truth": bool(lower <= truth <= upper), } )recovery_errors_svi = { row["Estimand"]: row["Absolute error"] for row in recovery_rows_svi}assert recovery_errors_svi[estimand_labels_svi["tau_x"]] <=0.20assert ( recovery_errors_svi[estimand_labels_svi["delta_p_positive"]] <=0.02)assert ( recovery_errors_svi[estimand_labels_svi["delta_mean_positive"]]<=0.25)svi_recovery = pl.DataFrame(recovery_rows_svi)svi_recovery.style.cols_hide(columns="Covers truth").fmt_number( columns=["Truth", "SVI mean", "Absolute error"], decimals=3)
Table 4: Recovery of three sample-standardized causal summaries in the 100,000-row simulation. Intervals are central 90% variational intervals; all three contain the true value.
Table 5: Parameter recovery by model component. Coverage is descriptive central 90% variational interval coverage across the parameters in each block.
Parameter block
Parameters
RMSE
Maximum absolute error
90% interval coverage
Positive-earnings gate intercepts
2
0.017
0.019
0.500
Positive-earnings gate slopes
18
0.018
0.048
0.778
Positive-earnings intercepts
2
0.014
0.018
0.000
Positive-earnings slopes
18
0.009
0.020
0.722
Positive-earnings scales
2
0.007
0.009
0.500
Table 4 checks whether the approximation answers the causal questions correctly in this known-data-generating process. Table 5 is deliberately more demanding: a small error in a high-dimensional coefficient block need not translate into a meaningful error in a standardized estimand, and nominal variational intervals need not have nominal frequentist coverage. A single simulated dataset makes the coverage column descriptive, not a calibration study.
A small-data comparison with NUTS
For the first 2,000 simulated workers, I fit the same observed-data likelihood twice: full-batch AutoNormal SVI and two-chain NUTS. The comparison is not intended to crown an inference algorithm from one dataset. It is a local check for a central limitation of mean-field SVI: posterior means can look plausible while uncertainty is distorted because posterior dependence is missing.
Table 6: Posterior summaries from full-batch mean-field SVI and two-chain NUTS on the same 2,000 simulated observations. Intervals are central 90% posterior intervals.
Table 7: Runtime and NUTS diagnostics for the 2,000-row approximation check. Times include JAX compilation and are not hardware-independent benchmarks.
Item
Value
Full-batch SVI comparison
2,000 rows; 6,000 full-data steps; 1.7 seconds
Two-chain NUTS comparison
2,000 rows; 1,000 warmup + 1,000 draws per chain; 4.6 seconds; 0 divergences; maximum R-hat 1.01; minimum bulk ESS 1269
Comparison seed
20260734
In Table 6, an SVI/NUTS posterior-SD ratio of one means equal posterior spread; values below or above one indicate that SVI is narrower or wider, respectively. Table 7 reports the local runtimes and NUTS diagnostics for this 2,000-row check. These times include compilation and are not portable speed claims. The comparison seed is 20260734, split into independent keys for full-batch SVI draws and NUTS.
The practical lesson is not to replace MCMC automatically. SVI trades posterior fidelity for scale. A stable ELBO diagnoses the optimization run, while recovery against known truth and comparison with NUTS probe different approximation errors. For a real large dataset, I would also repeat the optimization from several seeds, consider a richer guide when posterior dependence matters, run posterior predictive checks, and validate the summaries that drive the substantive decision.
References
References
Blei, David M., Alp Kucukelbir, and Jon D. McAuliffe. 2017. “Variational Inference: A Review for Statisticians.”Journal of the American Statistical Association 112 (518): 859–77. https://doi.org/10.1080/01621459.2017.1285773.
Ding, Peng, and Fan Li. 2018. “Causal Inference.”Statistical Science 33 (2): 214–37.
Gelman, Andrew, Aki Vehtari, Daniel Simpson, et al. 2020. “Bayesian Workflow.”arXiv Preprint arXiv:2011.01808, ahead of print. https://doi.org/10.48550/arXiv.2011.01808.
Imbens, Guido W, and Donald B Rubin. 2015. Causal Inference in Statistics, Social, and Biomedical Sciences. Cambridge University Press.
LaLonde, Robert J. 1986. “Evaluating the Econometric Evaluations of Training Programs with Experimental Data.”The American Economic Review, 604–20.
Lee, Joon-Ho, Avi Feller, and Sophia Rabe-Hesketh. 2018. Model-Based Inference for Causal Effects in Completely Randomized Experiments.
Mealli, F., Peng Ding, and Fan-qun Li. 2023. “Bayesian Causal Inference: A Critical Review.”Philosophical Transactions of the Royal Society A: Mathematical, Physical and Engineering Sciences 381 (2247): 20220153–53. https://doi.org/10.1098/rsta.2022.0153.
Rubin, Donald B. 1978. “Bayesian Inference for Causal Effects: The Role of Randomization.”The Annals of Statistics, 34–58.
Rubin, Donald B. 1974. “Estimating Causal Effects of Treatments in Randomized and Nonrandomized Studies.”Journal of Educational Psychology 66 (5): 688–701. https://doi.org/10.1037/h0037350.
Footnotes
Domain knowledge is not the opposite of objectivity. Every model encodes substantive assumptions, whether its author acknowledges them or not. I prefer to make those assumptions visible so that they can be criticized, tested where possible, and improved.↩︎
A nonparametric model is not an assumption-free model. Because we never observe both potential outcomes for the same unit, their joint dependence is generally not identified from the data. Inference about unit-level effects and other features of their joint distribution therefore requires additional assumptions or sensitivity analysis.↩︎
safe_y prevents vectorized code from evaluating a LogNormal density at nonpositive values in branches that will be discarded. The clipping keeps the inverse CDF away from its numerical endpoints at zero and one. Neither operation changes the mixture identity above.↩︎