Skip to content

Commit 6bd3c4e

Browse files
igerberclaude
andcommitted
Address PR #394 R1 review (1 P0, 2 P1, 1 P2)
DGP correctness (P0): Replace post-hoc mutation of `dose` and `first_treat` for one DMA with a Uniform[$5K, $50K] regional spend DGP where every DMA participates. The previous DGP zeroed the dose for one DMA AFTER the generator had baked the original positive-dose treatment effect into the outcome, producing a "never-treated reference" that still carried treated outcomes. The new DGP is internally consistent: outcomes are generated from the dose values that HAD then sees, no relabeling. Design 1 framing (P1): HAD's auto-detection now resolves to `continuous_near_d_lower` (Design 1) instead of `continuous_at_zero` (Design 1'), matching the "every market got some treatment, no untreated comparison group" narrative throughout. Target parameter is `WAS_d_lower` (per-$1K above the boundary spend, ~$5K), not `WAS`. Notebook Section 3 now explains the WAS_d_lower interpretation: multiply by `(actual_dose - d_lower)` for per-DMA lift estimates (a DMA at $30K saw ~(30 - 5) * 100 = 2,500 extra weekly visits, not 30 * 100 = 3,000). Section 3 acknowledges the Assumption 5/6 advisory the library fires for Design 1 (non-testable local linearity at the boundary) and explains why it holds in this DGP (linear by construction). Section 4 event-study fit filters the duplicate Assumption 5/6 warning. Stakeholder template (Section 5) frames the result as "per-$1K above the $5K floor" and flags the Assumption 6 caveat. Pretest description (P1): Section 6 extensions cell now describes the composite pretest workflow accurately: QUG + linearity (Stute, Yatchew-HR) on the two-period path. The notebook no longer claims this verifies parallel-trends; that is closed by the multi-period joint variants (`stute_joint_pretest`, `joint_pretrends_test`, `joint_homogeneity_test`). Drift test placebo-presence (P2): New `test_event_study_horizons_complete` asserts all 7 expected event-times (e=-4..-2, 0..3) are present, so per-horizon coverage tests can drop the `if e in event_times` guard that would silently pass on a truncated horizon list. Pre-placebo test no longer skips silently. Lock changes: - MAIN_SEED: 126 -> 87 (cleanest seed in the new DGP's seed search). - Locked numbers: overall att=100.0, SE=0.7, CI [98.6, 101.4], d_lower=5.2, dose_mean=24.7, n_treated=59 (out of 60). Docs synced: - CHANGELOG entry rewritten for the new design path / target / 13-test count. - Decision-tree code block updated to use the new DGP knobs (low=$5K, seed=87) and describe the boundary-anchored interpretation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8e92902 commit 6bd3c4e

4 files changed

Lines changed: 196 additions & 145 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- **Tutorial 20: HAD for National Brand Campaign with Regional Spend Intensity** (`docs/tutorials/20_had_brand_campaign.ipynb`) — end-to-end practitioner walkthrough for `HeterogeneousAdoptionDiD` on a 60-DMA panel where every market got the campaign at varying intensity and no untreated comparison group exists. Covers the headline Weighted Average Slope (WAS) on a 2-period collapse with `design="auto"` resolving to `continuous_at_zero`, the multi-week event study with per-week pointwise CIs and pre-launch placebos, and a stakeholder communication template. Companion drift-test file `tests/test_t20_had_brand_campaign_drift.py` (12 tests pinning panel composition, design auto-detection, overall WAS, CI endpoints, and per-horizon coverage). Cross-link added from `docs/practitioner_decision_tree.rst` § "Varying Spending Levels" pointing to T20 when the no-untreated-controls regime applies. T20 wired into the existing `had.py` entry in `docs/doc-deps.yaml` so future HAD source changes flag the new tutorial + decision-tree section via `/docs-impact`.
11+
- **Tutorial 20: HAD for National Brand Campaign with Regional Spend Intensity** (`docs/tutorials/20_had_brand_campaign.ipynb`) — end-to-end practitioner walkthrough for `HeterogeneousAdoptionDiD` on a 60-DMA panel where every market got the campaign at varying intensity and no untreated comparison group exists. The DGP uses Uniform[\$5K, \$50K] regional add-on spend per DMA (every DMA participates, no DMA at exactly \$0), so `design="auto"` resolves to `continuous_near_d_lower` (Design 1) with target `WAS_d_lower` — interpreted as the average per-dollar marginal effect of regional spend above the lightest-touch DMA's spend (`d_lower` ≈ \$5K). Covers the headline `WAS_d_lower` fit on a 2-period collapse, the multi-week event study with per-week pointwise CIs and pre-launch placebos, and a stakeholder communication template that flags the Assumption 5/6 caveat (non-testable local-linearity at the boundary). Companion drift-test file `tests/test_t20_had_brand_campaign_drift.py` (13 tests pinning panel composition, design auto-detection / target / d_lower, overall WAS_d_lower, CI endpoints, dose mean, n_units, full event-study horizon presence, and per-horizon coverage). Cross-link added from `docs/practitioner_decision_tree.rst` § "Varying Spending Levels" pointing to T20 when the no-untreated-controls regime applies. T20 wired into the existing `had.py` entry in `docs/doc-deps.yaml` so future HAD source changes flag the new tutorial + decision-tree section via `/docs-impact`.
1212

1313
## [3.3.1] - 2026-04-25
1414

docs/practitioner_decision_tree.rst

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,28 +255,31 @@ campaign window), :class:`~diff_diff.ContinuousDiD` cannot identify the
255255
dose-response curve - there is no untreated comparison group. Reach for
256256
:class:`~diff_diff.HeterogeneousAdoptionDiD` (alias :class:`~diff_diff.HAD`)
257257
instead. HAD identifies the average per-dollar marginal effect from the
258-
local-linear behavior of outcome changes near the dose boundary, returning
259-
a single Weighted Average Slope (WAS) on the dose scale.
258+
local-linear behavior of outcome changes near the *boundary* of the dose
259+
distribution (the lightest-touch unit's dose, called ``d_lower``), returning
260+
a Weighted Average Slope ``WAS_d_lower`` on the dose scale.
260261

261262
.. code-block:: python
262263
263264
from diff_diff import HAD, generate_continuous_did_data
264265
265-
# Markets where every unit gets some treatment dose; one anchors at $0
266+
# Markets where every unit gets some treatment dose - regional spend
267+
# ranges from a $5K floor (lightest-touch DMA) to $50K. No DMA at $0;
268+
# HAD anchors at the lightest-touch DMA's spend (d_lower) instead.
266269
data = generate_continuous_did_data(
267270
n_units=60, n_periods=8, cohort_periods=[5],
268271
never_treated_frac=0.0, dose_distribution="uniform",
269-
dose_params={"low": 0.0, "high": 50.0},
270-
att_function="linear", att_slope=100.0, seed=126,
272+
dose_params={"low": 5.0, "high": 50.0},
273+
att_function="linear", att_slope=100.0, seed=87,
271274
)
272-
# ... post-process to satisfy HAD's contract ...
275+
# ... zero out pre-treatment doses to satisfy HAD's D=0 in pre contract ...
273276
274277
had = HAD(design="auto")
275278
results = had.fit(
276279
data, outcome_col="outcome", dose_col="dose",
277280
time_col="period", unit_col="unit",
278281
)
279-
print(f"Per-$1K lift (WAS): {results.att:.1f}")
282+
print(f"Per-$1K lift above d_lower (WAS_d_lower): {results.att:.1f}")
280283
281284
.. tip::
282285

0 commit comments

Comments
 (0)