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/_present.py
ADDED
|
@@ -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
|