Note
-
Download Jupyter notebook:
https://docs.doubleml.org/stable/examples/did/py_panel_simple.ipynb.
Python: Panel Data Introduction#
In this example, we replicate the results from the guide Getting Started with the did Package of the did-R-package.
As the did-R-package the implementation of DoubleML is based on Callaway and Sant’Anna(2021).
The notebook requires the following packages:
[1]:
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression, LogisticRegression
from doubleml.data import DoubleMLPanelData
from doubleml.did import DoubleMLDIDMulti
Data#
The data we will use is simulated and part of the CSDID-Python-Package.
A description of the data generating process can be found at the CSDID-documentation.
[2]:
dta = pd.read_csv("https://raw.githubusercontent.com/d2cml-ai/csdid/main/data/sim_data.csv")
dta.head()
[2]:
G | X | id | cluster | period | Y | treat | |
---|---|---|---|---|---|---|---|
0 | 3 | -0.876233 | 1 | 5 | 1 | 5.562556 | 1 |
1 | 3 | -0.876233 | 1 | 5 | 2 | 4.349213 | 1 |
2 | 3 | -0.876233 | 1 | 5 | 3 | 7.134037 | 1 |
3 | 3 | -0.876233 | 1 | 5 | 4 | 6.243056 | 1 |
4 | 2 | -0.873848 | 2 | 36 | 1 | -3.659387 | 1 |
To work with the DoubleML-package, we initialize a DoubleMLPanelData
object.
Therefore, we set the never-treated units in group column G
to np.inf
(we have to change the datatype to float
).
[3]:
# set dtype for G to float
dta["G"] = dta["G"].astype(float)
dta.loc[dta["G"] == 0, "G"] = np.inf
dta.head()
[3]:
G | X | id | cluster | period | Y | treat | |
---|---|---|---|---|---|---|---|
0 | 3.0 | -0.876233 | 1 | 5 | 1 | 5.562556 | 1 |
1 | 3.0 | -0.876233 | 1 | 5 | 2 | 4.349213 | 1 |
2 | 3.0 | -0.876233 | 1 | 5 | 3 | 7.134037 | 1 |
3 | 3.0 | -0.876233 | 1 | 5 | 4 | 6.243056 | 1 |
4 | 2.0 | -0.873848 | 2 | 36 | 1 | -3.659387 | 1 |
Now, we can initialize the DoubleMLPanelData
object, specifying
y_col
: the outcomed_cols
: the group variable indicating the first treated period for each unitid_col
: the unique identification column for each unitt_col
: the time columnx_cols
: the additional pre-treatment controls
[4]:
dml_data = DoubleMLPanelData(
data=dta,
y_col="Y",
d_cols="G",
id_col="id",
t_col="period",
x_cols=["X"]
)
print(dml_data)
================== DoubleMLPanelData Object ==================
------------------ Data summary ------------------
Outcome variable: Y
Treatment variable(s): ['G']
Covariates: ['X']
Instrument variable(s): None
Time variable: period
Id variable: id
No. Observations: 3979
------------------ DataFrame info ------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 15916 entries, 0 to 15915
Columns: 7 entries, G to treat
dtypes: float64(3), int64(4)
memory usage: 870.5 KB
ATT Estimation#
The DoubleML-package implements estimation of group-time average treatment effect via the DoubleMLDIDMulti
class (see model documentation).
The class basically behaves like other DoubleML
classes and requires the specification of two learners (for more details on the regression elements, see score documentation). The model will be estimated using the fit()
method.
[5]:
dml_obj = DoubleMLDIDMulti(
obj_dml_data=dml_data,
ml_g=LinearRegression(),
ml_m=LogisticRegression(),
control_group="never_treated",
)
dml_obj.fit()
print(dml_obj)
================== DoubleMLDIDMulti Object ==================
------------------ Data summary ------------------
Outcome variable: Y
Treatment variable(s): ['G']
Covariates: ['X']
Instrument variable(s): None
Time variable: period
Id variable: id
No. Observations: 3979
------------------ Score & algorithm ------------------
Score function: observational
Control group: never_treated
Anticipation periods: 0
------------------ Machine learner ------------------
Learner ml_g: LinearRegression()
Learner ml_m: LogisticRegression()
Out-of-sample Performance:
Regression:
Learner ml_g0 RMSE: [[1.42679686 1.409863 1.39702421 1.42654656 1.40401708 1.42500164
1.42390712 1.4051695 1.42340476]]
Learner ml_g1 RMSE: [[1.4054328 1.43662522 1.39879287 1.41440101 1.4242088 1.38654634
1.45828237 1.41550119 1.40775095]]
Classification:
Learner ml_m Log Loss: [[0.69080063 0.69135757 0.69068634 0.68001716 0.67983069 0.67956284
0.66243302 0.66204822 0.66207717]]
------------------ Resampling ------------------
No. folds: 5
No. repeated sample splits: 1
------------------ Fit summary ------------------
coef std err t P>|t| 2.5 % 97.5 %
ATT(2.0,1,2) 0.921581 0.063994 14.401021 0.000000 0.796155 1.047007
ATT(2.0,1,3) 1.984952 0.064575 30.738534 0.000000 1.858386 2.111517
ATT(2.0,1,4) 2.954391 0.063139 46.791834 0.000000 2.830641 3.078141
ATT(3.0,1,2) -0.038851 0.066222 -0.586677 0.557421 -0.168645 0.090942
ATT(3.0,2,3) 1.106676 0.065494 16.897261 0.000000 0.978309 1.235042
ATT(3.0,2,4) 2.053930 0.065610 31.304978 0.000000 1.925336 2.182524
ATT(4.0,1,2) 0.000824 0.068371 0.012051 0.990385 -0.133180 0.134828
ATT(4.0,2,3) 0.062031 0.066425 0.933841 0.350386 -0.068160 0.192222
ATT(4.0,3,4) 0.954156 0.067303 14.176961 0.000000 0.822244 1.086068
The summary displays estimates of the \(ATT(g,t_\text{eval})\) effects for different combinations of \((g,t_\text{eval})\) via \(\widehat{ATT}(\mathrm{g},t_\text{pre},t_\text{eval})\), where
\(\mathrm{g}\) specifies the group
\(t_\text{pre}\) specifies the corresponding pre-treatment period
\(t_\text{eval}\) specifies the evaluation period
This corresponds to the estimates given in att_gt
function in the did-R-package, where the standard choice is \(t_\text{pre} = \min(\mathrm{g}, t_\text{eval}) - 1\) (without anticipation).
Remark that this includes pre-tests effects if \(\mathrm{g} > t_{eval}\), e.g. \(ATT(4,2)\).
As usual for the DoubleML-package, you can obtain joint confidence intervals via bootstrap.
[6]:
level = 0.95
ci = dml_obj.confint(level=level)
dml_obj.bootstrap(n_rep_boot=5000)
ci_joint = dml_obj.confint(level=level, joint=True)
ci_joint
[6]:
2.5 % | 97.5 % | |
---|---|---|
ATT(2.0,1,2) | 0.743467 | 1.099695 |
ATT(2.0,1,3) | 1.805220 | 2.164684 |
ATT(2.0,1,4) | 2.778657 | 3.130125 |
ATT(3.0,1,2) | -0.223167 | 0.145465 |
ATT(3.0,2,3) | 0.924386 | 1.288965 |
ATT(3.0,2,4) | 1.871317 | 2.236543 |
ATT(4.0,1,2) | -0.189471 | 0.191119 |
ATT(4.0,2,3) | -0.122850 | 0.246911 |
ATT(4.0,3,4) | 0.766832 | 1.141481 |
A visualization of the effects can be obtained via the plot_effects()
method.
Remark that the plot used joint confidence intervals per default.
[7]:
fig, ax = dml_obj.plot_effects()
/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/site-packages/matplotlib/cbook.py:1719: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
return math.isfinite(val)
/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/site-packages/matplotlib/cbook.py:1719: FutureWarning: Calling float on a single element Series is deprecated and will raise a TypeError in the future. Use float(ser.iloc[0]) instead
return math.isfinite(val)

