Skip to content

feat: add ChangesInChanges (CiC) + QDiD distributional DiD estimators with qte golden parity#682

Merged
igerber merged 3 commits into
mainfrom
cic-qdid-implementation
Jul 12, 2026
Merged

feat: add ChangesInChanges (CiC) + QDiD distributional DiD estimators with qte golden parity#682
igerber merged 3 commits into
mainfrom
cic-qdid-implementation

Conversation

@igerber

@igerber igerber commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • PR-B of the Changes-in-Changes initiative: implements the estimators against the scope reviewed and merged in PR-A (docs: add CiC/QDiD paper reviews (Athey-Imbens 2006, Callaway-Li-Oka 2018, Melly-Santangelo 2015, Ciaccio 2024) #678). Locked v1 scope: 2x2 design, continuous outcomes, bootstrap-only inference, panel + repeated cross-section modes, R qte parity.
  • ChangesInChanges (alias CiC): Athey-Imbens (2006) nonlinear DiD - counterfactual distribution F_10(F_00^{-1}(F_01(y))) (Theorem 3.1), plug-in ATT (eq. 36), quantile treatment effects (eq. 18) on a default 19-point grid pinned to R's exact seq(0.05, 0.95, 0.05) doubles. The empirical inverse is an exact port of R quantile.default type-1 arithmetic including its fuzz handling - the paper's eq. (35)/(A.1) ceiling-order-statistic convention.
  • QDiD: the paper's quantile-DiD comparison estimator, matching qte::QDiD() exactly (additive type-7 form; the finite-sample deviation from the paper's k^QDID transformation is documented with a labeled REGISTRY Note; population-equivalent, locked by a large-N test vs DifferenceInDifferences).
  • Bootstrap inference per qte's schemes (panel unit-block / RCS pooled resample; seedable, n_bootstrap=200 default; replicate-SD SEs, normal CIs, sup-t uniform bands at qte's fixed 95% level with the IQR-scaled critical value). Joint-NaN contract via safe_inference(_batch) incl. degenerate zero-spread columns; >= 50%-finite replicate gate.
  • Diagnostics: eq.-17 interior-range guard (points kept for qte parity, NaN inference outside, warning), support-violation / heavy-ties / QDiD-non-monotonicity warnings, panel hygiene (balancing per qte's makeBalancedPanel, duplicate and time-varying-group errors), non-finite outcome rejection.
  • Golden parity: benchmarks/R/generate_qte_golden.R (qte pinned 1.3.1 in requirements.R) -> committed benchmarks/data/qte_golden.json. 4 scenarios (normal/lognormal/small-n synthetic + qte's lalonde with heavy ties) x {CiC, QDiD} x {panel, RCS} at atol=1e-10 - CiC quantile vectors are bit-exact vs R in every scenario; type-1 micro-fixtures (incl. ECDF-composed and near-integer adversarial probs) at atol=0; seeded-R SE blocks (999 iters) compared statistically per repo convention.
  • Docs: REGISTRY entry (+ SE and R-equivalents cross-reference rows, flat-native results enumeration), docs/api/changes_in_changes.rst, references.rst, llms.txt + llms-full.txt entries, README/choosing_estimator/r_comparison/practitioner catalogs, estimator count 20 -> 22 swept across all surfaces (incl. paper.md + new Athey2006 bib entry), TODO deferral rows.

Methodology references (required if estimator / math changes)

  • Method name(s): ChangesInChanges (CiC), QDiD
  • Paper / source link(s): Athey, S., & Imbens, G. W. (2006). "Identification and Inference in Nonlinear Difference-in-Differences Models." Econometrica, 74(2), 431-497. https://doi.org/10.1111/j.1468-0262.2006.00668.x - equation-level review merged in docs: add CiC/QDiD paper reviews (Athey-Imbens 2006, Callaway-Li-Oka 2018, Melly-Santangelo 2015, Ciaccio 2024) #678 (docs/methodology/papers/athey-imbens-2006-review.md). Reference implementation: R qte 1.3.1 (Callaway), CiC()/QDiD().
  • Any intentional deviations from the source (and why): documented with labeled REGISTRY Notes - (1) bootstrap-only inference is a qte-parity convention, not a paper prescription (the paper's influence-function analytical SEs are deferred; closest published validity argument is Callaway-Li-Oka 2018's exchangeable bootstrap, reviewed in docs: add CiC/QDiD paper reviews (Athey-Imbens 2006, Callaway-Li-Oka 2018, Melly-Santangelo 2015, Ciaccio 2024) #678); (2) QDiD implements qte's additive type-7 finite-sample form rather than the paper's k^QDID transformation (population-equivalent; locked parity direction); (3) sup-t uniform bands are hard-coded 95% independent of alpha (qte parity); (4) the 10% duplicate-share ties heuristic is a library operationalization of the continuity assumption.
  • Deferred and documented (REGISTRY + TODO rows): covariates (Melly-Santangelo), discrete-outcome bounds, analytical SEs, staggered/multi-period, treatment-on-controls; tutorial notebook and practitioner handler tracked as follow-ups.

Validation

  • Tests added/updated: tests/test_changes_in_changes.py (unit/edge: validation, transactional set_params, sklearn clone, panel hygiene, NaN contracts incl. degenerate bootstrap, seed determinism, all warnings), tests/test_methodology_changes_in_changes.py (eq. A.2 sandwich + Lemma A.1 Galois identities, hand-calculated micro-example, QDiDDiD and CiCDiD population checks, exact monotone-transform equivariance, heterogeneous-QTE recovery), tests/test_changes_in_changes_parity.py (golden point/micro/SE parity with skip-if-absent guard + version pin), tests/test_aliases.py (CiC alias). 188 passing locally (+ full meta-test suite: aliases, agent discoverability, doc-deps integrity, doc snippets).
  • Backtest / simulation / notebook evidence (if applicable): golden-fixture parity vs R qte 1.3.1 as above; SE statistical parity within repo thresholds on 999-iteration seeded R runs.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_011hismLBFcUbUzvDRq8ruWb

igerber added a commit that referenced this pull request Jul 12, 2026
igerber added a commit that referenced this pull request Jul 12, 2026
@igerber igerber force-pushed the cic-qdid-implementation branch from bd49485 to 06e9cfb Compare July 12, 2026 19:43
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 06e9cfbcc2d6ca38fde45d08d0c687d25280e818


Overall Assessment: ✅ Looks good

Executive Summary

  • Affected methods: ChangesInChanges/CiC and QDiD.
  • No unmitigated P0/P1 methodology, variance/SE, or inference-contract defects found.
  • CiC matches the registry formula: type-1 empirical inverse, counterfactual draws, ATT, and QTE construction. diff_diff/changes_in_changes.py:L166-L180, docs/methodology/REGISTRY.md:L3598-L3607
  • QDiD uses the documented qte finite-sample type-7 form; this is explicitly labeled as a registry deviation from the paper’s transformation form, so it is not a defect. diff_diff/changes_in_changes.py:L183-L207, docs/methodology/REGISTRY.md:L3638-L3645
  • Edge-case handling is strong: empty cells, non-finite outcomes, panel hygiene, bootstrap failure gating, degenerate SEs, and interior-range NaN inference are covered.
  • I could not run the test suite locally because pytest is not installed in this environment.

Methodology

  • Severity: P3 informational, no action required
    Impact: The implementation intentionally follows R qte for bootstrap-only inference, QDiD’s finite-sample type-7 form, and fixed-95% sup-t bands. These differ from or extend the Athey-Imbens paper in places, but all are documented with registry notes.
    Concrete fix: None. Keep docs/methodology/REGISTRY.md, the R fixture generator, and parity tests synchronized when re-anchoring. docs/methodology/REGISTRY.md:L3609-L3613, docs/methodology/REGISTRY.md:L3643-L3645

No P0/P1 methodology mismatch found.

Code Quality

  • Severity: None
    Impact: Constructor parameters are propagated through get_params()/set_params() for both estimators, and the shared fit path revalidates after mutation. diff_diff/changes_in_changes.py:L751-L802, diff_diff/changes_in_changes.py:L858-L909
    Concrete fix: None.

Performance

  • Severity: None
    Impact: Bootstrap is straightforward Python/NumPy and scoped to the new estimators. No avoidable performance regression stood out in the reviewed diff.
    Concrete fix: None.

Maintainability

  • Severity: P3 informational, no action required
    Impact: Deferred full-integration items are tracked: tutorial notebook and dedicated practitioner handler. These are documentation/integration follow-ups, not correctness blockers.
    Concrete fix: None for this PR; already tracked in TODO.md. TODO.md:L46-L51

Tech Debt

  • Severity: P3 informational, no action required
    Impact: Deferred CiC extensions are documented: covariates, discrete-outcome bounds, analytical SEs, staggered/multi-period, and treatment-on-controls. The shipped code warns or fails closed where appropriate.
    Concrete fix: None for this PR; already tracked in TODO.md. TODO.md:L111-L117

Security

  • Severity: None
    Impact: I found no secrets or sensitive tokens in the changed estimator, docs, tests, or benchmark files. The committed golden fixture appears to contain generated/public benchmark data.
    Concrete fix: None.

Documentation/Tests

  • Severity: P3 informational
    Impact: The PR adds methodology tests, edge-case tests, and R qte parity tests, including type-1/type-7 micro-fixtures and SE parity checks. tests/test_methodology_changes_in_changes.py:L1-L13, tests/test_changes_in_changes.py:L1-L8, tests/test_changes_in_changes_parity.py:L1-L18
    Concrete fix: I could not execute them here because python -m pytest failed with No module named pytest; rerun the focused command in an environment with dev dependencies installed.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 12, 2026
igerber and others added 3 commits July 12, 2026 17:01
Athey & Imbens (2006) nonlinear DiD for the 2x2 design with continuous
outcomes: ChangesInChanges (alias CiC) recovers the treated group's full
counterfactual outcome distribution and quantile treatment effects; QDiD
is the paper's additive quantile-DiD comparison estimator.

- Point estimation matches R qte 1.3.1 exactly: CiC via an exact port of
  R quantile.default type-1 arithmetic incl. fuzz (bit-exact golden
  parity); QDiD via qte's type-7 additive form (REGISTRY Note documents
  the finite-sample deviation from the paper's k^QDID transformation)
- Bootstrap-only inference per qte's schemes (panel unit-block / RCS
  pooled resample; seedable; n_bootstrap=200; replicate-SD SEs, normal
  CIs, sup-t uniform bands at qte's fixed 95% level)
- Diagnostics: eq.-17 interior-range guard (NaN inference outside),
  support-violation / heavy-ties / QDiD-non-monotonicity warnings;
  joint-NaN inference contract via safe_inference(_batch)
- Golden fixtures: benchmarks/R/generate_qte_golden.R (qte pinned 1.3.1
  in requirements.R) -> benchmarks/data/qte_golden.json; parity tests
  cover 4 scenarios x {CiC,QDiD} x {panel,RCS} at atol=1e-10, type-1
  micro-fixtures at atol=0, seeded-R SE block statistically
- Docs: REGISTRY entry + cross-reference rows, api rst, references,
  llms(-full).txt, README/choosing/r_comparison/practitioner catalogs,
  estimator count 20 -> 22 (incl. paper.md + new Athey2006 bib entry),
  TODO deferral rows (covariates/bounds/analytical-SE/staggered/ATC +
  tutorial and practitioner-handler follow-ups)

Deferred and documented per the merged PR-A review stack (#678):
covariates, discrete-outcome bounds, analytical SEs, staggered designs,
treatment-on-controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hismLBFcUbUzvDRq8ruWb
…dation

- NaN-ify invalid per-quantile SEs (non-finite or <= 0, e.g. zero-spread
  degenerate bootstrap columns) before storing quantile_effects, so the
  stored se honors the joint-NaN contract and uniform_bands() cannot emit
  finite zero-width bands while pointwise inference is NaN; degenerate
  constant-cell regression test
- Pin _DEFAULT_QUANTILES to R's exact seq(0.05, 0.95, 0.05) doubles
  (np.arange differs at 5 of 19 indices by one ulp; type-1 order-statistic
  selection is ulp-sensitive); locked against the fixture probs plus a
  quantiles=None end-to-end golden parity test
- Reject non-finite (inf/-inf) outcome values with a counting ValueError
  after the NaN drop (dropna keeps inf); +/-inf tests
- get_params(deep=True) sklearn-clone-compatible signature on both classes
  (HAD precedent) + deep-flag equality and strengthened clone tests
- Explicit-interaction formulas derive (treatment, time) roles from the
  main-effect order so 'treated:post' and 'post:treated' are identical
  (CiC/QDiD are not symmetric in treatment/time); order-invariance test
- Add the qdid_rcs seeded SE block to the golden generator and SE parity
  parametrization (fixture regenerated; point values unchanged)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hismLBFcUbUzvDRq8ruWb
@igerber igerber force-pushed the cic-qdid-implementation branch from 06e9cfb to 9cb789c Compare July 12, 2026 21:02
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 9cb789ce29eba51b1371a75b77a54742d4b1d359


Overall Assessment: ✅ Looks good

Executive Summary

  • Affected methods: ChangesInChanges/CiC and QDiD.
  • No unmitigated P0/P1 methodology, variance/SE, inference-contract, or edge-case defects found.
  • CiC matches the registry formula: type-1 inverse CDF, counterfactual draws, ATT, and QTE construction. diff_diff/changes_in_changes.py:L166-L180, docs/methodology/REGISTRY.md:L3598-L3607
  • QDiD uses the documented qte finite-sample type-7 form; the paper-form deviation is explicitly labeled in the registry, so it is not a defect. diff_diff/changes_in_changes.py:L183-L207, docs/methodology/REGISTRY.md:L3638-L3645
  • Bootstrap-only inference, fixed-95% sup-t bands, and deferred analytical SEs are documented registry deviations or TODO-tracked scope choices.
  • One P3 documentation count drift remains in the registry replicate-weight matrix.

Methodology

  • Severity: P3 informational, no action required
    Impact: Bootstrap-only inference, QDiD’s finite-sample type-7 implementation, and fixed-95% uniform bands intentionally follow qte parity rather than paper-exact analytical SEs or transformation form. These deviations are labeled in the Methodology Registry.
    Concrete fix: None. Keep registry notes and parity fixtures synchronized when re-anchoring. docs/methodology/REGISTRY.md:L3609-L3612, docs/methodology/REGISTRY.md:L3643-L3645

No P0/P1 methodology mismatch found.

Code Quality

  • Severity: None
    Impact: Constructor parameters are propagated through get_params()/set_params() and the shared fit path for both estimators. The inference path uses safe_inference()/safe_inference_batch() and stores invalid SEs as NaN before result construction. diff_diff/changes_in_changes.py:L582-L590, diff_diff/changes_in_changes.py:L751-L802, diff_diff/changes_in_changes.py:L858-L921
    Concrete fix: None.

Performance

  • Severity: None
    Impact: Bootstrap logic is scoped to the new estimators and uses straightforward NumPy resampling with the documented default n_bootstrap=200. No avoidable performance regression stood out in the reviewed diff. diff_diff/changes_in_changes.py:L345-L443
    Concrete fix: None.

Maintainability

  • Severity: P3 informational, tracked in TODO.md
    Impact: Full integration follow-ups, including the tutorial notebook and dedicated practitioner handler, are explicitly tracked and are not correctness blockers. TODO.md:L50-L51
    Concrete fix: None for this PR.

Tech Debt

  • Severity: P3 informational, tracked in TODO.md
    Impact: Deferred extensions are documented: covariates, discrete-outcome bounds, analytical SEs, staggered/multi-period CiC, and treatment-on-controls. The shipped implementation fails closed or warns for the relevant v1 boundaries. TODO.md:L113-L117
    Concrete fix: None for this PR.

Security

  • Severity: None
    Impact: No secret-like values were found in the changed files. The committed golden fixture appears to contain generated/public benchmark data, not credentials or private tokens.
    Concrete fix: None.

Documentation/Tests

  • Severity: P3 minor
    Impact: The replicate-weight support matrix still says “13 of 20 public estimators” and “outside the 20-estimator count” after this PR moves other surfaces to 22 estimators. This is documentation-only, but it leaves a stale count in the registry. docs/methodology/REGISTRY.md:L5039-L5060
    Concrete fix: Update the matrix wording to the current estimator count, e.g. 13 of 22, and clarify where ChangesInChanges/QDiD sit in the unsupported/no-survey surface.

  • Severity: P3 informational
    Impact: The PR adds methodology, edge-case, degenerate-inference, and R qte parity tests. I could not execute them in this environment because python -m pytest ... failed with No module named pytest. tests/test_changes_in_changes.py:L211-L455, tests/test_methodology_changes_in_changes.py:L1-L13, tests/test_changes_in_changes_parity.py:L1-L18
    Concrete fix: Run the focused test set in an environment with dev dependencies installed.

@igerber igerber merged commit bfebb28 into main Jul 12, 2026
29 checks passed
@igerber igerber deleted the cic-qdid-implementation branch July 12, 2026 22:14
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