refactor: bundle experiment attributes into shared design/result types#966
refactor: bundle experiment attributes into shared design/result types#966drbenvincent wants to merge 8 commits into
Conversation
Collapse scattered per-class state (patsy design info, pre/post impacts, prediction scenarios, IV calibration) into _design.py and _results.py so reporting and future formula-engine swaps have one seam. Co-authored-by: Cursor <[email protected]>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
@BugBot review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9b0a715. Configure here.
Point ITS/SC/SDiD notebook examples at result.result.impact_post and related fields. Document local-first verification in AGENTS.md (CI as safety net; see #967). Co-authored-by: Cursor <[email protected]>
Use eval_env=1 in build_patsy_design so step/ramp resolve from the experiment module. Fix RD warning typo and SC multi-unit score assertion on result.result. Co-authored-by: Cursor <[email protected]>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #966 +/- ##
==========================================
+ Coverage 95.32% 95.44% +0.11%
==========================================
Files 96 99 +3
Lines 15221 15352 +131
Branches 878 885 +7
==========================================
+ Hits 14510 14652 +142
+ Misses 502 494 -8
+ Partials 209 206 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Add targeted tests for refactor edge paths, remove unused build_patsy_predictors, raise diff-cover to 97%, and document that prek does not run the patch gate. Co-authored-by: Cursor <[email protected]>
daimon-pymclabs
left a comment
There was a problem hiding this comment.
Review — bundling experiment attributes
Solid, well-structured refactor. All 16 checks are green (176 tests across 3.11/3.14, all notebook jobs execute, RTD builds, patch coverage holds). A cross-file caller trace confirms every internal read site — reporting.py, checks/placebo_in_time.py, the plotting methods — matches the new CausalResult / Scenario / PriorCalibration field names, so there's no internal breakage. The DesignTransform abstraction is a clean way to keep patsy off every formula experiment.
Per maintainer guidance, the public-API breakage (removing pre_pred/post_impact/ols_beta_params/etc. in favour of experiment.result.* and _prior_calibration.*) is acceptable pre-1.0 and is not a blocker. Worth a rename table in the release notes so users hit something searchable. Two concrete items below; I'm handling both in follow-up commits on this branch.
1. Leaked internal codename in shipped comment — causalpy/experiments/_design.py:118
# ponytail: eval_env=1 resolves custom transforms (step/ramp) from the caller's module.# ponytail: is an agent/tool codename, not appropriate for library source. The comment content is correct and useful — just the prefix needs dropping. (Same class of accidental leak as #953.)
2. Affected notebooks have stale outputs
The code cells were updated to the new API, but most notebooks were not re-executed — outputs still reflect the old-API run. its_lift_test.ipynb was re-executed (outputs refreshed); these were not: sc_pymc, iv_pymc, iv_weak_instruments, sdid_pymc, its_placebo_in_time_analysis, geolift1, multi_cell_geolift (code edited, 0 output changes). CI executes notebooks but discards outputs, so the stale committed outputs slip through. Re-executing these with runner.py --full and committing the refreshed outputs.
Minor (non-blocking)
_design.py:100—transform_ycallsbuild_design_matrices([y_design_info, x_design_info], …)and discards the X result, rebuilding the full predictor matrix on every outcome rebuild. Pass[self._y_design_info]only._results.py:40—impact_post_cumulativeis a stored field always derived fromimpact_post; a@propertywould prevent the two drifting apart.
Not this PR (pre-existing)
docs/dev/its_pymc copy.ipynbstill referencesresult.post_impact, but it's a stray "copy" scratch file already onmain, outside the built docs. Worth deleting in a separate cleanup.
daimon-pymclabs
left a comment
There was a problem hiding this comment.
Correction to my note above — to be clear, the two action items below are requests for the PR author, not things I'm taking on:
1. Drop the # ponytail: prefix on _design.py:118 (keep the comment text).
2. Re-execute the affected notebooks locally with real sampling and commit the refreshed outputs in this PR. The code cells were migrated to the new API but most outputs are stale (only its_lift_test was re-run). Stale set — code edited, outputs unchanged:
sc_pymc, iv_pymc, iv_weak_instruments, sdid_pymc, its_placebo_in_time_analysis, geolift1, multi_cell_geolift.
Two things to watch when doing this:
iv_pymcandiv_weak_instrumentsare inscripts/run_notebooks/skip_notebooks.yml, so CI never executes them — and they carry the_prior_calibrationedits. They're the highest-risk notebooks and need a manual full run (JAX/numpyro env) to confirm the new IV API actually works end-to-end.- CI's notebook job runs with mocked
pm.sample(the runner injects a mock unless--full), so a green CI does not mean the new-API cells executed against real output. Userunner.py --full(notemaketablesmust be installed for several notebooks).
Drop accidental ponytail comment prefix, rebuild outcomes without redundant X design pass, and make runner --full tolerate long MCMC silence plus skip-list notebooks for local IV refresh. Co-authored-by: Cursor <[email protected]>
IV example notebooks use numpyro sampling and JAX posterior predictive paths; document the dependency for local runner.py --full refreshes. Co-authored-by: Cursor <[email protected]>
JAX and ipykernel can leave non-daemon threads running after papermill finishes, which blocks clean interpreter shutdown and makes conda run appear hung even after the success log line. Co-authored-by: Cursor <[email protected]>
Re-execute sc_pymc, iv_pymc, iv_weak_instruments, sdid_pymc, geolift1, and multi_cell_geolift with runner.py --full so committed outputs match the bundled result API. Co-authored-by: Cursor <[email protected]>
|
I like the direction of pulling the Patsy rebuild logic into If we really may support multiple formula engines later, I’d suggest aiming for a compiled-formula boundary rather than a generic transform protocol. Right now there is only one implementation, and the protocol is still mostly exposing a thin wrapper around Patsy-specific compiled state. That gives us extra abstraction today without yet giving us a clean swap point tomorrow. Concretely, I think the simpler shape would be something like: compiled = compile_formula(formula, data)
X, y = compiled.fit_matrices()
X_new = compiled.transform_x(new_data)
y_new = compiled.transform_y(new_data)
labels = compiled.labels
outcome_name = compiled.outcome_nameBenefits of that shape:
So my suggestion would be: keep the extraction, but narrow/reframe it. |
|
Agreed — I think you've correctly identified the smell. As it stands Your compiled = compile_formula(formula, data)
X, y = compiled.fit_matrices()That's a single experiment-facing contract, and the backend choice collapses to the factory — which is the real swap point, not a generic transform protocol. +1 to "keep the extraction, narrow/reframe it." One wrinkle to design against up front: (To be clear, this is just review input — happy for you to drive the reshape.) |
Summary
This refactor tackles the experiment-class “god object” attribute sprawl identified in the attribute audit. It does not aim to shrink line count — the PR is ~+330 net LOC (new
_design.py/_results.pymodules, guards, and test churn) — but it does collapse ~50 scattered instance attributes into a small set of shared bundles.Core changes:
DesignTransform/PatsyDesignTransform(_design.py): patsyDesignInfois no longer stored on every formula experiment; rebuilds go throughtransform_x/transform_y. Future patsy → formulaic swap is a one-file change.CausalResult(_results.py): ITS, Synthetic Control, SDiD, and PiecewiseITS expose pre/post predictions, impacts, and score viaself.resultinstead of six duplicate top-level attrs.reporting.pyand placebo checks read this typed contract.intervention_pred,post_intervention_impact, etc.) become optionalintervention_result/post_intervention_result(CausalResult | None).Scenario: DiD/RD/RK prediction (input, output) pairs bundled for internal plotting.PriorCalibration: IV OLS first-stage/second-stage/naive fits moved off the public surface to_prior_calibration(publicpriorsunchanged).*_rawattrs: constructor scratch matrices are locals, notself+del.Attribute wins (approximate): ~50 duplicated/scattered attrs → ~15 bundled fields + 4 shared types; largest single-class gain is ITS (~14 fewer attrs in three-period mode, stable object shape).
Migration (pre-1.0 attribute renames)
pre_predresult.predictions_prepost_predresult.predictions_postpre_impactresult.impact_prepost_impactresult.impact_postpost_impact_cumulativeresult.impact_post_cumulativescore(top-level)result.scoreintervention_pred/intervention_impact/ …intervention_result.*post_intervention_pred/post_intervention_impact/ …post_intervention_result.*ols_beta_params,first_stage_reg,second_stage_reg,naive_reg_prior_calibration.beta_*,_prior_calibration.first_stage, …(inputs, output)pairs_control,_treatment,_counterfactual(Scenario)Public
priorson IV is unchanged.experiment.resultis the main entry point for counterfactual outputs on time-series experiments.Test plan
make test-patch-cov(176 tests)prek runon changed files (ruff, mypy, notebook validation)# ponytail:comment leak;transform_yonly rebuilds outcome design;runner.py --fullIOPub timeout + includeskip_notebooks.ymlentries for local output refreshrunner.py --fulland commit refreshed outputs (sc_pymc,iv_pymc,iv_weak_instruments,sdid_pymc,its_placebo_in_time_analysis,geolift1,multi_cell_geolift)Made with Cursor