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/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Disk-backed staggered difference-in-differences and TWFE diagnostics."""
|
|
2
|
+
|
|
3
|
+
import stream_limits as _limits
|
|
4
|
+
|
|
5
|
+
_limits.apply_scheduler_defaults()
|
|
6
|
+
# The compiled passes make many short parallel calls, which run faster when idle
|
|
7
|
+
# OpenMP threads sleep than when they spin. A policy the user has set is kept.
|
|
8
|
+
_limits.set_openmp_wait_policy()
|
|
9
|
+
|
|
10
|
+
from hdfe_stream.core.profile import Profile # noqa: E402
|
|
11
|
+
from hdfe_stream.core.reporting import etable # noqa: E402
|
|
12
|
+
from stream_limits import limit_threads # noqa: E402
|
|
13
|
+
|
|
14
|
+
from .absorbed import UnimputedObservationsWarning # noqa: E402
|
|
15
|
+
from .api import did_bjs, did_callaway_santanna, did_sun_abraham # noqa: E402
|
|
16
|
+
from .bacon import BaconResult, bacon_decomp # noqa: E402
|
|
17
|
+
from .bjs_placebo import BJSPlaceboResult # noqa: E402
|
|
18
|
+
from .bootstrap import BootstrapFailureWarning, RefitBootstrapResult, refit_bootstrap # noqa: E402
|
|
19
|
+
from .effective_sample import LowEffectiveSampleWarning # noqa: E402
|
|
20
|
+
from .etwfe import did_extended_twfe # noqa: E402
|
|
21
|
+
from .gardner import TwoStageResult, did_two_stage # noqa: E402
|
|
22
|
+
from .reference import ReferenceWindowWarning # noqa: E402
|
|
23
|
+
from .results import DIDResult # noqa: E402
|
|
24
|
+
from .stacked import StackedResult, did_stacked # noqa: E402
|
|
25
|
+
from .twfe_weights import TWFEWeightResult, twfe_weights # noqa: E402
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0.dev0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"did_callaway_santanna",
|
|
30
|
+
"did_sun_abraham",
|
|
31
|
+
"did_bjs",
|
|
32
|
+
"did_two_stage",
|
|
33
|
+
"did_extended_twfe",
|
|
34
|
+
"did_stacked",
|
|
35
|
+
"bacon_decomp",
|
|
36
|
+
"twfe_weights",
|
|
37
|
+
"refit_bootstrap",
|
|
38
|
+
"DIDResult",
|
|
39
|
+
"TwoStageResult",
|
|
40
|
+
"StackedResult",
|
|
41
|
+
"BJSPlaceboResult",
|
|
42
|
+
"BaconResult",
|
|
43
|
+
"TWFEWeightResult",
|
|
44
|
+
"RefitBootstrapResult",
|
|
45
|
+
"BootstrapFailureWarning",
|
|
46
|
+
"LowEffectiveSampleWarning",
|
|
47
|
+
"ReferenceWindowWarning",
|
|
48
|
+
"UnimputedObservationsWarning",
|
|
49
|
+
"Profile",
|
|
50
|
+
"etable",
|
|
51
|
+
"limit_threads",
|
|
52
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Private fresh-process refit entry point; configure limits before numerics."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pickle
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
job, output, threads, memory = sys.argv[1:]
|
|
12
|
+
import stream_limits
|
|
13
|
+
|
|
14
|
+
spill = Path(job).parent / "spill"
|
|
15
|
+
spill.mkdir()
|
|
16
|
+
stream_limits.set_limits(
|
|
17
|
+
threads=int(threads), memory_gb=float(memory), spill_dir=spill, override=True
|
|
18
|
+
)
|
|
19
|
+
# Import this same checkout/wheel, including when invoked from a notebook
|
|
20
|
+
# or from a working directory outside the package repository.
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
22
|
+
try:
|
|
23
|
+
# Let the package apply scheduler defaults before importing Polars.
|
|
24
|
+
from diff_stream.bootstrap import _refit_attempt # noqa: I001
|
|
25
|
+
|
|
26
|
+
import polars as pl
|
|
27
|
+
|
|
28
|
+
with open(job, "rb") as f:
|
|
29
|
+
task = pickle.load(f) # Private, locally written task; never external input.
|
|
30
|
+
result = _refit_attempt(task)
|
|
31
|
+
payload = dict(
|
|
32
|
+
result=result,
|
|
33
|
+
resources=dict(pid=os.getpid(), polars_threads=pl.thread_pool_size()),
|
|
34
|
+
)
|
|
35
|
+
except BaseException as error:
|
|
36
|
+
# Preserve resource/programming exceptions instead of treating them
|
|
37
|
+
# as statistical failures or returning a partial bootstrap result.
|
|
38
|
+
try:
|
|
39
|
+
pickle.dumps(error)
|
|
40
|
+
except Exception:
|
|
41
|
+
error = RuntimeError(f"{type(error).__name__}: {error}")
|
|
42
|
+
payload = dict(error=error, traceback=traceback.format_exc())
|
|
43
|
+
with open(output, "wb") as f:
|
|
44
|
+
pickle.dump(payload, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Reusable ATT mean contrasts with regression/estimated-propensity corrections.
|
|
2
|
+
|
|
3
|
+
Outcomes can be any columns, including changes supplied by a DiD consumer.
|
|
4
|
+
Scores are on the estimate scale, so joint covariance is sum_i phi_i phi_i'.
|
|
5
|
+
The array path supplements the shared expression-based IPW implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import polars as pl
|
|
12
|
+
from hdfe_stream.core.ipw import ipw_weight
|
|
13
|
+
from scipy.special import expit
|
|
14
|
+
|
|
15
|
+
from .nuisance import solve_checked
|
|
16
|
+
from .weights import weighted_system
|
|
17
|
+
|
|
18
|
+
_ATT_WEIGHT = ipw_weight("d", pl.col("p"), "att")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def att_parts(X, d, beta, control_max):
|
|
22
|
+
"""R-compatible ATT convention: cap p at 1-1e-6, trim controls only.
|
|
23
|
+
|
|
24
|
+
The propensity and regression models still use all selected units.
|
|
25
|
+
Trim membership is held fixed for analytic inference.
|
|
26
|
+
"""
|
|
27
|
+
p = np.minimum(expit(X @ beta), 1 - 1e-6)
|
|
28
|
+
raw = pl.DataFrame({"d": d, "p": p}).select(_ATT_WEIGHT.alias("w"))["w"].to_numpy()
|
|
29
|
+
control = np.where((~d) & (p < control_max), raw, 0.0)
|
|
30
|
+
return p, control
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AdjustedContrast:
|
|
35
|
+
nuisance: object
|
|
36
|
+
method: str
|
|
37
|
+
control_max: float
|
|
38
|
+
treated_total: float
|
|
39
|
+
control_total: float
|
|
40
|
+
treated_mean: np.ndarray
|
|
41
|
+
control_mean: np.ndarray
|
|
42
|
+
outcome_adjustment: np.ndarray
|
|
43
|
+
propensity_adjustment: np.ndarray
|
|
44
|
+
diagnostics: dict
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def estimate(self):
|
|
48
|
+
return self.treated_mean - self.control_mean
|
|
49
|
+
|
|
50
|
+
def scores(self, X, d, Y, weights=None):
|
|
51
|
+
if not len(d):
|
|
52
|
+
return np.zeros_like(Y)
|
|
53
|
+
if self.method == "reg":
|
|
54
|
+
p, wc = np.zeros(len(d)), np.zeros(len(d))
|
|
55
|
+
else:
|
|
56
|
+
p, wc = att_parts(X, d, self.nuisance.beta, self.control_max)
|
|
57
|
+
residual = Y if self.method == "ipw" else Y - X @ self.nuisance.regression
|
|
58
|
+
phi = d[:, None] * (residual - self.treated_mean) / self.treated_total
|
|
59
|
+
if self.method != "reg":
|
|
60
|
+
phi -= wc[:, None] * (residual - self.control_mean) / self.control_total
|
|
61
|
+
phi += ((d - p)[:, None] * X) @ self.propensity_adjustment
|
|
62
|
+
if self.method != "ipw":
|
|
63
|
+
phi += (~d)[:, None] * residual * (X @ self.outcome_adjustment)[:, None]
|
|
64
|
+
return phi if weights is None else phi * weights[:, None]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fit_contrasts(batches, transforms, systems, method, control_max, *, fast=None):
|
|
68
|
+
"""One common moment pass after nuisance fitting, for any number of outcomes.
|
|
69
|
+
|
|
70
|
+
``fast`` (``cs_kernels.BalancedCS``) does the pass in compiled code.
|
|
71
|
+
"""
|
|
72
|
+
moments = []
|
|
73
|
+
for s in systems:
|
|
74
|
+
K, L = s.regression.shape
|
|
75
|
+
moments.append(
|
|
76
|
+
dict(
|
|
77
|
+
w0=0.0,
|
|
78
|
+
w02=0.0,
|
|
79
|
+
s0=np.zeros(L),
|
|
80
|
+
s1=np.zeros(L),
|
|
81
|
+
xt=np.zeros(K),
|
|
82
|
+
xc=np.zeros(K),
|
|
83
|
+
xcy=np.zeros((K, L)),
|
|
84
|
+
H=np.zeros((K, K)),
|
|
85
|
+
pmin=1.0,
|
|
86
|
+
pmax=0.0,
|
|
87
|
+
trimmed=0,
|
|
88
|
+
capped=0,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
if fast is not None:
|
|
92
|
+
C = len(systems)
|
|
93
|
+
K = systems[0].regression.shape[0]
|
|
94
|
+
L = fast.Lmax
|
|
95
|
+
acc = dict(
|
|
96
|
+
s1=np.zeros((C, L)),
|
|
97
|
+
xt=np.zeros((C, K)),
|
|
98
|
+
w0=np.zeros(C),
|
|
99
|
+
w02=np.zeros(C),
|
|
100
|
+
s0=np.zeros((C, L)),
|
|
101
|
+
xc=np.zeros((C, K)),
|
|
102
|
+
xcy=np.zeros((C, K, L)),
|
|
103
|
+
hess=np.zeros((C, K, K)),
|
|
104
|
+
pmin=np.ones(C),
|
|
105
|
+
pmax=np.zeros(C),
|
|
106
|
+
trimmed=np.zeros(C, dtype=np.int64),
|
|
107
|
+
capped=np.zeros(C, dtype=np.int64),
|
|
108
|
+
)
|
|
109
|
+
beta = np.array([s.beta for s in systems])
|
|
110
|
+
reg = np.zeros((C, K, L))
|
|
111
|
+
for j, s in enumerate(systems):
|
|
112
|
+
reg[j, :, : s.regression.shape[1]] = s.regression
|
|
113
|
+
for raw in batches():
|
|
114
|
+
fast.moments(raw, method, beta, reg, control_max, K, acc)
|
|
115
|
+
for j, m in enumerate(moments):
|
|
116
|
+
n = systems[j].regression.shape[1]
|
|
117
|
+
m.update(
|
|
118
|
+
w0=float(acc["w0"][j]),
|
|
119
|
+
w02=float(acc["w02"][j]),
|
|
120
|
+
s1=acc["s1"][j, :n].copy(),
|
|
121
|
+
s0=acc["s0"][j, :n].copy(),
|
|
122
|
+
xt=acc["xt"][j].copy(),
|
|
123
|
+
xc=acc["xc"][j].copy(),
|
|
124
|
+
xcy=acc["xcy"][j, :, :n].copy(),
|
|
125
|
+
H=acc["hess"][j].copy(),
|
|
126
|
+
pmin=float(acc["pmin"][j]),
|
|
127
|
+
pmax=float(acc["pmax"][j]),
|
|
128
|
+
trimmed=int(acc["trimmed"][j]),
|
|
129
|
+
capped=int(acc["capped"][j]),
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
for raw in batches():
|
|
133
|
+
for j, transform in enumerate(transforms):
|
|
134
|
+
X, d, Y, w = weighted_system(transform(raw))
|
|
135
|
+
if not len(d):
|
|
136
|
+
continue
|
|
137
|
+
s, m = systems[j], moments[j]
|
|
138
|
+
residual = Y if method == "ipw" else Y - X @ s.regression
|
|
139
|
+
m["s1"] += w[d] @ residual[d]
|
|
140
|
+
m["xt"] += w[d] @ X[d]
|
|
141
|
+
if method != "reg":
|
|
142
|
+
p, wc = att_parts(X, d, s.beta, control_max)
|
|
143
|
+
wc = wc * w
|
|
144
|
+
m["pmin"] = min(m["pmin"], float(p.min()))
|
|
145
|
+
m["pmax"] = max(m["pmax"], float(p.max()))
|
|
146
|
+
m["trimmed"] += int(np.sum((~d) & (p >= control_max)))
|
|
147
|
+
m["capped"] += int(np.sum(p >= 1 - 1e-6))
|
|
148
|
+
m["w0"] += wc.sum()
|
|
149
|
+
m["w02"] += wc @ wc
|
|
150
|
+
m["s0"] += wc @ residual
|
|
151
|
+
m["xc"] += wc @ X
|
|
152
|
+
m["xcy"] += X.T @ (wc[:, None] * residual)
|
|
153
|
+
m["H"] += X.T @ ((w * p * (1 - p))[:, None] * X)
|
|
154
|
+
fitted = []
|
|
155
|
+
for s, m in zip(systems, moments, strict=True):
|
|
156
|
+
wt, wc = s.treated_weight, m["w0"]
|
|
157
|
+
if method != "reg" and wc <= 0:
|
|
158
|
+
raise ValueError("no controls remain after propensity trimming")
|
|
159
|
+
mt = m["s1"] / wt
|
|
160
|
+
mc = m["s0"] / wc if method != "reg" else np.zeros_like(mt)
|
|
161
|
+
jo = -m["xt"] / wt
|
|
162
|
+
if method == "dr":
|
|
163
|
+
jo += m["xc"] / wc
|
|
164
|
+
ao = (
|
|
165
|
+
solve_checked(s.control_gram, jo, "outcome regression")
|
|
166
|
+
if method != "ipw"
|
|
167
|
+
else np.zeros(len(jo))
|
|
168
|
+
)
|
|
169
|
+
jp = (
|
|
170
|
+
-(m["xcy"] - np.outer(m["xc"], mc)) / wc if method != "reg" else np.zeros_like(m["xcy"])
|
|
171
|
+
)
|
|
172
|
+
ap = solve_checked(m["H"], jp, "propensity information") if method != "reg" else jp
|
|
173
|
+
fitted.append(
|
|
174
|
+
AdjustedContrast(
|
|
175
|
+
s,
|
|
176
|
+
method,
|
|
177
|
+
control_max,
|
|
178
|
+
wt,
|
|
179
|
+
wc,
|
|
180
|
+
mt,
|
|
181
|
+
mc,
|
|
182
|
+
ao,
|
|
183
|
+
ap,
|
|
184
|
+
dict(
|
|
185
|
+
n_treated=s.n_treated,
|
|
186
|
+
n_controls=s.n_controls,
|
|
187
|
+
effective_treated=s.treated_weight**2 / s.treated_weight_squares,
|
|
188
|
+
trimmed_controls=m["trimmed"],
|
|
189
|
+
capped_probabilities=m["capped"],
|
|
190
|
+
effective_controls=wc * wc / m["w02"]
|
|
191
|
+
if method != "reg"
|
|
192
|
+
else s.control_weight**2 / s.control_weight_squares,
|
|
193
|
+
propensity_min=m["pmin"] if method != "reg" else None,
|
|
194
|
+
propensity_max=m["pmax"] if method != "reg" else None,
|
|
195
|
+
iterations=s.iterations,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return fitted
|