@@ -70,7 +70,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
7070- ** Wild cluster bootstrap** : Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
7171- ** Panel data support** : Two-way fixed effects estimator for panel designs
7272- ** Multi-period analysis** : Event-study style DiD with period-specific treatment effects
73- - ** Staggered adoption** : Callaway-Sant'Anna (2021) estimator for heterogeneous treatment timing
73+ - ** Staggered adoption** : Callaway-Sant'Anna (2021) and Sun-Abraham (2021) estimators for heterogeneous treatment timing
7474- ** Synthetic DiD** : Combined DiD with synthetic control for improved robustness
7575- ** Event study plots** : Publication-ready visualization of treatment effects
7676- ** Parallel trends testing** : Multiple methods including equivalence tests
@@ -87,7 +87,7 @@ We provide Jupyter notebook tutorials in `docs/tutorials/`:
8787| Notebook | Description |
8888| ----------| -------------|
8989| ` 01_basic_did.ipynb ` | Basic 2x2 DiD, formula interface, covariates, fixed effects, cluster-robust SE, wild bootstrap |
90- | ` 02_staggered_did.ipynb ` | Staggered adoption with Callaway-Sant'Anna, group-time effects, aggregation methods, Bacon decomposition |
90+ | ` 02_staggered_did.ipynb ` | Staggered adoption with Callaway-Sant'Anna and Sun-Abraham , group-time effects, aggregation methods, Bacon decomposition |
9191| ` 03_synthetic_did.ipynb ` | Synthetic DiD, unit/time weights, inference methods, regularization |
9292| ` 04_parallel_trends.ipynb ` | Testing parallel trends, equivalence tests, placebo tests, diagnostics |
9393| ` 05_honest_did.ipynb ` | Honest DiD sensitivity analysis, bounds, breakdown values, visualization |
@@ -762,12 +762,115 @@ results = cs.fit(
762762)
763763```
764764
765+ ### Sun-Abraham Interaction-Weighted Estimator
766+
767+ The Sun-Abraham (2021) estimator provides an alternative to Callaway-Sant'Anna using an interaction-weighted (IW) regression approach. Running both estimators serves as a useful robustness check—when they agree, results are more credible.
768+
769+ ``` python
770+ from diff_diff import SunAbraham
771+
772+ # Basic usage
773+ sa = SunAbraham()
774+ results = sa.fit(
775+ panel_data,
776+ outcome = ' sales' ,
777+ unit = ' firm_id' ,
778+ time = ' year' ,
779+ first_treat = ' first_treat' # 0 for never-treated, else first treatment year
780+ )
781+
782+ # View results
783+ results.print_summary()
784+
785+ # Event study effects (by relative time to treatment)
786+ for rel_time, effect in results.event_study_effects.items():
787+ print (f " e= { rel_time} : { effect[' effect' ]:.3f } (SE: { effect[' se' ]:.3f } ) " )
788+
789+ # Overall ATT
790+ print (f " Overall ATT: { results.overall_att:.3f } (SE: { results.overall_se:.3f } ) " )
791+
792+ # Cohort weights (how each cohort contributes to each event-time estimate)
793+ for rel_time, weights in results.cohort_weights.items():
794+ print (f " e= { rel_time} : { weights} " )
795+ ```
796+
797+ ** Parameters:**
798+
799+ ``` python
800+ SunAbraham(
801+ control_group = ' never_treated' , # or 'not_yet_treated'
802+ anticipation = 0 , # Periods before treatment with effects
803+ alpha = 0.05 , # Significance level
804+ cluster = None , # Column for cluster SEs
805+ n_bootstrap = 0 , # Bootstrap iterations (0 = analytical SEs)
806+ bootstrap_weights = ' rademacher' , # 'rademacher', 'mammen', or 'webb'
807+ seed = None # Random seed
808+ )
809+ ```
810+
811+ ** Bootstrap inference:**
812+
813+ ``` python
814+ # Bootstrap inference with 999 iterations
815+ sa = SunAbraham(
816+ n_bootstrap = 999 ,
817+ bootstrap_weights = ' rademacher' ,
818+ seed = 42
819+ )
820+ results = sa.fit(
821+ data,
822+ outcome = ' sales' ,
823+ unit = ' firm_id' ,
824+ time = ' year' ,
825+ first_treat = ' first_treat'
826+ )
827+
828+ # Access bootstrap results
829+ print (f " Overall ATT: { results.overall_att:.3f } " )
830+ print (f " Bootstrap SE: { results.bootstrap_results.overall_att_se:.3f } " )
831+ print (f " Bootstrap 95% CI: { results.bootstrap_results.overall_att_ci} " )
832+ print (f " Bootstrap p-value: { results.bootstrap_results.overall_att_p_value:.4f } " )
833+ ```
834+
835+ ** When to use Sun-Abraham vs Callaway-Sant'Anna:**
836+
837+ | Aspect | Sun-Abraham | Callaway-Sant'Anna |
838+ | --------| -------------| -------------------|
839+ | Approach | Interaction-weighted regression | 2x2 DiD aggregation |
840+ | Efficiency | More efficient under homogeneous effects | More robust to heterogeneity |
841+ | Weighting | Weights by cohort share at each relative time | Weights by sample size |
842+ | Use case | Robustness check, regression-based inference | Primary staggered DiD estimator |
843+
844+ ** Both estimators should give similar results when:**
845+ - Treatment effects are relatively homogeneous across cohorts
846+ - Parallel trends holds
847+
848+ ** Running both as robustness check:**
849+
850+ ``` python
851+ from diff_diff import CallawaySantAnna, SunAbraham
852+
853+ # Callaway-Sant'Anna
854+ cs = CallawaySantAnna()
855+ cs_results = cs.fit(data, outcome = ' y' , unit = ' unit' , time = ' time' , first_treat = ' first_treat' )
856+
857+ # Sun-Abraham
858+ sa = SunAbraham()
859+ sa_results = sa.fit(data, outcome = ' y' , unit = ' unit' , time = ' time' , first_treat = ' first_treat' )
860+
861+ # Compare
862+ print (f " Callaway-Sant'Anna ATT: { cs_results.overall_att:.3f } " )
863+ print (f " Sun-Abraham ATT: { sa_results.overall_att:.3f } " )
864+
865+ # If results differ substantially, investigate heterogeneity
866+ ```
867+
765868### Event Study Visualization
766869
767870Create publication-ready event study plots:
768871
769872``` python
770- from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna
873+ from diff_diff import plot_event_study, MultiPeriodDiD, CallawaySantAnna, SunAbraham
771874
772875# From MultiPeriodDiD
773876did = MultiPeriodDiD()
@@ -779,7 +882,13 @@ plot_event_study(results, title="Treatment Effects Over Time")
779882cs = CallawaySantAnna()
780883results = cs.fit(data, outcome = ' y' , unit = ' unit' , time = ' period' ,
781884 first_treat = ' first_treat' , aggregate = ' event_study' )
782- plot_event_study(results, title = " Staggered DiD Event Study" )
885+ plot_event_study(results, title = " Staggered DiD Event Study (CS)" )
886+
887+ # From SunAbraham
888+ sa = SunAbraham()
889+ results = sa.fit(data, outcome = ' y' , unit = ' unit' , time = ' period' ,
890+ first_treat = ' first_treat' )
891+ plot_event_study(results, title = " Staggered DiD Event Study (SA)" )
783892
784893# From a DataFrame
785894df = pd.DataFrame({
@@ -1410,6 +1519,63 @@ SyntheticDiD(
14101519| ` get_unit_weights_df() ` | Get unit weights as DataFrame |
14111520| ` get_time_weights_df() ` | Get time weights as DataFrame |
14121521
1522+ ### SunAbraham
1523+
1524+ ``` python
1525+ SunAbraham(
1526+ control_group = ' never_treated' , # or 'not_yet_treated'
1527+ anticipation = 0 , # Periods of anticipation effects
1528+ alpha = 0.05 , # Significance level for CIs
1529+ cluster = None , # Column for cluster-robust SEs
1530+ n_bootstrap = 0 , # Bootstrap iterations (0 = analytical SEs)
1531+ bootstrap_weights = ' rademacher' , # 'rademacher', 'mammen', or 'webb'
1532+ seed = None # Random seed
1533+ )
1534+ ```
1535+
1536+ ** fit() Parameters:**
1537+
1538+ | Parameter | Type | Description |
1539+ | -----------| ------| -------------|
1540+ | ` data ` | DataFrame | Panel data |
1541+ | ` outcome ` | str | Outcome variable column name |
1542+ | ` unit ` | str | Unit identifier column |
1543+ | ` time ` | str | Time period column |
1544+ | ` first_treat ` | str | Column with first treatment period (0 for never-treated) |
1545+ | ` covariates ` | list | Covariate column names |
1546+ | ` min_pre_periods ` | int | Minimum pre-treatment periods to include |
1547+ | ` min_post_periods ` | int | Minimum post-treatment periods to include |
1548+
1549+ ### SunAbrahamResults
1550+
1551+ ** Attributes:**
1552+
1553+ | Attribute | Description |
1554+ | -----------| -------------|
1555+ | ` event_study_effects ` | Dict mapping relative time to effect info |
1556+ | ` overall_att ` | Overall average treatment effect |
1557+ | ` overall_se ` | Standard error of overall ATT |
1558+ | ` overall_t_stat ` | T-statistic for overall ATT |
1559+ | ` overall_p_value ` | P-value for overall ATT |
1560+ | ` overall_conf_int ` | Confidence interval for overall ATT |
1561+ | ` cohort_weights ` | Dict mapping relative time to cohort weights |
1562+ | ` groups ` | List of treatment cohorts |
1563+ | ` time_periods ` | List of all time periods |
1564+ | ` n_obs ` | Total number of observations |
1565+ | ` n_treated_units ` | Number of ever-treated units |
1566+ | ` n_control_units ` | Number of never-treated units |
1567+ | ` is_significant ` | Boolean for significance at alpha |
1568+ | ` significance_stars ` | String of significance stars |
1569+ | ` bootstrap_results ` | SABootstrapResults (if bootstrap enabled) |
1570+
1571+ ** Methods:**
1572+
1573+ | Method | Description |
1574+ | --------| -------------|
1575+ | ` summary(alpha) ` | Get formatted summary string |
1576+ | ` print_summary(alpha) ` | Print summary to stdout |
1577+ | ` to_dataframe(level) ` | Convert to DataFrame ('event_study' or 'cohort') |
1578+
14131579### HonestDiD
14141580
14151581``` python
0 commit comments