Skip to content

docs(tutorials): bad-controls tutorial notebook 33 (Caetano et al. 2026, PR-C) - #831

Merged
igerber merged 1 commit into
mainfrom
bad-controls-tutorial
Sep 7, 2026
Merged

docs(tutorials): bad-controls tutorial notebook 33 (Caetano et al. 2026, PR-C)#831
igerber merged 1 commit into
mainfrom
bad-controls-tutorial

Conversation

@igerber

@igerber igerber commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • PR-C of the bad-controls initiative (PR-A docs(methodology): Caetano, Callaway, Payne & Sant'Anna (2026) bad-controls paper review (PR-A) #819 paper review, PR-B feat(dml): Caetano, Callaway, Payne & Sant'Anna (2026) bad-control lane on DMLDiD (PR-B) #829 estimator). Adds docs/tutorials/33_bad_controls.ipynb, "Bad Controls - Covariates That Treatment Can Affect": a staggered version of Caetano, Callaway, Payne & Sant'Anna (2026) Supplementary Appendix DGP 1 (the paper's coefficients, 4 periods, cohorts 3/4 plus never-treated, default_rng(5), n = 2000), on which the naive TWFE regression with the bad control at t (the paper's Estimator 1) lands at 0.4886 against a true 1.00; Approach 1 through base-period covariates without W (1.0540, the paper's inconsistent "ML (Pre-treatment)" arm) and with W (0.9754); the DMLDiD bad-control lane with W=["w"] (0.9766), Remark 5's W=["y"] (1.0169, labelled a deliberately misspecified sensitivity fit) and no W (1.0591); bad_control_summary() with the pre/post ATT_X reading (pre rows pre-test MP-5/MP-8 and should be zero; post rows check that treatment moves the covariate: 0.51 / 0.51 / 0.48 against 0.50); the event study; a ridge refit through the split-half nested stage (0.9770); the lane's restrictions. No "exclude X" arm (a maintainer decision: in the paper's linear designs every route that conditions on W coincides; the paper's discard bias is stated, not demonstrated). The tutorial names the full Approach-1 / Proposition-2/3 assumption maps (MP-4 maintained; MP-8 or MP-9; MP-6 on the never-treated side with trimming as regularization; MP-7 endpoint reduction) and makes no claim that the lane beats a CS fit that conditions on W.
  • Registered in docs/tutorials/index.rst (card + toctree, description count 34), docs/tutorials/README.md, diff_diff/guides/llms.txt (lines for 32 and 33), docs/doc-deps.yaml (dml_did, dml_did_results, staggered, _learners, linalg, utils, _crossfit, _dr_scores, practitioner).
  • tests/test_t33_bad_controls_drift.py: code-cell hashes, quoted-number pins, the pre/post ATT_X and identification-caveat wording pins, DGP re-derivation at 5e-4 (all seven estimates, the six ATT_X rows, the four event-study rows), source-fragment pins, and a |t| < 2 narrative guard on the correctly specified pre-period rows.
  • One small library change surfaced by the review: practitioner_next_steps() now labels a bad-control fit "DMLDiD (CCPS 2026 bad-control score)" instead of the Chang (2020) label (diff_diff/practitioner.py; asserted in tests/test_dml_did.py; changelog "Changed" bullet).
  • TODO.md: the PR-C row is removed.

Methodology references (required if estimator / math changes)

  • Method name(s): tutorial only; no estimator math changes. The practitioner display-name branch is a labelling change.
  • Paper / source link(s): Caetano, Callaway, Payne & Sant'Anna (2026), arXiv:2608.03881 (review on file at docs/methodology/papers/caetano-2026-review.md); Chang (2020).
  • Any intentional deviations from the source (and why): none new; the notebook cites the existing REGISTRY Notes (omega clip, CS-simple headline weighting vs Remark 4, analytical-only ATT_X, fail-closed lanes).

Validation

  • Tests added/updated: tests/test_t33_bad_controls_drift.py (new), tests/test_dml_did.py (practitioner banner assertion). Notebook executed once with the per-worktree kernel and validated with pytest --nbmake; test_docs_ia, test_doc_deps_integrity, test_tracking_files, test_changelog_fragments, test_guides, test_practitioner pass; Sphinx -W build passes with the new page.
  • Backtest / simulation / notebook evidence (if applicable): the notebook itself (locked seed-5 numbers; the seed choice and its rationale are disclosed in the prose and guarded by the drift test).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes (rendered warnings print messages only, no local paths)

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes (changelog.d/20260907-bad-controls-tutorial.md)

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Overall assessment: ✅ Looks good

Executive summary

  • No unmitigated P0/P1 findings.
  • Tutorial assumptions, scores, weighting, and ATT_X interpretation match the Registry and paper review.
  • Existing deviations are clearly disclosed and registered.
  • Practitioner label change is tested and has a valid changelog fragment.
  • Tests could not run because the environment lacks pytest/NumPy; changelog validation passed.

Methodology

  • P3 — Informational: CS-simple headline weighting, analytical-only ATT_X, odds clipping, and split-half nesting are documented deviations or implementation conventions (REGISTRY.md:L3250, 33_bad_controls.ipynb:L440).
    • Impact: None requiring action; the tutorial discloses each limitation.
    • Concrete fix: None.

No undocumented mismatch, missing assumption check, or variance/SE defect found.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

No findings. Unsupported bad-control paths and aggregation limitations remain tracked in TODO.md/DEFERRED.md.

Security

No findings; no secrets, PII, tracebacks, or local paths found.

Documentation/Tests

No findings. Numerical re-derivation, notebook drift guards, methodology wording pins, and label tests are comprehensive (test_t33_bad_controls_drift.py:L186, test_dml_did.py:L2815). The changelog fragment passes its validator.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Sep 7, 2026
Adds docs/tutorials/33_bad_controls.ipynb, the Caetano, Callaway, Payne &
Sant'Anna (2026) bad-controls walkthrough on a staggered version of the
paper's DGP 1: the naive TWFE regression with the bad control at t (the
paper's Estimator 1) misses by the full treatment effect on the covariate;
Approach 1 through base-period covariates with and without W; the DMLDiD
bad-control lane and its W choices (Remark 5's lagged outcome, none);
reading bad_control_summary() (pre rows pre-test MP-5/MP-8 and should be
zero, post rows check that treatment moves the covariate); the event
study; a ridge refit through the split-half nested stage; the lane's
restrictions. No "exclude X" arm: in the paper's linear designs every
route that conditions on W coincides, so the notebook makes no claim that
the lane beats a CS fit that conditions on W.

- Registered in docs/tutorials/index.rst (card + toctree; description
  count 34), docs/tutorials/README.md, diff_diff/guides/llms.txt (lines for
  32 and 33), docs/doc-deps.yaml (dml_did, dml_did_results, staggered,
  _learners, linalg, utils).
- tests/test_t33_bad_controls_drift.py: code-cell hashes, quoted-number
  and pre/post ATT_X wording pins, DGP re-derivation at 5e-4, source
  fragment pins, |t| < 2 narrative guard on the correctly specified
  pre-period rows.
- TODO.md: PR-C row removed; changelog fragment added.

- diff_diff/practitioner.py: practitioner_next_steps() names a bad-control fit
  "DMLDiD (CCPS 2026 bad-control score)" (was the Chang 2020 label); test in
  tests/test_dml_did.py; changelog "Changed" bullet.
@igerber
igerber force-pushed the bad-controls-tutorial branch from c909dab to 2ccf90f Compare September 7, 2026 22:02
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 2ccf90f26340987f3ace115925630f609fe39b51


Overall assessment: ✅ Looks good

Executive summary

  • No unmitigated P0/P1 findings.
  • No estimator, weighting, or variance code changed; the library change corrects a display label.
  • Tutorial assumptions and ATT_X interpretation match the cited paper review and Registry.
  • Numerical deviations and unsupported lanes are documented or tracked.
  • Static validation passed; runtime tests could not run because pytest, NumPy, and pandas are unavailable.

Methodology

  • P3 — Informational: Omega clipping, CS-simple headline weighting, analytical-only ATT_X, and split-half nesting are disclosed Registry conventions (docs/methodology/REGISTRY.md:L3252-L3424).
    • Impact: No defect; the tutorial clearly communicates these differences.
    • Concrete fix: None.

The Approach 1/2 assumption maps, comparison groups, DGP, and pre/post ATT_X interpretation agree with the paper review (docs/methodology/papers/caetano-2026-review.md:L163-L217, L290-L292, L358-L385).

Code Quality

No findings. The per-instance practitioner label is narrowly implemented and preserves the Chang label for ordinary fits (diff_diff/practitioner.py:L74-L95).

Performance

No findings.

Maintainability

No findings.

Tech Debt

  • P3 — Informational: Remark-4 weighting, ATT_X aggregation/bootstrap, and unsupported RCS, survey, anticipation, and universal-base lanes remain explicitly tracked in TODO.md and DEFERRED.md.
    • Impact: None requiring action for this PR.
    • Concrete fix: None.

Security

No findings; no secrets, PII, tracebacks, or local paths were found.

Documentation/Tests

No findings. Tests pin notebook code, outputs, assumptions, DGP re-derivation, estimates, SEs, and practitioner labels (tests/test_t33_bad_controls_drift.py:L186-L434, tests/test_dml_did.py:L2815-L2825). Changelog validation, YAML parsing, notebook hashes, and syntax checks passed.

@igerber
igerber merged commit 22ce714 into main Sep 7, 2026
31 checks passed
@igerber
igerber deleted the bad-controls-tutorial branch September 7, 2026 23:41
@igerber igerber mentioned this pull request Sep 8, 2026
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