csdid 0.2.7__py3-none-any.whl
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.
- csdid/__init__.py +0 -0
- csdid/_version.py +1 -0
- csdid/aggte_fnc/__init__.py +0 -0
- csdid/aggte_fnc/aggte.py +113 -0
- csdid/aggte_fnc/compute_aggte.py +495 -0
- csdid/aggte_fnc/utils.py +196 -0
- csdid/att_gt.py +245 -0
- csdid/attgt_fnc/__init__.py +0 -0
- csdid/attgt_fnc/compute_att_gt.py +507 -0
- csdid/attgt_fnc/preprocess_did.py +429 -0
- csdid/plots/__init__.py +0 -0
- csdid/plots/gplot.py +106 -0
- csdid/utils/__init__.py +0 -0
- csdid/utils/bmisc.py +55 -0
- csdid/utils/mboot.py +115 -0
- csdid-0.2.7.dist-info/METADATA +29 -0
- csdid-0.2.7.dist-info/RECORD +20 -0
- csdid-0.2.7.dist-info/WHEEL +5 -0
- csdid-0.2.7.dist-info/licenses/LICENSE +21 -0
- csdid-0.2.7.dist-info/top_level.txt +1 -0
csdid/__init__.py
ADDED
|
File without changes
|
csdid/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version = '0.2.7'
|
|
File without changes
|
csdid/aggte_fnc/aggte.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#' @title Aggregate Group-Time Average Treatment Effects
|
|
2
|
+
#'
|
|
3
|
+
#' @description A function to take group-time average treatment effects
|
|
4
|
+
#' and aggregate them into a smaller number of parameters. There are
|
|
5
|
+
#' several possible aggregations including "simple", "dynamic", "group",
|
|
6
|
+
#' and "calendar."
|
|
7
|
+
#'
|
|
8
|
+
#' @param MP an MP object (i.e., the results of the [att_gt()] method)
|
|
9
|
+
#' @param type Which type of aggregated treatment effect parameter to compute.
|
|
10
|
+
#' One option is "simple" (this just computes a weighted average of all
|
|
11
|
+
#' group-time average treatment effects with weights proportional to group
|
|
12
|
+
#' size). Other options are "dynamic" (this computes average effects across
|
|
13
|
+
#' different lengths of exposure to the treatment and is similar to an
|
|
14
|
+
#' "event study"; here the overall effect averages the effect of the
|
|
15
|
+
#' treatment across all positive lengths of exposure); "group" (this
|
|
16
|
+
#' is the default option and
|
|
17
|
+
#' computes average treatment effects across different groups; here
|
|
18
|
+
#' the overall effect averages the effect across different groups); and
|
|
19
|
+
#' "calendar" (this computes average treatment effects across different
|
|
20
|
+
#' time periods; here the overall effect averages the effect across each
|
|
21
|
+
#' time period).
|
|
22
|
+
#' @param balance_e If set (and if one computes dynamic effects), it balances
|
|
23
|
+
#' the sample with respect to event time. For example, if `balance.e=2`,
|
|
24
|
+
#' `aggte` will drop groups that are not exposed to treatment for
|
|
25
|
+
#' at least three periods. (the initial period when `e=0` as well as the
|
|
26
|
+
#' next two periods when `e=1` and the `e=2`). This ensures that
|
|
27
|
+
#' the composition of groups does not change when event time changes.
|
|
28
|
+
#' @param min_e For event studies, this is the smallest event time to compute
|
|
29
|
+
#' dynamic effects for. By default, `min_e = -Inf` so that effects at
|
|
30
|
+
#' all lengths of exposure are computed.
|
|
31
|
+
#' @param max_e For event studies, this is the largest event time to compute
|
|
32
|
+
#' dynamic effects for. By default, `max_e = Inf` so that effects at
|
|
33
|
+
#' all lengths of exposure are computed.
|
|
34
|
+
#' @param na.rm Logical value if we are to remove missing Values from analyses. Defaults is FALSE.
|
|
35
|
+
#' @param bstrap Boolean for whether or not to compute standard errors using
|
|
36
|
+
#' the multiplier bootstrap. If standard errors are clustered, then one
|
|
37
|
+
#' must set `bstrap=TRUE`. Default is value set in the MP object. If bstrap is `FALSE`, then analytical
|
|
38
|
+
#' standard errors are reported.
|
|
39
|
+
#' @param biters The number of bootstrap iterations to use. The default is the value set in the MP object,
|
|
40
|
+
#' and this is only applicable if `bstrap=TRUE`.
|
|
41
|
+
#'
|
|
42
|
+
#' @param cband Boolean for whether or not to compute a uniform confidence
|
|
43
|
+
#' band that covers all of the group-time average treatment effects
|
|
44
|
+
#' with fixed probability `1-alp`. In order to compute uniform confidence
|
|
45
|
+
#' bands, `bstrap` must also be set to `TRUE`. The default is
|
|
46
|
+
#' the value set in the MP object
|
|
47
|
+
#' @param alp the significance level, default is value set in the MP object.
|
|
48
|
+
#' @param clustervars A vector of variables to cluster on. At most, there
|
|
49
|
+
#' can be two variables (otherwise will throw an error) and one of these
|
|
50
|
+
#' must be the same as idname which allows for clustering at the individual
|
|
51
|
+
#' level. Default is the variables set in the MP object
|
|
52
|
+
|
|
53
|
+
#'
|
|
54
|
+
#' @return An [`AGGTEobj`] object that holds the results from the
|
|
55
|
+
#' aggregation
|
|
56
|
+
#'
|
|
57
|
+
#' @section Examples:
|
|
58
|
+
#'
|
|
59
|
+
#
|
|
60
|
+
#' Initial ATT(g,t) estimates from [att_gt()]
|
|
61
|
+
#'
|
|
62
|
+
#'
|
|
63
|
+
#' You can aggregate the ATT(g,t) in many ways.
|
|
64
|
+
#'
|
|
65
|
+
#' **Overall ATT:**
|
|
66
|
+
#' aggte(out, type = "simple")
|
|
67
|
+
#' ```
|
|
68
|
+
#'
|
|
69
|
+
#' **Dynamic ATT (Event-Study):**
|
|
70
|
+
#' aggte(out, type = "dynamic")
|
|
71
|
+
#' ```
|
|
72
|
+
#'
|
|
73
|
+
#' **ATT for each group:**
|
|
74
|
+
#' aggte(out, type = "group")
|
|
75
|
+
#' ```
|
|
76
|
+
#'
|
|
77
|
+
#' **ATT for each calendar year:**
|
|
78
|
+
#' aggte(out, type = "calendar")
|
|
79
|
+
#' ```
|
|
80
|
+
#'
|
|
81
|
+
#'
|
|
82
|
+
#'
|
|
83
|
+
from csdid.aggte_fnc.compute_aggte import compute_aggte
|
|
84
|
+
import inspect
|
|
85
|
+
|
|
86
|
+
def aggte(MP,
|
|
87
|
+
typec="group",
|
|
88
|
+
balance_e=None,
|
|
89
|
+
min_e=float('-inf'),
|
|
90
|
+
max_e=float('inf'),
|
|
91
|
+
na_rm=False,
|
|
92
|
+
bstrap=None,
|
|
93
|
+
biters=None,
|
|
94
|
+
cband=None,
|
|
95
|
+
alp=None,
|
|
96
|
+
clustervars=None):
|
|
97
|
+
call = inspect.currentframe().f_back.f_locals.copy()
|
|
98
|
+
|
|
99
|
+
return compute_aggte(MP=MP,
|
|
100
|
+
typec=typec,
|
|
101
|
+
balance_e=balance_e,
|
|
102
|
+
min_e=min_e,
|
|
103
|
+
max_e=max_e,
|
|
104
|
+
na_rm=na_rm,
|
|
105
|
+
bstrap=bstrap,
|
|
106
|
+
biters=biters,
|
|
107
|
+
cband=cband,
|
|
108
|
+
alp=alp,
|
|
109
|
+
clustervars=clustervars,
|
|
110
|
+
call=call)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.stats import norm
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from csdid.aggte_fnc.utils import get_agg_inf_func, get_se, wif, AGGTEobj
|
|
8
|
+
from csdid.utils.mboot import mboot
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
def compute_aggte(MP,
|
|
12
|
+
typec = "group",
|
|
13
|
+
balance_e = None,
|
|
14
|
+
min_e = float('-inf'),
|
|
15
|
+
max_e = float('inf'),
|
|
16
|
+
na_rm = False,
|
|
17
|
+
bstrap = None,
|
|
18
|
+
biters = None,
|
|
19
|
+
cband = None,
|
|
20
|
+
alp = None,
|
|
21
|
+
clustervars = None,
|
|
22
|
+
call = None):
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# unpack MP object
|
|
26
|
+
# =============================================================================
|
|
27
|
+
group = np.array( MP['group'] )
|
|
28
|
+
t = np.array( MP['t'] )
|
|
29
|
+
att = np.array( MP['att'] )
|
|
30
|
+
dp = MP['DIDparams']
|
|
31
|
+
tlist = np.array( dp['tlist'] )
|
|
32
|
+
glist = np.array( dp['glist'] )
|
|
33
|
+
data = dp['data']
|
|
34
|
+
inffunc = MP['inffunc']['inffunc']
|
|
35
|
+
n = MP['n']
|
|
36
|
+
gname = dp['gname']
|
|
37
|
+
tname = dp['tname']
|
|
38
|
+
idname = dp['idname']
|
|
39
|
+
# typec = MP['type']
|
|
40
|
+
panel = dp['panel']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if clustervars is None:
|
|
45
|
+
clustervars = dp['clustervars']
|
|
46
|
+
if bstrap is None:
|
|
47
|
+
bstrap = dp['bstrap']
|
|
48
|
+
if biters is None:
|
|
49
|
+
biters = dp['biters']
|
|
50
|
+
if alp is None:
|
|
51
|
+
alp = dp['alp']
|
|
52
|
+
if cband is None:
|
|
53
|
+
cband = dp['cband']
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Overwrite MP objects (to compute bootstrap)
|
|
58
|
+
MP['DIDparams']['clustervars'] = clustervars
|
|
59
|
+
MP['DIDparams']['bstrap'] = bstrap
|
|
60
|
+
MP['DIDparams']['biters'] = biters
|
|
61
|
+
MP['DIDparams']['alp'] = alp
|
|
62
|
+
MP['DIDparams']['cband'] = cband
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# Treat data
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
if typec not in ["simple", "dynamic", "group", "calendar"]:
|
|
69
|
+
raise ValueError("`typec` must be one of ['simple', 'dynamic', 'group', 'calendar']")
|
|
70
|
+
# Removing missing values
|
|
71
|
+
if na_rm:
|
|
72
|
+
notna = ~np.isnan(att)
|
|
73
|
+
group = group[notna]
|
|
74
|
+
t = t[notna]
|
|
75
|
+
att = att[notna]
|
|
76
|
+
inffunc = inffunc[:, notna]
|
|
77
|
+
glist = np.sort(np.unique(group))
|
|
78
|
+
|
|
79
|
+
if typec == "group":
|
|
80
|
+
gnotna = []
|
|
81
|
+
for g in glist:
|
|
82
|
+
indices = np.where((group == g) & (g <= t))
|
|
83
|
+
is_not_na = np.any(~np.isnan(att[indices]))
|
|
84
|
+
gnotna.append(is_not_na)
|
|
85
|
+
|
|
86
|
+
gnotna = np.array(gnotna)
|
|
87
|
+
glist = glist[gnotna]
|
|
88
|
+
not_all_na = np.isin(group, glist)
|
|
89
|
+
group = group[not_all_na]
|
|
90
|
+
t = t[not_all_na]
|
|
91
|
+
att = att[not_all_na]
|
|
92
|
+
inffunc = inffunc[:, not_all_na]
|
|
93
|
+
glist = np.sort(np.unique(group))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if (not na_rm) and np.any(np.isnan(att)):
|
|
97
|
+
raise ValueError("Missing values at att_gt found. If you want to remove these, set `na_rm = True`.")
|
|
98
|
+
|
|
99
|
+
if panel:
|
|
100
|
+
dta = data[data[tname] == tlist[0]]
|
|
101
|
+
else:
|
|
102
|
+
dta = data.groupby(idname).mean().reset_index()
|
|
103
|
+
dta = dta.iloc[:, 1:]
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# Treat data 2
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
originalt = t
|
|
111
|
+
originalgroup = group
|
|
112
|
+
originalglist = glist
|
|
113
|
+
originaltlist = tlist
|
|
114
|
+
# In case g's are not part of tlist
|
|
115
|
+
originalgtlist = np.sort(np.unique(np.concatenate((originaltlist, originalglist))))
|
|
116
|
+
uniquet = list(range(1, len(originalgtlist) + 1))
|
|
117
|
+
|
|
118
|
+
# Function to switch from "new" t values to original t values
|
|
119
|
+
def t2orig(t):
|
|
120
|
+
return originalgtlist[uniquet.index(t) if t in uniquet else -1]
|
|
121
|
+
|
|
122
|
+
# Function to switch between "original" t values and new t values
|
|
123
|
+
def orig2t(orig):
|
|
124
|
+
new_t = [uniquet[i] for i in range(len(originalgtlist)) if originalgtlist[i] == orig]
|
|
125
|
+
out = new_t[0] if new_t else None
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
t = [orig2t(orig) for orig in originalt]
|
|
129
|
+
group = [orig2t(orig) for orig in originalgroup]
|
|
130
|
+
glist = [orig2t(orig) for orig in originalglist]
|
|
131
|
+
tlist = np.asarray(list(set(t)))
|
|
132
|
+
maxT = max(t)
|
|
133
|
+
|
|
134
|
+
# Set the weights
|
|
135
|
+
# return data.columns
|
|
136
|
+
weights_ind = dta['w1'].to_numpy()
|
|
137
|
+
|
|
138
|
+
# We can work in overall probabilities because conditioning will cancel out
|
|
139
|
+
# since it shows up in numerator and denominator
|
|
140
|
+
pg = np.array([np.mean(weights_ind * (dta[gname].to_numpy() == g)) for g in originalglist])
|
|
141
|
+
|
|
142
|
+
# Length of this is equal to the number of groups
|
|
143
|
+
pgg = pg
|
|
144
|
+
|
|
145
|
+
# Same but length is equal to the number of ATT(g,t)
|
|
146
|
+
pg = [pg[glist.index(g)] for g in group]
|
|
147
|
+
|
|
148
|
+
# Which group time average treatment effects are post-treatment
|
|
149
|
+
keepers = [i for i in range(len(group)) if group[i] <= t[i] <= (group[i] + max_e)] ### added second condition to allow for limit on longest period included in att
|
|
150
|
+
|
|
151
|
+
# n x 1 vector of group variable
|
|
152
|
+
G = [orig2t(g) for g in dta[gname].to_numpy()]
|
|
153
|
+
|
|
154
|
+
# =============================================================================
|
|
155
|
+
# simple
|
|
156
|
+
# =============================================================================
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if typec == "simple":
|
|
160
|
+
# Simple ATT
|
|
161
|
+
# Averages all post-treatment ATT(g,t) with weights given by group size
|
|
162
|
+
pg = np.array(pg)
|
|
163
|
+
simple_att = np.sum(att[keepers] * pg[keepers]) / np.sum(pg[keepers])
|
|
164
|
+
if np.isnan(simple_att):
|
|
165
|
+
simple_att = None
|
|
166
|
+
|
|
167
|
+
# Get the part of the influence function coming from estimated weights
|
|
168
|
+
simple_wif = wif(keepers, pg, weights_ind, G, group)
|
|
169
|
+
|
|
170
|
+
# Get the overall influence function
|
|
171
|
+
simple_if = get_agg_inf_func(att = att ,
|
|
172
|
+
inffunc = inffunc ,
|
|
173
|
+
whichones = keepers ,
|
|
174
|
+
weights_agg = np.array(pg)[keepers]/np.sum(np.array(pg)[keepers]) ,
|
|
175
|
+
wif = simple_wif )[:, None]
|
|
176
|
+
|
|
177
|
+
# Get standard errors from the overall influence function
|
|
178
|
+
simple_se = get_se(simple_if, dp)
|
|
179
|
+
|
|
180
|
+
if simple_se is not None:
|
|
181
|
+
if simple_se <= np.sqrt(np.finfo(float).eps) * 10:
|
|
182
|
+
simple_se = None
|
|
183
|
+
|
|
184
|
+
AGGTEobj_print = AGGTEobj(overall_att=simple_att,
|
|
185
|
+
overall_se=simple_se,
|
|
186
|
+
typec=typec,
|
|
187
|
+
inf_function={'simple_att': simple_if},
|
|
188
|
+
call=call, DIDparams=dp)
|
|
189
|
+
|
|
190
|
+
return AGGTEobj_print
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# =============================================================================
|
|
194
|
+
# GRoup
|
|
195
|
+
# =============================================================================
|
|
196
|
+
|
|
197
|
+
if typec == "group":
|
|
198
|
+
group = np.array(group)
|
|
199
|
+
t = np.array(t)
|
|
200
|
+
pg = np.array(pg)
|
|
201
|
+
selective_att_g = [np.mean(att[( group== g) & (t >= g) & (t <= (group + max_e))]) for g in glist]
|
|
202
|
+
selective_att_g = np.asarray(selective_att_g)
|
|
203
|
+
selective_att_g[np.isnan(selective_att_g)] = None
|
|
204
|
+
|
|
205
|
+
selective_se_inner = [None] * len(glist)
|
|
206
|
+
for i, g in enumerate(glist):
|
|
207
|
+
whichg = np.where(np.logical_and.reduce((group == g, g <= t, t <= (group + max_e))))[0]
|
|
208
|
+
weightsg = pg[whichg] / np.sum(pg[whichg])
|
|
209
|
+
inf_func_g = get_agg_inf_func(att = att ,
|
|
210
|
+
inffunc = inffunc ,
|
|
211
|
+
whichones = whichg ,
|
|
212
|
+
weights_agg = weightsg ,
|
|
213
|
+
wif = None)[:, None]
|
|
214
|
+
se_g = get_se(inf_func_g, dp)
|
|
215
|
+
selective_se_inner[i] = {'inf_func': inf_func_g, 'se': se_g}
|
|
216
|
+
|
|
217
|
+
# recover standard errors separately by group
|
|
218
|
+
selective_se_g = np.asarray([item['se'] for item in selective_se_inner]).T
|
|
219
|
+
|
|
220
|
+
selective_se_g[selective_se_g <= np.sqrt(np.finfo(float).eps) * 10] = None
|
|
221
|
+
|
|
222
|
+
selective_inf_func_g = np.column_stack([elem["inf_func"] for elem in selective_se_inner])
|
|
223
|
+
|
|
224
|
+
# use multiplier bootstrap (across groups) to get critical value
|
|
225
|
+
# for constructing uniform confidence bands
|
|
226
|
+
selective_crit_val = norm.ppf(1 - alp/2)
|
|
227
|
+
|
|
228
|
+
if dp['cband']:
|
|
229
|
+
if not dp['bstrap']:
|
|
230
|
+
print("Used bootstrap procedure to compute simultaneous confidence band")
|
|
231
|
+
|
|
232
|
+
selective_crit_val = mboot(selective_inf_func_g, dp)['crit_val']
|
|
233
|
+
|
|
234
|
+
if np.isnan(selective_crit_val) or np.isinf(selective_crit_val):
|
|
235
|
+
print("Simultaneous critical value is NA. This probably happened because we cannot compute t-statistic (std errors are NA). We then report pointwise conf. intervals.")
|
|
236
|
+
selective_crit_val = norm.ppf(1 - alp/2)
|
|
237
|
+
dp['cband'] = False
|
|
238
|
+
|
|
239
|
+
if selective_crit_val < norm.ppf(1 - alp/2):
|
|
240
|
+
print("Simultaneous conf. band is somehow smaller than pointwise one using normal approximation. Since this is unusual, we are reporting pointwise confidence intervals")
|
|
241
|
+
selective_crit_val = norm.ppf(1 - alp/2)
|
|
242
|
+
dp['cband'] = False
|
|
243
|
+
|
|
244
|
+
if selective_crit_val >= 7:
|
|
245
|
+
print("Simultaneous critical value is arguably 'too large' to be reliable. This usually happens when the number of observations per group is small and/or there is not much variation in outcomes.")
|
|
246
|
+
|
|
247
|
+
# get overall att under selective treatment timing
|
|
248
|
+
# (here use pgg instead of pg because we can just look at each group)
|
|
249
|
+
selective_att = np.sum(selective_att_g * pgg) / np.sum(pgg)
|
|
250
|
+
|
|
251
|
+
# account for having to estimate pgg in the influence function
|
|
252
|
+
selective_wif = wif(keepers = np.arange(1, len(glist)+1)-1,
|
|
253
|
+
pg = pgg,
|
|
254
|
+
weights_ind = weights_ind,
|
|
255
|
+
G = G,
|
|
256
|
+
group = group)
|
|
257
|
+
|
|
258
|
+
# get overall influence function
|
|
259
|
+
selective_inf_func = get_agg_inf_func(att = selective_att_g,
|
|
260
|
+
inffunc = selective_inf_func_g,
|
|
261
|
+
whichones = np.arange(1, len(glist)+1)-1,
|
|
262
|
+
weights_agg = pgg/np.sum(pgg),
|
|
263
|
+
wif = selective_wif)[:, None]
|
|
264
|
+
|
|
265
|
+
# get overall standard error
|
|
266
|
+
selective_se = get_se(selective_inf_func, dp)
|
|
267
|
+
if not np.isnan(selective_se):
|
|
268
|
+
if selective_se <= np.sqrt(np.finfo(float).eps) * 10:
|
|
269
|
+
selective_se = None
|
|
270
|
+
|
|
271
|
+
AGGTEobj_print = AGGTEobj(overall_att = selective_att,
|
|
272
|
+
overall_se = selective_se,
|
|
273
|
+
typec = typec,
|
|
274
|
+
egt = originalglist,
|
|
275
|
+
att_egt = selective_att_g,
|
|
276
|
+
se_egt = selective_se_g,
|
|
277
|
+
crit_val_egt = selective_crit_val,
|
|
278
|
+
inf_function = {'selective_inf_func_g': selective_inf_func_g,
|
|
279
|
+
'selective_inf_func': selective_inf_func},
|
|
280
|
+
call = call,
|
|
281
|
+
DIDparams = dp)
|
|
282
|
+
|
|
283
|
+
return AGGTEobj_print
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# =============================================================================
|
|
287
|
+
# Dynamic
|
|
288
|
+
# =============================================================================
|
|
289
|
+
|
|
290
|
+
if typec == "dynamic":
|
|
291
|
+
# event times
|
|
292
|
+
# this looks at all available event times
|
|
293
|
+
# note: event times can be negative here.
|
|
294
|
+
# note: event time = 0 corresponds to "on impact"
|
|
295
|
+
eseq = np.unique(np.array(originalt) - np.array(originalgroup) ) # Subtract corresponding elements and convert to NumPy array
|
|
296
|
+
eseq = np.sort(eseq) # Sort the unique values in ascending order
|
|
297
|
+
|
|
298
|
+
# if the user specifies balance_e, then we are going to
|
|
299
|
+
# drop some event times and some groups; if not, we just
|
|
300
|
+
# keep everything (that is what this variable is for)
|
|
301
|
+
originalt = np.array(originalt)
|
|
302
|
+
originalgroup = np.array(originalgroup)
|
|
303
|
+
pg = np.array(pg)
|
|
304
|
+
include_balanced_gt = np.repeat(True, len(originalgroup))
|
|
305
|
+
|
|
306
|
+
if balance_e is not None:
|
|
307
|
+
include_balanced_gt = (t2orig(maxT) - originalgroup >= balance_e)
|
|
308
|
+
eseq = np.unique(originalt[include_balanced_gt] - originalgroup[include_balanced_gt])
|
|
309
|
+
eseq = np.sort(eseq)
|
|
310
|
+
eseq = eseq[(eseq <= balance_e) & (eseq >= balance_e - t2orig(maxT) + t2orig(1))]
|
|
311
|
+
eseq = eseq[(eseq >= min_e) & (eseq <= max_e)]
|
|
312
|
+
|
|
313
|
+
dynamic_att_e = []
|
|
314
|
+
for e in eseq:
|
|
315
|
+
whiche = np.where((originalt - originalgroup == e) & include_balanced_gt)
|
|
316
|
+
atte = att[whiche]
|
|
317
|
+
pge = pg[whiche] / np.sum(pg[whiche])
|
|
318
|
+
dynamic_att_e.append(np.sum(atte * pge))
|
|
319
|
+
|
|
320
|
+
dynamic_se_inner = []
|
|
321
|
+
for e in eseq:
|
|
322
|
+
whiche = np.where((originalt - originalgroup == e) & (include_balanced_gt) )[0]
|
|
323
|
+
pge = pg[whiche] / sum(pg[whiche])
|
|
324
|
+
wif_e = wif(whiche,
|
|
325
|
+
pg,
|
|
326
|
+
weights_ind,
|
|
327
|
+
G,
|
|
328
|
+
group)
|
|
329
|
+
inf_func_e = get_agg_inf_func(att = att,
|
|
330
|
+
inffunc = inffunc,
|
|
331
|
+
whichones = whiche,
|
|
332
|
+
weights_agg = pge,
|
|
333
|
+
wif = wif_e)[:, None]
|
|
334
|
+
se_e = get_se(inf_func_e, dp)
|
|
335
|
+
dynamic_se_inner.append({'inf_func': inf_func_e, 'se': se_e})
|
|
336
|
+
|
|
337
|
+
dynamic_se_e = np.array([item['se'] for item in dynamic_se_inner]).T
|
|
338
|
+
|
|
339
|
+
dynamic_se_e[dynamic_se_e <= np.sqrt(np.finfo(float).eps) * 10] = np.nan
|
|
340
|
+
|
|
341
|
+
dynamic_inf_func_e = np.column_stack([item['inf_func'] for item in dynamic_se_inner])
|
|
342
|
+
|
|
343
|
+
dynamic_crit_val = norm.ppf(1 - alp/2)
|
|
344
|
+
if dp['cband']:
|
|
345
|
+
if not dp['bstrap']:
|
|
346
|
+
print('Used bootstrap procedure to compute simultaneous confidence band')
|
|
347
|
+
dynamic_crit_val = mboot(dynamic_inf_func_e, dp)['crit_val']
|
|
348
|
+
|
|
349
|
+
if np.isnan(dynamic_crit_val) or np.isinf(dynamic_crit_val):
|
|
350
|
+
print('Simultaneous critical value is NA. This probably happened because we cannot compute t-statistic (std errors are NA). We then report pointwise conf. intervals.')
|
|
351
|
+
dynamic_crit_val = norm.ppf(1 - alp/2)
|
|
352
|
+
dp['cband'] = False
|
|
353
|
+
|
|
354
|
+
if dynamic_crit_val < norm.ppf(1 - alp/2):
|
|
355
|
+
print('Simultaneous conf. band is somehow smaller than pointwise one using normal approximation. Since this is unusual, we are reporting pointwise confidence intervals')
|
|
356
|
+
dynamic_crit_val = norm.ppf(1 - alp/2)
|
|
357
|
+
dp['cband'] = False
|
|
358
|
+
|
|
359
|
+
if dynamic_crit_val >= 7:
|
|
360
|
+
print("Simultaneous critical value is arguably 'too large' to be reliable. This usually happens when the number of observations per group is small and/or there is not much variation in outcomes.")
|
|
361
|
+
|
|
362
|
+
epos = eseq >= 0
|
|
363
|
+
dynamic_att = np.mean(np.array(dynamic_att_e)[epos])
|
|
364
|
+
dynamic_inf_func = get_agg_inf_func(att = np.array(dynamic_att_e)[epos],
|
|
365
|
+
inffunc = np.array(dynamic_inf_func_e[:, epos]),
|
|
366
|
+
whichones = np.arange(1, np.sum(epos)+1)-1,
|
|
367
|
+
weights_agg = np.repeat(1 / np.sum(epos), np.sum(epos)),
|
|
368
|
+
wif=None)[:, None]
|
|
369
|
+
|
|
370
|
+
dynamic_se = get_se(dynamic_inf_func, dp)
|
|
371
|
+
if not np.isnan(dynamic_se):
|
|
372
|
+
if dynamic_se <= np.sqrt(np.finfo(float).eps) * 10:
|
|
373
|
+
dynamic_se = np.nan
|
|
374
|
+
|
|
375
|
+
AGGTEobj_print = AGGTEobj(overall_att=dynamic_att,
|
|
376
|
+
overall_se=dynamic_se,
|
|
377
|
+
typec=typec,
|
|
378
|
+
egt=eseq,
|
|
379
|
+
att_egt=dynamic_att_e,
|
|
380
|
+
se_egt=dynamic_se_e,
|
|
381
|
+
crit_val_egt=dynamic_crit_val,
|
|
382
|
+
inf_function={'dynamic_inf_func_e': dynamic_inf_func_e,
|
|
383
|
+
'dynamic_inf_func': dynamic_inf_func},
|
|
384
|
+
call=call,
|
|
385
|
+
min_e=min_e,
|
|
386
|
+
max_e=max_e,
|
|
387
|
+
balance_e=balance_e,
|
|
388
|
+
DIDparams=dp)
|
|
389
|
+
|
|
390
|
+
return AGGTEobj_print
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# =============================================================================
|
|
395
|
+
# Calendar
|
|
396
|
+
# =============================================================================
|
|
397
|
+
|
|
398
|
+
# np.array(group)
|
|
399
|
+
if typec == "calendar":
|
|
400
|
+
minG = min(group)
|
|
401
|
+
calendar_tlist = tlist[tlist >= minG]
|
|
402
|
+
pg = np.array(pg)
|
|
403
|
+
calendar_att_t = []
|
|
404
|
+
group = np.array(group)
|
|
405
|
+
t = np.array(t)
|
|
406
|
+
for t1 in calendar_tlist:
|
|
407
|
+
whicht = np.where((t == t1) & (group <= t))[0]
|
|
408
|
+
attt = att[whicht]
|
|
409
|
+
pgt = pg[whicht] / np.sum(pg[whicht])
|
|
410
|
+
calendar_att_t.append(np.sum(pgt * attt))
|
|
411
|
+
|
|
412
|
+
# get standard errors and influence functions
|
|
413
|
+
# for each time specific att
|
|
414
|
+
calendar_se_inner = []
|
|
415
|
+
for t1 in calendar_tlist:
|
|
416
|
+
which_t = np.where((t == t1) & (group <= t))[0]
|
|
417
|
+
pgt = pg[which_t] / np.sum(pg[which_t])
|
|
418
|
+
wif_t = wif(keepers=which_t,
|
|
419
|
+
pg=pg,
|
|
420
|
+
weights_ind=weights_ind,
|
|
421
|
+
G=G,
|
|
422
|
+
group=group)
|
|
423
|
+
inf_func_t = get_agg_inf_func(att=att,
|
|
424
|
+
inffunc=inffunc,
|
|
425
|
+
whichones=which_t,
|
|
426
|
+
weights_agg=pgt,
|
|
427
|
+
wif=wif_t)[:, None]
|
|
428
|
+
se_t = get_se(inf_func_t, dp)
|
|
429
|
+
calendar_se_inner.append({"inf_func": inf_func_t, "se": se_t})
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# recover standard errors separately by time
|
|
434
|
+
calendar_se_t = np.array([se["se"] for se in calendar_se_inner]).T
|
|
435
|
+
calendar_se_t[calendar_se_t <= np.sqrt(np.finfo(float).eps) * 10] = np.nan
|
|
436
|
+
|
|
437
|
+
# recover influence function separately by time
|
|
438
|
+
calendar_inf_func_t = np.column_stack([se["inf_func"] for se in calendar_se_inner])
|
|
439
|
+
|
|
440
|
+
# use multiplier boostrap (across groups) to get critical value
|
|
441
|
+
# for constructing uniform confidence bands
|
|
442
|
+
calendar_crit_val = norm.ppf(1 - alp/2)
|
|
443
|
+
|
|
444
|
+
if dp['cband']:
|
|
445
|
+
if not dp['bstrap']:
|
|
446
|
+
warnings.warn('Used bootstrap procedure to compute simultaneous confidence band')
|
|
447
|
+
|
|
448
|
+
# mboot function is not provided, please define it separately
|
|
449
|
+
calendar_crit_val = mboot(calendar_inf_func_t, dp)['crit_val']
|
|
450
|
+
|
|
451
|
+
if np.isnan(calendar_crit_val) or np.isinf(calendar_crit_val):
|
|
452
|
+
warnings.warn('Simultaneous critical value is NA. This probably happened because we cannot compute t-statistic (std errors are NA). We then report pointwise conf. intervals.')
|
|
453
|
+
calendar_crit_val = norm.ppf(1 - alp/2)
|
|
454
|
+
dp['cband'] = False
|
|
455
|
+
|
|
456
|
+
if calendar_crit_val < norm.ppf(1 - alp/2):
|
|
457
|
+
warnings.warn('Simultaneous conf. band is somehow smaller than pointwise one using normal approximation. Since this is unusual, we are reporting pointwise confidence intervals.')
|
|
458
|
+
calendar_crit_val = norm.ppf(1 - alp/2)
|
|
459
|
+
dp['cband'] = False
|
|
460
|
+
|
|
461
|
+
if calendar_crit_val >= 7:
|
|
462
|
+
warnings.warn("Simultaneous critical value is arguably 'too large' to be reliable. This usually happens when the number of observations per group is small and/or there is not much variation in outcomes.")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# get overall att under calendar time effects
|
|
466
|
+
# this is just average over all time periods
|
|
467
|
+
calendar_att = np.mean(calendar_att_t)
|
|
468
|
+
|
|
469
|
+
# get overall influence function
|
|
470
|
+
calendar_inf_func = get_agg_inf_func(att=calendar_att_t,
|
|
471
|
+
inffunc=calendar_inf_func_t,
|
|
472
|
+
whichones=range(len(calendar_tlist)),
|
|
473
|
+
weights_agg=np.repeat(1/len(calendar_tlist), len(calendar_tlist)),
|
|
474
|
+
wif=None)[:, None]
|
|
475
|
+
calendar_inf_func = np.array(calendar_inf_func)
|
|
476
|
+
|
|
477
|
+
# get overall standard error
|
|
478
|
+
calendar_se = get_se(calendar_inf_func, dp)
|
|
479
|
+
if not np.isnan(calendar_se):
|
|
480
|
+
if calendar_se <= np.sqrt(np.finfo(float).eps) * 10:
|
|
481
|
+
calendar_se = np.nan
|
|
482
|
+
|
|
483
|
+
AGGTEobj_print = AGGTEobj(overall_att=calendar_att,
|
|
484
|
+
overall_se=calendar_se,
|
|
485
|
+
typec=typec,
|
|
486
|
+
egt=list(map(t2orig, calendar_tlist)),
|
|
487
|
+
att_egt=calendar_att_t,
|
|
488
|
+
se_egt=calendar_se_t,
|
|
489
|
+
crit_val_egt=calendar_crit_val,
|
|
490
|
+
inf_function={"calendar_inf_func_t": calendar_inf_func_t,
|
|
491
|
+
"calendar_inf_func": calendar_inf_func},
|
|
492
|
+
call=call,
|
|
493
|
+
DIDparams=dp)
|
|
494
|
+
|
|
495
|
+
return AGGTEobj_print
|