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,202 @@
1
+ """What a fit shows: public option names in diagnostics, original period labels.
2
+
3
+ The estimators work with internal option names and with consecutively
4
+ numbered periods (see periods.py). When a fit returns, its result keeps that
5
+ internal record for its own methods (``result._internal``) and shows the user
6
+ the same record with the public option names and values, and with cohorts and
7
+ periods under their original labels. Coefficient names and the cohort and time
8
+ columns of every table follow the same labels.
9
+ """
10
+
11
+ import re
12
+
13
+ import polars as pl
14
+
15
+ from .periods import PeriodLabels
16
+
17
+ IDENTITY = PeriodLabels(0, 1)
18
+
19
+ PUBLIC_ESTIMATORS = {
20
+ "cs": "did_callaway_santanna",
21
+ "sunab": "did_sun_abraham",
22
+ "bjs": "did_bjs",
23
+ "gardner": "did_two_stage",
24
+ "etwfe": "did_extended_twfe",
25
+ "stacked": "did_stacked",
26
+ }
27
+
28
+ # internal diagnostics key -> public key
29
+ KEYS = {
30
+ "reference_group_requested": "last_treated_controls",
31
+ "reference_group": "reference",
32
+ "balance_requested": "covariate_balance",
33
+ "balance_ecdf": "balance_distributions",
34
+ "pretrends": "pretrend_leads",
35
+ "bootstrap_reps": "multiplier_bootstrap_reps",
36
+ "bootstrap_seed": "multiplier_bootstrap_seed",
37
+ "bootstrap_weights": "multiplier_bootstrap_weights",
38
+ "bootstrap_unit_order": "multiplier_bootstrap_unit_order",
39
+ "panel_mode": "incomplete_units",
40
+ "est_method": "adjustment",
41
+ "trim_level": "propensity_trim",
42
+ "avgeffectsby": "residual_averaging",
43
+ "leaveout": "leave_out",
44
+ "ess_warn_threshold": "effective_sample_warning",
45
+ "ess_suppress_threshold": "min_effective_sample",
46
+ "specification": "event_study",
47
+ }
48
+ # public key -> {internal value: public value}
49
+ VALUES = {
50
+ "last_treated_controls": {
51
+ "auto": "if_needed",
52
+ "nevertreated": "never",
53
+ "lasttreated": "always",
54
+ },
55
+ "reference": {"nevertreated": "never_treated", "lasttreated": "last_treated"},
56
+ "incomplete_units": {"balanced": "error", "complete": "drop", "unbalanced": "keep"},
57
+ "control_group": {"nevertreated": "never_treated", "notyettreated": "not_yet_treated"},
58
+ "adjustment": {"reg": "regression", "ipw": "ipw", "dr": "doubly_robust"},
59
+ "event_study": {"dynamic": True, "static": False},
60
+ }
61
+ TIME_KEYS = {
62
+ "time_min",
63
+ "time_max",
64
+ "input_time_min",
65
+ "input_time_max",
66
+ "reference_cutoff",
67
+ "reference_last_clean_time",
68
+ "covariate_time",
69
+ "weight_time",
70
+ "time",
71
+ }
72
+ COHORT_KEYS = {"reference_cohort", "cohort"}
73
+ COHORT_DICTS = {"cohort_sizes", "cohort_weight_totals"}
74
+ COHORT_LISTS = {"controls", "contributing_cohorts", "cohorts"}
75
+
76
+
77
+ def labels_of(result):
78
+ return getattr(result, "_periods", None) or IDENTITY
79
+
80
+
81
+ def internal(result):
82
+ """The internal record a result's methods read (public one if none was kept)."""
83
+ record = getattr(result, "_internal", None)
84
+ return record if record is not None else result.diagnostics
85
+
86
+
87
+ def present(result, labels):
88
+ """Keep the internal record, show the public one; relabel names (in place)."""
89
+ labels = labels or IDENTITY
90
+ result._internal = dict(result.diagnostics)
91
+ result._periods = labels
92
+ result.diagnostics = public_diagnostics(result.diagnostics, labels, result.estimator)
93
+ if labels.step != 1:
94
+ result.coefnames = [label_name(n, labels) for n in result.coefnames]
95
+ ess = getattr(result, "_ess_table", None)
96
+ if ess is not None:
97
+ # Joined with aggregation contributors by coefficient name.
98
+ names = [
99
+ pl.col(c).map_elements(lambda n: label_name(n, labels), return_dtype=pl.String)
100
+ for c in ("coefficient", "cell")
101
+ ]
102
+ result._ess_table = label_frame(ess.with_columns(names), labels)
103
+ placebos = getattr(result, "_bjs_placebos", None)
104
+ if placebos is not None and getattr(placebos, "_internal", None) is None:
105
+ present(placebos, labels)
106
+ return result
107
+
108
+
109
+ def public_diagnostics(diagnostics, labels, estimator=None):
110
+ out = {}
111
+ for key, value in diagnostics.items():
112
+ if (
113
+ estimator == "etwfe"
114
+ and key == "inference"
115
+ and value in ("conditional", "unconditional")
116
+ ):
117
+ out["treat_sample_as_fixed"] = value == "conditional"
118
+ continue
119
+ key = KEYS.get(key, key)
120
+ if key in VALUES and isinstance(value, str | bool) and value in VALUES[key]:
121
+ value = VALUES[key][value]
122
+ out[key] = _relabel(key, value, labels)
123
+ out["period_step"] = labels.step
124
+ return out
125
+
126
+
127
+ def _relabel(key, value, labels):
128
+ if labels.step == 1 or value is None:
129
+ return value
130
+ if key in TIME_KEYS and isinstance(value, int):
131
+ return labels.label_int(value)
132
+ if key in COHORT_KEYS and isinstance(value, int):
133
+ return labels.label_cohort(value)
134
+ if key in COHORT_DICTS and isinstance(value, dict):
135
+ return {str(labels.label_cohort(int(k))): v for k, v in value.items()}
136
+ if key in COHORT_LISTS and isinstance(value, list):
137
+ return [labels.label_cohort(v) if isinstance(v, int) else v for v in value]
138
+ if key == "omitted_cells" and isinstance(value, list):
139
+ return [(labels.label_cohort(g), labels.label_int(t)) for g, t in value]
140
+ if isinstance(value, list) and value and isinstance(value[0], dict):
141
+ return [{k: _relabel(k, v, labels) for k, v in item.items()} for item in value]
142
+ return value
143
+
144
+
145
+ _NAME = re.compile(r"\b(g|cohort|t|calendar)::(\d+)")
146
+ _CELL = re.compile(r"\bATT::(\d+)::(\d+)") # extended TWFE's cell names
147
+
148
+
149
+ def label_name(name, labels):
150
+ """g::<cohort>, t::<period>, cohort::<g> and calendar::<t> under original labels."""
151
+
152
+ def replace(match):
153
+ kind, value = match.group(1), int(match.group(2))
154
+ label = labels.label_cohort(value) if kind in ("g", "cohort") else labels.label_int(value)
155
+ return f"{kind}::{label}"
156
+
157
+ name = _CELL.sub(
158
+ lambda m: (
159
+ f"ATT::{labels.label_cohort(int(m.group(1)))}::{labels.label_int(int(m.group(2)))}"
160
+ ),
161
+ name,
162
+ )
163
+ return _NAME.sub(replace, name)
164
+
165
+
166
+ def label_frame(frame, labels):
167
+ """A table's cohort and time columns under their original labels."""
168
+ if labels is None or labels.step == 1:
169
+ return frame
170
+ o, s = labels.origin, labels.step
171
+ exprs = []
172
+ for column in frame.columns:
173
+ if column in ("cohort", "treated_cohort", "control_cohort"):
174
+ c = pl.col(column)
175
+ exprs.append(pl.when(c == 0).then(0).otherwise(o + (c - o) * s).alias(column))
176
+ elif column in ("time", "calendar_time", "pre_start", "pre_end", "post_start", "post_end"):
177
+ exprs.append((o + (pl.col(column) - o) * s).alias(column))
178
+ elif column == "cohorts" and frame.schema[column] == pl.List(pl.Int64):
179
+ exprs.append(
180
+ pl.col(column).list.eval(
181
+ pl.when(pl.element() == 0).then(0).otherwise(o + (pl.element() - o) * s)
182
+ )
183
+ )
184
+ return frame.with_columns(exprs) if exprs else frame
185
+
186
+
187
+ def present_plan(diagnostics, labels):
188
+ """An aggregation's diagnostics (contributing and excluded cohorts) under labels."""
189
+ labels = labels or IDENTITY
190
+ return {key: _relabel(key, value, labels) for key, value in diagnostics.items()}
191
+
192
+
193
+ def public_profile(out):
194
+ """A returned Profile's notes under public names; any other result as it is."""
195
+ from hdfe_stream.core.profile import Profile
196
+
197
+ if isinstance(out, Profile):
198
+ out.notes = {k: v for k, v in public_diagnostics(out.notes, IDENTITY).items()}
199
+ for code, name in PUBLIC_ESTIMATORS.items():
200
+ if out.model == f"diff-stream {code}":
201
+ out.model = f"diff-stream {name}"
202
+ return out