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.
Files changed (71) hide show
  1. {diff_diff-3.0.1 → diff_diff-3.1.0}/PKG-INFO +123 -1
  2. {diff_diff-3.0.1 → diff_diff-3.1.0}/README.md +122 -0
  3. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/__init__.py +24 -1
  4. diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille.py +4736 -0
  5. diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +399 -0
  6. diff_diff-3.1.0/diff_diff/chaisemartin_dhaultfoeuille_results.py +1303 -0
  7. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/honest_did.py +192 -9
  8. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/power.py +612 -32
  9. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/prep.py +65 -20
  10. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/prep_dgp.py +411 -72
  11. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_event_study.py +60 -1
  12. {diff_diff-3.0.1 → diff_diff-3.1.0}/pyproject.toml +1 -1
  13. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/Cargo.lock +13 -13
  14. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/Cargo.toml +1 -1
  15. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/_backend.py +0 -0
  16. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/bacon.py +0 -0
  17. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/bootstrap_utils.py +0 -0
  18. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did.py +0 -0
  19. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did_bspline.py +0 -0
  20. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/continuous_did_results.py +0 -0
  21. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/datasets.py +0 -0
  22. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/diagnostics.py +0 -0
  23. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did.py +0 -0
  24. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_bootstrap.py +0 -0
  25. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_covariates.py +0 -0
  26. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_results.py +0 -0
  27. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/efficient_did_weights.py +0 -0
  28. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/estimators.py +0 -0
  29. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation.py +0 -0
  30. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation_bootstrap.py +0 -0
  31. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/imputation_results.py +0 -0
  32. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/linalg.py +0 -0
  33. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/practitioner.py +0 -0
  34. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/pretrends.py +0 -0
  35. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/results.py +0 -0
  36. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/stacked_did.py +0 -0
  37. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/stacked_did_results.py +0 -0
  38. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered.py +0 -0
  39. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_aggregation.py +0 -0
  40. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_bootstrap.py +0 -0
  41. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_results.py +0 -0
  42. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_triple_diff.py +0 -0
  43. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/staggered_triple_diff_results.py +0 -0
  44. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/sun_abraham.py +0 -0
  45. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/survey.py +0 -0
  46. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/synthetic_did.py +0 -0
  47. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/triple_diff.py +0 -0
  48. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop.py +0 -0
  49. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_global.py +0 -0
  50. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_local.py +0 -0
  51. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/trop_results.py +0 -0
  52. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/twfe.py +0 -0
  53. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage.py +0 -0
  54. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage_bootstrap.py +0 -0
  55. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/two_stage_results.py +0 -0
  56. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/utils.py +0 -0
  57. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/__init__.py +0 -0
  58. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_common.py +0 -0
  59. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_continuous.py +0 -0
  60. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_diagnostic.py +0 -0
  61. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_power.py +0 -0
  62. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_staggered.py +0 -0
  63. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/visualization/_synthetic.py +0 -0
  64. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/wooldridge.py +0 -0
  65. {diff_diff-3.0.1 → diff_diff-3.1.0}/diff_diff/wooldridge_results.py +0 -0
  66. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/build.rs +0 -0
  67. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/bootstrap.rs +0 -0
  68. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/lib.rs +0 -0
  69. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/linalg.rs +0 -0
  70. {diff_diff-3.0.1 → diff_diff-3.1.0}/rust/src/trop.rs +0 -0
  71. {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.1
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.1"
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",