diff-diff 0.3.0__tar.gz → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: A library for Difference-in-Differences causal inference analysis
5
5
  Author: diff-diff contributors
6
6
  License-Expression: MIT
@@ -69,28 +69,28 @@ did = DifferenceInDifferences()
69
69
  results = did.fit(data, outcome='outcome', treatment='treated', time='post')
70
70
 
71
71
  # View results
72
- print(results) # DiDResults(ATT=3.5000*, SE=1.2583, p=0.0367)
72
+ print(results) # DiDResults(ATT=3.0000, SE=1.7321, p=0.1583)
73
73
  results.print_summary()
74
74
  ```
75
75
 
76
76
  Output:
77
77
  ```
78
78
  ======================================================================
79
- Difference-in-Differences Estimation Results
79
+ Difference-in-Differences Estimation Results
80
80
  ======================================================================
81
81
 
82
- Observations: 8
83
- Treated units: 4
84
- Control units: 4
85
- R-squared: 0.9123
82
+ Observations: 8
83
+ Treated units: 4
84
+ Control units: 4
85
+ R-squared: 0.9055
86
86
 
87
87
  ----------------------------------------------------------------------
88
- Parameter Estimate Std. Err. t-stat P>|t|
88
+ Parameter Estimate Std. Err. t-stat P>|t|
89
89
  ----------------------------------------------------------------------
90
- ATT 3.5000 1.2583 2.782 0.0367
90
+ ATT 3.0000 1.7321 1.732 0.1583
91
91
  ----------------------------------------------------------------------
92
92
 
93
- 95% Confidence Interval: [0.3912, 6.6088]
93
+ 95% Confidence Interval: [-1.8089, 7.8089]
94
94
 
95
95
  Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
96
96
  ======================================================================
@@ -102,9 +102,14 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
102
102
  - **Pythonic results**: Easy access to coefficients, standard errors, and confidence intervals
103
103
  - **Multiple interfaces**: Column names or R-style formulas
104
104
  - **Robust inference**: Heteroskedasticity-robust (HC1) and cluster-robust standard errors
105
+ - **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
105
106
  - **Panel data support**: Two-way fixed effects estimator for panel designs
106
107
  - **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
108
+ - **Staggered adoption**: Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
107
109
  - **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
110
+ - **Event study plots**: Publication-ready visualization of treatment effects
111
+ - **Parallel trends testing**: Multiple methods including equivalence tests
112
+ - **Placebo tests**: Comprehensive diagnostics including fake timing, fake group, permutation, and leave-one-out tests
108
113
  - **Data prep utilities**: Helper functions for common data preparation tasks
109
114
 
110
115
  ## Data Preparation
