Skip to content

feat(validation): family-wide anticipation domain validation (M-144) - #771

Merged
igerber merged 1 commit into
mainfrom
feat/anticipation-domain-validation
Aug 16, 2026
Merged

feat(validation): family-wide anticipation domain validation (M-144)#771
igerber merged 1 commit into
mainfrom
feat/anticipation-domain-validation

Conversation

@igerber

@igerber igerber commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Family-wide anticipation domain validation (ledger row [M-144]; retires the TODO "library-wide anticipation domain validation" row). All nine anticipation-taking estimators (CallawaySantAnna, SunAbraham, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, EfficientDiD, WooldridgeDiD, SpilloverDiD) now validate anticipation at __init__ via the shared utils.validate_anticipation (non-negative integer; bool rejected; set_params transactional via the BaseEstimator probe re-init) AND re-check it on the fit path — the uniform direct-mutation defense.
  • The validator now RETURNS the normalized Python int, adopted via assignment at every call site (constructor and fit path), so numpy scalars — np.uint64 included — are normalized before any g - 1 - anticipation arithmetic can overflow. EfficientDiD.hausman_pretest normalizes its own argument (an unsigned scalar previously wrapped e < -ant and silently degraded the pretest to an all-NaN inconclusive result).
  • Motivation (measured): CallawaySantAnna(anticipation=-1) ran silently and moved the overall ATT by −85% (sign flip under control_group="not_yet_treated"); anticipation=True fit bit-identically to 1; SunAbraham(anticipation=1.5) returned att=nan without raising; StackedDiD(anticipation=1.5) crashed with an incidental range() TypeError.
  • SpilloverDiD gains eager construction validation; its in-fit check is swapped for the shared helper IN PLACE, preserving the PR SpilloverDiD: event_study=True per-event-time × ring decomposition (Wave C) #456 R2 ordering guarantee (validate before the ref-period arithmetic). WooldridgeDiD's weaker local >= 0 check is superseded (message text changed; None/str raw TypeErrorValueError; constructor error ordering moved behind the other constructor checks — disclosed in CHANGELOG/migration). The deprecated StaggeredTripleDifference stays construction-permissive by design (fit-validated via the shared engine).
  • Ledger/docs: M-144 minted (introduced_in: "4.0" per the locked release ladder, terminal done, phase-5 cell cited in docs/v4-design.md), tests/test_v4_matrix.py count bumps + narrative true-ups, migration-guide "Remaining 4.0 changes" bullet (numpy-scalar→int attribute retyping and the Wooldridge error-ordering change disclosed), REGISTRY family-wide adoption note + per-estimator pointer Notes, both LLM guides annotated, CHANGELOG entry.

Methodology references (required if estimator / math changes)

  • Method name(s): Input-domain validation only — no point-estimate, weighting, variance, or inference formula changes for valid inputs. The anticipation semantics being guarded are those documented per estimator (Callaway & Sant'Anna 2021 base-period/not-yet-treated threshold; Ortiz-Villavicencio & Sant'Anna 2025 staggered DDD engine; Wooldridge 2025 ETWFE treatment-cell window; etc.).
  • Paper / source link(s): See each estimator's REGISTRY.md section; the central note is the TripleDifference staggered-mode "anticipation domain validated from birth" note, extended with the Family-wide adoption (M-144) paragraph.
  • Any intentional deviations from the source (and why): None — the guard enforces the already-documented domain. The deprecated StaggeredTripleDifference's construction-permissive exception is recorded in REGISTRY.md (frozen 3.x API shape; engine validates at fit).

Validation

  • Tests added/updated: tests/test_anticipation_policy.py (new policy suite: roster guard, per-branch full-text message pins, type is int normalization at construction and after fit, transactional set_params rollback, fit-time mutation defense over eight classes, np.uint64 fit-normalization over all fit-path adopters including a TripleDifference staggered fit, hausman_pretest int-parity pin); tests/test_spillover.py (three construct-bad-then-fit tests re-pointed into constructor pins + three mutation-defense fit pins, the None ordering pin retained); tests/test_v4_matrix.py (M-144 row gating: floor/snapshot bumps + docstring enumerations).
  • Backtest / simulation / notebook evidence (if applicable): N/A — validation-only change; targeted estimator suites (staggered, sun_abraham, imputation, two_stage, stacked_did, continuous_did, efficient_did, wooldridge subset, spillover subset, triple_diff, staggered_triple_diff, survey DDD, base_estimator, guides, v4_matrix, naming_guard) all pass.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

