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.
@@ -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