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.
- {diff_diff-3.0.0 → diff_diff-3.0.2}/PKG-INFO +126 -3
- {diff_diff-3.0.0 → diff_diff-3.0.2}/README.md +123 -1
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/__init__.py +26 -1
- diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille.py +3372 -0
- diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +413 -0
- diff_diff-3.0.2/diff_diff/chaisemartin_dhaultfoeuille_results.py +1004 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/power.py +612 -32
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/prep.py +488 -7
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/prep_dgp.py +411 -72
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_event_study.py +60 -1
- {diff_diff-3.0.0 → diff_diff-3.0.2}/pyproject.toml +3 -2
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/Cargo.lock +26 -54
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/Cargo.toml +6 -5
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/bootstrap.rs +1 -1
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/linalg.rs +5 -5
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/trop.rs +3 -3
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/src/weights.rs +5 -5
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/_backend.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/bacon.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/datasets.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/estimators.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/honest_did.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/imputation_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/linalg.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/practitioner.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/stacked_did.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_triple_diff.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/survey.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/triple_diff.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/twfe.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/utils.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/wooldridge.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.0.0 → diff_diff-3.0.2}/rust/build.rs +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|