Prediction contract for causal impact#
CausalPy calculates Bayesian causal impact as the difference between observed outcomes and a counterfactual expected outcome in observed outcome units. Experiments call the model adapter’s predict(), which extracts posterior_predictive["mu"], and subtract that from the observed y. That subtraction only has a causal interpretation when mu is the conditional expectation :math:E[Y \mid \text{parameters}, \text{covariates}] on the response scale, excluding observation-level noise.
Gaussian models with an identity link make the linear predictor, conditional expectation, and outcome scale look interchangeable. For generalized linear models they are not. With a Poisson log link, the linear predictor :math:\eta is on the log-rate scale while counts live on the outcome scale; CausalPy needs :math:\exp(\eta) = E[Y \mid \cdot] in mu, not :math:\eta itself. The same principle applies to Bernoulli/logit models, where mu must be a probability in :math:(0, 1).
Three prediction quantities#
Quantity |
Typical name in CausalPy |
Scale |
Includes observation noise? |
Used for impact? |
|---|---|---|---|---|
Linear predictor / latent state |
model-specific (e.g. |
Link scale |
No |
No |
Conditional expected outcome |
|
Outcome / response scale |
No |
Yes |
Posterior predictive draw |
|
Outcome scale |
Yes |
No (by default) |
Impact uses the middle row: draw-level contrasts y - mu are computed in outcome units, then summarized. Plots and effect summaries inherit this convention unless a method documents a different estimand (for example link-scale regression coefficients).
Posterior transformations for nonlinear models must be applied draw by draw before contrasts or averages. In general, inverse_link(mean(eta)) differs from mean(inverse_link(eta)); g-computation averages unit-level potential outcomes on the response scale. See Estimands in CausalPy for how this relates to empirical estimands. A worked tutorial notebook is tracked in CausalPy#1017 <https://github.com/pymc-labs/CausalPy/issues/1017>_.
Backend obligations#
Every Bayesian backend that participates in impact calculation must expose outcome-scale conditional expectations through mu in the object returned by predict():
Backend |
|
Notes |
|---|---|---|
Native :class: |
|
Identity-link Gaussian models satisfy the contract by construction. Custom subclasses must inverse-link before naming the node |
:class: |
|
Gaussian observation model; |
:class: |
Upstream |
CausalPy treats these as outcome-scale expectations. For linked GLMs, pass the inverse-linked expectation as the latent until upstream exposes a dedicated expected-observation output ( |
scikit-learn adapters |
Point predictions on the outcome scale |
OLS backends return fitted values in |
sklearn backends compute impact as y_true - y_pred with the same outcome-scale requirement.
Score container#
Every model adapter returns scores in one canonical pandas.Series. Each treated unit has a required unit_{i}_r2 entry in treated-unit order. Bayesian backends additionally provide unit_{i}_r2_std when posterior draws supply dispersion; point-estimate backends omit the standard-deviation entry rather than inventing uncertainty.
For multi-output sklearn models, CausalPy computes one :math:R^2 per output instead of exposing sklearn’s default uniform average. The shared container does not make the statistics identical: PyMC reports Bayesian :math:R^2 and its posterior dispersion, while sklearn reports classical point-estimate :math:R^2. It only guarantees that consumers can locate each unit’s score without knowing which backend produced it.
This is a user-visible change for sklearn experiments: experiment.score is a keyed pandas.Series rather than a bare float. Single-output users should read experiment.score["unit_0_r2"].
For direct adapter callers, SklearnModelAdapter.score() keyword arguments follow :func:sklearn.metrics.r2_score (for example sample_weight), not the underlying estimator’s score method. multioutput is fixed to "raw_values" so each treated unit keeps its own unit_{i}_r2 entry.
Canonical coefficient container#
The model adapter’s coefficients() method preserves coefficient uncertainty in a canonical :class:xarray.DataArray named "coefficients". Every supported backend returns dimensions ("chain", "draw", "coeffs") in that order, with an optional trailing "treated_units" dimension when the model fits a separate coefficient vector per outcome. The coeffs coordinate contains the fitted design-matrix labels; treated_units contains the corresponding outcome labels.
PyMC adapters return all posterior draws, normalizing the built-in model families’ native coefficient names and label dimensions at the adapter boundary. sklearn adapters embed fitted point estimates with singleton chain and draw dimensions. Consumers therefore use the same xarray operations for every backend: point estimates are coefficients.mean(("chain", "draw")), while intervals are only meaningful when coefficients.sizes["chain"] * coefficients.sizes["draw"] > 1.
Coefficients remain on their model’s parameter scale; unlike the prediction contract, CausalPy does not inverse-link them to the response scale. Models without design-matrix coefficients, including the pymc-forecast adapter, raise NotImplementedError. Instrumental variables also retain a dedicated two-stage summary because its treatment and outcome equations are distinct coefficient surfaces rather than interchangeable backend containers.
Public name and compatibility#
The public posterior predictive name remains mu for backward compatibility. The contract is semantic, not lexical: mu must denote the conditional expected outcome in observed units. A future explicit name such as expected_outcome may be added in a minor release once all built-in backends are audited; until then, custom model authors should treat mu as that quantity.