Skip to content

feat(v4): EfficientDiD post-fit aggregate() via a lazy recompute kit (2(b) PR-3a, M-023/M-120) - #748

Merged
igerber merged 3 commits into
mainfrom
feat/v4-efficient-aggregate-kit
Aug 4, 2026
Merged

feat(v4): EfficientDiD post-fit aggregate() via a lazy recompute kit (2(b) PR-3a, M-023/M-120)#748
igerber merged 3 commits into
mainfrom
feat/v4-efficient-aggregate-kit

Conversation

@igerber

@igerber igerber commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Deprecates EfficientDiD.fit(aggregate=, balance_e=) (shared NOT_SUPPLIED sentinel, one joint FutureWarning, warn-and-still-work) in favor of post-fit EfficientDiDResults.aggregate(type, balance_e=) on the M-122 mixin contract - ledger row M-023 flips to shimmed and the new row M-120 claims the reserved balance_e slot (2(b) PR-3a of the v4 program).
  • The successor is a lazy CS-class recompute kit: fit() computes nothing extra; the results object retains an AggregationKit (the per-(g,t) EIF dict as the canonical payload, O(n_units) bookkeeping, and the post-overall df_survey snapshot); aggregate('event_study'/'group', balance_e=) recomputes on a throwaway aggregator host and aggregate('simple') relays the stored overall row bit-exact. Post-fit output matches the fit-time surfaces at 1e-14 across plain/cluster/survey-TSL/replicate fits (incl. dropped-replicate and n_valid<=1 degenerate arms), every balance_e, and both pt_assumption regimes.
  • The six aggregation methods move verbatim (one additive per-row df_used provenance capture) into the new diff_diff/efficient_did_aggregation.py, importable by both the estimator and the results module; efficient_did.py drops below the 2000-line monitoring band.
  • Bootstrapped fits fail closed on aggregate() at every level (CS parity); the fit-time bootstrap override clears the analytical df_used provenance; exact post-fit replay is a tracked TODO row. The public influence_functions diagnostic deep-copies the kit payload (an aliased public field would let user mutation silently corrupt recomputed inference - regression-pinned).
  • PT-Post reference provenance: a membership-gated reference_period property (SunAbraham rule - absent anchors are never synthesized) marks the materialized mechanical zero anchor is_reference in the container and corrects plot_event_study's inferred reference on anticipation>0 PT-Post fits.
  • honest/pretrends container admission stays REJECTED BY DESIGN for EfficientDiD-sourced containers (no joint event-study covariance); both terminal TypeErrors now state the rejection explicitly (M-093 third amendment; M-092 gains a per-row-df scope qualifier).
  • Docs: REGISTRY M-023/M-120 Note with the full retained-buffer enumeration (phrased as maxima), guides (llms-full/llms/README/llms-practitioner), API rst + autosummary stub, choosing_estimator, CHANGELOG, TODO + DEFERRED rows, dev-status refresh; tutorial 15 migrated to post-fit containers and re-executed. Practitioner guidance gains a post-fit aggregation step with a bootstrap carve-out.
  • ~120 deprecated call sites swept across six test files; warn hygiene proven under -W error::FutureWarning.

