diff-diff 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diff_diff-0.3.0/PKG-INFO +1145 -0
- diff_diff-0.3.0/README.md +1110 -0
- diff_diff-0.3.0/diff_diff/__init__.py +43 -0
- diff_diff-0.3.0/diff_diff/estimators.py +1561 -0
- diff_diff-0.3.0/diff_diff/prep.py +844 -0
- diff_diff-0.3.0/diff_diff/results.py +671 -0
- {diff_diff-0.1.0 → diff_diff-0.3.0}/diff_diff/utils.py +430 -37
- diff_diff-0.3.0/diff_diff.egg-info/PKG-INFO +1145 -0
- {diff_diff-0.1.0 → diff_diff-0.3.0}/diff_diff.egg-info/SOURCES.txt +3 -1
- {diff_diff-0.1.0 → diff_diff-0.3.0}/pyproject.toml +2 -2
- diff_diff-0.3.0/tests/test_estimators.py +2179 -0
- diff_diff-0.3.0/tests/test_prep.py +451 -0
- diff_diff-0.1.0/PKG-INFO +0 -421
- diff_diff-0.1.0/README.md +0 -386
- diff_diff-0.1.0/diff_diff/__init__.py +0 -12
- diff_diff-0.1.0/diff_diff/estimators.py +0 -701
- diff_diff-0.1.0/diff_diff/results.py +0 -170
- diff_diff-0.1.0/diff_diff.egg-info/PKG-INFO +0 -421
- diff_diff-0.1.0/tests/test_estimators.py +0 -690
- {diff_diff-0.1.0 → diff_diff-0.3.0}/diff_diff.egg-info/dependency_links.txt +0 -0
- {diff_diff-0.1.0 → diff_diff-0.3.0}/diff_diff.egg-info/requires.txt +0 -0
- {diff_diff-0.1.0 → diff_diff-0.3.0}/diff_diff.egg-info/top_level.txt +0 -0
- {diff_diff-0.1.0 → diff_diff-0.3.0}/setup.cfg +0 -0
diff_diff-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diff-diff
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
|
+
Author: diff-diff contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/igerber/diff-diff
|
|
8
|
+
Project-URL: Documentation, https://github.com/igerber/diff-diff#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/igerber/diff-diff
|
|
10
|
+
Project-URL: Issues, https://github.com/igerber/diff-diff/issues
|
|
11
|
+
Keywords: causal-inference,difference-in-differences,econometrics,statistics,treatment-effects
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: numpy>=1.20.0
|
|
24
|
+
Requires-Dist: pandas>=1.3.0
|
|
25
|
+
Requires-Dist: scipy>=1.7.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: sphinx>=6.0; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
35
|
+
|
|
36
|
+
# diff-diff
|
|
37
|
+
|
|
38
|
+
A Python library for Difference-in-Differences (DiD) causal inference analysis with an sklearn-like API and statsmodels-style outputs.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install diff-diff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/igerber/diff-diff.git
|
|
50
|
+
cd diff-diff
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import pandas as pd
|
|
58
|
+
from diff_diff import DifferenceInDifferences
|
|
59
|
+
|
|
60
|
+
# Create sample data
|
|
61
|
+
data = pd.DataFrame({
|
|
62
|
+
'outcome': [10, 11, 15, 18, 9, 10, 12, 13],
|
|
63
|
+
'treated': [1, 1, 1, 1, 0, 0, 0, 0],
|
|
64
|
+
'post': [0, 0, 1, 1, 0, 0, 1, 1]
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Fit the model
|
|
68
|
+
did = DifferenceInDifferences()
|
|
69
|
+
results = did.fit(data, outcome='outcome', treatment='treated', time='post')
|
|
70
|
+
|
|
71
|
+
# View results
|
|
72
|
+
print(results) # DiDResults(ATT=3.5000*, SE=1.2583, p=0.0367)
|
|
73
|
+
results.print_summary()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Output:
|
|
77
|
+
```
|
|
78
|
+
======================================================================
|
|
79
|
+
Difference-in-Differences Estimation Results
|
|
80
|
+
======================================================================
|
|
81
|
+
|
|
82
|
+
Observations: 8
|
|
83
|
+
Treated units: 4
|
|
84
|
+
Control units: 4
|
|
85
|
+
R-squared: 0.9123
|
|
86
|
+
|
|
87
|
+
----------------------------------------------------------------------
|
|
88
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
89
|
+
----------------------------------------------------------------------
|
|
90
|
+
ATT 3.5000 1.2583 2.782 0.0367
|
|
91
|
+
----------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
95% Confidence Interval: [0.3912, 6.6088]
|
|
94
|
+
|
|
95
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
96
|
+
======================================================================
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Features
|
|
100
|
+
|
|
101
|
+
- **sklearn-like API**: Familiar `fit()` interface with `get_params()` and `set_params()`
|
|
102
|
+
- **Pythonic results**: Easy access to coefficients, standard errors, and confidence intervals
|
|
103
|
+
- **Multiple interfaces**: Column names or R-style formulas
|
|
104
|
+
- **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
|
|
105
|
+
- **Panel data support**: Two-way fixed effects estimator for panel designs
|
|
106
|
+
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
|
|
107
|
+
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
|
|
108
|
+
- **Data prep utilities**: Helper functions for common data preparation tasks
|
|
109
|
+
|
|
110
|
+
## Data Preparation
|
|
111
|
+
|
|
112
|
+
diff-diff provides utility functions to help prepare your data for DiD analysis. These functions handle common data transformation tasks like creating treatment indicators, reshaping panel data, and validating data formats.
|
|
113
|
+
|
|
114
|
+
### Generate Sample Data
|
|
115
|
+
|
|
116
|
+
Create synthetic data with a known treatment effect for testing and learning:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from diff_diff import generate_did_data, DifferenceInDifferences
|
|
120
|
+
|
|
121
|
+
# Generate panel data with 100 units, 4 periods, and a treatment effect of 5
|
|
122
|
+
data = generate_did_data(
|
|
123
|
+
n_units=100,
|
|
124
|
+
n_periods=4,
|
|
125
|
+
treatment_effect=5.0,
|
|
126
|
+
treatment_fraction=0.5, # 50% of units are treated
|
|
127
|
+
treatment_period=2, # Treatment starts at period 2
|
|
128
|
+
seed=42
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Verify the estimator recovers the treatment effect
|
|
132
|
+
did = DifferenceInDifferences()
|
|
133
|
+
results = did.fit(data, outcome='outcome', treatment='treated', time='post')
|
|
134
|
+
print(f"Estimated ATT: {results.att:.2f} (true: 5.0)")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Create Treatment Indicators
|
|
138
|
+
|
|
139
|
+
Convert categorical variables or numeric thresholds to binary treatment indicators:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from diff_diff import make_treatment_indicator
|
|
143
|
+
|
|
144
|
+
# From categorical variable
|
|
145
|
+
df = make_treatment_indicator(
|
|
146
|
+
data,
|
|
147
|
+
column='state',
|
|
148
|
+
treated_values=['CA', 'NY', 'TX'] # These states are treated
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# From numeric threshold (e.g., firms above median size)
|
|
152
|
+
df = make_treatment_indicator(
|
|
153
|
+
data,
|
|
154
|
+
column='firm_size',
|
|
155
|
+
threshold=data['firm_size'].median()
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Treat units below threshold
|
|
159
|
+
df = make_treatment_indicator(
|
|
160
|
+
data,
|
|
161
|
+
column='income',
|
|
162
|
+
threshold=50000,
|
|
163
|
+
above_threshold=False # Units with income <= 50000 are treated
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Create Post-Treatment Indicators
|
|
168
|
+
|
|
169
|
+
Convert time/date columns to binary post-treatment indicators:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from diff_diff import make_post_indicator
|
|
173
|
+
|
|
174
|
+
# From specific post-treatment periods
|
|
175
|
+
df = make_post_indicator(
|
|
176
|
+
data,
|
|
177
|
+
time_column='year',
|
|
178
|
+
post_periods=[2020, 2021, 2022]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# From treatment start date
|
|
182
|
+
df = make_post_indicator(
|
|
183
|
+
data,
|
|
184
|
+
time_column='year',
|
|
185
|
+
treatment_start=2020 # All years >= 2020 are post-treatment
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Works with datetime columns
|
|
189
|
+
df = make_post_indicator(
|
|
190
|
+
data,
|
|
191
|
+
time_column='date',
|
|
192
|
+
treatment_start='2020-01-01'
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Reshape Wide to Long Format
|
|
197
|
+
|
|
198
|
+
Convert wide-format data (one row per unit, multiple time columns) to long format:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from diff_diff import wide_to_long
|
|
202
|
+
|
|
203
|
+
# Wide format: columns like sales_2019, sales_2020, sales_2021
|
|
204
|
+
wide_df = pd.DataFrame({
|
|
205
|
+
'firm_id': [1, 2, 3],
|
|
206
|
+
'industry': ['tech', 'retail', 'tech'],
|
|
207
|
+
'sales_2019': [100, 150, 200],
|
|
208
|
+
'sales_2020': [110, 160, 210],
|
|
209
|
+
'sales_2021': [120, 170, 220]
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
# Convert to long format for DiD
|
|
213
|
+
long_df = wide_to_long(
|
|
214
|
+
wide_df,
|
|
215
|
+
value_columns=['sales_2019', 'sales_2020', 'sales_2021'],
|
|
216
|
+
id_column='firm_id',
|
|
217
|
+
time_name='year',
|
|
218
|
+
value_name='sales',
|
|
219
|
+
time_values=[2019, 2020, 2021]
|
|
220
|
+
)
|
|
221
|
+
# Result: 9 rows (3 firms × 3 years), columns: firm_id, year, sales, industry
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Balance Panel Data
|
|
225
|
+
|
|
226
|
+
Ensure all units have observations for all time periods:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from diff_diff import balance_panel
|
|
230
|
+
|
|
231
|
+
# Keep only units with complete data (drop incomplete units)
|
|
232
|
+
balanced = balance_panel(
|
|
233
|
+
data,
|
|
234
|
+
unit_column='firm_id',
|
|
235
|
+
time_column='year',
|
|
236
|
+
method='inner'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Include all unit-period combinations (creates NaN for missing)
|
|
240
|
+
balanced = balance_panel(
|
|
241
|
+
data,
|
|
242
|
+
unit_column='firm_id',
|
|
243
|
+
time_column='year',
|
|
244
|
+
method='outer'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Fill missing values
|
|
248
|
+
balanced = balance_panel(
|
|
249
|
+
data,
|
|
250
|
+
unit_column='firm_id',
|
|
251
|
+
time_column='year',
|
|
252
|
+
method='fill',
|
|
253
|
+
fill_value=0 # Or None for forward/backward fill
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Validate Data
|
|
258
|
+
|
|
259
|
+
Check that your data meets DiD requirements before fitting:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from diff_diff import validate_did_data
|
|
263
|
+
|
|
264
|
+
# Validate and get informative error messages
|
|
265
|
+
result = validate_did_data(
|
|
266
|
+
data,
|
|
267
|
+
outcome='sales',
|
|
268
|
+
treatment='treated',
|
|
269
|
+
time='post',
|
|
270
|
+
unit='firm_id', # Optional: for panel-specific validation
|
|
271
|
+
raise_on_error=False # Return dict instead of raising
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if result['valid']:
|
|
275
|
+
print("Data is ready for DiD analysis!")
|
|
276
|
+
print(f"Summary: {result['summary']}")
|
|
277
|
+
else:
|
|
278
|
+
print("Issues found:")
|
|
279
|
+
for error in result['errors']:
|
|
280
|
+
print(f" - {error}")
|
|
281
|
+
|
|
282
|
+
for warning in result['warnings']:
|
|
283
|
+
print(f"Warning: {warning}")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Summarize Data by Groups
|
|
287
|
+
|
|
288
|
+
Get summary statistics for each treatment-time cell:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from diff_diff import summarize_did_data
|
|
292
|
+
|
|
293
|
+
summary = summarize_did_data(
|
|
294
|
+
data,
|
|
295
|
+
outcome='sales',
|
|
296
|
+
treatment='treated',
|
|
297
|
+
time='post'
|
|
298
|
+
)
|
|
299
|
+
print(summary)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Output:
|
|
303
|
+
```
|
|
304
|
+
n mean std min max
|
|
305
|
+
Control - Pre 250 100.5000 15.2340 65.0000 145.0000
|
|
306
|
+
Control - Post 250 105.2000 16.1230 68.0000 152.0000
|
|
307
|
+
Treated - Pre 250 101.2000 14.8900 67.0000 143.0000
|
|
308
|
+
Treated - Post 250 115.8000 17.5600 72.0000 165.0000
|
|
309
|
+
DiD Estimate - 9.9000 - - -
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Create Event Time for Staggered Designs
|
|
313
|
+
|
|
314
|
+
For designs where treatment occurs at different times:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from diff_diff import create_event_time
|
|
318
|
+
|
|
319
|
+
# Add event-time column relative to treatment timing
|
|
320
|
+
df = create_event_time(
|
|
321
|
+
data,
|
|
322
|
+
time_column='year',
|
|
323
|
+
treatment_time_column='treatment_year'
|
|
324
|
+
)
|
|
325
|
+
# Result: event_time = -2, -1, 0, 1, 2 relative to treatment
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Aggregate to Cohort Means
|
|
329
|
+
|
|
330
|
+
Aggregate unit-level data for visualization:
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from diff_diff import aggregate_to_cohorts
|
|
334
|
+
|
|
335
|
+
cohort_data = aggregate_to_cohorts(
|
|
336
|
+
data,
|
|
337
|
+
unit_column='firm_id',
|
|
338
|
+
time_column='year',
|
|
339
|
+
treatment_column='treated',
|
|
340
|
+
outcome='sales'
|
|
341
|
+
)
|
|
342
|
+
# Result: mean outcome by treatment group and period
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Usage
|
|
346
|
+
|
|
347
|
+
### Basic DiD with Column Names
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from diff_diff import DifferenceInDifferences
|
|
351
|
+
|
|
352
|
+
did = DifferenceInDifferences(robust=True, alpha=0.05)
|
|
353
|
+
results = did.fit(
|
|
354
|
+
data,
|
|
355
|
+
outcome='sales',
|
|
356
|
+
treatment='treated',
|
|
357
|
+
time='post_policy'
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Access results
|
|
361
|
+
print(f"ATT: {results.att:.4f}")
|
|
362
|
+
print(f"Standard Error: {results.se:.4f}")
|
|
363
|
+
print(f"P-value: {results.p_value:.4f}")
|
|
364
|
+
print(f"95% CI: {results.conf_int}")
|
|
365
|
+
print(f"Significant: {results.is_significant}")
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Using Formula Interface
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
# R-style formula syntax
|
|
372
|
+
results = did.fit(data, formula='outcome ~ treated * post')
|
|
373
|
+
|
|
374
|
+
# Explicit interaction syntax
|
|
375
|
+
results = did.fit(data, formula='outcome ~ treated + post + treated:post')
|
|
376
|
+
|
|
377
|
+
# With covariates
|
|
378
|
+
results = did.fit(data, formula='outcome ~ treated * post + age + income')
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Including Covariates
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
results = did.fit(
|
|
385
|
+
data,
|
|
386
|
+
outcome='outcome',
|
|
387
|
+
treatment='treated',
|
|
388
|
+
time='post',
|
|
389
|
+
covariates=['age', 'income', 'education']
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Fixed Effects
|
|
394
|
+
|
|
395
|
+
Use `fixed_effects` for low-dimensional categorical controls (creates dummy variables):
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
# State and industry fixed effects
|
|
399
|
+
results = did.fit(
|
|
400
|
+
data,
|
|
401
|
+
outcome='sales',
|
|
402
|
+
treatment='treated',
|
|
403
|
+
time='post',
|
|
404
|
+
fixed_effects=['state', 'industry']
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Access fixed effect coefficients
|
|
408
|
+
state_coefs = {k: v for k, v in results.coefficients.items() if k.startswith('state_')}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Use `absorb` for high-dimensional fixed effects (more efficient, uses within-transformation):
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
# Absorb firm-level fixed effects (efficient for many firms)
|
|
415
|
+
results = did.fit(
|
|
416
|
+
data,
|
|
417
|
+
outcome='sales',
|
|
418
|
+
treatment='treated',
|
|
419
|
+
time='post',
|
|
420
|
+
absorb=['firm_id']
|
|
421
|
+
)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Combine covariates with fixed effects:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
results = did.fit(
|
|
428
|
+
data,
|
|
429
|
+
outcome='sales',
|
|
430
|
+
treatment='treated',
|
|
431
|
+
time='post',
|
|
432
|
+
covariates=['size', 'age'], # Linear controls
|
|
433
|
+
fixed_effects=['industry'], # Low-dimensional FE (dummies)
|
|
434
|
+
absorb=['firm_id'] # High-dimensional FE (absorbed)
|
|
435
|
+
)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Cluster-Robust Standard Errors
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
did = DifferenceInDifferences(cluster='state')
|
|
442
|
+
results = did.fit(
|
|
443
|
+
data,
|
|
444
|
+
outcome='outcome',
|
|
445
|
+
treatment='treated',
|
|
446
|
+
time='post'
|
|
447
|
+
)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Two-Way Fixed Effects (Panel Data)
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
from diff_diff.estimators import TwoWayFixedEffects
|
|
454
|
+
|
|
455
|
+
twfe = TwoWayFixedEffects()
|
|
456
|
+
results = twfe.fit(
|
|
457
|
+
panel_data,
|
|
458
|
+
outcome='outcome',
|
|
459
|
+
treatment='treated',
|
|
460
|
+
time='year',
|
|
461
|
+
unit='firm_id'
|
|
462
|
+
)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Multi-Period DiD (Event Study)
|
|
466
|
+
|
|
467
|
+
For settings with multiple pre- and post-treatment periods:
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
from diff_diff import MultiPeriodDiD
|
|
471
|
+
|
|
472
|
+
# Fit with multiple time periods
|
|
473
|
+
did = MultiPeriodDiD()
|
|
474
|
+
results = did.fit(
|
|
475
|
+
panel_data,
|
|
476
|
+
outcome='sales',
|
|
477
|
+
treatment='treated',
|
|
478
|
+
time='period',
|
|
479
|
+
post_periods=[3, 4, 5], # Periods 3-5 are post-treatment
|
|
480
|
+
reference_period=0 # Reference period for comparison
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# View period-specific treatment effects
|
|
484
|
+
for period, effect in results.period_effects.items():
|
|
485
|
+
print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
|
|
486
|
+
|
|
487
|
+
# View average treatment effect across post-periods
|
|
488
|
+
print(f"Average ATT: {results.avg_att:.3f}")
|
|
489
|
+
print(f"Average SE: {results.avg_se:.3f}")
|
|
490
|
+
|
|
491
|
+
# Full summary with all period effects
|
|
492
|
+
results.print_summary()
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Output:
|
|
496
|
+
```
|
|
497
|
+
================================================================================
|
|
498
|
+
Multi-Period Difference-in-Differences Estimation Results
|
|
499
|
+
================================================================================
|
|
500
|
+
|
|
501
|
+
Observations: 600
|
|
502
|
+
Pre-treatment periods: 3
|
|
503
|
+
Post-treatment periods: 3
|
|
504
|
+
|
|
505
|
+
--------------------------------------------------------------------------------
|
|
506
|
+
Average Treatment Effect
|
|
507
|
+
--------------------------------------------------------------------------------
|
|
508
|
+
Average ATT 5.2000 0.8234 6.315 0.0000
|
|
509
|
+
--------------------------------------------------------------------------------
|
|
510
|
+
95% Confidence Interval: [3.5862, 6.8138]
|
|
511
|
+
|
|
512
|
+
Period-Specific Effects:
|
|
513
|
+
--------------------------------------------------------------------------------
|
|
514
|
+
Period Effect Std. Err. t-stat P>|t|
|
|
515
|
+
--------------------------------------------------------------------------------
|
|
516
|
+
3 4.5000 0.9512 4.731 0.0000***
|
|
517
|
+
4 5.2000 0.8876 5.858 0.0000***
|
|
518
|
+
5 5.9000 0.9123 6.468 0.0000***
|
|
519
|
+
--------------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
522
|
+
================================================================================
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Synthetic Difference-in-Differences
|
|
526
|
+
|
|
527
|
+
Synthetic DiD combines the strengths of Difference-in-Differences and Synthetic Control methods by re-weighting control units to better match treated units' pre-treatment outcomes.
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
from diff_diff import SyntheticDiD
|
|
531
|
+
|
|
532
|
+
# Fit Synthetic DiD model
|
|
533
|
+
sdid = SyntheticDiD()
|
|
534
|
+
results = sdid.fit(
|
|
535
|
+
panel_data,
|
|
536
|
+
outcome='gdp_growth',
|
|
537
|
+
treatment='treated',
|
|
538
|
+
unit='state',
|
|
539
|
+
time='year',
|
|
540
|
+
post_periods=[2015, 2016, 2017, 2018]
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# View results
|
|
544
|
+
results.print_summary()
|
|
545
|
+
print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
|
|
546
|
+
|
|
547
|
+
# Examine unit weights (which control units matter most)
|
|
548
|
+
weights_df = results.get_unit_weights_df()
|
|
549
|
+
print(weights_df.head(10))
|
|
550
|
+
|
|
551
|
+
# Examine time weights
|
|
552
|
+
time_weights_df = results.get_time_weights_df()
|
|
553
|
+
print(time_weights_df)
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Output:
|
|
557
|
+
```
|
|
558
|
+
===========================================================================
|
|
559
|
+
Synthetic Difference-in-Differences Estimation Results
|
|
560
|
+
===========================================================================
|
|
561
|
+
|
|
562
|
+
Observations: 500
|
|
563
|
+
Treated units: 1
|
|
564
|
+
Control units: 49
|
|
565
|
+
Pre-treatment periods: 6
|
|
566
|
+
Post-treatment periods: 4
|
|
567
|
+
Regularization (lambda): 0.0000
|
|
568
|
+
Pre-treatment fit (RMSE): 0.1234
|
|
569
|
+
|
|
570
|
+
---------------------------------------------------------------------------
|
|
571
|
+
Parameter Estimate Std. Err. t-stat P>|t|
|
|
572
|
+
---------------------------------------------------------------------------
|
|
573
|
+
ATT 2.5000 0.4521 5.530 0.0000
|
|
574
|
+
---------------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
95% Confidence Interval: [1.6139, 3.3861]
|
|
577
|
+
|
|
578
|
+
---------------------------------------------------------------------------
|
|
579
|
+
Top Unit Weights (Synthetic Control)
|
|
580
|
+
---------------------------------------------------------------------------
|
|
581
|
+
Unit state_12: 0.3521
|
|
582
|
+
Unit state_5: 0.2156
|
|
583
|
+
Unit state_23: 0.1834
|
|
584
|
+
Unit state_8: 0.1245
|
|
585
|
+
Unit state_31: 0.0892
|
|
586
|
+
(8 units with weight > 0.001)
|
|
587
|
+
|
|
588
|
+
Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
|
|
589
|
+
===========================================================================
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### When to Use Synthetic DiD Over Vanilla DiD
|
|
593
|
+
|
|
594
|
+
Use Synthetic DiD instead of standard DiD when:
|
|
595
|
+
|
|
596
|
+
1. **Few treated units**: When you have only one or a small number of treated units (e.g., a single state passed a policy), standard DiD averages across all controls equally. Synthetic DiD finds the optimal weighted combination of controls.
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
# Example: California passed a policy, want to estimate its effect
|
|
600
|
+
# Standard DiD would compare CA to the average of all other states
|
|
601
|
+
# Synthetic DiD finds states that together best match CA's pre-treatment trend
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
2. **Parallel trends is questionable**: When treated and control groups have different pre-treatment levels or trends, Synthetic DiD can construct a better counterfactual by matching the pre-treatment trajectory.
|
|
605
|
+
|
|
606
|
+
```python
|
|
607
|
+
# Example: A tech hub city vs rural areas
|
|
608
|
+
# Rural areas may not be a good comparison on average
|
|
609
|
+
# Synthetic DiD can weight urban/suburban controls more heavily
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
3. **Heterogeneous control units**: When control units are very different from each other, equal weighting (as in standard DiD) is suboptimal.
|
|
613
|
+
|
|
614
|
+
```python
|
|
615
|
+
# Example: Comparing a treated developing country to other countries
|
|
616
|
+
# Some control countries may be much more similar economically
|
|
617
|
+
# Synthetic DiD upweights the most comparable controls
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
4. **You want transparency**: Synthetic DiD provides explicit unit weights showing which controls contribute most to the comparison.
|
|
621
|
+
|
|
622
|
+
```python
|
|
623
|
+
# See exactly which units are driving the counterfactual
|
|
624
|
+
print(results.get_unit_weights_df())
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Key differences from standard DiD:**
|
|
628
|
+
|
|
629
|
+
| Aspect | Standard DiD | Synthetic DiD |
|
|
630
|
+
|--------|--------------|---------------|
|
|
631
|
+
| Control weighting | Equal (1/N) | Optimized to match pre-treatment |
|
|
632
|
+
| Time weighting | Equal across periods | Can emphasize informative periods |
|
|
633
|
+
| N treated required | Can be many | Works with 1 treated unit |
|
|
634
|
+
| Parallel trends | Assumed | Partially relaxed via matching |
|
|
635
|
+
| Interpretability | Simple average | Explicit weights |
|
|
636
|
+
|
|
637
|
+
**Parameters:**
|
|
638
|
+
|
|
639
|
+
```python
|
|
640
|
+
SyntheticDiD(
|
|
641
|
+
lambda_reg=0.0, # Regularization toward uniform weights (0 = no reg)
|
|
642
|
+
zeta=1.0, # Time weight regularization (higher = more uniform)
|
|
643
|
+
alpha=0.05, # Significance level
|
|
644
|
+
n_bootstrap=200, # Bootstrap iterations for SE (0 = placebo-based)
|
|
645
|
+
seed=None # Random seed for reproducibility
|
|
646
|
+
)
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Working with Results
|
|
650
|
+
|
|
651
|
+
### Export Results
|
|
652
|
+
|
|
653
|
+
```python
|
|
654
|
+
# As dictionary
|
|
655
|
+
results.to_dict()
|
|
656
|
+
# {'att': 3.5, 'se': 1.26, 'p_value': 0.037, ...}
|
|
657
|
+
|
|
658
|
+
# As DataFrame
|
|
659
|
+
df = results.to_dataframe()
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Check Significance
|
|
663
|
+
|
|
664
|
+
```python
|
|
665
|
+
if results.is_significant:
|
|
666
|
+
print(f"Effect is significant at {did.alpha} level")
|
|
667
|
+
|
|
668
|
+
# Get significance stars
|
|
669
|
+
print(f"ATT: {results.att}{results.significance_stars}")
|
|
670
|
+
# ATT: 3.5000*
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Access Full Regression Output
|
|
674
|
+
|
|
675
|
+
```python
|
|
676
|
+
# All coefficients
|
|
677
|
+
results.coefficients
|
|
678
|
+
# {'const': 9.5, 'treated': 1.0, 'post': 2.5, 'treated:post': 3.5}
|
|
679
|
+
|
|
680
|
+
# Variance-covariance matrix
|
|
681
|
+
results.vcov
|
|
682
|
+
|
|
683
|
+
# Residuals and fitted values
|
|
684
|
+
results.residuals
|
|
685
|
+
results.fitted_values
|
|
686
|
+
|
|
687
|
+
# R-squared
|
|
688
|
+
results.r_squared
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
## Checking Assumptions
|
|
692
|
+
|
|
693
|
+
### Parallel Trends
|
|
694
|
+
|
|
695
|
+
**Simple slope-based test:**
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
from diff_diff.utils import check_parallel_trends
|
|
699
|
+
|
|
700
|
+
trends = check_parallel_trends(
|
|
701
|
+
data,
|
|
702
|
+
outcome='outcome',
|
|
703
|
+
time='period',
|
|
704
|
+
treatment_group='treated'
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
print(f"Treated trend: {trends['treated_trend']:.4f}")
|
|
708
|
+
print(f"Control trend: {trends['control_trend']:.4f}")
|
|
709
|
+
print(f"Difference p-value: {trends['p_value']:.4f}")
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
**Robust distributional test (Wasserstein distance):**
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
from diff_diff.utils import check_parallel_trends_robust
|
|
716
|
+
|
|
717
|
+
results = check_parallel_trends_robust(
|
|
718
|
+
data,
|
|
719
|
+
outcome='outcome',
|
|
720
|
+
time='period',
|
|
721
|
+
treatment_group='treated',
|
|
722
|
+
unit='firm_id', # Unit identifier for panel data
|
|
723
|
+
pre_periods=[2018, 2019], # Pre-treatment periods
|
|
724
|
+
n_permutations=1000 # Permutations for p-value
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
print(f"Wasserstein distance: {results['wasserstein_distance']:.4f}")
|
|
728
|
+
print(f"Wasserstein p-value: {results['wasserstein_p_value']:.4f}")
|
|
729
|
+
print(f"KS test p-value: {results['ks_p_value']:.4f}")
|
|
730
|
+
print(f"Parallel trends plausible: {results['parallel_trends_plausible']}")
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The Wasserstein (Earth Mover's) distance compares the full distribution of outcome changes, not just means. This is more robust to:
|
|
734
|
+
- Non-normal distributions
|
|
735
|
+
- Heterogeneous effects across units
|
|
736
|
+
- Outliers
|
|
737
|
+
|
|
738
|
+
**Equivalence testing (TOST):**
|
|
739
|
+
|
|
740
|
+
```python
|
|
741
|
+
from diff_diff.utils import equivalence_test_trends
|
|
742
|
+
|
|
743
|
+
results = equivalence_test_trends(
|
|
744
|
+
data,
|
|
745
|
+
outcome='outcome',
|
|
746
|
+
time='period',
|
|
747
|
+
treatment_group='treated',
|
|
748
|
+
unit='firm_id',
|
|
749
|
+
equivalence_margin=0.5 # Define "practically equivalent"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
print(f"Mean difference: {results['mean_difference']:.4f}")
|
|
753
|
+
print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
754
|
+
print(f"Trends equivalent: {results['equivalent']}")
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## API Reference
|
|
758
|
+
|
|
759
|
+
### DifferenceInDifferences
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
DifferenceInDifferences(
|
|
763
|
+
robust=True, # Use HC1 robust standard errors
|
|
764
|
+
cluster=None, # Column for cluster-robust SEs
|
|
765
|
+
alpha=0.05 # Significance level for CIs
|
|
766
|
+
)
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Methods:**
|
|
770
|
+
|
|
771
|
+
| Method | Description |
|
|
772
|
+
|--------|-------------|
|
|
773
|
+
| `fit(data, outcome, treatment, time, ...)` | Fit the DiD model |
|
|
774
|
+
| `summary()` | Get formatted summary string |
|
|
775
|
+
| `print_summary()` | Print summary to stdout |
|
|
776
|
+
| `get_params()` | Get estimator parameters (sklearn-compatible) |
|
|
777
|
+
| `set_params(**params)` | Set estimator parameters (sklearn-compatible) |
|
|
778
|
+
|
|
779
|
+
**fit() Parameters:**
|
|
780
|
+
|
|
781
|
+
| Parameter | Type | Description |
|
|
782
|
+
|-----------|------|-------------|
|
|
783
|
+
| `data` | DataFrame | Input data |
|
|
784
|
+
| `outcome` | str | Outcome variable column name |
|
|
785
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
786
|
+
| `time` | str | Post-treatment indicator column (0/1) |
|
|
787
|
+
| `formula` | str | R-style formula (alternative to column names) |
|
|
788
|
+
| `covariates` | list | Linear control variables |
|
|
789
|
+
| `fixed_effects` | list | Categorical FE columns (creates dummies) |
|
|
790
|
+
| `absorb` | list | High-dimensional FE (within-transformation) |
|
|
791
|
+
|
|
792
|
+
### DiDResults
|
|
793
|
+
|
|
794
|
+
**Attributes:**
|
|
795
|
+
|
|
796
|
+
| Attribute | Description |
|
|
797
|
+
|-----------|-------------|
|
|
798
|
+
| `att` | Average Treatment effect on the Treated |
|
|
799
|
+
| `se` | Standard error of ATT |
|
|
800
|
+
| `t_stat` | T-statistic |
|
|
801
|
+
| `p_value` | P-value for H0: ATT = 0 |
|
|
802
|
+
| `conf_int` | Tuple of (lower, upper) confidence bounds |
|
|
803
|
+
| `n_obs` | Number of observations |
|
|
804
|
+
| `n_treated` | Number of treated units |
|
|
805
|
+
| `n_control` | Number of control units |
|
|
806
|
+
| `r_squared` | R-squared of regression |
|
|
807
|
+
| `coefficients` | Dictionary of all coefficients |
|
|
808
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
809
|
+
| `significance_stars` | String of significance stars |
|
|
810
|
+
|
|
811
|
+
**Methods:**
|
|
812
|
+
|
|
813
|
+
| Method | Description |
|
|
814
|
+
|--------|-------------|
|
|
815
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
816
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
817
|
+
| `to_dict()` | Convert to dictionary |
|
|
818
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
819
|
+
|
|
820
|
+
### MultiPeriodDiD
|
|
821
|
+
|
|
822
|
+
```python
|
|
823
|
+
MultiPeriodDiD(
|
|
824
|
+
robust=True, # Use HC1 robust standard errors
|
|
825
|
+
cluster=None, # Column for cluster-robust SEs
|
|
826
|
+
alpha=0.05 # Significance level for CIs
|
|
827
|
+
)
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**fit() Parameters:**
|
|
831
|
+
|
|
832
|
+
| Parameter | Type | Description |
|
|
833
|
+
|-----------|------|-------------|
|
|
834
|
+
| `data` | DataFrame | Input data |
|
|
835
|
+
| `outcome` | str | Outcome variable column name |
|
|
836
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
837
|
+
| `time` | str | Time period column (multiple values) |
|
|
838
|
+
| `post_periods` | list | List of post-treatment period values |
|
|
839
|
+
| `covariates` | list | Linear control variables |
|
|
840
|
+
| `fixed_effects` | list | Categorical FE columns (creates dummies) |
|
|
841
|
+
| `absorb` | list | High-dimensional FE (within-transformation) |
|
|
842
|
+
| `reference_period` | any | Omitted period for time dummies |
|
|
843
|
+
|
|
844
|
+
### MultiPeriodDiDResults
|
|
845
|
+
|
|
846
|
+
**Attributes:**
|
|
847
|
+
|
|
848
|
+
| Attribute | Description |
|
|
849
|
+
|-----------|-------------|
|
|
850
|
+
| `period_effects` | Dict mapping periods to PeriodEffect objects |
|
|
851
|
+
| `avg_att` | Average ATT across post-treatment periods |
|
|
852
|
+
| `avg_se` | Standard error of average ATT |
|
|
853
|
+
| `avg_t_stat` | T-statistic for average ATT |
|
|
854
|
+
| `avg_p_value` | P-value for average ATT |
|
|
855
|
+
| `avg_conf_int` | Confidence interval for average ATT |
|
|
856
|
+
| `n_obs` | Number of observations |
|
|
857
|
+
| `pre_periods` | List of pre-treatment periods |
|
|
858
|
+
| `post_periods` | List of post-treatment periods |
|
|
859
|
+
|
|
860
|
+
**Methods:**
|
|
861
|
+
|
|
862
|
+
| Method | Description |
|
|
863
|
+
|--------|-------------|
|
|
864
|
+
| `get_effect(period)` | Get PeriodEffect for specific period |
|
|
865
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
866
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
867
|
+
| `to_dict()` | Convert to dictionary |
|
|
868
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
869
|
+
|
|
870
|
+
### PeriodEffect
|
|
871
|
+
|
|
872
|
+
**Attributes:**
|
|
873
|
+
|
|
874
|
+
| Attribute | Description |
|
|
875
|
+
|-----------|-------------|
|
|
876
|
+
| `period` | Time period identifier |
|
|
877
|
+
| `effect` | Treatment effect estimate |
|
|
878
|
+
| `se` | Standard error |
|
|
879
|
+
| `t_stat` | T-statistic |
|
|
880
|
+
| `p_value` | P-value |
|
|
881
|
+
| `conf_int` | Confidence interval |
|
|
882
|
+
| `is_significant` | Boolean for significance at 0.05 |
|
|
883
|
+
| `significance_stars` | String of significance stars |
|
|
884
|
+
|
|
885
|
+
### SyntheticDiD
|
|
886
|
+
|
|
887
|
+
```python
|
|
888
|
+
SyntheticDiD(
|
|
889
|
+
lambda_reg=0.0, # L2 regularization for unit weights
|
|
890
|
+
zeta=1.0, # Regularization for time weights
|
|
891
|
+
alpha=0.05, # Significance level for CIs
|
|
892
|
+
n_bootstrap=200, # Bootstrap iterations for SE
|
|
893
|
+
seed=None # Random seed for reproducibility
|
|
894
|
+
)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**fit() Parameters:**
|
|
898
|
+
|
|
899
|
+
| Parameter | Type | Description |
|
|
900
|
+
|-----------|------|-------------|
|
|
901
|
+
| `data` | DataFrame | Panel data |
|
|
902
|
+
| `outcome` | str | Outcome variable column name |
|
|
903
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
904
|
+
| `unit` | str | Unit identifier column |
|
|
905
|
+
| `time` | str | Time period column |
|
|
906
|
+
| `post_periods` | list | List of post-treatment period values |
|
|
907
|
+
| `covariates` | list | Covariates to residualize out |
|
|
908
|
+
|
|
909
|
+
### SyntheticDiDResults
|
|
910
|
+
|
|
911
|
+
**Attributes:**
|
|
912
|
+
|
|
913
|
+
| Attribute | Description |
|
|
914
|
+
|-----------|-------------|
|
|
915
|
+
| `att` | Average Treatment effect on the Treated |
|
|
916
|
+
| `se` | Standard error (bootstrap or placebo-based) |
|
|
917
|
+
| `t_stat` | T-statistic |
|
|
918
|
+
| `p_value` | P-value |
|
|
919
|
+
| `conf_int` | Confidence interval |
|
|
920
|
+
| `n_obs` | Number of observations |
|
|
921
|
+
| `n_treated` | Number of treated units |
|
|
922
|
+
| `n_control` | Number of control units |
|
|
923
|
+
| `unit_weights` | Dict mapping control unit IDs to weights |
|
|
924
|
+
| `time_weights` | Dict mapping pre-treatment periods to weights |
|
|
925
|
+
| `pre_periods` | List of pre-treatment periods |
|
|
926
|
+
| `post_periods` | List of post-treatment periods |
|
|
927
|
+
| `pre_treatment_fit` | RMSE of synthetic vs treated in pre-period |
|
|
928
|
+
| `placebo_effects` | Array of placebo effect estimates |
|
|
929
|
+
|
|
930
|
+
**Methods:**
|
|
931
|
+
|
|
932
|
+
| Method | Description |
|
|
933
|
+
|--------|-------------|
|
|
934
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
935
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
936
|
+
| `to_dict()` | Convert to dictionary |
|
|
937
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
938
|
+
| `get_unit_weights_df()` | Get unit weights as DataFrame |
|
|
939
|
+
| `get_time_weights_df()` | Get time weights as DataFrame |
|
|
940
|
+
|
|
941
|
+
### Data Preparation Functions
|
|
942
|
+
|
|
943
|
+
#### generate_did_data
|
|
944
|
+
|
|
945
|
+
```python
|
|
946
|
+
generate_did_data(
|
|
947
|
+
n_units=100, # Number of units
|
|
948
|
+
n_periods=4, # Number of time periods
|
|
949
|
+
treatment_effect=5.0, # True ATT
|
|
950
|
+
treatment_fraction=0.5, # Fraction treated
|
|
951
|
+
treatment_period=2, # First post-treatment period
|
|
952
|
+
unit_fe_sd=2.0, # Unit fixed effect std dev
|
|
953
|
+
time_trend=0.5, # Linear time trend
|
|
954
|
+
noise_sd=1.0, # Idiosyncratic noise std dev
|
|
955
|
+
seed=None # Random seed
|
|
956
|
+
)
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
Returns DataFrame with columns: `unit`, `period`, `treated`, `post`, `outcome`, `true_effect`.
|
|
960
|
+
|
|
961
|
+
#### make_treatment_indicator
|
|
962
|
+
|
|
963
|
+
```python
|
|
964
|
+
make_treatment_indicator(
|
|
965
|
+
data, # Input DataFrame
|
|
966
|
+
column, # Column to create treatment from
|
|
967
|
+
treated_values=None, # Value(s) indicating treatment
|
|
968
|
+
threshold=None, # Numeric threshold for treatment
|
|
969
|
+
above_threshold=True, # If True, >= threshold is treated
|
|
970
|
+
new_column='treated' # Output column name
|
|
971
|
+
)
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
#### make_post_indicator
|
|
975
|
+
|
|
976
|
+
```python
|
|
977
|
+
make_post_indicator(
|
|
978
|
+
data, # Input DataFrame
|
|
979
|
+
time_column, # Time/period column
|
|
980
|
+
post_periods=None, # Specific post-treatment period(s)
|
|
981
|
+
treatment_start=None, # First post-treatment period
|
|
982
|
+
new_column='post' # Output column name
|
|
983
|
+
)
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
#### wide_to_long
|
|
987
|
+
|
|
988
|
+
```python
|
|
989
|
+
wide_to_long(
|
|
990
|
+
data, # Wide-format DataFrame
|
|
991
|
+
value_columns, # List of time-varying columns
|
|
992
|
+
id_column, # Unit identifier column
|
|
993
|
+
time_name='period', # Name for time column
|
|
994
|
+
value_name='value', # Name for value column
|
|
995
|
+
time_values=None # Values for time periods
|
|
996
|
+
)
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### balance_panel
|
|
1000
|
+
|
|
1001
|
+
```python
|
|
1002
|
+
balance_panel(
|
|
1003
|
+
data, # Panel DataFrame
|
|
1004
|
+
unit_column, # Unit identifier column
|
|
1005
|
+
time_column, # Time period column
|
|
1006
|
+
method='inner', # 'inner', 'outer', or 'fill'
|
|
1007
|
+
fill_value=None # Value for filling (if method='fill')
|
|
1008
|
+
)
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
#### validate_did_data
|
|
1012
|
+
|
|
1013
|
+
```python
|
|
1014
|
+
validate_did_data(
|
|
1015
|
+
data, # DataFrame to validate
|
|
1016
|
+
outcome, # Outcome column name
|
|
1017
|
+
treatment, # Treatment column name
|
|
1018
|
+
time, # Time/post column name
|
|
1019
|
+
unit=None, # Unit column (for panel validation)
|
|
1020
|
+
raise_on_error=True # Raise ValueError or return dict
|
|
1021
|
+
)
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
Returns dict with `valid`, `errors`, `warnings`, and `summary` keys.
|
|
1025
|
+
|
|
1026
|
+
#### summarize_did_data
|
|
1027
|
+
|
|
1028
|
+
```python
|
|
1029
|
+
summarize_did_data(
|
|
1030
|
+
data, # Input DataFrame
|
|
1031
|
+
outcome, # Outcome column name
|
|
1032
|
+
treatment, # Treatment column name
|
|
1033
|
+
time, # Time/post column name
|
|
1034
|
+
unit=None # Unit column (optional)
|
|
1035
|
+
)
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Returns DataFrame with summary statistics by treatment-time cell.
|
|
1039
|
+
|
|
1040
|
+
#### create_event_time
|
|
1041
|
+
|
|
1042
|
+
```python
|
|
1043
|
+
create_event_time(
|
|
1044
|
+
data, # Panel DataFrame
|
|
1045
|
+
time_column, # Calendar time column
|
|
1046
|
+
treatment_time_column, # Column with treatment timing
|
|
1047
|
+
new_column='event_time' # Output column name
|
|
1048
|
+
)
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
#### aggregate_to_cohorts
|
|
1052
|
+
|
|
1053
|
+
```python
|
|
1054
|
+
aggregate_to_cohorts(
|
|
1055
|
+
data, # Unit-level panel data
|
|
1056
|
+
unit_column, # Unit identifier column
|
|
1057
|
+
time_column, # Time period column
|
|
1058
|
+
treatment_column, # Treatment indicator column
|
|
1059
|
+
outcome, # Outcome variable column
|
|
1060
|
+
covariates=None # Additional columns to aggregate
|
|
1061
|
+
)
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## Requirements
|
|
1065
|
+
|
|
1066
|
+
- Python >= 3.9
|
|
1067
|
+
- numpy >= 1.20
|
|
1068
|
+
- pandas >= 1.3
|
|
1069
|
+
- scipy >= 1.7
|
|
1070
|
+
|
|
1071
|
+
## Development
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
# Install with dev dependencies
|
|
1075
|
+
pip install -e ".[dev]"
|
|
1076
|
+
|
|
1077
|
+
# Run tests
|
|
1078
|
+
pytest
|
|
1079
|
+
|
|
1080
|
+
# Format code
|
|
1081
|
+
black diff_diff tests
|
|
1082
|
+
ruff check diff_diff tests
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
## References
|
|
1086
|
+
|
|
1087
|
+
This library implements methods from the following scholarly works:
|
|
1088
|
+
|
|
1089
|
+
### Difference-in-Differences
|
|
1090
|
+
|
|
1091
|
+
- **Ashenfelter, O., & Card, D. (1985).** "Using the Longitudinal Structure of Earnings to Estimate the Effect of Training Programs." *The Review of Economics and Statistics*, 67(4), 648-660. [https://doi.org/10.2307/1924810](https://doi.org/10.2307/1924810)
|
|
1092
|
+
|
|
1093
|
+
- **Card, D., & Krueger, A. B. (1994).** "Minimum Wages and Employment: A Case Study of the Fast-Food Industry in New Jersey and Pennsylvania." *The American Economic Review*, 84(4), 772-793. [https://www.jstor.org/stable/2118030](https://www.jstor.org/stable/2118030)
|
|
1094
|
+
|
|
1095
|
+
- **Angrist, J. D., & Pischke, J.-S. (2009).** *Mostly Harmless Econometrics: An Empiricist's Companion*. Princeton University Press. Chapter 5: Differences-in-Differences.
|
|
1096
|
+
|
|
1097
|
+
### Two-Way Fixed Effects
|
|
1098
|
+
|
|
1099
|
+
- **Wooldridge, J. M. (2010).** *Econometric Analysis of Cross Section and Panel Data* (2nd ed.). MIT Press.
|
|
1100
|
+
|
|
1101
|
+
- **Imai, K., & Kim, I. S. (2021).** "On the Use of Two-Way Fixed Effects Regression Models for Causal Inference with Panel Data." *Political Analysis*, 29(3), 405-415. [https://doi.org/10.1017/pan.2020.33](https://doi.org/10.1017/pan.2020.33)
|
|
1102
|
+
|
|
1103
|
+
### Robust Standard Errors
|
|
1104
|
+
|
|
1105
|
+
- **White, H. (1980).** "A Heteroskedasticity-Consistent Covariance Matrix Estimator and a Direct Test for Heteroskedasticity." *Econometrica*, 48(4), 817-838. [https://doi.org/10.2307/1912934](https://doi.org/10.2307/1912934)
|
|
1106
|
+
|
|
1107
|
+
- **MacKinnon, J. G., & White, H. (1985).** "Some Heteroskedasticity-Consistent Covariance Matrix Estimators with Improved Finite Sample Properties." *Journal of Econometrics*, 29(3), 305-325. [https://doi.org/10.1016/0304-4076(85)90158-7](https://doi.org/10.1016/0304-4076(85)90158-7)
|
|
1108
|
+
|
|
1109
|
+
- **Cameron, A. C., Gelbach, J. B., & Miller, D. L. (2011).** "Robust Inference With Multiway Clustering." *Journal of Business & Economic Statistics*, 29(2), 238-249. [https://doi.org/10.1198/jbes.2010.07136](https://doi.org/10.1198/jbes.2010.07136)
|
|
1110
|
+
|
|
1111
|
+
### Synthetic Control Method
|
|
1112
|
+
|
|
1113
|
+
- **Abadie, A., & Gardeazabal, J. (2003).** "The Economic Costs of Conflict: A Case Study of the Basque Country." *The American Economic Review*, 93(1), 113-132. [https://doi.org/10.1257/000282803321455188](https://doi.org/10.1257/000282803321455188)
|
|
1114
|
+
|
|
1115
|
+
- **Abadie, A., Diamond, A., & Hainmueller, J. (2010).** "Synthetic Control Methods for Comparative Case Studies: Estimating the Effect of California's Tobacco Control Program." *Journal of the American Statistical Association*, 105(490), 493-505. [https://doi.org/10.1198/jasa.2009.ap08746](https://doi.org/10.1198/jasa.2009.ap08746)
|
|
1116
|
+
|
|
1117
|
+
- **Abadie, A., Diamond, A., & Hainmueller, J. (2015).** "Comparative Politics and the Synthetic Control Method." *American Journal of Political Science*, 59(2), 495-510. [https://doi.org/10.1111/ajps.12116](https://doi.org/10.1111/ajps.12116)
|
|
1118
|
+
|
|
1119
|
+
### Synthetic Difference-in-Differences
|
|
1120
|
+
|
|
1121
|
+
- **Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021).** "Synthetic Difference-in-Differences." *American Economic Review*, 111(12), 4088-4118. [https://doi.org/10.1257/aer.20190159](https://doi.org/10.1257/aer.20190159)
|
|
1122
|
+
|
|
1123
|
+
### Parallel Trends and Pre-Trend Testing
|
|
1124
|
+
|
|
1125
|
+
- **Roth, J. (2022).** "Pretest with Caution: Event-Study Estimates after Testing for Parallel Trends." *American Economic Review: Insights*, 4(3), 305-322. [https://doi.org/10.1257/aeri.20210236](https://doi.org/10.1257/aeri.20210236)
|
|
1126
|
+
|
|
1127
|
+
- **Rambachan, A., & Roth, J. (2023).** "A More Credible Approach to Parallel Trends." *The Review of Economic Studies*, 90(5), 2555-2591. [https://doi.org/10.1093/restud/rdad018](https://doi.org/10.1093/restud/rdad018)
|
|
1128
|
+
|
|
1129
|
+
### Multi-Period and Staggered Adoption
|
|
1130
|
+
|
|
1131
|
+
- **Callaway, B., & Sant'Anna, P. H. C. (2021).** "Difference-in-Differences with Multiple Time Periods." *Journal of Econometrics*, 225(2), 200-230. [https://doi.org/10.1016/j.jeconom.2020.12.001](https://doi.org/10.1016/j.jeconom.2020.12.001)
|
|
1132
|
+
|
|
1133
|
+
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. [https://doi.org/10.1016/j.jeconom.2020.09.006](https://doi.org/10.1016/j.jeconom.2020.09.006)
|
|
1134
|
+
|
|
1135
|
+
- **de Chaisemartin, C., & D'Haultfœuille, X. (2020).** "Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects." *American Economic Review*, 110(9), 2964-2996. [https://doi.org/10.1257/aer.20181169](https://doi.org/10.1257/aer.20181169)
|
|
1136
|
+
|
|
1137
|
+
### General Causal Inference
|
|
1138
|
+
|
|
1139
|
+
- **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
|
|
1140
|
+
|
|
1141
|
+
- **Cunningham, S. (2021).** *Causal Inference: The Mixtape*. Yale University Press. [https://mixtape.scunning.com/](https://mixtape.scunning.com/)
|
|
1142
|
+
|
|
1143
|
+
## License
|
|
1144
|
+
|
|
1145
|
+
MIT License
|