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.
Files changed (108) hide show
  1. diff_stream-0.1.0.dev0/CHANGELOG.md +60 -0
  2. diff_stream-0.1.0.dev0/CITATION.cff +20 -0
  3. diff_stream-0.1.0.dev0/LICENSE.md +21 -0
  4. diff_stream-0.1.0.dev0/MANIFEST.in +2 -0
  5. diff_stream-0.1.0.dev0/PKG-INFO +192 -0
  6. diff_stream-0.1.0.dev0/README.md +143 -0
  7. diff_stream-0.1.0.dev0/diff_stream/__init__.py +52 -0
  8. diff_stream-0.1.0.dev0/diff_stream/_bootstrap_worker.py +48 -0
  9. diff_stream-0.1.0.dev0/diff_stream/_core/__init__.py +5 -0
  10. diff_stream-0.1.0.dev0/diff_stream/_core/contrasts.py +199 -0
  11. diff_stream-0.1.0.dev0/diff_stream/_core/covariate_kernels.py +420 -0
  12. diff_stream-0.1.0.dev0/diff_stream/_core/cs_kernels.py +536 -0
  13. diff_stream-0.1.0.dev0/diff_stream/_core/cs_row_kernels.py +495 -0
  14. diff_stream-0.1.0.dev0/diff_stream/_core/design.py +105 -0
  15. diff_stream-0.1.0.dev0/diff_stream/_core/etwfe_kernels.py +288 -0
  16. diff_stream-0.1.0.dev0/diff_stream/_core/functionals.py +30 -0
  17. diff_stream-0.1.0.dev0/diff_stream/_core/multiplier.py +90 -0
  18. diff_stream-0.1.0.dev0/diff_stream/_core/nuisance.py +255 -0
  19. diff_stream-0.1.0.dev0/diff_stream/_core/partition.py +158 -0
  20. diff_stream-0.1.0.dev0/diff_stream/_core/process.py +61 -0
  21. diff_stream-0.1.0.dev0/diff_stream/_core/resample.py +171 -0
  22. diff_stream-0.1.0.dev0/diff_stream/_core/staging_kernels.py +90 -0
  23. diff_stream-0.1.0.dev0/diff_stream/_core/twostage_kernels.py +342 -0
  24. diff_stream-0.1.0.dev0/diff_stream/_core/weights.py +26 -0
  25. diff_stream-0.1.0.dev0/diff_stream/_core/within.py +81 -0
  26. diff_stream-0.1.0.dev0/diff_stream/_docs.py +100 -0
  27. diff_stream-0.1.0.dev0/diff_stream/_options.py +62 -0
  28. diff_stream-0.1.0.dev0/diff_stream/_present.py +202 -0
  29. diff_stream-0.1.0.dev0/diff_stream/absorbed.py +621 -0
  30. diff_stream-0.1.0.dev0/diff_stream/aggregation.py +225 -0
  31. diff_stream-0.1.0.dev0/diff_stream/api.py +1112 -0
  32. diff_stream-0.1.0.dev0/diff_stream/bacon.py +460 -0
  33. diff_stream-0.1.0.dev0/diff_stream/bacon_controls.py +188 -0
  34. diff_stream-0.1.0.dev0/diff_stream/balance.py +149 -0
  35. diff_stream-0.1.0.dev0/diff_stream/bjs.py +101 -0
  36. diff_stream-0.1.0.dev0/diff_stream/bjs_placebo.py +167 -0
  37. diff_stream-0.1.0.dev0/diff_stream/bootstrap.py +704 -0
  38. diff_stream-0.1.0.dev0/diff_stream/cohort_scores.py +68 -0
  39. diff_stream-0.1.0.dev0/diff_stream/cs.py +197 -0
  40. diff_stream-0.1.0.dev0/diff_stream/cs_unbalanced.py +336 -0
  41. diff_stream-0.1.0.dev0/diff_stream/effective_sample.py +73 -0
  42. diff_stream-0.1.0.dev0/diff_stream/estimators.py +95 -0
  43. diff_stream-0.1.0.dev0/diff_stream/etwfe.py +589 -0
  44. diff_stream-0.1.0.dev0/diff_stream/gardner.py +500 -0
  45. diff_stream-0.1.0.dev0/diff_stream/linear.py +339 -0
  46. diff_stream-0.1.0.dev0/diff_stream/periods.py +72 -0
  47. diff_stream-0.1.0.dev0/diff_stream/pretrend.py +97 -0
  48. diff_stream-0.1.0.dev0/diff_stream/py.typed +0 -0
  49. diff_stream-0.1.0.dev0/diff_stream/reference.py +125 -0
  50. diff_stream-0.1.0.dev0/diff_stream/results.py +521 -0
  51. diff_stream-0.1.0.dev0/diff_stream/simulate.py +80 -0
  52. diff_stream-0.1.0.dev0/diff_stream/stacked.py +405 -0
  53. diff_stream-0.1.0.dev0/diff_stream/store.py +706 -0
  54. diff_stream-0.1.0.dev0/diff_stream/twfe_store.py +181 -0
  55. diff_stream-0.1.0.dev0/diff_stream/twfe_weights.py +552 -0
  56. diff_stream-0.1.0.dev0/diff_stream/twostage_absorbed.py +190 -0
  57. diff_stream-0.1.0.dev0/diff_stream/unbalanced.py +297 -0
  58. diff_stream-0.1.0.dev0/diff_stream.egg-info/PKG-INFO +192 -0
  59. diff_stream-0.1.0.dev0/diff_stream.egg-info/SOURCES.txt +106 -0
  60. diff_stream-0.1.0.dev0/diff_stream.egg-info/dependency_links.txt +1 -0
  61. diff_stream-0.1.0.dev0/diff_stream.egg-info/requires.txt +24 -0
  62. diff_stream-0.1.0.dev0/diff_stream.egg-info/top_level.txt +1 -0
  63. diff_stream-0.1.0.dev0/pyproject.toml +100 -0
  64. diff_stream-0.1.0.dev0/setup.cfg +4 -0
  65. diff_stream-0.1.0.dev0/tests/test_absorbed.py +337 -0
  66. diff_stream-0.1.0.dev0/tests/test_aggregation.py +348 -0
  67. diff_stream-0.1.0.dev0/tests/test_anticipation.py +343 -0
  68. diff_stream-0.1.0.dev0/tests/test_bacon.py +284 -0
  69. diff_stream-0.1.0.dev0/tests/test_bacon_extended.py +162 -0
  70. diff_stream-0.1.0.dev0/tests/test_bacon_weights.py +233 -0
  71. diff_stream-0.1.0.dev0/tests/test_bjs_placebo.py +283 -0
  72. diff_stream-0.1.0.dev0/tests/test_castle.py +120 -0
  73. diff_stream-0.1.0.dev0/tests/test_conditional_cs.py +224 -0
  74. diff_stream-0.1.0.dev0/tests/test_covariate_kernels.py +98 -0
  75. diff_stream-0.1.0.dev0/tests/test_cross_sections.py +88 -0
  76. diff_stream-0.1.0.dev0/tests/test_cs_balance.py +223 -0
  77. diff_stream-0.1.0.dev0/tests/test_cs_kernels.py +103 -0
  78. diff_stream-0.1.0.dev0/tests/test_cs_row_kernels.py +99 -0
  79. diff_stream-0.1.0.dev0/tests/test_effective_sample.py +209 -0
  80. diff_stream-0.1.0.dev0/tests/test_equivalences.py +53 -0
  81. diff_stream-0.1.0.dev0/tests/test_estimators.py +250 -0
  82. diff_stream-0.1.0.dev0/tests/test_etwfe.py +236 -0
  83. diff_stream-0.1.0.dev0/tests/test_etwfe_kernels.py +101 -0
  84. diff_stream-0.1.0.dev0/tests/test_gardner.py +281 -0
  85. diff_stream-0.1.0.dev0/tests/test_hardening.py +114 -0
  86. diff_stream-0.1.0.dev0/tests/test_independent_columns.py +59 -0
  87. diff_stream-0.1.0.dev0/tests/test_inference.py +342 -0
  88. diff_stream-0.1.0.dev0/tests/test_inputs.py +179 -0
  89. diff_stream-0.1.0.dev0/tests/test_math_reform.py +67 -0
  90. diff_stream-0.1.0.dev0/tests/test_memory_layouts.py +192 -0
  91. diff_stream-0.1.0.dev0/tests/test_mpdta.py +38 -0
  92. diff_stream-0.1.0.dev0/tests/test_nuisance_scaling.py +72 -0
  93. diff_stream-0.1.0.dev0/tests/test_numerical_stress.py +69 -0
  94. diff_stream-0.1.0.dev0/tests/test_parallel_bootstrap.py +200 -0
  95. diff_stream-0.1.0.dev0/tests/test_reference.py +124 -0
  96. diff_stream-0.1.0.dev0/tests/test_reference_groups.py +195 -0
  97. diff_stream-0.1.0.dev0/tests/test_reference_populations.py +293 -0
  98. diff_stream-0.1.0.dev0/tests/test_refit_bootstrap.py +353 -0
  99. diff_stream-0.1.0.dev0/tests/test_richer_models.py +366 -0
  100. diff_stream-0.1.0.dev0/tests/test_stacked.py +180 -0
  101. diff_stream-0.1.0.dev0/tests/test_staging_kernels.py +129 -0
  102. diff_stream-0.1.0.dev0/tests/test_stata_review.py +116 -0
  103. diff_stream-0.1.0.dev0/tests/test_store_prefetch.py +75 -0
  104. diff_stream-0.1.0.dev0/tests/test_twfe_weights.py +315 -0
  105. diff_stream-0.1.0.dev0/tests/test_twostage_kernels.py +75 -0
  106. diff_stream-0.1.0.dev0/tests/test_unbalanced.py +304 -0
  107. diff_stream-0.1.0.dev0/tests/test_wait_policy.py +31 -0
  108. 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,2 @@
1
+ include LICENSE.md README.md CHANGELOG.md CITATION.cff
2
+ include diff_stream/py.typed
@@ -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()
@@ -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
+ """