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,181 @@
1
+ """Observed-cell staging for the TWFE-weight diagnostic (no cohort restrictions)."""
2
+
3
+ import numpy as np
4
+ import polars as pl
5
+ import pyarrow.parquet as pq
6
+
7
+ from ._core.design import CovariatePlan
8
+ from ._core.partition import GroupPartitions, Part, whole_group_batches
9
+ from ._core.weights import validate_weights
10
+ from .store import source
11
+
12
+
13
+ def inspect_weights(fml, data, panel, treatment, cohort, weights):
14
+ if not isinstance(fml, str) or fml.count("~") != 1:
15
+ raise ValueError("use an outcome formula such as 'y ~ x + z'")
16
+ y, rhs = [s.strip() for s in fml.split("~")]
17
+ if not y.isidentifier() or "|" in rhs:
18
+ raise ValueError(
19
+ "use a column-name outcome and covariates; unit/time effects are automatic"
20
+ )
21
+ if (treatment is None) == (cohort is None):
22
+ raise ValueError("specify exactly one of treatment or cohort")
23
+ d = treatment if treatment is not None else cohort
24
+ names = [y, *panel, d] + ([] if weights is None else [weights])
25
+ if len(panel) != 2 or any(not isinstance(n, str) or not n for n in names):
26
+ raise ValueError(
27
+ "panel must name unit/time columns; variable names must be nonempty strings"
28
+ )
29
+ if len(set(names)) != len(names):
30
+ raise ValueError("outcome, unit, time, treatment/cohort and weights must be distinct")
31
+ src = source(data)
32
+ schema = src.collect_schema()
33
+ if not schema[panel[1]].is_integer():
34
+ raise ValueError("time must contain positive integers (gaps are permitted)")
35
+ if not (schema[panel[0]].is_numeric() or schema[panel[0]] == pl.String):
36
+ raise ValueError("unit IDs must be numeric or strings")
37
+ for name in [y, d] + ([] if weights is None else [weights]):
38
+ if not (schema[name].is_numeric() or (name == treatment and schema[name] == pl.Boolean)):
39
+ raise ValueError(f"{name} must be numeric")
40
+ if cohort is not None and not schema[cohort].is_integer():
41
+ raise ValueError("cohort must contain integers, with 0 denoting never treated")
42
+ plan = None if rhs in ("1", "0", "-1") else CovariatePlan.parse(y, rhs, schema)
43
+ lf = src.select(
44
+ pl.col(panel[0]).alias("id"),
45
+ pl.col(panel[1]).cast(pl.Int64).alias("t"),
46
+ pl.col(d)
47
+ .cast(pl.Int64 if cohort is not None else pl.Float64)
48
+ .alias("g" if cohort else "d"),
49
+ pl.col(y).cast(pl.Float64).alias("y"),
50
+ (pl.lit(1.0) if weights is None else pl.col(weights).cast(pl.Float64)).alias("w"),
51
+ *(
52
+ []
53
+ if plan is None
54
+ else [pl.col(c).alias(f"__raw{j}") for j, c in enumerate(plan.required)]
55
+ ),
56
+ )
57
+ return lf, y, plan
58
+
59
+
60
+ class WeightStore(GroupPartitions):
61
+ def __init__(self, lf, root, budget, preprocess_mb, batch_units, plan, panel_mode, varying):
62
+ super().__init__(root, budget, preprocess_mb)
63
+ self.plan, self.panel_mode, self.varying = plan, panel_mode, varying
64
+ self.batch_units = batch_units
65
+ self.paths = []
66
+ self.n_units = self.n_obs = self.input_units = 0
67
+ self.weight_scale = 0.0
68
+ self.weights_time_invariant = True
69
+ self._guard(preprocess_mb * 1e6 + 96e6)
70
+ raw = self.root / "snapshot.parquet"
71
+ lf.sink_parquet(raw, engine="streaming", row_group_size=self.read_rows)
72
+ dates, size = set(), 0
73
+ with pq.ParquetFile(raw, pre_buffer=False) as pf:
74
+ self.input_rows = pf.metadata.num_rows
75
+ for batch in pf.iter_batches(batch_size=self.read_rows):
76
+ d = pl.from_arrow(batch)
77
+ if sum(d.null_count().row(0)):
78
+ raise ValueError(
79
+ "missing values are unsupported; clean required columns explicitly"
80
+ )
81
+ for c in ["y", "w", "d" if "d" in d.columns else "g"]:
82
+ if not d[c].is_finite().all():
83
+ raise ValueError("required numeric values must be finite")
84
+ if d.schema["id"].is_numeric() and not d["id"].is_finite().all():
85
+ raise ValueError("unit IDs must be finite")
86
+ if not (d["t"] > 0).all():
87
+ raise ValueError("time must contain positive integers")
88
+ if "d" in d.columns and not d["d"].is_in([0.0, 1.0]).all():
89
+ raise ValueError("treatment must be binary (0 or 1)")
90
+ if "g" in d.columns and not (d["g"] >= 0).all():
91
+ raise ValueError("cohort must be nonnegative, with 0 denoting never treated")
92
+ validate_weights(d["w"].to_numpy())
93
+ self.weight_scale = max(self.weight_scale, float(d["w"].max()))
94
+ dates.update(d["t"].unique().to_list())
95
+ self._guard(len(dates) * 128 + 32e6)
96
+ size += batch.nbytes
97
+ if plan is not None:
98
+ plan.discover(d)
99
+ self.times = np.array(sorted(dates), dtype=np.int64)
100
+ self.periods = len(dates)
101
+ if self.periods < 2:
102
+ raise ValueError("at least two observed periods are required")
103
+ if plan is not None:
104
+ plan.compile()
105
+ self.extra_row_bytes = len(plan.names) * 8
106
+ self._prepare(Part(raw, self.input_rows, size), 0)
107
+ if not self.n_obs:
108
+ raise ValueError("no observations remain after complete-panel selection")
109
+
110
+ def _leaf(self, part):
111
+ effective = part.size + part.rows * self.extra_row_bytes
112
+ self._guard(effective * 4 + 32e6)
113
+ with pq.ParquetFile(part.path, pre_buffer=False) as pf:
114
+ d = pl.from_arrow(pf.read()).sort(["id", "t"])
115
+ ids, t = d["id"].to_numpy(), d["t"].to_numpy()
116
+ starts = np.r_[0, np.flatnonzero(ids[1:] != ids[:-1]) + 1]
117
+ lengths = np.diff(np.r_[starts, len(d)])
118
+ u = np.repeat(np.arange(len(starts)), lengths)
119
+ if np.any((ids[1:] == ids[:-1]) & (t[1:] == t[:-1])):
120
+ raise ValueError("duplicate unit-time keys are unsupported")
121
+ w = d["w"].to_numpy()
122
+ constant = np.all(w == w[starts][u])
123
+ if not constant and not self.varying:
124
+ raise ValueError("time-varying weights require allow_time_varying_weights=True")
125
+ self.weights_time_invariant &= bool(constant)
126
+ if "g" in d.columns:
127
+ g = d["g"].to_numpy()
128
+ if np.any(g != g[starts][u]):
129
+ raise ValueError("cohort must be constant within unit")
130
+ d = d.with_columns(
131
+ ((pl.col("g") > 0) & (pl.col("t") >= pl.col("g"))).cast(pl.Float64).alias("d")
132
+ )
133
+ # Evaluate source covariates before sample deletion, as in PanelStore.
134
+ if self.plan is not None:
135
+ d = d.hstack(self.plan.evaluate(d))
136
+ self.input_units += len(starts)
137
+ complete = lengths == self.periods
138
+ if self.panel_mode == "balanced" and not complete.all():
139
+ raise ValueError("balanced panels required; set incomplete_units='keep' or 'drop'")
140
+ if self.panel_mode == "complete":
141
+ d = d.filter(pl.Series(complete[u]))
142
+ lengths = lengths[complete]
143
+ if len(d):
144
+ d = d.select("id", "t", "y", "d", "w", *[c for c in d.columns if c.startswith("__x")])
145
+ path = self._name("units")
146
+ d.write_parquet(path, row_group_size=self.read_rows)
147
+ self.paths.append(path)
148
+ self.n_units += len(lengths)
149
+ self.n_obs += len(d)
150
+ self.partition_count += 1
151
+ self.max_leaf_bytes = max(self.max_leaf_bytes, effective)
152
+ part.path.unlink()
153
+
154
+ def batches(self):
155
+ for path in self.paths:
156
+ yield from whole_group_batches(path, "id", self.batch_units, self.read_rows)
157
+
158
+ def matrices(self):
159
+ cols = (
160
+ []
161
+ if self.plan is None
162
+ else [j for j, n in enumerate(self.plan.names) if n != "Intercept"]
163
+ )
164
+ T, P = self.periods, self.periods - 1 + len(cols)
165
+ for frame in self.batches():
166
+ ids = frame["id"].to_numpy()
167
+ starts = np.r_[0, np.flatnonzero(ids[1:] != ids[:-1]) + 1]
168
+ lengths = np.diff(np.r_[starts, len(frame)])
169
+ u = np.repeat(np.arange(len(starts)), lengths)
170
+ t = np.searchsorted(self.times, frame["t"].to_numpy())
171
+ n = len(starts)
172
+ X = np.zeros((n, T, P))
173
+ time = t > 0
174
+ X[u[time], t[time], t[time] - 1] = 1
175
+ for k, j in enumerate(cols):
176
+ X[u, t, T - 1 + k] = frame[f"__x{j}"].to_numpy()
177
+ w, D, Y = np.zeros((n, T)), np.zeros((n, T)), np.zeros((n, T))
178
+ w[u, t] = frame["w"].to_numpy() / self.weight_scale
179
+ validate_weights(w[u, t])
180
+ D[u, t], Y[u, t] = frame["d"].to_numpy(), frame["y"].to_numpy()
181
+ yield frame, X, D, Y, w, u, t