Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions .claude/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,8 @@ everywhere; `to_dict()` / `to_dataframe()` on most classes. See
`.claude/../docs/methodology/REGISTRY.md` per estimator.

### Results Objects — 4.0 TARGET (do not write against this pre-4.0)
Canonical quintet becomes the native fields on every class; `overall_att`
family becomes FutureWarning properties (removed 5.0); one unified
event-study representation; aggregation via post-fit
`results.aggregate(type=)`. Normative spec: `docs/v4-design.md`; per-surface
lifecycle: `docs/v4-deprecations.yaml` (CI-enforced by
`tests/test_v4_matrix.py`).
Target contract: `docs/v4-design.md` section 5; per-surface lifecycle:
`docs/v4-deprecations.yaml` (CI-enforced by `tests/test_v4_matrix.py`).

### Column Naming — CURRENT (3.x)
- `unit` unit id (`unit_col` on HAD; `group` on dCDH — both slated for 4.0)
Expand All @@ -107,10 +103,8 @@ lifecycle: `docs/v4-deprecations.yaml` (CI-enforced by
(`cohort` on WooldridgeDiD — slated for 4.0)
- `covariates` covariate list (`controls` on dCDH — slated for 4.0)

### Column Naming — 4.0 TARGET
`outcome` / `unit` / `time` (calendar) / `post` (0/1) / `treatment` (0/1) /
`first_treat` / `covariates` / `partition` (DDD), no `_col` suffixes. Rules:
`docs/v4-design.md` section 8.
### Column Naming — 4.0 TARGET (do not write against this pre-4.0)
Target vocabulary and rules: `docs/v4-design.md` section 8.

## Session Notes

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
rejecting the design.

### Added
- **4.0-program planning consolidation + alias-diet decision** (internal;
no runtime change). Three consolidation-scope decisions locked 2026-07-31
and recorded in `docs/v4-design.md` §8: no staggered-family mega-merge, no
ImputationDiD↔TwoStageDiD merge, and a moderate **alias diet** — `CDiD`,
`Stacked` and `Gardner` are deprecated at 3.9 and removed at 4.0
(ledger rows M-132..M-134), with the 3.9 `FutureWarning` carried by a
module-level `__getattr__` (M-135, ships in phase 2d) because the
target classes survive; `EDiD` was initially slated but retained (review
evidence: it is the Chen-Sant'Anna-Xie paper's own estimator label); the
never-shipped `Spillover` alias introduction is
cancelled (M-063 → terminal). Ledger row count 110 → 114. `docs/v4-design.md`
§9 is de-staled against the ledger (the phase table gains the shipped
M-122..M-127 insertions, the M-064/M-082 phase-3 entries, and a completed
phase-5 roster with the M-031/M-016 carve-outs) and gains the canonical
"Remaining 3.9 sequence"; the TODO.md naming-guard row's phase-table
agreement spec is amended to the enforceable two-direction form; duplicated
flip-plan prose in DEFERRED.md and the REGISTRY M-010 note is trimmed to
cross-links.
- **`df_convention` becomes a library-wide three-value knob** ([M-127]).
`{"residual", "cluster", "normal"}` is now accepted everywhere the knob
exists, and the knob extends to five standalone estimators: `SunAbraham`,
Expand Down
2 changes: 1 addition & 1 deletion DEFERRED.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ here carry the remaining implementation work and cross-link their `M-xxx` ids.

| Issue | Location | PR | Priority |
|-------|----------|----|----------|
| Clustered inference df **default flip to `"cluster"` (G−1) at v4** — the `df_convention=` knob is now the library-wide THREE-VALUE surface (PR C / M-127, 3.9: DiD/TWFE/MPD/LinearRegression + SunAbraham/WooldridgeDiD-OLS/StackedDiD/ImputationDiD-pretrends at default `"residual"`, LPDiD already at `"cluster"`); the remaining work is the major-version default change (moves every clustered p-value/CI) + migration note + flipping `TestDfConvention`/`test_moderate_t_pins_residual_df_convention`/the per-estimator knob suites' expectations. Lifecycle tracked in docs/v4-deprecations.yaml (M-004..M-006 + M-128..M-131). | `diff_diff/linalg.py::LinearRegression`, `diff_diff/estimators.py`, `diff_diff/twfe.py`, `diff_diff/sun_abraham.py`, `diff_diff/wooldridge.py`, `diff_diff/stacked_did.py`, `diff_diff/imputation.py` | — | Medium |
| Clustered inference df **default flip at v4** (lifecycle + roster: docs/v4-deprecations.yaml M-004..M-006 + M-128..M-131; knob semantics: v4-design §7). Implementation work the ledger rows do not carry: the migration note, and flipping the test expectations that pin the current default (`TestDfConvention`, `test_moderate_t_pins_residual_df_convention`, the per-estimator knob suites). | `diff_diff/linalg.py::LinearRegression`, `diff_diff/estimators.py`, `diff_diff/twfe.py`, `diff_diff/sun_abraham.py`, `diff_diff/wooldridge.py`, `diff_diff/stacked_did.py`, `diff_diff/imputation.py` | — | Medium |

## Decision record — won't-fix / waived

Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m
| Tighten the mypy suppressions that back the enforced-zero posture: burn down `prep_dgp`'s per-module `[index]` override (needs a None-vs-array restructure that preserves the seeded RNG stream), and evaluate re-enabling the globally disabled codes (`arg-type`, `return-value`, `var-annotated`, `assignment`) one at a time — `assignment` alone hid several real annotation drifts found during the 2026-07 triage. | `pyproject.toml` `[tool.mypy]`, `diff_diff/prep_dgp.py` | lint-CI | Mid | Low |
| MMM interop follow-up: Meridian `roi_calibration_period` mask builder - accept the MMM's time index + channel order and emit the boolean `(n_media_times, n_media_channels)` mask so `.to_code()` scopes the prior to the experiment window automatically (today the caller passes a mask expression / `full_model_window=True`). | `diff_diff/mmm.py` | mmm-interop | Quick | Low |
| MMM interop PR-B: calibration tutorial notebook (fit DiD/CS -> scope -> `to_pymc_marketing_lift_test` / `to_meridian_roi_prior`) + a `llms-practitioner.txt` Step 8 pointer to the exporters as the MMM hand-off. | `docs/tutorials/`, `diff_diff/guides/llms-practitioner.txt` | mmm-interop | Mid | Low |
| Section-8 naming-completeness guard test: sweep the public surface (module-level functions, class `__init__`/`fit` params, results dataclass fields) for contract-rename violations and fail unless each hit is either a `docs/v4-deprecations.yaml` row or an allowlist entry carrying a stated reason (seed the allowlist with the documented domain-vocabulary exemptions: the staggered family's ATT(g,t) `group`/`groups`, `TripleDifference.fit[group]`, wrapper params dying with `M-070..M-077`, and params inherited from `DifferenceInDifferences.__init__`). Today the rules are normative prose and the ledger holds only what an audit remembered — two successive manual sweeps each found surfaces the prior one missed (`M-094`/`M-095`, then the 19-row `M-097..M-115` function sweep). Should land BEFORE Phase 2c so the rename PR works from a mechanically-verified list. Two further checks belong in the same guard, each having been missed by hand across successive review rounds: (a) **phase-table agreement** — every row's `phase` must appear in the matching `docs/v4-design.md` section 9 table entry and vice versa (the section 9 checklist asserts this agreement but nothing verifies it, and three separate rounds caught a stale table); (b) **consumer coverage** (section 8 rule 11) — for each rename row, grep `diff_diff/` and `docs/methodology/` for the old name and require every hit to be either in the row's `code_refs` or allowlisted, since `getattr(obj, "old_name", default)` degrades silently rather than raising after removal. | `tests/`, `docs/v4-design.md` | gating-completeness amendment | Mid | Medium |
| Section-8 naming-completeness guard test: sweep the public surface (module-level functions, class `__init__`/`fit` params, results dataclass fields) for contract-rename violations and fail unless each hit is either a `docs/v4-deprecations.yaml` row or an allowlist entry carrying a stated reason (seed the allowlist with the documented domain-vocabulary exemptions: the staggered family's ATT(g,t) `group`/`groups`, `TripleDifference.fit[group]`, wrapper params dying with `M-070..M-077`, and params inherited from `DifferenceInDifferences.__init__`). Today the rules are normative prose and the ledger holds only what an audit remembered — two successive manual sweeps each found surfaces the prior one missed (`M-094`/`M-095`, then the 19-row `M-097..M-115` function sweep). Should land BEFORE Phase 2c so the rename PR works from a mechanically-verified list. Two further checks belong in the same guard, each having been missed by hand across successive review rounds: (a) **phase-table agreement** (spec amended 2026-07-31; three separate rounds caught a stale table and nothing verifies the section 9 checklist's assertion) — direction 1: every row's current `phase` appears in the matching `docs/v4-design.md` section 9 table entry; direction 2: every id cited in a cell resolves to a live ledger row whose lifecycle includes work in that phase, mechanically: a citation of row R in the cell of phase P is legitimate iff P's ship-version (phases 2/3/4 → "3.9", phase 5 → "4.0", phase 6 → "4.1") appears among R's `introduced_in`/`deprecated_in`/`removed_in` — or `decision_due` for `env-default` rows (M-008 is scheduled solely via `decision_due: "4.0"` and its phase-5 citation must pass; make it a guard fixture) — (accepts shim-phase rows re-listed in the phase-5 removal roster; rejects a row with no lifecycle work at that version, e.g. M-031 cited in phase 5). Terminal rows (`done`/`removed`) are exempt in both directions. Parsing rule: cells cite ids as single tokens (`[M-122]`), compound single brackets (`[M-030..M-047]`, comma compounds), and endpoint-bracket ranges (`[M-132]..[M-135]`) — the guard's parser expands BOTH range forms plus comma compounds when computing cell membership (single-token matching alone yields zero interior ids). Known accepted limitation: phases 2 and 3 both ship 3.9, so the predicate cannot catch a row cited in the wrong 3.9 phase; (b) **consumer coverage** (section 8 rule 11) — for each rename row, grep `diff_diff/` and `docs/methodology/` for the old name and require every hit to be either in the row's `code_refs` or allowlisted, since `getattr(obj, "old_name", default)` degrades silently rather than raising after removal. (Sequenced in v4-design §9.) | `tests/`, `docs/v4-design.md` | gating-completeness amendment | Mid | Medium |
| Tracking-file contract guard test: reject NEW active deferred-work pointers at `TODO.md` (deferred rows live in `DEFERRED.md`; allowlist for historical/past-tense prose and actionable-row pointers) and assert rows cross-linking a `docs/v4-deprecations.yaml` `M-xxx` id don't restate ledger status. Origin: tracking-split local review R2. | `tests/`, `TODO.md`, `DEFERRED.md` | tracking-split | Quick | Low |
| Real-data CI canary for dataset-backed replication tests: `test_methodology_lwdid.py`'s Prop 99 / Walmart goldens skip (visibly) when loaders fall back to synthetic; add a lane or canary asserting `df.attrs["source"] == "lwdid_ssc_ancillary"` in CI so network regressions cannot silently de-gate the replication tests. Follow-on from the loader-fallback repair (#723), which made provenance explicit but deliberately did not add a network-dependent CI lane. | `tests/test_methodology_lwdid.py`, `.github/workflows/` | LWDiD validation suite | Quick | Low |
| df-provenance completion follow-up (PR C descoping): a Wooldridge public per-row df channel (a per-key dict that can also carry the hc2_bm BM contrast dofs the scalar `_df_analytic_fallback` cannot represent — the unified surface's Wooldridge df column stays NaN until then); ImputationDiD lead-horizon `event_study_df` (needs the `_compute_lead_coefficients`/`_aggregate_event_study` return-path change plus bootstrap-clearing semantics for post rows); and the `inference_df` (DiD/MPD/SA/StackedDiD) vs `df_inference` (CS/dCDH) adapter-naming unification. | `diff_diff/wooldridge_results.py`, `diff_diff/imputation.py`, `diff_diff/results_base.py` | #variance-PR-C | Mid | Low |
Expand Down
11 changes: 3 additions & 8 deletions docs/methodology/REGISTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,9 @@ under H0 of parallel trends, δ_e = 0 for all e < 0.
`fixest::feols(... | unit + time)` parity target) and the pooled default
produce identical point estimates only under balanced panels with no
covariates and simultaneous adoption; with unbalanced panels or covariates
the two projections differ. The 4.0 program (see `docs/v4-design.md`
section 4.1 and `docs/v4-deprecations.yaml` [M-010]) migrates the
event-study default to the unit-FE spec on the merged TwoWayFixedEffects,
keeping the pooled model reachable via `spec="pooled"` (required for
repeated cross-sections and for reproducing 3.x MPD numbers exactly); the
Phase 3 PR gates on a balanced-equivalence test, an
unbalanced-or-covariate divergence test, and pooled bit-exact parity vs
3.x MPD.
the two projections differ. The 4.0 merge changes the event-study default
to the unit-FE spec with the pooled model reachable via `spec="pooled"` —
plan and gates: `docs/v4-design.md` section 4.1, ledger row [M-010].

Post-treatment coefficients (e ≥ 0) estimate dynamic treatment effects.

Expand Down
64 changes: 59 additions & 5 deletions docs/v4-deprecations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
#
# FORMAT CONTRACT (parsed by a purpose-built scanner, no PyYAML): top-level
# `rows:` list; each row a flat mapping of single-line scalars; `code_refs` as
# an inline flow list; no anchors, no multi-line strings. Comments allowed.
# an inline flow list; no anchors, no multi-line strings. Line-leading comments
# only (a trailing `#` after a value is parsed as part of the value).
#
# Locator grammar: "diff_diff:Name" (top-level export), "diff_diff:Class[param]"
# (__init__ param), "diff_diff:Class.method[param]" (method param),
Expand Down Expand Up @@ -762,14 +763,14 @@ rows:
old: "Spillover"
new: null
old_target: null
new_target: "diff_diff:SpilloverDiD"
introduced_in: "3.9"
new_target: null
introduced_in: null
deprecated_in: null
removed_in: null
status: planned
status: done
phase: 2
code_refs: [diff_diff/__init__.py]
notes: "Introduce-only; follows the Stacked=StackedDiD precedent."
notes: "CANCELLED 2026-07-31 (alias-diet decision): the planned Spillover=SpilloverDiD introduction will never ship; row retained as an anti-resurrection pin (ids are never deleted; done with null new_target asserts the name stays absent)."

# ---- Module-level function wrappers (class form is canonical) ------------
- id: M-070
Expand Down Expand Up @@ -1457,3 +1458,56 @@ rows:
new_default: "'cluster'"
code_refs: [diff_diff/imputation.py]
notes: "PR C ([M-127]) extended the knob here with default 'residual' (pretrends lead regression only; BJS overall inference and the joint pretrend Wald F are knob-independent); flips with [M-004]..[M-006] at 4.0; covered by the same migration note."

# ---- Alias diet (2026-07-31 consolidation-scope decision) ----------------
- id: M-132
kind: alias
group: alias-table
old: "CDiD"
new: null
old_target: "diff_diff:ContinuousDiD"
new_target: null
deprecated_in: "3.9"
removed_in: "4.0"
status: planned
phase: 5
code_refs: [diff_diff/__init__.py, docs/api/estimators.rst, diff_diff/guides/llms-full.txt, diff_diff/guides/llms-practitioner.txt, tests/test_aliases.py]
notes: "Alias diet 2026-07-31: not an established literature name and one edit from CiC/DiD/DCDH. Target class survives, so no parent-class shim can carry the warning - the 3.9 FutureWarning rides the module __getattr__ shim ([M-135]); removal at 4.0. code_refs list the known reader surfaces (section 8 rule 11); the 2d/removal PRs also grep repo-wide."
- id: M-133
kind: alias
group: alias-table
old: "Stacked"
new: null
old_target: "diff_diff:StackedDiD"
new_target: null
deprecated_in: "3.9"
removed_in: "4.0"
status: planned
phase: 5
code_refs: [diff_diff/__init__.py, docs/api/estimators.rst, diff_diff/guides/llms-full.txt, diff_diff/guides/llms-practitioner.txt, diff_diff/guides/llms-autonomous.txt, tests/test_aliases.py, diff_diff/diagnostic_report.py, diff_diff/business_report.py, diff_diff/power.py]
notes: "Alias diet 2026-07-31: a vague English adjective saving three characters over StackedDiD, with zero lookup value. Target class survives - the 3.9 FutureWarning rides the module __getattr__ shim ([M-135]); removal at 4.0. code_refs list the known reader surfaces; the 2d/removal PRs also grep repo-wide."
- id: M-134
kind: alias
group: alias-table
old: "Gardner"
new: null
old_target: "diff_diff:TwoStageDiD"
new_target: null
deprecated_in: "3.9"
removed_in: "4.0"
status: planned
phase: 5
code_refs: [diff_diff/__init__.py, docs/api/estimators.rst, diff_diff/guides/llms-full.txt, diff_diff/guides/llms-practitioner.txt, diff_diff/guides/llms-autonomous.txt, tests/test_aliases.py, diff_diff/diagnostic_report.py, diff_diff/practitioner.py, diff_diff/business_report.py]
notes: "Alias diet 2026-07-31: author surname as a class name; the ecosystem name for the method is did2s and 'Gardner (2022)' prose citations are unaffected. Target class survives - the 3.9 FutureWarning rides the module __getattr__ shim ([M-135]); removal at 4.0. code_refs list the known reader surfaces; the 2d/removal PRs also grep repo-wide."
- id: M-135
kind: behavior
group: alias-table
old: "diff_diff:__getattr__"
new: null
introduced_in: "3.9"
deprecated_in: null
removed_in: null
status: planned
phase: 2
code_refs: [diff_diff/__init__.py]
notes: "Alias-diet warning mechanism: CDiD/Stacked/Gardner served via a PEP 562 module __getattr__ emitting FutureWarning naming the surviving class. Names stay in __all__ through 3.9 but leave module globals: dir()/vars() shrink symmetrically (test_dir_returns_full_module_namespace unaffected) and star-import fires the three warnings - accepted diet behavior, documented in v4-design section 3.2. Ships in phase 2d; test_due_rows_are_terminal forces it terminal before any 3.9 bump. Removals are [M-132]..[M-134]."
Loading
Loading