All nine anticipation-taking estimators (CallawaySantAnna, SunAbraham,
ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, EfficientDiD,
WooldridgeDiD, SpilloverDiD) now validate anticipation at __init__ via the
shared utils.validate_anticipation (non-negative integer; bool rejected;
set_params transactional via the BaseEstimator probe re-init) AND re-check
it on the fit path - the uniform direct-mutation defense, in the assignment
form: the validator now RETURNS the normalized Python int, so numpy scalars
(np.uint64 included) are normalized before any g-1-anticipation arithmetic
can overflow.

Previously seven of the nine accepted anything, and an out-of-domain window
silently changed the ESTIMAND: measured, CallawaySantAnna(anticipation=-1)
moved the overall ATT -85% and flipped its sign under
control_group="not_yet_treated"; anticipation=True fit bit-identically to 1;
SunAbraham(anticipation=1.5) returned att=nan without raising.

- utils.validate_anticipation: -> int, returns int(anticipation); docstring
  states the family-wide adoption.
- Seven constructors adopt the validating assignment; ContinuousDiD /
  EfficientDiD re-assign inside their param-validation helpers (idempotent
  on the fit-time re-run); TripleDifference keeps its early call and only
  normalizes at the assignment; Wooldridge's static < 0 check is deleted
  (dead anticipation param removed from _validate_constructor_args).
- Fit-path re-checks on all nine + the shared staggered engine (mixin
  docstring updated for the one assigned constructor attribute);
  EfficientDiD.hausman_pretest normalizes its own argument (uint64
  previously wrapped e < -ant and degraded the pretest to NaN-inconclusive).
- SpilloverDiD: eager construction validation + the in-fit re-check kept
  ordered before the ref-period arithmetic (PR #456 R2 guarantee); its three
  construct-bad-then-fit tests re-pointed into constructor pins + three
  mutation-defense fit pins (None kept as the ordering pin).
- New tests/test_anticipation_policy.py (M-081 sweep template): roster
  guard, full-text message pins per validator branch, boundary
  normalization (type is int), set_params rollback, fit-time mutation
  defense over eight classes, uint64 fit-normalization over all fit-path
  adopters incl. a TripleDifference staggered fit, hausman int-parity pin.
- Ledger row M-144 (introduced_in 4.0, phase 5, terminal done) +
  test_v4_matrix count bumps/narrative true-ups + v4-design phase-5 cell
  citation and snapshot narrative + migration-4.0.md "Remaining 4.0
  changes" bullet (numpy-scalar retyping + Wooldridge error-ordering
  disclosed) + REGISTRY family-wide adoption note + nine pointer notes +
  both LLM guides + CHANGELOG entry. TODO row retired.
@github-actions

Copy link
Copy Markdown

Overall assessment

Looks good — no unmitigated P0 or P1 findings.

Executive summary

  • All affected estimators consistently validate and normalize anticipation.
  • Valid-input estimands, weights, variance, and inference remain unchanged.
  • The deprecated StaggeredTripleDifference exception is explicitly documented.
  • No inference anti-patterns, security issues, or untracked debt found.

Methodology

  • Severity: P3 — informational
  • Impact: The nine estimator families, TripleDifference, and EfficientDiD.hausman_pretest now enforce the documented non-negative integer domain. This aligns with their discrete-period anticipation semantics and does not alter valid-input calculations.
  • Concrete fix: None. The policy and deprecated-class exception are documented in docs/methodology/REGISTRY.md:L3200-L3243; normalization is centralized in diff_diff/utils.py:L553-L586.

Code Quality

No findings. Validation is centralized and consistently assigned back to estimator state.

Performance

No findings. The added validation is constant-time.

Maintainability

No findings. The roster guard and shared validator reduce future policy drift.

Tech Debt

No findings. The corresponding TODO.md item is correctly retired; no new untracked limitation was introduced.

Security

No findings. No secrets, unsafe operations, or new external-input surfaces were introduced.

Documentation/Tests

No findings. Tests cover constructor validation, normalization, transactional set_params, fit-time mutation defense, Spillover ordering, and Hausman-pretest parity.

Execution note: the focused tests could not run because this review environment lacks pytest and NumPy; all changed Python files passed static AST parsing.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 16, 2026
@igerber
igerber merged commit d0d3237 into main Aug 16, 2026
39 of 40 checks passed
@igerber
igerber deleted the feat/anticipation-domain-validation branch August 16, 2026 13:03
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