diff-diff 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diff_diff-0.1.0/PKG-INFO +421 -0
- diff_diff-0.1.0/README.md +386 -0
- diff_diff-0.1.0/diff_diff/__init__.py +12 -0
- diff_diff-0.1.0/diff_diff/estimators.py +701 -0
- diff_diff-0.1.0/diff_diff/results.py +170 -0
- diff_diff-0.1.0/diff_diff/utils.py +581 -0
- diff_diff-0.1.0/diff_diff.egg-info/PKG-INFO +421 -0
- diff_diff-0.1.0/diff_diff.egg-info/SOURCES.txt +12 -0
- diff_diff-0.1.0/diff_diff.egg-info/dependency_links.txt +1 -0
- diff_diff-0.1.0/diff_diff.egg-info/requires.txt +14 -0
- diff_diff-0.1.0/diff_diff.egg-info/top_level.txt +1 -0
- diff_diff-0.1.0/pyproject.toml +80 -0
- diff_diff-0.1.0/setup.cfg +4 -0
- diff_diff-0.1.0/tests/test_estimators.py +690 -0
diff_diff-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diff-diff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library for Difference-in-Differences causal inference analysis
|
|
5
|
+
Author: diff-diff contributors
|
|
6
|
+
License: 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
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
|
|
109
|
+
### Basic DiD with Column Names
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from diff_diff import DifferenceInDifferences
|
|
113
|
+
|
|
114
|
+
did = DifferenceInDifferences(robust=True, alpha=0.05)
|
|
115
|
+
results = did.fit(
|
|
116
|
+
data,
|
|
117
|
+
outcome='sales',
|
|
118
|
+
treatment='treated',
|
|
119
|
+
time='post_policy'
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Access results
|
|
123
|
+
print(f"ATT: {results.att:.4f}")
|
|
124
|
+
print(f"Standard Error: {results.se:.4f}")
|
|
125
|
+
print(f"P-value: {results.p_value:.4f}")
|
|
126
|
+
print(f"95% CI: {results.conf_int}")
|
|
127
|
+
print(f"Significant: {results.is_significant}")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Using Formula Interface
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# R-style formula syntax
|
|
134
|
+
results = did.fit(data, formula='outcome ~ treated * post')
|
|
135
|
+
|
|
136
|
+
# Explicit interaction syntax
|
|
137
|
+
results = did.fit(data, formula='outcome ~ treated + post + treated:post')
|
|
138
|
+
|
|
139
|
+
# With covariates
|
|
140
|
+
results = did.fit(data, formula='outcome ~ treated * post + age + income')
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Including Covariates
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
results = did.fit(
|
|
147
|
+
data,
|
|
148
|
+
outcome='outcome',
|
|
149
|
+
treatment='treated',
|
|
150
|
+
time='post',
|
|
151
|
+
covariates=['age', 'income', 'education']
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Fixed Effects
|
|
156
|
+
|
|
157
|
+
Use `fixed_effects` for low-dimensional categorical controls (creates dummy variables):
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# State and industry fixed effects
|
|
161
|
+
results = did.fit(
|
|
162
|
+
data,
|
|
163
|
+
outcome='sales',
|
|
164
|
+
treatment='treated',
|
|
165
|
+
time='post',
|
|
166
|
+
fixed_effects=['state', 'industry']
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Access fixed effect coefficients
|
|
170
|
+
state_coefs = {k: v for k, v in results.coefficients.items() if k.startswith('state_')}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use `absorb` for high-dimensional fixed effects (more efficient, uses within-transformation):
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Absorb firm-level fixed effects (efficient for many firms)
|
|
177
|
+
results = did.fit(
|
|
178
|
+
data,
|
|
179
|
+
outcome='sales',
|
|
180
|
+
treatment='treated',
|
|
181
|
+
time='post',
|
|
182
|
+
absorb=['firm_id']
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Combine covariates with fixed effects:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
results = did.fit(
|
|
190
|
+
data,
|
|
191
|
+
outcome='sales',
|
|
192
|
+
treatment='treated',
|
|
193
|
+
time='post',
|
|
194
|
+
covariates=['size', 'age'], # Linear controls
|
|
195
|
+
fixed_effects=['industry'], # Low-dimensional FE (dummies)
|
|
196
|
+
absorb=['firm_id'] # High-dimensional FE (absorbed)
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Cluster-Robust Standard Errors
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
did = DifferenceInDifferences(cluster='state')
|
|
204
|
+
results = did.fit(
|
|
205
|
+
data,
|
|
206
|
+
outcome='outcome',
|
|
207
|
+
treatment='treated',
|
|
208
|
+
time='post'
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Two-Way Fixed Effects (Panel Data)
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from diff_diff.estimators import TwoWayFixedEffects
|
|
216
|
+
|
|
217
|
+
twfe = TwoWayFixedEffects()
|
|
218
|
+
results = twfe.fit(
|
|
219
|
+
panel_data,
|
|
220
|
+
outcome='outcome',
|
|
221
|
+
treatment='treated',
|
|
222
|
+
time='year',
|
|
223
|
+
unit='firm_id'
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Working with Results
|
|
228
|
+
|
|
229
|
+
### Export Results
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# As dictionary
|
|
233
|
+
results.to_dict()
|
|
234
|
+
# {'att': 3.5, 'se': 1.26, 'p_value': 0.037, ...}
|
|
235
|
+
|
|
236
|
+
# As DataFrame
|
|
237
|
+
df = results.to_dataframe()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Check Significance
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
if results.is_significant:
|
|
244
|
+
print(f"Effect is significant at {did.alpha} level")
|
|
245
|
+
|
|
246
|
+
# Get significance stars
|
|
247
|
+
print(f"ATT: {results.att}{results.significance_stars}")
|
|
248
|
+
# ATT: 3.5000*
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Access Full Regression Output
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# All coefficients
|
|
255
|
+
results.coefficients
|
|
256
|
+
# {'const': 9.5, 'treated': 1.0, 'post': 2.5, 'treated:post': 3.5}
|
|
257
|
+
|
|
258
|
+
# Variance-covariance matrix
|
|
259
|
+
results.vcov
|
|
260
|
+
|
|
261
|
+
# Residuals and fitted values
|
|
262
|
+
results.residuals
|
|
263
|
+
results.fitted_values
|
|
264
|
+
|
|
265
|
+
# R-squared
|
|
266
|
+
results.r_squared
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Checking Assumptions
|
|
270
|
+
|
|
271
|
+
### Parallel Trends
|
|
272
|
+
|
|
273
|
+
**Simple slope-based test:**
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from diff_diff.utils import check_parallel_trends
|
|
277
|
+
|
|
278
|
+
trends = check_parallel_trends(
|
|
279
|
+
data,
|
|
280
|
+
outcome='outcome',
|
|
281
|
+
time='period',
|
|
282
|
+
treatment_group='treated'
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
print(f"Treated trend: {trends['treated_trend']:.4f}")
|
|
286
|
+
print(f"Control trend: {trends['control_trend']:.4f}")
|
|
287
|
+
print(f"Difference p-value: {trends['p_value']:.4f}")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Robust distributional test (Wasserstein distance):**
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from diff_diff.utils import check_parallel_trends_robust
|
|
294
|
+
|
|
295
|
+
results = check_parallel_trends_robust(
|
|
296
|
+
data,
|
|
297
|
+
outcome='outcome',
|
|
298
|
+
time='period',
|
|
299
|
+
treatment_group='treated',
|
|
300
|
+
unit='firm_id', # Unit identifier for panel data
|
|
301
|
+
pre_periods=[2018, 2019], # Pre-treatment periods
|
|
302
|
+
n_permutations=1000 # Permutations for p-value
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
print(f"Wasserstein distance: {results['wasserstein_distance']:.4f}")
|
|
306
|
+
print(f"Wasserstein p-value: {results['wasserstein_p_value']:.4f}")
|
|
307
|
+
print(f"KS test p-value: {results['ks_p_value']:.4f}")
|
|
308
|
+
print(f"Parallel trends plausible: {results['parallel_trends_plausible']}")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The Wasserstein (Earth Mover's) distance compares the full distribution of outcome changes, not just means. This is more robust to:
|
|
312
|
+
- Non-normal distributions
|
|
313
|
+
- Heterogeneous effects across units
|
|
314
|
+
- Outliers
|
|
315
|
+
|
|
316
|
+
**Equivalence testing (TOST):**
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from diff_diff.utils import equivalence_test_trends
|
|
320
|
+
|
|
321
|
+
results = equivalence_test_trends(
|
|
322
|
+
data,
|
|
323
|
+
outcome='outcome',
|
|
324
|
+
time='period',
|
|
325
|
+
treatment_group='treated',
|
|
326
|
+
unit='firm_id',
|
|
327
|
+
equivalence_margin=0.5 # Define "practically equivalent"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
print(f"Mean difference: {results['mean_difference']:.4f}")
|
|
331
|
+
print(f"TOST p-value: {results['tost_p_value']:.4f}")
|
|
332
|
+
print(f"Trends equivalent: {results['equivalent']}")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## API Reference
|
|
336
|
+
|
|
337
|
+
### DifferenceInDifferences
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
DifferenceInDifferences(
|
|
341
|
+
robust=True, # Use HC1 robust standard errors
|
|
342
|
+
cluster=None, # Column for cluster-robust SEs
|
|
343
|
+
alpha=0.05 # Significance level for CIs
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Methods:**
|
|
348
|
+
|
|
349
|
+
| Method | Description |
|
|
350
|
+
|--------|-------------|
|
|
351
|
+
| `fit(data, outcome, treatment, time, ...)` | Fit the DiD model |
|
|
352
|
+
| `summary()` | Get formatted summary string |
|
|
353
|
+
| `print_summary()` | Print summary to stdout |
|
|
354
|
+
| `get_params()` | Get estimator parameters (sklearn-compatible) |
|
|
355
|
+
| `set_params(**params)` | Set estimator parameters (sklearn-compatible) |
|
|
356
|
+
|
|
357
|
+
**fit() Parameters:**
|
|
358
|
+
|
|
359
|
+
| Parameter | Type | Description |
|
|
360
|
+
|-----------|------|-------------|
|
|
361
|
+
| `data` | DataFrame | Input data |
|
|
362
|
+
| `outcome` | str | Outcome variable column name |
|
|
363
|
+
| `treatment` | str | Treatment indicator column (0/1) |
|
|
364
|
+
| `time` | str | Post-treatment indicator column (0/1) |
|
|
365
|
+
| `formula` | str | R-style formula (alternative to column names) |
|
|
366
|
+
| `covariates` | list | Linear control variables |
|
|
367
|
+
| `fixed_effects` | list | Categorical FE columns (creates dummies) |
|
|
368
|
+
| `absorb` | list | High-dimensional FE (within-transformation) |
|
|
369
|
+
|
|
370
|
+
### DiDResults
|
|
371
|
+
|
|
372
|
+
**Attributes:**
|
|
373
|
+
|
|
374
|
+
| Attribute | Description |
|
|
375
|
+
|-----------|-------------|
|
|
376
|
+
| `att` | Average Treatment effect on the Treated |
|
|
377
|
+
| `se` | Standard error of ATT |
|
|
378
|
+
| `t_stat` | T-statistic |
|
|
379
|
+
| `p_value` | P-value for H0: ATT = 0 |
|
|
380
|
+
| `conf_int` | Tuple of (lower, upper) confidence bounds |
|
|
381
|
+
| `n_obs` | Number of observations |
|
|
382
|
+
| `n_treated` | Number of treated units |
|
|
383
|
+
| `n_control` | Number of control units |
|
|
384
|
+
| `r_squared` | R-squared of regression |
|
|
385
|
+
| `coefficients` | Dictionary of all coefficients |
|
|
386
|
+
| `is_significant` | Boolean for significance at alpha |
|
|
387
|
+
| `significance_stars` | String of significance stars |
|
|
388
|
+
|
|
389
|
+
**Methods:**
|
|
390
|
+
|
|
391
|
+
| Method | Description |
|
|
392
|
+
|--------|-------------|
|
|
393
|
+
| `summary(alpha)` | Get formatted summary string |
|
|
394
|
+
| `print_summary(alpha)` | Print summary to stdout |
|
|
395
|
+
| `to_dict()` | Convert to dictionary |
|
|
396
|
+
| `to_dataframe()` | Convert to pandas DataFrame |
|
|
397
|
+
|
|
398
|
+
## Requirements
|
|
399
|
+
|
|
400
|
+
- Python >= 3.9
|
|
401
|
+
- numpy >= 1.20
|
|
402
|
+
- pandas >= 1.3
|
|
403
|
+
- scipy >= 1.7
|
|
404
|
+
|
|
405
|
+
## Development
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# Install with dev dependencies
|
|
409
|
+
pip install -e ".[dev]"
|
|
410
|
+
|
|
411
|
+
# Run tests
|
|
412
|
+
pytest
|
|
413
|
+
|
|
414
|
+
# Format code
|
|
415
|
+
black diff_diff tests
|
|
416
|
+
ruff check diff_diff tests
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT License
|