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,420 @@
|
|
|
1
|
+
"""Compiled passes of Sun-Abraham and BJS with covariates or extra fixed effects.
|
|
2
|
+
|
|
3
|
+
Both fit a within-unit regression on a design of period dummies and covariates
|
|
4
|
+
(``linear.RowDesign``); the NumPy code builds that design for each batch and
|
|
5
|
+
loops over cohorts and aggregation groups in Python. The kernels read the raw
|
|
6
|
+
batch arrays, build one unit's design rows as they go, and do the same sums in
|
|
7
|
+
one compiled call per batch. Units are processed in fixed-size chunks whose
|
|
8
|
+
partial sums are added in order, so results do not depend on the number of
|
|
9
|
+
threads. The NumPy code in ``linear.py`` and ``unbalanced.py`` remains the
|
|
10
|
+
reference; the tests compare the two.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numba import njit, prange
|
|
15
|
+
|
|
16
|
+
from .twostage_kernels import CHUNK, _design, _first_stage
|
|
17
|
+
|
|
18
|
+
ENABLED = True # tests switch it off to compare with the array path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _chunks(n):
|
|
22
|
+
return (n + CHUNK - 1) // CHUNK
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _arrays(raw):
|
|
26
|
+
g, y, x, w = raw
|
|
27
|
+
return (
|
|
28
|
+
np.ascontiguousarray(y, dtype=np.float64),
|
|
29
|
+
np.ascontiguousarray(x, dtype=np.float64),
|
|
30
|
+
np.ascontiguousarray(w, dtype=np.float64),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------ Sun-Abraham
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@njit(cache=True, inline="always")
|
|
38
|
+
def _sunab_row(x, i, t, T1, columns, cell, K, X, cols, vals):
|
|
39
|
+
"""Row (i, t) of the design: the kept columns, then the cohort-period cell."""
|
|
40
|
+
_design(x, i, t, T1, columns, X)
|
|
41
|
+
for a in range(K):
|
|
42
|
+
cols[a] = a
|
|
43
|
+
vals[a] = X[t, a]
|
|
44
|
+
n = K
|
|
45
|
+
if cell >= 0:
|
|
46
|
+
cols[n] = K + cell
|
|
47
|
+
vals[n] = 1.0
|
|
48
|
+
n += 1
|
|
49
|
+
return n
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@njit(parallel=True, cache=True)
|
|
53
|
+
def sunab_moments(gidx, y, x, w, columns, cell_at, chunk, Q, q, bad):
|
|
54
|
+
"""Within-unit Gram matrix and moments of [kept columns | cell indicators]."""
|
|
55
|
+
n, T = y.shape
|
|
56
|
+
K = columns.shape[0]
|
|
57
|
+
P = Q.shape[1]
|
|
58
|
+
T1 = T - 1
|
|
59
|
+
for c in prange(Q.shape[0]):
|
|
60
|
+
X = np.empty((T, K))
|
|
61
|
+
cols = np.empty((T, K + 1), dtype=np.int64)
|
|
62
|
+
vals = np.empty((T, K + 1))
|
|
63
|
+
count = np.zeros(T, dtype=np.int64)
|
|
64
|
+
s = np.zeros(P)
|
|
65
|
+
seen = np.zeros(P, dtype=np.bool_)
|
|
66
|
+
touched = np.empty(P, dtype=np.int64)
|
|
67
|
+
for i in range(c * chunk, min(n, (c + 1) * chunk)):
|
|
68
|
+
mass = 0.0
|
|
69
|
+
ysum = 0.0
|
|
70
|
+
ntouch = 0
|
|
71
|
+
for a in range(K):
|
|
72
|
+
touched[ntouch] = a
|
|
73
|
+
ntouch += 1
|
|
74
|
+
seen[a] = True
|
|
75
|
+
gi = gidx[i]
|
|
76
|
+
for t in range(T):
|
|
77
|
+
count[t] = _sunab_row(x, i, t, T1, columns, cell_at[gi, t], K, X, cols[t], vals[t])
|
|
78
|
+
wt = w[i, t]
|
|
79
|
+
yt = y[i, t] if np.isfinite(y[i, t]) else 0.0
|
|
80
|
+
mass += wt
|
|
81
|
+
ysum += wt * yt
|
|
82
|
+
for a in range(count[t]):
|
|
83
|
+
ca = cols[t, a]
|
|
84
|
+
wa = wt * vals[t, a]
|
|
85
|
+
q[c, ca] += wa * yt
|
|
86
|
+
s[ca] += wa
|
|
87
|
+
if not seen[ca]:
|
|
88
|
+
seen[ca] = True
|
|
89
|
+
touched[ntouch] = ca
|
|
90
|
+
ntouch += 1
|
|
91
|
+
for b in range(count[t]):
|
|
92
|
+
Q[c, ca, cols[t, b]] += wa * vals[t, b]
|
|
93
|
+
if mass <= 0.0:
|
|
94
|
+
bad[c] = 1
|
|
95
|
+
else:
|
|
96
|
+
for a in range(ntouch):
|
|
97
|
+
ca = touched[a]
|
|
98
|
+
q[c, ca] -= s[ca] * ysum / mass
|
|
99
|
+
for b in range(ntouch):
|
|
100
|
+
cb = touched[b]
|
|
101
|
+
Q[c, ca, cb] -= s[ca] * s[cb] / mass
|
|
102
|
+
for a in range(ntouch):
|
|
103
|
+
s[touched[a]] = 0.0
|
|
104
|
+
seen[touched[a]] = False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@njit(parallel=True, cache=True)
|
|
108
|
+
def sunab_scores(gidx, y, x, w, columns, cell_at, coef, H, out, bad):
|
|
109
|
+
"""Each unit's score times H: ``out[unit, :]``."""
|
|
110
|
+
n, T = y.shape
|
|
111
|
+
K = columns.shape[0]
|
|
112
|
+
M = H.shape[1]
|
|
113
|
+
T1 = T - 1
|
|
114
|
+
for c in prange((n + CHUNK - 1) // CHUNK):
|
|
115
|
+
X = np.empty((T, K))
|
|
116
|
+
cols = np.empty((T, K + 1), dtype=np.int64)
|
|
117
|
+
vals = np.empty((T, K + 1))
|
|
118
|
+
count = np.zeros(T, dtype=np.int64)
|
|
119
|
+
resid = np.zeros(T)
|
|
120
|
+
for i in range(c * CHUNK, min(n, (c + 1) * CHUNK)):
|
|
121
|
+
gi = gidx[i]
|
|
122
|
+
mass = 0.0
|
|
123
|
+
rsum = 0.0
|
|
124
|
+
for t in range(T):
|
|
125
|
+
count[t] = _sunab_row(x, i, t, T1, columns, cell_at[gi, t], K, X, cols[t], vals[t])
|
|
126
|
+
fit = 0.0
|
|
127
|
+
for a in range(count[t]):
|
|
128
|
+
fit += vals[t, a] * coef[cols[t, a]]
|
|
129
|
+
yt = y[i, t] if np.isfinite(y[i, t]) else 0.0
|
|
130
|
+
resid[t] = yt - fit
|
|
131
|
+
mass += w[i, t]
|
|
132
|
+
rsum += w[i, t] * resid[t]
|
|
133
|
+
if mass <= 0.0:
|
|
134
|
+
bad[c] = 1
|
|
135
|
+
continue
|
|
136
|
+
rsum /= mass
|
|
137
|
+
for t in range(T):
|
|
138
|
+
we = w[i, t] * (resid[t] - rsum)
|
|
139
|
+
if we == 0.0:
|
|
140
|
+
continue
|
|
141
|
+
for a in range(count[t]):
|
|
142
|
+
wa = we * vals[t, a]
|
|
143
|
+
ca = cols[t, a]
|
|
144
|
+
for m in range(M):
|
|
145
|
+
out[i, m] += wa * H[ca, m]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SunAbrahamKernels:
|
|
149
|
+
def __init__(self, store, cells, keep):
|
|
150
|
+
self.store, self.cells = store, cells
|
|
151
|
+
self.columns = np.ascontiguousarray(keep, dtype=np.int64)
|
|
152
|
+
self.K, self.M = len(self.columns), len(cells)
|
|
153
|
+
self.groups = np.asarray(sorted(store.gc))
|
|
154
|
+
t0 = np.asarray(store.times)
|
|
155
|
+
self.cell_at = np.full((len(self.groups), store.periods), -1, dtype=np.int64)
|
|
156
|
+
for j, (cg, t) in enumerate(cells):
|
|
157
|
+
self.cell_at[np.searchsorted(self.groups, cg), int(np.searchsorted(t0, t))] = j
|
|
158
|
+
|
|
159
|
+
def _index(self, g):
|
|
160
|
+
return np.searchsorted(self.groups, g).astype(np.int64)
|
|
161
|
+
|
|
162
|
+
def moments(self, batches):
|
|
163
|
+
P = self.K + self.M
|
|
164
|
+
Q, q = np.zeros((P, P)), np.zeros(P)
|
|
165
|
+
for raw in batches():
|
|
166
|
+
y, x, w = _arrays(raw)
|
|
167
|
+
k = _chunks(len(y))
|
|
168
|
+
parts = (np.zeros((k, P, P)), np.zeros((k, P)), np.zeros(k, dtype=np.int8))
|
|
169
|
+
sunab_moments(self._index(raw[0]), y, x, w, self.columns, self.cell_at, CHUNK, *parts)
|
|
170
|
+
if parts[2].any():
|
|
171
|
+
raise ValueError("each group requires a positive-weight estimation observation")
|
|
172
|
+
Q += parts[0].sum(axis=0)
|
|
173
|
+
q += parts[1].sum(axis=0)
|
|
174
|
+
return Q, q
|
|
175
|
+
|
|
176
|
+
def scores(self, raw, coef, H):
|
|
177
|
+
y, x, w = _arrays(raw)
|
|
178
|
+
out = np.zeros((len(y), H.shape[1]))
|
|
179
|
+
bad = np.zeros(_chunks(len(y)), dtype=np.int8)
|
|
180
|
+
sunab_scores(
|
|
181
|
+
self._index(raw[0]), y, x, w, self.columns, self.cell_at,
|
|
182
|
+
np.ascontiguousarray(coef), np.ascontiguousarray(H), out, bad,
|
|
183
|
+
) # fmt: skip
|
|
184
|
+
if bad.any():
|
|
185
|
+
raise ValueError("each group requires a positive-weight estimation observation")
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------------- BJS
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@njit(parallel=True, cache=True)
|
|
193
|
+
def bjs_cell_sums(
|
|
194
|
+
g, gidx, y, x, w, columns, theta, times, anticipation, cell_at, counts, beta, D, bad
|
|
195
|
+
): # fmt: skip
|
|
196
|
+
"""Cell effects and their derivative with respect to the first stage."""
|
|
197
|
+
n, T = y.shape
|
|
198
|
+
P = columns.shape[0]
|
|
199
|
+
T1 = T - 1
|
|
200
|
+
for c in prange(beta.shape[0]):
|
|
201
|
+
X = np.empty((T, P))
|
|
202
|
+
xm = np.zeros(P)
|
|
203
|
+
u1 = np.zeros(T)
|
|
204
|
+
s1 = np.zeros(P)
|
|
205
|
+
obs = np.zeros(T, dtype=np.bool_)
|
|
206
|
+
for i in range(c * CHUNK, min(n, (c + 1) * CHUNK)):
|
|
207
|
+
if _first_stage(
|
|
208
|
+
g, y, x, w, i, T1, columns, theta, times, anticipation, X, xm, u1, s1, obs
|
|
209
|
+
):
|
|
210
|
+
bad[c] = 1
|
|
211
|
+
continue
|
|
212
|
+
gi = gidx[i]
|
|
213
|
+
for t in range(T):
|
|
214
|
+
j = cell_at[gi, t]
|
|
215
|
+
if j < 0 or not obs[t]:
|
|
216
|
+
continue
|
|
217
|
+
sw = w[i, t] / counts[j]
|
|
218
|
+
beta[c, j] += sw * u1[t]
|
|
219
|
+
for a in range(P):
|
|
220
|
+
D[c, j, a] += sw * (X[t, a] - xm[a])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@njit(parallel=True, cache=True)
|
|
224
|
+
def bjs_totals(
|
|
225
|
+
g, gidx, y, x, w, columns, theta, times, anticipation, cell_at, cell_slot, slot_label,
|
|
226
|
+
slot_ptr, Aw, denom, numer, bad,
|
|
227
|
+
): # fmt: skip
|
|
228
|
+
"""Sums of squares and cross products of each unit's weighted totals and
|
|
229
|
+
effects within each residual-averaging group (``ObservedBJSInference.covariance``)."""
|
|
230
|
+
n, T = y.shape
|
|
231
|
+
P = columns.shape[0]
|
|
232
|
+
Pc = Aw.shape[0]
|
|
233
|
+
T1 = T - 1
|
|
234
|
+
for c in prange(denom.shape[0]):
|
|
235
|
+
X = np.empty((T, P))
|
|
236
|
+
xm = np.zeros(P)
|
|
237
|
+
u1 = np.zeros(T)
|
|
238
|
+
s1 = np.zeros(P)
|
|
239
|
+
obs = np.zeros(T, dtype=np.bool_)
|
|
240
|
+
for i in range(c * CHUNK, min(n, (c + 1) * CHUNK)):
|
|
241
|
+
if _first_stage(
|
|
242
|
+
g, y, x, w, i, T1, columns, theta, times, anticipation, X, xm, u1, s1, obs
|
|
243
|
+
):
|
|
244
|
+
bad[c] = 1
|
|
245
|
+
continue
|
|
246
|
+
gi = gidx[i]
|
|
247
|
+
lo = slot_ptr[gi]
|
|
248
|
+
S = slot_ptr[gi + 1] - lo
|
|
249
|
+
tot = np.zeros((S, Pc))
|
|
250
|
+
eff = np.zeros((S, Pc))
|
|
251
|
+
for t in range(T):
|
|
252
|
+
j = cell_at[gi, t]
|
|
253
|
+
if j < 0:
|
|
254
|
+
continue
|
|
255
|
+
sl = cell_slot[j]
|
|
256
|
+
if obs[t]:
|
|
257
|
+
for a in range(Pc):
|
|
258
|
+
tot[sl, a] += w[i, t] * Aw[a, j]
|
|
259
|
+
for a in range(Pc):
|
|
260
|
+
eff[sl, a] += w[i, t] * u1[t] * Aw[a, j]
|
|
261
|
+
for sl in range(S):
|
|
262
|
+
r = slot_label[lo + sl]
|
|
263
|
+
for a in range(Pc):
|
|
264
|
+
denom[c, a, r] += tot[sl, a] * tot[sl, a]
|
|
265
|
+
numer[c, a, r] += tot[sl, a] * eff[sl, a]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@njit(parallel=True, cache=True)
|
|
269
|
+
def bjs_scores(
|
|
270
|
+
g, gidx, y, x, w, columns, theta, times, anticipation, cell_at, cell_slot, slot_label,
|
|
271
|
+
slot_ptr, Aw, derivative, averages, denom, leaveout, out, invalid, bad,
|
|
272
|
+
): # fmt: skip
|
|
273
|
+
"""Each unit's score of the aggregates: ``out[unit, :]``; ``invalid[chunk, a]``
|
|
274
|
+
marks aggregates with a leave-out residual group of one contributor."""
|
|
275
|
+
n, T = y.shape
|
|
276
|
+
P = columns.shape[0]
|
|
277
|
+
Pc = Aw.shape[0]
|
|
278
|
+
T1 = T - 1
|
|
279
|
+
for c in prange((n + CHUNK - 1) // CHUNK):
|
|
280
|
+
X = np.empty((T, P))
|
|
281
|
+
xm = np.zeros(P)
|
|
282
|
+
u1 = np.zeros(T)
|
|
283
|
+
s1 = np.zeros(P)
|
|
284
|
+
obs = np.zeros(T, dtype=np.bool_)
|
|
285
|
+
for i in range(c * CHUNK, min(n, (c + 1) * CHUNK)):
|
|
286
|
+
if _first_stage(
|
|
287
|
+
g, y, x, w, i, T1, columns, theta, times, anticipation, X, xm, u1, s1, obs
|
|
288
|
+
):
|
|
289
|
+
bad[c] = 1
|
|
290
|
+
continue
|
|
291
|
+
gi = gidx[i]
|
|
292
|
+
lo = slot_ptr[gi]
|
|
293
|
+
S = slot_ptr[gi + 1] - lo
|
|
294
|
+
tot = np.zeros((S, Pc))
|
|
295
|
+
eff = np.zeros((S, Pc))
|
|
296
|
+
for t in range(T):
|
|
297
|
+
j = cell_at[gi, t]
|
|
298
|
+
if j < 0:
|
|
299
|
+
continue
|
|
300
|
+
sl = cell_slot[j]
|
|
301
|
+
if obs[t]:
|
|
302
|
+
for a in range(Pc):
|
|
303
|
+
tot[sl, a] += w[i, t] * Aw[a, j]
|
|
304
|
+
for a in range(Pc):
|
|
305
|
+
eff[sl, a] += w[i, t] * u1[t] * Aw[a, j]
|
|
306
|
+
for a in range(Pc):
|
|
307
|
+
acc = 0.0
|
|
308
|
+
for p in range(P):
|
|
309
|
+
acc += s1[p] * derivative[a, p]
|
|
310
|
+
out[i, a] = -acc
|
|
311
|
+
for sl in range(S):
|
|
312
|
+
r = slot_label[lo + sl]
|
|
313
|
+
for a in range(Pc):
|
|
314
|
+
contribution = eff[sl, a] - tot[sl, a] * averages[a, r]
|
|
315
|
+
if leaveout:
|
|
316
|
+
lev = 0.0
|
|
317
|
+
if denom[a, r] > 0.0:
|
|
318
|
+
lev = tot[sl, a] * tot[sl, a] / denom[a, r]
|
|
319
|
+
if abs(tot[sl, a]) > 0.0 and lev >= 1.0 - 1e-12:
|
|
320
|
+
invalid[c, a] = 1
|
|
321
|
+
if lev < 1.0 - 1e-12:
|
|
322
|
+
contribution = contribution / (1.0 - lev)
|
|
323
|
+
else:
|
|
324
|
+
contribution = 0.0
|
|
325
|
+
out[i, a] += contribution
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class BJSKernels:
|
|
329
|
+
"""Compiled passes of BJS with a ``RowDesign`` (``linear.fit_bjs`` and the
|
|
330
|
+
aggregation covariance in ``unbalanced.ObservedBJSInference``)."""
|
|
331
|
+
|
|
332
|
+
def __init__(self, store, cells, keep, theta, anticipation):
|
|
333
|
+
self.store, self.cells = store, cells
|
|
334
|
+
self.columns = np.ascontiguousarray(keep, dtype=np.int64)
|
|
335
|
+
self.theta = np.ascontiguousarray(theta, dtype=np.float64)
|
|
336
|
+
self.anticipation = int(anticipation)
|
|
337
|
+
self.times = np.ascontiguousarray(store.times, dtype=np.int64)
|
|
338
|
+
self.groups = np.asarray(sorted(store.gc))
|
|
339
|
+
self.cell_at = np.full((len(self.groups), store.periods), -1, dtype=np.int64)
|
|
340
|
+
for j, (cg, t) in enumerate(cells):
|
|
341
|
+
self.cell_at[np.searchsorted(self.groups, cg), int(np.searchsorted(self.times, t))] = j
|
|
342
|
+
|
|
343
|
+
def _arrays(self, raw):
|
|
344
|
+
y, x, w = _arrays(raw)
|
|
345
|
+
g = np.ascontiguousarray(raw[0], dtype=np.int64)
|
|
346
|
+
return g, np.searchsorted(self.groups, g).astype(np.int64), y, x, w
|
|
347
|
+
|
|
348
|
+
def cell_sums(self, batches, counts):
|
|
349
|
+
P, M = len(self.columns), len(self.cells)
|
|
350
|
+
beta, D = np.zeros(M), np.zeros((M, P))
|
|
351
|
+
counts = np.ascontiguousarray(counts, dtype=np.float64)
|
|
352
|
+
for raw in batches():
|
|
353
|
+
g, gi, y, x, w = self._arrays(raw)
|
|
354
|
+
k = _chunks(len(g))
|
|
355
|
+
parts = (np.zeros((k, M)), np.zeros((k, M, P)), np.zeros(k, dtype=np.int8))
|
|
356
|
+
bjs_cell_sums(
|
|
357
|
+
g, gi, y, x, w, self.columns, self.theta, self.times, self.anticipation,
|
|
358
|
+
self.cell_at, counts, *parts,
|
|
359
|
+
) # fmt: skip
|
|
360
|
+
if parts[2].any():
|
|
361
|
+
raise ValueError("BJS requires at least one untreated observation per unit")
|
|
362
|
+
beta += parts[0].sum(axis=0)
|
|
363
|
+
D += parts[1].sum(axis=0)
|
|
364
|
+
return beta, D
|
|
365
|
+
|
|
366
|
+
def grouping(self, labels, counts, A):
|
|
367
|
+
"""Per-cohort slots for the residual-averaging groups of ``labels`` (cell -> group)."""
|
|
368
|
+
G = len(self.groups)
|
|
369
|
+
slot_ptr = np.zeros(G + 1, dtype=np.int64)
|
|
370
|
+
cell_slot = np.zeros(len(self.cells), dtype=np.int64)
|
|
371
|
+
slot_label = []
|
|
372
|
+
for gi, cg in enumerate(self.groups):
|
|
373
|
+
seen = {}
|
|
374
|
+
for j, (cell_cohort, _) in enumerate(self.cells):
|
|
375
|
+
if cell_cohort == cg:
|
|
376
|
+
r = labels[j]
|
|
377
|
+
cell_slot[j] = seen.setdefault(r, len(seen))
|
|
378
|
+
slot_label.extend(seen)
|
|
379
|
+
slot_ptr[gi + 1] = len(slot_label)
|
|
380
|
+
return (
|
|
381
|
+
cell_slot,
|
|
382
|
+
np.asarray(slot_label, dtype=np.int64),
|
|
383
|
+
slot_ptr,
|
|
384
|
+
np.ascontiguousarray(A / np.asarray(counts)[None, :]),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def totals(self, batches, grouping, R):
|
|
388
|
+
cell_slot, slot_label, slot_ptr, Aw = grouping
|
|
389
|
+
Pc = Aw.shape[0]
|
|
390
|
+
denom, numer = np.zeros((Pc, R)), np.zeros((Pc, R))
|
|
391
|
+
for raw in batches():
|
|
392
|
+
g, gi, y, x, w = self._arrays(raw)
|
|
393
|
+
k = _chunks(len(g))
|
|
394
|
+
parts = (np.zeros((k, Pc, R)), np.zeros((k, Pc, R)), np.zeros(k, dtype=np.int8))
|
|
395
|
+
bjs_totals(
|
|
396
|
+
g, gi, y, x, w, self.columns, self.theta, self.times, self.anticipation,
|
|
397
|
+
self.cell_at, cell_slot, slot_label, slot_ptr, Aw, *parts,
|
|
398
|
+
) # fmt: skip
|
|
399
|
+
if parts[2].any():
|
|
400
|
+
raise ValueError("BJS requires at least one untreated observation per unit")
|
|
401
|
+
denom += parts[0].sum(axis=0)
|
|
402
|
+
numer += parts[1].sum(axis=0)
|
|
403
|
+
return denom, numer
|
|
404
|
+
|
|
405
|
+
def scores(self, raw, grouping, derivative, averages, denom, leaveout):
|
|
406
|
+
cell_slot, slot_label, slot_ptr, Aw = grouping
|
|
407
|
+
g, gi, y, x, w = self._arrays(raw)
|
|
408
|
+
out = np.zeros((len(g), Aw.shape[0]))
|
|
409
|
+
k = _chunks(len(g))
|
|
410
|
+
invalid = np.zeros((k, Aw.shape[0]), dtype=np.int8)
|
|
411
|
+
bad = np.zeros(k, dtype=np.int8)
|
|
412
|
+
bjs_scores(
|
|
413
|
+
g, gi, y, x, w, self.columns, self.theta, self.times, self.anticipation,
|
|
414
|
+
self.cell_at, cell_slot, slot_label, slot_ptr, Aw,
|
|
415
|
+
np.ascontiguousarray(derivative), np.ascontiguousarray(averages),
|
|
416
|
+
np.ascontiguousarray(denom), bool(leaveout), out, invalid, bad,
|
|
417
|
+
) # fmt: skip
|
|
418
|
+
if bad.any():
|
|
419
|
+
raise ValueError("BJS requires at least one untreated observation per unit")
|
|
420
|
+
return out, invalid.any(axis=0)
|