Methodology references (required if estimator / math changes)

  • Method name(s): EfficientDiD (Chen, Sant'Anna & Xie 2025) - aggregation entry-point migration only; the overall/event-study/group aggregation formulas, EIF/WIF variance machinery, and all stored numbers are unchanged (inertness-pinned at 1e-14).
  • Paper / source link(s): Chen, Sant'Anna & Xie (2025), arXiv:2506.17729v1; paper review on file at docs/methodology/papers/chen-santanna-xie-2025-review.md.
  • Any intentional deviations from the source (and why): documented in the new REGISTRY Note (post-fit aggregate() - rows M-023/M-120) - the lazy-kit memory contract (store_eif now governs only the public field), the anchor-horizon balance_e rule (the CS rule, divergent from Imputation/TwoStage's balanced-window rule), uniform bootstrap fail-close incl. the relayable 'simple' level (by decision, CS parity), the per-row df_used group relay (divergence from CS's scalar broadcast), and the regime-dependent PT-Post reference marking.

Validation

  • Tests added/updated: tests/test_aggregate_contract.py (TestEfficientShim + TestEfficientAggregate + internal-caller pin, 35 tests incl. the roster floor), tests/test_event_study_consumers.py (real-container rejection pin), tests/test_visualization.py (PT-Post plot-reference pin), tests/test_practitioner.py (handler branch + non-STEPS-key pins), tests/test_guides.py (shim-line pin), tests/test_v4_matrix.py / tests/test_naming_guard.py (ledger pins), plus the sweep across tests/test_efficient_did.py, tests/test_methodology_efficient_did.py, tests/test_efficient_did_validation.py, tests/test_survey_phase3.py.
  • Backtest / simulation / notebook evidence (if applicable): docs/tutorials/15_efficient_did.ipynb migrated and re-executed clean via nbmake; published-parity (HRS/Table-6) and MC suites re-run green post-migration. Perf: plain fit 23.31->23.35 ms and fit-time ES 23.68->23.67 ms (noise); plain-fit pickle 9,836->78,262 bytes (the documented kit payload).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

…(2(b) PR-3a, M-023/M-120)

fit(aggregate=, balance_e=) deprecates (shared NOT_SUPPLIED sentinel,
one joint FutureWarning, warn-and-still-work) in favor of post-fit
EfficientDiDResults.aggregate(type, balance_e=) on the M-122 mixin
contract - a lazy CS-class recompute: fit() computes nothing extra, the
results object retains an AggregationKit (per-(g,t) EIF dict referenced
canonically + O(n_units) bookkeeping + the post-overall df_survey
snapshot), and event_study/group recompute on a throwaway aggregator
host while simple relays the stored overall row bit-exact. Inertness
pinned at 1e-14 across plain/cluster/survey-TSL/replicate (incl.
dropped-replicate and n_valid<=1 degenerate arms) x balance_e x
PT-All/PT-Post.

Structure: the six aggregation methods move verbatim (one additive
per-row df_used capture) to the new diff_diff/efficient_did_aggregation
module importable by both estimator and results; efficient_did.py drops
below the 2000-line band. Bootstrapped fits fail closed on aggregate()
at every level (CS parity; fit-time group rows clear df_used under the
bootstrap override; exact replay is a TODO row). The public
influence_functions diagnostic deep-copies the kit payload (aliasing
would let user mutation corrupt recomputed inference - regression
pinned). PT-Post reference provenance ships via a membership-gated
reference_period property (SunAbraham rule; absent anchors never
synthesized) marking the materialized mechanical zero anchor in the
container and correcting plot_event_study's inferred reference on
anticipation>0 fits. honest/pretrends admission stays rejected BY
DESIGN (no joint ES covariance; both terminal TypeErrors state it).

Ledger: M-023 shimmed, new M-120 row claims the reserved balance_e
slot (anchor-horizon rule - the CS rule), M-092 scope qualifier, M-093
third amendment, matrix floor 118 + (120,120), naming-guard roster,
v4-design phase-5 cell. Docs: REGISTRY M-023/M-120 Note (full retained-
buffer enumeration as maxima), llms-full/llms/README/llms-practitioner,
api rst + autosummary stub, choosing_estimator, CHANGELOG, TODO +
DEFERRED rows, tutorial 15 migrated to post-fit containers and
re-executed. Practitioner gains a post-fit aggregation step with a
bootstrap carve-out (non-STEPS "aggregation" key). ~120 deprecated
call sites swept across six test files with warn hygiene proven under
-W error::FutureWarning.

Perf (300 units x 10 periods, 15-rep medians): plain fit 23.31->23.35ms
and fit-time ES 23.68->23.67ms (noise); plain-fit pickle 9,836->78,262
bytes (the documented kit payload).
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Overall assessment

Blocker — one unmitigated P0 finding.

Executive summary

  • Aggregation formulas and variance machinery match the registry and Chen–Sant’Anna–Xie review.
  • Documented balance, bootstrap, reference-period, and df deviations are not defects.
  • A public group_time_effects mutation can silently combine altered estimates with retained fit-time EIFs, producing invalid inference.
  • The tutorial still incorrectly says covariates are unsupported.

Methodology

P0 — [Newly identified] Aggregation mixes mutable public estimates with private fit-time EIFs

Location: diff_diff/efficient_did_results.py:L318-L340, diff_diff/efficient_did.py:L137-L147

Impact: aggregate("group") and aggregate("event_study") read self.group_time_effects and self.groups, while their variance calculations use the original private EIF payload. Mutating a public result row after fitting can therefore change an ATT while retaining an incompatible SE—or omit cohorts—without warning. This yields plausible but statistically invalid p-values and confidence intervals.

The PR correctly isolates public influence_functions, but not the other public aggregation inputs.

Concrete fix: Store an immutable/private snapshot of group-time effects and cohort membership in AggregationKit, and recompute exclusively from that snapshot. Also use kit-snapshotted provenance such as alpha, anticipation, and pt_assumption when building the carrier.

P3 — Documented methodology choices

Impact: The anchor-horizon balance_e rule, uniform bootstrap fail-close, PT-Post reference marking, and group df relay differ from some sibling estimators.

Concrete fix: None required; all are explicitly documented in REGISTRY.md under the M-023/M-120 note.

Code Quality

No additional findings. The extracted aggregation methods preserve centralized safe_inference() usage.

Performance

P3 — Always-retained EIF payload

Impact: Results and pickles grow by O(n_units × n_gt).

Concrete fix: None required for this PR; the optional store_kit=False work is tracked in DEFERRED.md.

Maintainability

No additional findings.

Tech Debt

P3 — Tracked aggregation limitations

Impact: Post-fit bootstrap replay and event-study per-row df provenance remain unavailable.

Concrete fix: None required; both are tracked in TODO.md.

Security

No findings. The retained kit avoids raw unit identifiers, and no secrets were found.

Documentation/Tests

P2 — Tutorial incorrectly claims covariates are unsupported

Location: docs/tutorials/15_efficient_did.ipynb:L572

Impact: The summary says “Phase 1 is no-covariates only,” contradicting the implemented and demonstrated doubly robust covariate path.

Concrete fix: Replace that takeaway with current covariate-support guidance.

Test execution was unavailable because pytest is not installed; all affected Python files and the notebook parsed successfully.

Path to Approval

  1. Snapshot all group/event aggregation inputs in the private kit and stop reading mutable public result fields during recomputation.
  2. Add regression tests showing mutations to group_time_effects and groups cannot change post-fit aggregation output.

…able public fields (CI review R1)

P0, verified by execution: aggregate('group'/'event_study') read the
MUTABLE public group_time_effects/groups/time_periods while variance
came from the retained fit-time EIFs - mutating a post-treatment public
row shifted the recomputed group ATT by 249 with ZERO se drift (the
mixed plausible-but-invalid state), and popping a cohort from the
public groups list silently dropped rows. The kit now snapshots the
three aggregation inputs at build time (per-row dict copies - values
are scalars/tuples; list copies for groups/time_periods; alpha and
anticipation were already kit fields) and _aggregate_compute reads
exclusively from the snapshots. Regression pin: post-fit aggregation
output invariant under public-field mutation. REGISTRY buffer
enumeration + M-023 ledger notes updated.

P2: tutorial 15's summary takeaway claimed "Phase 1 is no-covariates
only", contradicting the shipped doubly robust covariate path -
replaced with current covariate guidance + the post-fit aggregation
takeaway; notebook re-executed.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 629d299091a96018ede4ae2c0759c2dd6ac7dd64


Overall assessment

Blocker — the prior P0 is only partially resolved.

Executive summary

  • EfficientDiD aggregation formulas remain consistent with Chen–Sant’Anna–Xie and the registry.
  • Group-time rows, cohort lists, periods, and public EIFs are now privately isolated.
  • Mutable public provenance can still silently alter post-fit output, including replacing a genuine estimate with a reference zero.
  • The prior tutorial covariate error is corrected.
  • Documented deviations and tracked limitations are non-blocking.

Methodology

P0 — Prior finding partially unresolved: carrier still reads mutable public provenance

Location: diff_diff/efficient_did_results.py:L263-L285, diff_diff/efficient_did_results.py:L342-L355, diff_diff/results_base.py:L389-L400, diff_diff/results_base.py:L924-L945

Impact: The numerical aggregation uses kit-snapshotted inputs, but dataclasses.replace(self, ...) retains public pt_assumption, anticipation, and alpha. For example, changing a PT-All result’s public pt_assumption to "post" before aggregation makes the genuine e=-1 estimate appear to be a reference row. EventStudyResults.__post_init__() then silently overwrites its ATT with 0.0 and all inference with NaN. Public alpha mutation can likewise mislabel previously computed intervals.

This contradicts the registry’s private-snapshot contract at docs/methodology/REGISTRY.md:L1525.

Concrete fix: Store pt_assumption in the kit and build the carrier with kit.alpha, kit.anticipation, and the snapshotted PT regime. Avoid every public provenance read during recomputation; use kit.alpha for the simple relay as well.

P3 — Documented methodology choices

Impact: Anchor-horizon balancing, bootstrap fail-close, group-df relay, and PT-dependent reference marking are intentional.

Concrete fix: None; documented in docs/methodology/REGISTRY.md:L1525.

Code Quality

No additional findings.

Performance

P3 — Always-retained EIF payload

Impact: Results retain O(n_units × n_gt) data.

Concrete fix: None required; an opt-out is tracked in DEFERRED.md:L72.

Maintainability

No findings.

Tech Debt

P3 — Tracked aggregation limitations

Impact: Bootstrap replay and event-study per-row df remain deferred.

Concrete fix: None required; tracked in TODO.md:L24-L25.

Security

No findings or apparent secrets.

Documentation/Tests

The previous tutorial covariate error is fixed. Add mutation tests for pt_assumption, anticipation, and alpha; current coverage only mutates numerical rows and cohort/period lists.

Runtime tests could not run because NumPy is unavailable. Affected Python files and the notebook parsed successfully.

Path to Approval

  1. Construct event-study carriers entirely from kit-snapshotted provenance.
  2. Add tests proving mutations to public pt_assumption, anticipation, and alpha cannot change or mislabel post-fit aggregation.

… review R2)

