hdfe-stream 0.2.2__tar.gz → 0.4.0__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.
- hdfe_stream-0.4.0/CHANGELOG.md +290 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/CITATION.cff +2 -2
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/MANIFEST.in +1 -0
- hdfe_stream-0.4.0/PKG-INFO +242 -0
- hdfe_stream-0.4.0/README.md +185 -0
- hdfe_stream-0.4.0/docs/_static/custom.css +10 -0
- hdfe_stream-0.4.0/docs/benchmarks/akm.md +126 -0
- hdfe_stream-0.4.0/docs/benchmarks/covariates.md +101 -0
- hdfe_stream-0.4.0/docs/benchmarks/glm.md +79 -0
- hdfe_stream-0.4.0/docs/benchmarks/glm_covariates.md +71 -0
- hdfe_stream-0.4.0/docs/benchmarks/index.md +123 -0
- hdfe_stream-0.4.0/docs/benchmarks/kss.md +107 -0
- hdfe_stream-0.4.0/docs/changelog.md +2 -0
- hdfe_stream-0.4.0/docs/conf.py +70 -0
- hdfe_stream-0.4.0/docs/examples/akm_variance.md +102 -0
- hdfe_stream-0.4.0/docs/examples/connected_sets.md +198 -0
- hdfe_stream-0.4.0/docs/examples/fewer_fixed_effects.md +47 -0
- hdfe_stream-0.4.0/docs/examples/formulas.md +338 -0
- {hdfe_stream-0.2.2/docs → hdfe_stream-0.4.0/docs/examples}/glm.md +78 -154
- hdfe_stream-0.4.0/docs/examples/index.md +87 -0
- hdfe_stream-0.4.0/docs/examples/leave_out_kss.md +123 -0
- hdfe_stream-0.4.0/docs/examples/out_of_core.md +195 -0
- hdfe_stream-0.4.0/docs/examples/quickstart.md +101 -0
- hdfe_stream-0.4.0/docs/examples/reporting.md +73 -0
- hdfe_stream-0.4.0/docs/examples/varying_slopes.md +93 -0
- hdfe_stream-0.4.0/docs/examples/weights_and_iv.md +64 -0
- hdfe_stream-0.4.0/docs/examples/wide_designs.md +70 -0
- hdfe_stream-0.4.0/docs/index.md +201 -0
- hdfe_stream-0.2.2/docs/kss_methodological_differences.md → hdfe_stream-0.4.0/docs/kss/differences.md +11 -4
- hdfe_stream-0.4.0/docs/kss/draws.md +96 -0
- hdfe_stream-0.4.0/docs/kss/index.md +253 -0
- hdfe_stream-0.4.0/docs/kss/inference.md +265 -0
- hdfe_stream-0.4.0/docs/kss/streaming.md +86 -0
- hdfe_stream-0.4.0/docs/reference/connected.md +12 -0
- hdfe_stream-0.4.0/docs/reference/estimation.md +11 -0
- hdfe_stream-0.4.0/docs/reference/index.md +40 -0
- hdfe_stream-0.4.0/docs/reference/kss.md +19 -0
- hdfe_stream-0.4.0/docs/reference/lowlevel.md +16 -0
- hdfe_stream-0.4.0/docs/reference/results.md +30 -0
- hdfe_stream-0.4.0/docs/reference/utilities.md +42 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/__init__.py +33 -16
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/_types.py +1 -2
- hdfe_stream-0.4.0/hdfe_stream/api.py +256 -0
- hdfe_stream-0.4.0/hdfe_stream/connected.py +686 -0
- hdfe_stream-0.4.0/hdfe_stream/estimator.py +680 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/families.py +5 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/glm.py +128 -74
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/inference.py +93 -6
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/inverse.py +6 -3
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/kernels_base.py +103 -0
- hdfe_stream-0.4.0/hdfe_stream/kernels_glm.py +200 -0
- hdfe_stream-0.4.0/hdfe_stream/kernels_graph.py +352 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/kernels_inverse.py +0 -17
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/leaveout.py +163 -219
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/leaveout_match.py +3 -4
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/leaveout_se.py +43 -16
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/leaveout_weakid.py +42 -26
- hdfe_stream-0.4.0/hdfe_stream/multcomp.py +217 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/passes.py +258 -135
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/reporting.py +26 -2
- hdfe_stream-0.4.0/hdfe_stream/results.py +1032 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/simulate.py +116 -67
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/solve.py +65 -22
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/utils.py +6 -0
- hdfe_stream-0.4.0/hdfe_stream/wildboot.py +141 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/workspace.py +28 -13
- hdfe_stream-0.4.0/hdfe_stream.egg-info/PKG-INFO +242 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream.egg-info/SOURCES.txt +46 -4
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream.egg-info/requires.txt +8 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/pyproject.toml +14 -3
- hdfe_stream-0.4.0/tests/test_cells.py +152 -0
- hdfe_stream-0.4.0/tests/test_connected.py +449 -0
- hdfe_stream-0.4.0/tests/test_glm_kernel.py +37 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_inverse.py +27 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_leaveout.py +164 -66
- hdfe_stream-0.4.0/tests/test_memory_budget.py +58 -0
- hdfe_stream-0.4.0/tests/test_postestimation.py +226 -0
- hdfe_stream-0.4.0/tests/test_predict.py +174 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_sample.py +4 -3
- hdfe_stream-0.4.0/tests/test_wildboot.py +282 -0
- hdfe_stream-0.2.2/CHANGELOG.md +0 -136
- hdfe_stream-0.2.2/PKG-INFO +0 -632
- hdfe_stream-0.2.2/README.md +0 -583
- hdfe_stream-0.2.2/docs/kss.md +0 -604
- hdfe_stream-0.2.2/hdfe_stream/api.py +0 -183
- hdfe_stream-0.2.2/hdfe_stream/estimator.py +0 -492
- hdfe_stream-0.2.2/hdfe_stream/kernels_glm.py +0 -54
- hdfe_stream-0.2.2/hdfe_stream/kernels_graph.py +0 -135
- hdfe_stream-0.2.2/hdfe_stream/results.py +0 -563
- hdfe_stream-0.2.2/hdfe_stream.egg-info/PKG-INFO +0 -632
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/LICENSE.md +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_disk.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_disk.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_memory.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_memory.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_time.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/akm_time.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_disk.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_disk.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_memory.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_memory.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_time.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/covariates_time.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_disk.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_disk.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_memory.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_memory.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_time.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_covariates_time.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_disk.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_disk.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_memory.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_memory.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_time.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/glm_time.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_disk.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_disk.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_memory.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_memory.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_time.dark.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/docs/figures/kss_time.light.svg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/_columns.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/am_interval.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/feterms.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/formula.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/kernels_se.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/kernels_slopes.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/py.typed +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream/report.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream.egg-info/dependency_links.txt +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/hdfe_stream.egg-info/top_level.txt +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/setup.cfg +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/conftest.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_am_interval.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_cleanup.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_column_names.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_crv3.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_feis.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_fewer_fe.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_formula.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_glm.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_iv_weights.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_logging.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_lowlevel.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_ols.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_reporting.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_simulate.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_singletons.py +0 -0
- {hdfe_stream-0.2.2 → hdfe_stream-0.4.0}/tests/test_vcov.py +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
New features.
|
|
6
|
+
|
|
7
|
+
- **`HDFEResult.predict()`** predicts on new data (a DataFrame, a LazyFrame, or
|
|
8
|
+
a Parquet path or glob) or on the fit's source, lazily, so the data can be
|
|
9
|
+
larger than memory. It returns the data with a prediction column: the linear
|
|
10
|
+
predictor, or with `type="response"` a GLM's mean. Rows with a fixed-effect
|
|
11
|
+
level the fit did not estimate get null. It supports `i()` covariates,
|
|
12
|
+
interacted fixed effects, varying slopes, and IV models.
|
|
13
|
+
- **`HDFEResult.confint()`**: confidence intervals, pointwise or, with
|
|
14
|
+
`joint=True`, simultaneous over the selected coefficients, as pyfixest's.
|
|
15
|
+
- **`HDFEResult.wald_test()`**: Wald tests of R beta = q, or that the
|
|
16
|
+
coefficients selected by `keep`/`drop` are zero, under the current vcov, with
|
|
17
|
+
F or chi-squared p-values. F is reported by default for any OLS hypothesis
|
|
18
|
+
(chi-squared for GLMs), with `df_t` denominator degrees of freedom, so a
|
|
19
|
+
one-coefficient test has `tidy()`'s p-value.
|
|
20
|
+
- **`bonferroni()` and `holm()`**: multiple-testing corrections of one or
|
|
21
|
+
more coefficients' p-values across models (results, `HDFEMulti`, or lists
|
|
22
|
+
of them). Holm's step-down adjustment controls the familywise error rate as
|
|
23
|
+
Bonferroni's does, and never rejects less often.
|
|
24
|
+
- **The wild cluster bootstrap: `bootcluster=`, `HDFEResult.wildboottest()`,
|
|
25
|
+
`rwolf()` and `wyoung()`.** With `bootcluster=`, the fit keeps, for each
|
|
26
|
+
cluster, the sums the bootstrap needs (about G x k^2 numbers, collected in the
|
|
27
|
+
residual pass it makes anyway; nothing without the option). After the fit,
|
|
28
|
+
`wildboottest()` runs the restricted or unrestricted wild cluster bootstrap of
|
|
29
|
+
any coefficient from those sums, without reading the data, and `rwolf()` and
|
|
30
|
+
`wyoung()` give Romano-Wolf and Westfall-Young adjusted p-values across models
|
|
31
|
+
on the same draws. One-way, OLS only, and every fixed effect must be nested
|
|
32
|
+
within the clusters (a fixed effect with few levels, such as year, can go in
|
|
33
|
+
as covariates instead).
|
|
34
|
+
|
|
35
|
+
Fixes.
|
|
36
|
+
|
|
37
|
+
- `cluster="firm_id"`, a single string, was read as a list of characters; it
|
|
38
|
+
now means one clustering, as documented.
|
|
39
|
+
|
|
40
|
+
Performance.
|
|
41
|
+
|
|
42
|
+
- **Cells are collapsed from sorted rows, without a hash aggregation.** Pass 0
|
|
43
|
+
now sorts each worker's rows by the other fixed effects' levels too, so that
|
|
44
|
+
every cell (one combination of levels) is a run of consecutive rows, and pass
|
|
45
|
+
1 collapses the runs as it reads the sorted rows, in a multithreaded kernel.
|
|
46
|
+
This replaces Polars' group_by over each slice of rows, and the cell table it
|
|
47
|
+
wrote to disk and read back. Where nearly every row is a cell of its own
|
|
48
|
+
(e.g., worker, firm and year effects on a worker-year panel), pass 0 sorts by
|
|
49
|
+
the streamed dimension alone. The new `collapse_cells` option (`"auto"`, the
|
|
50
|
+
default; True; False) overrides that choice, and
|
|
51
|
+
`diagnostics["cells_collapsed"]` reports it. Together with the faster
|
|
52
|
+
iterations below, this reduced compute time by 5-31% on large AKM
|
|
53
|
+
configurations, and peak disk by up to 40%, with the same or lower memory.
|
|
54
|
+
- **Faster conjugate-gradient iterations.** The vector work of each iteration
|
|
55
|
+
(dot products, norms, and the updates of the solution, residual and search
|
|
56
|
+
direction) runs as fused, multithreaded kernels instead of single-threaded
|
|
57
|
+
numpy operations.
|
|
58
|
+
- **Faster IRLS steps for Poisson, logit and probit.** Each step's read of the
|
|
59
|
+
rows (every row's linear predictor, the family's mean, weight and working
|
|
60
|
+
response, the deviance and log-likelihood, and the sums per cell) runs as one
|
|
61
|
+
fused, multithreaded kernel, instead of a dozen single-threaded numpy
|
|
62
|
+
operations over each chunk. On large regression benchmarks, compute time
|
|
63
|
+
declined by 11-32%.
|
|
64
|
+
- **Pass 1 builds the Jacobi diagonal and step 2's right-hand side** chunk by
|
|
65
|
+
chunk while it has the cells in memory, instead of reading the identifying
|
|
66
|
+
cells back for each, when the variables are solved in one block (up to
|
|
67
|
+
`rhs_block` of them). Each of those reads is a pass over data as large as
|
|
68
|
+
the cell table, which matters most where the scratch disk is slow.
|
|
69
|
+
- **A memory budget for per-thread accumulators.** `stream_cg` and the
|
|
70
|
+
leave-out passes give each thread its own level-sized accumulator, which
|
|
71
|
+
with many threads and many levels could take more memory than the rest of
|
|
72
|
+
the fit (64 threads, 30 million firm-years and 8 columns: about 120 GB,
|
|
73
|
+
allocated afresh every iteration). They now get one copy per thread only
|
|
74
|
+
while all of them fit in half of `max_s_gb`, with fewer threads on those
|
|
75
|
+
passes when that binds, and `stream_cg` allocates its copies once per
|
|
76
|
+
solve. Lowering `max_s_gb` tightens it.
|
|
77
|
+
|
|
78
|
+
## 0.3.0
|
|
79
|
+
|
|
80
|
+
New features.
|
|
81
|
+
|
|
82
|
+
- **`connected_components`** finds the connected components of the graph of
|
|
83
|
+
two fixed-effect dimensions (either may be an interaction, `firm_id^year`)
|
|
84
|
+
by union-find over the rows as they stream, holding level-sized arrays
|
|
85
|
+
only. It returns a table per dimension giving each level its component,
|
|
86
|
+
numbered by size so that component 0 is the largest (ties go to the
|
|
87
|
+
component with the smallest level of the first dimension), and a table of
|
|
88
|
+
component sizes. `largest()` filters the data to the largest connected
|
|
89
|
+
set and `label()` adds each row's component, both lazily (on the data the
|
|
90
|
+
components were found in, or on other data passed to them). `min_degree`
|
|
91
|
+
removes levels with fewer distinct partners in the other dimension,
|
|
92
|
+
repeating until none is left, before the components are found: `(2, 1)`
|
|
93
|
+
keeps only movers, `2` the 2-core. `leave_one_out=True` prunes to the
|
|
94
|
+
leave-one-out connected set of KSS estimation instead. `rank_by` says
|
|
95
|
+
whether "largest" counts rows (the default) or the levels of one dimension.
|
|
96
|
+
- **`connected=`, `min_degree=` and `connected_fe=`** in `feols_stream`,
|
|
97
|
+
`fepois_stream`, `feglm_stream` and `StreamingHDFE` restrict the fit to a
|
|
98
|
+
pair of fixed effects (`connected_fe`, by default the first two listed),
|
|
99
|
+
among the rows with no missing values and before singletons are dropped:
|
|
100
|
+
`min_degree` prunes levels with too few distinct partners, then
|
|
101
|
+
`connected="largest"` keeps the largest connected set of what is left, or
|
|
102
|
+
`connected="leave_one_out"` the leave-one-out connected set, as
|
|
103
|
+
`connected_components` does. `sample()` reports the rows they drop as
|
|
104
|
+
`"min_degree"`, `"disconnected"` and `"leave_one_out"`, and
|
|
105
|
+
`diagnostics["connected"]` and the summary count them.
|
|
106
|
+
- **Python 3.15** is supported and tested in CI. pyfixest, which the
|
|
107
|
+
`formula` extra needs, has no 3.15 wheels yet (0.60.0), and building it
|
|
108
|
+
from source needs Rust and `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`; the
|
|
109
|
+
rest of the package installs from wheels.
|
|
110
|
+
|
|
111
|
+
Changes that can affect existing code.
|
|
112
|
+
|
|
113
|
+
- **`leave_one_out_connected` and `LeaveOutSet` are removed.** Use
|
|
114
|
+
`connected_components(data, "worker_id + firm_id", leave_one_out=True)`,
|
|
115
|
+
whose `.largest()` is the pruned panel, or `connected="leave_one_out"`
|
|
116
|
+
in a fit. The set is the same: on simulated panels with many components,
|
|
117
|
+
workers observed once and several rounds of cut workers, the workers and
|
|
118
|
+
firms kept are identical. It is found 5-6 times faster (38s against 218s on
|
|
119
|
+
150 million rows), and a null worker or firm id no longer makes it fail.
|
|
120
|
+
- **`leave_out_kss` prunes in the fit**, with `connected="leave_one_out"`, so
|
|
121
|
+
the leave-one-out connected set is that of the rows the model can use:
|
|
122
|
+
before, rows with a missing outcome or covariate were part of the network
|
|
123
|
+
being pruned. `lo.pruning` is now a dict of the pruning's counts (the
|
|
124
|
+
fit's `diagnostics["connected"]["leave_one_out"]`), and the rows it dropped
|
|
125
|
+
are those `lo.fit.sample()` labels `"leave_one_out"`.
|
|
126
|
+
- **The `component` column of `fixef()` is numbered by size**, so component
|
|
127
|
+
0 is the largest (by rows in the estimation sample), as in
|
|
128
|
+
`connected_components`. Ties go to the component with the smallest level of
|
|
129
|
+
that dimension. It was numbered by each component's smallest level.
|
|
130
|
+
- **Removed `hdfe_stream.iter_group_chunks` and `StreamingHDFE.from_formula`.**
|
|
131
|
+
The first read only the fit's internal row files, sorted by its internal
|
|
132
|
+
group code, and stays internal; the second duplicated `feols_stream`.
|
|
133
|
+
|
|
134
|
+
Fixes.
|
|
135
|
+
|
|
136
|
+
- **The leave-out operator's null space is right with several connected
|
|
137
|
+
components.** `apply_inverse` built one null vector for every number up to
|
|
138
|
+
the largest component label, which with several components left gaps, and
|
|
139
|
+
each gap added a spurious vector. Leave-out estimation runs on a connected
|
|
140
|
+
set and was not affected.
|
|
141
|
+
|
|
142
|
+
Documentation.
|
|
143
|
+
|
|
144
|
+
- **A documentation site**, at
|
|
145
|
+
[leetucker.github.io/hdfe-stream](https://leetucker.github.io/hdfe-stream/),
|
|
146
|
+
built with Sphinx from `docs/` and published by a new GitHub Actions
|
|
147
|
+
workflow. It has a page for each example, a section on leave-out variance
|
|
148
|
+
components (bringing together `docs/kss.md`, the methodological differences
|
|
149
|
+
and a derivation of the correction), a page for each benchmark, a function
|
|
150
|
+
reference built from the docstrings, and this changelog. `docs/kss.md`,
|
|
151
|
+
`docs/glm.md` and `docs/kss_methodological_differences.md` moved into it,
|
|
152
|
+
and the README is shorter, pointing to it. The public docstrings are now
|
|
153
|
+
numpydoc, documenting every option, including `tol`, `maxiter`,
|
|
154
|
+
`row_group_size` and `log_level` of `StreamingHDFE`, and the attributes of
|
|
155
|
+
`HDFEResult` and the other result classes. `pip install
|
|
156
|
+
"hdfe-stream[docs]"` installs what building it needs.
|
|
157
|
+
- **New example, `connected_sets.py`**, and additions to the others:
|
|
158
|
+
`sample()` and joining the residuals back on `row_id`, `fe_dof`, and CRV3
|
|
159
|
+
(`formulas.py`); `summary_json` (`quickstart.py`); `keep=` and a
|
|
160
|
+
decomposition by group (`akm_variance.py`); and `StreamingGLM` (`glm.py`).
|
|
161
|
+
|
|
162
|
+
## 0.2.2
|
|
163
|
+
|
|
164
|
+
- **Polars 2.0 support.** `fit.sample()` returns the source's rows in the source's
|
|
165
|
+
order again. Polars 2.0 collects with the streaming engine by default, whose
|
|
166
|
+
joins do not keep row order unless asked, so the rows came back shuffled.
|
|
167
|
+
Under Polars 2.0, repeated fits of the same model can differ in the last
|
|
168
|
+
digit or so (around 1e-16 relative): its streaming engine adds floating-point
|
|
169
|
+
sums in an order that depends on thread scheduling. Under Polars 1.x they
|
|
170
|
+
are identical bit for bit.
|
|
171
|
+
- **Dropping singletons no longer inflates memory with a wide design.** The
|
|
172
|
+
dropped levels were anti-joined after the design was evaluated, so the
|
|
173
|
+
streaming join buffered every design column. The rows are now dropped from
|
|
174
|
+
the source columns before the design is evaluated.
|
|
175
|
+
- **Finding singletons takes fewer scans.** The search used to stop only
|
|
176
|
+
after a full round over the fixed effects found nothing. It now stops once
|
|
177
|
+
every fixed effect has been checked since the last drop, since a dimension's
|
|
178
|
+
own drop cannot leave a singleton in it: four scans instead of six for a
|
|
179
|
+
typical three-way model, about 40% less time spent on singletons, and the
|
|
180
|
+
same memory. The rows dropped are the same.
|
|
181
|
+
- **Sorting the buckets and building the cell table takes less memory under
|
|
182
|
+
Polars 2.0.** Each bucket is sorted with Polars' in-memory engine and then
|
|
183
|
+
written, rather than sunk through the streaming engine, whose sort peaked
|
|
184
|
+
about 1.5x higher. Similarly, Pass 1 grouped each bucket with a streaming
|
|
185
|
+
`group_by`, which under Polars 2.0 gives every thread its own hash table: with
|
|
186
|
+
as many cells as rows, as in an AKM panel, the fit's peak more than doubled.
|
|
187
|
+
The rows are sorted by fe[0] group, so each bucket is now grouped a slice of
|
|
188
|
+
whole groups at a time and the cells appended in order. This reduces memory
|
|
189
|
+
use in AKM specifications and leaves wide designs unchanged or a little
|
|
190
|
+
better.
|
|
191
|
+
- **The explicit solver builds its matrix faster and in less memory.** The
|
|
192
|
+
reduced matrix S is now written directly in compressed form, one row at a
|
|
193
|
+
time, instead of being assembled from chunks of coordinate triples that
|
|
194
|
+
were merged with scipy. Each row's size is counted first, so S is allocated
|
|
195
|
+
once and nothing larger than S is held while it is built; `max_s_gb` is
|
|
196
|
+
checked before S is allocated rather than partway through. Estimates are
|
|
197
|
+
unchanged up to rounding.
|
|
198
|
+
- **Removed `triple_budget` and `dense_max_levels`** from `StreamingHDFE`. They
|
|
199
|
+
tuned the old way of building S and have no counterpart in the new one.
|
|
200
|
+
- **Importing hdfe_stream costs a little less memory.** p-values, critical
|
|
201
|
+
values and the weak-identification interval use `scipy.special`'s
|
|
202
|
+
distribution functions instead of `scipy.stats`, which is no longer
|
|
203
|
+
imported. The values are the same.
|
|
204
|
+
- **Empty run directories no longer pile up on NFS.** A file deleted while
|
|
205
|
+
still open or memory-mapped becomes a hidden .nfs file on NFS, so its run
|
|
206
|
+
directory could not be removed and was left empty once the process exited.
|
|
207
|
+
A failed removal is now retried after collecting garbage on every platform
|
|
208
|
+
(it was Windows only); the leave-out functions release their memory maps as
|
|
209
|
+
soon as they are done (`reload_intermediates()` maps them back in); and
|
|
210
|
+
empty run directories more than a minute old are removed by every new fit
|
|
211
|
+
in the same working directory and by `hdfe_stream.cleanup()`.
|
|
212
|
+
- **Slightly less verbose default logging.** CG iteration messages are now
|
|
213
|
+
logged at DEBUG level and therefore not printed by default. Lower your log
|
|
214
|
+
level to obtain this information.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## 0.2.1
|
|
218
|
+
|
|
219
|
+
- **`fe_dof="exact"` no longer depends on the order of the fixed effects.**
|
|
220
|
+
With three or more fixed effects it counted the connected components of the
|
|
221
|
+
streamed dimension and whichever came next in the formula, so reordering the
|
|
222
|
+
formula could change the standard errors. It now counts the components of
|
|
223
|
+
every pair of dimensions and uses the pair with the most, which is never
|
|
224
|
+
more than the true number of redundant levels. With two fixed effects
|
|
225
|
+
nothing changes, except in the clustered case below.
|
|
226
|
+
- **Clustered standard errors with fixed effects nested in the clusters.**
|
|
227
|
+
Under `fe_dof="exact"`, the components of a pair of fixed effects that are
|
|
228
|
+
both nested within the clusters were subtracted from K, although those
|
|
229
|
+
fixed effects' levels are already dropped from it. K could then fall below
|
|
230
|
+
the number of covariates and the standard errors were too small. Such a pair
|
|
231
|
+
is no longer used, and with no other pair K is pyfixest's.
|
|
232
|
+
- **`fit.sample()` labels every singleton.** With three or more fixed effects
|
|
233
|
+
that each had singleton levels, rows dropped only for the third or a later
|
|
234
|
+
dimension came back with `in_sample=True`, so the counts disagreed with
|
|
235
|
+
`n_obs` and the summary's singleton count. The fit itself was right. GLM
|
|
236
|
+
`"separation"` labels had the same fault.
|
|
237
|
+
- **A NaN in a fixed-effect or cluster column is missing**, as a null is,
|
|
238
|
+
rather than a level of its own. For fixed effects this matches pyfixest;
|
|
239
|
+
for clusters, where pyfixest raises an error, the row is dropped as it is
|
|
240
|
+
for a null cluster. `sample()` reports these rows as `"missing"`.
|
|
241
|
+
- **Polars 1.41 is excluded** from the supported versions. Its streaming join
|
|
242
|
+
panics (`called Option::unwrap() on a None value`) once a frame has about
|
|
243
|
+
130 columns, which a model with more than about 120 covariates reaches in
|
|
244
|
+
pass 0. Polars 1.40 and earlier, and 1.42 and later, are unaffected.
|
|
245
|
+
- **A panic inside Polars is reported as a `RuntimeError`** that names the
|
|
246
|
+
Polars version, says it is a Polars bug, and suggests a fix, instead of a
|
|
247
|
+
bare `PanicException`.
|
|
248
|
+
|
|
249
|
+
## 0.2.0
|
|
250
|
+
|
|
251
|
+
Changes to defaults, so results can differ from 0.1.0 without any change to
|
|
252
|
+
your code.
|
|
253
|
+
|
|
254
|
+
- **Default `vcov` is now `"iid"`**, with or without fixed effects, matching
|
|
255
|
+
pyfixest 0.60. Before, a model with fixed effects defaulted to CRV1 clustered
|
|
256
|
+
on the first fixed effect. Pass `vcov={"CRV1": "worker_id"}` for the old
|
|
257
|
+
behavior.
|
|
258
|
+
- **Singleton observations are dropped by default** (`fixef_rm="singleton"`),
|
|
259
|
+
as in pyfixest, repeating until none is left. `fixef_rm="none"` keeps them.
|
|
260
|
+
`leave_out_kss` fits with `fixef_rm="none"`, since its pruning defines the
|
|
261
|
+
sample.
|
|
262
|
+
|
|
263
|
+
New features.
|
|
264
|
+
|
|
265
|
+
- **`row_id` in `resid()`**: each row carries its position in the source before
|
|
266
|
+
any row was dropped, so residuals join back onto the source (`row_id=` renames
|
|
267
|
+
the column).
|
|
268
|
+
- **`fit.sample()`** returns the source, lazily, with `row_id`, `in_sample` and
|
|
269
|
+
`dropped_because` (`"missing"`, `"singleton"` or `"separation"`).
|
|
270
|
+
`multi.sample()` returns `{formula: frame}`.
|
|
271
|
+
- **Polars `DataFrame` inputs** are accepted wherever a Parquet path or
|
|
272
|
+
`LazyFrame` was.
|
|
273
|
+
- **Reserved column prefix `__hdfe_`.** All of the estimator's internal columns
|
|
274
|
+
now use it, and a source with a column starting with it is refused. This fixes
|
|
275
|
+
fits failing when a user column was named `w` (with weights), `v0`, `c1`,
|
|
276
|
+
`gcode` and similar.
|
|
277
|
+
|
|
278
|
+
## 0.1.0
|
|
279
|
+
|
|
280
|
+
First release.
|
|
281
|
+
|
|
282
|
+
- `feols_stream`: out-of-core OLS with several high-dimensional fixed effects,
|
|
283
|
+
varying slopes, weights, IV, and CRV1/CRV3 and other variance estimators.
|
|
284
|
+
- `fepois_stream` and `feglm_stream`: Poisson, logit and probit by iteratively
|
|
285
|
+
reweighted least squares over the same passes.
|
|
286
|
+
- AKM variance decomposition and leave-out (KSS) bias correction with
|
|
287
|
+
standard errors and weak-identification diagnostics.
|
|
288
|
+
- Reporting through pyfixest (`to_pyfixest()`, `etable`).
|
|
289
|
+
- `summary_json()` and `summary_dict()`: the model-level information of
|
|
290
|
+
`summary()` as JSON or a dict, for saving to disk.
|
|
@@ -7,8 +7,8 @@ authors:
|
|
|
7
7
|
- family-names: Tucker
|
|
8
8
|
given-names: Lee
|
|
9
9
|
email: lee.c.tucker@census.gov
|
|
10
|
-
version: 0.
|
|
11
|
-
date-released: 2026-10-
|
|
10
|
+
version: 0.4.0
|
|
11
|
+
date-released: 2026-10-16
|
|
12
12
|
license: MIT
|
|
13
13
|
repository-code: "https://github.com/leetucker/hdfe-stream"
|
|
14
14
|
url: "https://github.com/leetucker/hdfe-stream"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hdfe-stream
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Out-of-core regression with several high-dimensional fixed effects: OLS, Poisson, logit and probit, with AKM/KSS variance decompositions
|
|
5
|
+
Author: Lee Tucker
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/leetucker/hdfe-stream
|
|
8
|
+
Project-URL: Source, https://github.com/leetucker/hdfe-stream
|
|
9
|
+
Project-URL: Issues, https://github.com/leetucker/hdfe-stream/issues
|
|
10
|
+
Project-URL: Documentation, https://leetucker.github.io/hdfe-stream/
|
|
11
|
+
Project-URL: Changelog, https://github.com/leetucker/hdfe-stream/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: econometrics,fixed effects,high-dimensional,out-of-core,AKM,KSS,pyfixest
|
|
13
|
+
Classifier: Development Status :: 3 - 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: Programming Language :: Python :: 3.15
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE.md
|
|
28
|
+
Requires-Dist: polars!=1.41.*,>=1.37
|
|
29
|
+
Requires-Dist: numpy>=1.24
|
|
30
|
+
Requires-Dist: numba>=0.59
|
|
31
|
+
Requires-Dist: pyarrow>=14.0
|
|
32
|
+
Requires-Dist: scipy>=1.11
|
|
33
|
+
Provides-Extra: within
|
|
34
|
+
Requires-Dist: within-py>=0.3.0; extra == "within"
|
|
35
|
+
Provides-Extra: amg
|
|
36
|
+
Requires-Dist: pyamg>=5.1; extra == "amg"
|
|
37
|
+
Provides-Extra: formula
|
|
38
|
+
Requires-Dist: pyfixest<0.61.0,>=0.60.0; extra == "formula"
|
|
39
|
+
Requires-Dist: formulaic>=1.0; extra == "formula"
|
|
40
|
+
Requires-Dist: pandas>=2.0; extra == "formula"
|
|
41
|
+
Provides-Extra: all
|
|
42
|
+
Requires-Dist: hdfe-stream[amg,formula,within]; extra == "all"
|
|
43
|
+
Provides-Extra: benchmark
|
|
44
|
+
Requires-Dist: hdfe-stream[formula]; extra == "benchmark"
|
|
45
|
+
Requires-Dist: matplotlib>=3.7; extra == "benchmark"
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: sphinx>=8.0; extra == "docs"
|
|
48
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
49
|
+
Requires-Dist: furo>=2024.8.6; extra == "docs"
|
|
50
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
51
|
+
Requires-Dist: sphinx-design>=0.6; extra == "docs"
|
|
52
|
+
Provides-Extra: test
|
|
53
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
54
|
+
Requires-Dist: hdfe-stream[all]; extra == "test"
|
|
55
|
+
Requires-Dist: wildboottest>=0.3.2; extra == "test"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# hdfe-stream
|
|
59
|
+
|
|
60
|
+
Regression with several high-dimensional fixed effects, on data that does not
|
|
61
|
+
fit in memory: linear (`feols_stream`), and Poisson, logit and probit
|
|
62
|
+
(`fepois_stream`, `feglm_stream`), with the Kline–Saggio–Sølvsten bias
|
|
63
|
+
correction for AKM variance decompositions (`leave_out_kss`).
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from hdfe_stream import feols_stream
|
|
67
|
+
|
|
68
|
+
fit = feols_stream(
|
|
69
|
+
"log_earn ~ age_squared + age_cubed | worker_id + firm_id + year",
|
|
70
|
+
"data/*.parquet", # a Parquet path or glob, never fully loaded
|
|
71
|
+
workdir="scratch", # where intermediates go: disk, not memory
|
|
72
|
+
vcov={"CRV1": "worker_id"},
|
|
73
|
+
)
|
|
74
|
+
fit.summary()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Formulas follow [pyfixest](https://github.com/py-econometrics/pyfixest)/fixest
|
|
78
|
+
syntax and are parsed by pyfixest's own parser, so the syntax, the coefficient
|
|
79
|
+
names and the results all match. The difference is where the work happens: rows
|
|
80
|
+
are streamed off disk in batches and never held as a design matrix.
|
|
81
|
+
|
|
82
|
+
The data can be a Parquet path or glob, a Polars LazyFrame, or a Polars
|
|
83
|
+
DataFrame. Using file paths or LazyFrames keeps memory usage to a minimum, but
|
|
84
|
+
using DataFrames works well for smaller data and for trying things out.
|
|
85
|
+
|
|
86
|
+
**Documentation:
|
|
87
|
+
[leetucker.github.io/hdfe-stream](https://leetucker.github.io/hdfe-stream/)**,
|
|
88
|
+
with examples, the KSS bias correction, benchmarks and the function reference.
|
|
89
|
+
|
|
90
|
+
## Features
|
|
91
|
+
|
|
92
|
+
- [Any number of fixed effects](https://leetucker.github.io/hdfe-stream/examples/formulas.html#fixed-effects),
|
|
93
|
+
including [one or none](https://leetucker.github.io/hdfe-stream/examples/fewer_fixed_effects.html),
|
|
94
|
+
and interactions (`firm_id^year`)
|
|
95
|
+
- [pyfixest formula syntax](https://leetucker.github.io/hdfe-stream/examples/formulas.html#covariates):
|
|
96
|
+
transformations, categoricals, interactions, event studies
|
|
97
|
+
- [Several models in one call](https://leetucker.github.io/hdfe-stream/examples/formulas.html#several-models-at-once),
|
|
98
|
+
sharing passes over the data
|
|
99
|
+
- [Standard errors](https://leetucker.github.io/hdfe-stream/examples/formulas.html#standard-errors):
|
|
100
|
+
iid, robust, multi-way clustered, and one-way CRV3
|
|
101
|
+
- [Weighted least squares and 2SLS](https://leetucker.github.io/hdfe-stream/examples/weights_and_iv.html)
|
|
102
|
+
- [Poisson, logit and probit](https://leetucker.github.io/hdfe-stream/examples/glm.html)
|
|
103
|
+
- [Varying slopes in fixed effects](https://leetucker.github.io/hdfe-stream/examples/varying_slopes.html)
|
|
104
|
+
(`worker_id[t]`), on the streamed dimension only
|
|
105
|
+
- [Wide designs](https://leetucker.github.io/hdfe-stream/examples/wide_designs.html),
|
|
106
|
+
with hundreds of covariates
|
|
107
|
+
- [Connected components](https://leetucker.github.io/hdfe-stream/examples/connected_sets.html),
|
|
108
|
+
the largest connected set, and `min_degree` pruning
|
|
109
|
+
- [AKM variance decompositions](https://leetucker.github.io/hdfe-stream/examples/akm_variance.html),
|
|
110
|
+
in a streaming pass
|
|
111
|
+
- [KSS leave-out variance components](https://leetucker.github.io/hdfe-stream/kss/index.html),
|
|
112
|
+
with standard errors and weak-identification diagnostics
|
|
113
|
+
- [The sample, row by row](https://leetucker.github.io/hdfe-stream/examples/formulas.html#missing-values-and-the-sample):
|
|
114
|
+
which rows were dropped and why, and residuals joined back to the source
|
|
115
|
+
- [Regression tables and plots](https://leetucker.github.io/hdfe-stream/examples/reporting.html)
|
|
116
|
+
through pyfixest
|
|
117
|
+
- [Memory and disk controls](https://leetucker.github.io/hdfe-stream/examples/out_of_core.html),
|
|
118
|
+
a choice of solvers, and logging
|
|
119
|
+
- [Simulated panels](https://leetucker.github.io/hdfe-stream/examples/index.html#the-simulated-data)
|
|
120
|
+
with known effects, for testing
|
|
121
|
+
|
|
122
|
+
## When to use this, and when not to
|
|
123
|
+
|
|
124
|
+
**Use pyfixest.** It is the better tool whenever your data fits in memory:
|
|
125
|
+
mature, broader in scope, more inference options, and no scratch directory to
|
|
126
|
+
think about. If a `pandas` DataFrame of your data fits comfortably in RAM, stop
|
|
127
|
+
reading.
|
|
128
|
+
|
|
129
|
+
**Use this** when it doesn't. The case it was built for is a matched
|
|
130
|
+
employer–employee panel — tens of millions of rows, millions of worker effects,
|
|
131
|
+
an AKM variance decomposition — on a machine or a secure enclave where the data
|
|
132
|
+
is larger than the memory you are allowed. Concretely, reach for it when:
|
|
133
|
+
|
|
134
|
+
- the data does not fit in memory, or fits so tightly that nothing else does;
|
|
135
|
+
- you have one very high-cardinality dimension (workers, patients, students)
|
|
136
|
+
whose groups each touch only a few levels of the others;
|
|
137
|
+
- you want **worker-specific slopes** as well as intercepts (`worker_id[t]`),
|
|
138
|
+
which multiplies the parameter count by the number of slopes;
|
|
139
|
+
- the design is **wide**: hundreds of covariates, such as a categorical
|
|
140
|
+
expanded into fine indicators or many interactions. An in-memory library
|
|
141
|
+
needs rows × covariates in memory before it starts; this needs one batch of
|
|
142
|
+
rows × covariates at a time, with any number of fixed effects, including one
|
|
143
|
+
or none (see [many covariates](https://leetucker.github.io/hdfe-stream/benchmarks/covariates.html));
|
|
144
|
+
- you need a hard, predictable memory ceiling — a shared cluster, or a job that
|
|
145
|
+
must not be the one that gets killed.
|
|
146
|
+
|
|
147
|
+
You are trading memory for disk, and you need scratch space: about five times
|
|
148
|
+
the size of your Parquet input while the fit runs, and only the result files
|
|
149
|
+
once it finishes. See [performance](#performance) for what that buys. Point
|
|
150
|
+
`workdir=` at it, or set the `HDFE_STREAM_WORKDIR` environment variable once
|
|
151
|
+
(for example to a cluster's scratch filesystem) and leave `workdir` out; a
|
|
152
|
+
`workdir` passed to a call takes precedence, and with neither, the system
|
|
153
|
+
temporary directory is used.
|
|
154
|
+
|
|
155
|
+
## How it works, in one paragraph
|
|
156
|
+
|
|
157
|
+
One fixed-effect dimension — normally the largest — is **streamed**: it
|
|
158
|
+
is never represented as a vector in memory. Every other dimension is held as a
|
|
159
|
+
vector sized by its number of *levels*, not rows. Rows are read in batches,
|
|
160
|
+
hash-partitioned into buckets by the streamed dimension, sorted one bucket at a
|
|
161
|
+
time, and reduced to a table of cells (one per combination of fixed-effect
|
|
162
|
+
levels). The reduced normal equations for the non-streamed dimensions are then
|
|
163
|
+
solved by conjugate gradient, and the streamed effects are recovered group by
|
|
164
|
+
group in a final pass that writes residuals and fixed effects straight to
|
|
165
|
+
Parquet. So memory scales with firms × years, not with workers × rows, and that
|
|
166
|
+
asymmetry is the whole design. The covariates travel with the rows: what is
|
|
167
|
+
held at once is one bucket or batch of rows × covariates, never all of them,
|
|
168
|
+
which is why a wide design fits where an in-memory one does not. Full detail is in
|
|
169
|
+
[`hdfe_stream/__init__.py`](https://github.com/leetucker/hdfe-stream/blob/main/hdfe_stream/__init__.py).
|
|
170
|
+
|
|
171
|
+
## Install
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pip install hdfe-stream[formula] # formula syntax needs pyfixest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`polars`, `numpy`, `numba`, `pyarrow` and `scipy` are required. The extras:
|
|
178
|
+
|
|
179
|
+
| extra | what it adds |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `formula` | `feols_stream` formula syntax and pyfixest reporting (pyfixest, formulaic, pandas) |
|
|
182
|
+
| `within` | the `solver="within"` backend |
|
|
183
|
+
| `amg` | algebraic multigrid preconditioning (`precond="amg"`) for hard geometries |
|
|
184
|
+
| `all` | all of the above |
|
|
185
|
+
| `test` | the test suite |
|
|
186
|
+
| `docs` | building the documentation (Sphinx) |
|
|
187
|
+
|
|
188
|
+
On Python 3.15, pyfixest 0.60 (for the `formula` extra) has no wheels yet. It
|
|
189
|
+
builds from source with a Rust toolchain and
|
|
190
|
+
`PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` set; everything else installs from
|
|
191
|
+
wheels.
|
|
192
|
+
|
|
193
|
+
Without `formula` you still get `StreamingHDFE`, the lower-level interface that
|
|
194
|
+
takes column names or Polars expressions instead of a formula string.
|
|
195
|
+
|
|
196
|
+
## Performance
|
|
197
|
+
|
|
198
|
+
The standard three-way AKM regression,
|
|
199
|
+
`log_earn ~ age_squared + age_cubed | worker_id + firm_id + year`, clustered by
|
|
200
|
+
worker, on a simulated panel of 5 million workers and 42.5 million rows,
|
|
201
|
+
against [pyfixest](https://github.com/py-econometrics/pyfixest) and
|
|
202
|
+
[xhdfe](https://github.com/reisportela/xhdfe-xfe). Every configuration agrees on
|
|
203
|
+
the coefficients to within 3e-10.
|
|
204
|
+
|
|
205
|
+
| configuration | wall time | peak memory | peak disk |
|
|
206
|
+
|---|---:|---:|---:|
|
|
207
|
+
| pyfixest (MAP, its default) | 1,960 s | 20.7 GB | — |
|
|
208
|
+
| pyfixest (LSMR) | 138 s | 25.3 GB | — |
|
|
209
|
+
| xhdfe | 238 s | 19.0 GB | — |
|
|
210
|
+
| hdfe_stream (`explicit`, its default) | 52 s | 4.1 GB | 4.1 GB |
|
|
211
|
+
| hdfe_stream (`stream_cg`) | 54 s | 4.0 GB | 4.1 GB |
|
|
212
|
+
| **hdfe_stream (`stream_cg`, low memory)** | **60 s** | **3.2 GB** | 4.3 GB |
|
|
213
|
+
|
|
214
|
+
<picture>
|
|
215
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/leetucker/hdfe-stream/main/docs/figures/akm_memory.dark.svg">
|
|
216
|
+
<img alt="AKM regression: peak memory against the number of workers, one line per configuration, log-log" src="https://raw.githubusercontent.com/leetucker/hdfe-stream/main/docs/figures/akm_memory.light.svg">
|
|
217
|
+
</picture>
|
|
218
|
+
|
|
219
|
+
On small data the in-memory libraries use less memory: an hdfe_stream process
|
|
220
|
+
costs about 0.9 GB before it has done anything. The
|
|
221
|
+
[benchmarks](https://leetucker.github.io/hdfe-stream/benchmarks/index.html) cover every size from 212,000 rows up,
|
|
222
|
+
designs with up to 503 covariates, Poisson and logit, and the KSS
|
|
223
|
+
decomposition, with how each was measured.
|
|
224
|
+
|
|
225
|
+
## Documentation
|
|
226
|
+
|
|
227
|
+
- [**Examples**](https://leetucker.github.io/hdfe-stream/examples/index.html): one page per feature, on simulated
|
|
228
|
+
data that ships with the package: formulas and standard errors, weights and
|
|
229
|
+
IV, Poisson and logit, wide designs, varying slopes, connected sets, running
|
|
230
|
+
out of core, and reporting.
|
|
231
|
+
- [**Leave-out variance components**](https://leetucker.github.io/hdfe-stream/kss/index.html): the
|
|
232
|
+
Kline–Saggio–Sølvsten bias correction for the AKM decomposition, its standard
|
|
233
|
+
errors and weak-identification diagnostic, and every way it differs from the
|
|
234
|
+
reference implementations.
|
|
235
|
+
- [**Benchmarks**](https://leetucker.github.io/hdfe-stream/benchmarks/index.html): time, memory and disk against
|
|
236
|
+
pyfixest and xhdfe.
|
|
237
|
+
- [**Function reference**](https://leetucker.github.io/hdfe-stream/reference/index.html): every option.
|
|
238
|
+
- [**Limitations**](https://leetucker.github.io/hdfe-stream/index.html#limitations): what it does not do.
|
|
239
|
+
- [**Changelog**](https://github.com/leetucker/hdfe-stream/blob/main/CHANGELOG.md).
|
|
240
|
+
|
|
241
|
+
The scripts behind the examples are in [`examples/`](https://github.com/leetucker/hdfe-stream/tree/main/examples), and the
|
|
242
|
+
benchmarks in [`benchmarks/`](https://github.com/leetucker/hdfe-stream/tree/main/benchmarks).
|