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.
Files changed (56) hide show
  1. diff_stream/__init__.py +52 -0
  2. diff_stream/_bootstrap_worker.py +48 -0
  3. diff_stream/_core/__init__.py +5 -0
  4. diff_stream/_core/contrasts.py +199 -0
  5. diff_stream/_core/covariate_kernels.py +420 -0
  6. diff_stream/_core/cs_kernels.py +536 -0
  7. diff_stream/_core/cs_row_kernels.py +495 -0
  8. diff_stream/_core/design.py +105 -0
  9. diff_stream/_core/etwfe_kernels.py +288 -0
  10. diff_stream/_core/functionals.py +30 -0
  11. diff_stream/_core/multiplier.py +90 -0
  12. diff_stream/_core/nuisance.py +255 -0
  13. diff_stream/_core/partition.py +158 -0
  14. diff_stream/_core/process.py +61 -0
  15. diff_stream/_core/resample.py +171 -0
  16. diff_stream/_core/staging_kernels.py +90 -0
  17. diff_stream/_core/twostage_kernels.py +342 -0
  18. diff_stream/_core/weights.py +26 -0
  19. diff_stream/_core/within.py +81 -0
  20. diff_stream/_docs.py +100 -0
  21. diff_stream/_options.py +62 -0
  22. diff_stream/_present.py +202 -0
  23. diff_stream/absorbed.py +621 -0
  24. diff_stream/aggregation.py +225 -0
  25. diff_stream/api.py +1112 -0
  26. diff_stream/bacon.py +460 -0
  27. diff_stream/bacon_controls.py +188 -0
  28. diff_stream/balance.py +149 -0
  29. diff_stream/bjs.py +101 -0
  30. diff_stream/bjs_placebo.py +167 -0
  31. diff_stream/bootstrap.py +704 -0
  32. diff_stream/cohort_scores.py +68 -0
  33. diff_stream/cs.py +197 -0
  34. diff_stream/cs_unbalanced.py +336 -0
  35. diff_stream/effective_sample.py +73 -0
  36. diff_stream/estimators.py +95 -0
  37. diff_stream/etwfe.py +589 -0
  38. diff_stream/gardner.py +500 -0
  39. diff_stream/linear.py +339 -0
  40. diff_stream/periods.py +72 -0
  41. diff_stream/pretrend.py +97 -0
  42. diff_stream/py.typed +0 -0
  43. diff_stream/reference.py +125 -0
  44. diff_stream/results.py +521 -0
  45. diff_stream/simulate.py +80 -0
  46. diff_stream/stacked.py +405 -0
  47. diff_stream/store.py +706 -0
  48. diff_stream/twfe_store.py +181 -0
  49. diff_stream/twfe_weights.py +552 -0
  50. diff_stream/twostage_absorbed.py +190 -0
  51. diff_stream/unbalanced.py +297 -0
  52. diff_stream-0.1.0.dev0.dist-info/METADATA +192 -0
  53. diff_stream-0.1.0.dev0.dist-info/RECORD +56 -0
  54. diff_stream-0.1.0.dev0.dist-info/WHEEL +5 -0
  55. diff_stream-0.1.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  56. diff_stream-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -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,5 @@
1
+ """Local bounded infrastructure, pending promotion where consumers need it.
2
+
3
+ IPW, influence-function covariance and first-stage helpers are imported from
4
+ hdfe_stream.core. Treatment/population conventions remain outside this package.
5
+ """
@@ -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