Completes the R1 isolation: verified by execution, flipping a PT-All
fit's public pt_assumption to "post" made the carrier's
reference_period property mark the GENUINE e=-1 estimate (att 0.0945)
as a reference row - post_init then zeroed its ATT and NaN'd its
inference; a mutated live anticipation shifted which row got zeroed.
The kit now snapshots pt_assumption and the n_treated+n_control total,
the ES carrier is built with the kit's pt_assumption/anticipation/alpha
(never the live public fields), and the simple relay takes alpha and n
from the kit. Regression pin extended: post-fit aggregation output and
container labeling invariant under pt_assumption/anticipation/alpha
mutation, PT-Post fits still mark their genuine anchor. REGISTRY and
M-023 ledger snapshot enumerations updated.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 06f1d905eefefbb185165e4781e22e4241fe7bcd


Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • Prior mutable-provenance P0 is resolved via kit snapshots.
  • Aggregation and EIF/WIF variance formulas remain methodology-consistent.
  • Empty results, bootstrap refusal, and NaN inference paths fail safely.
  • Documented deviations and tracked limitations are non-blocking.
  • Runtime tests were unavailable because NumPy and pytest are not installed.

Methodology

P3 — Documented implementation choices

Impact: Anchor-horizon balancing, bootstrap fail-close, group DF relay, and PT-dependent reference marking intentionally differ from other estimators.

Concrete fix: None. Documented in docs/methodology/REGISTRY.md:L1525.

The prior P0 is resolved in diff_diff/efficient_did_results.py:L307-L368; recomputation now uses snapshotted pt_assumption, anticipation, and alpha.

Code Quality

No findings. New inference paths consistently use safe_inference().

Performance

P3 — Always-retained EIF payload

Impact: Results retain O(n_units × n_gt) data.

Concrete fix: None required; an opt-out is tracked in DEFERRED.md:L72.

Maintainability

No findings. Extracting the shared aggregation mixin reduces estimator-module size without changing formulas.

Tech Debt

P3 — Tracked limitations

Impact: Bootstrap replay and event-study per-row DF provenance remain unavailable.

Concrete fix: None for this PR; both are tracked in TODO.md:L24-L25.

Security

No findings or apparent secrets.

Documentation/Tests

No findings. Mutation isolation, empty surfaces, bootstrap behavior, survey DF cases, and parameter validation have focused coverage in tests/test_aggregate_contract.py:L1835-L1940.

Runtime execution was not possible in the review environment; affected Python files parsed successfully.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 4, 2026
@igerber
igerber merged commit 70db1ef into main Aug 4, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant