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
@igerber
igerber deleted the feat/v4-efficient-aggregate-kit branch August 15, 2026 16:49
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