Repository navigation
feat(v4): item 6 of the 4.0 cut - unify the DDD results container (M-014, M-140, M-141) - #878
Conversation
…014, M-140, M-141)
TripleDifference.fit returns ONE TripleDifferenceResults for both designs.
The surviving 2x2x2 class IS the unified container: its positional layout
is byte-stable (the four cell counts and estimation_method gained
defaults), the staggered structure is appended with defaults, and a
design discriminator ("2x2x2" / "staggered") says which engine fit it.
The canonical quintet is native in both designs; the group-time table is
empty in 2x2x2; event_study_effects / group_effects / cband_crit_value
are carrier fields that are None on every 4.0 fit; the five fit-time
overall_*_es fields and the never-populated influence_functions are gone;
df_inference records the 2x2x2 engine's t df (the LWDiDResults contract).
The module moved from staggered_triple_diff_results.py to
triple_diff_results.py (re-exported from triple_diff.py, the 3.x pickle
class path) so the shared staggered engine imports it without a cycle.
StaggeredTripleDiffResults is removed.
Post-fit aggregate() (rows M-140/M-141, the fit-time aggregate=/balance_e=
keywords are removed - TypeError): the container is an AggregationMixin
adopter. 'simple' relays the stored headline in both designs; 'event_study'
and 'group' recompute from the retained CallawaySantAnna kit in the
staggered design (built on the eligibility-zeroed precomputed copy so the
eligible-treated weighting is preserved) and REPLAY the fit-time multiplier
bootstrap on bootstrapped fits; the 2x2x2 design fails those levels closed
naming first_treat=. The paper's Eq. 4.14 overall (the 3.x overall_att_es
family) is aggregate('simple', weights='event_time') - a weights selector,
not a vocabulary word (user decision 2026-10-27) - reproducing the engine's
df rule (per-statistic effective df, else the fit's sentinelled survey df
carried as the ddd_df_survey_original bookkeeping key) and both fit-time
warnings. power.py's staggered-boundary guard drops its by-value pair.
Consumers dispatch on the design through _reporting_helpers.results_design
/ is_staggered_ddd (DR's _DESIGN_APPLICABILITY / _DESIGN_PT_METHOD tables
beside the class-name tables; BR's never-enabled narration; a staggered
practitioner handler); headline_attribute reads att in both designs (the
last overall_att); DR gains a top-level design key and BR
estimator.design (additive; no version bump - DR 3.0 / BR 4.0 have not
shipped, folded into PR 5b's REPORTING.md Note). plot_group_effects and
its siblings raise on the empty 2x2x2 table.
Ledger M-014 / M-140 / M-141 -> removed with tests/test_v4_unify_ddd.py as
test_ref (removal pins, shape and positional layout, the aggregate
vocabulary and guards, python-backend-guarded literal oracles for the
analytical, balance_e, bootstrap-replay and survey lanes captured on the
unmodified tree, both Eq. 4.14 warnings, read surfaces under
FutureWarning=error, the duck-typed consumers, pickles incl. the 3.x class
path, the report surface and practitioner routing). The DDD suites port
to the post-fit route; the aggregate-contract, event-study-surface,
staggered-aggregation, alias, serialization, target-parameter, report and
naming-guard suites follow. Spec section 4.2 / section 6 / item 6 SHIPPED
notes, the REGISTRY post-fit Note, REPORTING.md, the migration guide,
guides, API rst, autosummary stubs, tutorial 08 (re-executed) and the
tracking rows follow.
⛔ Blocker
MethodologyP0 — Preserve event-time labels through automatic diagnostics. Locations: The new staggered-DDD dispatch enables automatic post-fit event-study derivation. That path reaches Impact: Pre-treatment evidence disappears without a warning. An isolated reproduction with three pre-period p-values Concrete fix: Preserve numeric horizon keys and compare their actual values against the pre-period cutoff. Remove the lossy conversion from both the adapter and P3 — Documented aggregation conventions; no action required. Location: Eligible-treated cohort weighting, per-group WIF inference, and the distinction between the headline ATT and Eq. 4.14 are explicitly documented. The retained aggregation machinery follows these conventions; they are not defects. Code QualityNo additional findings beyond the event-time conversion above. PerformanceNo blocking findings identified. MaintainabilityNo additional findings identified. Tech DebtP3 — Bootstrap seed provenance remains tracked. Location: Impact: Serialized results do not directly expose the bootstrap seed. This predates the PR and is explicitly tracked. Concrete fix: No action required for this PR; retain the coordinated container-provenance follow-up. SecurityNo security findings identified in the reviewed changes. Documentation/TestsP2 — The balancing oracle does not exercise partial cohort removal. Locations: Impact: The Concrete fix: Add a The changelog requirement is satisfied. Methodology verification used the Registry and repository’s version-pinned scholarly review; the primary paper PDF was not independently retrieved. Path to Approval
|
… and heterogeneity collectors CI review on PR #878 (P0): the DR adapter keyed the derived aggregate('event_study') surface through int(k) and the pre-period / heterogeneity collectors compared int(k) against the anticipation-aware boundary. On a half-step calendar (e = t - g in -1.5, -1.0, -0.5, ...) -1.5 merged into -1 and -0.5 into the treatment horizon 0, so pre-periods silently left the PT family (a staggered DDD fit on a halved calendar reported "no pre-periods"; a CS fit on the same calendar kept one of its three). Pre-existing for every post-fit event-study producer; this PR's staggered DDD route is the newest adopter. _event_time_value / _event_time_key replace the four int() sites: integer-valued horizons keep Python int keys (the ImputationDiD/TwoStageDiD raw dtype), fractional horizons stay float, pre/post comparisons use the numeric value, and the labels stay hash-equal to the surface's vcov_index so the joint Wald still aligns. Tests: adapter unit pins (horizons [-1.5, -1.0, -0.5] with p-values [1e-20, 0.92, 1e-30] all survive, covariance index alignment, raw-field route, helper edge cases), the heterogeneity collectors' numeric split, and a staggered DDD half-step-calendar fit whose DiagnosticReport PT runs the covariance-backed joint Wald over every negative horizon. Also the review's P2: balance_e=3 oracles (cohort 4 unobserved at e=3 -> dropped, e=-2 gone) for the analytical event study / Eq. 4.14 overall and the bootstrap replay (per-horizon se, cband, percentile overall), captured on the unmodified tree. REPORTING.md derived-route Note and the changelog fragment record the fix.
|
🔁 AI review rerun (requested by @igerber) Head SHA: ✅ Looks goodExecutive summary
MethodologyPrevious P0 — Resolved. The adapter preserves fractional keys, and the collector compares their actual values against the anticipation-aware boundary. The reported false reassurance was eliminated in isolated source execution; retained labels also mapped to covariance positions P3 — Documented aggregation conventions; no action required. Impact: Eligible-treated weighting, per-cohort WIF inference, and the distinction between the headline ATT and Eq. 4.14 remain explicit conventions. The post-fit implementation retains the shared aggregation machinery. Concrete fix: None. Cross-checking used the Registry, source docstrings, and the repository’s version-pinned review of Ortiz-Villavicencio & Sant’Anna (2025), v3, particularly Eqs. 4.13–4.14 and Corollary 4.2. The primary PDF was not independently retrieved. Code QualityNo additional findings. The shared event-time helpers consistently replace truncation in the affected diagnostic collectors. PerformanceNo new findings within the re-review scope. MaintainabilityNo new findings within the re-review scope. Tech DebtP3 — Bootstrap seed provenance remains tracked. Impact: Results do not directly expose the seed behind bootstrap inference. This existing observability limitation remains tracked for both affected containers. Concrete fix: No action required for this PR; retain the coordinated follow-up. SecurityNo security findings identified in the reviewed changes. Documentation/TestsPrevious P2 — Resolved. The new Fractional-label regression coverage includes adapter/covariance alignment and an actual half-step staggered-DDD diagnostic route: Syntax checks passed for 15 affected source/test files. Full estimator, bootstrap, and joint-Wald integration tests remain unverified in this environment. |
Summary
docs/v4-design.mdsection 9; rowsM-014,M-140,M-141): the DDD container unification.TripleDifference.fit()returns ONETripleDifferenceResultsfor both designs. The surviving 2x2x2 class IS the unified container - its positional layout is byte-stable (the four cell counts andestimation_methodgained defaults), the staggered structure (group_time_effects,groups,time_periods, unit counts,control_group,base_period,anticipation,bootstrap_results, ...) is appended with defaults, anddesign("2x2x2"/"staggered") discriminates (__post_init__validates the design-specific invariants). The canonical quintet is native in both designs (born canonical, after item 5);group_time_effectsis{}in 2x2x2;event_study_effects/group_effects/cband_crit_valueare carrier fields,Noneon every 4.0 fit; the five fit-timeoverall_*_esfields and the never-populatedinfluence_functionsare dropped; newdf_inferencerecords the 2x2x2 engine's t df (theLWDiDResults.df_inferencecontract) soaggregate('simple')'s df column is honest in both designs. The module movedstaggered_triple_diff_results.py->triple_diff_results.py(git mv; re-exported fromtriple_diff.py, the 3.x pickle class path) so the shared staggered engine imports the container without a cycle.StaggeredTripleDiffResultsis removed fromdiff_diff/__all__(3.x staggered pickles cannot load, like every removed class; 3.x 2x2x2 pickles load via__setstate__backfill).aggregate()(rowsM-140/M-141; the fit-timeaggregate=/balance_e=keywords are REMOVED -TypeErrorin both designs):_AGGREGATE_SUPPORTED = ('simple', 'event_study', 'group'), no'total'.'simple'relays the stored headline in both designs (n_kindunits / obs; df fromresolve_inference_df);'event_study'/'group'recompute from the retained CallawaySantAnna kit in the staggered design (staggered.py's_build_aggregation_kiton the eligibility-zeroedprecomputedcopy, so the REGISTRY "Deviation from R" weighting is preserved exactly) and on bootstrapped fits REPLAY the fit-time multiplier bootstrap from the kit'sBootstrapReplaySpec(the CS / DMLDiD / EfficientDiD mechanism); the 2x2x2 design fails those levels (andweights=/balance_e=) closed namingfirst_treat=. The paper's Eq. 4.14 overall (the 3.xoverall_att_esfamily) isaggregate('simple', weights='event_time')- aweights=SELECTOR on the existing type (the LWDiD / Wooldridge hook), not a vocabulary word (user decision 2026-10-27) - acceptingbalance_e=, reproducing the engine's df rule (per-statistic effective df, else the fit's sentinelled survey df carried as the newddd_df_survey_originalbookkeeping key, so NaN-at-fit stays NaN post-fit) and both fit-time warnings;weights='event_time'on other levels andbalance_e=on the plain relay are rejected rather than ignored.summary()no longer prints the event-study / cohort / Eq. 4.14 blocks (it points at the post-fit calls);to_dataframe(level=None)defaults to'group_time'(staggered) /'overall'(2x2x2),'event_study'/'group'raise with the post-fit pointer.power.py's staggered-boundary guard drops its by-value pair (anestimator_kwargscarryingaggregatesurfaces asfit()'sTypeError)._reporting_helpers.results_design/is_staggered_ddd(scoped toTripleDifferenceResults- the HAD containers carry an unrelateddesignfield); DR keeps one class-name key per class in_APPLICABILITY/_PT_METHOD(the exhaustiveness guard builds a stub from every key) and adds_DESIGN_APPLICABILITY/_DESIGN_PT_METHODread through_applicability_for/_pt_method_for; the "re-fit with aggregate=" derivation branch is gone (the genericaggregate('event_study')auto-derivation serves the staggered design; the 2x2x2 design has no PT row, exactly as today); BR's never-enabled narration and the practitioner handler (new staggered branch) branch on the design;headline_attributereads"att"in both designs (the LAST"overall_att"); DR gains a top-leveldesignkey and BRestimator.design(Nonefor every other estimator). NO schema bump (user decision 2026-10-27): DR 3.0 / BR 4.0 have not shipped - PR 5b's REPORTING.md Note is extended.results_base._ABSENT_SURFACE_HINTSgains theTripleDifferenceResultsentry;build_event_study_surface(cross)now raises that hint instead of a TypeError;plot_group_effectsand its two siblings raiseValueErroron the empty 2x2x2 table; DR'sgroup_time_effectsbranches use truthiness.M-014/M-140/M-141planned->removedwithtest_ref: tests/test_v4_unify_ddd.py(notes carry the mechanics, the module move, the pickle policy, theweights='event_time'decision and the power change);M-013notes close item (6);M-050/M-093/M-146code_refsfollow the module rename. Spec: section 4.2 "Results" and the section-6 amendment get SHIPPED notes, the vocabulary paragraph records the selector decision, item 6 gets its SHIPPED note. REGISTRY: the "Result-class field naming" exception sentence goes (every container canonical), the TripleDifference Dispatch bullet and power Note drop the by-value pair, the results-container Note is rewritten for the 4.0 state, a new- **Note (post-fit aggregate() - rows M-014/M-140/M-141):**records the vocabulary / relay / replay / Eq. 4.14 / df-rule / warning contract, the StaggeredTripleDifference "Aggregation" prose and the R cross-validation sentence name the post-fit call, the seed Note says the unification landed withoutseed. TODO.md: the "post-fit aggregate() for the staggered DDD container" row is retired; the seed-provenance row is reworded.docs/doc-deps.yamlmaps the renamed module (driftmedium;triple_diff.rstin,staggered.rstout).docs/api/triple_diff.rst(post-fit example, one-container sentence,aggregate/epv_summaryin the methods list),staggered.rstloses theStaggeredTripleDiffResultssection,index.rstits autosummary entry;docs/api/_autosummary/regenerated viamake -C docs html(theTripleDifferenceResultsstub changes, theStaggeredTripleDiffResultsstub is deleted; no other churn);llms-full.txt(staggered signature block without the two keywords, usage with the post-fit calls, theTripleDifferenceResultstable withdesignand the staggered fields, methods incl.aggregate(...), the DR "Schema top-level keys" list gainsdesign),llms.txtcatalog line,llms-autonomous.txtBR / DR schema bullets;docs/migration-4.0.md(worked post-fit block in the DDD merge section, pickle sentence, appendix Fix cells forM-014/M-140/M-141- the*(not yet implemented)*markers are gone);METHODOLOGY_REVIEW.mdstaggered-DDD rows; tutorial 08 cells 22-23 ported and the notebook re-executed under the Rust backend (cell 19's 2x2x2to_dataframe()output gains thedesign/pscore_trim/alpha/df_inferencecolumns).tests/test_naming_guard.py:_CS_GROUPS_CLASSESgainsTripleDifferenceResults(itsgroupsnames the ATT(g,t) cohort; the dying class passed only through theM-014class exemption), the_CS_GROUPS_READERpath follows the rename, theaggregate-token allowlist reasons say no fit-timeaggregate=survives.tests/test_guides.pygains the TripleDifference*_fit_blocks_have_no_fit_time_aggregatepin (both signature blocks).Methodology references (required if estimator / math changes)
ATT(g, t)cells and the same CS-simple overall; the event-study / cohort / Eq. 4.14 aggregations run post-fit through the SAMECallawaySantAnnaAggregationMixinmethods on the SAME eligibility-zeroed bookkeeping, and the bootstrap replay regenerates the SAME weight stream (pinned against literals captured on the unmodified tree at rtol 1e-9: event-study rows, cohort rows, the Eq. 4.14 quintet on the analytical /balance_e=1/ survey-pweight lanes, the bootstrap sup-t critical value, percentile SEs and the replayed Eq. 4.14 inference).## TripleDifference/## StaggeredTripleDifference.aggregate('simple', weights='event_time')(user decision 2026-10-27, recorded in the spec vocabulary paragraph and the REGISTRY Note).Validation
tests/test_v4_unify_ddd.py(removal pins incl. the TypeError on both designs and the signature; shape, degenerate 2x2x2 view, positional layout,__post_init__guards,df_inferenceprovenance; aggregate vocabulary and guards; python-backend-guarded literal oracles - analytical ES / group / Eq. 4.14 quintet,balance_e=1, then_bootstrap=49, seed=7, cband=Truereplay (cband, percentile SEs, Eq. 4.14 percentile inference), the survey-pweight lane incl. the df column; both Eq. 4.14 warnings incl. the single-PSU bootstrap lane; read surfaces underFutureWarning=error,to_dataframelevels,to_dictblocks, summary / repr / epv_summary, the duck-typed consumers; pickle round trip, simulated 3.x 2x2x2 state, the 3.x class path;describe_target_parameter, DR (staggered derives PT post-fit; 2x2x2 has no PT row;designkey;Noneelsewhere), BR (both control groups, both designs), practitioner routing). Ported to the post-fit route:tests/test_v4_merge_ddd.py(oracle gates readaggregate(...); Gate E inverted; the removed-keyword TypeError pins; the power boundary test),test_staggered_triple_diff.py,test_survey_staggered_ddd.py,test_methodology_staggered_triple_diff.py(R goldenoverall_att_eskeys read throughaggregate('simple', weights='event_time')),test_aggregate_contract.py,test_event_study_surface.py(thelegacy_standincarrier like CS),test_staggered_aggregation.py;test_result_aliases.py(SDDD exception gone),test_results_serialization.pyroster,test_target_parameter.py(design-aware stub + the_DESIGN_APPLICABILITYexhaustiveness mirror),test_business_report.pystubs,test_diagnostic_report.py(_TOP_LEVEL_KEYS+ the derived-PT test),test_naming_guard.py,test_guides.py.DIFF_DIFF_BACKEND=pythonstep-1 group (unify_ddd, merge_ddd, triple_diff, result_aliases, results_serialization: 366 passed, every oracle lane un-skipped); report / consumer group under-W error::FutureWarning(target_parameter, diagnostic_report, business_report, practitioner, aggregate_contract, event_study_surface, event_study_consumers: 1310 passed); estimator group under-W error::FutureWarning(staggered_triple_diff, survey_staggered_ddd, methodology_staggered_triple_diff, survey_r_crossvalidation, power, staggered, staggered_aggregation, base_estimator: 905 passed); ledger / guard / tracking / changelog / doc-deps / guides / doc-snippets (933 passed) and docs_ia (20 passed); full defaultpytestand--nbmakeon tutorial 08: see the run note below. CI-pinnedmypy diff_diffclean (111 files); black / ruff clean; py3.9compileallof the touched tests clean;make -C docs htmlexit 0.StaggeredTripleDiffResults/staggered_triple_diff_resultssurvive only in provenance prose, the removal pins and the frozen capture script;overall_att_es/overall_se_essurvive only in the liveCSBootstrapResults.overall_att_es_*payload fields, provenance prose, the R-golden JSON keys and the inline oracle literals; the AST sweep forfit(aggregate=|balance_e=)overdiff_diff/,tests/and the notebooks returns onlypytest.raises(TypeError)removal pins (verified per site) and the frozen capture script.DIFF_DIFF_BACKEND=rust; the post-fitaggregate('event_study')frame and the Eq. 4.14 row render in cell 23.Security / privacy
Changelog
changelog.d/20261027-v4-pr6-ddd-container-unification.md(### Breaking Changesx2 +### Internal)Follow-ups
cluster=False), 8 (M-093), 9 (M-090 docs incl. the REGISTRY archive framing for the removed classes), 10 release.