Effect Aggregation#
As the did-R-package, the \(ATT\)’s can be aggregated to summarize multiple effects. For details on different aggregations and details on their interpretations see Callaway and Sant’Anna(2021).
The aggregations are implemented via the aggregate()
method.
Group Aggregation#
To obtain group-specific effects it is possible to aggregate several \(\widehat{ATT}(\mathrm{g},t_\text{pre},t_\text{eval})\) values based on the group \(\mathrm{g}\) by setting the aggregation="group"
argument.
[8]:
aggregated = dml_obj.aggregate(aggregation="group")
print(aggregated)
_ = aggregated.plot_effects()
================== DoubleMLDIDAggregation Object ==================
Group Aggregation
------------------ Overall Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
1.487329 0.034161 43.539374 0.0 1.420376 1.554283
------------------ Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
2.0 1.953641 0.052178 37.441996 0.0 1.851375 2.055908
3.0 1.580303 0.056357 28.041057 0.0 1.469846 1.690760
4.0 0.954156 0.067303 14.176961 0.0 0.822244 1.086068
------------------ Additional Information ------------------
Score function: observational
Control group: never_treated
Anticipation periods: 0
/home/runner/work/doubleml-docs/doubleml-docs/doubleml-for-py/doubleml/did/did_aggregation.py:368: UserWarning: Joint confidence intervals require bootstrapping which hasn't been performed yet. Automatically applying '.aggregated_frameworks.bootstrap(method="normal", n_rep_boot=500)' with default values. For different bootstrap settings, call bootstrap() explicitly before plotting.
warnings.warn(

The output is a DoubleMLDIDAggregation
object which includes an overall aggregation summary based on group size.
Time Aggregation#
This aggregates \(\widehat{ATT}(\mathrm{g},t_\text{pre},t_\text{eval})\), based on \(t_\text{eval}\), but weighted with respect to group size. Corresponds to Calendar Time Effects from the did-R-package.
For calendar time effects set aggregation="time"
.
[9]:
aggregated_time = dml_obj.aggregate("time")
print(aggregated_time)
fig, ax = aggregated_time.plot_effects()
================== DoubleMLDIDAggregation Object ==================
Time Aggregation
------------------ Overall Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
1.479942 0.035057 42.215475 0.0 1.411231 1.548652
------------------ Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
2 0.921581 0.063994 14.401021 0.0 0.796155 1.047007
3 1.547161 0.051353 30.127812 0.0 1.446510 1.647811
4 1.971083 0.046551 42.342326 0.0 1.879845 2.062322
------------------ Additional Information ------------------
Score function: observational
Control group: never_treated
Anticipation periods: 0
/home/runner/work/doubleml-docs/doubleml-docs/doubleml-for-py/doubleml/did/did_aggregation.py:368: UserWarning: Joint confidence intervals require bootstrapping which hasn't been performed yet. Automatically applying '.aggregated_frameworks.bootstrap(method="normal", n_rep_boot=500)' with default values. For different bootstrap settings, call bootstrap() explicitly before plotting.
warnings.warn(

Event Study Aggregation#
Finally, aggregation="eventstudy"
aggregates \(\widehat{ATT}(\mathrm{g},t_\text{pre},t_\text{eval})\) based on exposure time \(e = t_\text{eval} - \mathrm{g}\) (respecting group size).
[10]:
aggregated_eventstudy = dml_obj.aggregate("eventstudy")
print(aggregated_eventstudy)
fig, ax = aggregated_eventstudy.plot_effects()
================== DoubleMLDIDAggregation Object ==================
Event Study Aggregation
------------------ Overall Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
1.989003 0.038694 51.403798 0.0 1.913164 2.064841
------------------ Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
-2.0 0.000824 0.068371 0.012051 0.990385 -0.133180 0.134828
-1.0 0.012924 0.040384 0.320034 0.748942 -0.066228 0.092076
0.0 0.993282 0.030649 32.408027 0.000000 0.933210 1.053353
1.0 2.019335 0.045738 44.149901 0.000000 1.929690 2.108980
2.0 2.954391 0.063139 46.791834 0.000000 2.830641 3.078141
------------------ Additional Information ------------------
Score function: observational
Control group: never_treated
Anticipation periods: 0
/home/runner/work/doubleml-docs/doubleml-docs/doubleml-for-py/doubleml/did/did_aggregation.py:368: UserWarning: Joint confidence intervals require bootstrapping which hasn't been performed yet. Automatically applying '.aggregated_frameworks.bootstrap(method="normal", n_rep_boot=500)' with default values. For different bootstrap settings, call bootstrap() explicitly before plotting.
warnings.warn(

Aggregation Details#
The DoubleMLDIDAggregation
objects include several DoubleMLFrameworks
which support methods like bootstrap()
or confint()
. Further, the weights can be accessed via the properties
overall_aggregation_weights
: weights for the overall aggregationaggregation_weights
: weights for the aggregation
To clarify, e.g. for the eventstudy aggregation
[11]:
print(aggregated_eventstudy)
================== DoubleMLDIDAggregation Object ==================
Event Study Aggregation
------------------ Overall Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
1.989003 0.038694 51.403798 0.0 1.913164 2.064841
------------------ Aggregated Effects ------------------
coef std err t P>|t| 2.5 % 97.5 %
-2.0 0.000824 0.068371 0.012051 0.990385 -0.133180 0.134828
-1.0 0.012924 0.040384 0.320034 0.748942 -0.066228 0.092076
0.0 0.993282 0.030649 32.408027 0.000000 0.933210 1.053353
1.0 2.019335 0.045738 44.149901 0.000000 1.929690 2.108980
2.0 2.954391 0.063139 46.791834 0.000000 2.830641 3.078141
------------------ Additional Information ------------------
Score function: observational
Control group: never_treated
Anticipation periods: 0
Here, the overall effect aggregation aggregates each effect with positive exposure
[12]:
print(aggregated_eventstudy.overall_aggregation_weights)
[0. 0. 0.33333333 0.33333333 0.33333333]
If one would like to consider how the aggregated effect with \(e=0\) is computed, one would have to look at the third set of weights within the aggregation_weights
property
[13]:
aggregated_eventstudy.aggregation_weights[2]
[13]:
array([0.32875335, 0. , 0. , 0. , 0.32674263,
0. , 0. , 0. , 0.34450402])
Taking a look at the original dml_obj
, one can see that this combines the following estimates:
\(\widehat{ATT}(2,1,2)\)
\(\widehat{ATT}(3,2,3)\)
\(\widehat{ATT}(4,3,4)\)
[14]:
print(dml_obj.summary)
coef std err t P>|t| 2.5 % 97.5 %
ATT(2.0,1,2) 0.921581 0.063994 14.401021 0.000000 0.796155 1.047007
ATT(2.0,1,3) 1.984952 0.064575 30.738534 0.000000 1.858386 2.111517
ATT(2.0,1,4) 2.954391 0.063139 46.791834 0.000000 2.830641 3.078141
ATT(3.0,1,2) -0.038851 0.066222 -0.586677 0.557421 -0.168645 0.090942
ATT(3.0,2,3) 1.106676 0.065494 16.897261 0.000000 0.978309 1.235042
ATT(3.0,2,4) 2.053930 0.065610 31.304978 0.000000 1.925336 2.182524
ATT(4.0,1,2) 0.000824 0.068371 0.012051 0.990385 -0.133180 0.134828
ATT(4.0,2,3) 0.062031 0.066425 0.933841 0.350386 -0.068160 0.192222
ATT(4.0,3,4) 0.954156 0.067303 14.176961 0.000000 0.822244 1.086068