diff-stream 0.1.0.dev0__tar.gz
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-0.1.0.dev0/CHANGELOG.md +60 -0
- diff_stream-0.1.0.dev0/CITATION.cff +20 -0
- diff_stream-0.1.0.dev0/LICENSE.md +21 -0
- diff_stream-0.1.0.dev0/MANIFEST.in +2 -0
- diff_stream-0.1.0.dev0/PKG-INFO +192 -0
- diff_stream-0.1.0.dev0/README.md +143 -0
- diff_stream-0.1.0.dev0/diff_stream/__init__.py +52 -0
- diff_stream-0.1.0.dev0/diff_stream/_bootstrap_worker.py +48 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/__init__.py +5 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/contrasts.py +199 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/covariate_kernels.py +420 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/cs_kernels.py +536 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/cs_row_kernels.py +495 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/design.py +105 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/etwfe_kernels.py +288 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/functionals.py +30 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/multiplier.py +90 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/nuisance.py +255 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/partition.py +158 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/process.py +61 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/resample.py +171 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/staging_kernels.py +90 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/twostage_kernels.py +342 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/weights.py +26 -0
- diff_stream-0.1.0.dev0/diff_stream/_core/within.py +81 -0
- diff_stream-0.1.0.dev0/diff_stream/_docs.py +100 -0
- diff_stream-0.1.0.dev0/diff_stream/_options.py +62 -0
- diff_stream-0.1.0.dev0/diff_stream/_present.py +202 -0
- diff_stream-0.1.0.dev0/diff_stream/absorbed.py +621 -0
- diff_stream-0.1.0.dev0/diff_stream/aggregation.py +225 -0
- diff_stream-0.1.0.dev0/diff_stream/api.py +1112 -0
- diff_stream-0.1.0.dev0/diff_stream/bacon.py +460 -0
- diff_stream-0.1.0.dev0/diff_stream/bacon_controls.py +188 -0
- diff_stream-0.1.0.dev0/diff_stream/balance.py +149 -0
- diff_stream-0.1.0.dev0/diff_stream/bjs.py +101 -0
- diff_stream-0.1.0.dev0/diff_stream/bjs_placebo.py +167 -0
- diff_stream-0.1.0.dev0/diff_stream/bootstrap.py +704 -0
- diff_stream-0.1.0.dev0/diff_stream/cohort_scores.py +68 -0
- diff_stream-0.1.0.dev0/diff_stream/cs.py +197 -0
- diff_stream-0.1.0.dev0/diff_stream/cs_unbalanced.py +336 -0
- diff_stream-0.1.0.dev0/diff_stream/effective_sample.py +73 -0
- diff_stream-0.1.0.dev0/diff_stream/estimators.py +95 -0
- diff_stream-0.1.0.dev0/diff_stream/etwfe.py +589 -0
- diff_stream-0.1.0.dev0/diff_stream/gardner.py +500 -0
- diff_stream-0.1.0.dev0/diff_stream/linear.py +339 -0
- diff_stream-0.1.0.dev0/diff_stream/periods.py +72 -0
- diff_stream-0.1.0.dev0/diff_stream/pretrend.py +97 -0
- diff_stream-0.1.0.dev0/diff_stream/py.typed +0 -0
- diff_stream-0.1.0.dev0/diff_stream/reference.py +125 -0
- diff_stream-0.1.0.dev0/diff_stream/results.py +521 -0
- diff_stream-0.1.0.dev0/diff_stream/simulate.py +80 -0
- diff_stream-0.1.0.dev0/diff_stream/stacked.py +405 -0
- diff_stream-0.1.0.dev0/diff_stream/store.py +706 -0
- diff_stream-0.1.0.dev0/diff_stream/twfe_store.py +181 -0
- diff_stream-0.1.0.dev0/diff_stream/twfe_weights.py +552 -0
- diff_stream-0.1.0.dev0/diff_stream/twostage_absorbed.py +190 -0
- diff_stream-0.1.0.dev0/diff_stream/unbalanced.py +297 -0
- diff_stream-0.1.0.dev0/diff_stream.egg-info/PKG-INFO +192 -0
- diff_stream-0.1.0.dev0/diff_stream.egg-info/SOURCES.txt +106 -0
- diff_stream-0.1.0.dev0/diff_stream.egg-info/dependency_links.txt +1 -0
- diff_stream-0.1.0.dev0/diff_stream.egg-info/requires.txt +24 -0
- diff_stream-0.1.0.dev0/diff_stream.egg-info/top_level.txt +1 -0
- diff_stream-0.1.0.dev0/pyproject.toml +100 -0
- diff_stream-0.1.0.dev0/setup.cfg +4 -0
- diff_stream-0.1.0.dev0/tests/test_absorbed.py +337 -0
- diff_stream-0.1.0.dev0/tests/test_aggregation.py +348 -0
- diff_stream-0.1.0.dev0/tests/test_anticipation.py +343 -0
- diff_stream-0.1.0.dev0/tests/test_bacon.py +284 -0
- diff_stream-0.1.0.dev0/tests/test_bacon_extended.py +162 -0
- diff_stream-0.1.0.dev0/tests/test_bacon_weights.py +233 -0
- diff_stream-0.1.0.dev0/tests/test_bjs_placebo.py +283 -0
- diff_stream-0.1.0.dev0/tests/test_castle.py +120 -0
- diff_stream-0.1.0.dev0/tests/test_conditional_cs.py +224 -0
- diff_stream-0.1.0.dev0/tests/test_covariate_kernels.py +98 -0
- diff_stream-0.1.0.dev0/tests/test_cross_sections.py +88 -0
- diff_stream-0.1.0.dev0/tests/test_cs_balance.py +223 -0
- diff_stream-0.1.0.dev0/tests/test_cs_kernels.py +103 -0
- diff_stream-0.1.0.dev0/tests/test_cs_row_kernels.py +99 -0
- diff_stream-0.1.0.dev0/tests/test_effective_sample.py +209 -0
- diff_stream-0.1.0.dev0/tests/test_equivalences.py +53 -0
- diff_stream-0.1.0.dev0/tests/test_estimators.py +250 -0
- diff_stream-0.1.0.dev0/tests/test_etwfe.py +236 -0
- diff_stream-0.1.0.dev0/tests/test_etwfe_kernels.py +101 -0
- diff_stream-0.1.0.dev0/tests/test_gardner.py +281 -0
- diff_stream-0.1.0.dev0/tests/test_hardening.py +114 -0
- diff_stream-0.1.0.dev0/tests/test_independent_columns.py +59 -0
- diff_stream-0.1.0.dev0/tests/test_inference.py +342 -0
- diff_stream-0.1.0.dev0/tests/test_inputs.py +179 -0
- diff_stream-0.1.0.dev0/tests/test_math_reform.py +67 -0
- diff_stream-0.1.0.dev0/tests/test_memory_layouts.py +192 -0
- diff_stream-0.1.0.dev0/tests/test_mpdta.py +38 -0
- diff_stream-0.1.0.dev0/tests/test_nuisance_scaling.py +72 -0
- diff_stream-0.1.0.dev0/tests/test_numerical_stress.py +69 -0
- diff_stream-0.1.0.dev0/tests/test_parallel_bootstrap.py +200 -0
- diff_stream-0.1.0.dev0/tests/test_reference.py +124 -0
- diff_stream-0.1.0.dev0/tests/test_reference_groups.py +195 -0
- diff_stream-0.1.0.dev0/tests/test_reference_populations.py +293 -0
- diff_stream-0.1.0.dev0/tests/test_refit_bootstrap.py +353 -0
- diff_stream-0.1.0.dev0/tests/test_richer_models.py +366 -0
- diff_stream-0.1.0.dev0/tests/test_stacked.py +180 -0
- diff_stream-0.1.0.dev0/tests/test_staging_kernels.py +129 -0
- diff_stream-0.1.0.dev0/tests/test_stata_review.py +116 -0
- diff_stream-0.1.0.dev0/tests/test_store_prefetch.py +75 -0
- diff_stream-0.1.0.dev0/tests/test_twfe_weights.py +315 -0
- diff_stream-0.1.0.dev0/tests/test_twostage_kernels.py +75 -0
- diff_stream-0.1.0.dev0/tests/test_unbalanced.py +304 -0
- diff_stream-0.1.0.dev0/tests/test_wait_policy.py +31 -0
- diff_stream-0.1.0.dev0/tests/test_weights.py +260 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased (0.1.0)
|
|
4
|
+
|
|
5
|
+
The first release.
|
|
6
|
+
|
|
7
|
+
- **Estimators**, one function each: `did_callaway_santanna` (regression,
|
|
8
|
+
IPW and doubly robust covariate adjustment; never-treated or not-yet-treated
|
|
9
|
+
controls; varying or universal base periods; panels or repeated cross
|
|
10
|
+
sections), `did_sun_abraham`, `did_bjs`
|
|
11
|
+
(the authors' standard errors, residual averaging groups, leave-out, and the
|
|
12
|
+
pre-trend test of `pretrend_leads`), `did_two_stage` (event study or overall
|
|
13
|
+
ATT), `did_extended_twfe` (linear; conditional or unconditional inference)
|
|
14
|
+
and `did_stacked` (stacked DiD with Wing, Freedman and Hollingsworth's
|
|
15
|
+
corrective weights, without building the stack).
|
|
16
|
+
Sun-Abraham, BJS and the two-stage estimator take covariates and additional
|
|
17
|
+
fixed effects (`"y ~ x | region^year"`) and absorb fixed effects with any
|
|
18
|
+
number of levels (county-by-year) through hdfe-stream
|
|
19
|
+
(`fixed_effect_solver`). BJS drops treated observations whose untreated
|
|
20
|
+
outcome cannot be imputed, with an `UnimputedObservationsWarning` and the
|
|
21
|
+
affected cells in its diagnostics.
|
|
22
|
+
- **Speed**: Callaway-Sant'Anna with covariates (balanced and unbalanced panels, and
|
|
23
|
+
repeated cross sections), the two-stage estimator, extended TWFE, and Sun-Abraham and BJS with
|
|
24
|
+
covariates run their passes over the data in compiled code, in parallel, as do the
|
|
25
|
+
validation and summing when the panel is first staged, and the next batch of units is read in
|
|
26
|
+
the background. Importing the package sets `OMP_WAIT_POLICY=passive` (unless set), which
|
|
27
|
+
speeds the compiled passes. Results are identical for any number of threads. The first fit after installation compiles for a few seconds, once.
|
|
28
|
+
- **Benchmarks** against R's did, fixest, didimputation, did2s, etwfe and
|
|
29
|
+
bacondecomp and Python's diff-diff and pyfixest, from 10,000 to 10 million
|
|
30
|
+
units, with covariates and with fixed effects of millions of levels
|
|
31
|
+
(`docs/benchmarks/`).
|
|
32
|
+
- **Data**: Polars frames or Parquet larger than memory; treatment timing as
|
|
33
|
+
a first-treated-period column (`cohort=`, 0 or null for never treated) or a
|
|
34
|
+
0/1 column (`treatment=`); evenly spaced periods (every second year, ...),
|
|
35
|
+
with event times counted in periods and the original labels shown; unbalanced panels
|
|
36
|
+
(`incomplete_units="error" | "drop" | "keep"`), sampling weights (optionally
|
|
37
|
+
time-varying), anticipation, and panels where every unit is eventually
|
|
38
|
+
treated (`last_treated_controls`).
|
|
39
|
+
- **Results**: cohort-period effects (`cohort_time()`) and their aggregation
|
|
40
|
+
by event time, cohort, calendar period or overall (`aggregate()`), with
|
|
41
|
+
balanced windows (`balanced_horizon`, `balance_window`) and the cells and
|
|
42
|
+
cohorts behind each estimate (`contributions()`, `composition()`);
|
|
43
|
+
pre-trend tests; effective sample sizes and covariate balance for
|
|
44
|
+
Callaway-Sant'Anna.
|
|
45
|
+
- **Inference**: standard errors clustered by unit (CRV0, CRV1) that include
|
|
46
|
+
the uncertainty in aggregation shares; Callaway-Sant'Anna's multiplier
|
|
47
|
+
bootstrap with simultaneous bands; `refit_bootstrap`, which refits any
|
|
48
|
+
estimator on unit resamples, serially or in parallel.
|
|
49
|
+
- **Diagnostics**: `bacon_decomp` (with covariates, weights and
|
|
50
|
+
always-treated units) and `twfe_weights` (`feTR`, with switching treatment).
|
|
51
|
+
- **Resources**: memory and thread limits from the job (stream-limits);
|
|
52
|
+
staging in memory when the panel fits and in whole-unit pieces on disk
|
|
53
|
+
otherwise, with batch sizes that adapt to the memory left; `profile=True`.
|
|
54
|
+
- Validated against R did/DRDID, fixest, didimputation, did2s, etwfe/emfx,
|
|
55
|
+
bacondecomp and TwoWayFEWeights, and the authors' Stata `did_imputation`.
|
|
56
|
+
|
|
57
|
+
Known limitation: on macOS, the propensity logit of IPW and doubly robust
|
|
58
|
+
Callaway-Sant'Anna can fail to converge when two covariates are almost
|
|
59
|
+
identical (a condition number near 1e10); it converges on Linux and Windows.
|
|
60
|
+
The test for that design is marked as an expected failure on macOS.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software in your research, please cite it as below."
|
|
3
|
+
type: software
|
|
4
|
+
title: "diff-stream"
|
|
5
|
+
abstract: "Out-of-core difference-in-differences with staggered treatment timing: Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess imputation, Gardner's two-stage estimator, Wooldridge's extended two-way fixed effects and stacked DiD, with the Goodman-Bacon decomposition and TWFE weights as diagnostics. Standard errors are clustered by unit and include the uncertainty in aggregation shares; fixed effects with many levels are absorbed with hdfe-stream. Results are tested against the authors' R and Stata packages."
|
|
6
|
+
authors:
|
|
7
|
+
- family-names: Tucker
|
|
8
|
+
given-names: Lee
|
|
9
|
+
email: lee.c.tucker@census.gov
|
|
10
|
+
license: MIT
|
|
11
|
+
repository-code: "https://github.com/leetucker/diff-stream"
|
|
12
|
+
url: "https://github.com/leetucker/diff-stream"
|
|
13
|
+
keywords:
|
|
14
|
+
- econometrics
|
|
15
|
+
- difference-in-differences
|
|
16
|
+
- staggered adoption
|
|
17
|
+
- event study
|
|
18
|
+
- treatment effects
|
|
19
|
+
- two-way fixed effects
|
|
20
|
+
- out-of-core
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lee Tucker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diff-stream
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Staggered difference-in-differences on data larger than memory: Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess imputation, two-stage DiD and extended TWFE, with TWFE diagnostics
|
|
5
|
+
Author: Lee Tucker
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/leetucker/diff-stream
|
|
8
|
+
Project-URL: Source, https://github.com/leetucker/diff-stream
|
|
9
|
+
Project-URL: Issues, https://github.com/leetucker/diff-stream/issues
|
|
10
|
+
Project-URL: Documentation, https://leetucker.github.io/diff-stream/
|
|
11
|
+
Project-URL: Changelog, https://github.com/leetucker/diff-stream/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: econometrics,difference-in-differences,staggered adoption,event study,Callaway-Sant'Anna,Sun-Abraham,imputation,two-way fixed effects,Goodman-Bacon,treatment effects,out-of-core
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE.md
|
|
27
|
+
Requires-Dist: hdfe-stream[formula]>=0.10.0
|
|
28
|
+
Requires-Dist: stream-limits>=0.3.0
|
|
29
|
+
Requires-Dist: polars!=1.41.*,>=1.37
|
|
30
|
+
Requires-Dist: numpy>=2.0
|
|
31
|
+
Requires-Dist: scipy>=1.13
|
|
32
|
+
Requires-Dist: numba>=0.60
|
|
33
|
+
Requires-Dist: pyarrow>=16.0
|
|
34
|
+
Provides-Extra: benchmark
|
|
35
|
+
Requires-Dist: stream-limits[launcher]>=0.3.0; extra == "benchmark"
|
|
36
|
+
Requires-Dist: matplotlib>=3.7; extra == "benchmark"
|
|
37
|
+
Requires-Dist: psutil>=5.9; extra == "benchmark"
|
|
38
|
+
Requires-Dist: pandas>=2.0; extra == "benchmark"
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: sphinx>=8.0; extra == "docs"
|
|
41
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
42
|
+
Requires-Dist: furo>=2024.8.6; extra == "docs"
|
|
43
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
44
|
+
Requires-Dist: sphinx-design>=0.6; extra == "docs"
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
47
|
+
Requires-Dist: stream-limits[testing]>=0.3.0; extra == "test"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# diff-stream
|
|
51
|
+
|
|
52
|
+
Difference-in-differences with staggered treatment timing, on panels larger than
|
|
53
|
+
memory. Includes Callaway and Sant'Anna, Sun and Abraham, Borusyak, Jaravel and
|
|
54
|
+
Spiess imputation, Gardner's two-stage DiD, Wooldridge's extended two-way fixed
|
|
55
|
+
effects, and stacked DiD. Two diagnostics show what a conventional two-way fixed
|
|
56
|
+
effects estimate averages. Results are tested against the authors' R and Stata
|
|
57
|
+
packages.
|
|
58
|
+
|
|
59
|
+
It reads Parquet in a stream, sorts the panel once (in memory when it fits,
|
|
60
|
+
in whole-unit pieces on disk when it does not), and computes standard errors
|
|
61
|
+
clustered by unit without holding all units' scores.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from diff_stream import did_callaway_santanna
|
|
65
|
+
|
|
66
|
+
fit = did_callaway_santanna(
|
|
67
|
+
"earnings ~ age + C(region)",
|
|
68
|
+
"panel/*.parquet",
|
|
69
|
+
panel=("person", "year"),
|
|
70
|
+
cohort="first_treated_year",
|
|
71
|
+
control_group="not_yet_treated",
|
|
72
|
+
)
|
|
73
|
+
fit.aggregate().summary() # event study
|
|
74
|
+
fit.aggregate("overall").summary() # overall ATT
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Status: in development** (version 0.1.0.dev0, not yet on PyPI). The
|
|
78
|
+
estimators, inference and documentation are complete and tested; the
|
|
79
|
+
[benchmarks](docs/benchmarks/index.md) compare them with other implementations.
|
|
80
|
+
|
|
81
|
+
## What it does
|
|
82
|
+
|
|
83
|
+
**Estimators** (each cites its paper in its docstring)
|
|
84
|
+
- `did_callaway_santanna`: group-time effects, with outcome regression, IPW
|
|
85
|
+
or doubly robust covariate adjustment; never-treated or not-yet-treated
|
|
86
|
+
controls; varying or universal base periods; panels or repeated cross
|
|
87
|
+
sections.
|
|
88
|
+
- `did_sun_abraham`: the interaction-weighted event study, with covariates
|
|
89
|
+
and additional fixed effects.
|
|
90
|
+
- `did_bjs`: imputation, with the authors' standard errors (residual
|
|
91
|
+
averaging groups, leave-out) and their pre-trend test.
|
|
92
|
+
- `did_two_stage`: Gardner's two-stage DiD, event study or overall ATT.
|
|
93
|
+
- `did_extended_twfe`: Wooldridge's extended TWFE (linear), with
|
|
94
|
+
covariate-interacted effects.
|
|
95
|
+
- `did_stacked`: stacked DiD with corrective weights, without building the
|
|
96
|
+
stack.
|
|
97
|
+
|
|
98
|
+
**Data**
|
|
99
|
+
- Unbalanced panels, sampling weights (also time-varying), anticipation, and
|
|
100
|
+
panels where every unit is eventually treated (the last-treated cohort as
|
|
101
|
+
comparison).
|
|
102
|
+
|
|
103
|
+
**Results and inference**
|
|
104
|
+
- Aggregation by event time, cohort and calendar period, or overall, with
|
|
105
|
+
balanced event windows and the cells and cohorts behind every estimate.
|
|
106
|
+
- Standard errors clustered by unit, including the uncertainty in the cohort
|
|
107
|
+
shares used to aggregate; pre-trend tests.
|
|
108
|
+
- Callaway-Sant'Anna's multiplier bootstrap for simultaneous bands, and a
|
|
109
|
+
bootstrap that refits any estimator on unit resamples, in parallel.
|
|
110
|
+
|
|
111
|
+
**Diagnostics**
|
|
112
|
+
- `bacon_decomp`: the Goodman-Bacon decomposition (with covariates and
|
|
113
|
+
weights).
|
|
114
|
+
- `twfe_weights`: de Chaisemartin and D'Haultfoeuille's weights and
|
|
115
|
+
sensitivity measures.
|
|
116
|
+
|
|
117
|
+
**Resources**
|
|
118
|
+
- `n_threads=` and `max_memory_gb=` respect a job's limits (stream-limits);
|
|
119
|
+
`profile=True` reports the memory and disk a fit needs, without fitting.
|
|
120
|
+
- Results with hdfe-stream's reporting: `summary()`, `tidy()`, `confint()`,
|
|
121
|
+
`wald_test()`, `etable`.
|
|
122
|
+
|
|
123
|
+
## Why it is fast
|
|
124
|
+
|
|
125
|
+
- **Sorted once.** The panel is sorted by unit in one piece when it fits in
|
|
126
|
+
memory, in whole-unit pieces on disk otherwise; later passes read whole units
|
|
127
|
+
in large batches.
|
|
128
|
+
- **Sufficient statistics.** Without covariates, the estimators reduce to
|
|
129
|
+
cohort-by-period means plus unit scores computed in parallel.
|
|
130
|
+
- **Covariance by cohort.** A unit's scores touch only its cohort's cells, so
|
|
131
|
+
the joint covariance of hundreds of effects is accumulated cohort by cohort.
|
|
132
|
+
- **Shared passes.** All comparisons' propensity and outcome models are
|
|
133
|
+
fitted together.
|
|
134
|
+
|
|
135
|
+
On a simulated panel of one million units over eight periods (8 million
|
|
136
|
+
rows), each estimator fits in one to two seconds on a 16-vCPU cloud instance,
|
|
137
|
+
and ten million units (80 million rows) in under half a minute, with a peak of
|
|
138
|
+
about 4 GB. With `max_memory_gb=2`, a million units fit within about 0.8 GB.
|
|
139
|
+
The benchmarks give the comparisons with R and Python implementations.
|
|
140
|
+
|
|
141
|
+
## Validation against other implementations
|
|
142
|
+
|
|
143
|
+
| Implementation | Compared on |
|
|
144
|
+
|---|---|
|
|
145
|
+
| R did, DRDID | Callaway-Sant'Anna: effects, full covariance, aggregations, multiplier bootstrap; the `mpdta` example |
|
|
146
|
+
| R fixest (`sunab`) | Sun-Abraham: coefficients, covariance, aggregation; the Castle Doctrine panel |
|
|
147
|
+
| Stata `did_imputation` (authors), R didimputation | BJS: estimates and full covariance, every averaging group, leave-out, pre-trend leads |
|
|
148
|
+
| R did2s (authors) | two-stage: coefficients and covariance |
|
|
149
|
+
| R etwfe, emfx (authors) | extended TWFE: effects and the covariance of every aggregation |
|
|
150
|
+
| fixest on the stacked data | stacked DiD: coefficients and covariance |
|
|
151
|
+
| R bacondecomp | Goodman-Bacon: every comparison; the `math_reform` example |
|
|
152
|
+
| R TwoWayFEWeights (authors) | TWFE weights (`feTR`) and sensitivity measures |
|
|
153
|
+
|
|
154
|
+
Agreement is within 2e-7 absolute, usually 1e-10. The frozen references and
|
|
155
|
+
the R and Stata scripts that make them are in `tests/data/` and
|
|
156
|
+
`benchmarks/external/`; `docs/implementations.md` lists the conventions on
|
|
157
|
+
which these packages differ.
|
|
158
|
+
|
|
159
|
+
## Install
|
|
160
|
+
|
|
161
|
+
diff-stream is not on PyPI yet. Install it from the repository:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install git+https://github.com/leetucker/diff-stream
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Python 3.10 or later. It depends on hdfe-stream (with its formula extra),
|
|
168
|
+
stream-limits, Polars, NumPy 2, numba, SciPy and pyarrow.
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
Build the documentation locally with:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install -e ".[docs]"
|
|
176
|
+
sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It covers choosing an estimator, your data, each estimator, results and
|
|
180
|
+
inference, the diagnostics, worked examples with their output, how it works,
|
|
181
|
+
memory and threads, the conventions that differ from other packages, and the
|
|
182
|
+
validation. `examples/` has runnable scripts, and `benchmarks/` the
|
|
183
|
+
performance comparisons (see `benchmarks/README.md`).
|
|
184
|
+
|
|
185
|
+
A sibling of [hdfe-stream](https://github.com/leetucker/hdfe-stream),
|
|
186
|
+
[logit-stream](https://github.com/leetucker/logit-stream),
|
|
187
|
+
[dpd-stream](https://github.com/leetucker/dpd-stream) and
|
|
188
|
+
[match-stream](https://github.com/leetucker/match-stream).
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# diff-stream
|
|
2
|
+
|
|
3
|
+
Difference-in-differences with staggered treatment timing, on panels larger than
|
|
4
|
+
memory. Includes Callaway and Sant'Anna, Sun and Abraham, Borusyak, Jaravel and
|
|
5
|
+
Spiess imputation, Gardner's two-stage DiD, Wooldridge's extended two-way fixed
|
|
6
|
+
effects, and stacked DiD. Two diagnostics show what a conventional two-way fixed
|
|
7
|
+
effects estimate averages. Results are tested against the authors' R and Stata
|
|
8
|
+
packages.
|
|
9
|
+
|
|
10
|
+
It reads Parquet in a stream, sorts the panel once (in memory when it fits,
|
|
11
|
+
in whole-unit pieces on disk when it does not), and computes standard errors
|
|
12
|
+
clustered by unit without holding all units' scores.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from diff_stream import did_callaway_santanna
|
|
16
|
+
|
|
17
|
+
fit = did_callaway_santanna(
|
|
18
|
+
"earnings ~ age + C(region)",
|
|
19
|
+
"panel/*.parquet",
|
|
20
|
+
panel=("person", "year"),
|
|
21
|
+
cohort="first_treated_year",
|
|
22
|
+
control_group="not_yet_treated",
|
|
23
|
+
)
|
|
24
|
+
fit.aggregate().summary() # event study
|
|
25
|
+
fit.aggregate("overall").summary() # overall ATT
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Status: in development** (version 0.1.0.dev0, not yet on PyPI). The
|
|
29
|
+
estimators, inference and documentation are complete and tested; the
|
|
30
|
+
[benchmarks](docs/benchmarks/index.md) compare them with other implementations.
|
|
31
|
+
|
|
32
|
+
## What it does
|
|
33
|
+
|
|
34
|
+
**Estimators** (each cites its paper in its docstring)
|
|
35
|
+
- `did_callaway_santanna`: group-time effects, with outcome regression, IPW
|
|
36
|
+
or doubly robust covariate adjustment; never-treated or not-yet-treated
|
|
37
|
+
controls; varying or universal base periods; panels or repeated cross
|
|
38
|
+
sections.
|
|
39
|
+
- `did_sun_abraham`: the interaction-weighted event study, with covariates
|
|
40
|
+
and additional fixed effects.
|
|
41
|
+
- `did_bjs`: imputation, with the authors' standard errors (residual
|
|
42
|
+
averaging groups, leave-out) and their pre-trend test.
|
|
43
|
+
- `did_two_stage`: Gardner's two-stage DiD, event study or overall ATT.
|
|
44
|
+
- `did_extended_twfe`: Wooldridge's extended TWFE (linear), with
|
|
45
|
+
covariate-interacted effects.
|
|
46
|
+
- `did_stacked`: stacked DiD with corrective weights, without building the
|
|
47
|
+
stack.
|
|
48
|
+
|
|
49
|
+
**Data**
|
|
50
|
+
- Unbalanced panels, sampling weights (also time-varying), anticipation, and
|
|
51
|
+
panels where every unit is eventually treated (the last-treated cohort as
|
|
52
|
+
comparison).
|
|
53
|
+
|
|
54
|
+
**Results and inference**
|
|
55
|
+
- Aggregation by event time, cohort and calendar period, or overall, with
|
|
56
|
+
balanced event windows and the cells and cohorts behind every estimate.
|
|
57
|
+
- Standard errors clustered by unit, including the uncertainty in the cohort
|
|
58
|
+
shares used to aggregate; pre-trend tests.
|
|
59
|
+
- Callaway-Sant'Anna's multiplier bootstrap for simultaneous bands, and a
|
|
60
|
+
bootstrap that refits any estimator on unit resamples, in parallel.
|
|
61
|
+
|
|
62
|
+
**Diagnostics**
|
|
63
|
+
- `bacon_decomp`: the Goodman-Bacon decomposition (with covariates and
|
|
64
|
+
weights).
|
|
65
|
+
- `twfe_weights`: de Chaisemartin and D'Haultfoeuille's weights and
|
|
66
|
+
sensitivity measures.
|
|
67
|
+
|
|
68
|
+
**Resources**
|
|
69
|
+
- `n_threads=` and `max_memory_gb=` respect a job's limits (stream-limits);
|
|
70
|
+
`profile=True` reports the memory and disk a fit needs, without fitting.
|
|
71
|
+
- Results with hdfe-stream's reporting: `summary()`, `tidy()`, `confint()`,
|
|
72
|
+
`wald_test()`, `etable`.
|
|
73
|
+
|
|
74
|
+
## Why it is fast
|
|
75
|
+
|
|
76
|
+
- **Sorted once.** The panel is sorted by unit in one piece when it fits in
|
|
77
|
+
memory, in whole-unit pieces on disk otherwise; later passes read whole units
|
|
78
|
+
in large batches.
|
|
79
|
+
- **Sufficient statistics.** Without covariates, the estimators reduce to
|
|
80
|
+
cohort-by-period means plus unit scores computed in parallel.
|
|
81
|
+
- **Covariance by cohort.** A unit's scores touch only its cohort's cells, so
|
|
82
|
+
the joint covariance of hundreds of effects is accumulated cohort by cohort.
|
|
83
|
+
- **Shared passes.** All comparisons' propensity and outcome models are
|
|
84
|
+
fitted together.
|
|
85
|
+
|
|
86
|
+
On a simulated panel of one million units over eight periods (8 million
|
|
87
|
+
rows), each estimator fits in one to two seconds on a 16-vCPU cloud instance,
|
|
88
|
+
and ten million units (80 million rows) in under half a minute, with a peak of
|
|
89
|
+
about 4 GB. With `max_memory_gb=2`, a million units fit within about 0.8 GB.
|
|
90
|
+
The benchmarks give the comparisons with R and Python implementations.
|
|
91
|
+
|
|
92
|
+
## Validation against other implementations
|
|
93
|
+
|
|
94
|
+
| Implementation | Compared on |
|
|
95
|
+
|---|---|
|
|
96
|
+
| R did, DRDID | Callaway-Sant'Anna: effects, full covariance, aggregations, multiplier bootstrap; the `mpdta` example |
|
|
97
|
+
| R fixest (`sunab`) | Sun-Abraham: coefficients, covariance, aggregation; the Castle Doctrine panel |
|
|
98
|
+
| Stata `did_imputation` (authors), R didimputation | BJS: estimates and full covariance, every averaging group, leave-out, pre-trend leads |
|
|
99
|
+
| R did2s (authors) | two-stage: coefficients and covariance |
|
|
100
|
+
| R etwfe, emfx (authors) | extended TWFE: effects and the covariance of every aggregation |
|
|
101
|
+
| fixest on the stacked data | stacked DiD: coefficients and covariance |
|
|
102
|
+
| R bacondecomp | Goodman-Bacon: every comparison; the `math_reform` example |
|
|
103
|
+
| R TwoWayFEWeights (authors) | TWFE weights (`feTR`) and sensitivity measures |
|
|
104
|
+
|
|
105
|
+
Agreement is within 2e-7 absolute, usually 1e-10. The frozen references and
|
|
106
|
+
the R and Stata scripts that make them are in `tests/data/` and
|
|
107
|
+
`benchmarks/external/`; `docs/implementations.md` lists the conventions on
|
|
108
|
+
which these packages differ.
|
|
109
|
+
|
|
110
|
+
## Install
|
|
111
|
+
|
|
112
|
+
diff-stream is not on PyPI yet. Install it from the repository:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install git+https://github.com/leetucker/diff-stream
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Python 3.10 or later. It depends on hdfe-stream (with its formula extra),
|
|
119
|
+
stream-limits, Polars, NumPy 2, numba, SciPy and pyarrow.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
Build the documentation locally with:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[docs]"
|
|
127
|
+
sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It covers choosing an estimator, your data, each estimator, results and
|
|
131
|
+
inference, the diagnostics, worked examples with their output, how it works,
|
|
132
|
+
memory and threads, the conventions that differ from other packages, and the
|
|
133
|
+
validation. `examples/` has runnable scripts, and `benchmarks/` the
|
|
134
|
+
performance comparisons (see `benchmarks/README.md`).
|
|
135
|
+
|
|
136
|
+
A sibling of [hdfe-stream](https://github.com/leetucker/hdfe-stream),
|
|
137
|
+
[logit-stream](https://github.com/leetucker/logit-stream),
|
|
138
|
+
[dpd-stream](https://github.com/leetucker/dpd-stream) and
|
|
139
|
+
[match-stream](https://github.com/leetucker/match-stream).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT.
|
|
@@ -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()
|