Package: paneleventstudy
paneleventstudyThis Python package implements the panel (and single entity) event study models, covering the naive two-way fixed effects implementation, and the interaction-weighted implementation from Sun and Abraham (2021) (derived fromhttps://github.com/lsun20/EventStudyInteract).The package includes three sets of functions:Data cleaning: Functions to prepare data frames for the analytical set of functions, e.g., ensuring that they are in the right format, and have the right columns (with the right content)Analytical: Direct implementation of the event study modelsUtilities: Tools to assist the user in setting up input-output flowsInstallationpip install paneleventstudyExamplesRefer to the JuPyTeR notebookexample_paneleventstudy.ipynbImplementation Notes (Data Cleaning)Counting and dropping missing observationsDocumentationpaneleventstudy.dropmissing(data,event)Parametersdata:pandas dataframeevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)OutputA copy ofdatawith rows corresponding to missingeventdroppedDisplay on the interface the number of rows indata, and the number of rows in the output data frameChecking if input dataframe is a balanced panelMost panel event study methods in the literature, and is the case at present for all methods covered in this package, only work with balanced panel data.
That is to say that all entities in the data set must have the same number of time periods.
This package checks if the input data is indeed a balanced panel with entities $i \in [0, 1, ..., N-1, N]$ and time periods $t \in [0, 1, 2, ..., T-1, T]$ in two steps.Check if all entities $i$ have the same number of time periods
$$L(\mathbf{t}_{i}) = L_T \ \forall \ i \ for \ L_T \in \mathcal{N}^{+} $$Optionally, if the calendar time variable in the input data frame is numeric, further check if the smallest and largest time values are the same for all entities $i$$$\min(\mathbf{t}_{i}) = L_Tmin \ \forall \ i \ for \ L_Tmin \in \mathcal{N}^{0+} $$$$\max(\mathbf{t}_{i}) = L_Tmax \ \forall \ i \ for \ L_Tmax \in \mathcal{N}^{+} $$Documentationpaneleventstudy.balancepanel(data,group,event,calendartime,check_minmax=True)Parametersdata:pandas dataframegroup:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)calendartime:
Integers or integers matching the label of the column indatacontaining calendar times going from the earliest to last time period; this can be user-fed or generated fromgencalendartime_numericscalendartime.check_minmax:
Boolean to trigger option for a deeper check, which verifies if all entities ingrouphave the same minimum and maximum values incalendartime; default isTrue, and can be used when thecalendartimecolumn is generted fromgencalendartime_numericscalendartime, or are already preset as integersOutputA Boolean indicating ifdatais balanced.Generate column indicating control groups (never-treated / last-treated)In the difference in difference (DiD) methodology, which event studies are a variant of, if treatment is truly exogenous, the treatment effect is estimated by comparing the average outcomes of the treated group (received treatment) against the control group (did not receive treatment).Discussing endogeneity aside, in panel event studies, and indeed dynamic DiD, it is possible for no groups to be never-treated, e.g., in staggered DiD setups.
Choosing the right control group is essential in establishing the right counterfactual, on which unbiased or consistent treatment effect estimates are conditioned on.
In these cases, we may want to use the last-treated group as a control group. This was argued prominently in recent DiD papers, such asCallaway and Sant'AnnaandSun and Abraham (2021).This function tells us which group(s) is / are the control groups, whether never-treated or last-treated, which is essential for the analytical functions later.Documentationpaneleventstudy.identifycontrols(data,group,event)Parametersdata:pandas dataframegroup:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)OutputA copy ofdatawith a new column labelledcontrol_groupindicating if the entity ingroupis a control group (never-treated or last-treated).Generate relative time column (treatment onset = 0)Event studies methodologies essentially estimate the dynamic treatment effect relative to the onset of treatment (i.e., before and after treatment).
This is akin to asking "what is the effect of treatment $D$ on outcome $Y$ at different timepoints relative to the treatment timing?".This function generates a column containing these relative times from two sets of information:Calendar time; andWhen the treatment or event happenedDocumentationpaneleventstudy.genreltime(data,group,event,calendartime,reltime='reltime',check_balance=True)Parametersdata:pandas dataframegroup:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)calendartime:
Integers matching the label of the column indatacontaining calendar times going from 0 (earliest time period) to T (last time period); this can be generated fromgencalendartime_numericscalendartimereltime:
String to be used as the label of a new column containing relative times going from -L to +K as integers, with 0 being the timing of treatment onsetcheck_balance:
Checks ifdatais a balanced panel; default option isTrueOutputA copy ofdatawith a new column labelledreltimecontaining the relative times for all calendar times incalendarby entities ingroup.Generate column indicating treatment cohortsSun and Abraham (2021)'s interaction-weighted event study methodology requires (1) the estimation of cohort-specific treatment effects, and (2) cohort shares by relative times.
To do this, the methodology requires an identifier for groups that were treated in the same calendar time.Documentationpaneleventstudy.gencohort(data,group,event,calendartime,cohort='cohort',check_balance=True)Parametersdata:pandas dataframegroup:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)calendartime:
Integers matching the label of the column indatacontaining calendar times going from 0 (earliest time period) to T (last time period); this can be generated fromgencalendartime_numericscalendartimecohort:
String to be used as the label of a new column containing the treatment cohort of respective entities ingroup; default is'cohort'check_balance:
Checks ifdatais a balanced panel; default option isTrueOutputA copy ofdatawith a new column labelledcohortindicating the treatment cohort that the entitiesgroupbelong to.Generate calendar time with integersFor generalise across the infinitely many possible formats that calendar times can be presented in (e.g., miliseconds, seconds, days, months, quarters, years, or even custom ones), calendar times can be converted into numerics.
This eases computation in the rest of the package, by converting the calendar time column into integers starting from 0 (earliest) to T (latest).Documentationpaneleventstudy.gencalendartime_numerics(data,group,event,calendartime,calendartime_numerics='ct')Parametersdata:pandas dataframegroup:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)calendartime:
Column matching the label of the column indatacontaining calendar times going from the earliest time period to the last time periodcalendartime_numerics:
String to be used as the label of a new column containing the calendar times converted into nonnegative integers with 0 being the earliest, and T being the latest periodOutputA copy ofdatawith a new column labelledcalendartime_numericswith numeric version ofcalendartime, which can then be passed to the analytical functions.Identify collinear or invariant columnsThe basic functional form of estimating equations in the DiD and event study methodology is a linear regression, which requires variables in the RHS of the equation to not be multicollinear, or invariant.
This function checks if this is indeed the case.Documentationpaneleventstudy.checkcollinear(data,rhs)Parametersdata:pandas dataframerhs:
A list containing strings matching the labels of the columns indatato be checked for collinearity and invariance; precedence goes to columns in the rightmost ofrhs(if two columns are collinear, the one appearing later inrhsis not included in the output)OutputA list of labels inrhswhich should be dropped to avoid multicollinearity or invariance inrhscolumns indata.Identify linearly dependent columnsThe basic functional form of estimating equations in the DiD and event study methodology is a linear regression, which requires the matrix containing the variables in the RHS of the equation to satisfy full column rank.
This function checks if this is indeed the case.Documentationpaneleventstudy.checkfullrank(data,rhs,intercept='Intercept')Parametersdata:pandas dataframerhs:
A list containing strings matching the labels of the columns indatato be checked for full rank; precedence goes to columns in the rightmost ofrhsintercept:
String containing the label of the intercept column (column of numerics 1), which will be given precedence in the procedure; set asNoneif no intercepts are contained indata, and the default is'Intercept', which is the default when usingpatsy.dmatrices()OutputA list of labels inrhswhich should be dropped to for the matrix containingrhscolumns indatato satisfy full rank.Implementation Notes (Analytical)Naive Two-Way Fixed Effects (TWFE) Panel Event StudyEstimates dynamic treatment effects using a standard TWFE model.
Specifically, we are interested in estimating $\beta_{l}$, the coefficients on leads and lags of treatment dummies, where $l$ is relative time as inSun and Abraham (2021), i.e., the time period relative to treatment onset.
$l=0$ refers to when the treatment was applied to entity $i$.$D_{i,t}^{l}$ are dummies switching on if entity $i$ is in calendar time $t$, and is $l$ periods relative to the treatment onset. That is also to say that $D_{i, t}^{l} \ \forall \ t, l$ never-treated entities will take values $0$.
A TWFE regression model includes entity fixed effects ($\alpha_i$), and time fixed effects ($\alpha_t$). $\mathbf{X_{i, t}}$ is an optional vector of time-varying (within-entity) controls.
$\epsilon_{i, t}$ are the errors.$$Y_{i,t} = \alpha_i + \alpha_t + \sum_{l=-K}^{-2} \beta_{l} D_{i, t}^{l} + \sum_{l=0}^{M} \beta_{l} D_{i, t}^{l} + \mathbf{X_{i, t} \gamma} + \epsilon_{i, t}$$Documentationpaneleventstudy.naivetwfe_eventstudy(data,outcome,event,group,reltime,calendartime,covariates,vcov_type='robust',check_balance=True)Parametersdata:pandas dataframeoutcome:
String matching the label of the column indatacorresponding to the outcome variable; this is the LHS variable in the regressionevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)group:
String matching the label of the column indatacontaining the categorical levels of the individual entitiesreltime:
Integers matching the label of the column indatacontaining relative times going from -L to +K, with 0 being the timing of treatment onset; this can be generated fromcalendartimegenerated fromgenreltime, andreltime=-1is automatically chosen as the reference periodcalendartime:
Integers matching the label of the column indatacontaining calendar times going from 0 (earliest time period) to T (last time period); this can be generated fromgencalendartime_numericscalendartime.covariates:
List of columns corresponding to control variables indatato be included in the RHS of the regression; if no covariates are to be included, setcovariates=[]vcov_type:
String corresponding to the type of variance-covariance estimator inlinearmodels.PanelOLS.fit(), which is called during the estimation process; default option is'robust'check_balance:
Checks ifdatais a balanced panel; default option isTrueOutputReturns apandas dataframewith 3 columns, indexed toreltime:parameter: The point estimates of the interaction-weighted average treatment affectslower: The lower confidence bound ofparameterupper:  The upper confidence bound ofparameterInteraction-Weighted Panel Event StudyEstimates dynamic treatment effects using the interaction-weighted estimator described inSun and Abraham (2021).
Again, for the following structural equation, we are interested in estimating $\beta_{l}$, the coefficients on leads and lags of treatment dummies, where $l$ is relative time as inSun and Abraham (2021), i.e., the time period relative to treatment onset.
$l=0$ refers to when the treatment was applied to entity $i$.$$Y_{i,t} = \alpha_i + \alpha_t + \sum_{l=-K}^{-2} \beta_{l} D_{i, t}^{l} + \sum_{l=0}^{M} \beta_{l} D_{i, t}^{l} + \mathbf{X_{i, t} \gamma} + \eta_{i, t}$$This implementation has 3 broad steps.Calculate the cohort shares by relative time, $\mathbb{E} (E_i = e | E_i \in g )$ where $g$ is the set of relative times included in the analysis. This package uses a no-constant linear regression model with an OLS estimator as per the Sun and Abraham (2021)'s original Stata packagehere. Using a linear regression approach, instead of simple tabulation, allows for calculation of standard errors of the cohort share estimates.$$1{E_i = e | E_i \in g } = w_{e,l} D_{i, t}^{l} + e_i$$Estimate the cohort-specific average treatment effects, $CATT_{e, l}$, by interacting the cohort dummy with the treatment / relative time dummy, $1(E_i = e) D_{i,t}^{l}$.$$Y_{i,t} = \alpha_i + \alpha_t + \sum_{l=-K}^{-2} \delta_{l} 1(E_i = e) D_{i,t}^{l} + \sum_{l=0}^{M} \delta_{l} 1(E_i = e) D_{i,t}^{l} + \mathbf{X_{i, t} \gamma} + \varepsilon_{i, t}$$Calculate the interaction-weighted average treatment effects using output from steps 1 and 2 for every relative time $l$. In this current version, the estimated confidence bands are scaled the same way.$$\hat{\beta_l} = \sum_{e} \hat{\delta_{l}} \hat{w_{e,l}} \ \forall \ l$$Documentationpaneleventstudy.interactionweighted_eventstudy(data,outcome,event,group,cohort,reltime,calendartime,covariates,vcov_type='robust',check_balance=True)Parametersdata:pandas dataframeoutcome:
String matching the label of the column indatacorresponding to the outcome variable; this is the LHS variable in the regressionevent:
String matching the label of the column indatacorresponding to the event variable; this should be a dummy variable indicating the pre- (values 0 prior to relative time 0) and post- periods (values 1 from relative time 0 onwards)group:
String matching the label of the column indatacontaining the categorical levels of the individual entitiescohort:
Integers matching the label of the column indatacontaining the categorical levels of the cohorts in the data set generated fromgencohort(e.g., all entities treated in calendar time 3 should take the value 3 in this column)reltime:
Integers matching the label of the column indatacontaining relative times going from -L to +K, with 0 being the timing of treatment onset; this can be generated fromcalendartimegenerated fromgenreltime, andreltime=-1is automatically chosen as the reference periodcalendartime:
Integers matching the label of the column indatacontaining calendar times going from 0 (earliest time period) to T (last time period); this can be generated fromgencalendartime_numericscalendartime.covariates:
List of columns corresponding to control variables indatato be included in the RHS of the regression; if no covariates are to be included, setcovariates=[]vcov_type:
String corresponding to the type of variance-covariance estimator inlinearmodels.PanelOLS.fit(), which is called during the estimation process; default option is'robust'check_balance:
Checks ifdatais a balanced panel; default option isTrueOutputReturns apandas dataframewith 3 columns, indexed toreltime:parameter: The point estimates of the interaction-weighted average treatment affectslower: The lower confidence bound ofparameterupper:  The upper confidence bound ofparameterSingle Entity Event StudyEstimates dynamic treatment effects ($\beta_{l}$ coefficients on leads and lags of treatment dummies) using an single entity linear regression model with an OLS estimator, where $l=0$ refers to when the treatment was applied.
$D_{l}$ are dummies switching when the entity is $l$ periods relative to the treatment onset.
The linear regression includes a constant ($\alpha$), is an optional vector of controls ($\mathbf{X_{t}}$).
$\epsilon_{t}$ are the errors.$$Y_{t} = \alpha + \sum_{l=-K}^{-2} \beta_{l} D_{l} + \sum_{l=0}^{M} \beta_{l} D_{l} + \mathbf{X_{t} \gamma} + \epsilon_{t}$$Documentationpaneleventstudy.timeseries_eventstudy(data,outcome,reltime,covariates,vcov_type='HC3')Parametersdata:pandas dataframeoutcome:
String matching the label of the column indatacorresponding to the outcome variable; this is the LHS variable in the regressionreltime:
Integers matching the label of the column indatacontaining relative times going from -L to +K, with 0 being the timing of treatment onsetcovariates:
List of columns corresponding to control variables indatato be included in the RHS of the regression; if no covariates are to be included, setcovariates=[]vcov_type:
String corresponding to the type of variance-covariance estimator instatsmodels.regression.linear_model.RegressionResults.get_robustcov_results, which is called during the estimation process; default option is'HC3'OutputReturns apandas dataframewith 3 columns, indexed toreltime:parameter: The point estimates of the interaction-weighted average treatment affectslower: The lower confidence bound ofparameterupper:  The upper confidence bound ofparameterImplementation Notes (Utilities)Plotting Event Study Lead and Lag CoefficientsThis function calls plotly'sgraph_objectsmodule to show the event study estimates (dynamic treatment effects) to be shown as a line chart, together with their confidence bands (can be manually excluded).
Moreover, it exports an interactive graph as a html file via plotly'splotly.io.write_html(), and a static graph as a png file via plotly's [plotly.io.write_image()].
Users of this package may, of course, opt to other charting packages, modules, or scripts to plot the event study estimates.Documentationpaneleventstudy.eventstudyplot(input,big_title='Event Study Plot (With 95% CIs)',path_output='',name_output='eventstudyplot')Parametersinput:
Output from the analytical functions (paneleventstudy.naivetwfe_eventstudy(),paneleventstudy.interactionweighted_eventstudy(),paneleventstudy.timeseries_eventstudy()); manually exclude columnslowerandupperif only the point estimates are to be shownbig_title:
String containing the main title of the figure; default is'Event Study Plot (With 95% CIs)'path_output:
String containing the directory of where the output files should be saved in; default is'', i.e., the present working directoryname_output=
String containing the file name of the image and html file to be generated; default is'eventstudyplot'OutputRequirementsPython Packagespandas>=1.4.3numpy>=1.23.0linearmodels>=4.27plotly>=5.9.0statsmodels>=0.13.2sympy>=1.10.1