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
diff_stream/api.py
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
"""The panel estimators: Callaway-Sant'Anna, Sun-Abraham and BJS imputation."""
|
|
2
|
+
|
|
3
|
+
from numbers import Integral
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import polars as pl
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
from hdfe_stream.core.profile import Component, Profile
|
|
11
|
+
from stream_limits import MemoryBudget, current_rss_gb, detect_cpus, thread_scope
|
|
12
|
+
|
|
13
|
+
from . import _docs, _options
|
|
14
|
+
from ._core.multiplier import MAX_DRAW_ENTRIES, StreamingMultiplier, validate_bootstrap
|
|
15
|
+
from ._core.partition import preprocess_budget_mb, staging_bytes
|
|
16
|
+
from ._present import present, public_profile
|
|
17
|
+
from .bjs import BJSInference
|
|
18
|
+
from .bjs_placebo import fit_placebos
|
|
19
|
+
from .cohort_scores import CohortScoreCovariance
|
|
20
|
+
from .cs import fit_cs
|
|
21
|
+
from .effective_sample import finish_result, threshold
|
|
22
|
+
from .estimators import change_scores, fit_means, setup
|
|
23
|
+
from .results import DIDResult
|
|
24
|
+
from .store import PanelStore, inspect
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fit(
|
|
28
|
+
fml,
|
|
29
|
+
data,
|
|
30
|
+
*,
|
|
31
|
+
panel,
|
|
32
|
+
cohort=None,
|
|
33
|
+
treatment=None,
|
|
34
|
+
estimator="cs",
|
|
35
|
+
control_group="nevertreated",
|
|
36
|
+
base_period="varying",
|
|
37
|
+
vcov="CRV0",
|
|
38
|
+
batch_units=None,
|
|
39
|
+
threads=None,
|
|
40
|
+
memory_gb=None,
|
|
41
|
+
memory_reserve=None,
|
|
42
|
+
workdir=None,
|
|
43
|
+
profile=False,
|
|
44
|
+
preprocess_mb=None,
|
|
45
|
+
avgeffectsby="cohort_time",
|
|
46
|
+
leaveout=False,
|
|
47
|
+
est_method="dr",
|
|
48
|
+
trim_level=0.995,
|
|
49
|
+
panel_mode="balanced",
|
|
50
|
+
anticipation=0,
|
|
51
|
+
weights=None,
|
|
52
|
+
allow_time_varying_weights=False,
|
|
53
|
+
bootstrap_reps=0,
|
|
54
|
+
bootstrap_seed=None,
|
|
55
|
+
bootstrap_weights="rademacher",
|
|
56
|
+
pretrends=0,
|
|
57
|
+
ess_warn_threshold=30,
|
|
58
|
+
ess_suppress_threshold=None,
|
|
59
|
+
specification="dynamic",
|
|
60
|
+
reference_period=None,
|
|
61
|
+
fixed_effects="cohort",
|
|
62
|
+
inference="unconditional",
|
|
63
|
+
max_design_columns=2048,
|
|
64
|
+
reference_group="auto",
|
|
65
|
+
balance=False,
|
|
66
|
+
balance_ecdf=False,
|
|
67
|
+
fe_solver="auto",
|
|
68
|
+
):
|
|
69
|
+
"""Fit any panel estimator from internal option names.
|
|
70
|
+
|
|
71
|
+
The public functions (did_callaway_santanna, ..., did_extended_twfe) and
|
|
72
|
+
refit_bootstrap translate their options with ``_options.internal`` and
|
|
73
|
+
call this. The two-stage and extended-TWFE estimators are dispatched to
|
|
74
|
+
their own modules.
|
|
75
|
+
"""
|
|
76
|
+
if reference_group not in ("auto", "nevertreated", "lasttreated"):
|
|
77
|
+
raise ValueError("reference_group must be auto, nevertreated or lasttreated")
|
|
78
|
+
if not isinstance(balance, bool) or not isinstance(balance_ecdf, bool):
|
|
79
|
+
raise ValueError("covariate_balance and balance_distributions must be True or False")
|
|
80
|
+
if (balance or balance_ecdf) and estimator != "cs":
|
|
81
|
+
raise ValueError("covariate balance diagnostics are available only for CS")
|
|
82
|
+
if balance_ecdf and not balance:
|
|
83
|
+
raise ValueError("balance_distributions requires covariate_balance=True")
|
|
84
|
+
if estimator == "etwfe":
|
|
85
|
+
if (
|
|
86
|
+
base_period != "varying"
|
|
87
|
+
or avgeffectsby != "cohort_time"
|
|
88
|
+
or leaveout
|
|
89
|
+
or est_method != "dr"
|
|
90
|
+
or trim_level != 0.995
|
|
91
|
+
or bootstrap_reps != 0
|
|
92
|
+
or bootstrap_seed is not None
|
|
93
|
+
or bootstrap_weights != "rademacher"
|
|
94
|
+
or pretrends != 0
|
|
95
|
+
or ess_warn_threshold not in (None, 30)
|
|
96
|
+
or ess_suppress_threshold is not None
|
|
97
|
+
or specification != "dynamic"
|
|
98
|
+
or reference_period is not None
|
|
99
|
+
):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"ETWFE does not accept CS/BJS/Gardner-specific options or bootstrap options"
|
|
102
|
+
)
|
|
103
|
+
from .etwfe import _extended_twfe
|
|
104
|
+
|
|
105
|
+
return _extended_twfe(
|
|
106
|
+
fml,
|
|
107
|
+
data,
|
|
108
|
+
panel=panel,
|
|
109
|
+
cohort=cohort,
|
|
110
|
+
treatment=treatment,
|
|
111
|
+
control_group=control_group,
|
|
112
|
+
fixed_effects=fixed_effects,
|
|
113
|
+
inference=inference,
|
|
114
|
+
max_design_columns=max_design_columns,
|
|
115
|
+
vcov=vcov,
|
|
116
|
+
weights=weights,
|
|
117
|
+
allow_time_varying_weights=allow_time_varying_weights,
|
|
118
|
+
panel_mode=panel_mode,
|
|
119
|
+
anticipation=anticipation,
|
|
120
|
+
batch_units=batch_units or 4096,
|
|
121
|
+
threads=threads,
|
|
122
|
+
memory_gb=memory_gb,
|
|
123
|
+
memory_reserve=memory_reserve,
|
|
124
|
+
preprocess_mb=preprocess_mb,
|
|
125
|
+
workdir=workdir,
|
|
126
|
+
profile=profile,
|
|
127
|
+
reference_group=reference_group,
|
|
128
|
+
)
|
|
129
|
+
if fixed_effects != "cohort" or inference != "unconditional" or max_design_columns != 2048:
|
|
130
|
+
raise ValueError("fixed_effects, inference and max_design_columns are ETWFE options")
|
|
131
|
+
if estimator == "gardner":
|
|
132
|
+
if (
|
|
133
|
+
control_group != "nevertreated"
|
|
134
|
+
or base_period != "varying"
|
|
135
|
+
or avgeffectsby != "cohort_time"
|
|
136
|
+
or leaveout
|
|
137
|
+
or est_method != "dr"
|
|
138
|
+
or trim_level != 0.995
|
|
139
|
+
or bootstrap_reps != 0
|
|
140
|
+
or bootstrap_seed is not None
|
|
141
|
+
or bootstrap_weights != "rademacher"
|
|
142
|
+
or pretrends != 0
|
|
143
|
+
or ess_warn_threshold not in (None, 30)
|
|
144
|
+
or ess_suppress_threshold is not None
|
|
145
|
+
):
|
|
146
|
+
raise ValueError("Gardner does not accept CS/BJS-specific options or bootstrap options")
|
|
147
|
+
from .gardner import _two_stage
|
|
148
|
+
|
|
149
|
+
return _two_stage(
|
|
150
|
+
fml,
|
|
151
|
+
data,
|
|
152
|
+
panel=panel,
|
|
153
|
+
cohort=cohort,
|
|
154
|
+
treatment=treatment,
|
|
155
|
+
specification=specification,
|
|
156
|
+
reference_period=reference_period,
|
|
157
|
+
vcov=vcov,
|
|
158
|
+
batch_units=batch_units or 4096,
|
|
159
|
+
threads=threads,
|
|
160
|
+
memory_gb=memory_gb,
|
|
161
|
+
memory_reserve=memory_reserve,
|
|
162
|
+
workdir=workdir,
|
|
163
|
+
profile=profile,
|
|
164
|
+
preprocess_mb=preprocess_mb,
|
|
165
|
+
panel_mode=panel_mode,
|
|
166
|
+
anticipation=anticipation,
|
|
167
|
+
weights=weights,
|
|
168
|
+
allow_time_varying_weights=allow_time_varying_weights,
|
|
169
|
+
reference_group=reference_group,
|
|
170
|
+
)
|
|
171
|
+
if specification != "dynamic" or reference_period is not None:
|
|
172
|
+
raise ValueError("specification and reference_period are Gardner options")
|
|
173
|
+
validate_bootstrap(bootstrap_reps, bootstrap_seed, bootstrap_weights)
|
|
174
|
+
if not isinstance(allow_time_varying_weights, bool):
|
|
175
|
+
raise ValueError("allow_time_varying_weights must be True or False")
|
|
176
|
+
bootstrap_reps = int(bootstrap_reps)
|
|
177
|
+
if estimator != "cs" and bootstrap_reps:
|
|
178
|
+
raise ValueError("unit multiplier bootstrap is currently available only for CS")
|
|
179
|
+
if (
|
|
180
|
+
isinstance(anticipation, (bool, np.bool_))
|
|
181
|
+
or not isinstance(anticipation, Integral)
|
|
182
|
+
or not 0 <= anticipation <= np.iinfo(np.int64).max
|
|
183
|
+
):
|
|
184
|
+
raise ValueError("anticipation must be a nonnegative integer within the int64 range")
|
|
185
|
+
anticipation = int(anticipation)
|
|
186
|
+
if (
|
|
187
|
+
isinstance(pretrends, (bool, np.bool_))
|
|
188
|
+
or not isinstance(pretrends, Integral)
|
|
189
|
+
or not 0 <= pretrends <= np.iinfo(np.int64).max - anticipation
|
|
190
|
+
):
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"pretrend_leads must be a nonnegative integer with anticipation+pretrend_leads within int64 range"
|
|
193
|
+
)
|
|
194
|
+
pretrends = int(pretrends)
|
|
195
|
+
if estimator != "bjs" and pretrends:
|
|
196
|
+
raise ValueError("pretrends is available only for BJS")
|
|
197
|
+
if panel_mode not in ("balanced", "complete", "unbalanced"):
|
|
198
|
+
raise ValueError("panel_mode must be 'balanced', 'complete' or 'unbalanced'")
|
|
199
|
+
if estimator not in ("cs", "sunab", "bjs"):
|
|
200
|
+
raise ValueError("estimator must be 'cs', 'sunab', 'bjs', 'gardner' or 'etwfe'")
|
|
201
|
+
if est_method not in ("reg", "ipw", "dr"):
|
|
202
|
+
raise ValueError("est_method must be 'reg', 'ipw' or 'dr'")
|
|
203
|
+
if isinstance(trim_level, bool) or not np.isfinite(trim_level) or not 0 < trim_level <= 1:
|
|
204
|
+
raise ValueError("propensity_trim must be in (0, 1]")
|
|
205
|
+
if estimator != "cs" and (est_method != "dr" or trim_level != 0.995):
|
|
206
|
+
raise ValueError("est_method and trim_level are CS options")
|
|
207
|
+
if control_group not in ("nevertreated", "notyettreated"):
|
|
208
|
+
raise ValueError("unknown control_group")
|
|
209
|
+
if base_period not in ("varying", "universal"):
|
|
210
|
+
raise ValueError("unknown base_period")
|
|
211
|
+
if estimator != "cs" and control_group != "nevertreated":
|
|
212
|
+
raise ValueError("notyettreated controls are available only for CS")
|
|
213
|
+
if vcov not in ("CRV0", "CRV1"):
|
|
214
|
+
raise ValueError("vcov must be 'CRV0' or 'CRV1'")
|
|
215
|
+
if batch_units is not None and (
|
|
216
|
+
not isinstance(batch_units, int) or isinstance(batch_units, bool) or batch_units < 1
|
|
217
|
+
):
|
|
218
|
+
raise ValueError("batch_units must be a positive integer or None")
|
|
219
|
+
if threads is not None and (
|
|
220
|
+
not isinstance(threads, int) or isinstance(threads, bool) or threads < 1
|
|
221
|
+
):
|
|
222
|
+
raise ValueError("n_threads must be a positive integer")
|
|
223
|
+
if avgeffectsby not in ("cohort_time", "event_time", "cohort", "all"):
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"residual_averaging must be 'cohort_time', 'event_time', 'cohort' or 'all'"
|
|
226
|
+
)
|
|
227
|
+
if not isinstance(leaveout, bool):
|
|
228
|
+
raise ValueError("leave_out must be True or False")
|
|
229
|
+
if estimator != "bjs" and (leaveout or avgeffectsby != "cohort_time"):
|
|
230
|
+
raise ValueError("leaveout and avgeffectsby options are available only for BJS")
|
|
231
|
+
ess_warn_threshold = threshold(ess_warn_threshold, "effective_sample_warning")
|
|
232
|
+
ess_suppress_threshold = threshold(ess_suppress_threshold, "min_effective_sample")
|
|
233
|
+
if estimator != "cs" and (
|
|
234
|
+
ess_suppress_threshold is not None or ess_warn_threshold not in (None, 30)
|
|
235
|
+
):
|
|
236
|
+
raise ValueError("ESS warning/suppression thresholds are CS options")
|
|
237
|
+
|
|
238
|
+
balance_collector = None
|
|
239
|
+
|
|
240
|
+
def finish(result):
|
|
241
|
+
if balance_collector is not None:
|
|
242
|
+
result._balance_table = balance_collector.table()
|
|
243
|
+
result.diagnostics["balance_source_passes"] = balance_collector.source_passes
|
|
244
|
+
result.diagnostics["balance_scale"] = (
|
|
245
|
+
"pooled before-weighting SD, held fixed after weighting"
|
|
246
|
+
)
|
|
247
|
+
result = finish_result(result, ess_warn_threshold, ess_suppress_threshold)
|
|
248
|
+
return present(result, store.period_labels)
|
|
249
|
+
|
|
250
|
+
budget = MemoryBudget(memory_gb, memory_reserve)
|
|
251
|
+
automatic_preprocess = preprocess_mb is None
|
|
252
|
+
preprocess_mb = preprocess_budget_mb(preprocess_mb, budget)
|
|
253
|
+
baseline = current_rss_gb() or 0
|
|
254
|
+
with thread_scope(threads):
|
|
255
|
+
if fe_solver not in ("auto", "dense", "iterative"):
|
|
256
|
+
raise ValueError("fixed_effect_solver must be 'auto', 'dense' or 'iterative'")
|
|
257
|
+
lf, yname, design, fe_terms, iterative = inspect(
|
|
258
|
+
fml,
|
|
259
|
+
data,
|
|
260
|
+
panel,
|
|
261
|
+
cohort,
|
|
262
|
+
treatment=treatment,
|
|
263
|
+
allow_covariates=True,
|
|
264
|
+
weights=weights,
|
|
265
|
+
allow_fe=estimator != "cs",
|
|
266
|
+
fe_solver=fe_solver,
|
|
267
|
+
keep_fe_columns=estimator == "bjs",
|
|
268
|
+
)
|
|
269
|
+
absorbed = fe_terms if iterative else None
|
|
270
|
+
if automatic_preprocess:
|
|
271
|
+
# counting the levels of large fixed effects used memory: size the
|
|
272
|
+
# default from what is left now
|
|
273
|
+
preprocess_mb = preprocess_budget_mb(None, budget)
|
|
274
|
+
if not budget.fits((preprocess_mb * 1e6 + 96e6) / 1e9):
|
|
275
|
+
raise MemoryError(
|
|
276
|
+
"preprocessing buffers exceed job headroom before staging; reduce preprocess_mb or request more memory"
|
|
277
|
+
)
|
|
278
|
+
if workdir is not None:
|
|
279
|
+
Path(workdir).mkdir(parents=True, exist_ok=True)
|
|
280
|
+
with TemporaryDirectory(prefix="diff-stream-", dir=workdir) as run:
|
|
281
|
+
try:
|
|
282
|
+
store = PanelStore(
|
|
283
|
+
lf,
|
|
284
|
+
run,
|
|
285
|
+
batch_units or 16384,
|
|
286
|
+
budget,
|
|
287
|
+
preprocess_mb,
|
|
288
|
+
design,
|
|
289
|
+
panel_mode,
|
|
290
|
+
anticipation,
|
|
291
|
+
require_never_treated=reference_group == "nevertreated",
|
|
292
|
+
retain_actual_cohort=bool(pretrends),
|
|
293
|
+
allow_time_varying_weights=allow_time_varying_weights,
|
|
294
|
+
)
|
|
295
|
+
except ValueError as error:
|
|
296
|
+
if "256" in str(error) and "|" in fml and absorbed is None:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"{error}; fixed_effect_solver='iterative' absorbs the additional "
|
|
299
|
+
"fixed effects instead"
|
|
300
|
+
) from None
|
|
301
|
+
raise
|
|
302
|
+
from .reference import reference_store
|
|
303
|
+
|
|
304
|
+
store, reference_metadata = reference_store(store, reference_group, anticipation)
|
|
305
|
+
unimputed = {}
|
|
306
|
+
if estimator == "bjs" and fe_terms:
|
|
307
|
+
from .absorbed import drop_unimputable
|
|
308
|
+
|
|
309
|
+
store, unimputed = drop_unimputable(store, fe_terms, anticipation)
|
|
310
|
+
if balance:
|
|
311
|
+
from .balance import CSBalanceCollector
|
|
312
|
+
|
|
313
|
+
balance_collector = CSBalanceCollector(store, balance_ecdf)
|
|
314
|
+
# Preparation readers/writers are closed. Return their unused Arrow
|
|
315
|
+
# pool pages before reserving inference/JIT buffers (not live data).
|
|
316
|
+
pa.default_memory_pool().release_unused()
|
|
317
|
+
times, gc, N, means = store.times, store.gc, store.n_units, store.means
|
|
318
|
+
groups, cells, bases, controls, sizes = setup(
|
|
319
|
+
estimator, times, gc, control_group, base_period, anticipation
|
|
320
|
+
)
|
|
321
|
+
omitted = []
|
|
322
|
+
if not store.is_balanced:
|
|
323
|
+
supported = []
|
|
324
|
+
for j, (g, t) in enumerate(cells):
|
|
325
|
+
ti, bi = t - times[0], bases[j]
|
|
326
|
+
ok = store.cell_counts[g][ti] > 0
|
|
327
|
+
if estimator == "cs":
|
|
328
|
+
cc = np.sum(
|
|
329
|
+
[
|
|
330
|
+
store.cell_counts[h]
|
|
331
|
+
for h, take in zip(groups, controls[j], strict=True)
|
|
332
|
+
if take
|
|
333
|
+
],
|
|
334
|
+
axis=0,
|
|
335
|
+
)
|
|
336
|
+
ok = ok and store.cell_counts[g][bi] > 0 and cc[ti] > 0 and cc[bi] > 0
|
|
337
|
+
if ok:
|
|
338
|
+
supported.append(j)
|
|
339
|
+
else:
|
|
340
|
+
omitted.append(
|
|
341
|
+
dict(
|
|
342
|
+
cohort=g,
|
|
343
|
+
time=t,
|
|
344
|
+
reason="no observations in a required cohort/date stratum",
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
cells = [cells[j] for j in supported]
|
|
348
|
+
bases, controls = bases[supported], controls[supported]
|
|
349
|
+
if not cells:
|
|
350
|
+
raise ValueError("no supported observed-row comparisons")
|
|
351
|
+
sample_diagnostics = dict(
|
|
352
|
+
**reference_metadata,
|
|
353
|
+
**unimputed,
|
|
354
|
+
balance_requested=balance,
|
|
355
|
+
balance_ecdf=balance_ecdf,
|
|
356
|
+
pretrends=pretrends,
|
|
357
|
+
placebo_cohort_policy="original known adoption dates, including reference/future cohorts"
|
|
358
|
+
if pretrends
|
|
359
|
+
else None,
|
|
360
|
+
bootstrap_reps=bootstrap_reps,
|
|
361
|
+
bootstrap_seed=int(bootstrap_seed) if bootstrap_seed is not None else None,
|
|
362
|
+
bootstrap_weights=bootstrap_weights if bootstrap_reps else None,
|
|
363
|
+
bootstrap_unit_order="staged whole-unit traversal; seed reproducible for fixed partition layout"
|
|
364
|
+
if bootstrap_reps
|
|
365
|
+
else None,
|
|
366
|
+
sampling_weights=weights,
|
|
367
|
+
weight_convention=(
|
|
368
|
+
"positive time-invariant unit sampling weights"
|
|
369
|
+
if store.weights_time_invariant
|
|
370
|
+
else "positive time-varying observation weights"
|
|
371
|
+
)
|
|
372
|
+
if weights is not None
|
|
373
|
+
else "equal unit weights",
|
|
374
|
+
allow_time_varying_weights=allow_time_varying_weights,
|
|
375
|
+
weights_time_invariant=store.weights_time_invariant,
|
|
376
|
+
unit_weight_anchor=store.unit_weight_anchor,
|
|
377
|
+
weight_scale=store.weight_scale / store.weight_normalizer,
|
|
378
|
+
total_unit_weight=float(N * (store.weight_scale / store.weight_normalizer)),
|
|
379
|
+
cohort_weight_totals={
|
|
380
|
+
str(g): float(v * (store.weight_scale / store.weight_normalizer))
|
|
381
|
+
for g, v in store.gw.items()
|
|
382
|
+
},
|
|
383
|
+
anticipation=anticipation,
|
|
384
|
+
reference_event_time=-anticipation - 1,
|
|
385
|
+
cohort_sizes={str(g): int(n) for g, n in gc.items()},
|
|
386
|
+
time_min=int(times[0]),
|
|
387
|
+
time_max=int(times[-1]),
|
|
388
|
+
panel_mode=panel_mode,
|
|
389
|
+
balanced_sample=store.is_balanced,
|
|
390
|
+
input_units=store.n_units_input,
|
|
391
|
+
input_rows=store.n_rows,
|
|
392
|
+
dropped_units=store.n_units_input - N,
|
|
393
|
+
dropped_rows=store.n_rows - store.n_rows_used,
|
|
394
|
+
omitted_cells=omitted,
|
|
395
|
+
)
|
|
396
|
+
M, T, G = len(cells), len(times), len(groups)
|
|
397
|
+
rich_regression = estimator != "cs" and (
|
|
398
|
+
design is not None
|
|
399
|
+
or not store.weights_time_invariant
|
|
400
|
+
or absorbed is not None
|
|
401
|
+
or (estimator == "bjs" and not store.is_balanced)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Numerical buffers grow with the batch; an automatic batch starts
|
|
405
|
+
# large (fast) and halves until it fits the memory left.
|
|
406
|
+
def buffers(B):
|
|
407
|
+
numeric = 8 * (12 * M * M + 4 * B * M + 8 * G * T * T + 8 * T * T + 4 * B * T)
|
|
408
|
+
if pretrends:
|
|
409
|
+
P = T - 1 + pretrends
|
|
410
|
+
numeric += 8 * (8 * P * P + 4 * T * P + 8 * B * P + 8 * B * T)
|
|
411
|
+
numeric += 1024 * len(store.actual_gc) * min(pretrends, T) + 32e6
|
|
412
|
+
if bootstrap_reps:
|
|
413
|
+
numeric += 8 * (
|
|
414
|
+
6 * bootstrap_reps * M
|
|
415
|
+
+ 2 * max(bootstrap_reps, min(B * bootstrap_reps, MAX_DRAW_ENTRIES))
|
|
416
|
+
)
|
|
417
|
+
if rich_regression:
|
|
418
|
+
K = len(design.names) if design is not None else 0
|
|
419
|
+
P = T - 1 + K + (M if estimator == "sunab" else pretrends)
|
|
420
|
+
numeric += 8 * (8 * P * P + 12 * B * T * (2 * T + K + pretrends))
|
|
421
|
+
elif design is not None:
|
|
422
|
+
K = len(design.names)
|
|
423
|
+
# Nuisance Grams are system-by-K-by-K and row designs are
|
|
424
|
+
# batch-by-time-by-K. Cross-cell scores/covariance above have
|
|
425
|
+
# no covariate axis: neither M-by-M-by-K nor B-by-M-by-K is
|
|
426
|
+
# materialized. Keep generous allowances for four unbalanced
|
|
427
|
+
# outcome strata, Newton information and contrast derivatives.
|
|
428
|
+
numeric += 8 * (8 * M * K * K + 8 * M * T * K + 12 * B * T * K)
|
|
429
|
+
return numeric
|
|
430
|
+
|
|
431
|
+
B = min(batch_units or 16384, N)
|
|
432
|
+
if batch_units is None:
|
|
433
|
+
while B > 256 and not budget.fits(buffers(B) / 1e9 + 128e6 / 1e9):
|
|
434
|
+
B //= 2
|
|
435
|
+
numeric = buffers(B)
|
|
436
|
+
store.batch_units = B
|
|
437
|
+
p = Profile(
|
|
438
|
+
model=f"diff-stream {estimator}",
|
|
439
|
+
threads=threads or detect_cpus(),
|
|
440
|
+
memory_limit_gb=budget.limit_gb,
|
|
441
|
+
memory_reserve=budget.reserve,
|
|
442
|
+
polars_threads=pl.thread_pool_size(),
|
|
443
|
+
baseline_gb=baseline,
|
|
444
|
+
components=[
|
|
445
|
+
Component(
|
|
446
|
+
"numerical buffers and runtime", numeric + 192e6, numeric * 1.5 + 256e6
|
|
447
|
+
),
|
|
448
|
+
Component(
|
|
449
|
+
"bounded partition preprocessing",
|
|
450
|
+
staging_bytes(preprocess_mb, store) + 96e6,
|
|
451
|
+
preprocess_mb * 1e6 + 256e6,
|
|
452
|
+
),
|
|
453
|
+
Component(
|
|
454
|
+
"snapshot, hash partitions and sorted leaves",
|
|
455
|
+
64 * N * T,
|
|
456
|
+
128 * N * T,
|
|
457
|
+
where="disk",
|
|
458
|
+
),
|
|
459
|
+
],
|
|
460
|
+
notes={
|
|
461
|
+
**sample_diagnostics,
|
|
462
|
+
"n_units": N,
|
|
463
|
+
"periods": T,
|
|
464
|
+
"cells": M,
|
|
465
|
+
"batch_units": B,
|
|
466
|
+
"validation": "whole-unit disk partitions; no global unit-ID table",
|
|
467
|
+
"partitions": store.partition_count,
|
|
468
|
+
"max_partition_depth": store.max_depth,
|
|
469
|
+
"max_decoded_leaf_bytes": store.max_leaf_bytes,
|
|
470
|
+
"preprocess_mb": preprocess_mb,
|
|
471
|
+
"covariate_names": design.names if design is not None else [],
|
|
472
|
+
},
|
|
473
|
+
warnings=[
|
|
474
|
+
"Heuristic estimate: source query, readers, writers and allocator retention can exceed allowances."
|
|
475
|
+
],
|
|
476
|
+
)
|
|
477
|
+
if profile:
|
|
478
|
+
return p
|
|
479
|
+
if not budget.fits(numeric / 1e9 + 128e6 / 1e9):
|
|
480
|
+
raise MemoryError(
|
|
481
|
+
"estimated numerical buffers exceed job headroom; reduce batch_units or request more memory"
|
|
482
|
+
)
|
|
483
|
+
multiplier = (
|
|
484
|
+
StreamingMultiplier(2 * M, bootstrap_reps, bootstrap_seed, bootstrap_weights)
|
|
485
|
+
if bootstrap_reps
|
|
486
|
+
else None
|
|
487
|
+
)
|
|
488
|
+
multiplier_fields = (
|
|
489
|
+
dict(
|
|
490
|
+
_multiplier_draws=multiplier.draws[:, :M],
|
|
491
|
+
_multiplier_shares=multiplier.draws[:, M:],
|
|
492
|
+
)
|
|
493
|
+
if multiplier is not None
|
|
494
|
+
else {}
|
|
495
|
+
)
|
|
496
|
+
placebo_fields = {}
|
|
497
|
+
if pretrends:
|
|
498
|
+
if absorbed is not None:
|
|
499
|
+
from .absorbed import fit_placebos_absorbed
|
|
500
|
+
|
|
501
|
+
placebo = fit_placebos_absorbed(
|
|
502
|
+
store,
|
|
503
|
+
pretrends,
|
|
504
|
+
anticipation,
|
|
505
|
+
absorbed,
|
|
506
|
+
fml=fml,
|
|
507
|
+
depvar=yname,
|
|
508
|
+
panel=panel,
|
|
509
|
+
vcov=vcov,
|
|
510
|
+
threads=threads,
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
placebo = fit_placebos(
|
|
514
|
+
store,
|
|
515
|
+
pretrends,
|
|
516
|
+
anticipation,
|
|
517
|
+
fml=fml,
|
|
518
|
+
depvar=yname,
|
|
519
|
+
panel=panel,
|
|
520
|
+
vcov=vcov,
|
|
521
|
+
)
|
|
522
|
+
placebo.diagnostics.update(sampling_weights=weights, panel_mode=panel_mode)
|
|
523
|
+
placebo_fields = dict(_bjs_placebos=placebo)
|
|
524
|
+
if not store.is_balanced or (store.weighted and estimator != "cs") or rich_regression:
|
|
525
|
+
from .cs_unbalanced import fit_cs_unbalanced
|
|
526
|
+
from .unbalanced import fit_bjs, fit_sunab
|
|
527
|
+
|
|
528
|
+
context, cross, share_cov, metadata = None, None, None, {}
|
|
529
|
+
if estimator == "cs":
|
|
530
|
+
att, counts, V, cross, share_cov, metadata = fit_cs_unbalanced(
|
|
531
|
+
store,
|
|
532
|
+
groups,
|
|
533
|
+
cells,
|
|
534
|
+
bases,
|
|
535
|
+
controls,
|
|
536
|
+
est_method,
|
|
537
|
+
trim_level,
|
|
538
|
+
multiplier,
|
|
539
|
+
balance_collector=balance_collector,
|
|
540
|
+
)
|
|
541
|
+
elif rich_regression:
|
|
542
|
+
from . import linear
|
|
543
|
+
|
|
544
|
+
if absorbed is not None and estimator == "sunab":
|
|
545
|
+
from .absorbed import fit_sunab_absorbed
|
|
546
|
+
|
|
547
|
+
att, counts, V, info = fit_sunab_absorbed(
|
|
548
|
+
store, cells, absorbed, threads=threads
|
|
549
|
+
)
|
|
550
|
+
model = None
|
|
551
|
+
metadata.update(info)
|
|
552
|
+
elif estimator == "sunab":
|
|
553
|
+
att, counts, V, model = linear.fit_sunab(store, cells, anticipation)
|
|
554
|
+
else:
|
|
555
|
+
if absorbed is not None:
|
|
556
|
+
from .absorbed import fit_bjs_absorbed
|
|
557
|
+
|
|
558
|
+
att, counts, V, context, info = fit_bjs_absorbed(
|
|
559
|
+
store,
|
|
560
|
+
cells,
|
|
561
|
+
absorbed,
|
|
562
|
+
avgeffectsby,
|
|
563
|
+
leaveout,
|
|
564
|
+
workdir,
|
|
565
|
+
anticipation,
|
|
566
|
+
threads=threads,
|
|
567
|
+
)
|
|
568
|
+
model = None
|
|
569
|
+
metadata.update(info)
|
|
570
|
+
else:
|
|
571
|
+
att, counts, V, context, model = linear.fit_bjs(
|
|
572
|
+
store, cells, avgeffectsby, leaveout, workdir, anticipation
|
|
573
|
+
)
|
|
574
|
+
if model is not None:
|
|
575
|
+
metadata.update(
|
|
576
|
+
regression_backend="streamed weighted unit elimination; bounded joint design",
|
|
577
|
+
regression_columns=[model.names[j] for j in model.keep],
|
|
578
|
+
absorbed_or_redundant_columns=model.dropped,
|
|
579
|
+
)
|
|
580
|
+
metadata.update(
|
|
581
|
+
covariate_names=design.names if design is not None else [],
|
|
582
|
+
fixed_effect_solver="iterative" if absorbed is not None else "dense",
|
|
583
|
+
)
|
|
584
|
+
elif estimator == "sunab":
|
|
585
|
+
att, counts, V = fit_sunab(store, cells, anticipation)
|
|
586
|
+
else:
|
|
587
|
+
att, counts, V, context = fit_bjs(
|
|
588
|
+
store, cells, avgeffectsby, leaveout, workdir, anticipation
|
|
589
|
+
)
|
|
590
|
+
factor = N / (N - 1) if vcov == "CRV1" else 1.0
|
|
591
|
+
diagnostics = dict(
|
|
592
|
+
**sample_diagnostics,
|
|
593
|
+
control_group=control_group,
|
|
594
|
+
base_period=base_period if estimator == "cs" else f"g-{anticipation + 1}",
|
|
595
|
+
inference="unit clustered; observed-row analytic scores",
|
|
596
|
+
periods=T,
|
|
597
|
+
batch_units=B,
|
|
598
|
+
preprocess_mb=preprocess_mb,
|
|
599
|
+
partitions=store.partition_count,
|
|
600
|
+
max_decoded_leaf_bytes=store.max_leaf_bytes,
|
|
601
|
+
warnings=[],
|
|
602
|
+
)
|
|
603
|
+
diagnostics.update(metadata)
|
|
604
|
+
if omitted:
|
|
605
|
+
diagnostics["warnings"].append(
|
|
606
|
+
"Some cells have no required observed-row stratum; inspect omitted_cells."
|
|
607
|
+
)
|
|
608
|
+
if context is not None:
|
|
609
|
+
diagnostics.update(
|
|
610
|
+
avgeffectsby=avgeffectsby,
|
|
611
|
+
leaveout=leaveout,
|
|
612
|
+
aggregation_storage="temporary Parquet; call cleanup() after aggregation",
|
|
613
|
+
)
|
|
614
|
+
if np.isnan(V).any():
|
|
615
|
+
diagnostics["warnings"].append(
|
|
616
|
+
"Some cell leave-out variances are undefined; aggregate over supported groups before inference."
|
|
617
|
+
)
|
|
618
|
+
return finish(
|
|
619
|
+
DIDResult(
|
|
620
|
+
**placebo_fields,
|
|
621
|
+
**multiplier_fields,
|
|
622
|
+
fml=fml,
|
|
623
|
+
depvar=yname,
|
|
624
|
+
coefnames=[f"g::{g}:t::{t}" for g, t in cells],
|
|
625
|
+
beta=att,
|
|
626
|
+
vcov=V * factor,
|
|
627
|
+
vcov_type=vcov,
|
|
628
|
+
df_t=N - 1,
|
|
629
|
+
all_vcovs={vcov: (V * factor, N - 1)},
|
|
630
|
+
n_obs=store.n_rows_used,
|
|
631
|
+
n_clusters={panel[0]: N},
|
|
632
|
+
estimator=estimator,
|
|
633
|
+
cells=cells,
|
|
634
|
+
counts=counts,
|
|
635
|
+
observed_counts=np.array(
|
|
636
|
+
[store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
|
|
637
|
+
),
|
|
638
|
+
cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
|
|
639
|
+
n_units=N,
|
|
640
|
+
diagnostics=diagnostics,
|
|
641
|
+
_share_cross=cross * factor if cross is not None else None,
|
|
642
|
+
_share_cov=share_cov * factor if share_cov is not None else None,
|
|
643
|
+
_bjs_inference=context,
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
if (
|
|
647
|
+
design is not None
|
|
648
|
+
or (store.weighted and estimator == "cs")
|
|
649
|
+
or multiplier is not None
|
|
650
|
+
):
|
|
651
|
+
att, counts, V, cross, share_cov, metadata = fit_cs(
|
|
652
|
+
store,
|
|
653
|
+
groups,
|
|
654
|
+
cells,
|
|
655
|
+
bases,
|
|
656
|
+
controls,
|
|
657
|
+
est_method,
|
|
658
|
+
trim_level,
|
|
659
|
+
multiplier,
|
|
660
|
+
balance_collector=balance_collector,
|
|
661
|
+
)
|
|
662
|
+
factor = N / (N - 1) if vcov == "CRV1" else 1.0
|
|
663
|
+
return finish(
|
|
664
|
+
DIDResult(
|
|
665
|
+
**multiplier_fields,
|
|
666
|
+
fml=fml,
|
|
667
|
+
depvar=yname,
|
|
668
|
+
coefnames=[f"g::{g}:t::{t}" for g, t in cells],
|
|
669
|
+
beta=att,
|
|
670
|
+
vcov=V * factor,
|
|
671
|
+
vcov_type=vcov,
|
|
672
|
+
df_t=N - 1,
|
|
673
|
+
all_vcovs={vcov: (V * factor, N - 1)},
|
|
674
|
+
n_obs=store.n_rows_used,
|
|
675
|
+
n_clusters={panel[0]: N},
|
|
676
|
+
estimator=estimator,
|
|
677
|
+
cells=cells,
|
|
678
|
+
counts=counts,
|
|
679
|
+
observed_counts=np.array(
|
|
680
|
+
[store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
|
|
681
|
+
),
|
|
682
|
+
cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
|
|
683
|
+
n_units=N,
|
|
684
|
+
_share_cross=cross * factor,
|
|
685
|
+
_share_cov=share_cov * factor,
|
|
686
|
+
diagnostics=dict(
|
|
687
|
+
**sample_diagnostics,
|
|
688
|
+
inference="unit clustered; nuisance-adjusted analytic scores",
|
|
689
|
+
control_group=control_group,
|
|
690
|
+
base_period=base_period,
|
|
691
|
+
periods=T,
|
|
692
|
+
batch_units=B,
|
|
693
|
+
partitions=store.partition_count,
|
|
694
|
+
preprocess_mb=preprocess_mb,
|
|
695
|
+
max_decoded_leaf_bytes=store.max_leaf_bytes,
|
|
696
|
+
**metadata,
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
if estimator == "cs" and est_method != "reg":
|
|
701
|
+
ng = np.array([gc[g] for g, t in cells])
|
|
702
|
+
probability = np.minimum(ng / (ng + controls @ sizes), 1 - 1e-6)
|
|
703
|
+
if np.any(probability >= trim_level):
|
|
704
|
+
raise ValueError(
|
|
705
|
+
"no controls remain after propensity trimming in an intercept-only comparison"
|
|
706
|
+
)
|
|
707
|
+
att, counts, args = fit_means(
|
|
708
|
+
estimator, times, groups, cells, bases, controls, sizes, means, anticipation
|
|
709
|
+
)
|
|
710
|
+
V, cross, share_cov = np.zeros((M, M)), np.zeros((M, M)), np.zeros((M, M))
|
|
711
|
+
scatter = np.zeros((G, T, T)) if estimator == "bjs" else None
|
|
712
|
+
# Cell scores are zero outside a unit's cohort's cells; accumulate
|
|
713
|
+
# by cohort. Shares are uncentered here and centered below.
|
|
714
|
+
by_cohort = CohortScoreCovariance(args[0]) if estimator != "bjs" else None
|
|
715
|
+
for g, y in store.batches():
|
|
716
|
+
idx = np.searchsorted(groups, g)
|
|
717
|
+
if estimator == "bjs":
|
|
718
|
+
for k in np.unique(idx):
|
|
719
|
+
centered = y[idx == k] - means[k]
|
|
720
|
+
use = (
|
|
721
|
+
times < groups[k] - anticipation
|
|
722
|
+
if groups[k]
|
|
723
|
+
else np.ones(T, dtype=bool)
|
|
724
|
+
)
|
|
725
|
+
centered -= centered[:, use].mean(axis=1, keepdims=True)
|
|
726
|
+
scatter[k] += centered.T @ centered
|
|
727
|
+
else:
|
|
728
|
+
scores, _ = change_scores(idx, y, *args, sizes)
|
|
729
|
+
by_cohort.add(idx, scores)
|
|
730
|
+
if by_cohort is not None:
|
|
731
|
+
V, cross, share_cov = by_cohort.blocks()
|
|
732
|
+
if estimator != "cs":
|
|
733
|
+
cross, share_cov = np.zeros((M, M)), np.zeros((M, M))
|
|
734
|
+
bjs_inference = None
|
|
735
|
+
if estimator == "bjs":
|
|
736
|
+
_, _, Z, theta, H = args
|
|
737
|
+
bjs_inference = BJSInference(
|
|
738
|
+
np.array(groups),
|
|
739
|
+
sizes,
|
|
740
|
+
times,
|
|
741
|
+
cells,
|
|
742
|
+
means,
|
|
743
|
+
Z,
|
|
744
|
+
theta,
|
|
745
|
+
H,
|
|
746
|
+
scatter,
|
|
747
|
+
avgeffectsby,
|
|
748
|
+
leaveout,
|
|
749
|
+
anticipation,
|
|
750
|
+
)
|
|
751
|
+
V = bjs_inference.covariance(
|
|
752
|
+
np.eye(M), allow_unidentified=avgeffectsby != "cohort_time"
|
|
753
|
+
)
|
|
754
|
+
factor = N / (N - 1) if vcov == "CRV1" else 1.0
|
|
755
|
+
diagnostics = {
|
|
756
|
+
**sample_diagnostics,
|
|
757
|
+
"control_group": control_group,
|
|
758
|
+
"base_period": base_period if estimator == "cs" else f"g-{anticipation + 1}",
|
|
759
|
+
"est_method": est_method if estimator == "cs" else None,
|
|
760
|
+
"inference": "unit clustered; normal; "
|
|
761
|
+
+ ("BJS smart residual averaging" if estimator == "bjs" else "analytic scores"),
|
|
762
|
+
"avgeffectsby": avgeffectsby if estimator == "bjs" else None,
|
|
763
|
+
"leaveout": leaveout if estimator == "bjs" else None,
|
|
764
|
+
"periods": T,
|
|
765
|
+
"batch_units": B,
|
|
766
|
+
"preprocess_mb": preprocess_mb,
|
|
767
|
+
"partitions": store.partition_count,
|
|
768
|
+
"max_decoded_leaf_bytes": store.max_leaf_bytes,
|
|
769
|
+
"warnings": (
|
|
770
|
+
["Small BJS cohorts: consider leaveout=True and broader avgeffectsby groups."]
|
|
771
|
+
if estimator == "bjs" and not leaveout and min(gc[g] for g in gc if g) < 20
|
|
772
|
+
else []
|
|
773
|
+
),
|
|
774
|
+
}
|
|
775
|
+
if estimator == "cs":
|
|
776
|
+
diagnostics["comparisons"] = [
|
|
777
|
+
dict(
|
|
778
|
+
cohort=g,
|
|
779
|
+
time=t,
|
|
780
|
+
cells=[j],
|
|
781
|
+
effective_treated=float(gc[g]),
|
|
782
|
+
effective_controls=float(controls[j] @ sizes),
|
|
783
|
+
)
|
|
784
|
+
for j, (g, t) in enumerate(cells)
|
|
785
|
+
]
|
|
786
|
+
if np.isnan(V).any():
|
|
787
|
+
diagnostics["warnings"].append(
|
|
788
|
+
"Some cell leave-out variances are undefined; aggregate over supported groups before inference."
|
|
789
|
+
)
|
|
790
|
+
return finish(
|
|
791
|
+
DIDResult(
|
|
792
|
+
**placebo_fields,
|
|
793
|
+
fml=fml,
|
|
794
|
+
depvar=yname,
|
|
795
|
+
coefnames=[f"g::{g}:t::{t}" for g, t in cells],
|
|
796
|
+
beta=att,
|
|
797
|
+
vcov=V * factor,
|
|
798
|
+
vcov_type=vcov,
|
|
799
|
+
df_t=N - 1,
|
|
800
|
+
all_vcovs={vcov: (V * factor, N - 1)},
|
|
801
|
+
n_obs=store.n_rows_used,
|
|
802
|
+
n_clusters={panel[0]: N},
|
|
803
|
+
estimator=estimator,
|
|
804
|
+
cells=cells,
|
|
805
|
+
counts=counts,
|
|
806
|
+
observed_counts=np.array(
|
|
807
|
+
[store.cell_counts[g][t - times[0]] for g, t in cells], dtype=float
|
|
808
|
+
),
|
|
809
|
+
cohort_counts=np.array([gc[g] for g, t in cells], dtype=float),
|
|
810
|
+
n_units=N,
|
|
811
|
+
diagnostics=diagnostics,
|
|
812
|
+
_share_cross=cross * factor if estimator == "cs" else None,
|
|
813
|
+
_share_cov=(share_cov - np.outer(counts, counts) / N) * factor
|
|
814
|
+
if estimator == "cs"
|
|
815
|
+
else None,
|
|
816
|
+
_bjs_inference=bjs_inference,
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _public(estimator, options):
|
|
822
|
+
"""Call _fit with a public function's arguments (its locals())."""
|
|
823
|
+
options = dict(options)
|
|
824
|
+
fml, data = options.pop("fml"), options.pop("data")
|
|
825
|
+
return public_profile(_fit(fml, data, estimator=estimator, **_options.internal(options)))
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@_docs.document
|
|
829
|
+
def did_callaway_santanna(
|
|
830
|
+
fml,
|
|
831
|
+
data,
|
|
832
|
+
*,
|
|
833
|
+
panel=None,
|
|
834
|
+
time=None,
|
|
835
|
+
cohort=None,
|
|
836
|
+
treatment=None,
|
|
837
|
+
control_group="never_treated",
|
|
838
|
+
base_period="varying",
|
|
839
|
+
adjustment="doubly_robust",
|
|
840
|
+
propensity_trim=0.995,
|
|
841
|
+
anticipation=0,
|
|
842
|
+
weights=None,
|
|
843
|
+
allow_time_varying_weights=False,
|
|
844
|
+
incomplete_units="error",
|
|
845
|
+
last_treated_controls="if_needed",
|
|
846
|
+
vcov="CRV0",
|
|
847
|
+
multiplier_bootstrap_reps=0,
|
|
848
|
+
multiplier_bootstrap_seed=None,
|
|
849
|
+
multiplier_bootstrap_weights="rademacher",
|
|
850
|
+
effective_sample_warning=30,
|
|
851
|
+
min_effective_sample=None,
|
|
852
|
+
covariate_balance=False,
|
|
853
|
+
balance_distributions=False,
|
|
854
|
+
n_threads=None,
|
|
855
|
+
max_memory_gb=None,
|
|
856
|
+
memory_reserve=None,
|
|
857
|
+
workdir=None,
|
|
858
|
+
profile=False,
|
|
859
|
+
batch_units=None,
|
|
860
|
+
preprocess_mb=None,
|
|
861
|
+
):
|
|
862
|
+
"""Callaway and Sant'Anna's group-time average treatment effects.
|
|
863
|
+
|
|
864
|
+
Estimates the average effect for each cohort (units first treated in the
|
|
865
|
+
same period) in each period, by comparing its outcome changes with those
|
|
866
|
+
of a control group, then aggregates them with ``aggregate()``. Covariates
|
|
867
|
+
enter by outcome regression, inverse probability weighting or the doubly
|
|
868
|
+
robust combination of Sant'Anna and Zhao (2020), as in R's did.
|
|
869
|
+
|
|
870
|
+
Callaway, B. and Sant'Anna, P. H. C. (2021). Difference-in-differences
|
|
871
|
+
with multiple time periods. *Journal of Econometrics* 225(2), 200-230.
|
|
872
|
+
|
|
873
|
+
Sant'Anna, P. H. C. and Zhao, J. (2020). Doubly robust
|
|
874
|
+
difference-in-differences estimators. *Journal of Econometrics* 219(1),
|
|
875
|
+
101-122.
|
|
876
|
+
|
|
877
|
+
Parameters
|
|
878
|
+
----------
|
|
879
|
+
fml : str
|
|
880
|
+
``"y ~ 1"``, or ``"y ~ x + C(region)"`` with covariates (up to 256
|
|
881
|
+
design columns), measured in the comparison's base period.
|
|
882
|
+
{DATA}
|
|
883
|
+
control_group : {"never_treated", "not_yet_treated"}, default "never_treated"
|
|
884
|
+
The comparison units: never-treated units only, or also units not yet
|
|
885
|
+
treated by the period compared.
|
|
886
|
+
base_period : {"varying", "universal"}, default "varying"
|
|
887
|
+
The pre-treatment comparison period: the period just before each
|
|
888
|
+
pre-treatment cell ("varying"), or the last clean period before
|
|
889
|
+
treatment for every cell ("universal"). Post-treatment cells use the
|
|
890
|
+
latter either way.
|
|
891
|
+
adjustment : {"doubly_robust", "regression", "ipw"}, default "doubly_robust"
|
|
892
|
+
How covariates enter: outcome regression on the controls, inverse
|
|
893
|
+
probability weighting with a logit propensity score, or both.
|
|
894
|
+
propensity_trim : float, default 0.995
|
|
895
|
+
Controls with a propensity score above this are dropped from IPW and
|
|
896
|
+
doubly robust comparisons (treated units are kept). 1 disables it.
|
|
897
|
+
{ANTICIPATION}
|
|
898
|
+
{WEIGHTS}
|
|
899
|
+
{INCOMPLETE_UNITS}
|
|
900
|
+
With "keep", comparisons use each period's observed rows, as R's did
|
|
901
|
+
does for unbalanced panels.
|
|
902
|
+
{LAST_TREATED}
|
|
903
|
+
{VCOV}
|
|
904
|
+
multiplier_bootstrap_reps : int, default 0
|
|
905
|
+
Draws of the multiplier bootstrap, which perturbs each unit's
|
|
906
|
+
influence function instead of refitting (R did's default inference).
|
|
907
|
+
It gives simultaneous confidence bands through
|
|
908
|
+
``multiplier_confint()``; the analytic standard errors are unchanged.
|
|
909
|
+
0 skips it. For a bootstrap that refits the model, see
|
|
910
|
+
``refit_bootstrap``.
|
|
911
|
+
multiplier_bootstrap_seed : int, optional
|
|
912
|
+
Seed for the multiplier draws.
|
|
913
|
+
multiplier_bootstrap_weights : {"rademacher", "mammen", "webb", "norm"}, default "rademacher"
|
|
914
|
+
The distribution of the multiplier draws.
|
|
915
|
+
effective_sample_warning : float or None, default 30
|
|
916
|
+
Warn (``LowEffectiveSampleWarning``) when a comparison's treated or
|
|
917
|
+
control group has an effective sample size (Kish) below this. None
|
|
918
|
+
turns the warning off.
|
|
919
|
+
min_effective_sample : float, optional
|
|
920
|
+
Report coefficients whose effective sample is below this as NaN.
|
|
921
|
+
``suppress()`` does the same after fitting.
|
|
922
|
+
covariate_balance : bool, default False
|
|
923
|
+
Save covariate balance tables for each comparison, before and after
|
|
924
|
+
propensity weighting, for ``covariate_balance()``.
|
|
925
|
+
balance_distributions : bool, default False
|
|
926
|
+
With ``covariate_balance``, also compare the covariates' empirical
|
|
927
|
+
distributions.
|
|
928
|
+
{RESOURCES}
|
|
929
|
+
|
|
930
|
+
Returns
|
|
931
|
+
-------
|
|
932
|
+
DIDResult or Profile
|
|
933
|
+
``cohort_time()`` lists the group-time effects; ``aggregate()`` gives
|
|
934
|
+
an event study, ``aggregate("overall")`` the overall ATT.
|
|
935
|
+
"""
|
|
936
|
+
options = dict(locals())
|
|
937
|
+
if time is not None:
|
|
938
|
+
options = _cross_sections(options)
|
|
939
|
+
elif panel is None:
|
|
940
|
+
raise ValueError("give panel=(unit, time), or time= for repeated cross sections")
|
|
941
|
+
options.pop("time")
|
|
942
|
+
return _cross_section_labels(_public("cs", options), time)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _cross_sections(options):
|
|
946
|
+
"""Repeated cross sections: every row is its own unit, as in R did (panel=FALSE)."""
|
|
947
|
+
if options["panel"] is not None:
|
|
948
|
+
raise ValueError(
|
|
949
|
+
"give panel=(unit, time) for a panel or time= for cross sections, not both"
|
|
950
|
+
)
|
|
951
|
+
if options["incomplete_units"] != "error":
|
|
952
|
+
raise ValueError("incomplete_units does not apply to repeated cross sections")
|
|
953
|
+
from .store import source
|
|
954
|
+
|
|
955
|
+
frame = source(options["data"])
|
|
956
|
+
name = "observation"
|
|
957
|
+
while name in frame.collect_schema().names():
|
|
958
|
+
name = "_" + name
|
|
959
|
+
options.update(
|
|
960
|
+
data=frame.with_row_index(name),
|
|
961
|
+
panel=(name, options["time"]),
|
|
962
|
+
incomplete_units="keep",
|
|
963
|
+
)
|
|
964
|
+
return options
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _cross_section_labels(result, time):
|
|
968
|
+
if isinstance(result, DIDResult):
|
|
969
|
+
result.diagnostics["repeated_cross_sections"] = time is not None
|
|
970
|
+
if time is not None:
|
|
971
|
+
result.diagnostics["incomplete_units"] = None
|
|
972
|
+
result.n_clusters = {"observation": result.n_units}
|
|
973
|
+
return result
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@_docs.document
|
|
977
|
+
def did_sun_abraham(
|
|
978
|
+
fml,
|
|
979
|
+
data,
|
|
980
|
+
*,
|
|
981
|
+
panel,
|
|
982
|
+
cohort=None,
|
|
983
|
+
treatment=None,
|
|
984
|
+
anticipation=0,
|
|
985
|
+
weights=None,
|
|
986
|
+
allow_time_varying_weights=False,
|
|
987
|
+
incomplete_units="error",
|
|
988
|
+
last_treated_controls="if_needed",
|
|
989
|
+
fixed_effect_solver="auto",
|
|
990
|
+
vcov="CRV0",
|
|
991
|
+
n_threads=None,
|
|
992
|
+
max_memory_gb=None,
|
|
993
|
+
memory_reserve=None,
|
|
994
|
+
workdir=None,
|
|
995
|
+
profile=False,
|
|
996
|
+
batch_units=None,
|
|
997
|
+
preprocess_mb=None,
|
|
998
|
+
):
|
|
999
|
+
"""Sun and Abraham's interaction-weighted event study.
|
|
1000
|
+
|
|
1001
|
+
Regresses the outcome on a full set of cohort-by-period treatment
|
|
1002
|
+
indicators, with unit and time fixed effects, so that each cohort's
|
|
1003
|
+
effects are estimated separately; ``aggregate()`` then averages them by
|
|
1004
|
+
event time with cohort-share weights. The period before treatment
|
|
1005
|
+
(before ``cohort - anticipation``) is the omitted reference.
|
|
1006
|
+
|
|
1007
|
+
Sun, L. and Abraham, S. (2021). Estimating dynamic treatment effects in
|
|
1008
|
+
event studies with heterogeneous treatment effects. *Journal of
|
|
1009
|
+
Econometrics* 225(2), 175-199.
|
|
1010
|
+
|
|
1011
|
+
Parameters
|
|
1012
|
+
----------
|
|
1013
|
+
fml : str
|
|
1014
|
+
``"y ~ 1"``; covariates ``"y ~ x"``; additional fixed effects after
|
|
1015
|
+
a bar, ``"y ~ x | region^time"``. Unit and time fixed effects are
|
|
1016
|
+
always included.
|
|
1017
|
+
{DATA}
|
|
1018
|
+
{ANTICIPATION}
|
|
1019
|
+
{WEIGHTS}
|
|
1020
|
+
{INCOMPLETE_UNITS}
|
|
1021
|
+
{LAST_TREATED}
|
|
1022
|
+
{FIXED_EFFECT_SOLVER}
|
|
1023
|
+
{VCOV}
|
|
1024
|
+
{RESOURCES}
|
|
1025
|
+
|
|
1026
|
+
Returns
|
|
1027
|
+
-------
|
|
1028
|
+
DIDResult or Profile
|
|
1029
|
+
``cohort_time()`` lists the cohort-period coefficients;
|
|
1030
|
+
``aggregate()`` gives the event study.
|
|
1031
|
+
"""
|
|
1032
|
+
return _public("sunab", locals())
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
@_docs.document
|
|
1036
|
+
def did_bjs(
|
|
1037
|
+
fml,
|
|
1038
|
+
data,
|
|
1039
|
+
*,
|
|
1040
|
+
panel,
|
|
1041
|
+
cohort=None,
|
|
1042
|
+
treatment=None,
|
|
1043
|
+
residual_averaging="cohort_time",
|
|
1044
|
+
leave_out=False,
|
|
1045
|
+
pretrend_leads=0,
|
|
1046
|
+
anticipation=0,
|
|
1047
|
+
weights=None,
|
|
1048
|
+
allow_time_varying_weights=False,
|
|
1049
|
+
incomplete_units="error",
|
|
1050
|
+
last_treated_controls="if_needed",
|
|
1051
|
+
fixed_effect_solver="auto",
|
|
1052
|
+
vcov="CRV0",
|
|
1053
|
+
n_threads=None,
|
|
1054
|
+
max_memory_gb=None,
|
|
1055
|
+
memory_reserve=None,
|
|
1056
|
+
workdir=None,
|
|
1057
|
+
profile=False,
|
|
1058
|
+
batch_units=None,
|
|
1059
|
+
preprocess_mb=None,
|
|
1060
|
+
):
|
|
1061
|
+
"""Borusyak, Jaravel and Spiess's imputation estimator.
|
|
1062
|
+
|
|
1063
|
+
Fits unit and time fixed effects (and any covariates) on the untreated
|
|
1064
|
+
observations only, imputes each treated observation's untreated outcome,
|
|
1065
|
+
and averages the differences. The standard errors are the authors'
|
|
1066
|
+
conservative ones, as in their Stata command ``did_imputation``.
|
|
1067
|
+
|
|
1068
|
+
Borusyak, K., Jaravel, X. and Spiess, J. (2024). Revisiting event-study
|
|
1069
|
+
designs: robust and efficient estimation. *Review of Economic Studies*
|
|
1070
|
+
91(6), 3253-3285.
|
|
1071
|
+
|
|
1072
|
+
Parameters
|
|
1073
|
+
----------
|
|
1074
|
+
fml : str
|
|
1075
|
+
``"y ~ 1"``; covariates ``"y ~ x"``; additional fixed effects after
|
|
1076
|
+
a bar, ``"y ~ x | region^time"``. Unit and time fixed effects are
|
|
1077
|
+
always included. Treated observations whose fixed-effect level has no
|
|
1078
|
+
untreated observations cannot be imputed: they are dropped, with an
|
|
1079
|
+
``UnimputedObservationsWarning``, and ``fit.diagnostics`` reports
|
|
1080
|
+
them (``unimputed_observations``, ``unimputed_cells``).
|
|
1081
|
+
{DATA}
|
|
1082
|
+
residual_averaging : {"cohort_time", "event_time", "cohort", "all"}, default "cohort_time"
|
|
1083
|
+
The groups within which the standard errors average treated
|
|
1084
|
+
residuals (the authors' ``avgeffectsby``). Coarser groups give more
|
|
1085
|
+
conservative standard errors and need fewer observations per group.
|
|
1086
|
+
leave_out : bool, default False
|
|
1087
|
+
Leave each unit out of its own group's average residual (the authors'
|
|
1088
|
+
``leaveout``), which reduces the downward bias in small groups.
|
|
1089
|
+
pretrend_leads : int, default 0
|
|
1090
|
+
For a pre-trend test: fit a separate regression on the untreated
|
|
1091
|
+
observations with indicators for the last ``pretrend_leads`` periods
|
|
1092
|
+
before treatment. ``placebos()`` returns those coefficients and
|
|
1093
|
+
``pretrend_test()`` tests that they are jointly zero. The treatment
|
|
1094
|
+
effects are unchanged. 0 skips it.
|
|
1095
|
+
{ANTICIPATION}
|
|
1096
|
+
{WEIGHTS}
|
|
1097
|
+
{INCOMPLETE_UNITS}
|
|
1098
|
+
{LAST_TREATED}
|
|
1099
|
+
{FIXED_EFFECT_SOLVER}
|
|
1100
|
+
{VCOV}
|
|
1101
|
+
{RESOURCES}
|
|
1102
|
+
|
|
1103
|
+
Returns
|
|
1104
|
+
-------
|
|
1105
|
+
DIDResult or Profile
|
|
1106
|
+
``cohort_time()`` lists the imputed effects by cohort and period;
|
|
1107
|
+
``aggregate()`` gives the event study, ``aggregate("overall")`` the
|
|
1108
|
+
overall ATT. Weighted, unbalanced or covariate fits, and fits with
|
|
1109
|
+
additional fixed effects, keep temporary files for aggregation until
|
|
1110
|
+
``cleanup()``.
|
|
1111
|
+
"""
|
|
1112
|
+
return _public("bjs", locals())
|