diff-stream 0.1.0.dev0__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.
- diff_stream/__init__.py +52 -0
- diff_stream/_bootstrap_worker.py +48 -0
- diff_stream/_core/__init__.py +5 -0
- diff_stream/_core/contrasts.py +199 -0
- diff_stream/_core/covariate_kernels.py +420 -0
- diff_stream/_core/cs_kernels.py +536 -0
- diff_stream/_core/cs_row_kernels.py +495 -0
- diff_stream/_core/design.py +105 -0
- diff_stream/_core/etwfe_kernels.py +288 -0
- diff_stream/_core/functionals.py +30 -0
- diff_stream/_core/multiplier.py +90 -0
- diff_stream/_core/nuisance.py +255 -0
- diff_stream/_core/partition.py +158 -0
- diff_stream/_core/process.py +61 -0
- diff_stream/_core/resample.py +171 -0
- diff_stream/_core/staging_kernels.py +90 -0
- diff_stream/_core/twostage_kernels.py +342 -0
- diff_stream/_core/weights.py +26 -0
- diff_stream/_core/within.py +81 -0
- diff_stream/_docs.py +100 -0
- diff_stream/_options.py +62 -0
- diff_stream/_present.py +202 -0
- diff_stream/absorbed.py +621 -0
- diff_stream/aggregation.py +225 -0
- diff_stream/api.py +1112 -0
- diff_stream/bacon.py +460 -0
- diff_stream/bacon_controls.py +188 -0
- diff_stream/balance.py +149 -0
- diff_stream/bjs.py +101 -0
- diff_stream/bjs_placebo.py +167 -0
- diff_stream/bootstrap.py +704 -0
- diff_stream/cohort_scores.py +68 -0
- diff_stream/cs.py +197 -0
- diff_stream/cs_unbalanced.py +336 -0
- diff_stream/effective_sample.py +73 -0
- diff_stream/estimators.py +95 -0
- diff_stream/etwfe.py +589 -0
- diff_stream/gardner.py +500 -0
- diff_stream/linear.py +339 -0
- diff_stream/periods.py +72 -0
- diff_stream/pretrend.py +97 -0
- diff_stream/py.typed +0 -0
- diff_stream/reference.py +125 -0
- diff_stream/results.py +521 -0
- diff_stream/simulate.py +80 -0
- diff_stream/stacked.py +405 -0
- diff_stream/store.py +706 -0
- diff_stream/twfe_store.py +181 -0
- diff_stream/twfe_weights.py +552 -0
- diff_stream/twostage_absorbed.py +190 -0
- diff_stream/unbalanced.py +297 -0
- diff_stream-0.1.0.dev0.dist-info/METADATA +192 -0
- diff_stream-0.1.0.dev0.dist-info/RECORD +56 -0
- diff_stream-0.1.0.dev0.dist-info/WHEEL +5 -0
- diff_stream-0.1.0.dev0.dist-info/licenses/LICENSE.md +21 -0
- diff_stream-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Compiled passes of the Callaway-Sant'Anna covariate fit on balanced panels.
|
|
2
|
+
|
|
3
|
+
A fit has many comparisons (a cohort against its controls, with the covariates
|
|
4
|
+
of one period), and every pass over the staged units visits each unit once per
|
|
5
|
+
comparison it belongs to. In NumPy that is a few hundred small array operations
|
|
6
|
+
per batch, which the interpreter's lock serializes. The kernels here do the
|
|
7
|
+
same sums in one compiled call per batch, in parallel over comparisons.
|
|
8
|
+
|
|
9
|
+
Each comparison is handled by one thread and adds its rows in the order they
|
|
10
|
+
come, so the sums, and with them the estimates, do not depend on the number of
|
|
11
|
+
threads. The NumPy code in ``nuisance.py``, ``contrasts.py`` and ``cs.py``
|
|
12
|
+
remains the reference (and the path for unbalanced panels); the tests compare
|
|
13
|
+
the two.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from numba import njit, prange
|
|
18
|
+
|
|
19
|
+
ENABLED = True # tests switch it off to compare with the array path
|
|
20
|
+
|
|
21
|
+
REG, IPW, DR = 0, 1, 2
|
|
22
|
+
METHOD_CODES = {"reg": REG, "ipw": IPW, "dr": DR}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@njit(cache=True, inline="always")
|
|
26
|
+
def _expit(z):
|
|
27
|
+
if z >= 0.0:
|
|
28
|
+
return 1.0 / (1.0 + np.exp(-z))
|
|
29
|
+
e = np.exp(z)
|
|
30
|
+
return e / (1.0 + e)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@njit(parallel=True, cache=True)
|
|
34
|
+
def initial_sums(
|
|
35
|
+
g,
|
|
36
|
+
y,
|
|
37
|
+
x,
|
|
38
|
+
w2,
|
|
39
|
+
members,
|
|
40
|
+
cohort,
|
|
41
|
+
xper,
|
|
42
|
+
wper,
|
|
43
|
+
cell_ptr,
|
|
44
|
+
tgt,
|
|
45
|
+
base,
|
|
46
|
+
need_reg,
|
|
47
|
+
counts,
|
|
48
|
+
wsums,
|
|
49
|
+
scale,
|
|
50
|
+
gram,
|
|
51
|
+
reg,
|
|
52
|
+
):
|
|
53
|
+
"""Counts, weight sums, design scales and the control regression's moments.
|
|
54
|
+
|
|
55
|
+
counts[c] = (treated, controls); wsums[c] = (treated weight, control weight,
|
|
56
|
+
treated weight squares, control weight squares).
|
|
57
|
+
"""
|
|
58
|
+
n = g.shape[0]
|
|
59
|
+
K = x.shape[2]
|
|
60
|
+
for c in prange(members.shape[0]):
|
|
61
|
+
first = cell_ptr[c]
|
|
62
|
+
L = cell_ptr[c + 1] - first
|
|
63
|
+
xp = xper[c]
|
|
64
|
+
wp = wper[c]
|
|
65
|
+
# local accumulators: neighbouring comparisons' outputs share cache lines
|
|
66
|
+
cnt = np.zeros(2, dtype=np.int64)
|
|
67
|
+
ws = np.zeros(4)
|
|
68
|
+
sc = np.zeros(K)
|
|
69
|
+
gr = np.zeros((K, K))
|
|
70
|
+
rg = np.zeros((K, L))
|
|
71
|
+
for i in range(n):
|
|
72
|
+
if members[c, g[i]] == 0:
|
|
73
|
+
continue
|
|
74
|
+
treated = g[i] == cohort[c]
|
|
75
|
+
wi = w2[i, wp]
|
|
76
|
+
if treated:
|
|
77
|
+
cnt[0] += 1
|
|
78
|
+
ws[0] += wi
|
|
79
|
+
ws[2] += wi * wi
|
|
80
|
+
else:
|
|
81
|
+
cnt[1] += 1
|
|
82
|
+
ws[1] += wi
|
|
83
|
+
ws[3] += wi * wi
|
|
84
|
+
for a in range(K):
|
|
85
|
+
xa = x[i, xp, a]
|
|
86
|
+
sc[a] += wi * xa * xa
|
|
87
|
+
if need_reg and not treated:
|
|
88
|
+
for a in range(K):
|
|
89
|
+
xa = x[i, xp, a] * wi
|
|
90
|
+
for b in range(K):
|
|
91
|
+
gr[a, b] += xa * x[i, xp, b]
|
|
92
|
+
for k in range(L):
|
|
93
|
+
rg[a, k] += xa * (y[i, tgt[first + k]] - y[i, base[first + k]])
|
|
94
|
+
counts[c, :] = cnt
|
|
95
|
+
wsums[c, :] = ws
|
|
96
|
+
scale[c, :] = sc
|
|
97
|
+
if need_reg:
|
|
98
|
+
gram[c, :, :] = gr
|
|
99
|
+
reg[c, :, :L] = rg
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@njit(parallel=True, cache=True)
|
|
103
|
+
def newton_stats(g, x, w2, members, cohort, xper, wper, beta, active, loss, grad, hess, margin):
|
|
104
|
+
"""Logit loss, gradient, information and smallest margin of the active comparisons."""
|
|
105
|
+
n = g.shape[0]
|
|
106
|
+
K = x.shape[2]
|
|
107
|
+
for c in prange(members.shape[0]):
|
|
108
|
+
if not active[c]:
|
|
109
|
+
continue
|
|
110
|
+
xp = xper[c]
|
|
111
|
+
wp = wper[c]
|
|
112
|
+
total = 0.0
|
|
113
|
+
low = np.inf
|
|
114
|
+
gd = np.zeros(K)
|
|
115
|
+
hs = np.zeros((K, K))
|
|
116
|
+
for i in range(n):
|
|
117
|
+
if members[c, g[i]] == 0:
|
|
118
|
+
continue
|
|
119
|
+
d = 1.0 if g[i] == cohort[c] else 0.0
|
|
120
|
+
wi = w2[i, wp]
|
|
121
|
+
eta = 0.0
|
|
122
|
+
for a in range(K):
|
|
123
|
+
eta += x[i, xp, a] * beta[c, a]
|
|
124
|
+
e = np.exp(-abs(eta))
|
|
125
|
+
p = 1.0 / (1.0 + e) if eta >= 0.0 else e / (1.0 + e)
|
|
126
|
+
total += wi * (max(eta, 0.0) + np.log1p(e) - d * eta)
|
|
127
|
+
r = wi * (d - p)
|
|
128
|
+
v = wi * p * (1.0 - p)
|
|
129
|
+
for a in range(K):
|
|
130
|
+
xa = x[i, xp, a]
|
|
131
|
+
gd[a] += xa * r
|
|
132
|
+
xv = xa * v
|
|
133
|
+
for b in range(K):
|
|
134
|
+
hs[a, b] += xv * x[i, xp, b]
|
|
135
|
+
m = (2.0 * d - 1.0) * eta
|
|
136
|
+
if m < low:
|
|
137
|
+
low = m
|
|
138
|
+
loss[c] = total
|
|
139
|
+
grad[c, :] = gd
|
|
140
|
+
hess[c, :, :] = hs
|
|
141
|
+
margin[c] = low
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@njit(parallel=True, cache=True)
|
|
145
|
+
def contrast_moments(
|
|
146
|
+
g,
|
|
147
|
+
y,
|
|
148
|
+
x,
|
|
149
|
+
w2,
|
|
150
|
+
members,
|
|
151
|
+
cohort,
|
|
152
|
+
xper,
|
|
153
|
+
wper,
|
|
154
|
+
cell_ptr,
|
|
155
|
+
tgt,
|
|
156
|
+
base,
|
|
157
|
+
method,
|
|
158
|
+
beta,
|
|
159
|
+
reg,
|
|
160
|
+
control_max,
|
|
161
|
+
s1,
|
|
162
|
+
xt,
|
|
163
|
+
w0,
|
|
164
|
+
w02,
|
|
165
|
+
s0,
|
|
166
|
+
xc,
|
|
167
|
+
xcy,
|
|
168
|
+
hess,
|
|
169
|
+
pmin,
|
|
170
|
+
pmax,
|
|
171
|
+
trimmed,
|
|
172
|
+
capped,
|
|
173
|
+
):
|
|
174
|
+
"""The moments of one pass after the nuisance fits (``fit_contrasts``)."""
|
|
175
|
+
n = g.shape[0]
|
|
176
|
+
K = x.shape[2]
|
|
177
|
+
cap = 1.0 - 1e-6
|
|
178
|
+
for c in prange(members.shape[0]):
|
|
179
|
+
first = cell_ptr[c]
|
|
180
|
+
L = cell_ptr[c + 1] - first
|
|
181
|
+
xp = xper[c]
|
|
182
|
+
wp = wper[c]
|
|
183
|
+
res = np.empty(L)
|
|
184
|
+
a1 = np.zeros(L)
|
|
185
|
+
axt = np.zeros(K)
|
|
186
|
+
a0 = np.zeros(L)
|
|
187
|
+
axc = np.zeros(K)
|
|
188
|
+
axcy = np.zeros((K, L))
|
|
189
|
+
ah = np.zeros((K, K))
|
|
190
|
+
sw0 = 0.0
|
|
191
|
+
sw02 = 0.0
|
|
192
|
+
lo = 1.0
|
|
193
|
+
hi = 0.0
|
|
194
|
+
nt = 0
|
|
195
|
+
nc = 0
|
|
196
|
+
for i in range(n):
|
|
197
|
+
if members[c, g[i]] == 0:
|
|
198
|
+
continue
|
|
199
|
+
treated = g[i] == cohort[c]
|
|
200
|
+
wi = w2[i, wp]
|
|
201
|
+
for k in range(L):
|
|
202
|
+
r = y[i, tgt[first + k]] - y[i, base[first + k]]
|
|
203
|
+
if method != IPW:
|
|
204
|
+
for a in range(K):
|
|
205
|
+
r -= x[i, xp, a] * reg[c, a, k]
|
|
206
|
+
res[k] = r
|
|
207
|
+
if treated:
|
|
208
|
+
for k in range(L):
|
|
209
|
+
a1[k] += wi * res[k]
|
|
210
|
+
for a in range(K):
|
|
211
|
+
axt[a] += wi * x[i, xp, a]
|
|
212
|
+
if method == REG:
|
|
213
|
+
continue
|
|
214
|
+
eta = 0.0
|
|
215
|
+
for a in range(K):
|
|
216
|
+
eta += x[i, xp, a] * beta[c, a]
|
|
217
|
+
p = min(_expit(eta), cap)
|
|
218
|
+
if p < lo:
|
|
219
|
+
lo = p
|
|
220
|
+
if p > hi:
|
|
221
|
+
hi = p
|
|
222
|
+
if (not treated) and p >= control_max:
|
|
223
|
+
nt += 1
|
|
224
|
+
if p >= cap:
|
|
225
|
+
nc += 1
|
|
226
|
+
wc = 0.0
|
|
227
|
+
if (not treated) and p < control_max:
|
|
228
|
+
wc = p / (1.0 - p) * wi
|
|
229
|
+
sw0 += wc
|
|
230
|
+
sw02 += wc * wc
|
|
231
|
+
for k in range(L):
|
|
232
|
+
a0[k] += wc * res[k]
|
|
233
|
+
v = wi * p * (1.0 - p)
|
|
234
|
+
for a in range(K):
|
|
235
|
+
xa = x[i, xp, a]
|
|
236
|
+
axc[a] += wc * xa
|
|
237
|
+
for k in range(L):
|
|
238
|
+
axcy[a, k] += xa * wc * res[k]
|
|
239
|
+
xv = xa * v
|
|
240
|
+
for b in range(K):
|
|
241
|
+
ah[a, b] += xv * x[i, xp, b]
|
|
242
|
+
s1[c, :L] = a1
|
|
243
|
+
xt[c, :] = axt
|
|
244
|
+
s0[c, :L] = a0
|
|
245
|
+
xc[c, :] = axc
|
|
246
|
+
xcy[c, :, :L] = axcy
|
|
247
|
+
hess[c, :, :] = ah
|
|
248
|
+
w0[c] = sw0
|
|
249
|
+
w02[c] = sw02
|
|
250
|
+
pmin[c] = lo
|
|
251
|
+
pmax[c] = hi
|
|
252
|
+
trimmed[c] = nt
|
|
253
|
+
capped[c] = nc
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@njit(parallel=True, cache=True)
|
|
257
|
+
def unit_scores(
|
|
258
|
+
g,
|
|
259
|
+
y,
|
|
260
|
+
x,
|
|
261
|
+
w2,
|
|
262
|
+
members,
|
|
263
|
+
cohort,
|
|
264
|
+
xper,
|
|
265
|
+
wper,
|
|
266
|
+
cell_ptr,
|
|
267
|
+
tgt,
|
|
268
|
+
base,
|
|
269
|
+
col,
|
|
270
|
+
method,
|
|
271
|
+
beta,
|
|
272
|
+
reg,
|
|
273
|
+
control_max,
|
|
274
|
+
treated_mean,
|
|
275
|
+
control_mean,
|
|
276
|
+
treated_total,
|
|
277
|
+
control_total,
|
|
278
|
+
outcome_adj,
|
|
279
|
+
prop_adj,
|
|
280
|
+
scores,
|
|
281
|
+
):
|
|
282
|
+
"""Each unit's influence-function scores, written into its comparison's columns."""
|
|
283
|
+
n = g.shape[0]
|
|
284
|
+
K = x.shape[2]
|
|
285
|
+
cap = 1.0 - 1e-6
|
|
286
|
+
for c in prange(members.shape[0]):
|
|
287
|
+
first = cell_ptr[c]
|
|
288
|
+
L = cell_ptr[c + 1] - first
|
|
289
|
+
xp = xper[c]
|
|
290
|
+
wp = wper[c]
|
|
291
|
+
for i in range(n):
|
|
292
|
+
if members[c, g[i]] == 0:
|
|
293
|
+
continue
|
|
294
|
+
treated = g[i] == cohort[c]
|
|
295
|
+
wi = w2[i, wp]
|
|
296
|
+
p = 0.0
|
|
297
|
+
wc = 0.0
|
|
298
|
+
if method != REG:
|
|
299
|
+
eta = 0.0
|
|
300
|
+
for a in range(K):
|
|
301
|
+
eta += x[i, xp, a] * beta[c, a]
|
|
302
|
+
p = min(_expit(eta), cap)
|
|
303
|
+
if (not treated) and p < control_max:
|
|
304
|
+
wc = p / (1.0 - p)
|
|
305
|
+
oadj = 0.0
|
|
306
|
+
if method != IPW and not treated:
|
|
307
|
+
for a in range(K):
|
|
308
|
+
oadj += x[i, xp, a] * outcome_adj[c, a]
|
|
309
|
+
dpx = (1.0 if treated else 0.0) - p
|
|
310
|
+
for k in range(L):
|
|
311
|
+
r = y[i, tgt[first + k]] - y[i, base[first + k]]
|
|
312
|
+
if method != IPW:
|
|
313
|
+
for a in range(K):
|
|
314
|
+
r -= x[i, xp, a] * reg[c, a, k]
|
|
315
|
+
phi = 0.0
|
|
316
|
+
if treated:
|
|
317
|
+
phi += (r - treated_mean[c, k]) / treated_total[c]
|
|
318
|
+
if method != REG:
|
|
319
|
+
phi -= wc * (r - control_mean[c, k]) / control_total[c]
|
|
320
|
+
adj = 0.0
|
|
321
|
+
for a in range(K):
|
|
322
|
+
adj += x[i, xp, a] * prop_adj[c, a, k]
|
|
323
|
+
phi += dpx * adj
|
|
324
|
+
if method != IPW and not treated:
|
|
325
|
+
phi += r * oadj
|
|
326
|
+
scores[i, col[first + k]] = phi * wi
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class BalancedCS:
|
|
330
|
+
"""The compiled passes for a set of comparisons on balanced batches.
|
|
331
|
+
|
|
332
|
+
``comparisons`` are ``cs.Comparison`` objects. ``size`` is the number of
|
|
333
|
+
cohort labels (members tables have that length).
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def __init__(self, comparisons, n_cells):
|
|
337
|
+
self.comparisons = comparisons
|
|
338
|
+
C = len(comparisons)
|
|
339
|
+
self.C = C
|
|
340
|
+
self.members = np.ascontiguousarray(
|
|
341
|
+
np.vstack([c.members for c in comparisons]), dtype=np.uint8
|
|
342
|
+
)
|
|
343
|
+
self.cohort = np.array([c.cohort for c in comparisons], dtype=np.int64)
|
|
344
|
+
self.xper = np.array([c.xperiod for c in comparisons], dtype=np.int64)
|
|
345
|
+
self.wper = np.array([c.weightperiod for c in comparisons], dtype=np.int64)
|
|
346
|
+
sizes = np.array([len(c.cells) for c in comparisons], dtype=np.int64)
|
|
347
|
+
self.cell_ptr = np.r_[0, np.cumsum(sizes)].astype(np.int64)
|
|
348
|
+
self.tgt = np.concatenate([c.targets for c in comparisons]).astype(np.int64)
|
|
349
|
+
self.base = np.concatenate([c.bases for c in comparisons]).astype(np.int64)
|
|
350
|
+
self.col = np.concatenate([np.asarray(c.cells) for c in comparisons]).astype(np.int64)
|
|
351
|
+
self.sizes = sizes
|
|
352
|
+
self.Lmax = int(sizes.max())
|
|
353
|
+
self.n_cells = n_cells
|
|
354
|
+
|
|
355
|
+
def arrays(self, raw):
|
|
356
|
+
"""(g, y, x, w2): the batch as contiguous arrays, weights as units by periods."""
|
|
357
|
+
g, y, x, w = raw
|
|
358
|
+
g = np.ascontiguousarray(g, dtype=np.int64)
|
|
359
|
+
y = np.ascontiguousarray(y, dtype=np.float64)
|
|
360
|
+
x = np.ascontiguousarray(x, dtype=np.float64)
|
|
361
|
+
w = np.asarray(w, dtype=np.float64)
|
|
362
|
+
w2 = np.ascontiguousarray(w if w.ndim == 2 else w[:, None])
|
|
363
|
+
return g, y, x, w2
|
|
364
|
+
|
|
365
|
+
def _wper(self, w2):
|
|
366
|
+
return self.wper if w2.shape[1] > 1 else np.zeros(self.C, dtype=np.int64)
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------- nuisance
|
|
369
|
+
def initial(self, raw, K, need_reg, out):
|
|
370
|
+
"""Add a batch's counts, weights, scales and control moments to ``out``."""
|
|
371
|
+
g, y, x, w2 = self.arrays(raw)
|
|
372
|
+
C = self.C
|
|
373
|
+
counts = np.zeros((C, 2), dtype=np.int64)
|
|
374
|
+
wsums = np.zeros((C, 4))
|
|
375
|
+
scale = np.zeros((C, K))
|
|
376
|
+
gram = np.zeros((C, K, K)) if need_reg else np.zeros((1, 1, 1))
|
|
377
|
+
reg = np.zeros((C, K, self.Lmax)) if need_reg else np.zeros((1, 1, 1))
|
|
378
|
+
initial_sums(
|
|
379
|
+
g,
|
|
380
|
+
y,
|
|
381
|
+
x,
|
|
382
|
+
w2,
|
|
383
|
+
self.members,
|
|
384
|
+
self.cohort,
|
|
385
|
+
self.xper,
|
|
386
|
+
self._wper(w2),
|
|
387
|
+
self.cell_ptr,
|
|
388
|
+
self.tgt,
|
|
389
|
+
self.base,
|
|
390
|
+
need_reg,
|
|
391
|
+
counts,
|
|
392
|
+
wsums,
|
|
393
|
+
scale,
|
|
394
|
+
gram,
|
|
395
|
+
reg,
|
|
396
|
+
)
|
|
397
|
+
out["counts"] += counts
|
|
398
|
+
out["wsums"] += wsums
|
|
399
|
+
out["scale"] += scale
|
|
400
|
+
if need_reg:
|
|
401
|
+
out["gram"] += gram
|
|
402
|
+
out["reg"] += reg
|
|
403
|
+
|
|
404
|
+
def newton(self, raw, betas, active, K):
|
|
405
|
+
"""(loss, gradient, information, margin) of a batch for the active comparisons."""
|
|
406
|
+
g, _, x, w2 = self.arrays(raw)
|
|
407
|
+
C = self.C
|
|
408
|
+
loss = np.zeros(C)
|
|
409
|
+
grad = np.zeros((C, K))
|
|
410
|
+
hess = np.zeros((C, K, K))
|
|
411
|
+
margin = np.full(C, np.inf)
|
|
412
|
+
newton_stats(
|
|
413
|
+
g,
|
|
414
|
+
x,
|
|
415
|
+
w2,
|
|
416
|
+
self.members,
|
|
417
|
+
self.cohort,
|
|
418
|
+
self.xper,
|
|
419
|
+
self._wper(w2),
|
|
420
|
+
betas,
|
|
421
|
+
active,
|
|
422
|
+
loss,
|
|
423
|
+
grad,
|
|
424
|
+
hess,
|
|
425
|
+
margin,
|
|
426
|
+
)
|
|
427
|
+
return loss, grad, hess, margin
|
|
428
|
+
|
|
429
|
+
# ---------------------------------------------------------------- contrasts
|
|
430
|
+
def moments(self, raw, method, beta, reg, control_max, K, acc):
|
|
431
|
+
g, y, x, w2 = self.arrays(raw)
|
|
432
|
+
C, L = self.C, self.Lmax
|
|
433
|
+
part = dict(
|
|
434
|
+
s1=np.zeros((C, L)),
|
|
435
|
+
xt=np.zeros((C, K)),
|
|
436
|
+
w0=np.zeros(C),
|
|
437
|
+
w02=np.zeros(C),
|
|
438
|
+
s0=np.zeros((C, L)),
|
|
439
|
+
xc=np.zeros((C, K)),
|
|
440
|
+
xcy=np.zeros((C, K, L)),
|
|
441
|
+
hess=np.zeros((C, K, K)),
|
|
442
|
+
pmin=np.ones(C),
|
|
443
|
+
pmax=np.zeros(C),
|
|
444
|
+
trimmed=np.zeros(C, dtype=np.int64),
|
|
445
|
+
capped=np.zeros(C, dtype=np.int64),
|
|
446
|
+
)
|
|
447
|
+
contrast_moments(
|
|
448
|
+
g,
|
|
449
|
+
y,
|
|
450
|
+
x,
|
|
451
|
+
w2,
|
|
452
|
+
self.members,
|
|
453
|
+
self.cohort,
|
|
454
|
+
self.xper,
|
|
455
|
+
self._wper(w2),
|
|
456
|
+
self.cell_ptr,
|
|
457
|
+
self.tgt,
|
|
458
|
+
self.base,
|
|
459
|
+
METHOD_CODES[method],
|
|
460
|
+
beta,
|
|
461
|
+
reg,
|
|
462
|
+
control_max,
|
|
463
|
+
part["s1"],
|
|
464
|
+
part["xt"],
|
|
465
|
+
part["w0"],
|
|
466
|
+
part["w02"],
|
|
467
|
+
part["s0"],
|
|
468
|
+
part["xc"],
|
|
469
|
+
part["xcy"],
|
|
470
|
+
part["hess"],
|
|
471
|
+
part["pmin"],
|
|
472
|
+
part["pmax"],
|
|
473
|
+
part["trimmed"],
|
|
474
|
+
part["capped"],
|
|
475
|
+
)
|
|
476
|
+
for key in ("s1", "xt", "w0", "w02", "s0", "xc", "xcy", "hess", "trimmed", "capped"):
|
|
477
|
+
acc[key] += part[key]
|
|
478
|
+
acc["pmin"] = np.minimum(acc["pmin"], part["pmin"])
|
|
479
|
+
acc["pmax"] = np.maximum(acc["pmax"], part["pmax"])
|
|
480
|
+
|
|
481
|
+
# ---------------------------------------------------------------- scores
|
|
482
|
+
def scores(self, raw, method, packed, control_max):
|
|
483
|
+
"""The batch's scores, units by cells; ``packed`` as ``pack_fitted`` makes it."""
|
|
484
|
+
g, y, x, w2 = self.arrays(raw)
|
|
485
|
+
out = np.zeros((len(g), self.n_cells))
|
|
486
|
+
unit_scores(
|
|
487
|
+
g,
|
|
488
|
+
y,
|
|
489
|
+
x,
|
|
490
|
+
w2,
|
|
491
|
+
self.members,
|
|
492
|
+
self.cohort,
|
|
493
|
+
self.xper,
|
|
494
|
+
self._wper(w2),
|
|
495
|
+
self.cell_ptr,
|
|
496
|
+
self.tgt,
|
|
497
|
+
self.base,
|
|
498
|
+
self.col,
|
|
499
|
+
METHOD_CODES[method],
|
|
500
|
+
packed["beta"],
|
|
501
|
+
packed["reg"],
|
|
502
|
+
control_max,
|
|
503
|
+
packed["treated_mean"],
|
|
504
|
+
packed["control_mean"],
|
|
505
|
+
packed["treated_total"],
|
|
506
|
+
packed["control_total"],
|
|
507
|
+
packed["outcome_adj"],
|
|
508
|
+
packed["prop_adj"],
|
|
509
|
+
out,
|
|
510
|
+
)
|
|
511
|
+
return out
|
|
512
|
+
|
|
513
|
+
def pack_fitted(self, fitted, K):
|
|
514
|
+
"""Arrays of the fitted contrasts' parameters, padded to the widest comparison."""
|
|
515
|
+
C, L = self.C, self.Lmax
|
|
516
|
+
packed = dict(
|
|
517
|
+
beta=np.zeros((C, K)),
|
|
518
|
+
reg=np.zeros((C, K, L)),
|
|
519
|
+
treated_mean=np.zeros((C, L)),
|
|
520
|
+
control_mean=np.zeros((C, L)),
|
|
521
|
+
treated_total=np.ones(C),
|
|
522
|
+
control_total=np.ones(C),
|
|
523
|
+
outcome_adj=np.zeros((C, K)),
|
|
524
|
+
prop_adj=np.zeros((C, K, L)),
|
|
525
|
+
)
|
|
526
|
+
for j, f in enumerate(fitted):
|
|
527
|
+
n = self.sizes[j]
|
|
528
|
+
packed["beta"][j] = f.nuisance.beta
|
|
529
|
+
packed["reg"][j, :, :n] = f.nuisance.regression
|
|
530
|
+
packed["treated_mean"][j, :n] = f.treated_mean
|
|
531
|
+
packed["control_mean"][j, :n] = f.control_mean
|
|
532
|
+
packed["treated_total"][j] = f.treated_total
|
|
533
|
+
packed["control_total"][j] = f.control_total
|
|
534
|
+
packed["outcome_adj"][j] = f.outcome_adjustment
|
|
535
|
+
packed["prop_adj"][j, :, :n] = f.propensity_adjustment
|
|
536
|
+
return packed
|