Skip to content

Commit bdb4fd0

Browse files
committed
test(dml): replicate Chang (2020) §4.2.2 RCS simulation DGP (DML PR-B2)
Replaces the "paper's own §4 RCS DGPs are NOT replicated" caveat with the real thing, scoped by five dual-review rounds that executed the candidate designs: - §4.2.2 (kernel-estimation design, arXiv v3 p. 19) replicated verbatim in tests/test_methodology_dml_did.py: an exact-draw-order frame generator; a DGP-shape pin covering the distributions, all three innovation scales (0.1/0.2/0.3), stationary-sampling checks (T independent of (D, X)), the design's built-in confounded unadjusted contrast (-> theta0 + 1 = 4, its covariate-adjustment point), and both correct-specification facts (true propensity sigmoid(X - 1/2) by Bayes; true l20 = 0.25 X); seed-pinned recovery at both paper Ns (sieve + linear at N=500, linear at N=200, each with a 4*se gate, a seed-pin absolute bound, and a discriminating comparison requiring the adjusted estimate to beat the unadjusted contrast); and a slow MC coverage class (the file's third), seeds offset from the incumbent streams. Fits run under a strict warning gate that tolerates exactly the documented panel=False Assumption 2.3 UserWarning and the occasional propensity-trimming UserWarning (rare fitted-tail excursions on the unbounded-X design) and fails on anything else. "N(0, 0.1)" adopted as VARIANCE 0.1 with the ambiguity recorded. - §4.2.1 (ML-estimation design) extracted into the paper review but documented as NOT replicable with the bundled unpenalized learners: order-of-magnitude noise at (N=500, p=100) (SE ~4-10, EPV ~1.4-1.6 vs the solve_logit threshold 10, fitted out-of-fold clipping ~5-29% of rows - estimated propensities; true-propensity tail mass is only ~0.06%), and a fail-closed linear-outcome rank deficiency at (N=200, p=100) even under an oracle propensity (ridge/sieve fit there). TODO row narrowed accordingly (penalized propensity learner + p-robust outcome learner); the review gains the full §4.2 parameterizations, a ninth suspected-typo entry (the printed-but-unused beta_0, with the trend-cancellation note), and a Gaps entry for the paper's own regularity-condition quirks (Gaussian kernel vs Assumption 3.3; unbounded-index propensities vs a.s. strict overlap 3.2(a) in BOTH §4.2 designs). - REGISTRY: Case 2 checklist bullet updated + a standalone replication-scope Note (native learners stand in for kernel first stages; overlap quirk; §4.2.1 evidence). dr_scores test comment updated (parameterization now extracted; §4.2.2 replicated). CHANGELOG Added entry. Verified: default lane + both existing suites green on Rust and pure-Python backends; the new MC coverage class green on both (200 reps Rust / 22-rep reduced band pure-Python); the seed-pinned recovery values reproduce the review measurements exactly (att 2.7581/2.9054/2.5580, unadjusted contrast 4.008/3.978/3.617).
1 parent d9b93dd commit bdb4fd0

6 files changed

Lines changed: 226 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **Chang (2020) §4.2.2 RCS simulation-DGP replication** (`tests/test_methodology_dml_did.py`,
12+
DML PR-B2): the paper's own kernel-design repeated-cross-section DGP as
13+
maintainer validation fixtures for `DMLDiD(panel=False)` — a DGP-shape pin
14+
(distributions, all three innovation scales, the design's built-in confounded
15+
contrast → θ₀+1, and both correct-specification facts), seed-pinned recovery at
16+
both paper sample sizes with a discriminating comparison against the unadjusted
17+
contrast, and a slow Monte Carlo coverage lane. The §4.2 parameterizations are
18+
extracted into the paper review; the §4.2.1 ML design is documented as not
19+
replicable with the bundled unpenalized learners (narrowed TODO row) and the
20+
REGISTRY carries the replication-scope Note.
21+
1022
### Fixed
1123
- **Per-row event-study df provenance (M-092 completion)** for the four
1224
remaining holes — `EfficientDiD`, `ImputationDiD`, `ContinuousDiD`, and

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m
7979

