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.
Files changed (97) hide show
  1. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/CHANGELOG.md +60 -0
  2. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/CITATION.cff +2 -2
  3. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/PKG-INFO +48 -43
  4. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/README.md +47 -42
  5. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_disk.dark.svg +104 -104
  6. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_disk.light.svg +104 -104
  7. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_memory.dark.svg +132 -132
  8. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_memory.light.svg +132 -132
  9. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_time.dark.svg +238 -211
  10. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/akm_time.light.svg +238 -211
  11. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.dark.svg +61 -73
  12. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.light.svg +61 -73
  13. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.dark.svg +68 -66
  14. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.light.svg +68 -66
  15. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_time.dark.svg +110 -108
  16. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/covariates_time.light.svg +110 -108
  17. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.dark.svg +172 -199
  18. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.light.svg +172 -199
  19. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.dark.svg +99 -99
  20. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.light.svg +99 -99
  21. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.dark.svg +187 -211
  22. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.light.svg +187 -211
  23. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_disk.dark.svg +225 -252
  24. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_disk.light.svg +225 -252
  25. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_memory.dark.svg +182 -182
  26. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_memory.light.svg +182 -182
  27. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_time.dark.svg +207 -207
  28. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/glm_time.light.svg +207 -207
  29. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_disk.dark.svg +115 -130
  30. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_disk.light.svg +115 -130
  31. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_memory.dark.svg +110 -110
  32. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_memory.light.svg +110 -110
  33. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_time.dark.svg +184 -199
  34. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/figures/kss_time.light.svg +184 -199
  35. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/glm.md +32 -30
  36. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/kss.md +18 -18
  37. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/__init__.py +6 -5
  38. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/am_interval.py +10 -6
  39. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/estimator.py +12 -8
  40. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/inverse.py +15 -0
  41. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_base.py +157 -74
  42. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_slopes.py +0 -51
  43. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout.py +2 -2
  44. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_match.py +13 -10
  45. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/passes.py +90 -41
  46. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/reporting.py +5 -5
  47. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/results.py +14 -9
  48. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/solve.py +46 -78
  49. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/utils.py +10 -2
  50. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/workspace.py +64 -6
  51. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/PKG-INFO +48 -43
  52. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/pyproject.toml +1 -1
  53. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_cleanup.py +57 -0
  54. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_leaveout.py +5 -3
  55. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_logging.py +18 -0
  56. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/LICENSE.md +0 -0
  57. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/MANIFEST.in +0 -0
  58. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/docs/kss_methodological_differences.md +0 -0
  59. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/_columns.py +0 -0
  60. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/_types.py +0 -0
  61. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/api.py +0 -0
  62. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/families.py +0 -0
  63. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/feterms.py +0 -0
  64. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/formula.py +0 -0
  65. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/glm.py +0 -0
  66. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/inference.py +0 -0
  67. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_glm.py +0 -0
  68. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_graph.py +0 -0
  69. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_inverse.py +0 -0
  70. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/kernels_se.py +0 -0
  71. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_se.py +0 -0
  72. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_weakid.py +0 -0
  73. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/py.typed +0 -0
  74. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/report.py +0 -0
  75. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream/simulate.py +0 -0
  76. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/SOURCES.txt +0 -0
  77. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/dependency_links.txt +0 -0
  78. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/requires.txt +0 -0
  79. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/top_level.txt +0 -0
  80. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/setup.cfg +0 -0
  81. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/conftest.py +0 -0
  82. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_am_interval.py +0 -0
  83. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_column_names.py +0 -0
  84. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_crv3.py +0 -0
  85. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_feis.py +0 -0
  86. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_fewer_fe.py +0 -0
  87. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_formula.py +0 -0
  88. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_glm.py +0 -0
  89. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_inverse.py +0 -0
  90. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_iv_weights.py +0 -0
  91. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_lowlevel.py +0 -0
  92. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_ols.py +0 -0
  93. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_reporting.py +0 -0
  94. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_sample.py +0 -0
  95. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_simulate.py +0 -0
  96. {hdfe_stream-0.2.1 → hdfe_stream-0.2.2}/tests/test_singletons.py +0 -0
  97. {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.1
11
- date-released: 2026-10-05
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.1
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 4e-10, so these are measurements of equally good answers. Reproduce
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) | 888 s | 20.7 GB | — |
282
- | pyfixest (LSMR) | 150 s | 25.8 GB | — |
283
- | xhdfe | 126 s | 19.1 GB | — |
284
- | hdfe_stream (`stream_cg`) | 40 s | 5.4 GB | 3.9 GB |
285
- | **hdfe_stream (`stream_cg`, low memory)** | **48 s** | **3.7 GB** | 4.0 GB |
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–26 GB: pyfixest's LSMR run needed almost all of this machine's 26 GB.
291
- hdfe_stream peaks at 5.4 GB with its defaults and 3.7 GB with the batch and
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 20% more time and the same answer. What it
294
- spends instead is disk: about 90 bytes per row, 4 GB at 42.5 million rows, freed
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.8 GB at 25,000
297
- workers, 1.3 GB at a million, 3.7 GB at five million.
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 400,000 workers up, hdfe_stream is the
300
- fastest configuration measured. At 5 million workers it takes 40 s against
301
- xhdfe's 126 s and pyfixest's 150 s (LSMR) or 888 s (MAP). With AKM panel data,
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.8 GB, most of it importing polars,
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.27 GB and
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 about 6x slower than `LsmrDemeaner`
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.7 GB at 5
320
- million workers, where `stream_cg` and `explicit` stay near 5.4 GB. Prefer
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 | 219 s, 5.7 GB | 17 s, 7.3 GB | 18 s, 9.0 GB | 7 s, 2.8 GB | 7 s, 2.9 GB |
358
- | 41 | 522 s, 13.1 GB | 56 s, 17.3 GB | 55 s, 18.1 GB | 21 s, 7.0 GB | 20 s, 3.7 GB |
359
- | 83 | 1,026 s, 24.8 GB | 189 s, 25.4 GB | — | 71 s, 12.5 GB | 56 s, 4.5 GB |
360
- | 167 | — | — | — | 114 s, 23.6 GB | 112 s, 5.1 GB |
361
- | 503 | — | — | — | — | **351 s, 8.6 GB** |
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 pyfixest was within 1 GB of this machine's 26 GB
365
- at 83 indicators and out of memory at 167; xhdfe was already out of memory at
366
- 83. hdfe_stream sized to the
367
- design goes from 2.9 GB at 8 indicators to 5.1 GB at 167 and 8.6 GB at 503. Of
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.6 GB at 167, out of
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 6 minutes where 167 take 2. hdfe_stream is the fastest configuration at
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.5 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
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, 6 GB at 503.
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 | 7.1 s | 3,861 MB | 779 MB |
425
- | 1,000,000 | 500,000 | 7.4 s | 1,560 MB | 781 MB |
426
- | 250,000 | 200,000 | 7.8 s | 1,343 MB | 795 MB |
427
- | 100,000 | 100,000 | 9.3 s | 1,280 MB | 789 MB |
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
- Most of the saving comes from the first step down. The defaults are tuned for a
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.13.5, polars 1.44.2, numpy 2.5.3, numba 0.67.0, scipy 1.18.1,
443
- pyfixest 0.60.0, xhdfe 2.28.0 (CPU backend). The exact versions are in
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
- 5.3 GB of memory against xhdfe's 20 GB, with standard errors in 46 minutes
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 4e-10, so these are measurements of equally good answers. Reproduce
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) | 888 s | 20.7 GB | — |
233
- | pyfixest (LSMR) | 150 s | 25.8 GB | — |
234
- | xhdfe | 126 s | 19.1 GB | — |
235
- | hdfe_stream (`stream_cg`) | 40 s | 5.4 GB | 3.9 GB |
236
- | **hdfe_stream (`stream_cg`, low memory)** | **48 s** | **3.7 GB** | 4.0 GB |
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–26 GB: pyfixest's LSMR run needed almost all of this machine's 26 GB.
242
- hdfe_stream peaks at 5.4 GB with its defaults and 3.7 GB with the batch and
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 20% more time and the same answer. What it
245
- spends instead is disk: about 90 bytes per row, 4 GB at 42.5 million rows, freed
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.8 GB at 25,000
248
- workers, 1.3 GB at a million, 3.7 GB at five million.
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 400,000 workers up, hdfe_stream is the
251
- fastest configuration measured. At 5 million workers it takes 40 s against
252
- xhdfe's 126 s and pyfixest's 150 s (LSMR) or 888 s (MAP). With AKM panel data,
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.8 GB, most of it importing polars,
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.27 GB and
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 about 6x slower than `LsmrDemeaner`
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.7 GB at 5
271
- million workers, where `stream_cg` and `explicit` stay near 5.4 GB. Prefer
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 | 219 s, 5.7 GB | 17 s, 7.3 GB | 18 s, 9.0 GB | 7 s, 2.8 GB | 7 s, 2.9 GB |
309
- | 41 | 522 s, 13.1 GB | 56 s, 17.3 GB | 55 s, 18.1 GB | 21 s, 7.0 GB | 20 s, 3.7 GB |
310
- | 83 | 1,026 s, 24.8 GB | 189 s, 25.4 GB | — | 71 s, 12.5 GB | 56 s, 4.5 GB |
311
- | 167 | — | — | — | 114 s, 23.6 GB | 112 s, 5.1 GB |
312
- | 503 | — | — | — | — | **351 s, 8.6 GB** |
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 pyfixest was within 1 GB of this machine's 26 GB
316
- at 83 indicators and out of memory at 167; xhdfe was already out of memory at
317
- 83. hdfe_stream sized to the
318
- design goes from 2.9 GB at 8 indicators to 5.1 GB at 167 and 8.6 GB at 503. Of
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.6 GB at 167, out of
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 6 minutes where 167 take 2. hdfe_stream is the fastest configuration at
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.5 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
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, 6 GB at 503.
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 | 7.1 s | 3,861 MB | 779 MB |
376
- | 1,000,000 | 500,000 | 7.4 s | 1,560 MB | 781 MB |
377
- | 250,000 | 200,000 | 7.8 s | 1,343 MB | 795 MB |
378
- | 100,000 | 100,000 | 9.3 s | 1,280 MB | 789 MB |
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
- Most of the saving comes from the first step down. The defaults are tuned for a
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.13.5, polars 1.44.2, numpy 2.5.3, numba 0.67.0, scipy 1.18.1,
394
- pyfixest 0.60.0, xhdfe 2.28.0 (CPU backend). The exact versions are in
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
- 5.3 GB of memory against xhdfe's 20 GB, with standard errors in 46 minutes
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,