Skip to content

0.23.0 (2026-08-04)

Latest

Choose a tag to compare

@talgalili talgalili released this 04 Aug 11:42

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 (fitpredictset_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 side

See 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_columns is renamed to outcomes_hat_columns — the
    parameter on Sample.from_frame / SampleFrame.from_frame /
    SampleFrame._create and the SampleFrame.predicted_outcome_columns property
    (internal _column_roles key "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
    BalanceDFOutcomesHat view, and an outcomes_hat() factory on SampleFrame,
    BalanceFrame, and Sample. Ŷ columns are excluded from covariates and the
    role is preserved across all frame conversions. Predicted columns follow a
    <outcome>_hat convention — other names log a warning (they do not raise), and
    from_frame warns when an undeclared _hat column would be inferred as a
    covariate.

  • balance.outcome_models — pure fit/predict primitives. New package
    parallel to weighting_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 a HistGradientBoosting{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_weight must be one-dimensional, finite, and strictly positive;
    categorical handling uses native categoricals on scikit-learn >= 1.4 and falls
    back to one-hot + StandardScaler otherwise.

  • weighted_r2 — new
    balance.stats_and_plots.weighted_stats.weighted_r2(y_true, y_pred, w=None)
    returns the weighted 1 - 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 the perf field of the outcome
    models.

  • Frame-level fit_outcome_model / predict_outcomes / fit_predict_outcomes
    on SampleFrame, BalanceFrame, and Sample — all keyword-only, with the
    model exposed on a new read-only outcome_model property. Behaviour worth
    knowing before you upgrade:

    • The fit is unweighted by default (weighted=False); pass weighted=True
      to use the active weights (TypeError if the estimator's fit does not
      accept sample_weight).
    • fit_outcome_model() does not persist predictions (like sklearn's fit);
      predict_outcomes() does. Re-fitting drops stale <outcome>_hat columns.
    • On a BalanceFrame these delegate to the responder, so the model survives
      adjust()
      — closing a gap where it was previously lost — and is preserved
      across set_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_columns that 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) or na_action="drop".
  • μ̂_OM with a bootstrap CI, and μ̂_DR via aipw().

    • outcomes_hat().mean() on a target-backed BalanceFrame is the g-computation
      estimate. It raises (pointing at predict_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 to ci_method="bootstrap", which captures the
      outcome-model uncertainty the analytic interval ignores; it is deterministic
      given random_seed and requires a BalanceFrame with a target and a fitted
      model
      . On a lone SampleFrame or a target-less Sample it raises unless you
      pass ci_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, and adjust()-calibrated responder weights whose total matches the
      target-weight total within relative tolerance 1e-6, so call adjust(...)
      before aipw() and do not rescale its output. It warns when the responder
      weights are constant (μ̂_DR reduces to μ̂_OM). Point estimate only — no
      CI yet; see the TODOs in balance/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 μ̂_IPW and its
    analytic CI, plus μ̂_OM and μ̂_DR as 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) — μ̂_OM vs μ̂_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
    poststratification model_glance output.

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 μ̂_DR preconditions.
  • R-oracle cross-checks (test_outcome_model_vs_r.py, test_aipw_vs_r.py)
    validate μ̂_OM and μ̂_DR against base-R lm, mirroring the CBPS-vs-R
    precedent. Fixtures in balance/datasets/sim_data_{outcome_model,aipw}*.csv,
    generators in tests/r_oracles/.
  • Summary-helper regression coverage for rake and poststratification diagnostics.

Contributors

@talgalili, @neuralsorcerer