8080
| Issue | Location | Origin | Effort | Priority |
8181
|-------|----------|--------|--------|----------|
82-
| Replicate Chang (2020) §4's own RCS simulation DGPs (pp. 17-21, "fully specified" per the paper review) as recovery/coverage fixtures for the `DMLDiD(panel=False)` lane — the shipped tests use a library-authored RCS design (documented in the REGISTRY checklist caveat); needs the paper PDF to extract the parameterization | `tests/test_methodology_dml_did.py`, `docs/methodology/papers/chang-2020-review.md` | DML PR-B2 | Mid | Low |
82+
| Replicate Chang (2020) §4.2.1 ML-design RCS DGP — requires a penalized (Lasso-class) propensity learner or a maintainer fixture via the duck-typed `propensity_learner=` object route; native logit+linear verified pure noise at (N=500, p=100) across ~20 review seeds (att ~ −23..+16, SE ~4-10, EPV ~1.4-1.6 vs threshold 10, fitted out-of-fold clipping ~5-29%, mean ~14%); at (N=200, p=100) `outcome_learner="linear"` fails closed on control-fold rank deficiency even under an oracle propensity (use ridge/sieve there); §4.2.2 is replicated | `tests/test_methodology_dml_did.py`, `diff_diff/_learners.py` | DML PR-B2 | Mid | Low |
8383
| Optional scheduled end-to-end execution gate for the MMM tutorials (29/30): a cron-only workflow (or extension of `mmm-interop.yml`) that executes both notebooks in isolated exact-pin environments, so a stale/invalid committed posterior cannot stay green indefinitely - today the hybrid posture (deliberate: notebooks execute locally with committed outputs; CI smoke-tests the exporters without sampling; drift tests pin source + committed-output needles) leaves the MCMC claims un-re-executed in CI | `.github/workflows/mmm-interop.yml`, `docs/tutorials/29_mmm_calibration_pymc.ipynb`, `docs/tutorials/30_mmm_calibration_meridian.ipynb` | mmm-interop | Mid | Low |
8484
| Committed `fixest::feols` event-study golden for TWFE `event_study=True` (within + pooled specs, unbalanced + covariate panels, matched CR1 cluster convention, per-period effects + vcov block) - the in-suite gates are shared-core cross-checks (TWFE-within == MPD-absorb, pooled == MPD bit-exact), so a defect common to the shared core would pass; the live-R harness (`benchmarks/R/benchmark_multiperiod.R`, `feols(y ~ treated * time_f \| unit)`) validated the within design in `docs/benchmarks.rst` but is not a committed regression test - follow the `fixest_did_twfe_golden.json` committed-golden pattern (pytest.skip when absent) | `tests/test_fixest_did_twfe_parity.py`, `benchmarks/R/` | 3(a) R2 | Mid | Medium |
8585
| Type-blind `n_bootstrap` acceptance in already-validated estimators - HAD bool (`isinstance(..., int)` passes `True`, runs as 1 replicate), dCDH bool+float (its bare `< 0` check passes both `True` and `2.5`), TROP float (`2.5` passes the `>= 2` floor), SyntheticDiD float under all three variance methods + bool/negative under jackknife (its floor check is skipped there) - align these local checks with the `utils.validate_n_bootstrap` type guard (M-081 kept them out of the sweep: it scoped to previously-UNvalidated estimators only) | `diff_diff/had.py`, `diff_diff/chaisemartin_dhaultfoeuille.py`, `diff_diff/trop.py`, `diff_diff/synthetic_did.py` | 2(d) PR-B | Quick | Low |

docs/methodology/REGISTRY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3129,6 +3129,8 @@ the finite-dimensional `p_0` is handled by the variance correction below.
31293129
at 0.0 observed diff under identical folds and sklearn learners).
31303130
- R / Stata: none — the paper ships no companion package.
31313131

