Highlights
This release adds outcome modelling to balance. Until now balance answered
"how do I reweight my sample to look like the target population?"; it now also
answers "what is the population mean of my outcome?", with three estimators you
can compare side by side:
| estimator | call | consistent if |
|---|---|---|
μ̂_IPW |
outcomes().mean() |
the weighting model is correct |
μ̂_OM (g-computation) |
outcomes_hat().mean() |
the outcome model is correct |
μ̂_DR (AIPW) |
aipw() |
either one is correct (doubly robust) |
The API mirrors the existing weighting axis (fit → predict → set_fitted_*),
and the fitted model is stored on the responder, so it survives adjust() and
set_target():
bf = sample.adjust(method="ipw").set_target(target)
bf.fit_outcome_model() # fit ĝ on the responders (model="auto")
bf.predict_outcomes(on="target") # score the target -> "<outcome>_hat" columns
bf.outcomes_hat().mean() # μ̂_OM
bf.aipw() # μ̂_DR
bf.summary() # all three, side by sideSee the outcome-model tutorial
for the end-to-end walkthrough (including bootstrap CIs and train/holdout
transfer), and
architecture_0_23_0.md
for the design rationale.
Breaking Changes
predicted_outcome_columnsis renamed tooutcomes_hat_columns— the
parameter onSample.from_frame/SampleFrame.from_frame/
SampleFrame._createand theSampleFrame.predicted_outcome_columnsproperty
(internal_column_roleskey"predicted"→"outcomes_hat"). Removed
outright with no alias.
Migration: rename the argument if you passed it explicitly. Nothing else is
needed, and no deprecation cycle was run, because the role was reserved
scaffolding added in 0.19.0 that was never populated or consumed.
New Features
-
outcomes_hat(Ŷ) is now a first-class column role with the same accessor
and view machinery as covariates, weights, and outcomes:
add_outcomes_hat_column(),df_outcomes_hat,outcomes_hat_columns, a new
BalanceDFOutcomesHatview, and anoutcomes_hat()factory onSampleFrame,
BalanceFrame, andSample. Ŷ columns are excluded from covariates and the
role is preserved across all frame conversions. Predicted columns follow a
<outcome>_hatconvention — other names log a warning (they do not raise), and
from_framewarns when an undeclared_hatcolumn would be inferred as a
covariate. -
balance.outcome_models— pure fit/predict primitives. New package
parallel toweighting_methods/, mirroring the IPW fit-store-replay
conventions.fit_outcome_model()fits a regressor (continuous outcome) or
classifier (binary outcome) per outcome column and returns a stored model dict;
predict_outcome()replays the frozen preprocessing on new covariates.
model="auto"picks aHistGradientBoosting{Regressor,Classifier}and is
pluggable (a single estimator, a{"_discrete", "_continuous"}type map, or a
per-column map). Notable constraints:na_action="drop"is rejected;
sample_weightmust be one-dimensional, finite, and strictly positive;
categorical handling uses native categoricals on scikit-learn >= 1.4 and falls
back to one-hot +StandardScalerotherwise. -
weighted_r2— new
balance.stats_and_plots.weighted_stats.weighted_r2(y_true, y_pred, w=None)
returns the weighted1 - SS_res / SS_tot. Unweighted, it matches
sklearn.metrics.r2_score; NaN/inf rows are dropped (mutual-NA handling, like
the other weighted-stats helpers). It powers theperffield of the outcome
models. -
Frame-level
fit_outcome_model/predict_outcomes/fit_predict_outcomes
onSampleFrame,BalanceFrame, andSample— all keyword-only, with the
model exposed on a new read-onlyoutcome_modelproperty. Behaviour worth
knowing before you upgrade:- The fit is unweighted by default (
weighted=False); passweighted=True
to use the active weights (TypeErrorif the estimator'sfitdoes not
acceptsample_weight). fit_outcome_model()does not persist predictions (like sklearn'sfit);
predict_outcomes()does. Re-fitting drops stale<outcome>_hatcolumns.- On a
BalanceFramethese delegate to the responder, so the model survives
adjust()— closing a gap where it was previously lost — and is preserved
acrossset_target().predict_outcomes(on="target")deep-copies the
target before writing, so a caller's target is never mutated in place. keep_only_some_rows_columnsthat drops responder rows invalidates the
stored model; a column-only filter keeps it.set_fitted_outcome_model(fitted)applies an already-fitted model to a
holdout frame with the same covariate schema (the counterpart to
set_fitted_model). Nothing is re-fit — estimators are shared by identity. It
raises on schema mismatch or on models fit with non-deterministic
transformations (quantize/fct_lump) orna_action="drop".
- The fit is unweighted by default (
-
μ̂_OMwith a bootstrap CI, andμ̂_DRviaaipw().outcomes_hat().mean()on a target-backedBalanceFrameis the g-computation
estimate. It raises (pointing atpredict_outcomes(on="target")) when a
model is fit but the target is unpopulated, rather than silently returning the
responder's in-sample mean.mean_with_ci()defaults toci_method="bootstrap", which captures the
outcome-model uncertainty the analytic interval ignores; it is deterministic
givenrandom_seedand requires aBalanceFramewith a target and a fitted
model. On a loneSampleFrameor a target-lessSampleit raises unless you
passci_method="analytic".BalanceFrame.aipw()returns the doubly-robust estimate per outcome —
consistent if either model is correct. It requires a fitted outcome model, a
target, andadjust()-calibrated responder weights whose total matches the
target-weight total within relative tolerance1e-6, so calladjust(...)
beforeaipw()and do not rescale its output. It warns when the responder
weights are constant (μ̂_DRreduces toμ̂_OM). Point estimate only — no
CI yet; see the TODOs inbalance/outcome_models/aipw.py.
-
summary()reports the estimator trio when a fitted outcome model and a
target are both present: an "Outcome estimates" section withμ̂_IPWand its
analytic CI, plusμ̂_OMandμ̂_DRas point estimates. With no outcome model
fit,summary()output is unchanged. Separately,
outcomes_hat().summary()scopes any doubly-robust claim to the fit weights
— a linear learner with an intercept fit with non-uniform weights reports
"doubly robust w.r.t. weights <col>"; everything else reports plain
"g-computation (not doubly robust)". -
Diagnostics: compact model-glance rows for rake and poststratification
adjustments, covering rake convergence metadata and persisted poststratification
matching-cell metadata when available.
Documentation
- New outcome-model tutorial
(tutorials/balance_outcome_model.ipynb) —μ̂_OMvsμ̂_IPW, bootstrap CIs, and
train/holdout transfer. - New design doc:
architecture_0_23_0.md. - Docstring, statistical-method, and notebook examples for the new rake and
poststratificationmodel_glanceoutput.
Tests
- New outcome-model and AIPW suites (
test_outcome_model.py,test_aipw.py,
test_outcome_model_oracle.py) covering the primitives, the frame-level API,
the bootstrap CI, and theμ̂_DRpreconditions. - R-oracle cross-checks (
test_outcome_model_vs_r.py,test_aipw_vs_r.py)
validateμ̂_OMandμ̂_DRagainst base-Rlm, mirroring the CBPS-vs-R
precedent. Fixtures inbalance/datasets/sim_data_{outcome_model,aipw}*.csv,
generators intests/r_oracles/. - Summary-helper regression coverage for rake and poststratification diagnostics.