hdfe-stream 0.2.1__tar.gz → 0.2.2__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.2.1 → hdfe_stream-0.2.2}/CHANGELOG.md +60 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/CITATION.cff +2 -2
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/PKG-INFO +48 -43
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/README.md +47 -42
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_disk.dark.svg +104 -104
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_disk.light.svg +104 -104
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_memory.dark.svg +132 -132
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_memory.light.svg +132 -132
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_time.dark.svg +238 -211
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_time.light.svg +238 -211
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.dark.svg +61 -73
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.light.svg +61 -73
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.dark.svg +68 -66
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.light.svg +68 -66
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_time.dark.svg +110 -108
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_time.light.svg +110 -108
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.dark.svg +172 -199
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.light.svg +172 -199
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.dark.svg +99 -99
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.light.svg +99 -99
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.dark.svg +187 -211
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.light.svg +187 -211
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_disk.dark.svg +225 -252
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_disk.light.svg +225 -252
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_memory.dark.svg +182 -182
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_memory.light.svg +182 -182
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_time.dark.svg +207 -207
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_time.light.svg +207 -207
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_disk.dark.svg +115 -130
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_disk.light.svg +115 -130
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_memory.dark.svg +110 -110
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_memory.light.svg +110 -110
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_time.dark.svg +184 -199
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_time.light.svg +184 -199
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/glm.md +32 -30
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/kss.md +18 -18
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/__init__.py +6 -5
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/am_interval.py +10 -6
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/estimator.py +12 -8
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/inverse.py +15 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_base.py +157 -74
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_slopes.py +0 -51
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout.py +2 -2
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_match.py +13 -10
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/passes.py +90 -41
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/reporting.py +5 -5
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/results.py +14 -9
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/solve.py +46 -78
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/utils.py +10 -2
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/workspace.py +64 -6
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/PKG-INFO +48 -43
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/pyproject.toml +1 -1
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_cleanup.py +57 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_leaveout.py +5 -3
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_logging.py +18 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/LICENSE.md +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/MANIFEST.in +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/kss_methodological_differences.md +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/_columns.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/_types.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/api.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/families.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/feterms.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/formula.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/glm.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/inference.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_glm.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_graph.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_inverse.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_se.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_se.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_weakid.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/py.typed +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/report.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/simulate.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/SOURCES.txt +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/dependency_links.txt +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/requires.txt +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/top_level.txt +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/setup.cfg +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/conftest.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_am_interval.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_column_names.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_crv3.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_feis.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_fewer_fe.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_formula.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_glm.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_inverse.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_iv_weights.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_lowlevel.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_ols.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_reporting.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_sample.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_simulate.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_singletons.py +0 -0
- {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_vcov.py +0 -0
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- **Polars 2.0 support.** `fit.sample()` returns the source's rows in the source's
|
|
6
|
+
order again. Polars 2.0 collects with the streaming engine by default, whose
|
|
7
|
+
joins do not keep row order unless asked, so the rows came back shuffled.
|
|
8
|
+
Under Polars 2.0, repeated fits of the same model can differ in the last
|
|
9
|
+
digit or so (around 1e-16 relative): its streaming engine adds floating-point
|
|
10
|
+
sums in an order that depends on thread scheduling. Under Polars 1.x they
|
|
11
|
+
are identical bit for bit.
|
|
12
|
+
- **Dropping singletons no longer inflates memory with a wide design.** The
|
|
13
|
+
dropped levels were anti-joined after the design was evaluated, so the
|
|
14
|
+
streaming join buffered every design column. The rows are now dropped from
|
|
15
|
+
the source columns before the design is evaluated.
|
|
16
|
+
- **Finding singletons takes fewer scans.** The search used to stop only
|
|
17
|
+
after a full round over the fixed effects found nothing. It now stops once
|
|
18
|
+
every fixed effect has been checked since the last drop, since a dimension's
|
|
19
|
+
own drop cannot leave a singleton in it: four scans instead of six for a
|
|
20
|
+
typical three-way model, about 40% less time spent on singletons, and the
|
|
21
|
+
same memory. The rows dropped are the same.
|
|
22
|
+
- **Sorting the buckets and building the cell table takes less memory under
|
|
23
|
+
Polars 2.0.** Each bucket is sorted with Polars' in-memory engine and then
|
|
24
|
+
written, rather than sunk through the streaming engine, whose sort peaked
|
|
25
|
+
about 1.5x higher. Similarly, Pass 1 grouped each bucket with a streaming
|
|
26
|
+
`group_by`, which under Polars 2.0 gives every thread its own hash table: with
|
|
27
|
+
as many cells as rows, as in an AKM panel, the fit's peak more than doubled.
|
|
28
|
+
The rows are sorted by fe[0] group, so each bucket is now grouped a slice of
|
|
29
|
+
whole groups at a time and the cells appended in order. This reduces memory
|
|
30
|
+
use in AKM specifications and leaves wide designs unchanged or a little
|
|
31
|
+
better.
|
|
32
|
+
- **The explicit solver builds its matrix faster and in less memory.** The
|
|
33
|
+
reduced matrix S is now written directly in compressed form, one row at a
|
|
34
|
+
time, instead of being assembled from chunks of coordinate triples that
|
|
35
|
+
were merged with scipy. Each row's size is counted first, so S is allocated
|
|
36
|
+
once and nothing larger than S is held while it is built; `max_s_gb` is
|
|
37
|
+
checked before S is allocated rather than partway through. Estimates are
|
|
38
|
+
unchanged up to rounding.
|
|
39
|
+
- **Removed `triple_budget` and `dense_max_levels`** from `StreamingHDFE`. They
|
|
40
|
+
tuned the old way of building S and have no counterpart in the new one.
|
|
41
|
+
- **Per-iteration CG messages are logged at DEBUG.** With `verbose=True`, the
|
|
42
|
+
"CG iter N: max rel. residual" lines go to the logger at DEBUG, whatever
|
|
43
|
+
`log_level` is, so they appear only when the logger is enabled for DEBUG.
|
|
44
|
+
Without a logger they are no longer printed. Other progress messages are
|
|
45
|
+
unchanged.
|
|
46
|
+
- **Importing hdfe_stream costs a little less memory.** p-values, critical
|
|
47
|
+
values and the weak-identification interval use `scipy.special`'s
|
|
48
|
+
distribution functions instead of `scipy.stats`, which is no longer
|
|
49
|
+
imported. The values are the same.
|
|
50
|
+
- **Empty run directories no longer pile up on NFS.** A file deleted while
|
|
51
|
+
still open or memory-mapped becomes a hidden .nfs file on NFS, so its run
|
|
52
|
+
directory could not be removed and was left empty once the process exited.
|
|
53
|
+
A failed removal is now retried after collecting garbage on every platform
|
|
54
|
+
(it was Windows only); the leave-out functions release their memory maps as
|
|
55
|
+
soon as they are done (`reload_intermediates()` maps them back in); and
|
|
56
|
+
empty run directories more than a minute old are removed by every new fit
|
|
57
|
+
in the same working directory and by `hdfe_stream.cleanup()`.
|
|
58
|
+
- **Slightly less verbose default logging.** CG iteration messages are now
|
|
59
|
+
logged at DEBUG level and therefore not printed by default. Lower your log
|
|
60
|
+
level to obtain this information.
|
|
61
|
+
|
|
62
|
+
|
|
3
63
|
## 0.2.1
|
|
4
64
|
|
|
5
65
|
- **`fe_dof="exact"` no longer depends on the order of the fixed effects.**
|
|
@@ -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.2.
|
|
11
|
-
date-released: 2026-10-
|
|
10
|
+
version: 0.2.2
|
|
11
|
+
date-released: 2026-10-08
|
|
12
12
|
license: MIT
|
|
13
13
|
repository-code: "https://github.com/leetucker/hdfe-stream"
|
|
14
14
|
url: "https://github.com/leetucker/hdfe-stream"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hdfe-stream
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Out-of-core regression with several high-dimensional fixed effects: OLS, Poisson, logit and probit, with AKM/KSS variance decompositions
|
|
5
5
|
Author: Lee Tucker
|
|
6
6
|
License-Expression: MIT
|
|
@@ -242,7 +242,7 @@ demeaners, and with [xhdfe](https://github.com/reisportela/xhdfe-xfe), on its
|
|
|
242
242
|
CPU backend. Sizes run from 25,000 to 5,000,000 workers, with firms = workers /
|
|
243
243
|
15 at every size and about 8.5 rows per worker, so the largest panel has 42.5
|
|
244
244
|
million rows. Every configuration agrees with every other on the coefficients
|
|
245
|
-
to within
|
|
245
|
+
to within 3e-10, so these are measurements of equally good answers. Reproduce
|
|
246
246
|
with:
|
|
247
247
|
|
|
248
248
|
```bash
|
|
@@ -278,46 +278,48 @@ At 5 million workers:
|
|
|
278
278
|
|
|
279
279
|
| configuration | wall time | peak memory | peak disk |
|
|
280
280
|
|---|---:|---:|---:|
|
|
281
|
-
| pyfixest (MAP, its default) |
|
|
282
|
-
| pyfixest (LSMR) |
|
|
283
|
-
| xhdfe |
|
|
284
|
-
| hdfe_stream (`
|
|
285
|
-
|
|
|
281
|
+
| pyfixest (MAP, its default) | 1,960 s | 20.7 GB | — |
|
|
282
|
+
| pyfixest (LSMR) | 138 s | 25.3 GB | — |
|
|
283
|
+
| xhdfe | 238 s | 19.0 GB | — |
|
|
284
|
+
| hdfe_stream (`explicit`, its default) | 52 s | 4.1 GB | 4.1 GB |
|
|
285
|
+
| hdfe_stream (`stream_cg`) | 54 s | 4.0 GB | 4.1 GB |
|
|
286
|
+
| **hdfe_stream (`stream_cg`, low memory)** | **60 s** | **3.2 GB** | 4.3 GB |
|
|
286
287
|
|
|
287
288
|
### Reading the figures
|
|
288
289
|
|
|
289
290
|
**Memory is the point.** At 5 million workers the in-memory libraries peak at
|
|
290
|
-
19–
|
|
291
|
-
hdfe_stream peaks at
|
|
291
|
+
19–25 GB: pyfixest's LSMR run needed almost all of this machine's 26 GB.
|
|
292
|
+
hdfe_stream peaks at 4.1 GB with its defaults and 3.2 GB with the batch and
|
|
292
293
|
bucket sizes turned down ("low memory": `rows_per_bucket=250_000`,
|
|
293
|
-
`batch_rows=200_000`), for about
|
|
294
|
-
spends instead is disk: about
|
|
294
|
+
`batch_rows=200_000`), for about 16% more time and the same answer. What it
|
|
295
|
+
spends instead is disk: about 97 bytes per row, 4.1 GB at 42.5 million rows, freed
|
|
295
296
|
when the fit finishes. The in-memory libraries' peak grows in proportion to the
|
|
296
|
-
data. hdfe_stream's low-memory setting grows far more slowly: 0.
|
|
297
|
-
workers, 1.
|
|
297
|
+
data. hdfe_stream's low-memory setting grows far more slowly: 0.9 GB at 25,000
|
|
298
|
+
workers, 1.4 GB at a million, 3.2 GB at five million.
|
|
298
299
|
|
|
299
|
-
**It is not slower for it.** From about
|
|
300
|
-
fastest configuration measured
|
|
301
|
-
xhdfe
|
|
300
|
+
**It is not slower for it.** From about a million workers up, hdfe_stream is
|
|
301
|
+
the fastest configuration measured, and at 400,000 it is within half a second
|
|
302
|
+
of xhdfe (4.1 s against 3.7 s). At 5 million workers it takes 52 s against
|
|
303
|
+
pyfixest's 138 s (LSMR) or 1,960 s (MAP) and xhdfe's 238 s. With AKM panel data,
|
|
302
304
|
reducing rows to worker-firm cells before solving more than pays for the disk
|
|
303
305
|
traffic. The simulated panel has about 8.5 rows per worker and one cell per
|
|
304
306
|
row, which suits this approach; a specification where the cell table is no
|
|
305
307
|
smaller than the data, and the fixed effects are less local, may do worse.
|
|
306
308
|
|
|
307
309
|
**Below about 400,000 workers, the in-memory libraries use less memory.**
|
|
308
|
-
Running any hdfe_stream fit costs about 0.
|
|
310
|
+
Running any hdfe_stream fit costs about 0.9 GB, most of it importing polars,
|
|
309
311
|
numba and scipy and starting Polars' streaming engine and numba's thread pools.
|
|
310
|
-
On small data that floor dominates: at 25,000 workers xhdfe peaks at 0.
|
|
312
|
+
On small data that floor dominates: at 25,000 workers xhdfe peaks at 0.29 GB and
|
|
311
313
|
pyfixest at about 0.5 GB. This is the concrete version of "use pyfixest (or
|
|
312
314
|
xhdfe) if your data fits".
|
|
313
315
|
|
|
314
316
|
**The default pyfixest demeaner is not the one to compare against.**
|
|
315
|
-
`MapDemeaner` (alternating projections) is
|
|
316
|
-
from 400,000 workers up, since many worker effects are the case alternating
|
|
317
|
+
`MapDemeaner` (alternating projections) is 6 to 14 times slower than
|
|
318
|
+
`LsmrDemeaner` from 400,000 workers up, since many worker effects are the case alternating
|
|
317
319
|
projections struggles with. If you are comparing, compare against LSMR.
|
|
318
320
|
|
|
319
|
-
**hdfe_stream's `within` solver** uses more memory at scale: 14.
|
|
320
|
-
million workers, where `stream_cg` and `explicit` stay near
|
|
321
|
+
**hdfe_stream's `within` solver** uses more memory at scale: 14.9 GB at 5
|
|
322
|
+
million workers, where `stream_cg` and `explicit` stay near 4 GB. Prefer
|
|
321
323
|
those when memory is the constraint.
|
|
322
324
|
|
|
323
325
|
### Many covariates
|
|
@@ -354,17 +356,17 @@ Wall time and peak memory ("—": ran out of this machine's 26 GB):
|
|
|
354
356
|
|
|
355
357
|
| covariates | pyfixest (MAP) | pyfixest (LSMR) | xhdfe | hdfe_stream | hdfe_stream, sized |
|
|
356
358
|
|---:|---:|---:|---:|---:|---:|
|
|
357
|
-
| 8 |
|
|
358
|
-
| 41 |
|
|
359
|
-
| 83 |
|
|
360
|
-
| 167 | — | — | — |
|
|
361
|
-
| 503 | — | — | — | — | **
|
|
359
|
+
| 8 | 132 s, 5.7 GB | 16 s, 7.3 GB | 15 s, 9.1 GB | 7 s, 3.2 GB | 7 s, 3.0 GB |
|
|
360
|
+
| 41 | 520 s, 13.8 GB | 62 s, 18.2 GB | 60 s, 18.2 GB | 22 s, 6.9 GB | 20 s, 3.7 GB |
|
|
361
|
+
| 83 | 976 s, 25.2 GB | 156 s, 25.8 GB | 125 s, 26.0 GB | 58 s, 12.4 GB | 44 s, 4.9 GB |
|
|
362
|
+
| 167 | — | — | — | 127 s, 23.0 GB | 81 s, 5.1 GB |
|
|
363
|
+
| 503 | — | — | — | — | **329 s, 8.6 GB** |
|
|
362
364
|
|
|
363
365
|
**The in-memory libraries run out first.** Their peak grows with rows ×
|
|
364
|
-
covariates. At 8.5 million rows
|
|
365
|
-
at 83 indicators and out of memory at 167
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
covariates. At 8.5 million rows all three were within about 1 GB of this
|
|
367
|
+
machine's 26 GB at 83 indicators, and out of memory at 167. hdfe_stream sized
|
|
368
|
+
to the design goes from 3.0 GB at 8 indicators to 5.1 GB at 167 and 8.6 GB at
|
|
369
|
+
503. Of
|
|
368
370
|
that last figure, about 4.9 GB is the memory-mapped cell table: file pages the
|
|
369
371
|
kernel can drop under pressure. The memory the fit itself allocated peaked at
|
|
370
372
|
4.6 GB (a separate profile, reading the process's anonymous and file-backed
|
|
@@ -372,7 +374,7 @@ pages apart).
|
|
|
372
374
|
|
|
373
375
|
**Size the batches to the design.** At its defaults hdfe_stream holds a whole
|
|
374
376
|
bucket of up to 20 million rows, with every covariate, while it sorts; that is
|
|
375
|
-
fine for a handful of covariates and not for hundreds (23.
|
|
377
|
+
fine for a handful of covariates and not for hundreds (23.0 GB at 167, out of
|
|
376
378
|
memory at 503). Every step that holds rows holds all the covariates, so scale
|
|
377
379
|
`rows_per_bucket`, `batch_rows` and `row_group_size` down with the width. The
|
|
378
380
|
benchmark aims at about 1 GB of design per bucket and 250 MB per batch and row
|
|
@@ -387,16 +389,16 @@ fit = feols_stream(fml, data, rows_per_bucket=min(20_000_000, int(1e9 // per_row
|
|
|
387
389
|
|
|
388
390
|
**Time grows with the square of the width.** The cross-products cost k × k per
|
|
389
391
|
row; they are formed chunk by chunk as BLAS matrix products, and 503 indicators
|
|
390
|
-
take
|
|
392
|
+
take 5½ minutes where 167 take under 1½. hdfe_stream is the fastest configuration at
|
|
391
393
|
every width measured.
|
|
392
394
|
|
|
393
395
|
**Timings on this machine are noisy where memory is tight.** Each point is one
|
|
394
396
|
run. Repeated runs of the default configuration at 83 indicators, which holds
|
|
395
|
-
12.
|
|
397
|
+
12.4 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
|
|
396
398
|
The differences between libraries above are far larger than that.
|
|
397
399
|
|
|
398
400
|
**Disk grows with the width too**, since the working copy of the rows carries
|
|
399
|
-
every covariate: 0.3 GB at 8 indicators,
|
|
401
|
+
every covariate: 0.3 GB at 8 indicators, 5.5 GB at 503.
|
|
400
402
|
|
|
401
403
|
**How the design is built matters.** A formula term such as `i(age_bin)` is
|
|
402
404
|
carried through the partitioning as the one column it is computed from, and the
|
|
@@ -421,12 +423,13 @@ python benchmarks/akm_benchmark.py --memory-sweep 1000000
|
|
|
421
423
|
|
|
422
424
|
| `rows_per_bucket` | `batch_rows` | wall time | peak memory | peak disk |
|
|
423
425
|
|---:|---:|---:|---:|---:|
|
|
424
|
-
| 20,000,000 (default) | 2,000,000 |
|
|
425
|
-
| 1,000,000 | 500,000 |
|
|
426
|
-
| 250,000 | 200,000 |
|
|
427
|
-
| 100,000 | 100,000 |
|
|
426
|
+
| 20,000,000 (default) | 2,000,000 | 11.2 s | 2,341 MB | 818 MB |
|
|
427
|
+
| 1,000,000 | 500,000 | 11.0 s | 1,849 MB | 821 MB |
|
|
428
|
+
| 250,000 | 200,000 | 11.6 s | 1,384 MB | 863 MB |
|
|
429
|
+
| 100,000 | 100,000 | 11.6 s | 1,555 MB | 876 MB |
|
|
428
430
|
|
|
429
|
-
|
|
431
|
+
The saving comes from the first two steps down; below that, the peak rises
|
|
432
|
+
again. The defaults are tuned for a
|
|
430
433
|
machine with room to spare; if memory is the binding constraint, set
|
|
431
434
|
`rows_per_bucket` to something near what you can afford and leave the rest
|
|
432
435
|
alone (with many covariates, scale `batch_rows` and `row_group_size` down too;
|
|
@@ -439,8 +442,10 @@ Leave-out estimation has a memory knob of its own, `scratch_mb`; see
|
|
|
439
442
|
[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance).
|
|
440
443
|
|
|
441
444
|
Measured on an Intel Core Ultra 7 258V, 8 cores, 26 GB RAM, Linux (WSL2);
|
|
442
|
-
Python 3.
|
|
443
|
-
pyfixest 0.60.0, xhdfe 2.28.
|
|
445
|
+
Python 3.14.8, polars 2.0.0, numpy 2.5.3, numba 0.68.0, scipy 1.18.1,
|
|
446
|
+
pyfixest 0.60.0, xhdfe 2.28.1 (CPU backend). In the regression benchmarks
|
|
447
|
+
hdfe_stream was measured on 2026-10-07 and pyfixest and xhdfe on 2026-10-06;
|
|
448
|
+
the GLM and KSS benchmarks were measured on 2026-10-07 and 2026-10-08. The exact versions are in
|
|
444
449
|
[benchmarks/results/machine.json](https://github.com/leetucker/hdfe-stream/blob/main/benchmarks/results/machine.json). Numba
|
|
445
450
|
kernels are compiled on first use and cached to disk; the benchmark runs a
|
|
446
451
|
warm-up fit so compilation is not charged to any configuration.
|
|
@@ -548,7 +553,7 @@ errors, and KSS's weak-identification diagnostic and q = 1 interval.
|
|
|
548
553
|
**[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md)** covers the options, the standard errors and their
|
|
549
554
|
measured coverage, weak identification, validation, reproducibility, and
|
|
550
555
|
[performance against xhdfe](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance): at 5 million workers,
|
|
551
|
-
|
|
556
|
+
4.8 GB of memory against xhdfe's 20 GB, with standard errors in 49 minutes
|
|
552
557
|
where xhdfe's did not finish in three hours.
|
|
553
558
|
**[docs/kss_methodological_differences.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss_methodological_differences.md)**
|
|
554
559
|
lists every way this implementation differs from LeaveOutTwoWay,
|
|
@@ -193,7 +193,7 @@ demeaners, and with [xhdfe](https://github.com/reisportela/xhdfe-xfe), on its
|
|
|
193
193
|
CPU backend. Sizes run from 25,000 to 5,000,000 workers, with firms = workers /
|
|
194
194
|
15 at every size and about 8.5 rows per worker, so the largest panel has 42.5
|
|
195
195
|
million rows. Every configuration agrees with every other on the coefficients
|
|
196
|
-
to within
|
|
196
|
+
to within 3e-10, so these are measurements of equally good answers. Reproduce
|
|
197
197
|
with:
|
|
198
198
|
|
|
199
199
|
```bash
|
|
@@ -229,46 +229,48 @@ At 5 million workers:
|
|
|
229
229
|
|
|
230
230
|
| configuration | wall time | peak memory | peak disk |
|
|
231
231
|
|---|---:|---:|---:|
|
|
232
|
-
| pyfixest (MAP, its default) |
|
|
233
|
-
| pyfixest (LSMR) |
|
|
234
|
-
| xhdfe |
|
|
235
|
-
| hdfe_stream (`
|
|
236
|
-
|
|
|
232
|
+
| pyfixest (MAP, its default) | 1,960 s | 20.7 GB | — |
|
|
233
|
+
| pyfixest (LSMR) | 138 s | 25.3 GB | — |
|
|
234
|
+
| xhdfe | 238 s | 19.0 GB | — |
|
|
235
|
+
| hdfe_stream (`explicit`, its default) | 52 s | 4.1 GB | 4.1 GB |
|
|
236
|
+
| hdfe_stream (`stream_cg`) | 54 s | 4.0 GB | 4.1 GB |
|
|
237
|
+
| **hdfe_stream (`stream_cg`, low memory)** | **60 s** | **3.2 GB** | 4.3 GB |
|
|
237
238
|
|
|
238
239
|
### Reading the figures
|
|
239
240
|
|
|
240
241
|
**Memory is the point.** At 5 million workers the in-memory libraries peak at
|
|
241
|
-
19–
|
|
242
|
-
hdfe_stream peaks at
|
|
242
|
+
19–25 GB: pyfixest's LSMR run needed almost all of this machine's 26 GB.
|
|
243
|
+
hdfe_stream peaks at 4.1 GB with its defaults and 3.2 GB with the batch and
|
|
243
244
|
bucket sizes turned down ("low memory": `rows_per_bucket=250_000`,
|
|
244
|
-
`batch_rows=200_000`), for about
|
|
245
|
-
spends instead is disk: about
|
|
245
|
+
`batch_rows=200_000`), for about 16% more time and the same answer. What it
|
|
246
|
+
spends instead is disk: about 97 bytes per row, 4.1 GB at 42.5 million rows, freed
|
|
246
247
|
when the fit finishes. The in-memory libraries' peak grows in proportion to the
|
|
247
|
-
data. hdfe_stream's low-memory setting grows far more slowly: 0.
|
|
248
|
-
workers, 1.
|
|
248
|
+
data. hdfe_stream's low-memory setting grows far more slowly: 0.9 GB at 25,000
|
|
249
|
+
workers, 1.4 GB at a million, 3.2 GB at five million.
|
|
249
250
|
|
|
250
|
-
**It is not slower for it.** From about
|
|
251
|
-
fastest configuration measured
|
|
252
|
-
xhdfe
|
|
251
|
+
**It is not slower for it.** From about a million workers up, hdfe_stream is
|
|
252
|
+
the fastest configuration measured, and at 400,000 it is within half a second
|
|
253
|
+
of xhdfe (4.1 s against 3.7 s). At 5 million workers it takes 52 s against
|
|
254
|
+
pyfixest's 138 s (LSMR) or 1,960 s (MAP) and xhdfe's 238 s. With AKM panel data,
|
|
253
255
|
reducing rows to worker-firm cells before solving more than pays for the disk
|
|
254
256
|
traffic. The simulated panel has about 8.5 rows per worker and one cell per
|
|
255
257
|
row, which suits this approach; a specification where the cell table is no
|
|
256
258
|
smaller than the data, and the fixed effects are less local, may do worse.
|
|
257
259
|
|
|
258
260
|
**Below about 400,000 workers, the in-memory libraries use less memory.**
|
|
259
|
-
Running any hdfe_stream fit costs about 0.
|
|
261
|
+
Running any hdfe_stream fit costs about 0.9 GB, most of it importing polars,
|
|
260
262
|
numba and scipy and starting Polars' streaming engine and numba's thread pools.
|
|
261
|
-
On small data that floor dominates: at 25,000 workers xhdfe peaks at 0.
|
|
263
|
+
On small data that floor dominates: at 25,000 workers xhdfe peaks at 0.29 GB and
|
|
262
264
|
pyfixest at about 0.5 GB. This is the concrete version of "use pyfixest (or
|
|
263
265
|
xhdfe) if your data fits".
|
|
264
266
|
|
|
265
267
|
**The default pyfixest demeaner is not the one to compare against.**
|
|
266
|
-
`MapDemeaner` (alternating projections) is
|
|
267
|
-
from 400,000 workers up, since many worker effects are the case alternating
|
|
268
|
+
`MapDemeaner` (alternating projections) is 6 to 14 times slower than
|
|
269
|
+
`LsmrDemeaner` from 400,000 workers up, since many worker effects are the case alternating
|
|
268
270
|
projections struggles with. If you are comparing, compare against LSMR.
|
|
269
271
|
|
|
270
|
-
**hdfe_stream's `within` solver** uses more memory at scale: 14.
|
|
271
|
-
million workers, where `stream_cg` and `explicit` stay near
|
|
272
|
+
**hdfe_stream's `within` solver** uses more memory at scale: 14.9 GB at 5
|
|
273
|
+
million workers, where `stream_cg` and `explicit` stay near 4 GB. Prefer
|
|
272
274
|
those when memory is the constraint.
|
|
273
275
|
|
|
274
276
|
### Many covariates
|
|
@@ -305,17 +307,17 @@ Wall time and peak memory ("—": ran out of this machine's 26 GB):
|
|
|
305
307
|
|
|
306
308
|
| covariates | pyfixest (MAP) | pyfixest (LSMR) | xhdfe | hdfe_stream | hdfe_stream, sized |
|
|
307
309
|
|---:|---:|---:|---:|---:|---:|
|
|
308
|
-
| 8 |
|
|
309
|
-
| 41 |
|
|
310
|
-
| 83 |
|
|
311
|
-
| 167 | — | — | — |
|
|
312
|
-
| 503 | — | — | — | — | **
|
|
310
|
+
| 8 | 132 s, 5.7 GB | 16 s, 7.3 GB | 15 s, 9.1 GB | 7 s, 3.2 GB | 7 s, 3.0 GB |
|
|
311
|
+
| 41 | 520 s, 13.8 GB | 62 s, 18.2 GB | 60 s, 18.2 GB | 22 s, 6.9 GB | 20 s, 3.7 GB |
|
|
312
|
+
| 83 | 976 s, 25.2 GB | 156 s, 25.8 GB | 125 s, 26.0 GB | 58 s, 12.4 GB | 44 s, 4.9 GB |
|
|
313
|
+
| 167 | — | — | — | 127 s, 23.0 GB | 81 s, 5.1 GB |
|
|
314
|
+
| 503 | — | — | — | — | **329 s, 8.6 GB** |
|
|
313
315
|
|
|
314
316
|
**The in-memory libraries run out first.** Their peak grows with rows ×
|
|
315
|
-
covariates. At 8.5 million rows
|
|
316
|
-
at 83 indicators and out of memory at 167
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
covariates. At 8.5 million rows all three were within about 1 GB of this
|
|
318
|
+
machine's 26 GB at 83 indicators, and out of memory at 167. hdfe_stream sized
|
|
319
|
+
to the design goes from 3.0 GB at 8 indicators to 5.1 GB at 167 and 8.6 GB at
|
|
320
|
+
503. Of
|
|
319
321
|
that last figure, about 4.9 GB is the memory-mapped cell table: file pages the
|
|
320
322
|
kernel can drop under pressure. The memory the fit itself allocated peaked at
|
|
321
323
|
4.6 GB (a separate profile, reading the process's anonymous and file-backed
|
|
@@ -323,7 +325,7 @@ pages apart).
|
|
|
323
325
|
|
|
324
326
|
**Size the batches to the design.** At its defaults hdfe_stream holds a whole
|
|
325
327
|
bucket of up to 20 million rows, with every covariate, while it sorts; that is
|
|
326
|
-
fine for a handful of covariates and not for hundreds (23.
|
|
328
|
+
fine for a handful of covariates and not for hundreds (23.0 GB at 167, out of
|
|
327
329
|
memory at 503). Every step that holds rows holds all the covariates, so scale
|
|
328
330
|
`rows_per_bucket`, `batch_rows` and `row_group_size` down with the width. The
|
|
329
331
|
benchmark aims at about 1 GB of design per bucket and 250 MB per batch and row
|
|
@@ -338,16 +340,16 @@ fit = feols_stream(fml, data, rows_per_bucket=min(20_000_000, int(1e9 // per_row
|
|
|
338
340
|
|
|
339
341
|
**Time grows with the square of the width.** The cross-products cost k × k per
|
|
340
342
|
row; they are formed chunk by chunk as BLAS matrix products, and 503 indicators
|
|
341
|
-
take
|
|
343
|
+
take 5½ minutes where 167 take under 1½. hdfe_stream is the fastest configuration at
|
|
342
344
|
every width measured.
|
|
343
345
|
|
|
344
346
|
**Timings on this machine are noisy where memory is tight.** Each point is one
|
|
345
347
|
run. Repeated runs of the default configuration at 83 indicators, which holds
|
|
346
|
-
12.
|
|
348
|
+
12.4 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
|
|
347
349
|
The differences between libraries above are far larger than that.
|
|
348
350
|
|
|
349
351
|
**Disk grows with the width too**, since the working copy of the rows carries
|
|
350
|
-
every covariate: 0.3 GB at 8 indicators,
|
|
352
|
+
every covariate: 0.3 GB at 8 indicators, 5.5 GB at 503.
|
|
351
353
|
|
|
352
354
|
**How the design is built matters.** A formula term such as `i(age_bin)` is
|
|
353
355
|
carried through the partitioning as the one column it is computed from, and the
|
|
@@ -372,12 +374,13 @@ python benchmarks/akm_benchmark.py --memory-sweep 1000000
|
|
|
372
374
|
|
|
373
375
|
| `rows_per_bucket` | `batch_rows` | wall time | peak memory | peak disk |
|
|
374
376
|
|---:|---:|---:|---:|---:|
|
|
375
|
-
| 20,000,000 (default) | 2,000,000 |
|
|
376
|
-
| 1,000,000 | 500,000 |
|
|
377
|
-
| 250,000 | 200,000 |
|
|
378
|
-
| 100,000 | 100,000 |
|
|
377
|
+
| 20,000,000 (default) | 2,000,000 | 11.2 s | 2,341 MB | 818 MB |
|
|
378
|
+
| 1,000,000 | 500,000 | 11.0 s | 1,849 MB | 821 MB |
|
|
379
|
+
| 250,000 | 200,000 | 11.6 s | 1,384 MB | 863 MB |
|
|
380
|
+
| 100,000 | 100,000 | 11.6 s | 1,555 MB | 876 MB |
|
|
379
381
|
|
|
380
|
-
|
|
382
|
+
The saving comes from the first two steps down; below that, the peak rises
|
|
383
|
+
again. The defaults are tuned for a
|
|
381
384
|
machine with room to spare; if memory is the binding constraint, set
|
|
382
385
|
`rows_per_bucket` to something near what you can afford and leave the rest
|
|
383
386
|
alone (with many covariates, scale `batch_rows` and `row_group_size` down too;
|
|
@@ -390,8 +393,10 @@ Leave-out estimation has a memory knob of its own, `scratch_mb`; see
|
|
|
390
393
|
[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance).
|
|
391
394
|
|
|
392
395
|
Measured on an Intel Core Ultra 7 258V, 8 cores, 26 GB RAM, Linux (WSL2);
|
|
393
|
-
Python 3.
|
|
394
|
-
pyfixest 0.60.0, xhdfe 2.28.
|
|
396
|
+
Python 3.14.8, polars 2.0.0, numpy 2.5.3, numba 0.68.0, scipy 1.18.1,
|
|
397
|
+
pyfixest 0.60.0, xhdfe 2.28.1 (CPU backend). In the regression benchmarks
|
|
398
|
+
hdfe_stream was measured on 2026-10-07 and pyfixest and xhdfe on 2026-10-06;
|
|
399
|
+
the GLM and KSS benchmarks were measured on 2026-10-07 and 2026-10-08. The exact versions are in
|
|
395
400
|
[benchmarks/results/machine.json](https://github.com/leetucker/hdfe-stream/blob/main/benchmarks/results/machine.json). Numba
|
|
396
401
|
kernels are compiled on first use and cached to disk; the benchmark runs a
|
|
397
402
|
warm-up fit so compilation is not charged to any configuration.
|
|
@@ -499,7 +504,7 @@ errors, and KSS's weak-identification diagnostic and q = 1 interval.
|
|
|
499
504
|
**[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md)** covers the options, the standard errors and their
|
|
500
505
|
measured coverage, weak identification, validation, reproducibility, and
|
|
501
506
|
[performance against xhdfe](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance): at 5 million workers,
|
|
502
|
-
|
|
507
|
+
4.8 GB of memory against xhdfe's 20 GB, with standard errors in 49 minutes
|
|
503
508
|
where xhdfe's did not finish in three hours.
|
|
504
509
|
**[docs/kss_methodological_differences.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss_methodological_differences.md)**
|
|
505
510
|
lists every way this implementation differs from LeaveOutTwoWay,
|