Repository navigation
refactor(v4): PR 4c-i of the 4.0 cut - decouple the TWFE event-study mode from the MultiPeriodDiD containers and migrate the MPD-fixture suites - #868
Open
igerber wants to merge 1 commit into
Conversation
…mode from the MultiPeriodDiD containers and migrate the MPD-fixture suites Cut-sequence item 4c-i (docs/v4-design.md section 9; item 4c split into 4c-i/4c-ii, user decision 2026-10-24). The TWFE event-study mode no longer constructs a MultiPeriodDiDResults: DifferenceInDifferences._fit_event_study_core returns a private _EventStudyCorePayload, TwoWayFixedEffects.fit(..., event_study=True) builds its EventStudyResults from it via results_base._from_event_study_payload, and MultiPeriodDiD.fit wraps the same payload into its container (estimators._mpd_results_from_payload). _from_mpd and the payload route share one calendar-surface builder (_calendar_surface_from_arrays, sorted-label contract enforced with an explicit raise), so the two paths cannot drift; _resolve_scalar_df_survey's metadata half is split out as _scalar_df_survey_from_metadata. No user-visible behavior change; mypy zero errors. Oracle (captured BEFORE the refactor): tests/_capture_v4_merge_mpd_oracles.py ran on the UNMODIFIED tree (68e383b, python backend; the rust capture agreed at rtol 1e-12 / atol 1e-13) and wrote tests/fixtures/v4_twfe_event_study_oracles.json - every EventStudyResults field of the TWFE surface AND of the MultiPeriodDiD twin across eleven lanes (pooled default / explicit cluster / hc2_bm; within + cluster / hc2 / Conley / survey TSL / JK1 replicates; string labels; a non-suffix partition; a rank-deficient NaN post period). Lanes live in tests/helpers/twfe_es_oracle_lanes.py (shared by the script and the test; the fixture echoes each lane so an edited lane fails as lane drift). tests/test_twfe_event_study_decoupling.py pins the refactored surface against that capture (CI bounds at the scipy-drift tolerance 1e-9), the MPD twin against its capture, payload-vs-_from_mpd byte identity, the full-container TWFE-vs-MPD identity (exact pooled / 1e-12 within), the sorted-zip contract on an unsorted hand-built dict, the df_survey resolver split, and the post_period_average helper's MPD arithmetic incl. its NaN contract. Suite migration (ported = replaced; MPD-only tests stay for 4c-ii): honest_did, pretrends (+ a TWFE pre_periods= class; the MPD class is MPD-only because it promotes declared-post labels), methodology pretrends/honest_did (hand-built calendar surfaces, reference inserted so no coefficient is demoted), visualization (the native-MPD plot test stays as the period_effects-branch pin), plotly, Conley (unit= kept: Conley requires it and never auto-clusters), survey, replicate weights, survey_phase6, vcov_type (incl. a within+hc2 R-parity sibling class; the one-way HC2-BM lane goes to TODO.md), estimators (a TwoWayFixedEffectsEventStudy twin class; the class-scoped panels hoisted to module builders), the event-study surface roster (two native producers), variance-conventions (two twfe_es_* rows + regenerated table), and the DiagnosticReport/BusinessReport generic fixtures (static TWFE fit, legacy MPD fixture kept for the event-study-lane and dispatch pins). Ported avg_att/avg_se reads use tests/helpers/event_study_average.py (MPD's arithmetic); hand-built containers use tests/helpers/event_study_surface_builders.py. Spec: item 4c split; 4c-ii records the spec="within" benchmark relabel, the M-016 record type and the build_event_study_surface sniff reorder. Ledger: M-010/M-011 notes only (no status flips). TODO row for the HC2-BM lane.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
docs/v4-design.mdsection 9; item 4c is split into 4c-i/4c-ii in this diff - user decision 2026-10-24). The TWFE event-study mode is decoupled from the MultiPeriodDiD containers:DifferenceInDifferences._fit_event_study_corereturns a private_EventStudyCorePayload,TwoWayFixedEffects.fit(..., event_study=True)builds itsEventStudyResultsfrom it viaresults_base._from_event_study_payload, and onlyMultiPeriodDiD.fitstill wraps the payload intoMultiPeriodDiDResults(estimators._mpd_results_from_payload)._from_mpdand the payload route share one calendar-surface builder (_calendar_surface_from_arrays; the sorted-label contract is an explicit raise), and_resolve_scalar_df_survey's metadata half is split out as_scalar_df_survey_from_metadata. No user-visible behavior changes; the ledger'sM-010/M-011"decouple before deleting" obligation is met, so PR 4c-ii can delete the class.tests/_capture_v4_merge_mpd_oracles.pyran on the unmodified tree at68e383bd(python backend; the rust capture agreed at rtol 1e-12 / atol 1e-13) and wrotetests/fixtures/v4_twfe_event_study_oracles.json- everyEventStudyResultsfield of the TWFE surface AND of its MultiPeriodDiD twin across eleven lanes (pooled default / explicit cluster / hc2_bm; within + explicit cluster / hc2 carve-out / Conley / survey TSL / JK1 replicates; string labels; a non-suffix partition; a rank-deficient NaN post period). Lanes live in the sharedtests/helpers/twfe_es_oracle_lanes.py; the fixture echoes each lane so an edited lane fails as lane drift, not as a numeric regression.TwoWayFixedEffects().fit(..., event_study=True)while MultiPeriodDiD is still the live oracle (the 3a -> 3b pattern): honest_did, pretrends (plus a TWFEpre_periods=class), methodology pretrends/honest_did, visualization, plotly, Conley, survey, replicate weights, survey_phase6, vcov_type (plus a within+hc2 R-parity sibling class on thempd_absorbed_fe_didgolden), estimators (aTestTwoWayFixedEffectsEventStudytwin class), the event-study surface roster (two native producers), variance-conventions (twotwfe_es_*rows, table regenerated) and the DiagnosticReport / BusinessReport generic fixtures (static TWFE fit, per the user's decision). MPD-only tests stay for 4c-ii: the class API / deprecation /avg_*pins, the MPD-vs-TWFE parity oracles, multi-absorb/fixed_effects=/ wild-bootstrap designs, container-field assertions, the MPD-branch consumer tests, the native-MPD plot test and the report classes' event-study-lane and name-string-dispatch pins (kept onlegacy_mpd_fit).spec="within"benchmark-arm relabel (the arm fits the unit-FE design,absorb=["unit"]vs Rfeols(... | unit)), theM-016view's record type and thebuild_event_study_surfacesniff reorder; ledgerM-010/M-011notes updated (no status flips); aTODO.mdrow tracks the one-way HC2-BM absorbed-FE R-parity lane that the within spec cannot express.Methodology references (required if estimator / math changes)
Validation
tests/test_twfe_event_study_decoupling.py(65 tests: committed-oracle comparison of the TWFE surface and the MPD twin per lane, lane-echo drift pin, payload-vs-_from_mpdbyte identity, full-container TWFE-vs-MPD identity, the unsorted-dict sorted-zip contract, thedf_surveyresolver split,post_period_averageidentity with MPD'savg_att/avg_seand its NaN contract on a real rank-deficient fit; 65 passed on both backends and under the py3.9-leg pins scipy 1.13.1 / numpy 2.0.2); shared helperstests/helpers/twfe_es_oracle_lanes.py,event_study_average.py,event_study_surface_builders.py; the ported suites above (honest_did 88, pretrends 74, methodology_honest_did 38, methodology_pretrends 37, visualization 30, plotly 43, conley 150, survey 138, replicate 37, phase6 80, vcov_type 137, estimators 174+, event_study_surface 195 + 2 skipped, variance_conventions incl. the two slow rows, diagnostic_report 167, business_report 202). Untouched MPD-consuming and roster suites re-run unmodified after the refactor (results_serialization, result_aliases, practitioner, base_estimator, aliases, profile_panel, balance_interop_contract, target_parameter, power's MPD tests, naming_guard, v4_matrix): all green.tests/test_v4_merge_mpd.py(72) unchanged and green on both backends. Doc gates (test_v4_matrix,test_naming_guard,test_tracking_files,test_doc_deps_integrity,test_changelog_fragments,test_docs_ia,test_guides, the variance-conventions table gate) green after the spec/ledger/TODO/changelog edits. CI-pinnedmypy diff_diff: no issues; black/ruff clean; py3.9compileallclean.tests/test_t23_spillover_tva_drift.py, the TWFE event-study consumer, green on both backends).Security / privacy
Changelog
changelog.d/20261024-v4-pr4c-i-twfe-es-decoupling.md(### Internal,### Testing)Follow-ups
MultiPeriodDiD,MultiPeriodDiDResults,PeriodEffect,EventStudy;_from_mpd,_mpd_results_from_payload, the payload'savg_*block), the ledger flips (M-010/M-011/M-012-> removed;M-060/M-007/M-083-> done;M-016locator ->EventStudyResults.period_effects, shimmed), theM-016record view, theM-083raise, thepyproject.tomlfilter, the benchmark arm ->spec="within", the power registry entry, the rosters, the two markdown notebook cells, thebuild_event_study_surfacesniff reorder, thelegacy_mpd_fitfixtures and every MPD-only test named in the spec item, the MPD half oftests/test_twfe_event_study_decoupling.pyand the oracle'smpd_twinrecords, the caller-lessHonestDiD._estimate_max_pre_violationMPD branch, and a re-pin of the twotwfe_es_*variance rows once the core's post-average block goes (theirtail_dfcarries that call today).cluster_name/n_clusters/vcov_type) onEventStudyResults: the auto-clustering event-study mode has no surface for it (TODO candidate).