diff-diff 3.0.0__tar.gz → 3.0.2__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.0 → diff_diff-3.0.2}/PKG-INFO +126 -3
  2. {diff_diff-3.0.0 → diff_diff-3.0.2}/README.md +123 -1
  3. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/__init__.py +26 -1
  4. diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille.py +3372 -0
  5. diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +413 -0
  6. diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille_results.py +1004 -0
  7. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/power.py +612 -32
  8. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/prep.py +488 -7
  9. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/prep_dgp.py +411 -72
  10. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_event_study.py +60 -1
  11. {diff_diff-3.0.0 → diff_diff-3.0.2}/pyproject.toml +3 -2
  12. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/Cargo.lock +26 -54
  13. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/Cargo.toml +6 -5
  14. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/bootstrap.rs +1 -1
  15. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/linalg.rs +5 -5
  16. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/trop.rs +3 -3
  17. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/weights.rs +5 -5
  18. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/_backend.py +0 -0
  19. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/bacon.py +0 -0
  20. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/bootstrap_utils.py +0 -0
  21. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did.py +0 -0
  22. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did_bspline.py +0 -0
  23. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did_results.py +0 -0
  24. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/datasets.py +0 -0
  25. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/diagnostics.py +0 -0
  26. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did.py +0 -0
  27. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_bootstrap.py +0 -0
  28. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_covariates.py +0 -0
  29. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_results.py +0 -0
  30. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_weights.py +0 -0
  31. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/estimators.py +0 -0
  32. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/honest_did.py +0 -0
  33. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation.py +0 -0
  34. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation_bootstrap.py +0 -0
  35. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation_results.py +0 -0
  36. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/linalg.py +0 -0
  37. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/practitioner.py +0 -0
  38. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/pretrends.py +0 -0
  39. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/results.py +0 -0
  40. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/stacked_did.py +0 -0
  41. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/stacked_did_results.py +0 -0
  42. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered.py +0 -0
  43. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_aggregation.py +0 -0
  44. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_bootstrap.py +0 -0
  45. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_results.py +0 -0
  46. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_triple_diff.py +0 -0
  47. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_triple_diff_results.py +0 -0
  48. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/sun_abraham.py +0 -0
  49. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/survey.py +0 -0
  50. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/synthetic_did.py +0 -0
  51. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/triple_diff.py +0 -0
  52. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop.py +0 -0
  53. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_global.py +0 -0
  54. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_local.py +0 -0
  55. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_results.py +0 -0
  56. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/twfe.py +0 -0
  57. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage.py +0 -0
  58. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage_bootstrap.py +0 -0
  59. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage_results.py +0 -0
  60. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/utils.py +0 -0
  61. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/__init__.py +0 -0
  62. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_common.py +0 -0
  63. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_continuous.py +0 -0
  64. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_diagnostic.py +0 -0
  65. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_power.py +0 -0
  66. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_staggered.py +0 -0
  67. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_synthetic.py +0 -0
  68. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/wooldridge.py +0 -0
  69. {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/wooldridge_results.py +0 -0
  70. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/build.rs +0 -0
  71. {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/lib.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 3.0.0
3
+ Version: 3.0.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
13
14
  Classifier: Topic :: Scientific/Engineering :: Mathematics
14
15
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
16
  Classifier: Topic :: Scientific/Engineering
@@ -40,7 +41,7 @@ Summary: Difference-in-Differences causal inference with sklearn-like API. Calla
40
41
  Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects,event-study,staggered-adoption,parallel-trends,synthetic-control,panel-data,did,twfe,callaway-santanna,honest-did,sensitivity-analysis
41
42
  Author: diff-diff contributors
42
43
  License-Expression: MIT
43
- Requires-Python: >=3.9, <3.14
44
+ Requires-Python: >=3.9, <3.15
44
45
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
45
46
  Project-URL: Documentation, https://diff-diff.readthedocs.io
46
47
  Project-URL: Homepage, https://github.com/igerber/diff-diff
@@ -125,6 +126,17 @@ After estimation, call `practitioner_next_steps(results)` for context-aware guid
125
126
 
126
127
  Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
127
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
+
128
140
  ## Features
129
141
 
130
142
  - **sklearn-like API**: Familiar `fit()` interface with `get_params()` and `set_params()`
@@ -135,6 +147,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
135
147
  - **Panel data support**: Two-way fixed effects estimator for panel designs
136
148
  - **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
137
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
138
151
  - **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
139
152
  - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
140
153
  - **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
@@ -146,6 +159,7 @@ Detailed guide: [`docs/llms-practitioner.txt`](docs/llms-practitioner.txt)
146
159
  - **Pre-trends power analysis**: Roth (2022) minimum detectable violation (MDV) and power curves for pre-trends tests
147
160
  - **Power analysis**: MDE, sample size, and power calculations for study design; simulation-based power for any estimator
148
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
149
163
  - **Validated against R**: Benchmarked against `did`, `synthdid`, and `fixest` packages (see [benchmarks](docs/benchmarks.rst))
150
164
 
151
165
  ## Estimator Aliases
@@ -168,6 +182,7 @@ All estimators have short aliases for convenience:
168
182
  | `Bacon` | `BaconDecomposition` | Goodman-Bacon decomposition |
169
183
  | `EDiD` | `EfficientDiD` | Efficient DiD |
170
184
  | `ETWFE` | `WooldridgeDiD` | Wooldridge ETWFE (2021/2023) |
185
+ | `DCDH` | `ChaisemartinDHaultfoeuille` | de Chaisemartin & D'Haultfœuille (2020) — reversible treatments |
171
186
 
172
187
  `TROP` already uses its short canonical name and needs no alias.
173
188
 
@@ -192,6 +207,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
192
207
  | `13_stacked_did.ipynb` | Stacked DiD (Wing et al. 2024), Q-weights, sub-experiment inspection, trimming, clean control definitions |
193
208
  | `15_efficient_did.ipynb` | Efficient DiD (Chen et al. 2025), optimal weighting, PT-All vs PT-Post, efficiency gains, bootstrap inference |
194
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 |
195
211
 
196
212
  ## Data Preparation
197
213
 
@@ -1188,6 +1204,113 @@ EfficientDiD(
1188
1204
  | Covariates | Not yet (Phase 2) | Supported (OR, IPW, DR) |
1189
1205
  | When to choose | Maximum efficiency, PT-All credible | Covariates needed, weaker PT |
1190
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
+
1191
1314
  ### Triple Difference (DDD)
1192
1315
 
1193
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).
@@ -2819,7 +2942,7 @@ Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`,
2819
2942
 
2820
2943
  ## Requirements
2821
2944
 
2822
- - Python 3.9 - 3.13
2945
+ - Python 3.9 - 3.14
2823
2946
  - numpy >= 1.20
2824
2947
  - pandas >= 1.3
2825
2948
  - scipy >= 1.7
@@ -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).
@@ -2769,7 +2891,7 @@ Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`,
2769
2891
 
2770
2892
  ## Requirements
2771
2893
 
2772
- - Python 3.9 - 3.13
2894
+ - Python 3.9 - 3.14
2773
2895
  - numpy >= 1.20
2774
2896
  - pandas >= 1.3
2775
2897
  - scipy >= 1.7
@@ -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,
@@ -78,6 +79,7 @@ from diff_diff.pretrends import (
78
79
  compute_pretrends_power,
79
80
  )
80
81
  from diff_diff.prep import (
82
+ aggregate_survey,
81
83
  aggregate_to_cohorts,
82
84
  balance_panel,
83
85
  create_event_time,
@@ -87,6 +89,7 @@ from diff_diff.prep import (
87
89
  generate_event_study_data,
88
90
  generate_factor_data,
89
91
  generate_panel_data,
92
+ generate_reversible_did_data,
90
93
  generate_staggered_data,
91
94
  generate_staggered_ddd_data,
92
95
  generate_survey_did_data,
@@ -159,6 +162,16 @@ from diff_diff.efficient_did import (
159
162
  EfficientDiDResults,
160
163
  EDiDBootstrapResults,
161
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
+ )
162
175
  from diff_diff.trop import (
163
176
  TROP,
164
177
  TROPResults,
@@ -213,8 +226,9 @@ Stacked = StackedDiD
213
226
  Bacon = BaconDecomposition
214
227
  EDiD = EfficientDiD
215
228
  ETWFE = WooldridgeDiD
229
+ DCDH = ChaisemartinDHaultfoeuille
216
230
 
217
- __version__ = "3.0.0"
231
+ __version__ = "3.0.2"
218
232
  __all__ = [
219
233
  # Estimators
220
234
  "DifferenceInDifferences",
@@ -222,6 +236,7 @@ __all__ = [
222
236
  "MultiPeriodDiD",
223
237
  "SyntheticDiD",
224
238
  "CallawaySantAnna",
239
+ "ChaisemartinDHaultfoeuille",
225
240
  "ContinuousDiD",
226
241
  "SunAbraham",
227
242
  "ImputationDiD",
@@ -236,6 +251,7 @@ __all__ = [
236
251
  "SDiD",
237
252
  "CS",
238
253
  "CDiD",
254
+ "DCDH",
239
255
  "SA",
240
256
  "BJS",
241
257
  "Gardner",
@@ -279,6 +295,12 @@ __all__ = [
279
295
  "EfficientDiDResults",
280
296
  "EDiDBootstrapResults",
281
297
  "EDiD",
298
+ # ChaisemartinDHaultfoeuille (dCDH)
299
+ "ChaisemartinDHaultfoeuilleResults",
300
+ "DCDHBootstrapResults",
301
+ "TWFEWeightsResult",
302
+ "chaisemartin_dhaultfoeuille",
303
+ "twowayfeweights",
282
304
  # WooldridgeDiD (ETWFE)
283
305
  "WooldridgeDiD",
284
306
  "WooldridgeDiDResults",
@@ -327,7 +349,9 @@ __all__ = [
327
349
  "generate_staggered_ddd_data",
328
350
  "generate_survey_did_data",
329
351
  "generate_continuous_did_data",
352
+ "generate_reversible_did_data",
330
353
  "create_event_time",
354
+ "aggregate_survey",
331
355
  "aggregate_to_cohorts",
332
356
  "rank_control_units",
333
357
  # Honest DiD sensitivity analysis
@@ -345,6 +369,7 @@ __all__ = [
345
369
  "SimulationMDEResults",
346
370
  "SimulationPowerResults",
347
371
  "SimulationSampleSizeResults",
372
+ "SurveyPowerConfig",
348
373
  "compute_mde",
349
374
  "compute_power",
350
375
  "compute_sample_size",