diff-diff 3.0.1__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diff_diff-3.0.1 → diff_diff-3.1.0}/PKG-INFO +123 -1
- {diff_diff-3.0.1 → diff_diff-3.1.0}/README.md +122 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/__init__.py +24 -1
- diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille.py +4736 -0
- diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +399 -0
- diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille_results.py +1303 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/honest_did.py +192 -9
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/power.py +612 -32
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/prep.py +65 -20
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/prep_dgp.py +411 -72
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_event_study.py +60 -1
- {diff_diff-3.0.1 → diff_diff-3.1.0}/pyproject.toml +1 -1
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/Cargo.lock +13 -13
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/Cargo.toml +1 -1
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/_backend.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/bacon.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/datasets.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/estimators.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/linalg.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/practitioner.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/stacked_did.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_triple_diff.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/survey.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/triple_diff.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/twfe.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/utils.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/wooldridge.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/build.rs +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/bootstrap.rs +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/lib.rs +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/linalg.rs +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/trop.rs +0 -0
- {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/weights.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Operating System :: OS Independent
|
|
@@ -126,6 +126,17 @@ After estimation, call `practitioner_next_steps(results)` for context-aware guid
|
|
|
126
126
|
|
|
127
127
|
Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
128
128
|
|
|
129
|
+
## For Data Scientists
|
|
130
|
+
|
|
131
|
+
Measuring campaign lift? Evaluating a product launch? diff-diff handles the causal inference so you can focus on the business question.
|
|
132
|
+
|
|
133
|
+
- **[Which method fits my problem?](docs/practitioner_decision_tree.rst)** - Start from your business scenario (campaign in some markets, staggered rollout, survey data) and find the right estimator
|
|
134
|
+
- **[Getting started for practitioners](docs/practitioner_getting_started.rst)** - End-to-end walkthrough: marketing campaign -> causal estimate -> stakeholder-ready result
|
|
135
|
+
- **[Brand awareness survey tutorial](docs/tutorials/17_brand_awareness_survey.ipynb)** - Full example with complex survey design, brand funnel analysis, and staggered rollouts
|
|
136
|
+
- **Have BRFSS/ACS/CPS individual records?** Use [`aggregate_survey()`](docs/api/prep.rst) to roll respondent-level microdata into a geographic-period panel with inverse-variance precision weights. The returned second-stage design uses analytic weights (`aweight`), so it works directly with `DifferenceInDifferences`, `TwoWayFixedEffects`, `MultiPeriodDiD`, `SunAbraham`, `ContinuousDiD`, and `EfficientDiD` (estimators marked **Full** in the [survey support matrix](docs/choosing_estimator.rst))
|
|
137
|
+
|
|
138
|
+
Already know DiD? The [academic quickstart](docs/quickstart.rst) and [estimator guide](docs/choosing_estimator.rst) cover the full technical details.
|
|
139
|
+
|
|
129
140
|
## Features
|
|
130
141
|
|
|
131
142
|
- **sklearn-like API**: Familiar `fit()` interface with `get_params()` and `set_params()`
|
|
@@ -136,6 +147,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
|
136
147
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
137
148
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
138
149
|
- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), Borusyak-Jaravel-Spiess (2024) imputation, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing, Freedman & Hollingsworth 2024), Efficient DiD (Chen, Sant'Anna & Xie 2025), and Wooldridge ETWFE (2021/2023) estimators for heterogeneous treatment timing
|
|
150
|
+
- **Reversible (non-absorbing) treatments**: de Chaisemartin-D'Haultfœuille `DID_M` estimator for treatments that switch on AND off over time (marketing campaigns, seasonal promotions, on/off policy cycles) — the only library option for non-absorbing treatments
|
|
139
151
|
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
140
152
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
141
153
|
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
|
|
@@ -147,6 +159,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
|
147
159
|
- **Pre-trends power analysis**: Roth (2022) minimum detectable violation (MDV) and power curves for pre-trends tests
|
|
148
160
|
- **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
|
|
149
161
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
162
|
+
- **Survey microdata aggregation**: `aggregate_survey()` rolls individual-level survey data (BRFSS, ACS, CPS, NHANES) into geographic-period panels with design-based precision weights for second-stage DiD
|
|
150
163
|
- **Validated against R**: Benchmarked against `did`, `synthdid`, and `fixest` packages (see [benchmarks](docs/benchmarks.rst))
|
|
151
164
|
|
|
152
165
|
## Estimator Aliases
|
|
@@ -169,6 +182,7 @@ All estimators have short aliases for convenience:
|
|
|
169
182
|
| `Bacon` | `BaconDecomposition` | Goodman-Bacon decomposition |
|
|
170
183
|
| `EDiD` | `EfficientDiD` | Efficient DiD |
|
|
171
184
|
| `ETWFE` | `WooldridgeDiD` | Wooldridge ETWFE (2021/2023) |
|
|
185
|
+
| `DCDH` | `ChaisemartinDHaultfoeuille` | de Chaisemartin & D'Haultfœuille (2020) — reversible treatments |
|
|
172
186
|
|
|
173
187
|
`TROP` already uses its short canonical name and needs no alias.
|
|
174
188
|
|
|
@@ -193,6 +207,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
193
207
|
| `13_stacked_did.ipynb` | Stacked DiD (Wing et al. 2024), Q-weights, sub-experiment inspection, trimming, clean control definitions |
|
|
194
208
|
| `15_efficient_did.ipynb` | Efficient DiD (Chen et al. 2025), optimal weighting, PT-All vs PT-Post, efficiency gains, bootstrap inference |
|
|
195
209
|
| `16_survey_did.ipynb` | Survey-aware DiD with complex sampling designs (strata, PSU, FPC, weights), replicate weights, subpopulation analysis, DEFF diagnostics |
|
|
210
|
+
| `17_brand_awareness_survey.ipynb` | Measuring campaign impact on brand awareness with survey data — naive vs. survey-corrected comparison, brand funnel analysis, staggered rollouts, stakeholder communication |
|
|
196
211
|
|
|
197
212
|
## Data Preparation
|
|
198
213
|
|
|
@@ -1189,6 +1204,113 @@ EfficientDiD(
|
|
|
1189
1204
|
| Covariates | Not yet (Phase 2) | Supported (OR, IPW, DR) |
|
|
1190
1205
|
| When to choose | Maximum efficiency, PT-All credible | Covariates needed, weaker PT |
|
|
1191
1206
|
|
|
1207
|
+
### de Chaisemartin-D'Haultfœuille (dCDH) for Reversible Treatments
|
|
1208
|
+
|
|
1209
|
+
`ChaisemartinDHaultfoeuille` (alias `DCDH`) is the only library estimator that handles **non-absorbing (reversible) treatments** — treatment can switch on AND off over time. This is the natural fit for marketing campaigns, seasonal promotions, on/off policy cycles.
|
|
1210
|
+
|
|
1211
|
+
Ships `DID_M` (= `DID_1` at horizon `l = 1`) plus the full multi-horizon event study `DID_l` for `l = 1..L_max` via the `L_max` parameter. Phase 3 will add covariate adjustment.
|
|
1212
|
+
|
|
1213
|
+
```python
|
|
1214
|
+
from diff_diff import ChaisemartinDHaultfoeuille
|
|
1215
|
+
from diff_diff.prep import generate_reversible_did_data
|
|
1216
|
+
|
|
1217
|
+
# Generate a reversible-treatment panel
|
|
1218
|
+
data = generate_reversible_did_data(
|
|
1219
|
+
n_groups=80, n_periods=6, pattern="single_switch", seed=42,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
# Fit the estimator
|
|
1223
|
+
est = ChaisemartinDHaultfoeuille()
|
|
1224
|
+
results = est.fit(
|
|
1225
|
+
data,
|
|
1226
|
+
outcome="outcome",
|
|
1227
|
+
group="group",
|
|
1228
|
+
time="period",
|
|
1229
|
+
treatment="treatment",
|
|
1230
|
+
)
|
|
1231
|
+
results.print_summary()
|
|
1232
|
+
|
|
1233
|
+
# Decomposition
|
|
1234
|
+
print(f"DID_M (overall): {results.overall_att:.3f}")
|
|
1235
|
+
print(f"DID_+ (joiners): {results.joiners_att:.3f}")
|
|
1236
|
+
print(f"DID_- (leavers): {results.leavers_att:.3f}")
|
|
1237
|
+
print(f"Placebo (DID^pl): {results.placebo_effect:.3f}")
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
**Parameters:**
|
|
1241
|
+
|
|
1242
|
+
```python
|
|
1243
|
+
ChaisemartinDHaultfoeuille(
|
|
1244
|
+
alpha=0.05, # Significance level
|
|
1245
|
+
n_bootstrap=0, # 0 = analytical SE only; >0 = multiplier bootstrap
|
|
1246
|
+
bootstrap_weights="rademacher", # 'rademacher', 'mammen', or 'webb'
|
|
1247
|
+
seed=None, # Random seed for bootstrap
|
|
1248
|
+
placebo=True, # Auto-compute single-lag placebo
|
|
1249
|
+
twfe_diagnostic=True, # Auto-compute TWFE decomposition diagnostic
|
|
1250
|
+
drop_larger_lower=True, # Drop multi-switch groups (matches R DIDmultiplegtDYN)
|
|
1251
|
+
rank_deficient_action="warn", # Used by TWFE diagnostic OLS
|
|
1252
|
+
)
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
**What you get back on the results object:**
|
|
1256
|
+
|
|
1257
|
+
| Field | Description |
|
|
1258
|
+
|-------|-------------|
|
|
1259
|
+
| `overall_att`, `overall_se`, `overall_conf_int` | `DID_M` when `L_max=None`; cost-benefit `delta` when `L_max > 1` (delta-method SE from per-horizon SEs) |
|
|
1260
|
+
| `joiners_att`, `leavers_att` | Decomposition into the joiners (`DID_+`) and leavers (`DID_-`) views |
|
|
1261
|
+
| `placebo_effect` | Single-lag placebo (`DID_M^pl`) point estimate |
|
|
1262
|
+
| `per_period_effects` | Per-period decomposition with explicit A11-violation flags |
|
|
1263
|
+
| `twfe_weights`, `twfe_fraction_negative`, `twfe_sigma_fe`, `twfe_beta_fe` | Theorem 1 decomposition diagnostic |
|
|
1264
|
+
| `n_groups_dropped_crossers`, `n_groups_dropped_singleton_baseline` | Filter counts (multi-switch groups dropped before estimation; singleton-baseline groups excluded from variance) |
|
|
1265
|
+
| `n_groups_dropped_never_switching` | Backwards-compatibility metadata. Never-switching groups participate in the variance via stable-control roles; this field is no longer a filter count. |
|
|
1266
|
+
|
|
1267
|
+
**Multi-horizon event study** (Phase 2 - pass `L_max` to `fit()`):
|
|
1268
|
+
|
|
1269
|
+
```python
|
|
1270
|
+
results = est.fit(data, outcome="outcome", group="group",
|
|
1271
|
+
time="period", treatment="treatment", L_max=5)
|
|
1272
|
+
|
|
1273
|
+
# Per-horizon effects with analytical SE
|
|
1274
|
+
for horizon in sorted(results.event_study_effects):
|
|
1275
|
+
e = results.event_study_effects[horizon]
|
|
1276
|
+
print(f" l={horizon}: DID_l={e['effect']:.3f} (SE={e['se']:.3f})")
|
|
1277
|
+
|
|
1278
|
+
# Cost-benefit delta (becomes overall_att when L_max > 1)
|
|
1279
|
+
print(f"Cost-benefit delta: {results.cost_benefit_delta['delta']:.3f}")
|
|
1280
|
+
|
|
1281
|
+
# Normalized effects: DID^n_l = DID_l / l (for binary treatment)
|
|
1282
|
+
for horizon in sorted(results.normalized_effects):
|
|
1283
|
+
print(f" DID^n_{horizon} = {results.normalized_effects[horizon]['effect']:.3f}")
|
|
1284
|
+
|
|
1285
|
+
# Event study DataFrame (includes placebos as negative horizons)
|
|
1286
|
+
df = results.to_dataframe("event_study")
|
|
1287
|
+
|
|
1288
|
+
# Plot (integrates with plot_event_study)
|
|
1289
|
+
from diff_diff import plot_event_study
|
|
1290
|
+
plot_event_study(results)
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
**Standalone TWFE decomposition diagnostic** (without fitting the full estimator):
|
|
1294
|
+
|
|
1295
|
+
```python
|
|
1296
|
+
from diff_diff import twowayfeweights
|
|
1297
|
+
|
|
1298
|
+
diagnostic = twowayfeweights(
|
|
1299
|
+
data, outcome="outcome", group="group", time="period", treatment="treatment",
|
|
1300
|
+
)
|
|
1301
|
+
print(f"Plain TWFE coefficient: {diagnostic.beta_fe:.3f}")
|
|
1302
|
+
print(f"Fraction of negative weights: {diagnostic.fraction_negative:.3f}")
|
|
1303
|
+
print(f"sigma_fe (sign-flipping threshold): {diagnostic.sigma_fe:.3f}")
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
> **Note:** Placebo SE is `NaN` for both the single-lag `DID_M^pl` and the dynamic placebos `DID^{pl}_l`. The point estimates are meaningful for visual pre-trends inspection; formal placebo inference (influence-function derivation) is deferred to a follow-up. See `REGISTRY.md` for the full contract.
|
|
1307
|
+
|
|
1308
|
+
> **Note:** By default (`drop_larger_lower=True`), the estimator drops groups whose treatment switches more than once before estimation. This matches R `DIDmultiplegtDYN`'s default and is required for the analytical variance formula to be consistent with the point estimate. Each drop emits an explicit warning.
|
|
1309
|
+
|
|
1310
|
+
> **Note:** Phase 1 requires panels with a **balanced baseline** (every group observed at the first global period) and **no interior period gaps**. Late-entry groups (missing the baseline) raise `ValueError`; interior-gap groups are dropped with a warning; terminally-missing groups (early exit / right-censoring) are retained and contribute from their observed periods only. This is a documented deviation from R `DIDmultiplegtDYN`, which supports unbalanced panels — see [`docs/methodology/REGISTRY.md`](docs/methodology/REGISTRY.md) for the rationale, the defensive guards that make terminal missingness safe, and workarounds for unbalanced inputs.
|
|
1311
|
+
|
|
1312
|
+
> **Note:** Survey design (`survey_design`), covariate adjustment (`controls`), group-specific linear trends (`trends_linear`), and HonestDiD integration (`honest_did`) are not yet supported. They raise `NotImplementedError` with phase pointers - see [`ROADMAP.md`](ROADMAP.md) for the Phase 3 rollout.
|
|
1313
|
+
|
|
1192
1314
|
### Triple Difference (DDD)
|
|
1193
1315
|
|
|
1194
1316
|
Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations).
|
|
@@ -75,6 +75,17 @@ After estimation, call `practitioner_next_steps(results)` for context-aware guid
|
|
|
75
75
|
|
|
76
76
|
Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
77
77
|
|
|
78
|
+
## For Data Scientists
|
|
79
|
+
|
|
80
|
+
Measuring campaign lift? Evaluating a product launch? diff-diff handles the causal inference so you can focus on the business question.
|
|
81
|
+
|
|
82
|
+
- **[Which method fits my problem?](docs/practitioner_decision_tree.rst)** - Start from your business scenario (campaign in some markets, staggered rollout, survey data) and find the right estimator
|
|
83
|
+
- **[Getting started for practitioners](docs/practitioner_getting_started.rst)** - End-to-end walkthrough: marketing campaign -> causal estimate -> stakeholder-ready result
|
|
84
|
+
- **[Brand awareness survey tutorial](docs/tutorials/17_brand_awareness_survey.ipynb)** - Full example with complex survey design, brand funnel analysis, and staggered rollouts
|
|
85
|
+
- **Have BRFSS/ACS/CPS individual records?** Use [`aggregate_survey()`](docs/api/prep.rst) to roll respondent-level microdata into a geographic-period panel with inverse-variance precision weights. The returned second-stage design uses analytic weights (`aweight`), so it works directly with `DifferenceInDifferences`, `TwoWayFixedEffects`, `MultiPeriodDiD`, `SunAbraham`, `ContinuousDiD`, and `EfficientDiD` (estimators marked **Full** in the [survey support matrix](docs/choosing_estimator.rst))
|
|
86
|
+
|
|
87
|
+
Already know DiD? The [academic quickstart](docs/quickstart.rst) and [estimator guide](docs/choosing_estimator.rst) cover the full technical details.
|
|
88
|
+
|
|
78
89
|
## Features
|
|
79
90
|
|
|
80
91
|
- **sklearn-like API**: Familiar `fit()` interface with `get_params()` and `set_params()`
|
|
@@ -85,6 +96,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
|
85
96
|
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
86
97
|
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
87
98
|
- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), Borusyak-Jaravel-Spiess (2024) imputation, Two-Stage DiD (Gardner 2022), Stacked DiD (Wing, Freedman & Hollingsworth 2024), Efficient DiD (Chen, Sant'Anna & Xie 2025), and Wooldridge ETWFE (2021/2023) estimators for heterogeneous treatment timing
|
|
99
|
+
- **Reversible (non-absorbing) treatments**: de Chaisemartin-D'Haultfœuille `DID_M` estimator for treatments that switch on AND off over time (marketing campaigns, seasonal promotions, on/off policy cycles) — the only library option for non-absorbing treatments
|
|
88
100
|
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
|
|
89
101
|
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
90
102
|
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
|
|
@@ -96,6 +108,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
|
|
|
96
108
|
- **Pre-trends power analysis**: Roth (2022) minimum detectable violation (MDV) and power curves for pre-trends tests
|
|
97
109
|
- **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
|
|
98
110
|
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
111
|
+
- **Survey microdata aggregation**: `aggregate_survey()` rolls individual-level survey data (BRFSS, ACS, CPS, NHANES) into geographic-period panels with design-based precision weights for second-stage DiD
|
|
99
112
|
- **Validated against R**: Benchmarked against `did`, `synthdid`, and `fixest` packages (see [benchmarks](docs/benchmarks.rst))
|
|
100
113
|
|
|
101
114
|
## Estimator Aliases
|
|
@@ -118,6 +131,7 @@ All estimators have short aliases for convenience:
|
|
|
118
131
|
| `Bacon` | `BaconDecomposition` | Goodman-Bacon decomposition |
|
|
119
132
|
| `EDiD` | `EfficientDiD` | Efficient DiD |
|
|
120
133
|
| `ETWFE` | `WooldridgeDiD` | Wooldridge ETWFE (2021/2023) |
|
|
134
|
+
| `DCDH` | `ChaisemartinDHaultfoeuille` | de Chaisemartin & D'Haultfœuille (2020) — reversible treatments |
|
|
121
135
|
|
|
122
136
|
`TROP` already uses its short canonical name and needs no alias.
|
|
123
137
|
|
|
@@ -142,6 +156,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
|
|
|
142
156
|
| `13_stacked_did.ipynb` | Stacked DiD (Wing et al. 2024), Q-weights, sub-experiment inspection, trimming, clean control definitions |
|
|
143
157
|
| `15_efficient_did.ipynb` | Efficient DiD (Chen et al. 2025), optimal weighting, PT-All vs PT-Post, efficiency gains, bootstrap inference |
|
|
144
158
|
| `16_survey_did.ipynb` | Survey-aware DiD with complex sampling designs (strata, PSU, FPC, weights), replicate weights, subpopulation analysis, DEFF diagnostics |
|
|
159
|
+
| `17_brand_awareness_survey.ipynb` | Measuring campaign impact on brand awareness with survey data — naive vs. survey-corrected comparison, brand funnel analysis, staggered rollouts, stakeholder communication |
|
|
145
160
|
|
|
146
161
|
## Data Preparation
|
|
147
162
|
|
|
@@ -1138,6 +1153,113 @@ EfficientDiD(
|
|
|
1138
1153
|
| Covariates | Not yet (Phase 2) | Supported (OR, IPW, DR) |
|
|
1139
1154
|
| When to choose | Maximum efficiency, PT-All credible | Covariates needed, weaker PT |
|
|
1140
1155
|
|
|
1156
|
+
### de Chaisemartin-D'Haultfœuille (dCDH) for Reversible Treatments
|
|
1157
|
+
|
|
1158
|
+
`ChaisemartinDHaultfoeuille` (alias `DCDH`) is the only library estimator that handles **non-absorbing (reversible) treatments** — treatment can switch on AND off over time. This is the natural fit for marketing campaigns, seasonal promotions, on/off policy cycles.
|
|
1159
|
+
|
|
1160
|
+
Ships `DID_M` (= `DID_1` at horizon `l = 1`) plus the full multi-horizon event study `DID_l` for `l = 1..L_max` via the `L_max` parameter. Phase 3 will add covariate adjustment.
|
|
1161
|
+
|
|
1162
|
+
```python
|
|
1163
|
+
from diff_diff import ChaisemartinDHaultfoeuille
|
|
1164
|
+
from diff_diff.prep import generate_reversible_did_data
|
|
1165
|
+
|
|
1166
|
+
# Generate a reversible-treatment panel
|
|
1167
|
+
data = generate_reversible_did_data(
|
|
1168
|
+
n_groups=80, n_periods=6, pattern="single_switch", seed=42,
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
# Fit the estimator
|
|
1172
|
+
est = ChaisemartinDHaultfoeuille()
|
|
1173
|
+
results = est.fit(
|
|
1174
|
+
data,
|
|
1175
|
+
outcome="outcome",
|
|
1176
|
+
group="group",
|
|
1177
|
+
time="period",
|
|
1178
|
+
treatment="treatment",
|
|
1179
|
+
)
|
|
1180
|
+
results.print_summary()
|
|
1181
|
+
|
|
1182
|
+
# Decomposition
|
|
1183
|
+
print(f"DID_M (overall): {results.overall_att:.3f}")
|
|
1184
|
+
print(f"DID_+ (joiners): {results.joiners_att:.3f}")
|
|
1185
|
+
print(f"DID_- (leavers): {results.leavers_att:.3f}")
|
|
1186
|
+
print(f"Placebo (DID^pl): {results.placebo_effect:.3f}")
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
**Parameters:**
|
|
1190
|
+
|
|
1191
|
+
```python
|
|
1192
|
+
ChaisemartinDHaultfoeuille(
|
|
1193
|
+
alpha=0.05, # Significance level
|
|
1194
|
+
n_bootstrap=0, # 0 = analytical SE only; >0 = multiplier bootstrap
|
|
1195
|
+
bootstrap_weights="rademacher", # 'rademacher', 'mammen', or 'webb'
|
|
1196
|
+
seed=None, # Random seed for bootstrap
|
|
1197
|
+
placebo=True, # Auto-compute single-lag placebo
|
|
1198
|
+
twfe_diagnostic=True, # Auto-compute TWFE decomposition diagnostic
|
|
1199
|
+
drop_larger_lower=True, # Drop multi-switch groups (matches R DIDmultiplegtDYN)
|
|
1200
|
+
rank_deficient_action="warn", # Used by TWFE diagnostic OLS
|
|
1201
|
+
)
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
**What you get back on the results object:**
|
|
1205
|
+
|
|
1206
|
+
| Field | Description |
|
|
1207
|
+
|-------|-------------|
|
|
1208
|
+
| `overall_att`, `overall_se`, `overall_conf_int` | `DID_M` when `L_max=None`; cost-benefit `delta` when `L_max > 1` (delta-method SE from per-horizon SEs) |
|
|
1209
|
+
| `joiners_att`, `leavers_att` | Decomposition into the joiners (`DID_+`) and leavers (`DID_-`) views |
|
|
1210
|
+
| `placebo_effect` | Single-lag placebo (`DID_M^pl`) point estimate |
|
|
1211
|
+
| `per_period_effects` | Per-period decomposition with explicit A11-violation flags |
|
|
1212
|
+
| `twfe_weights`, `twfe_fraction_negative`, `twfe_sigma_fe`, `twfe_beta_fe` | Theorem 1 decomposition diagnostic |
|
|
1213
|
+
| `n_groups_dropped_crossers`, `n_groups_dropped_singleton_baseline` | Filter counts (multi-switch groups dropped before estimation; singleton-baseline groups excluded from variance) |
|
|
1214
|
+
| `n_groups_dropped_never_switching` | Backwards-compatibility metadata. Never-switching groups participate in the variance via stable-control roles; this field is no longer a filter count. |
|
|
1215
|
+
|
|
1216
|
+
**Multi-horizon event study** (Phase 2 - pass `L_max` to `fit()`):
|
|
1217
|
+
|
|
1218
|
+
```python
|
|
1219
|
+
results = est.fit(data, outcome="outcome", group="group",
|
|
1220
|
+
time="period", treatment="treatment", L_max=5)
|
|
1221
|
+
|
|
1222
|
+
# Per-horizon effects with analytical SE
|
|
1223
|
+
for horizon in sorted(results.event_study_effects):
|
|
1224
|
+
e = results.event_study_effects[horizon]
|
|
1225
|
+
print(f" l={horizon}: DID_l={e['effect']:.3f} (SE={e['se']:.3f})")
|
|
1226
|
+
|
|
1227
|
+
# Cost-benefit delta (becomes overall_att when L_max > 1)
|
|
1228
|
+
print(f"Cost-benefit delta: {results.cost_benefit_delta['delta']:.3f}")
|
|
1229
|
+
|
|
1230
|
+
# Normalized effects: DID^n_l = DID_l / l (for binary treatment)
|
|
1231
|
+
for horizon in sorted(results.normalized_effects):
|
|
1232
|
+
print(f" DID^n_{horizon} = {results.normalized_effects[horizon]['effect']:.3f}")
|
|
1233
|
+
|
|
1234
|
+
# Event study DataFrame (includes placebos as negative horizons)
|
|
1235
|
+
df = results.to_dataframe("event_study")
|
|
1236
|
+
|
|
1237
|
+
# Plot (integrates with plot_event_study)
|
|
1238
|
+
from diff_diff import plot_event_study
|
|
1239
|
+
plot_event_study(results)
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
**Standalone TWFE decomposition diagnostic** (without fitting the full estimator):
|
|
1243
|
+
|
|
1244
|
+
```python
|
|
1245
|
+
from diff_diff import twowayfeweights
|
|
1246
|
+
|
|
1247
|
+
diagnostic = twowayfeweights(
|
|
1248
|
+
data, outcome="outcome", group="group", time="period", treatment="treatment",
|
|
1249
|
+
)
|
|
1250
|
+
print(f"Plain TWFE coefficient: {diagnostic.beta_fe:.3f}")
|
|
1251
|
+
print(f"Fraction of negative weights: {diagnostic.fraction_negative:.3f}")
|
|
1252
|
+
print(f"sigma_fe (sign-flipping threshold): {diagnostic.sigma_fe:.3f}")
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
> **Note:** Placebo SE is `NaN` for both the single-lag `DID_M^pl` and the dynamic placebos `DID^{pl}_l`. The point estimates are meaningful for visual pre-trends inspection; formal placebo inference (influence-function derivation) is deferred to a follow-up. See `REGISTRY.md` for the full contract.
|
|
1256
|
+
|
|
1257
|
+
> **Note:** By default (`drop_larger_lower=True`), the estimator drops groups whose treatment switches more than once before estimation. This matches R `DIDmultiplegtDYN`'s default and is required for the analytical variance formula to be consistent with the point estimate. Each drop emits an explicit warning.
|
|
1258
|
+
|
|
1259
|
+
> **Note:** Phase 1 requires panels with a **balanced baseline** (every group observed at the first global period) and **no interior period gaps**. Late-entry groups (missing the baseline) raise `ValueError`; interior-gap groups are dropped with a warning; terminally-missing groups (early exit / right-censoring) are retained and contribute from their observed periods only. This is a documented deviation from R `DIDmultiplegtDYN`, which supports unbalanced panels — see [`docs/methodology/REGISTRY.md`](docs/methodology/REGISTRY.md) for the rationale, the defensive guards that make terminal missingness safe, and workarounds for unbalanced inputs.
|
|
1260
|
+
|
|
1261
|
+
> **Note:** Survey design (`survey_design`), covariate adjustment (`controls`), group-specific linear trends (`trends_linear`), and HonestDiD integration (`honest_did`) are not yet supported. They raise `NotImplementedError` with phase pointers - see [`ROADMAP.md`](ROADMAP.md) for the Phase 3 rollout.
|
|
1262
|
+
|
|
1141
1263
|
### Triple Difference (DDD)
|
|
1142
1264
|
|
|
1143
1265
|
Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations).
|
|
@@ -63,6 +63,7 @@ from diff_diff.power import (
|
|
|
63
63
|
SimulationMDEResults,
|
|
64
64
|
SimulationPowerResults,
|
|
65
65
|
SimulationSampleSizeResults,
|
|
66
|
+
SurveyPowerConfig,
|
|
66
67
|
compute_mde,
|
|
67
68
|
compute_power,
|
|
68
69
|
compute_sample_size,
|
|
@@ -88,6 +89,7 @@ from diff_diff.prep import (
|
|
|
88
89
|
generate_event_study_data,
|
|
89
90
|
generate_factor_data,
|
|
90
91
|
generate_panel_data,
|
|
92
|
+
generate_reversible_did_data,
|
|
91
93
|
generate_staggered_data,
|
|
92
94
|
generate_staggered_ddd_data,
|
|
93
95
|
generate_survey_did_data,
|
|
@@ -160,6 +162,16 @@ from diff_diff.efficient_did import (
|
|
|
160
162
|
EfficientDiDResults,
|
|
161
163
|
EDiDBootstrapResults,
|
|
162
164
|
)
|
|
165
|
+
from diff_diff.chaisemartin_dhaultfoeuille import (
|
|
166
|
+
ChaisemartinDHaultfoeuille,
|
|
167
|
+
TWFEWeightsResult,
|
|
168
|
+
chaisemartin_dhaultfoeuille,
|
|
169
|
+
twowayfeweights,
|
|
170
|
+
)
|
|
171
|
+
from diff_diff.chaisemartin_dhaultfoeuille_results import (
|
|
172
|
+
ChaisemartinDHaultfoeuilleResults,
|
|
173
|
+
DCDHBootstrapResults,
|
|
174
|
+
)
|
|
163
175
|
from diff_diff.trop import (
|
|
164
176
|
TROP,
|
|
165
177
|
TROPResults,
|
|
@@ -214,8 +226,9 @@ Stacked = StackedDiD
|
|
|
214
226
|
Bacon = BaconDecomposition
|
|
215
227
|
EDiD = EfficientDiD
|
|
216
228
|
ETWFE = WooldridgeDiD
|
|
229
|
+
DCDH = ChaisemartinDHaultfoeuille
|
|
217
230
|
|
|
218
|
-
__version__ = "3.0
|
|
231
|
+
__version__ = "3.1.0"
|
|
219
232
|
__all__ = [
|
|
220
233
|
# Estimators
|
|
221
234
|
"DifferenceInDifferences",
|
|
@@ -223,6 +236,7 @@ __all__ = [
|
|
|
223
236
|
"MultiPeriodDiD",
|
|
224
237
|
"SyntheticDiD",
|
|
225
238
|
"CallawaySantAnna",
|
|
239
|
+
"ChaisemartinDHaultfoeuille",
|
|
226
240
|
"ContinuousDiD",
|
|
227
241
|
"SunAbraham",
|
|
228
242
|
"ImputationDiD",
|
|
@@ -237,6 +251,7 @@ __all__ = [
|
|
|
237
251
|
"SDiD",
|
|
238
252
|
"CS",
|
|
239
253
|
"CDiD",
|
|
254
|
+
"DCDH",
|
|
240
255
|
"SA",
|
|
241
256
|
"BJS",
|
|
242
257
|
"Gardner",
|
|
@@ -280,6 +295,12 @@ __all__ = [
|
|
|
280
295
|
"EfficientDiDResults",
|
|
281
296
|
"EDiDBootstrapResults",
|
|
282
297
|
"EDiD",
|
|
298
|
+
# ChaisemartinDHaultfoeuille (dCDH)
|
|
299
|
+
"ChaisemartinDHaultfoeuilleResults",
|
|
300
|
+
"DCDHBootstrapResults",
|
|
301
|
+
"TWFEWeightsResult",
|
|
302
|
+
"chaisemartin_dhaultfoeuille",
|
|
303
|
+
"twowayfeweights",
|
|
283
304
|
# WooldridgeDiD (ETWFE)
|
|
284
305
|
"WooldridgeDiD",
|
|
285
306
|
"WooldridgeDiDResults",
|
|
@@ -328,6 +349,7 @@ __all__ = [
|
|
|
328
349
|
"generate_staggered_ddd_data",
|
|
329
350
|
"generate_survey_did_data",
|
|
330
351
|
"generate_continuous_did_data",
|
|
352
|
+
"generate_reversible_did_data",
|
|
331
353
|
"create_event_time",
|
|
332
354
|
"aggregate_survey",
|
|
333
355
|
"aggregate_to_cohorts",
|
|
@@ -347,6 +369,7 @@ __all__ = [
|
|
|
347
369
|
"SimulationMDEResults",
|
|
348
370
|
"SimulationPowerResults",
|
|
349
371
|
"SimulationSampleSizeResults",
|
|
372
|
+
"SurveyPowerConfig",
|
|
350
373
|
"compute_mde",
|
|
351
374
|
"compute_power",
|
|
352
375
|
"compute_sample_size",
|