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/gardner.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Gardner's two-stage DiD with bounded unit elimination and adjusted inference."""
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from numbers import Integral
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import polars as pl
|
|
11
|
+
import pyarrow as pa
|
|
12
|
+
from hdfe_stream.core.influence import ClusterScoreAccumulator
|
|
13
|
+
from hdfe_stream.core.profile import Component, Profile
|
|
14
|
+
from hdfe_stream.core.results_base import RegressionResult
|
|
15
|
+
from stream_limits import MemoryBudget, current_rss_gb, detect_cpus, thread_scope
|
|
16
|
+
|
|
17
|
+
from . import _docs, _options
|
|
18
|
+
from ._core import twostage_kernels
|
|
19
|
+
from ._core.nuisance import solve_checked
|
|
20
|
+
from ._core.partition import preprocess_budget_mb, staging_bytes
|
|
21
|
+
from ._core.within import row_sums
|
|
22
|
+
from ._present import internal, label_frame, labels_of, present, public_profile
|
|
23
|
+
from .cs import RETAIN_SHARE, RetainedBatches
|
|
24
|
+
from .linear import RowDesign
|
|
25
|
+
from .store import PanelStore, inspect
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(kw_only=True)
|
|
29
|
+
class TwoStageResult(RegressionResult):
|
|
30
|
+
"""Direct second-stage coefficients, rather than cohort-time estimates.
|
|
31
|
+
|
|
32
|
+
Selection returns fitted coefficients without changing either regression.
|
|
33
|
+
No temporary store survives estimation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
n_units: int
|
|
37
|
+
_support: pl.DataFrame = field(repr=False)
|
|
38
|
+
_internal: dict | None = field(default=None, repr=False)
|
|
39
|
+
_periods: object = field(default=None, repr=False)
|
|
40
|
+
estimator = "gardner"
|
|
41
|
+
cells = ()
|
|
42
|
+
_normal_inference = True
|
|
43
|
+
|
|
44
|
+
def support(self):
|
|
45
|
+
"""Observed cohort/date counts and original-scale weights per coefficient."""
|
|
46
|
+
return label_frame(self._support, labels_of(self))
|
|
47
|
+
|
|
48
|
+
def contributions(self):
|
|
49
|
+
"""Support table used by the common pre-treatment Wald helper."""
|
|
50
|
+
return self.support()
|
|
51
|
+
|
|
52
|
+
def select(self, horizons):
|
|
53
|
+
"""Select fitted event-time coefficients, keeping their joint covariance."""
|
|
54
|
+
if internal(self)["specification"] != "dynamic":
|
|
55
|
+
raise ValueError("horizon selection requires an event-study fit (event_study=True)")
|
|
56
|
+
try:
|
|
57
|
+
requested = list(horizons)
|
|
58
|
+
except TypeError as exc:
|
|
59
|
+
raise ValueError("horizons must contain fitted integer event times") from exc
|
|
60
|
+
if not requested or any(
|
|
61
|
+
isinstance(h, bool) or not isinstance(h, Integral) for h in requested
|
|
62
|
+
):
|
|
63
|
+
raise ValueError("horizons must contain fitted integer event times")
|
|
64
|
+
names = [f"event::{int(h)}" for h in sorted(set(requested))]
|
|
65
|
+
if not set(names) <= set(self.coefnames):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"requested horizons were not estimated (possibly the reference period)"
|
|
68
|
+
)
|
|
69
|
+
ix = [self.coefnames.index(name) for name in names]
|
|
70
|
+
out = copy(self)
|
|
71
|
+
out.coefnames = names
|
|
72
|
+
out.beta = self.beta[ix].copy()
|
|
73
|
+
out.vcov = self.vcov[np.ix_(ix, ix)].copy()
|
|
74
|
+
out.all_vcovs = {self.vcov_type: (out.vcov, self.df_t)}
|
|
75
|
+
out._support = self._support.filter(pl.col("aggregate").is_in(names))
|
|
76
|
+
out.diagnostics = {
|
|
77
|
+
**self.diagnostics,
|
|
78
|
+
"selected_horizons": sorted(set(map(int, requested))),
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
def pretrend_test(self, *, level="auto", horizons=None):
|
|
83
|
+
from .pretrend import pretrend_test
|
|
84
|
+
|
|
85
|
+
return pretrend_test(self, level=level, horizons=horizons)
|
|
86
|
+
|
|
87
|
+
def with_vcov(self, vcov):
|
|
88
|
+
if vcov not in ("CRV0", "CRV1"):
|
|
89
|
+
raise ValueError("vcov must be 'CRV0' or 'CRV1'")
|
|
90
|
+
factor = self.n_units / (self.n_units - 1)
|
|
91
|
+
ratio = (factor if vcov == "CRV1" else 1) / (factor if self.vcov_type == "CRV1" else 1)
|
|
92
|
+
out = copy(self)
|
|
93
|
+
out.vcov = self.vcov * ratio
|
|
94
|
+
out.vcov_type = vcov
|
|
95
|
+
out.all_vcovs = {vcov: (out.vcov, out.df_t)}
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def second_stage_support(store, specification, reference, anticipation):
|
|
100
|
+
"""The second stage's coefficients: (support table rows, event times, names).
|
|
101
|
+
|
|
102
|
+
Static fits have one indicator for affected observations (t >=
|
|
103
|
+
g-anticipation); dynamic fits one per observed event time except the
|
|
104
|
+
reference.
|
|
105
|
+
"""
|
|
106
|
+
support = []
|
|
107
|
+
scale = store.weight_scale / store.weight_normalizer
|
|
108
|
+
for g, counts in sorted(store.cell_counts.items()):
|
|
109
|
+
if not g:
|
|
110
|
+
continue
|
|
111
|
+
for ti, t in enumerate(store.times):
|
|
112
|
+
event = int(t) - g
|
|
113
|
+
if not counts[ti] or (specification == "static" and event < -anticipation):
|
|
114
|
+
continue
|
|
115
|
+
if specification == "dynamic" and event == reference:
|
|
116
|
+
continue
|
|
117
|
+
support.append(
|
|
118
|
+
dict(
|
|
119
|
+
aggregate="ATT" if specification == "static" else f"event::{event}",
|
|
120
|
+
cohort=int(g),
|
|
121
|
+
time=int(t),
|
|
122
|
+
event_time=event,
|
|
123
|
+
observed_units=int(counts[ti]),
|
|
124
|
+
weight_total=float(store.cell_weights[g][ti] * scale),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
if not support:
|
|
128
|
+
raise ValueError("no supported Gardner second-stage coefficients")
|
|
129
|
+
horizons = sorted({r["event_time"] for r in support}) if specification == "dynamic" else []
|
|
130
|
+
names = [f"event::{h}" for h in horizons] if specification == "dynamic" else ["ATT"]
|
|
131
|
+
return support, horizons, names
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _fit(store, specification, reference, anticipation):
|
|
135
|
+
model = RowDesign(store)
|
|
136
|
+
compiled = twostage_kernels.ENABLED
|
|
137
|
+
source = RetainedBatches(
|
|
138
|
+
model.raw_batches,
|
|
139
|
+
retain=compiled
|
|
140
|
+
and store.n_units * store.periods * 8 * (2 + len(model.names))
|
|
141
|
+
<= RETAIN_SHARE * store.budget.headroom_gb() * 1e9,
|
|
142
|
+
)
|
|
143
|
+
Q, q = model.reduce(
|
|
144
|
+
clean=lambda g: (g[:, None] == 0) | (store.times < g[:, None] - anticipation),
|
|
145
|
+
source=source,
|
|
146
|
+
)
|
|
147
|
+
support, horizons, names = second_stage_support(store, specification, reference, anticipation)
|
|
148
|
+
P, M = len(model.keep), len(names)
|
|
149
|
+
theta = solve_checked(Q, q, "Gardner untreated covariate/fixed-effect design is unidentified")
|
|
150
|
+
|
|
151
|
+
def batches():
|
|
152
|
+
for g, y, X, w in model.batches():
|
|
153
|
+
obs, _, xm, u1, score1 = model.residuals(
|
|
154
|
+
g, y, X, w, theta, anticipation, label="Gardner"
|
|
155
|
+
)
|
|
156
|
+
event = store.times[None, :] - g[:, None]
|
|
157
|
+
if specification == "static":
|
|
158
|
+
Z = ((g[:, None] != 0) & (event >= -anticipation))[:, :, None]
|
|
159
|
+
else:
|
|
160
|
+
Z = (g[:, None, None] != 0) & (event[:, :, None] == np.asarray(horizons))
|
|
161
|
+
Z = (Z & obs[:, :, None]).astype(np.float64)
|
|
162
|
+
yield X - xm[:, None], Z, w, u1, score1
|
|
163
|
+
|
|
164
|
+
if compiled:
|
|
165
|
+
kernels = twostage_kernels.TwoStageKernels(store.times, anticipation)
|
|
166
|
+
dynamic = specification == "dynamic"
|
|
167
|
+
marks = np.asarray(horizons if dynamic else [0], dtype=np.int64)
|
|
168
|
+
mass, rhs, C = kernels.second_stage(source, model.keep, theta, marks, dynamic)
|
|
169
|
+
if np.any(mass <= 0):
|
|
170
|
+
raise ValueError("Gardner second-stage design is unidentified")
|
|
171
|
+
beta = rhs / mass
|
|
172
|
+
H = solve_checked(Q, C.T, "Gardner first-stage inference design is unidentified")
|
|
173
|
+
accumulator = ClusterScoreAccumulator()
|
|
174
|
+
for raw in source():
|
|
175
|
+
accumulator.add_clusters(
|
|
176
|
+
kernels.scores(raw, model.keep, theta, marks, dynamic, beta, H, mass)
|
|
177
|
+
)
|
|
178
|
+
return beta, accumulator.sums, names, pl.DataFrame(support), model
|
|
179
|
+
|
|
180
|
+
# Unit first-stage scores vanish exactly after clean-sample elimination.
|
|
181
|
+
# The Schur-complement cross derivative uses X - clean-unit-mean(X),
|
|
182
|
+
# even though the second-stage regression itself does not demean Z.
|
|
183
|
+
mass, rhs, C = np.zeros(M), np.zeros(M), np.zeros((M, P))
|
|
184
|
+
for xc, Z, w, u1, _ in batches():
|
|
185
|
+
flat = Z.reshape(-1, M)
|
|
186
|
+
mass += flat.T @ w.ravel()
|
|
187
|
+
rhs += flat.T @ (w * u1).ravel()
|
|
188
|
+
C += flat.T @ (w.reshape(-1, 1) * xc.reshape(-1, xc.shape[2]))
|
|
189
|
+
if np.any(mass <= 0):
|
|
190
|
+
raise ValueError("Gardner second-stage design is unidentified")
|
|
191
|
+
beta = rhs / mass # Supported indicators are mutually exclusive; no intercept.
|
|
192
|
+
H = solve_checked(Q, C.T, "Gardner first-stage inference design is unidentified")
|
|
193
|
+
accumulator = ClusterScoreAccumulator()
|
|
194
|
+
for _, Z, w, u1, score1 in batches():
|
|
195
|
+
u2 = u1 - Z @ beta
|
|
196
|
+
score2 = row_sums(w * u2, Z)
|
|
197
|
+
accumulator.add_clusters((score2 - score1 @ H) / mass)
|
|
198
|
+
return beta, accumulator.sums, names, pl.DataFrame(support), model
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _two_stage(
|
|
202
|
+
fml,
|
|
203
|
+
data,
|
|
204
|
+
*,
|
|
205
|
+
panel,
|
|
206
|
+
cohort=None,
|
|
207
|
+
treatment=None,
|
|
208
|
+
specification="dynamic",
|
|
209
|
+
reference_period=None,
|
|
210
|
+
vcov="CRV0",
|
|
211
|
+
batch_units=4096,
|
|
212
|
+
threads=None,
|
|
213
|
+
memory_gb=None,
|
|
214
|
+
memory_reserve=None,
|
|
215
|
+
workdir=None,
|
|
216
|
+
profile=False,
|
|
217
|
+
preprocess_mb=None,
|
|
218
|
+
panel_mode="balanced",
|
|
219
|
+
anticipation=0,
|
|
220
|
+
weights=None,
|
|
221
|
+
allow_time_varying_weights=False,
|
|
222
|
+
reference_group="auto",
|
|
223
|
+
fe_solver="auto",
|
|
224
|
+
):
|
|
225
|
+
"""Two-stage DiD from internal option names; see did_two_stage.
|
|
226
|
+
|
|
227
|
+
Static fits estimate one indicator for affected observations (t >=
|
|
228
|
+
g-anticipation); dynamic fits every observed event time except the
|
|
229
|
+
reference. There is no second-stage intercept: the omitted reference,
|
|
230
|
+
never-treated rows and a last-treated reference cohort (with its leads)
|
|
231
|
+
have a zero second-stage design. The covariance clusters unit scores
|
|
232
|
+
corrected for the estimated first stage.
|
|
233
|
+
"""
|
|
234
|
+
if specification not in ("static", "dynamic"):
|
|
235
|
+
raise ValueError("specification must be 'static' or 'dynamic'")
|
|
236
|
+
if reference_group not in ("auto", "nevertreated", "lasttreated"):
|
|
237
|
+
raise ValueError("reference_group must be auto, nevertreated or lasttreated")
|
|
238
|
+
if (
|
|
239
|
+
isinstance(anticipation, (bool, np.bool_))
|
|
240
|
+
or not isinstance(anticipation, Integral)
|
|
241
|
+
or not 0 <= anticipation < np.iinfo(np.int64).max
|
|
242
|
+
):
|
|
243
|
+
raise ValueError("anticipation must be a nonnegative integer within the int64 range")
|
|
244
|
+
anticipation = int(anticipation)
|
|
245
|
+
if reference_period is not None and (
|
|
246
|
+
specification != "dynamic"
|
|
247
|
+
or isinstance(reference_period, (bool, np.bool_))
|
|
248
|
+
or not isinstance(reference_period, Integral)
|
|
249
|
+
or not -np.iinfo(np.int64).max <= reference_period < -anticipation
|
|
250
|
+
):
|
|
251
|
+
raise ValueError(
|
|
252
|
+
"reference_period must be a clean integer event time, with event_study=True"
|
|
253
|
+
)
|
|
254
|
+
reference = -anticipation - 1 if reference_period is None else int(reference_period)
|
|
255
|
+
if vcov not in ("CRV0", "CRV1"):
|
|
256
|
+
raise ValueError("vcov must be 'CRV0' or 'CRV1'")
|
|
257
|
+
if panel_mode not in ("balanced", "complete", "unbalanced"):
|
|
258
|
+
raise ValueError("panel_mode must be 'balanced', 'complete' or 'unbalanced'")
|
|
259
|
+
for value, name in [(batch_units, "batch_units"), (threads, "n_threads")]:
|
|
260
|
+
if value is not None and (
|
|
261
|
+
isinstance(value, bool) or not isinstance(value, int) or value < 1
|
|
262
|
+
):
|
|
263
|
+
raise ValueError(f"{name} must be a positive integer")
|
|
264
|
+
if batch_units is None:
|
|
265
|
+
raise ValueError("batch_units must be a positive integer")
|
|
266
|
+
if not isinstance(allow_time_varying_weights, bool):
|
|
267
|
+
raise ValueError("allow_time_varying_weights must be boolean")
|
|
268
|
+
budget = MemoryBudget(memory_gb, memory_reserve)
|
|
269
|
+
preprocess_mb = preprocess_budget_mb(preprocess_mb, budget)
|
|
270
|
+
baseline = current_rss_gb() or 0
|
|
271
|
+
with thread_scope(threads):
|
|
272
|
+
if fe_solver not in ("auto", "dense", "iterative"):
|
|
273
|
+
raise ValueError("fixed_effect_solver must be 'auto', 'dense' or 'iterative'")
|
|
274
|
+
lf, yname, design, fe_terms, iterative = inspect(
|
|
275
|
+
fml,
|
|
276
|
+
data,
|
|
277
|
+
panel,
|
|
278
|
+
cohort,
|
|
279
|
+
treatment=treatment,
|
|
280
|
+
allow_covariates=True,
|
|
281
|
+
weights=weights,
|
|
282
|
+
allow_fe=True,
|
|
283
|
+
fe_solver=fe_solver,
|
|
284
|
+
)
|
|
285
|
+
if not budget.fits((preprocess_mb * 1e6 + 96e6) / 1e9):
|
|
286
|
+
raise MemoryError(
|
|
287
|
+
"preprocessing buffers exceed job headroom before staging; reduce preprocess_mb or request more memory"
|
|
288
|
+
)
|
|
289
|
+
if workdir is not None:
|
|
290
|
+
Path(workdir).mkdir(parents=True, exist_ok=True)
|
|
291
|
+
with TemporaryDirectory(prefix="diff-stream-gardner-", dir=workdir) as run:
|
|
292
|
+
store = PanelStore(
|
|
293
|
+
lf,
|
|
294
|
+
run,
|
|
295
|
+
batch_units,
|
|
296
|
+
budget,
|
|
297
|
+
preprocess_mb,
|
|
298
|
+
design,
|
|
299
|
+
panel_mode,
|
|
300
|
+
anticipation,
|
|
301
|
+
allow_time_varying_weights=allow_time_varying_weights,
|
|
302
|
+
require_never_treated=reference_group == "nevertreated",
|
|
303
|
+
)
|
|
304
|
+
from .reference import reference_store
|
|
305
|
+
|
|
306
|
+
store, reference_metadata = reference_store(store, reference_group, anticipation)
|
|
307
|
+
pa.default_memory_pool().release_unused()
|
|
308
|
+
T, B = store.periods, min(batch_units, store.n_units)
|
|
309
|
+
K = T - 1 + (len(design.names) if design is not None else 0)
|
|
310
|
+
events = {
|
|
311
|
+
int(t) - g
|
|
312
|
+
for g, counts in store.cell_counts.items()
|
|
313
|
+
if g
|
|
314
|
+
for t, n in zip(store.times, counts, strict=True)
|
|
315
|
+
if n
|
|
316
|
+
}
|
|
317
|
+
if specification == "dynamic" and reference not in events:
|
|
318
|
+
raise ValueError("reference_period is not observed in the retained treated cohorts")
|
|
319
|
+
M = len(events - {reference}) if specification == "dynamic" else 1
|
|
320
|
+
numeric = 8 * (10 * K * K + 6 * M * M + 8 * K * M + 12 * B * T * (K + M + T))
|
|
321
|
+
diagnostics = dict(
|
|
322
|
+
**reference_metadata,
|
|
323
|
+
specification=specification,
|
|
324
|
+
aggregation="event_time" if specification == "dynamic" else "static",
|
|
325
|
+
anticipation=anticipation,
|
|
326
|
+
reference_event_time=reference if specification == "dynamic" else None,
|
|
327
|
+
reference_second_stage="zero for designated reference cohorts, including their leads",
|
|
328
|
+
time_min=int(store.times[0]),
|
|
329
|
+
time_max=int(store.times[-1]),
|
|
330
|
+
base_period="zero second-stage design for omitted reference and never-treated observations; no intercept",
|
|
331
|
+
panel_mode=panel_mode,
|
|
332
|
+
balanced_sample=store.is_balanced,
|
|
333
|
+
n_units_input=store.n_units_input,
|
|
334
|
+
dropped_units=store.n_units_input - store.n_units,
|
|
335
|
+
n_rows_used=store.n_rows_used,
|
|
336
|
+
sampling_weights=weights,
|
|
337
|
+
weights_time_invariant=store.weights_time_invariant,
|
|
338
|
+
weight_convention="observation WLS in both stages; unit clustering",
|
|
339
|
+
partitions=store.partition_count,
|
|
340
|
+
max_decoded_leaf_bytes=store.max_leaf_bytes,
|
|
341
|
+
preprocess_mb=preprocess_mb,
|
|
342
|
+
inference="unit scores with first-stage correction; CRV1 is N/(N-1) only",
|
|
343
|
+
)
|
|
344
|
+
if profile:
|
|
345
|
+
return Profile(
|
|
346
|
+
model="diff-stream gardner",
|
|
347
|
+
threads=threads or detect_cpus(),
|
|
348
|
+
memory_limit_gb=budget.limit_gb,
|
|
349
|
+
memory_reserve=budget.reserve,
|
|
350
|
+
polars_threads=pl.thread_pool_size(),
|
|
351
|
+
baseline_gb=baseline,
|
|
352
|
+
components=[
|
|
353
|
+
Component(
|
|
354
|
+
"numerical buffers and runtime", numeric + 192e6, numeric * 1.5 + 256e6
|
|
355
|
+
),
|
|
356
|
+
Component(
|
|
357
|
+
"bounded partition preprocessing",
|
|
358
|
+
staging_bytes(preprocess_mb, store) + 96e6,
|
|
359
|
+
preprocess_mb * 1e6 + 256e6,
|
|
360
|
+
),
|
|
361
|
+
Component(
|
|
362
|
+
"snapshot and sorted partitions",
|
|
363
|
+
64 * store.n_units * T,
|
|
364
|
+
128 * store.n_units * T,
|
|
365
|
+
where="disk",
|
|
366
|
+
),
|
|
367
|
+
],
|
|
368
|
+
notes={
|
|
369
|
+
**diagnostics,
|
|
370
|
+
"n_units": store.n_units,
|
|
371
|
+
"periods": T,
|
|
372
|
+
"coefficients": M,
|
|
373
|
+
},
|
|
374
|
+
warnings=[
|
|
375
|
+
"Heuristic; third-party source operators and allocators are not hard capped."
|
|
376
|
+
],
|
|
377
|
+
)
|
|
378
|
+
if not budget.fits(numeric / 1e9 + 0.128):
|
|
379
|
+
raise MemoryError(
|
|
380
|
+
"estimated numerical buffers exceed job headroom; reduce batch_units or request more memory"
|
|
381
|
+
)
|
|
382
|
+
if iterative:
|
|
383
|
+
from .twostage_absorbed import fit_two_stage_absorbed
|
|
384
|
+
|
|
385
|
+
support, horizons, names = second_stage_support(
|
|
386
|
+
store, specification, reference, anticipation
|
|
387
|
+
)
|
|
388
|
+
beta, V, absorbed_info = fit_two_stage_absorbed(
|
|
389
|
+
store, fe_terms, specification, horizons, anticipation, threads=threads
|
|
390
|
+
)
|
|
391
|
+
fit_info = {
|
|
392
|
+
"first_stage_columns": absorbed_info["first_stage_columns"],
|
|
393
|
+
"absorbed_or_redundant_columns": absorbed_info["absorbed_or_redundant_columns"],
|
|
394
|
+
"fixed_effect_solver": "iterative",
|
|
395
|
+
"absorbed_terms": absorbed_info["absorbed_terms"],
|
|
396
|
+
"regression_backend": absorbed_info["regression_backend"],
|
|
397
|
+
}
|
|
398
|
+
support = pl.DataFrame(support)
|
|
399
|
+
else:
|
|
400
|
+
beta, V, names, support, model = _fit(store, specification, reference, anticipation)
|
|
401
|
+
fit_info = {
|
|
402
|
+
"first_stage_columns": [model.names[j] for j in model.keep],
|
|
403
|
+
"absorbed_or_redundant_columns": model.dropped,
|
|
404
|
+
"fixed_effect_solver": "dense",
|
|
405
|
+
}
|
|
406
|
+
V *= store.n_units / (store.n_units - 1) if vcov == "CRV1" else 1
|
|
407
|
+
V = (V + V.T) / 2
|
|
408
|
+
result = TwoStageResult(
|
|
409
|
+
fml=fml,
|
|
410
|
+
depvar=yname,
|
|
411
|
+
coefnames=names,
|
|
412
|
+
beta=beta,
|
|
413
|
+
vcov=V,
|
|
414
|
+
vcov_type=vcov,
|
|
415
|
+
df_t=store.n_units - 1,
|
|
416
|
+
n_obs=store.n_rows_used,
|
|
417
|
+
n_units=store.n_units,
|
|
418
|
+
n_clusters={panel[0]: store.n_units},
|
|
419
|
+
all_vcovs={vcov: (V, store.n_units - 1)},
|
|
420
|
+
weights=weights,
|
|
421
|
+
_support=support,
|
|
422
|
+
diagnostics={
|
|
423
|
+
**diagnostics,
|
|
424
|
+
**fit_info,
|
|
425
|
+
"warnings": [],
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
return present(result, store.period_labels)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@_docs.document
|
|
432
|
+
def did_two_stage(
|
|
433
|
+
fml,
|
|
434
|
+
data,
|
|
435
|
+
*,
|
|
436
|
+
panel,
|
|
437
|
+
cohort=None,
|
|
438
|
+
treatment=None,
|
|
439
|
+
event_study=True,
|
|
440
|
+
reference_period=None,
|
|
441
|
+
anticipation=0,
|
|
442
|
+
weights=None,
|
|
443
|
+
allow_time_varying_weights=False,
|
|
444
|
+
incomplete_units="error",
|
|
445
|
+
last_treated_controls="if_needed",
|
|
446
|
+
vcov="CRV0",
|
|
447
|
+
fixed_effect_solver="auto",
|
|
448
|
+
n_threads=None,
|
|
449
|
+
max_memory_gb=None,
|
|
450
|
+
memory_reserve=None,
|
|
451
|
+
workdir=None,
|
|
452
|
+
profile=False,
|
|
453
|
+
batch_units=4096,
|
|
454
|
+
preprocess_mb=None,
|
|
455
|
+
):
|
|
456
|
+
"""Gardner's two-stage difference-in-differences.
|
|
457
|
+
|
|
458
|
+
The first stage fits unit and time fixed effects (and any covariates) on
|
|
459
|
+
the untreated observations only; the second stage regresses the
|
|
460
|
+
first-stage residuals of all observations on treatment indicators. The
|
|
461
|
+
standard errors account for the estimated first stage. The point
|
|
462
|
+
estimates match BJS imputation in many designs; the inference differs.
|
|
463
|
+
|
|
464
|
+
Gardner, J. (2022). Two-stage differences in differences. arXiv:2207.05943.
|
|
465
|
+
|
|
466
|
+
Butts, K. and Gardner, J. (2022). did2s: two-stage
|
|
467
|
+
difference-in-differences. *The R Journal* 14(3), 162-173.
|
|
468
|
+
|
|
469
|
+
Parameters
|
|
470
|
+
----------
|
|
471
|
+
fml : str
|
|
472
|
+
``"y ~ 1"``; first-stage covariates ``"y ~ x"``; additional fixed
|
|
473
|
+
effects after a bar, ``"y ~ x | region^time"``. Unit and time fixed
|
|
474
|
+
effects are always included. Fixed effects with many levels
|
|
475
|
+
(county-by-year) are absorbed iteratively; see ``fixed_effect_solver``.
|
|
476
|
+
{DATA}
|
|
477
|
+
event_study : bool, default True
|
|
478
|
+
Estimate one coefficient per event time (an event study). False
|
|
479
|
+
estimates a single overall effect of treatment.
|
|
480
|
+
reference_period : int, optional
|
|
481
|
+
With ``event_study``, the omitted pre-treatment event time. Default:
|
|
482
|
+
the last clean period, ``-anticipation - 1``.
|
|
483
|
+
{ANTICIPATION}
|
|
484
|
+
{WEIGHTS}
|
|
485
|
+
{INCOMPLETE_UNITS}
|
|
486
|
+
{LAST_TREATED}
|
|
487
|
+
{VCOV}
|
|
488
|
+
{FIXED_EFFECT_SOLVER}
|
|
489
|
+
{RESOURCES}
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
TwoStageResult or Profile
|
|
494
|
+
The second-stage coefficients (``event::h`` or ``ATT``) and their
|
|
495
|
+
joint covariance. ``select(horizons)`` reports a subset of event
|
|
496
|
+
times without refitting.
|
|
497
|
+
"""
|
|
498
|
+
options = dict(locals())
|
|
499
|
+
fml, data = options.pop("fml"), options.pop("data")
|
|
500
|
+
return public_profile(_two_stage(fml, data, **_options.internal(options)))
|