feat(v4): ImputationDiD + TwoStageDiD post-fit aggregate() via panel-backed kits (2(b) PR-3b, M-021/M-022/M-118/M-119) - #749
Conversation
…backed kits (2(b) PR-3b, M-021/M-022/M-118/M-119)
Deprecates fit(aggregate=, balance_e=) on both estimators (3.9, removed
4.0) in favor of post-fit results.aggregate('event_study'/'group'/
'simple', balance_e=) on the shared M-122 mixin contract - the
CS/EfficientDiD register: a plain fit() never warns; supplying ANY
value (None included) warns once via the shared NOT_SUPPLIED sentinel;
the deprecated path returns exactly the numbers it always did; the
imputation_did/two_stage_did wrappers forward the sentinel so plain
wrapper calls never warn.
Both estimators are PANEL-BACKED recomputers (no compact influence
payload can honor a different balance_e): the aggregation + variance
engines move verbatim into new import-leaf modules
imputation_aggregation.py / two_stage_aggregation.py (imputation.py
2961 -> 1491 lines; two_stage.py 3551 -> 2430, exiting the splitting
band), inherited by the estimators (fit-time byte-identical) and hosted
post-fit by throwaway kit aggregators populated exclusively from kit
snapshots. ImputationDiD's kit references the SAME per-fit objects
_fit_data already retains for pretrend_test() - zero marginal memory,
pickles +392 B via memoization. TwoStageDiD's kit is the first panel
retention on its results: a deduplicated column-subset copy of the
working frame (documented memory-contract change; +170 KB plain /
+434 KB replicate on the 3,000-row benchmark; store_kit opt-out
tracked in DEFERRED.md).
Inertness gate: post-fit output equals the fit-time surfaces at 1e-14
across plain/covariate/cluster (incl. cluster=unit-column)/
anticipation/horizon_max/pretrends/Prop-5/survey-TSL/always-treated-
pad/replicate (healthy, dropped, undefined-df) fits and balance_e
{0,1,2}, with replicate arms LEVEL-MATCHED: compute_replicate_refit_
variance validates replicates jointly, so aggregate(L) reproduces
fit(aggregate=L) exactly and the deprecated fit(aggregate=)'s coupling
of the OVERALL row to the joint stack is a documented migration delta
on degenerate designs (pinned by contract tests). TwoStage post-fit
event-study containers reproduce the M-092 contract exactly (joint
GMM vcov + index + df analytical; vcov=None + replayed df replicate).
Bootstrapped fits fail closed at every level including 'simple'
(uniform CS/EDiD parity; replay tracked in TODO.md); a fit whose
bootstrap FAILED aggregates normally. Imputation pretrends+replicate
post-fit ES raises the same NotImplementedError the fit-time gate
raises.
Behavior deltas beyond the shim: (1) ImputationDiD's M-127
df_convention inert-config warning predicate is now REACHABILITY-based
(post-fit aggregate('event_study') reaches the pretrends lead
inference on analytical fits); (2) fixed a pre-existing TwoStageDiD
corner where an event study with EVERY non-reference horizon
Proposition-5-unidentified returned a reference-only surface, dropping
the all-NaN Prop-5 rows and their warning. Container admission into
honest/pretrends is NOT widened: ImputationDiD rejected BY DESIGN (no
joint ES covariance); TwoStageDiD DEFERRED pending a normalization
derivation (pre-period coefficients are stage-1 residual means while
HonestDiD hard-codes delta_0=0) - both TypeErrors state the grounds;
DEFERRED.md carries the paper-gated row.
Ledger: M-021/M-022 flip to shimmed; new rows M-118/M-119 claim the
reserved balance_e slots (balanced-window rule documented, divergent
from CS/EDiD's anchor-horizon rule); M-092 qualifier extends to
ImputationDiD; M-093 fourth amendment; M-127 amendment; floor 118 ->
120. Tests: two new contract-test blocks (~70 tests incl. wrapper
balance_e pins, the all-Prop-5 regression, covariate arms), consumer
rejection classes on REAL containers, practitioner/guides pins,
monkeypatch-target repoints for the moved solver globals (spillover
sites deliberately untouched), and a 199-site deprecated-call-site
sweep via message-scoped filterwarnings marks. Docs: llms x3, README
step-7 line, practitioner handlers, api rst + stubs, REGISTRY notes
(a)-(e) for both estimators + M-127 amendment, variance-conventions,
CHANGELOG, dev-status, doc-deps, TODO/DEFERRED rows, citation
retargets (re-derived, CHANGELOG history exempt); tutorials 11/12/15
migrated to post-fit and re-executed.
Overall assessment✅ Looks good — no unmitigated P0 or P1 findings. Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
…it-identity (CI matrix) The healthy-design arm of test_replicate_overall_row_migration_delta (both estimator twins) asserted BIT-identical overall_se between a plain fit and a fit(aggregate='group') fit. The two replicate-variance computations route their matmul through different BLAS kernel shapes ([overall] vs [overall, groups] stacked layouts), which reassociates at ~1 ULP - bit-identical on macOS/Accelerate, not on OpenBLAS-ARM (ubuntu-24.04-arm py3.11/3.13/3.14) or Windows (py3.13/3.14). Compare at rtol=1e-12: a REAL replicate-drop coupling delta is O(se) itself, far above the band, so the pin's discriminating power is unchanged.
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment✅ Looks good — no unmitigated P0 or P1 findings. Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Summary
fit(aggregate=, balance_e=)on ImputationDiD and TwoStageDiD (3.9, removed 4.0; ledger rows M-021/M-022 flip to shimmed, new rows M-118/M-119 claim the reserved balance_e slots) in favor of post-fitresults.aggregate('event_study'/'group'/'simple', balance_e=)on the shared mixin contract - the CS/EfficientDiD register: a plainfit()never warns; supplying ANY value (None included) warns once; the deprecated path returns exactly the numbers it always did; theimputation_did/two_stage_didwrappers forward the shared sentinel so plain wrapper calls never warn.imputation_aggregation.py/two_stage_aggregation.py(imputation.py2,961→1,491 lines;two_stage.py3,551→2,430, exiting the "consider splitting" band), inherited by the estimators (fit-time byte-identical) and hosted post-fit by throwaway kit aggregators populated exclusively from kit snapshots (public-field mutation cannot reach recompute - pinned)._fit_dataalready retains forpretrend_test()- zero marginal memory, pickle +392 B via memoization. TwoStageDiD's kit is the FIRST panel retention on its results: a deduplicated column-subset copy of the working frame (+170 KB plain / +434 KB replicate on the 3,000-row benchmark;store_kitopt-out tracked in DEFERRED.md). Fit timing unchanged within noise (matched pre/post bands overlap).cluster=naming the unit column)/anticipation/horizon_max/pretrends/Prop-5/survey-TSL/always-treated-pad/replicate (healthy, dropped, undefined-df) fits x balance_e {0,1,2}. Replicate arms are LEVEL-MATCHED (aggregate(L)≡fit(aggregate=L); the deprecated joint-stack coupling of the OVERALL row is a documented migration delta on degenerate designs, pinned). TwoStage post-fit containers reproduce the M-092 vcov/index/df contract exactly.'simple'(uniform CS/EDiD parity; replay tracked in TODO.md); a fit whose bootstrap FAILED aggregates normally. Container admission NOT widened: ImputationDiD rejected BY DESIGN (no joint ES covariance); TwoStageDiD DEFERRED pending a normalization derivation (DEFERRED.md paper-gated row) - both consumer TypeErrors state the grounds.aggregate('event_study')reaches the pretrends lead inference on analytical fits); (2) fixed a pre-existing TwoStageDiD corner where an event study with EVERY non-reference horizon Proposition-5-unidentified returned a reference-only surface, dropping the all-NaN Prop-5 rows and their warning.Methodology references (required if estimator / math changes)
docs/methodology/REGISTRY.md§ImputationDiD / §TwoStageDiD.df_used(Imputation) vs scalar df relay (TwoStage), the M-127 reachability predicate, and the admission decisions (Imputation by-design; TwoStage deferred).Validation
tests/test_aggregate_contract.py(two new estimator blocks, ~70 tests),tests/test_event_study_consumers.py(rejection classes on real containers),tests/test_practitioner.py,tests/test_guides.py,tests/test_v4_matrix.py,tests/test_naming_guard.py, plus the 199-site sweep and monkeypatch-target repoints across 16 existing test files (test_imputation.py,test_two_stage.py, methodology/survey/pretrends/replicate suites; spillover sites deliberately untouched and verified).assert_allclose1e-14 (documented in the REGISTRY notes and pinned in the contract tests); R-parity and Stata-golden suites unchanged; tutorials11_imputation_did.ipynb,12_two_stage_did.ipynb,15_efficient_did.ipynbmigrated and re-executed clean via nbmake.Security / privacy