3132+
- **Note:** Chang §4.2 simulation-DGP replication scope (PR-B2). The §4.2.2 kernel-design RCS DGP (arXiv v3 p. 19) is replicated verbatim in `tests/test_methodology_dml_did.py`, estimated with the library's NATIVE learners rather than the paper's Gaussian-kernel first stages (no kernel first stage is bundled); the fixture is correctly specified on both nuisances (true propensity `sigmoid(X − 1/2)` by Bayes' rule; true `ℓ₂₀ = 0.25·X`), so it validates paper-DGP faithfulness — the discriminating assertion compares against the design's own confounded unadjusted contrast (→ θ₀ + 1), not against a misspecified learner. The design's unbounded X means the a.s. strict-overlap Assumption 3.2(a) holds for no fixed κ > 0 (mass outside [0.01, 0.99] ≈ 2.1e-5 — immaterial at the fixture Ns; the same quirk applies more strongly to §4.2.1, tail ≈ 0.06%). The §4.2.1 ML design is NOT replicable with the bundled unpenalized learners: at (N=500, p=100) the native logit+linear stack is order-of-magnitude noise across seeds (SE ~4-10; `solve_logit` EPV ~1.4-1.6 vs threshold 10; fitted out-of-fold clipping ~5-29% of rows — estimated propensities, not true ones, whose out-of-[0.01, 0.99] mass is ≈0.06%), and at (N=200, p=100) the linear outcome learner fails closed on control-fold rank deficiency even under an oracle propensity (ridge/sieve fit there) — tracked in TODO.md (penalized propensity learner + p-robust outcome learner).
3133+
31323134
**Requirements checklist (shipped Case 1 panel + Case 2 RCS staggered lanes):**
31333135
- [x] Neyman-orthogonal Case 1 score (3.1) implemented exactly (Abadie score + mean-zero adjustment; `chang_panel_score`)
31343136
- [x] DML2 cross-fitting: per-cell K-fold partition, nuisances fit on fold complements, never on the evaluation fold
@@ -3139,7 +3141,7 @@ the finite-dimensional `p_0` is handled by the variance correction below.
31393141
- [x] Per-cell degenerate guards (zero treated/control, cell < K, singleton stratum, empty untreated complement) — closed skip vocabulary, consolidated warning
31403142
- [x] Normal-approximation inference via `safe_inference()`
31413143
- [x] Validation: 2-period DoubleMLDID + staggered per-cell DoubleMLDIDBinary parity spikes (version-pinned, committed, golden literals consumed in-tests) + oracle-nuisance closed-form equivalence + degenerate-cell hand-pipeline equivalence (rtol 1e-14 — BLAS reduces differently-laid-out inputs in platform-dependent order, so bit identity does not hold cross-platform) + Monte Carlo coverage sanity
3142-
- [x] Case 2 (repeated cross sections): Equation 3.2 score + λ-corrected Theorem 2 variance — SHIPPED as `DMLDiD(panel=False)` (`chang_rcs_score` / `chang_rcs_lambda_slope` / `chang_rcs_score_augmented`; equation-level fixtures, oracle closed forms, derivative-identity checks, DR both directions, characterization spike, MC coverage). The paper's own §4 RCS simulation DGPs are NOT replicated (tracked TODO row — needs the paper PDF pp. 17-21); the shipped recovery tests use a library-authored RCS design.
3144+
- [x] Case 2 (repeated cross sections): Equation 3.2 score + λ-corrected Theorem 2 variance — SHIPPED as `DMLDiD(panel=False)` (`chang_rcs_score` / `chang_rcs_lambda_slope` / `chang_rcs_score_augmented`; equation-level fixtures, oracle closed forms, derivative-identity checks, DR both directions, characterization spike, MC coverage). The paper's own §4.2.2 kernel-design RCS DGP is replicated (`tests/test_methodology_dml_did.py` "Chang Sec. 4.2.2" section: DGP-shape pin, seed-pinned recovery at both paper Ns with a discriminating comparison against the design's confounded unadjusted contrast, slow MC coverage — see the §4.2 replication Note below); the §4.2.1 ML design is not replicable with the bundled learners (narrowed TODO.md row).
31433145
- [ ] Case 3 (multilevel treatment): deferred (`DEFERRED.md`; implementation-required overlap conditions per the paper review's Case 3 caution)
31443146

31453147
---

docs/methodology/papers/chang-2020-review.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ items remain open, tracked in DEFERRED.md):
192192
- [x] Normal-approximation inference via `safe_inference()` (no-design fits; survey/bare-cluster fits use finite-df t inference via `df=df_survey` — library extension, not from the paper)
193193
- [ ] Multilevel treatment (Case 3): open — DEFERRED
194194
- [ ] Case 3 guards: open — DEFERRED (see the Case 3 caution above)
195-
- [x] Validation (Case 1): `doubleml.DoubleMLDID` (2-period) + `DoubleMLDIDBinary` (staggered per-cell, end-to-end public fit) parity spikes, doubleml==0.11.4 pinned, golden literals in-repo; [x] Case 2 equation-level fixtures — SHIPPED (closed-form/oracle fixtures, derivative-identity checks, DR both directions, `DoubleMLDIDCSBinary` characterization spike — no parity oracle exists). CAVEAT: the paper's own §4 RCS simulation DGPs (pp. 17-21) are NOT replicated — the shipped recovery/coverage tests use a library-authored RCS design; replication is a tracked TODO.md row (needs the paper PDF); [ ] Case 3 fixtures — open
195+
- [x] Validation (Case 1): `doubleml.DoubleMLDID` (2-period) + `DoubleMLDIDBinary` (staggered per-cell, end-to-end public fit) parity spikes, doubleml==0.11.4 pinned, golden literals in-repo; [x] Case 2 equation-level fixtures — SHIPPED (closed-form/oracle fixtures, derivative-identity checks, DR both directions, `DoubleMLDIDCSBinary` characterization spike — no parity oracle exists). [x] §4.2.2 kernel-design RCS DGP replicated (`tests/test_methodology_dml_did.py`, "Chang Sec. 4.2.2" section: DGP-shape pin, seed-pinned recovery at both paper Ns with a discriminating comparison against the design's own confounded unadjusted contrast, slow MC coverage; the library's native learners stand in for the paper's Gaussian-kernel first stages, and the fixture is correctly specified on BOTH nuisances, so it adds paper-DGP faithfulness, not plug-in-bias reproduction). The §4.2.1 ML-design DGP is extracted below but NOT replicable with the bundled unpenalized learners — narrowed TODO.md row (penalized propensity learner + ridge/sieve outcome learner required); [ ] Case 3 fixtures — open
196196

197197
---
198198

@@ -244,6 +244,8 @@ items remain open, tracked in DEFERRED.md):
244244
7. **Case 2 response in the Lasso penalty-loading recipe (appendix "The Lasso Penalty", p. 26):** the recipe literally prints "Let `y_i` denote `Y_i(1) - Y_i(0)` or `(T_i - λ̂_k)`" — the Case 2 response omits the `Y_i` multiplier (verified against the PDF page 2026-08-22), while the modified-Lasso objective it feeds (p. 12) minimizes over `(T_i - λ̂_k)·Y_i - q_i'β`. Loadings computed on a bare `(T_i - λ̂_k)` would produce a different first-stage estimator; implement loadings on the objective's response, `(T_i - λ̂_k)·Y_i`, pending comparison with the published article.
245245
8. **`ȳ_k` population/denominator inconsistency (p. 26):** `ȳ_k = M^{-1} Σ_{i∈I_k^c} y_i` is defined as the mean over the FULL auxiliary sample (size `M`), while both loading sums run over the untreated subset `I_kz^c` with normalizer `1/M_k` (verified against the PDF page 2026-08-22). Whether the centering mean was intended over `I_k^c` or `I_kz^c` is not resolvable from the arXiv text; record the choice made at implementation time and compare with the published article.
246246