@@ -342,6 +347,79 @@ cohort_data = aggregate_to_cohorts(
342
347
  # Result: mean outcome by treatment group and period
343
348
  ```
344
349
 
350
+ ### Rank Control Units
351
+
352
+ Select the best control units for DiD or Synthetic DiD analysis by ranking them based on pre-treatment outcome similarity:
353
+
354
+ ```python
355
+ from diff_diff import rank_control_units, generate_did_data
356
+
357
+ # Generate sample data
358
+ data = generate_did_data(n_units=50, n_periods=6, seed=42)
359
+
360
+ # Rank control units by their similarity to treated units
361
+ ranking = rank_control_units(
362
+ data,
363
+ unit_column='unit',
364
+ time_column='period',
365
+ outcome_column='outcome',
366
+ treatment_column='treated',
367
+ n_top=10 # Return top 10 controls
368
+ )
369
+
370
+ print(ranking[['unit', 'quality_score', 'pre_trend_rmse']])
371
+ ```
372
+
373
+ Output:
374
+ ```
375
+ unit quality_score pre_trend_rmse
376
+ 0 35 1.0000 0.4521
377
+ 1 42 0.9234 0.5123
378
+ 2 28 0.8876 0.5892
379
+ ...
380
+ ```
381
+
382
+ With covariates for matching:
383
+
384
+ ```python
385
+ # Add covariate-based matching
386
+ ranking = rank_control_units(
387
+ data,
388
+ unit_column='unit',
389
+ time_column='period',
390
+ outcome_column='outcome',
391
+ treatment_column='treated',
392
+ covariates=['size', 'age'], # Match on these too
393
+ outcome_weight=0.7, # 70% weight on outcome trends
394
+ covariate_weight=0.3 # 30% weight on covariate similarity
395
+ )
396
+ ```
397
+
398
+ Filter data for SyntheticDiD using top controls:
399
+
400
+ ```python
401
+ from diff_diff import SyntheticDiD
402
+
403
+ # Get top control units
404
+ top_controls = ranking['unit'].tolist()
405
+
406
+ # Filter data to treated + top controls
407
+ filtered_data = data[
408
+ (data['treated'] == 1) | (data['unit'].isin(top_controls))
409
+ ]
410
+
411
+ # Fit SyntheticDiD with selected controls
412
+ sdid = SyntheticDiD()
413
+ results = sdid.fit(
414
+ filtered_data,
415
+ outcome='outcome',
416
+ treatment='treated',
417
+ unit='unit',
418
+ time='period',
419
+ post_periods=[3, 4, 5]
420
+ )
421
+ ```
422
+
345
423
  ## Usage
346
424
 
347
425
  ### Basic DiD with Column Names
@@ -447,6 +525,36 @@ results = did.fit(
447
525
  )
448
526
  ```
449
527
 
528
+ ### Wild Cluster Bootstrap
529
+
530
+ When you have few clusters (<50), standard cluster-robust SEs are biased. Wild cluster bootstrap provides valid inference even with 5-10 clusters.
531
+
532
+ ```python
533
+ # Use wild bootstrap for inference
534
+ did = DifferenceInDifferences(
535
+ cluster='state',
536
+ inference='wild_bootstrap',
537
+ n_bootstrap=999,
538
+ bootstrap_weights='rademacher', # or 'webb' for <10 clusters, 'mammen'
539
+ seed=42
540
+ )
541
+ results = did.fit(data, outcome='y', treatment='treated', time='post')
542
+
543
+ # Results include bootstrap-based SE and p-value
544
+ print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})")
545
+ print(f"P-value: {results.p_value:.4f}")
546
+ print(f"95% CI: {results.conf_int}")
547
+ print(f"Inference method: {results.inference_method}")
548
+ print(f"Number of clusters: {results.n_clusters}")
549
+ ```
550
+
551
+ **Weight types:**
552
+ - `'rademacher'` - Default, ±1 with p=0.5, good for most cases
553
+ - `'webb'` - 6-point distribution, recommended for <10 clusters
554
+ - `'mammen'` - Two-point distribution, alternative to Rademacher
555
+
556
+ Works with `DifferenceInDifferences` and `TwoWayFixedEffects` estimators.
557
+
450
558
  ### Two-Way Fixed Effects (Panel Data)
451
559
 
452
560
  ```python
@@ -522,6 +630,148 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
522
630
  ================================================================================
523
631
  ```
524
632
 
633
+ ### Staggered Difference-in-Differences (Callaway-Sant'Anna)
634
+
635
+ 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.
636
+
637
+ ```python
638
+ from diff_diff import CallawaySantAnna
639
+
640
+ # Panel data with staggered treatment
641
+ # 'first_treat' = period when unit was first treated (0 if never treated)
642
+ cs = CallawaySantAnna()
643
+ results = cs.fit(
644
+ panel_data,
645
+ outcome='sales',
646
+ unit='firm_id',
647
+ time='year',
648
+ first_treat='first_treat', # 0 for never-treated, else first treatment year
649
+ aggregate='event_study' # Compute event study effects
650
+ )
651
+
652
+ # View results
653
+ results.print_summary()
654
+
655
+ # Access group-time effects ATT(g,t)
656
+ for (group, time), effect in results.group_time_effects.items():
657
+ print(f"Cohort {group}, Period {time}: {effect['effect']:.3f}")
658
+
659
+ # Event study effects (averaged by relative time)
660
+ for rel_time, effect in results.event_study_effects.items():
661
+ print(f"e={rel_time}: {effect['effect']:.3f} (SE: {effect['se']:.3f})")
662
+
663
+ # Convert to DataFrame
664
+ df = results.to_dataframe(level='event_study')
665
+ ```
666
+
667
+ Output:
668
+ ```
669
+ =====================================================================================
670
+ Callaway-Sant'Anna Staggered Difference-in-Differences Results
671
+ =====================================================================================
672
+
673
+ Total observations: 600
674
+ Treated units: 35
675
+ Control units: 15
676
+ Treatment cohorts: 3
677
+ Time periods: 8
678
+ Control group: never_treated
679
+
680
+ -------------------------------------------------------------------------------------
681
+ Overall Average Treatment Effect on the Treated
682
+ -------------------------------------------------------------------------------------
683
+ Parameter Estimate Std. Err. t-stat P>|t| Sig.
684
+ -------------------------------------------------------------------------------------
685
+ ATT 2.5000 0.3521 7.101 0.0000 ***
686
+ -------------------------------------------------------------------------------------
687
+
688
+ 95% Confidence Interval: [1.8099, 3.1901]
689
+
690
+ -------------------------------------------------------------------------------------
691
+ Event Study (Dynamic) Effects
692
+ -------------------------------------------------------------------------------------
693
+ Rel. Period Estimate Std. Err. t-stat P>|t| Sig.
694
+ -------------------------------------------------------------------------------------
695
+ 0 2.1000 0.4521 4.645 0.0000 ***
696
+ 1 2.5000 0.4123 6.064 0.0000 ***
697
+ 2 2.8000 0.5234 5.349 0.0000 ***
698
+ -------------------------------------------------------------------------------------
699
+
700
+ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
701
+ =====================================================================================
702
+ ```
703
+
704
+ **When to use Callaway-Sant'Anna vs TWFE:**
705
+
706
+ | Scenario | Use TWFE | Use Callaway-Sant'Anna |
707
+ |----------|----------|------------------------|
708
+ | All units treated at same time | ✓ | ✓ |
709
+ | Staggered adoption, homogeneous effects | ✓ | ✓ |
710
+ | Staggered adoption, heterogeneous effects | ✗ | ✓ |
711
+ | Need event study with staggered timing | ✗ | ✓ |
712
+ | Fewer than ~20 treated units | ✓ | Depends on design |
713
+
714
+ **Parameters:**
715
+
716
+ ```python
717
+ CallawaySantAnna(
718
+ control_group='never_treated', # or 'not_yet_treated'
719
+ anticipation=0, # Periods before treatment with effects
720
+ estimation_method='dr', # 'dr', 'ipw', or 'reg'
721
+ alpha=0.05, # Significance level
722
+ cluster=None, # Column for cluster SEs
723
+ n_bootstrap=0, # Must be 0 (bootstrap not yet implemented)
724
+ seed=None # Random seed
725
+ )
726
+ ```
727
+
728
+ **Current limitations:**
729
+ - Bootstrap inference (`n_bootstrap > 0`) is not yet implemented
730
+ - Covariate adjustment for conditional parallel trends is not yet implemented
731
+
732
+ ### Event Study Visualization
733
+
734
+ Create publication-ready event study plots:
735
+
736
+ ```python
737
+ from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
738
+
739
+ # From MultiPeriodDiD
740
+ did = MultiPeriodDiD()
741
+ results = did.fit(data, outcome='y', treatment='treated',
742
+ time='period', post_periods=[3, 4, 5])
743
+ plot_event_study(results, title="Treatment Effects Over Time")
744
+
745
+ # From CallawaySantAnna (with event study aggregation)
746
+ cs = CallawaySantAnna()
747
+ results = cs.fit(data, outcome='y', unit='unit', time='period',
748
+ first_treat='first_treat', aggregate='event_study')
749
+ plot_event_study(results, title="Staggered DiD Event Study")
750
+
751
+ # From a DataFrame
752
+ df = pd.DataFrame({
753
+ 'period': [-2, -1, 0, 1, 2],
754
+ 'effect': [0.1, 0.05, 0.0, 2.5, 2.8],
755
+ 'se': [0.3, 0.25, 0.0, 0.4, 0.45]
756
+ })
757
+ plot_event_study(df, reference_period=0)
758
+
759
+ # With customization
760
+ ax = plot_event_study(
761
+ results,
762
+ title="Dynamic Treatment Effects",
763
+ xlabel="Years Relative to Treatment",
764
+ ylabel="Effect on Sales ($1000s)",
765
+ color="#2563eb",
766
+ marker="o",
767
+ shade_pre=True, # Shade pre-treatment region
768
+ show_zero_line=True, # Horizontal line at y=0
769
+ show_reference_line=True, # Vertical line at reference period
770
+ figsize=(10, 6),
771
+ show=False # Don't call plt.show(), return axes
772
+ )
773
+ ```
774
+
525
775
  ### Synthetic Difference-in-Differences
526
776
 
527
777
  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.
@@ -754,6 +1004,120 @@ print(f"TOST p-value: {results['tost_p_value']:.4f}")
754
1004
  print(f"Trends equivalent: {results['equivalent']}")
755
1005
  ```
756
1006
 
1007
+ ### Placebo Tests
1008
+
1009
+ Placebo tests help validate the parallel trends assumption by checking whether effects appear where they shouldn't (before treatment or in untreated groups).
1010
+
1011
+ **Fake timing test:**
1012
+
1013
+ ```python
1014
+ from diff_diff import run_placebo_test
1015
+
1016
+ # Test: Is there an effect before treatment actually occurred?
1017
+ # Actual treatment is at period 3 (post_periods=[3, 4, 5])
1018
+ # We test if a "fake" treatment at period 1 shows an effect
1019
+ results = run_placebo_test(
1020
+ data,
1021
+ outcome='outcome',
1022
+ treatment='treated',
1023
+ time='period',
1024
+ test_type='fake_timing',
1025
+ fake_treatment_period=1, # Pretend treatment was in period 1
1026
+ post_periods=[3, 4, 5] # Actual post-treatment periods
1027
+ )
1028
+
1029
+ print(results.summary())
1030
+ # If parallel trends hold, placebo_effect should be ~0 and not significant
1031
+ print(f"Placebo effect: {results.placebo_effect:.3f} (p={results.p_value:.3f})")
1032
+ print(f"Is significant (bad): {results.is_significant}")
1033
+ ```
1034
+
1035
+ **Fake group test:**
1036
+
1037
+ ```python
1038
+ # Test: Is there an effect among never-treated units?
1039
+ # Get some control unit IDs to use as "fake treated"
1040
+ control_units = data[data['treated'] == 0]['firm_id'].unique()[:5]
1041
+
1042
+ results = run_placebo_test(
1043
+ data,
1044
+ outcome='outcome',
1045
+ treatment='treated',
1046
+ time='period',
1047
+ unit='firm_id',
1048
+ test_type='fake_group',
1049
+ fake_treatment_group=list(control_units), # List of control unit IDs
1050
+ post_periods=[3, 4, 5]
1051
+ )
1052
+ ```
1053
+
1054
+ **Permutation test:**
1055
+
1056
+ ```python
1057
+ # Randomly reassign treatment and compute distribution of effects
1058
+ # Note: requires binary post indicator (use 'post' column, not 'period')
1059
+ results = run_placebo_test(
1060
+ data,
1061
+ outcome='outcome',
1062
+ treatment='treated',
1063
+ time='post', # Binary post-treatment indicator
1064
+ unit='firm_id',
1065
+ test_type='permutation',
1066
+ n_permutations=1000,
1067
+ seed=42
1068
+ )
1069
+
1070
+ print(f"Original effect: {results.original_effect:.3f}")
1071
+ print(f"Permutation p-value: {results.p_value:.4f}")
1072
+ # Low p-value indicates the effect is unlikely to be due to chance
1073
+ ```
1074
+
1075
+ **Leave-one-out sensitivity:**
1076
+
1077
+ ```python
1078
+ # Test sensitivity to individual treated units
1079
+ # Note: requires binary post indicator (use 'post' column, not 'period')
1080
+ results = run_placebo_test(
1081
+ data,
1082
+ outcome='outcome',
1083
+ treatment='treated',
1084
+ time='post', # Binary post-treatment indicator
1085
+ unit='firm_id',
1086
+ test_type='leave_one_out'
1087
+ )
1088
+
1089
+ # Check if any single unit drives the result
1090
+ print(results.leave_one_out_effects) # Effect when each unit is dropped
1091
+ ```
1092
+
1093
+ **Run all placebo tests:**
1094
+
1095
+ ```python
1096
+ from diff_diff import run_all_placebo_tests
1097
+
1098
+ # Comprehensive diagnostic suite
1099
+ # Note: This function runs fake_timing tests on pre-treatment periods.
1100
+ # The permutation and leave_one_out tests require a binary post indicator,
1101
+ # so they may return errors if the data uses multi-period time column.
1102
+ all_results = run_all_placebo_tests(
1103
+ data,
1104
+ outcome='outcome',
1105
+ treatment='treated',
1106
+ time='period',
1107
+ unit='firm_id',
1108
+ pre_periods=[0, 1, 2],
1109
+ post_periods=[3, 4, 5],
1110
+ n_permutations=500,
1111
+ seed=42
1112
+ )
1113
+
1114
+ for test_name, result in all_results.items():
1115
+ if hasattr(result, 'p_value'):
1116
+ print(f"{test_name}: p={result.p_value:.3f}, significant={result.is_significant}")
1117
+ elif isinstance(result, dict) and 'error' in result:
1118
+ print(f"{test_name}: Error - {result['error']}")
1119
+ ```
1120
+
757
1121
  ## API Reference
758
1122
 
759
1123
  ### DifferenceInDifferences
@@ -1061,6 +1425,31 @@ aggregate_to_cohorts(
1061
1425
  )
1062
1426
  ```
1063
1427
 
1428
+ #### rank_control_units
1429
+
1430
+ ```python
1431
+ rank_control_units(
1432
+ data, # Panel data in long format
1433
+ unit_column, # Unit identifier column
1434
+ time_column, # Time period column
1435
+ outcome_column, # Outcome variable column
1436
+ treatment_column=None, # Treatment indicator column (0/1)
1437
+ treated_units=None, # Explicit list of treated unit IDs
1438
+ pre_periods=None, # Pre-treatment periods (default: first half)
1439
+ covariates=None, # Covariate columns for matching
1440
+ outcome_weight=0.7, # Weight for outcome trend similarity (0-1)
1441
+ covariate_weight=0.3, # Weight for covariate distance (0-1)
1442
+ exclude_units=None, # Units to exclude from control pool
1443
+ require_units=None, # Units that must appear in output
1444
+ n_top=None, # Return only top N controls
1445
+ suggest_treatment_candidates=False, # Identify treatment candidates
1446
+ n_treatment_candidates=5, # Number of treatment candidates
1447
+ lambda_reg=0.0 # Regularization for synthetic weights
1448
+ )
1449
+ ```
1450
+
1451
+ Returns DataFrame with columns: `unit`, `quality_score`, `outcome_trend_score`, `covariate_score`, `synthetic_weight`, `pre_trend_rmse`, `is_required`.
1452
+
1064
1453
  ## Requirements
1065
1454
 
1066
1455
  - Python >= 3.9
@@ -1108,6 +1497,18 @@ This library implements methods from the following scholarly works:
1108
1497
 
1109
1498
  - **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
1499
 
1500
+ ### Wild Cluster Bootstrap
1501
+
1502
+ - **Cameron, A. C., Gelbach, J. B., & Miller, D. L. (2008).** "Bootstrap-Based Improvements for Inference with Clustered Errors." *The Review of Economics and Statistics*, 90(3), 414-427. [https://doi.org/10.1162/rest.90.3.414](https://doi.org/10.1162/rest.90.3.414)
1503
+
1504
+ - **Webb, M. D. (2014).** "Reworking Wild Bootstrap Based Inference for Clustered Errors." Queen's Economics Department Working Paper No. 1315. [https://www.econ.queensu.ca/sites/econ.queensu.ca/files/qed_wp_1315.pdf](https://www.econ.queensu.ca/sites/econ.queensu.ca/files/qed_wp_1315.pdf)
1505
+
1506
+ - **MacKinnon, J. G., & Webb, M. D. (2018).** "The Wild Bootstrap for Few (Treated) Clusters." *The Econometrics Journal*, 21(2), 114-135. [https://doi.org/10.1111/ectj.12107](https://doi.org/10.1111/ectj.12107)
1507
+
1508
+ ### Placebo Tests and DiD Diagnostics
1509
+
1510
+ - **Bertrand, M., Duflo, E., & Mullainathan, S. (2004).** "How Much Should We Trust Differences-in-Differences Estimates?" *The Quarterly Journal of Economics*, 119(1), 249-275. [https://doi.org/10.1162/003355304772839588](https://doi.org/10.1162/003355304772839588)
1511
+
1111
1512
  ### Synthetic Control Method
1112
1513
 
1113
1514
  - **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)
@@ -1126,14 +1527,20 @@ This library implements methods from the following scholarly works:
1126
1527
 
1127
1528
  - **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
1529
 
1530
+ - **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)
1531
+
1129
1532
  ### Multi-Period and Staggered Adoption
1130
1533
 
1131
1534
  - **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
1535
 
1536
+ - **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)
1537
+
1133
1538
  - **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
1539
 
1135
1540
  - **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
1541
 
1542
+ - **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)
1543
+
1137
1544
  ### General Causal Inference
1138
1545
 
1139
1546
  - **Imbens, G. W., & Rubin, D. B. (2015).** *Causal Inference for Statistics, Social, and Biomedical Sciences: An Introduction*. Cambridge University Press.