From b76d2c22623c05956487ff51f1a79d5f66248373 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 15 May 2026 06:40:11 -0400 Subject: [PATCH 1/4] Restore navigable cross-refs to REGISTRY.md / REPORTING.md as GitHub URLs PR #410 converted 4 :doc: refs to .md files (myst-parser absent) and 4 markdown links to REGISTRY.html (orphan target) into plain code literals. That avoided Sphinx warnings but left users without a clickable path to the methodology contract documents from the API reference pages. This commit restores rendered-doc usability by linking the same labels to the canonical GitHub blob URLs (REGISTRY.md / REPORTING.md live as unrendered .md files; GitHub renders them and is the only navigable target until myst-parser is added): - docs/api/business_report.rst (2 instances) - docs/api/diagnostic_report.rst (2 instances) - docs/tutorials/18_geo_experiments.ipynb (2 instances) - docs/tutorials/19_dcdh_marketing_pulse.ipynb (3 instances) Total 9 RST/markdown references restored from plain code literals to external links. No methodology change; pure rendered-surface fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/api/business_report.rst | 8 +++++--- docs/api/diagnostic_report.rst | 8 +++++--- docs/tutorials/18_geo_experiments.ipynb | 6 +++--- docs/tutorials/19_dcdh_marketing_pulse.ipynb | 8 ++++---- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/api/business_report.rst b/docs/api/business_report.rst index d63ded5d..8209a864 100644 --- a/docs/api/business_report.rst +++ b/docs/api/business_report.rst @@ -47,7 +47,8 @@ pretest to opt in. Methodology deviations (no traffic-light gates, pre-trends verdict thresholds, power-aware phrasing, unit-translation policy, schema -stability) are documented in ``docs/methodology/REPORTING.md``. +stability) are documented in `docs/methodology/REPORTING.md +`_. The schema carries a top-level ``target_parameter`` block (experimental) naming what the headline scalar represents per @@ -62,8 +63,9 @@ distinguishes the populated-surface subcase (per-horizon table available on ``linear_trends_effects``) from the empty-surface subcase (no horizons survived estimation; re-fit with a larger ``L_max`` or with ``trends_linear=False``). See the "Target -parameter" section of ``docs/methodology/REPORTING.md`` for the -full per-estimator dispatch table and schema shape. +parameter" section of `docs/methodology/REPORTING.md +`_ +for the full per-estimator dispatch table and schema shape. Example ------- diff --git a/docs/api/diagnostic_report.rst b/docs/api/diagnostic_report.rst index d86c8a71..3ed8328f 100644 --- a/docs/api/diagnostic_report.rst +++ b/docs/api/diagnostic_report.rst @@ -13,13 +13,15 @@ result. Methodology deviations (no traffic-light gates, opt-in placebo battery, estimator-native diagnostic routing, power-aware phrasing -threshold) are documented in ``docs/methodology/REPORTING.md``. +threshold) are documented in `docs/methodology/REPORTING.md +`_. The schema carries a top-level ``target_parameter`` block (experimental) naming what the headline scalar represents per estimator. See the "Target parameter" section of -``docs/methodology/REPORTING.md`` for the per-estimator dispatch and -schema shape. +`docs/methodology/REPORTING.md +`_ +for the per-estimator dispatch and schema shape. Data-dependent checks (2x2 parallel trends on simple DiD, Goodman-Bacon decomposition on staggered estimators, the EfficientDiD diff --git a/docs/tutorials/18_geo_experiments.ipynb b/docs/tutorials/18_geo_experiments.ipynb index 1be2e26d..7b1e2c64 100644 --- a/docs/tutorials/18_geo_experiments.ipynb +++ b/docs/tutorials/18_geo_experiments.ipynb @@ -40,7 +40,7 @@ "cell_type": "markdown", "id": "t18-cell-005", "metadata": {}, - "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in `docs/methodology/REGISTRY.md`.\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.rst) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." + "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md).\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.rst) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." }, { "cell_type": "code", @@ -432,7 +432,7 @@ "cell_type": "markdown", "id": "t18-cell-015", "metadata": {}, - "source": "Synthetic Difference-in-Differences finds two sets of weights:\n\n1. **Unit weights** ($\\omega_j$): a weighted blend of control markets whose pre-period trajectory matches the treated markets' pre-period trajectory.\n2. **Time weights** ($\\lambda_t$): a weighting of pre-treatment periods that emphasizes the baseline weeks most informative for the comparison.\n\nThe ATT estimator combines both: take the time-weighted average of (treated mean minus unit-weighted control mean), then subtract the same quantity computed in the pre-period. The unit weights make the synthetic control match the treated group; the time weights make the comparison robust to pre-treatment level differences.\n\nThis is the method introduced in [Arkhangelsky, Athey, Hirshberg, Imbens, & Wager (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159). Algorithmic details and any documented deviations from the R `synthdid` reference live in `docs/methodology/REGISTRY.md`." + "source": "Synthetic Difference-in-Differences finds two sets of weights:\n\n1. **Unit weights** ($\\omega_j$): a weighted blend of control markets whose pre-period trajectory matches the treated markets' pre-period trajectory.\n2. **Time weights** ($\\lambda_t$): a weighting of pre-treatment periods that emphasizes the baseline weeks most informative for the comparison.\n\nThe ATT estimator combines both: take the time-weighted average of (treated mean minus unit-weighted control mean), then subtract the same quantity computed in the pre-period. The unit weights make the synthetic control match the treated group; the time weights make the comparison robust to pre-treatment level differences.\n\nThis is the method introduced in [Arkhangelsky, Athey, Hirshberg, Imbens, & Wager (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159). Algorithmic details and any documented deviations from the R `synthdid` reference live in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md)." }, { "cell_type": "code", @@ -1070,4 +1070,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/docs/tutorials/19_dcdh_marketing_pulse.ipynb b/docs/tutorials/19_dcdh_marketing_pulse.ipynb index 37d1363e..d107719e 100644 --- a/docs/tutorials/19_dcdh_marketing_pulse.ipynb +++ b/docs/tutorials/19_dcdh_marketing_pulse.ipynb @@ -14,7 +14,7 @@ "cell_type": "markdown", "id": "t19-cell-002", "metadata": {}, - "source": "## 1. The Marketing Pulse Problem\n\nYour team runs paid-promo pulses across 60 markets. Some markets ran the promo at the start of the quarter and turned it off as the campaign budget rolled to the next geo (leavers); others started untreated and switched the promo on at some point during the quarter (joiners). Leadership wants the average lift on weekly checkout sessions while the promo was on.\n\n**Why dCDH.** This panel has *reversible* (non-absorbing) treatment in the dCDH sense: across the panel, the promo turns on in some markets and off in others - both directions appear in the same dataset. Every other modern staggered-DiD estimator in diff-diff (Callaway-Sant'Anna, Sun-Abraham, Wooldridge ETWFE, ImputationDiD, TwoStageDiD, EfficientDiD) assumes treatment is absorbing: once treated, always treated. They simply don't apply to a panel that contains leavers. dCDH does, following [de Chaisemartin & D'Haultfoeuille (2020)](https://www.aeaweb.org/articles?id=10.1257/aer.20181169) and the [dynamic companion paper](https://www.nber.org/papers/w29873).\n\n**Scope of this tutorial.** Each market in our panel switches *at most once* during the quarter (the dCDH paper's Assumption 5, which the default analytical SE path requires). So a market is either a stable-untreated unit, a joiner that turns the promo on exactly once, a leaver that turns it off exactly once, or a stable-treated unit. dCDH does support multi-switch within-market paths (e.g., on-off-on cycles) via `drop_larger_lower=False` plus `by_path=k` for per-path effects, but that's a separate scope - see the extensions section at the end. Implementation details and any documented deviations from R's `did_multiplegt_dyn` reference live in `docs/methodology/REGISTRY.md`." + "source": "## 1. The Marketing Pulse Problem\n\nYour team runs paid-promo pulses across 60 markets. Some markets ran the promo at the start of the quarter and turned it off as the campaign budget rolled to the next geo (leavers); others started untreated and switched the promo on at some point during the quarter (joiners). Leadership wants the average lift on weekly checkout sessions while the promo was on.\n\n**Why dCDH.** This panel has *reversible* (non-absorbing) treatment in the dCDH sense: across the panel, the promo turns on in some markets and off in others - both directions appear in the same dataset. Every other modern staggered-DiD estimator in diff-diff (Callaway-Sant'Anna, Sun-Abraham, Wooldridge ETWFE, ImputationDiD, TwoStageDiD, EfficientDiD) assumes treatment is absorbing: once treated, always treated. They simply don't apply to a panel that contains leavers. dCDH does, following [de Chaisemartin & D'Haultfoeuille (2020)](https://www.aeaweb.org/articles?id=10.1257/aer.20181169) and the [dynamic companion paper](https://www.nber.org/papers/w29873).\n\n**Scope of this tutorial.** Each market in our panel switches *at most once* during the quarter (the dCDH paper's Assumption 5, which the default analytical SE path requires). So a market is either a stable-untreated unit, a joiner that turns the promo on exactly once, a leaver that turns it off exactly once, or a stable-treated unit. dCDH does support multi-switch within-market paths (e.g., on-off-on cycles) via `drop_larger_lower=False` plus `by_path=k` for per-path effects, but that's a separate scope - see the extensions section at the end. Implementation details and any documented deviations from R's `did_multiplegt_dyn` reference live in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md)." }, { "cell_type": "code", @@ -38,7 +38,7 @@ "cell_type": "markdown", "id": "t19-cell-004", "metadata": {}, - "source": "## 2. The Data\n\nWe'll simulate a panel that mirrors a marketing pulse campaign:\n\n- **60 markets**, each observed for **8 weeks**\n- Some markets started the quarter with the promo on and switched it off (leavers); others started untreated and switched the promo on (joiners). Each market switches exactly once during the panel - the A5 single-switch contract (see `docs/methodology/REGISTRY.md`) the analytical SE is derived under.\n- Outcome: weekly checkout sessions per market, baseline ~110\n- True treatment effect: **+12 sessions per market-week** when the promo is on, with mild cell-level heterogeneity around that average." + "source": "## 2. The Data\n\nWe'll simulate a panel that mirrors a marketing pulse campaign:\n\n- **60 markets**, each observed for **8 weeks**\n- Some markets started the quarter with the promo on and switched it off (leavers); others started untreated and switched the promo on (joiners). Each market switches exactly once during the panel - the A5 single-switch contract (see [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md)) the analytical SE is derived under.\n- Outcome: weekly checkout sessions per market, baseline ~110\n- True treatment effect: **+12 sessions per market-week** when the promo is on, with mild cell-level heterogeneity around that average." }, { "cell_type": "code", @@ -341,7 +341,7 @@ "cell_type": "markdown", "id": "t19-cell-021", "metadata": {}, - "source": "## 6. Extensions and Where to Go Next\n\nThis tutorial covered the core dCDH workflow on a reversible panel: `DID_M` with the joiners/leavers split, plus the `L_max` multi-horizon event study with multiplier bootstrap. The library also supports several extensions we did not demonstrate here:\n\n- **Per-trajectory disaggregation** (`by_path=k`): when joiners and leavers each follow a few common treatment paths (e.g., on-off-on vs on-on-off), `by_path=k` reports the event study separately for the top-k most common observed paths. Useful for pulse campaigns where the schedule varies across markets.\n- **Group-specific linear trends** (`trends_linear=True`): allows each market to have its own pre-treatment slope, absorbing differential trends.\n- **State-set-specific trends** (`trends_nonparam=...`): allows non-parametric trends shared within state-set strata.\n- **HonestDiD sensitivity analysis** (`honest_did=True`): Rambachan-Roth (2023) bounds on the post-treatment effects under controlled parallel-trends violations, computed on the placebo event-study surface.\n- **Survey-design support** (`survey_design=...`): Taylor-series linearization with sampling weights, strata, PSU, and FPC.\n\nSee [`docs/api/chaisemartin_dhaultfoeuille.rst`](../api/chaisemartin_dhaultfoeuille.html) for the full parameter reference and `docs/methodology/REGISTRY.md` for the methodology contract on each surface." + "source": "## 6. Extensions and Where to Go Next\n\nThis tutorial covered the core dCDH workflow on a reversible panel: `DID_M` with the joiners/leavers split, plus the `L_max` multi-horizon event study with multiplier bootstrap. The library also supports several extensions we did not demonstrate here:\n\n- **Per-trajectory disaggregation** (`by_path=k`): when joiners and leavers each follow a few common treatment paths (e.g., on-off-on vs on-on-off), `by_path=k` reports the event study separately for the top-k most common observed paths. Useful for pulse campaigns where the schedule varies across markets.\n- **Group-specific linear trends** (`trends_linear=True`): allows each market to have its own pre-treatment slope, absorbing differential trends.\n- **State-set-specific trends** (`trends_nonparam=...`): allows non-parametric trends shared within state-set strata.\n- **HonestDiD sensitivity analysis** (`honest_did=True`): Rambachan-Roth (2023) bounds on the post-treatment effects under controlled parallel-trends violations, computed on the placebo event-study surface.\n- **Survey-design support** (`survey_design=...`): Taylor-series linearization with sampling weights, strata, PSU, and FPC.\n\nSee [`docs/api/chaisemartin_dhaultfoeuille.rst`](../api/chaisemartin_dhaultfoeuille.html) for the full parameter reference and [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md) for the methodology contract on each surface." }, { "cell_type": "markdown", @@ -387,4 +387,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From f7c25a0a6d790b76e9a1e03852017a92c2951358 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 15 May 2026 06:49:38 -0400 Subject: [PATCH 2/4] Restore T18 practitioner_decision_tree link to rendered HTML + anchor PR #410 downgraded the T18 introduction's "practitioner decision tree" link from a navigable HTML anchor target to a raw .rst source path: pre-#410: ../practitioner_decision_tree.html#few-test-markets post-#410: ../practitioner_decision_tree.rst The .rst path bypasses Sphinx's rendered version, breaks the anchor jump to the "Few Test Markets" branch, and (on RTD) sends users to a non-existent URL. This commit restores the pre-#410 rendered-HTML target + anchor. practitioner_decision_tree.rst is rendered by Sphinx; the anchor "few-test-markets" exists at docs/practitioner_decision_tree.rst:323. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/tutorials/18_geo_experiments.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/18_geo_experiments.ipynb b/docs/tutorials/18_geo_experiments.ipynb index 7b1e2c64..0a5930a2 100644 --- a/docs/tutorials/18_geo_experiments.ipynb +++ b/docs/tutorials/18_geo_experiments.ipynb @@ -40,7 +40,7 @@ "cell_type": "markdown", "id": "t18-cell-005", "metadata": {}, - "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md).\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.rst) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." + "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md).\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.html#few-test-markets) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." }, { "cell_type": "code", From 35a4337c4937ad8e97739aa9db7fff9d09a09994 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 15 May 2026 06:58:02 -0400 Subject: [PATCH 3/4] TODO: track in-site Sphinx render for REPORTING.md / REGISTRY.md Pilot-410 holistic codex review (R3) repeatedly flagged the GitHub blob/main URLs introduced by the prior commit as version-mismatched for stable docs. The constraint is structural: REPORTING.md and REGISTRY.md are .md files, and the Sphinx build has no myst-parser, so :doc: / :ref: targets do not exist. GitHub URLs are the best navigable option without expanding scope. This commit records the proper fix as a follow-up: render both .md files as in-site Sphinx pages (via myst-parser or RST conversion) so the API/tutorial cross-refs can switch to :doc:. Co-Authored-By: Claude Opus 4.7 (1M context) --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 5128c648..12277102 100644 --- a/TODO.md +++ b/TODO.md @@ -141,6 +141,7 @@ Deferred items from PR reviews that were not addressed before merge. | HonestDiD `test_m0_short_circuit` uses wall-clock `elapsed < 0.5s` as a proxy for "short-circuit path taken" instead of calling the full optimizer. Replace with a direct correctness signal (mock/spy the optimizer or check a state flag) so the test doesn't depend on CI timing. Not flaky today at 500ms, but load-bearing correctness on a timing proxy is brittle. | `tests/test_methodology_honest_did.py:246` | — | Low | | SyntheticDiD: rename internal `placebo_effects` variable to `variance_effects` (or `resampled_effects`). Misleading name across the placebo/bootstrap/jackknife dispatch paths — holds three different contents depending on variance method. Low-risk refactor; user-facing field rename should preserve `placebo_effects` as a deprecated alias for one release. | `synthetic_did.py`, `results.py` | follow-up | Medium | | AI review CI: pin workflow contract via test (uses `openai/codex-action@v1`, passes `prompt-file`, reads `steps.run_codex.outputs.final-message`, preserves diff-exclude paths and comment markers). Currently only the wrapper-tag and closing-tag-escape strings are asserted. | `tests/test_openai_review.py`, `.github/workflows/ai_pr_review.yml` | #416 | Low | +| Render `docs/methodology/REPORTING.md` and `docs/methodology/REGISTRY.md` as in-site Sphinx pages so cross-references can use `:doc:` instead of off-site GitHub `blob/main` URLs. Current state (#410 fix-audit-r2) restores navigable links via `blob/main`, but stable-docs readers can land on a different revision than the package version they are reading. Two viable paths: (a) add `myst-parser` to `docs/conf.py` extensions + docs extras and link with `:doc:`, or (b) convert both files to `.rst`. | `docs/conf.py`, `docs/api/business_report.rst`, `docs/api/diagnostic_report.rst`, `docs/tutorials/18_geo_experiments.ipynb`, `docs/tutorials/19_dcdh_marketing_pulse.ipynb` | follow-up | Low | --- From 8366f5009cac9b1df985665ac2996804ea6258a0 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 15 May 2026 07:19:21 -0400 Subject: [PATCH 4/4] Use .rst#anchor form for nbsphinx local-file resolution CI sphinx-build -W failed on the prior practitioner_decision_tree fix: WARNING: File not found: 'practitioner_decision_tree.html#few-test-markets' [nbsphinx.localfile] nbsphinx's localfile resolver checks the target as a source-side file. .html does not exist as a source artifact (only after build), so the warning fires; -W escalates to error. The .rst#anchor form satisfies both constraints: nbsphinx finds the source .rst, and Sphinx/nbsphinx renders the link to .html#anchor in the built HTML (verified locally via make -C docs clean && make html SPHINXOPTS="-W"; produced practitioner_decision_tree.html#few-test-markets with the matching id="few-test-markets" target in the rendered page). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/tutorials/18_geo_experiments.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/18_geo_experiments.ipynb b/docs/tutorials/18_geo_experiments.ipynb index 0a5930a2..d2ff8098 100644 --- a/docs/tutorials/18_geo_experiments.ipynb +++ b/docs/tutorials/18_geo_experiments.ipynb @@ -40,7 +40,7 @@ "cell_type": "markdown", "id": "t18-cell-005", "metadata": {}, - "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md).\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.html#few-test-markets) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." + "source": "**Why diff-diff.** The diff-diff library implements Synthetic Difference-in-Differences following [Arkhangelsky et al. (2021)](https://www.aeaweb.org/articles?id=10.1257/aer.20190159) - both the unit weights and the time weights, the placebo standard error procedure from the paper, and full panel-data interpretability. Implementation details and any documented deviations from the R `synthdid` reference are tracked in [`docs/methodology/REGISTRY.md`](https://github.com/igerber/diff-diff/blob/main/docs/methodology/REGISTRY.md).\n\nThis tutorial sits in SDiD's documented sweet spot: a small number of treated markets in a larger pool of donor controls, where basic DiD's averaging doesn't help and you need a counterfactual built specifically for your treated markets. The library's [practitioner decision tree](../practitioner_decision_tree.rst#few-test-markets) puts SyntheticDiD on the \"Few Test Markets\" branch for exactly this reason.\n\nIf you've been using GeoLift, CausalImpact, or rolling your own synthetic control in pandas, this tutorial gives you the canonical SDiD implementation in Python with the diagnostics, inference, and stakeholder packaging in one place." }, { "cell_type": "code",