247+
9. **§4.2.1's printed-but-unused `β_0` (p. 19):** the RCS ML simulation section prints "where `β_0 = γ_0 + 0.5`" although `β_0` appears in none of its equations (`Y⁰(0) = 1 + ε_1` as printed), while §4.1.1 (p. 18) and §4.3.1 (p. 20) print `Y⁰(0) = X'β_0 + ε_1` in the parallel position — a suspected dropped `X'β_0` term; not resolvable from the arXiv text. Either reading leaves §4.2.1's parallel trends intact (the level term cancels in the recursive trend `Y⁰(1) = Y⁰(0) + 1 + ε_2`) and `ℓ_20` constant in X, so the library's §4.2.1 non-replicability conclusion (above) is robust to the ambiguity; check the published version.
248+
247249
**Contradiction — where `p̂_k` (and `λ̂_k`) is computed (algorithms vs proofs):**
248250
- Both printed algorithm statements agree with each other and were verified against the PDF pages directly (2026-08-22): main-text Algorithm 1 (p. 11) prints `p̂_k = (1/n) Σ_{i∈I_k^c} D_i` and the appendix multilevel algorithm (p. 25, step 2) prints `p̂_w = (1/n) Σ_{i∈I_k^c} D_i` — in both cases a `1/n` normalizer (n = |I_k|) over the **auxiliary sample `I_k^c`** (size `M = N - n`), which is not a valid mean of anything as printed (typo 1 above).
249251
- The proofs of Theorems 1-2, however, use `p̂_k = E_{n,k}[D]` (fold mean over the **main fold `I_k`**, pp. 31, 33, 37), i.e. `p̂_k - p_0 = E_{n,k}[D - p_0]`, which is what generates the `G_1p0 (D - p_0)` influence-correction term in the variance algebra.
@@ -258,6 +260,8 @@ items remain open, tracked in DEFERRED.md):
258260
- Multilevel treatment: variance estimator and full theorem statements are not printed in the reviewed text (Theorems 2/4 cover Cases 1-2 only; multilevel results "can be proven using the same arguments", p. 13). The analogy `Ĝ_wp = -θ̃_w/p̂_w` is plausible but NOT stated in the paper.
259261
- Theorem 2 proof for the repeated-cross-sections case concludes within the reviewed range (pp. 51-56); no gaps remain in the proofs of Theorems 1-4 as reviewed, but the appendix's stated Lasso-penalty constants `c`, `γ`, `B` have no defaults (p. 26).
260262
- No clustering, no bootstrap, no missing-data handling, no aggregation across multilevel treatment arms.
263+
- The §4 simulation designs violate the paper's own regularity conditions in two symmetric ways: the Gaussian kernel used for the kernel-estimation variants violates Assumption 3.3's compact-support requirement (see the Tuning Parameters kernel row), and BOTH §4.2 propensities are unbounded-Gaussian-index logistics, so the a.s. strict-overlap Assumption 3.2(a) (`Pr(κ ≤ g_0(X) ≤ 1−κ) = 1` for fixed `κ > 0`) holds for NO fixed κ — §4.2.1's tail mass outside [0.01, 0.99] is ≈0.06% (`X'γ_0 ~ N(0.685, 1.21²)`), §4.2.2's ≈2.1e-5 (immaterial at the simulated Ns, but a theory/design inconsistency worth recording).
261264
- Empirical application (Section 5, pp. 21-22, repeated cross sections via Eqs. 2.2/3.2): Sequeira (2016) tariff/bribery data, N = 1084. Table 1 exact estimate (SE) pairs (verified against the PDF 2026-08-22): Sequeira (2016) TWFE -2.928 (0.944); Abadie kernel -7.986 (3.028); orthogonal θ̃ kernel -8.670 (3.643); Abadie Lasso -7.499 (2.746); orthogonal θ̃ Lasso -9.191 (4.854) — usable as a rough replication target only if the Sequeira data is obtainable.
262-
- Simulation DGPs (Section 4, pp. 17-21) are fully specified and are the recommended validation fixtures: e.g. 4.1.1 repeated outcomes ML: `N ∈ {200, 500}`, `p ∈ {100, 300}`, `X ~ N(0, I_p)`, `γ_0 = (1, 1/2, 1/3, 1/4, 1/5, 0, ...)`, logistic PS, `β_0 = γ_0 + 0.5`, `θ_0 = 3`, errors N(0, 0.1) (variance/SD as printed "N(0,0.1)" — ambiguous); 4.3.1 multilevel: `W ∈ {0,1,2}`, shares (0.3, 0.3, 0.4), `θ_10 = 3`, `θ_20 = 6`.
265+
- Simulation DGPs (Section 4, pp. 17-21) are fully specified and are the recommended validation fixtures: e.g. 4.1.1 repeated outcomes ML: `N ∈ {200, 500}`, `p ∈ {100, 300}`, `X ~ N(0, I_p)`, `γ_0 = (1, 1/2, 1/3, 1/4, 1/5, 0, ...)`, logistic PS, `β_0 = γ_0 + 0.5`, `θ_0 = 3`, errors N(0, 0.1) (variance/SD as printed "N(0,0.1)" — ambiguous as printed; the shipped §4.2.2 fixtures ADOPT the variance-0.1 reading, `σ = √0.1`, stated in the fixture comment); 4.3.1 multilevel: `W ∈ {0,1,2}`, shares (0.3, 0.3, 0.4), `θ_10 = 3`, `θ_20 = 6`.
266+
- §4.2 repeated-cross-section DGPs (pp. 18-20, extracted 2026-08-29 for the PR-B2 fixtures). **§4.2.1 (ML estimation):** `N ∈ {200, 500}`, `p ∈ {100, 300}`, `X_i ~ N(0.3·1, I_p)` (note the 0.3 mean, unlike §4.1.1), same `γ_0`, logistic PS `P(D=1|X) = sigmoid(X'γ_0)`; as printed `Y⁰(0) = 1 + ε_1`, `Y⁰(1) = Y⁰(0) + 1 + ε_2`, `Y¹(1) = θ_0 + Y⁰(1) + ε_3`, `θ_0 = 3`, `T ~ Bernoulli(0.5)`, observed `Y = Y(0) + T(Y(1) − Y(0))` (see suspected-typo 9 on the printed-but-unused `β_0`). Under our native unpenalized learners this design is NOT estimable: at (N=500, p=100) the logit+linear stack is pure noise across ~20 review seeds (att ~ −23..+16, SE ~4-10; `solve_logit`'s EPV guard fires every fold at EPV ~1.4-1.6 vs threshold 10; FITTED out-of-fold clipping ~5-29% of rows across seeds, mean ~14% — the estimated-propensity leg of strict overlap; TRUE-propensity mass outside [0.01, 0.99] is only ≈0.06%, `X'γ_0 ~ N(0.685, 1.21²)`), and at (N=200, p=100) `outcome_learner="linear"` fails closed on control-fold rank deficiency even under an oracle propensity (ridge/sieve fit there). Reproduce by generating the equations above at (500, 100) with `DMLDiD(panel=False)` and native learners over any batch of ~20 seeds — the qualitative behavior is seed-robust; the ranges are deliberately qualitative, not seed-pinned. **§4.2.2 (kernel estimation):** `N ∈ {200, 500}`, `D ~ Bernoulli(0.5)`, scalar `X | D ~ N(D, 1)`, `Y⁰(0) = ε_1`, `Y⁰(1) = Y⁰(0) + X + ε_2`, `Y¹(1) = θ_0 + Y⁰(1) + ε_3`, `θ_0 = 3`, `T ~ Bernoulli(0.5)`, same observation rule. True propensity `= sigmoid(X − 1/2)` by Bayes' rule; true `ℓ_20(X) = λ_0(1−λ_0)·X = 0.25·X`. The trend `X + ε_2` with group-imbalanced X violates UNCONDITIONAL parallel trends by construction (the unadjusted 2×2 contrast converges to `θ_0 + 1 = 4`) — the design's covariate-adjustment point. Replicated verbatim in `tests/test_methodology_dml_did.py`.
263267
- Finding to preserve for docs: in the repeated-cross-section simulations the orthogonal estimator appears well centered on the truth but visibly NOISIER than Abadie's plug-in at small N (Figures 9-14) — expect larger SEs. Orthogonality removes first-order nuisance-estimation bias (an asymptotic property under the Theorem 1 rates); it does NOT guarantee finite-sample unbiasedness, and histogram centering in simulations cannot establish it.

0 commit comments

Comments
 (0)