diff-diff 0.2.0__tar.gz → 0.4.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,1394 @@
1
+ Metadata-Version: 2.4
2
+ Name: diff-diff
3
+ Version: 0.4.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
+ - **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
108
+ - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
109
+ - **Event study plots**: Publication-ready visualization of treatment effects
110
+ - **Parallel trends testing**: Multiple methods including equivalence tests
111
+ - **Data prep utilities**: Helper functions for common data preparation tasks
112
+
113
+ ## Data Preparation
114
+
115
+ 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.
116
+
117
+ ### Generate Sample Data
118
+
119
+ Create synthetic data with a known treatment effect for testing and learning:
120
+
121
+ ```python
122
+ from diff_diff import generate_did_data, DifferenceInDifferences
123
+
124
+ # Generate panel data with 100 units, 4 periods, and a treatment effect of 5
125
+ data = generate_did_data(
126
+ n_units=100,
127
+ n_periods=4,
128
+ treatment_effect=5.0,
129
+ treatment_fraction=0.5, # 50% of units are treated
130
+ treatment_period=2, # Treatment starts at period 2
131
+ seed=42
132
+ )
133
+
134
+ # Verify the estimator recovers the treatment effect
135
+ did = DifferenceInDifferences()
136
+ results = did.fit(data, outcome='outcome', treatment='treated', time='post')
137
+ print(f"Estimated ATT: {results.att:.2f} (true: 5.0)")
138
+ ```
139
+
140
+ ### Create Treatment Indicators
141
+
142
+ Convert categorical variables or numeric thresholds to binary treatment indicators:
143
+
144
+ ```python
145
+ from diff_diff import make_treatment_indicator
146
+
147
+ # From categorical variable
148
+ df = make_treatment_indicator(
149
+ data,
150
+ column='state',
151
+ treated_values=['CA', 'NY', 'TX'] # These states are treated
152
+ )
153
+
154
+ # From numeric threshold (e.g., firms above median size)
155
+ df = make_treatment_indicator(
156
+ data,
157
+ column='firm_size',
158
+ threshold=data['firm_size'].median()
159
+ )
160
+
161
+ # Treat units below threshold
162
+ df = make_treatment_indicator(
163
+ data,
164
+ column='income',
165
+ threshold=50000,
166
+ above_threshold=False # Units with income <= 50000 are treated
167
+ )
168
+ ```
169
+
170
+ ### Create Post-Treatment Indicators
171
+
172
+ Convert time/date columns to binary post-treatment indicators:
173
+
174
+ ```python
175
+ from diff_diff import make_post_indicator
176
+
177
+ # From specific post-treatment periods
178
+ df = make_post_indicator(
179
+ data,
180
+ time_column='year',
181
+ post_periods=[2020, 2021, 2022]
182
+ )
183
+
184
+ # From treatment start date
185
+ df = make_post_indicator(
186
+ data,
187
+ time_column='year',
188
+ treatment_start=2020 # All years >= 2020 are post-treatment
189
+ )
190
+
191
+ # Works with datetime columns
192
+ df = make_post_indicator(
193
+ data,
194
+ time_column='date',
195
+ treatment_start='2020-01-01'
196
+ )
197
+ ```
198
+
199
+ ### Reshape Wide to Long Format
200
+
201
+ Convert wide-format data (one row per unit, multiple time columns) to long format:
202
+
203
+ ```python
204
+ from diff_diff import wide_to_long
205
+
206
+ # Wide format: columns like sales_2019, sales_2020, sales_2021
207
+ wide_df = pd.DataFrame({
208
+ 'firm_id': [1, 2, 3],
209
+ 'industry': ['tech', 'retail', 'tech'],
210
+ 'sales_2019': [100, 150, 200],
211
+ 'sales_2020': [110, 160, 210],
212
+ 'sales_2021': [120, 170, 220]
213
+ })
214
+
215
+ # Convert to long format for DiD
216
+ long_df = wide_to_long(
217
+ wide_df,
218
+ value_columns=['sales_2019', 'sales_2020', 'sales_2021'],
219
+ id_column='firm_id',
220
+ time_name='year',
221
+ value_name='sales',
222
+ time_values=[2019, 2020, 2021]
223
+ )
224
+ # Result: 9 rows (3 firms × 3 years), columns: firm_id, year, sales, industry
225
+ ```
226
+
227
+ ### Balance Panel Data
228
+
229
+ Ensure all units have observations for all time periods:
230
+
231
+ ```python
232
+ from diff_diff import balance_panel
233
+
234
+ # Keep only units with complete data (drop incomplete units)
235
+ balanced = balance_panel(
236
+ data,
237
+ unit_column='firm_id',
238
+ time_column='year',
239
+ method='inner'
240
+ )
241
+
242
+ # Include all unit-period combinations (creates NaN for missing)
243
+ balanced = balance_panel(
244
+ data,
245
+ unit_column='firm_id',
246
+ time_column='year',
247
+ method='outer'
248
+ )
249
+
250
+ # Fill missing values
251
+ balanced = balance_panel(
252
+ data,
253
+ unit_column='firm_id',
254
+ time_column='year',
255
+ method='fill',
256
+ fill_value=0 # Or None for forward/backward fill
257
+ )
258
+ ```
259
+
260
+ ### Validate Data
261
+
262
+ Check that your data meets DiD requirements before fitting:
263
+
264
+ ```python
265
+ from diff_diff import validate_did_data
266
+
267
+ # Validate and get informative error messages
268
+ result = validate_did_data(
269
+ data,
270
+ outcome='sales',
271
+ treatment='treated',
272
+ time='post',
273
+ unit='firm_id', # Optional: for panel-specific validation
274
+ raise_on_error=False # Return dict instead of raising
275
+ )
276
+
277
+ if result['valid']:
278
+ print("Data is ready for DiD analysis!")
279
+ print(f"Summary: {result['summary']}")
280
+ else:
281
+ print("Issues found:")
282
+ for error in result['errors']:
283
+ print(f" - {error}")
284
+
285
+ for warning in result['warnings']:
286
+ print(f"Warning: {warning}")
287
+ ```
288
+
289
+ ### Summarize Data by Groups
290
+
291
+ Get summary statistics for each treatment-time cell:
292
+
293
+ ```python
294
+ from diff_diff import summarize_did_data
295
+
296
+ summary = summarize_did_data(
297
+ data,
298
+ outcome='sales',
299
+ treatment='treated',
300
+ time='post'
301
+ )
302
+ print(summary)
303
+ ```
304
+
305
+ Output:
306
+ ```
307
+ n mean std min max
308
+ Control - Pre 250 100.5000 15.2340 65.0000 145.0000
309
+ Control - Post 250 105.2000 16.1230 68.0000 152.0000
310
+ Treated - Pre 250 101.2000 14.8900 67.0000 143.0000
311
+ Treated - Post 250 115.8000 17.5600 72.0000 165.0000
312
+ DiD Estimate - 9.9000 - - -
313
+ ```
314
+
315
+ ### Create Event Time for Staggered Designs
316
+
317
+ For designs where treatment occurs at different times:
318
+
319
+ ```python
320
+ from diff_diff import create_event_time
321
+
322
+ # Add event-time column relative to treatment timing
323
+ df = create_event_time(
324
+ data,
325
+ time_column='year',
326
+ treatment_time_column='treatment_year'
327
+ )
328
+ # Result: event_time = -2, -1, 0, 1, 2 relative to treatment
329
+ ```
330
+
331
+ ### Aggregate to Cohort Means
332
+
333
+ Aggregate unit-level data for visualization:
334
+
335
+ ```python
336
+ from diff_diff import aggregate_to_cohorts
337
+
338
+ cohort_data = aggregate_to_cohorts(
339
+ data,
340
+ unit_column='firm_id',
341
+ time_column='year',
342
+ treatment_column='treated',
343
+ outcome='sales'
344
+ )
345
+ # Result: mean outcome by treatment group and period
346
+ ```
347
+
348
+ ### Rank Control Units
349
+
350
+ Select the best control units for DiD or Synthetic DiD analysis by ranking them based on pre-treatment outcome similarity:
351
+
352
+ ```python
353
+ from diff_diff import rank_control_units, generate_did_data
354
+
355
+ # Generate sample data
356
+ data = generate_did_data(n_units=50, n_periods=6, seed=42)
357
+
358
+ # Rank control units by their similarity to treated units
359
+ ranking = rank_control_units(
360
+ data,
361
+ unit_column='unit',
362
+ time_column='period',
363
+ outcome_column='outcome',
364
+ treatment_column='treated',
365
+ n_top=10 # Return top 10 controls
366
+ )
367
+
368
+ print(ranking[['unit', 'quality_score', 'pre_trend_rmse']])
369
+ ```
370
+
371
+ Output:
372
+ ```
373
+ unit quality_score pre_trend_rmse
374
+ 0 35 1.0000 0.4521
375
+ 1 42 0.9234 0.5123
376
+ 2 28 0.8876 0.5892
377
+ ...
378
+ ```
379
+
380
+ With covariates for matching:
381
+
382
+ ```python
383
+ # Add covariate-based matching
384
+ ranking = rank_control_units(
385
+ data,
386
+ unit_column='unit',
387
+ time_column='period',
388
+ outcome_column='outcome',
389
+ treatment_column='treated',
390
+ covariates=['size', 'age'], # Match on these too
391
+ outcome_weight=0.7, # 70% weight on outcome trends
392
+ covariate_weight=0.3 # 30% weight on covariate similarity
393
+ )
394
+ ```
395
+
396
+ Filter data for SyntheticDiD using top controls:
397
+
398
+ ```python
399
+ from diff_diff import SyntheticDiD
400
+
401
+ # Get top control units
402
+ top_controls = ranking['unit'].tolist()
403
+
404
+ # Filter data to treated + top controls
405
+ filtered_data = data[
406
+ (data['treated'] == 1) | (data['unit'].isin(top_controls))
407
+ ]
408
+
409
+ # Fit SyntheticDiD with selected controls
410
+ sdid = SyntheticDiD()
411
+ results = sdid.fit(
412
+ filtered_data,
413
+ outcome='outcome',
414
+ treatment='treated',
415
+ unit='unit',
416
+ time='period',
417
+ post_periods=[3, 4, 5]
418
+ )
419
+ ```
420
+
421
+ ## Usage
422
+
423
+ ### Basic DiD with Column Names
424
+
425
+ ```python
426
+ from diff_diff import DifferenceInDifferences
427
+
428
+ did = DifferenceInDifferences(robust=True, alpha=0.05)
429
+ results = did.fit(
430
+ data,
431
+ outcome='sales',
432
+ treatment='treated',
433
+ time='post_policy'
434
+ )
435
+
436
+ # Access results
437
+ print(f"ATT: {results.att:.4f}")
438
+ print(f"Standard Error: {results.se:.4f}")
439
+ print(f"P-value: {results.p_value:.4f}")
440
+ print(f"95% CI: {results.conf_int}")
441
+ print(f"Significant: {results.is_significant}")
442
+ ```
443
+
444
+ ### Using Formula Interface
445
+
446
+ ```python
447
+ # R-style formula syntax
448
+ results = did.fit(data, formula='outcome ~ treated * post')
449
+
450
+ # Explicit interaction syntax
451
+ results = did.fit(data, formula='outcome ~ treated + post + treated:post')
452
+
453
+ # With covariates
454
+ results = did.fit(data, formula='outcome ~ treated * post + age + income')
455
+ ```
456
+
457
+ ### Including Covariates
458
+
459
+ ```python
460
+ results = did.fit(
461
+ data,
462
+ outcome='outcome',
463
+ treatment='treated',
464
+ time='post',
465
+ covariates=['age', 'income', 'education']
466
+ )
467
+ ```
468
+
469
+ ### Fixed Effects
470
+
471
+ Use `fixed_effects` for low-dimensional categorical controls (creates dummy variables):
472
+
473
+ ```python
474
+ # State and industry fixed effects
475
+ results = did.fit(
476
+ data,
477
+ outcome='sales',
478
+ treatment='treated',
479
+ time='post',
480
+ fixed_effects=['state', 'industry']
481
+ )
482
+
483
+ # Access fixed effect coefficients
484
+ state_coefs = {k: v for k, v in results.coefficients.items() if k.startswith('state_')}
485
+ ```
486
+
487
+ Use `absorb` for high-dimensional fixed effects (more efficient, uses within-transformation):
488
+
489
+ ```python
490
+ # Absorb firm-level fixed effects (efficient for many firms)
491
+ results = did.fit(
492
+ data,
493
+ outcome='sales',
494
+ treatment='treated',
495
+ time='post',
496
+ absorb=['firm_id']
497
+ )
498
+ ```
499
+
500
+ Combine covariates with fixed effects:
501
+
502
+ ```python
503
+ results = did.fit(
504
+ data,
505
+ outcome='sales',
506
+ treatment='treated',
507
+ time='post',
508
+ covariates=['size', 'age'], # Linear controls
509
+ fixed_effects=['industry'], # Low-dimensional FE (dummies)
510
+ absorb=['firm_id'] # High-dimensional FE (absorbed)
511
+ )
512
+ ```
513
+
514
+ ### Cluster-Robust Standard Errors
515
+
516
+ ```python
517
+ did = DifferenceInDifferences(cluster='state')
518
+ results = did.fit(
519
+ data,
520
+ outcome='outcome',
521
+ treatment='treated',
522
+ time='post'
523
+ )
524
+ ```
525
+
526
+ ### Two-Way Fixed Effects (Panel Data)
527
+
528
+ ```python
529
+ from diff_diff.estimators import TwoWayFixedEffects
530
+
531
+ twfe = TwoWayFixedEffects()
532
+ results = twfe.fit(
533
+ panel_data,
534
+ outcome='outcome',
535
+ treatment='treated',
536
+ time='year',
537
+ unit='firm_id'
538
+ )
539
+ ```
540
+
541
+ ### Multi-Period DiD (Event Study)
542
+
543
+ For settings with multiple pre- and post-treatment periods:
544
+
545
+ ```python
546
+ from diff_diff import MultiPeriodDiD
547
+
548
+ # Fit with multiple time periods
549
+ did = MultiPeriodDiD()
550
+ results = did.fit(
551
+ panel_data,
552
+ outcome='sales',
553
+ treatment='treated',
554
+ time='period',
555
+ post_periods=[3, 4, 5], # Periods 3-5 are post-treatment
556
+ reference_period=0 # Reference period for comparison
557
+ )
558
+
559
+ # View period-specific treatment effects
560
+ for period, effect in results.period_effects.items():
561
+ print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})")
562
+
563
+ # View average treatment effect across post-periods
564
+ print(f"Average ATT: {results.avg_att:.3f}")
565
+ print(f"Average SE: {results.avg_se:.3f}")
566
+
567
+ # Full summary with all period effects
568
+ results.print_summary()
569
+ ```
570
+
571
+ Output:
572
+ ```
573
+ ================================================================================
574
+ Multi-Period Difference-in-Differences Estimation Results
575
+ ================================================================================
576
+
577
+ Observations: 600
578
+ Pre-treatment periods: 3
579
+ Post-treatment periods: 3
580
+
581
+ --------------------------------------------------------------------------------
582
+ Average Treatment Effect
583
+ --------------------------------------------------------------------------------
584
+ Average ATT 5.2000 0.8234 6.315 0.0000
585
+ --------------------------------------------------------------------------------
586
+ 95% Confidence Interval: [3.5862, 6.8138]
587
+
588
+ Period-Specific Effects:
589
+ --------------------------------------------------------------------------------
590
+ Period Effect Std. Err. t-stat P>|t|
591
+ --------------------------------------------------------------------------------
592
+ 3 4.5000 0.9512 4.731 0.0000***
593
+ 4 5.2000 0.8876 5.858 0.0000***
594
+ 5 5.9000 0.9123 6.468 0.0000***
595
+ --------------------------------------------------------------------------------
596
+
597
+ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
598
+ ================================================================================
599
+ ```
600
+
601
+ ### Staggered Difference-in-Differences (Callaway-Sant'Anna)
602
+
603
+ When treatment is adopted at different times by different units, traditional TWFE estimators can be biased. The Callaway-Sant'Anna estimator provides unbiased estimates with staggered adoption.
604
+
605
+ ```python
606
+ from diff_diff import CallawaySantAnna
607
+
608
+ # Panel data with staggered treatment
609
+ # 'first_treat' = period when unit was first treated (0 if never treated)
610
+ cs = CallawaySantAnna()
611
+ results = cs.fit(
612
+ panel_data,
613
+ outcome='sales',
614
+ unit='firm_id',
615
+ time='year',
616
+ first_treat='first_treat', # 0 for never-treated, else first treatment year
617
+ aggregate='event_study' # Compute event study effects
618
+ )
619
+
620
+ # View results
621
+ results.print_summary()
622
+
623
+ # Access group-time effects ATT(g,t)
624
+ for (group, time), effect in results.group_time_effects.items():
625
+ print(f"Cohort {group}, Period {time}: {effect['effect']:.3f}")
626
+
627
+ # Event study effects (averaged by relative time)
628
+ for rel_time, effect in results.event_study_effects.items():
629
+ print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
630
+
631
+ # Convert to DataFrame
632
+ df = results.to_dataframe(level='event_study')
633
+ ```
634
+
635
+ Output:
636
+ ```
637
+ =====================================================================================
638
+ Callaway-Sant'Anna Staggered Difference-in-Differences Results
639
+ =====================================================================================
640
+
641
+ Total observations: 600
642
+ Treated units: 35
643
+ Control units: 15
644
+ Treatment cohorts: 3
645
+ Time periods: 8
646
+ Control group: never_treated
647
+
648
+ -------------------------------------------------------------------------------------
649
+ Overall Average Treatment Effect on the Treated
650
+ -------------------------------------------------------------------------------------
651
+ Parameter Estimate Std. Err. t-stat P>|t| Sig.
652
+ -------------------------------------------------------------------------------------
653
+ ATT 2.5000 0.3521 7.101 0.0000 ***
654
+ -------------------------------------------------------------------------------------
655
+
656
+ 95% Confidence Interval: [1.8099, 3.1901]
657
+
658
+ -------------------------------------------------------------------------------------
659
+ Event Study (Dynamic) Effects
660
+ -------------------------------------------------------------------------------------
661
+ Rel. Period Estimate Std. Err. t-stat P>|t| Sig.
662
+ -------------------------------------------------------------------------------------
663
+ 0 2.1000 0.4521 4.645 0.0000 ***
664
+ 1 2.5000 0.4123 6.064 0.0000 ***
665
+ 2 2.8000 0.5234 5.349 0.0000 ***
666
+ -------------------------------------------------------------------------------------
667
+
668
+ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
669
+ =====================================================================================
670
+ ```
671
+
672
+ **When to use Callaway-Sant'Anna vs TWFE:**
673
+
674
+ | Scenario | Use TWFE | Use Callaway-Sant'Anna |
675
+ |----------|----------|------------------------|
676
+ | All units treated at same time | ✓ | ✓ |
677
+ | Staggered adoption, homogeneous effects | ✓ | ✓ |
678
+ | Staggered adoption, heterogeneous effects | ✗ | ✓ |
679
+ | Need event study with staggered timing | ✗ | ✓ |
680
+ | Fewer than ~20 treated units | ✓ | Depends on design |
681
+
682
+ **Parameters:**
683
+
684
+ ```python
685
+ CallawaySantAnna(
686
+ control_group='never_treated', # or 'not_yet_treated'
687
+ anticipation=0, # Periods before treatment with effects
688
+ estimation_method='dr', # 'dr', 'ipw', or 'reg'
689
+ alpha=0.05, # Significance level
690
+ cluster=None, # Column for cluster SEs
691
+ n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
692
+ seed=None # Random seed
693
+ )
694
+ ```
695
+
696
+ **Current limitations:**
697
+ - Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
698
+ - Covariate adjustment for conditional parallel trends is not yet implemented
699
+
700
+ ### Event Study Visualization
701
+
702
+ Create publication-ready event study plots:
703
+
704
+ ```python
705
+ from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
706
+
707
+ # From MultiPeriodDiD
708
+ did = MultiPeriodDiD()
709
+ results = did.fit(data, outcome='y', treatment='treated',
710
+ time='period', post_periods=[3, 4, 5])
711
+ plot_event_study(results, title="Treatment Effects Over Time")
712
+
713
+ # From CallawaySantAnna (with event study aggregation)
714
+ cs = CallawaySantAnna()
715
+ results = cs.fit(data, outcome='y', unit='unit', time='period',
716
+ first_treat='first_treat', aggregate='event_study')
717
+ plot_event_study(results, title="Staggered DiD Event Study")
718
+
719
+ # From a DataFrame
720
+ df = pd.DataFrame({
721
+ 'period': [-2, -1, 0, 1, 2],
722
+ 'effect': [0.1, 0.05, 0.0, 2.5, 2.8],
723
+ 'se': [0.3, 0.25, 0.0, 0.4, 0.45]
724
+ })
725
+ plot_event_study(df, reference_period=0)
726
+
727
+ # With customization
728
+ ax = plot_event_study(
729
+ results,
730
+ title="Dynamic Treatment Effects",
731
+ xlabel="Years Relative to Treatment",
732
+ ylabel="Effect on Sales ($1000s)",
733
+ color="#2563eb",
734
+ marker="o",
735
+ shade_pre=True, # Shade pre-treatment region
736
+ show_zero_line=True, # Horizontal line at y=0
737
+ show_reference_line=True, # Vertical line at reference period
738
+ figsize=(10, 6),
739
+ show=False # Don't call plt.show(), return axes
740
+ )
741
+ ```
742
+
743
+ ### Synthetic Difference-in-Differences
744
+
745
+ 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.
746
+
747
+ ```python
748
+ from diff_diff import SyntheticDiD
749
+
750
+ # Fit Synthetic DiD model
751
+ sdid = SyntheticDiD()
752
+ results = sdid.fit(
753
+ panel_data,
754
+ outcome='gdp_growth',
755
+ treatment='treated',
756
+ unit='state',
757
+ time='year',
758
+ post_periods=[2015, 2016, 2017, 2018]
759
+ )
760
+
761
+ # View results
762
+ results.print_summary()
763
+ print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
764
+
765
+ # Examine unit weights (which control units matter most)
766
+ weights_df = results.get_unit_weights_df()
767
+ print(weights_df.head(10))
768
+
769
+ # Examine time weights
770
+ time_weights_df = results.get_time_weights_df()
771
+ print(time_weights_df)
772
+ ```
773
+
774
+ Output:
775
+ ```
776
+ ===========================================================================
777
+ Synthetic Difference-in-Differences Estimation Results
778
+ ===========================================================================
779
+
780
+ Observations: 500
781
+ Treated units: 1
782
+ Control units: 49
783
+ Pre-treatment periods: 6
784
+ Post-treatment periods: 4
785
+ Regularization (lambda): 0.0000
786
+ Pre-treatment fit (RMSE): 0.1234
787
+
788
+ ---------------------------------------------------------------------------
789
+ Parameter Estimate Std. Err. t-stat P>|t|
790
+ ---------------------------------------------------------------------------
791
+ ATT 2.5000 0.4521 5.530 0.0000
792
+ ---------------------------------------------------------------------------
793
+
794
+ 95% Confidence Interval: [1.6139, 3.3861]
795
+
796
+ ---------------------------------------------------------------------------
797
+ Top Unit Weights (Synthetic Control)
798
+ ---------------------------------------------------------------------------
799
+ Unit state_12: 0.3521
800
+ Unit state_5: 0.2156
801
+ Unit state_23: 0.1834
802
+ Unit state_8: 0.1245
803
+ Unit state_31: 0.0892
804
+ (8 units with weight > 0.001)
805
+
806
+ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
807
+ ===========================================================================
808
+ ```
809
+
810
+ #### When to Use Synthetic DiD Over Vanilla DiD
811
+
812
+ Use Synthetic DiD instead of standard DiD when:
813
+
814
+ 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.
815
+
816
+ ```python
817
+ # Example: California passed a policy, want to estimate its effect
818
+ # Standard DiD would compare CA to the average of all other states
819
+ # Synthetic DiD finds states that together best match CA's pre-treatment trend
820
+ ```
821
+
822
+ 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.
823
+
824
+ ```python
825
+ # Example: A tech hub city vs rural areas
826
+ # Rural areas may not be a good comparison on average
827
+ # Synthetic DiD can weight urban/suburban controls more heavily
828
+ ```
829
+
830
+ 3. **Heterogeneous control units**: When control units are very different from each other, equal weighting (as in standard DiD) is suboptimal.
831
+
832
+ ```python
833
+ # Example: Comparing a treated developing country to other countries
834
+ # Some control countries may be much more similar economically
835
+ # Synthetic DiD upweights the most comparable controls
836
+ ```
837
+
838
+ 4. **You want transparency**: Synthetic DiD provides explicit unit weights showing which controls contribute most to the comparison.
839
+
840
+ ```python
841
+ # See exactly which units are driving the counterfactual
842
+ print(results.get_unit_weights_df())
843
+ ```
844
+
845
+ **Key differences from standard DiD:**
846
+
847
+ | Aspect | Standard DiD | Synthetic DiD |
848
+ |--------|--------------|---------------|
849
+ | Control weighting | Equal (1/N) | Optimized to match pre-treatment |
850
+ | Time weighting | Equal across periods | Can emphasize informative periods |
851
+ | N treated required | Can be many | Works with 1 treated unit |
852
+ | Parallel trends | Assumed | Partially relaxed via matching |
853
+ | Interpretability | Simple average | Explicit weights |
854
+
855
+ **Parameters:**
856
+
857
+ ```python
858
+ SyntheticDiD(
859
+ lambda_reg=0.0, # Regularization toward uniform weights (0 = no reg)
860
+ zeta=1.0, # Time weight regularization (higher = more uniform)
861
+ alpha=0.05, # Significance level
862
+ n_bootstrap=200, # Bootstrap iterations for SE (0 = placebo-based)
863
+ seed=None # Random seed for reproducibility
864
+ )
865
+ ```
866
+
867
+ ## Working with Results
868
+
869
+ ### Export Results
870
+
871
+ ```python
872
+ # As dictionary
873
+ results.to_dict()
874
+ # {'att': 3.5, 'se': 1.26, 'p_value': 0.037, ...}
875
+
876
+ # As DataFrame
877
+ df = results.to_dataframe()
878
+ ```
879
+
880
+ ### Check Significance
881
+
882
+ ```python
883
+ if results.is_significant:
884
+ print(f"Effect is significant at {did.alpha} level")
885
+
886
+ # Get significance stars
887
+ print(f"ATT: {results.att}{results.significance_stars}")
888
+ # ATT: 3.5000*
889
+ ```
890
+
891
+ ### Access Full Regression Output
892
+
893
+ ```python
894
+ # All coefficients
895
+ results.coefficients
896
+ # {'const': 9.5, 'treated': 1.0, 'post': 2.5, 'treated:post': 3.5}
897
+
898
+ # Variance-covariance matrix
899
+ results.vcov
900
+
901
+ # Residuals and fitted values
902
+ results.residuals
903
+ results.fitted_values
904
+
905
+ # R-squared
906
+ results.r_squared
907
+ ```
908
+
909
+ ## Checking Assumptions
910
+
911
+ ### Parallel Trends
912
+
913
+ **Simple slope-based test:**
914
+
915
+ ```python
916
+ from diff_diff.utils import check_parallel_trends
917
+
918
+ trends = check_parallel_trends(
919
+ data,
920
+ outcome='outcome',
921
+ time='period',
922
+ treatment_group='treated'
923
+ )
924
+
925
+ print(f"Treated trend: {trends['treated_trend']:.4f}")
926
+ print(f"Control trend: {trends['control_trend']:.4f}")
927
+ print(f"Difference p-value: {trends['p_value']:.4f}")
928
+ ```
929
+
930
+ **Robust distributional test (Wasserstein distance):**
931
+
932
+ ```python
933
+ from diff_diff.utils import check_parallel_trends_robust
934
+
935
+ results = check_parallel_trends_robust(
936
+ data,
937
+ outcome='outcome',
938
+ time='period',
939
+ treatment_group='treated',
940
+ unit='firm_id', # Unit identifier for panel data
941
+ pre_periods=[2018, 2019], # Pre-treatment periods
942
+ n_permutations=1000 # Permutations for p-value
943
+ )
944
+
945
+ print(f"Wasserstein distance: {results['wasserstein_distance']:.4f}")
946
+ print(f"Wasserstein p-value: {results['wasserstein_p_value']:.4f}")
947
+ print(f"KS test p-value: {results['ks_p_value']:.4f}")
948
+ print(f"Parallel trends plausible: {results['parallel_trends_plausible']}")
949
+ ```
950
+
951
+ The Wasserstein (Earth Mover's) distance compares the full distribution of outcome changes, not just means. This is more robust to:
952
+ - Non-normal distributions
953
+ - Heterogeneous effects across units
954
+ - Outliers
955
+
956
+ **Equivalence testing (TOST):**
957
+
958
+ ```python
959
+ from diff_diff.utils import equivalence_test_trends
960
+
961
+ results = equivalence_test_trends(
962
+ data,
963
+ outcome='outcome',
964
+ time='period',
965
+ treatment_group='treated',
966
+ unit='firm_id',
967
+ equivalence_margin=0.5 # Define "practically equivalent"
968
+ )
969
+
970
+ print(f"Mean difference: {results['mean_difference']:.4f}")
971
+ print(f"TOST p-value: {results['tost_p_value']:.4f}")
972
+ print(f"Trends equivalent: {results['equivalent']}")
973
+ ```
974
+
975
+ ## API Reference
976
+
977
+ ### DifferenceInDifferences
978
+
979
+ ```python
980
+ DifferenceInDifferences(
981
+ robust=True, # Use HC1 robust standard errors
982
+ cluster=None, # Column for cluster-robust SEs
983
+ alpha=0.05 # Significance level for CIs
984
+ )
985
+ ```
986
+
987
+ **Methods:**
988
+
989
+ | Method | Description |
990
+ |--------|-------------|
991
+ | `fit(data, outcome, treatment, time, ...)` | Fit the DiD model |
992
+ | `summary()` | Get formatted summary string |
993
+ | `print_summary()` | Print summary to stdout |
994
+ | `get_params()` | Get estimator parameters (sklearn-compatible) |
995
+ | `set_params(**params)` | Set estimator parameters (sklearn-compatible) |
996
+
997
+ **fit() Parameters:**
998
+
999
+ | Parameter | Type | Description |
1000
+ |-----------|------|-------------|
1001
+ | `data` | DataFrame | Input data |
1002
+ | `outcome` | str | Outcome variable column name |
1003
+ | `treatment` | str | Treatment indicator column (0/1) |
1004
+ | `time` | str | Post-treatment indicator column (0/1) |
1005
+ | `formula` | str | R-style formula (alternative to column names) |
1006
+ | `covariates` | list | Linear control variables |
1007
+ | `fixed_effects` | list | Categorical FE columns (creates dummies) |
1008
+ | `absorb` | list | High-dimensional FE (within-transformation) |
1009
+
1010
+ ### DiDResults
1011
+
1012
+ **Attributes:**
1013
+
1014
+ | Attribute | Description |
1015
+ |-----------|-------------|
1016
+ | `att` | Average Treatment effect on the Treated |
1017
+ | `se` | Standard error of ATT |
1018
+ | `t_stat` | T-statistic |
1019
+ | `p_value` | P-value for H0: ATT = 0 |
1020
+ | `conf_int` | Tuple of (lower, upper) confidence bounds |
1021
+ | `n_obs` | Number of observations |
1022
+ | `n_treated` | Number of treated units |
1023
+ | `n_control` | Number of control units |
1024
+ | `r_squared` | R-squared of regression |
1025
+ | `coefficients` | Dictionary of all coefficients |
1026
+ | `is_significant` | Boolean for significance at alpha |
1027
+ | `significance_stars` | String of significance stars |
1028
+
1029
+ **Methods:**
1030
+
1031
+ | Method | Description |
1032
+ |--------|-------------|
1033
+ | `summary(alpha)` | Get formatted summary string |
1034
+ | `print_summary(alpha)` | Print summary to stdout |
1035
+ | `to_dict()` | Convert to dictionary |
1036
+ | `to_dataframe()` | Convert to pandas DataFrame |
1037
+
1038
+ ### MultiPeriodDiD
1039
+
1040
+ ```python
1041
+ MultiPeriodDiD(
1042
+ robust=True, # Use HC1 robust standard errors
1043
+ cluster=None, # Column for cluster-robust SEs
1044
+ alpha=0.05 # Significance level for CIs
1045
+ )
1046
+ ```
1047
+
1048
+ **fit() Parameters:**
1049
+
1050
+ | Parameter | Type | Description |
1051
+ |-----------|------|-------------|
1052
+ | `data` | DataFrame | Input data |
1053
+ | `outcome` | str | Outcome variable column name |
1054
+ | `treatment` | str | Treatment indicator column (0/1) |
1055
+ | `time` | str | Time period column (multiple values) |
1056
+ | `post_periods` | list | List of post-treatment period values |
1057
+ | `covariates` | list | Linear control variables |
1058
+ | `fixed_effects` | list | Categorical FE columns (creates dummies) |
1059
+ | `absorb` | list | High-dimensional FE (within-transformation) |
1060
+ | `reference_period` | any | Omitted period for time dummies |
1061
+
1062
+ ### MultiPeriodDiDResults
1063
+
1064
+ **Attributes:**
1065
+
1066
+ | Attribute | Description |
1067
+ |-----------|-------------|
1068
+ | `period_effects` | Dict mapping periods to PeriodEffect objects |
1069
+ | `avg_att` | Average ATT across post-treatment periods |
1070
+ | `avg_se` | Standard error of average ATT |
1071
+ | `avg_t_stat` | T-statistic for average ATT |
1072
+ | `avg_p_value` | P-value for average ATT |
1073
+ | `avg_conf_int` | Confidence interval for average ATT |
1074
+ | `n_obs` | Number of observations |
1075
+ | `pre_periods` | List of pre-treatment periods |
1076
+ | `post_periods` | List of post-treatment periods |
1077
+
1078
+ **Methods:**
1079
+
1080
+ | Method | Description |
1081
+ |--------|-------------|
1082
+ | `get_effect(period)` | Get PeriodEffect for specific period |
1083
+ | `summary(alpha)` | Get formatted summary string |
1084
+ | `print_summary(alpha)` | Print summary to stdout |
1085
+ | `to_dict()` | Convert to dictionary |
1086
+ | `to_dataframe()` | Convert to pandas DataFrame |
1087
+
1088
+ ### PeriodEffect
1089
+
1090
+ **Attributes:**
1091
+
1092
+ | Attribute | Description |
1093
+ |-----------|-------------|
1094
+ | `period` | Time period identifier |
1095
+ | `effect` | Treatment effect estimate |
1096
+ | `se` | Standard error |
1097
+ | `t_stat` | T-statistic |
1098
+ | `p_value` | P-value |
1099
+ | `conf_int` | Confidence interval |
1100
+ | `is_significant` | Boolean for significance at 0.05 |
1101
+ | `significance_stars` | String of significance stars |
1102
+
1103
+ ### SyntheticDiD
1104
+
1105
+ ```python
1106
+ SyntheticDiD(
1107
+ lambda_reg=0.0, # L2 regularization for unit weights
1108
+ zeta=1.0, # Regularization for time weights
1109
+ alpha=0.05, # Significance level for CIs
1110
+ n_bootstrap=200, # Bootstrap iterations for SE
1111
+ seed=None # Random seed for reproducibility
1112
+ )
1113
+ ```
1114
+
1115
+ **fit() Parameters:**
1116
+
1117
+ | Parameter | Type | Description |
1118
+ |-----------|------|-------------|
1119
+ | `data` | DataFrame | Panel data |
1120
+ | `outcome` | str | Outcome variable column name |
1121
+ | `treatment` | str | Treatment indicator column (0/1) |
1122
+ | `unit` | str | Unit identifier column |
1123
+ | `time` | str | Time period column |
1124
+ | `post_periods` | list | List of post-treatment period values |
1125
+ | `covariates` | list | Covariates to residualize out |
1126
+
1127
+ ### SyntheticDiDResults
1128
+
1129
+ **Attributes:**
1130
+
1131
+ | Attribute | Description |
1132
+ |-----------|-------------|
1133
+ | `att` | Average Treatment effect on the Treated |
1134
+ | `se` | Standard error (bootstrap or placebo-based) |
1135
+ | `t_stat` | T-statistic |
1136
+ | `p_value` | P-value |
1137
+ | `conf_int` | Confidence interval |
1138
+ | `n_obs` | Number of observations |
1139
+ | `n_treated` | Number of treated units |
1140
+ | `n_control` | Number of control units |
1141
+ | `unit_weights` | Dict mapping control unit IDs to weights |
1142
+ | `time_weights` | Dict mapping pre-treatment periods to weights |
1143
+ | `pre_periods` | List of pre-treatment periods |
1144
+ | `post_periods` | List of post-treatment periods |
1145
+ | `pre_treatment_fit` | RMSE of synthetic vs treated in pre-period |
1146
+ | `placebo_effects` | Array of placebo effect estimates |
1147
+
1148
+ **Methods:**
1149
+
1150
+ | Method | Description |
1151
+ |--------|-------------|
1152
+ | `summary(alpha)` | Get formatted summary string |
1153
+ | `print_summary(alpha)` | Print summary to stdout |
1154
+ | `to_dict()` | Convert to dictionary |
1155
+ | `to_dataframe()` | Convert to pandas DataFrame |
1156
+ | `get_unit_weights_df()` | Get unit weights as DataFrame |
1157
+ | `get_time_weights_df()` | Get time weights as DataFrame |
1158
+
1159
+ ### Data Preparation Functions
1160
+
1161
+ #### generate_did_data
1162
+
1163
+ ```python
1164
+ generate_did_data(
1165
+ n_units=100, # Number of units
1166
+ n_periods=4, # Number of time periods
1167
+ treatment_effect=5.0, # True ATT
1168
+ treatment_fraction=0.5, # Fraction treated
1169
+ treatment_period=2, # First post-treatment period
1170
+ unit_fe_sd=2.0, # Unit fixed effect std dev
1171
+ time_trend=0.5, # Linear time trend
1172
+ noise_sd=1.0, # Idiosyncratic noise std dev
1173
+ seed=None # Random seed
1174
+ )
1175
+ ```
1176
+
1177
+ Returns DataFrame with columns: `unit`, `period`, `treated`, `post`, `outcome`, `true_effect`.
1178
+
1179
+ #### make_treatment_indicator
1180
+
1181
+ ```python
1182
+ make_treatment_indicator(
1183
+ data, # Input DataFrame
1184
+ column, # Column to create treatment from
1185
+ treated_values=None, # Value(s) indicating treatment
1186
+ threshold=None, # Numeric threshold for treatment
1187
+ above_threshold=True, # If True, >= threshold is treated
1188
+ new_column='treated' # Output column name
1189
+ )
1190
+ ```
1191
+
1192
+ #### make_post_indicator
1193
+
1194
+ ```python
1195
+ make_post_indicator(
1196
+ data, # Input DataFrame
1197
+ time_column, # Time/period column
1198
+ post_periods=None, # Specific post-treatment period(s)
1199
+ treatment_start=None, # First post-treatment period
1200
+ new_column='post' # Output column name
1201
+ )
1202
+ ```
1203
+
1204
+ #### wide_to_long
1205
+
1206
+ ```python
1207
+ wide_to_long(
1208
+ data, # Wide-format DataFrame
1209
+ value_columns, # List of time-varying columns
1210
+ id_column, # Unit identifier column
1211
+ time_name='period', # Name for time column
1212
+ value_name='value', # Name for value column
1213
+ time_values=None # Values for time periods
1214
+ )
1215
+ ```
1216
+
1217
+ #### balance_panel
1218
+
1219
+ ```python
1220
+ balance_panel(
1221
+ data, # Panel DataFrame
1222
+ unit_column, # Unit identifier column
1223
+ time_column, # Time period column
1224
+ method='inner', # 'inner', 'outer', or 'fill'
1225
+ fill_value=None # Value for filling (if method='fill')
1226
+ )
1227
+ ```
1228
+
1229
+ #### validate_did_data
1230
+
1231
+ ```python
1232
+ validate_did_data(
1233
+ data, # DataFrame to validate
1234
+ outcome, # Outcome column name
1235
+ treatment, # Treatment column name
1236
+ time, # Time/post column name
1237
+ unit=None, # Unit column (for panel validation)
1238
+ raise_on_error=True # Raise ValueError or return dict
1239
+ )
1240
+ ```
1241
+
1242
+ Returns dict with `valid`, `errors`, `warnings`, and `summary` keys.
1243
+
1244
+ #### summarize_did_data
1245
+
1246
+ ```python
1247
+ summarize_did_data(
1248
+ data, # Input DataFrame
1249
+ outcome, # Outcome column name
1250
+ treatment, # Treatment column name
1251
+ time, # Time/post column name
1252
+ unit=None # Unit column (optional)
1253
+ )
1254
+ ```
1255
+
1256
+ Returns DataFrame with summary statistics by treatment-time cell.
1257
+
1258
+ #### create_event_time
1259
+
1260
+ ```python
1261
+ create_event_time(
1262
+ data, # Panel DataFrame
1263
+ time_column, # Calendar time column
1264
+ treatment_time_column, # Column with treatment timing
1265
+ new_column='event_time' # Output column name
1266
+ )
1267
+ ```
1268
+
1269
+ #### aggregate_to_cohorts
1270
+
1271
+ ```python
1272
+ aggregate_to_cohorts(
1273
+ data, # Unit-level panel data
1274
+ unit_column, # Unit identifier column
1275
+ time_column, # Time period column
1276
+ treatment_column, # Treatment indicator column
1277
+ outcome, # Outcome variable column
1278
+ covariates=None # Additional columns to aggregate
1279
+ )
1280
+ ```
1281
+
1282
+ #### rank_control_units
1283
+
1284
+ ```python
1285
+ rank_control_units(
1286
+ data, # Panel data in long format
1287
+ unit_column, # Unit identifier column
1288
+ time_column, # Time period column
1289
+ outcome_column, # Outcome variable column
1290
+ treatment_column=None, # Treatment indicator column (0/1)
1291
+ treated_units=None, # Explicit list of treated unit IDs
1292
+ pre_periods=None, # Pre-treatment periods (default: first half)
1293
+ covariates=None, # Covariate columns for matching
1294
+ outcome_weight=0.7, # Weight for outcome trend similarity (0-1)
1295
+ covariate_weight=0.3, # Weight for covariate distance (0-1)
1296
+ exclude_units=None, # Units to exclude from control pool
1297
+ require_units=None, # Units that must appear in output
1298
+ n_top=None, # Return only top N controls
1299
+ suggest_treatment_candidates=False, # Identify treatment candidates
1300
+ n_treatment_candidates=5, # Number of treatment candidates
1301
+ lambda_reg=0.0 # Regularization for synthetic weights
1302
+ )
1303
+ ```
1304
+
1305
+ Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, `covariate_score`, `synthetic_weight`, `pre_trend_rmse`, `is_required`.
1306
+
1307
+ ## Requirements
1308
+
1309
+ - Python >= 3.9
1310
+ - numpy >= 1.20
1311
+ - pandas >= 1.3
1312
+ - scipy >= 1.7
1313
+
1314
+ ## Development
1315
+
1316
+ ```bash
1317
+ # Install with dev dependencies
1318
+ pip install -e ".[dev]"
1319
+
1320
+ # Run tests
1321
+ pytest
1322
+
1323
+ # Format code
1324
+ black diff_diff tests
1325
+ ruff check diff_diff tests
1326
+ ```
1327
+
1328
+ ## References
1329
+
1330
+ This library implements methods from the following scholarly works:
1331
+
1332
+ ### Difference-in-Differences
1333
+
1334
+ - **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)
1335
+
1336
+ - **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)
1337
+
1338
+ - **Angrist, J. D., & Pischke, J.-S. (2009).** *Mostly Harmless Econometrics: An Empiricist's Companion*. Princeton University Press. Chapter 5: Differences-in-Differences.
1339
+
1340
+ ### Two-Way Fixed Effects
1341
+
1342
+ - **Wooldridge, J. M. (2010).** *Econometric Analysis of Cross Section and Panel Data* (2nd ed.). MIT Press.
1343
+
1344
+ - **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)
1345
+
1346
+ ### Robust Standard Errors
1347
+
1348
+ - **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)
1349
+
1350
+ - **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)
1351
+
1352
+ - **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)
1353
+
1354
+ ### Synthetic Control Method
1355
+
1356
+ - **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)
1357
+
1358
+ - **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)
1359
+
1360
+ - **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)
1361
+
1362
+ ### Synthetic Difference-in-Differences
1363
+
1364
+ - **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)
1365
+
1366
+ ### Parallel Trends and Pre-Trend Testing
1367
+
1368
+ - **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)
1369
+
1370
+ - **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)
1371
+
1372
+ - **Lakens, D. (2017).** "Equivalence Tests: A Practical Primer for t Tests, Correlations, and Meta-Analyses." *Social Psychological and Personality Science*, 8(4), 355-362. [https://doi.org/10.1177/1948550617697177](https://doi.org/10.1177/1948550617697177)
1373
+
1374
+ ### Multi-Period and Staggered Adoption
1375
+
1376
+ - **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)
1377
+
1378
+ - **Sant'Anna, P. H. C., & Zhao, J. (2020).** "Doubly Robust Difference-in-Differences Estimators." *Journal of Econometrics*, 219(1), 101-122. [https://doi.org/10.1016/j.jeconom.2020.06.003](https://doi.org/10.1016/j.jeconom.2020.06.003)
1379
+
1380
+ - **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)
1381
+
1382
+ - **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)
1383
+
1384
+ - **Goodman-Bacon, A. (2021).** "Difference-in-Differences with Variation in Treatment Timing." *Journal of Econometrics*, 225(2), 254-277. [https://doi.org/10.1016/j.jeconom.2021.03.014](https://doi.org/10.1016/j.jeconom.2021.03.014)
1385
+
1386
+ ### General Causal Inference
1387
+
1388
+ - **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.
1389
+
1390
+ - **Cunningham, S. (2021).** *Causal Inference: The Mixtape*. Yale University Press. [https://mixtape.scunning.com/](https://mixtape.scunning.com/)
1391
+
1392
+ ## License
1393
+
1394
+ MIT License