hdfe-stream 0.2.0__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.2/CHANGELOG.md +136 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/CITATION.cff +2 -2
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/PKG-INFO +67 -57
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/README.md +65 -55
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_disk.dark.svg +104 -104
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_disk.light.svg +104 -104
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_memory.dark.svg +132 -132
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_memory.light.svg +132 -132
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_time.dark.svg +238 -211
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/akm_time.light.svg +238 -211
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.dark.svg +61 -73
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_disk.light.svg +61 -73
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.dark.svg +68 -66
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_memory.light.svg +68 -66
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_time.dark.svg +110 -108
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/covariates_time.light.svg +110 -108
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.dark.svg +172 -199
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_disk.light.svg +172 -199
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.dark.svg +99 -99
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_memory.light.svg +99 -99
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.dark.svg +187 -211
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_covariates_time.light.svg +187 -211
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_disk.dark.svg +225 -252
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_disk.light.svg +225 -252
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_memory.dark.svg +182 -182
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_memory.light.svg +182 -182
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_time.dark.svg +207 -207
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/glm_time.light.svg +207 -207
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_disk.dark.svg +115 -130
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_disk.light.svg +115 -130
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_memory.dark.svg +110 -110
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_memory.light.svg +110 -110
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_time.dark.svg +184 -199
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/figures/kss_time.light.svg +184 -199
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/glm.md +32 -30
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/kss.md +18 -18
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/__init__.py +6 -5
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/am_interval.py +10 -6
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/estimator.py +48 -13
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/glm.py +1 -1
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/inference.py +8 -4
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/inverse.py +15 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_base.py +178 -79
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_slopes.py +0 -51
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/leaveout.py +2 -2
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_match.py +13 -10
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/passes.py +130 -46
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/reporting.py +5 -5
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/results.py +19 -13
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/solve.py +46 -78
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/utils.py +10 -2
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/workspace.py +64 -6
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/PKG-INFO +67 -57
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/requires.txt +1 -1
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/pyproject.toml +2 -2
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_cleanup.py +71 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_leaveout.py +5 -3
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_logging.py +18 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_ols.py +62 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_sample.py +57 -0
- hdfe_stream-0.2.0/CHANGELOG.md +0 -44
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/LICENSE.md +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/MANIFEST.in +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/docs/kss_methodological_differences.md +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/_columns.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/_types.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/api.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/families.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/feterms.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/formula.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_glm.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_graph.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_inverse.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/kernels_se.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_se.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/leaveout_weakid.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/py.typed +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/report.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream/simulate.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/SOURCES.txt +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/dependency_links.txt +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/hdfe_stream.egg-info/top_level.txt +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/setup.cfg +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/conftest.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_am_interval.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_column_names.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_crv3.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_feis.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_fewer_fe.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_formula.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_glm.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_inverse.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_iv_weights.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_lowlevel.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_reporting.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_simulate.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_singletons.py +0 -0
- {hdfe_stream-0.2.0 → hdfe_stream-0.2.2}/tests/test_vcov.py +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
63
|
+
## 0.2.1
|
|
64
|
+
|
|
65
|
+
- **`fe_dof="exact"` no longer depends on the order of the fixed effects.**
|
|
66
|
+
With three or more fixed effects it counted the connected components of the
|
|
67
|
+
streamed dimension and whichever came next in the formula, so reordering the
|
|
68
|
+
formula could change the standard errors. It now counts the components of
|
|
69
|
+
every pair of dimensions and uses the pair with the most, which is never
|
|
70
|
+
more than the true number of redundant levels. With two fixed effects
|
|
71
|
+
nothing changes, except in the clustered case below.
|
|
72
|
+
- **Clustered standard errors with fixed effects nested in the clusters.**
|
|
73
|
+
Under `fe_dof="exact"`, the components of a pair of fixed effects that are
|
|
74
|
+
both nested within the clusters were subtracted from K, although those
|
|
75
|
+
fixed effects' levels are already dropped from it. K could then fall below
|
|
76
|
+
the number of covariates and the standard errors were too small. Such a pair
|
|
77
|
+
is no longer used, and with no other pair K is pyfixest's.
|
|
78
|
+
- **`fit.sample()` labels every singleton.** With three or more fixed effects
|
|
79
|
+
that each had singleton levels, rows dropped only for the third or a later
|
|
80
|
+
dimension came back with `in_sample=True`, so the counts disagreed with
|
|
81
|
+
`n_obs` and the summary's singleton count. The fit itself was right. GLM
|
|
82
|
+
`"separation"` labels had the same fault.
|
|
83
|
+
- **A NaN in a fixed-effect or cluster column is missing**, as a null is,
|
|
84
|
+
rather than a level of its own. For fixed effects this matches pyfixest;
|
|
85
|
+
for clusters, where pyfixest raises an error, the row is dropped as it is
|
|
86
|
+
for a null cluster. `sample()` reports these rows as `"missing"`.
|
|
87
|
+
- **Polars 1.41 is excluded** from the supported versions. Its streaming join
|
|
88
|
+
panics (`called Option::unwrap() on a None value`) once a frame has about
|
|
89
|
+
130 columns, which a model with more than about 120 covariates reaches in
|
|
90
|
+
pass 0. Polars 1.40 and earlier, and 1.42 and later, are unaffected.
|
|
91
|
+
- **A panic inside Polars is reported as a `RuntimeError`** that names the
|
|
92
|
+
Polars version, says it is a Polars bug, and suggests a fix, instead of a
|
|
93
|
+
bare `PanicException`.
|
|
94
|
+
|
|
95
|
+
## 0.2.0
|
|
96
|
+
|
|
97
|
+
Changes to defaults, so results can differ from 0.1.0 without any change to
|
|
98
|
+
your code.
|
|
99
|
+
|
|
100
|
+
- **Default `vcov` is now `"iid"`**, with or without fixed effects, matching
|
|
101
|
+
pyfixest 0.60. Before, a model with fixed effects defaulted to CRV1 clustered
|
|
102
|
+
on the first fixed effect. Pass `vcov={"CRV1": "worker_id"}` for the old
|
|
103
|
+
behavior.
|
|
104
|
+
- **Singleton observations are dropped by default** (`fixef_rm="singleton"`),
|
|
105
|
+
as in pyfixest, repeating until none is left. `fixef_rm="none"` keeps them.
|
|
106
|
+
`leave_out_kss` fits with `fixef_rm="none"`, since its pruning defines the
|
|
107
|
+
sample.
|
|
108
|
+
|
|
109
|
+
New features.
|
|
110
|
+
|
|
111
|
+
- **`row_id` in `resid()`**: each row carries its position in the source before
|
|
112
|
+
any row was dropped, so residuals join back onto the source (`row_id=` renames
|
|
113
|
+
the column).
|
|
114
|
+
- **`fit.sample()`** returns the source, lazily, with `row_id`, `in_sample` and
|
|
115
|
+
`dropped_because` (`"missing"`, `"singleton"` or `"separation"`).
|
|
116
|
+
`multi.sample()` returns `{formula: frame}`.
|
|
117
|
+
- **Polars `DataFrame` inputs** are accepted wherever a Parquet path or
|
|
118
|
+
`LazyFrame` was.
|
|
119
|
+
- **Reserved column prefix `__hdfe_`.** All of the estimator's internal columns
|
|
120
|
+
now use it, and a source with a column starting with it is refused. This fixes
|
|
121
|
+
fits failing when a user column was named `w` (with weights), `v0`, `c1`,
|
|
122
|
+
`gcode` and similar.
|
|
123
|
+
|
|
124
|
+
## 0.1.0
|
|
125
|
+
|
|
126
|
+
First release.
|
|
127
|
+
|
|
128
|
+
- `feols_stream`: out-of-core OLS with several high-dimensional fixed effects,
|
|
129
|
+
varying slopes, weights, IV, and CRV1/CRV3 and other variance estimators.
|
|
130
|
+
- `fepois_stream` and `feglm_stream`: Poisson, logit and probit by iteratively
|
|
131
|
+
reweighted least squares over the same passes.
|
|
132
|
+
- AKM variance decomposition and leave-out (KSS) bias correction with
|
|
133
|
+
standard errors and weak-identification diagnostics.
|
|
134
|
+
- Reporting through pyfixest (`to_pyfixest()`, `etable`).
|
|
135
|
+
- `summary_json()` and `summary_dict()`: the model-level information of
|
|
136
|
+
`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.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
|
|
@@ -24,7 +24,7 @@ Classifier: Typing :: Typed
|
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE.md
|
|
27
|
-
Requires-Dist: polars
|
|
27
|
+
Requires-Dist: polars!=1.41.*,>=1.37
|
|
28
28
|
Requires-Dist: numpy>=1.24
|
|
29
29
|
Requires-Dist: numba>=0.59
|
|
30
30
|
Requires-Dist: pyarrow>=14.0
|
|
@@ -144,19 +144,24 @@ takes column names or Polars expressions instead of a formula string.
|
|
|
144
144
|
|
|
145
145
|
## Features
|
|
146
146
|
|
|
147
|
-
**Fixed effects.** Any number of dimensions, including one (a within
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
(`
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
(`fe_dof="
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
**Fixed effects.** Any number of dimensions, including one (a within regression,
|
|
148
|
+
`y ~ x | worker_id`) and none (OLS with an intercept, `y ~ x`). Interactions
|
|
149
|
+
with `^` (`firm_id^year`). Varying slopes on the streamed dimension in fixest
|
|
150
|
+
syntax (`worker_id[t]`, `worker_id[t, t2]`). Connected components of the
|
|
151
|
+
worker–firm graph are computed and reported. The degrees-of-freedom correction
|
|
152
|
+
either removes the redundant fixed-effect levels it can identify
|
|
153
|
+
(`fe_dof="exact"`, the default) or follows pyfixest's convention
|
|
154
|
+
(`fe_dof="pyfixest"`). `"exact"` counts the connected components of each pair of
|
|
155
|
+
fixed-effect dimensions and uses the pair with the most, plus one redundant
|
|
156
|
+
level for each further dimension. With two fixed effects this is the exact
|
|
157
|
+
number of identified parameters. With more it is an upper bound, so standard
|
|
158
|
+
errors are conservative. For clustered standard errors, fixed effects nested
|
|
159
|
+
within the clusters are dropped from the count as in pyfixest, and a pair of two
|
|
160
|
+
nested fixed effects is not used. Singleton observations, those alone in their
|
|
161
|
+
level of some fixed effect, are dropped as in pyfixest, repeating until none is
|
|
162
|
+
left (`fixef_rm="singleton"`, the default; `fixef_rm="none"` keeps them).
|
|
163
|
+
Finding them costs a few extra passes over the fixed-effect columns and memory
|
|
164
|
+
for the singleton levels, not for the rows.
|
|
160
165
|
|
|
161
166
|
**Covariates.** Transformations (`I(age**2)`, `log(x)`), categoricals (`C(x)`,
|
|
162
167
|
string columns), interactions (`:`, `*`), event-study terms
|
|
@@ -237,7 +242,7 @@ demeaners, and with [xhdfe](https://github.com/reisportela/xhdfe-xfe), on its
|
|
|
237
242
|
CPU backend. Sizes run from 25,000 to 5,000,000 workers, with firms = workers /
|
|
238
243
|
15 at every size and about 8.5 rows per worker, so the largest panel has 42.5
|
|
239
244
|
million rows. Every configuration agrees with every other on the coefficients
|
|
240
|
-
to within
|
|
245
|
+
to within 3e-10, so these are measurements of equally good answers. Reproduce
|
|
241
246
|
with:
|
|
242
247
|
|
|
243
248
|
```bash
|
|
@@ -273,46 +278,48 @@ At 5 million workers:
|
|
|
273
278
|
|
|
274
279
|
| configuration | wall time | peak memory | peak disk |
|
|
275
280
|
|---|---:|---:|---:|
|
|
276
|
-
| pyfixest (MAP, its default) |
|
|
277
|
-
| pyfixest (LSMR) |
|
|
278
|
-
| xhdfe |
|
|
279
|
-
| hdfe_stream (`
|
|
280
|
-
|
|
|
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 |
|
|
281
287
|
|
|
282
288
|
### Reading the figures
|
|
283
289
|
|
|
284
290
|
**Memory is the point.** At 5 million workers the in-memory libraries peak at
|
|
285
|
-
19–
|
|
286
|
-
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
|
|
287
293
|
bucket sizes turned down ("low memory": `rows_per_bucket=250_000`,
|
|
288
|
-
`batch_rows=200_000`), for about
|
|
289
|
-
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
|
|
290
296
|
when the fit finishes. The in-memory libraries' peak grows in proportion to the
|
|
291
|
-
data. hdfe_stream's low-memory setting grows far more slowly: 0.
|
|
292
|
-
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.
|
|
293
299
|
|
|
294
|
-
**It is not slower for it.** From about
|
|
295
|
-
fastest configuration measured
|
|
296
|
-
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,
|
|
297
304
|
reducing rows to worker-firm cells before solving more than pays for the disk
|
|
298
305
|
traffic. The simulated panel has about 8.5 rows per worker and one cell per
|
|
299
306
|
row, which suits this approach; a specification where the cell table is no
|
|
300
307
|
smaller than the data, and the fixed effects are less local, may do worse.
|
|
301
308
|
|
|
302
309
|
**Below about 400,000 workers, the in-memory libraries use less memory.**
|
|
303
|
-
Running any hdfe_stream fit costs about 0.
|
|
310
|
+
Running any hdfe_stream fit costs about 0.9 GB, most of it importing polars,
|
|
304
311
|
numba and scipy and starting Polars' streaming engine and numba's thread pools.
|
|
305
|
-
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
|
|
306
313
|
pyfixest at about 0.5 GB. This is the concrete version of "use pyfixest (or
|
|
307
314
|
xhdfe) if your data fits".
|
|
308
315
|
|
|
309
316
|
**The default pyfixest demeaner is not the one to compare against.**
|
|
310
|
-
`MapDemeaner` (alternating projections) is
|
|
311
|
-
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
|
|
312
319
|
projections struggles with. If you are comparing, compare against LSMR.
|
|
313
320
|
|
|
314
|
-
**hdfe_stream's `within` solver** uses more memory at scale: 14.
|
|
315
|
-
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
|
|
316
323
|
those when memory is the constraint.
|
|
317
324
|
|
|
318
325
|
### Many covariates
|
|
@@ -349,17 +356,17 @@ Wall time and peak memory ("—": ran out of this machine's 26 GB):
|
|
|
349
356
|
|
|
350
357
|
| covariates | pyfixest (MAP) | pyfixest (LSMR) | xhdfe | hdfe_stream | hdfe_stream, sized |
|
|
351
358
|
|---:|---:|---:|---:|---:|---:|
|
|
352
|
-
| 8 |
|
|
353
|
-
| 41 |
|
|
354
|
-
| 83 |
|
|
355
|
-
| 167 | — | — | — |
|
|
356
|
-
| 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** |
|
|
357
364
|
|
|
358
365
|
**The in-memory libraries run out first.** Their peak grows with rows ×
|
|
359
|
-
covariates. At 8.5 million rows
|
|
360
|
-
at 83 indicators and out of memory at 167
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
363
370
|
that last figure, about 4.9 GB is the memory-mapped cell table: file pages the
|
|
364
371
|
kernel can drop under pressure. The memory the fit itself allocated peaked at
|
|
365
372
|
4.6 GB (a separate profile, reading the process's anonymous and file-backed
|
|
@@ -367,7 +374,7 @@ pages apart).
|
|
|
367
374
|
|
|
368
375
|
**Size the batches to the design.** At its defaults hdfe_stream holds a whole
|
|
369
376
|
bucket of up to 20 million rows, with every covariate, while it sorts; that is
|
|
370
|
-
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
|
|
371
378
|
memory at 503). Every step that holds rows holds all the covariates, so scale
|
|
372
379
|
`rows_per_bucket`, `batch_rows` and `row_group_size` down with the width. The
|
|
373
380
|
benchmark aims at about 1 GB of design per bucket and 250 MB per batch and row
|
|
@@ -382,16 +389,16 @@ fit = feols_stream(fml, data, rows_per_bucket=min(20_000_000, int(1e9 // per_row
|
|
|
382
389
|
|
|
383
390
|
**Time grows with the square of the width.** The cross-products cost k × k per
|
|
384
391
|
row; they are formed chunk by chunk as BLAS matrix products, and 503 indicators
|
|
385
|
-
take
|
|
392
|
+
take 5½ minutes where 167 take under 1½. hdfe_stream is the fastest configuration at
|
|
386
393
|
every width measured.
|
|
387
394
|
|
|
388
395
|
**Timings on this machine are noisy where memory is tight.** Each point is one
|
|
389
396
|
run. Repeated runs of the default configuration at 83 indicators, which holds
|
|
390
|
-
12.
|
|
397
|
+
12.4 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
|
|
391
398
|
The differences between libraries above are far larger than that.
|
|
392
399
|
|
|
393
400
|
**Disk grows with the width too**, since the working copy of the rows carries
|
|
394
|
-
every covariate: 0.3 GB at 8 indicators,
|
|
401
|
+
every covariate: 0.3 GB at 8 indicators, 5.5 GB at 503.
|
|
395
402
|
|
|
396
403
|
**How the design is built matters.** A formula term such as `i(age_bin)` is
|
|
397
404
|
carried through the partitioning as the one column it is computed from, and the
|
|
@@ -416,12 +423,13 @@ python benchmarks/akm_benchmark.py --memory-sweep 1000000
|
|
|
416
423
|
|
|
417
424
|
| `rows_per_bucket` | `batch_rows` | wall time | peak memory | peak disk |
|
|
418
425
|
|---:|---:|---:|---:|---:|
|
|
419
|
-
| 20,000,000 (default) | 2,000,000 |
|
|
420
|
-
| 1,000,000 | 500,000 |
|
|
421
|
-
| 250,000 | 200,000 |
|
|
422
|
-
| 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 |
|
|
423
430
|
|
|
424
|
-
|
|
431
|
+
The saving comes from the first two steps down; below that, the peak rises
|
|
432
|
+
again. The defaults are tuned for a
|
|
425
433
|
machine with room to spare; if memory is the binding constraint, set
|
|
426
434
|
`rows_per_bucket` to something near what you can afford and leave the rest
|
|
427
435
|
alone (with many covariates, scale `batch_rows` and `row_group_size` down too;
|
|
@@ -434,8 +442,10 @@ Leave-out estimation has a memory knob of its own, `scratch_mb`; see
|
|
|
434
442
|
[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance).
|
|
435
443
|
|
|
436
444
|
Measured on an Intel Core Ultra 7 258V, 8 cores, 26 GB RAM, Linux (WSL2);
|
|
437
|
-
Python 3.
|
|
438
|
-
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
|
|
439
449
|
[benchmarks/results/machine.json](https://github.com/leetucker/hdfe-stream/blob/main/benchmarks/results/machine.json). Numba
|
|
440
450
|
kernels are compiled on first use and cached to disk; the benchmark runs a
|
|
441
451
|
warm-up fit so compilation is not charged to any configuration.
|
|
@@ -543,7 +553,7 @@ errors, and KSS's weak-identification diagnostic and q = 1 interval.
|
|
|
543
553
|
**[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md)** covers the options, the standard errors and their
|
|
544
554
|
measured coverage, weak identification, validation, reproducibility, and
|
|
545
555
|
[performance against xhdfe](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance): at 5 million workers,
|
|
546
|
-
|
|
556
|
+
4.8 GB of memory against xhdfe's 20 GB, with standard errors in 49 minutes
|
|
547
557
|
where xhdfe's did not finish in three hours.
|
|
548
558
|
**[docs/kss_methodological_differences.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss_methodological_differences.md)**
|
|
549
559
|
lists every way this implementation differs from LeaveOutTwoWay,
|
|
@@ -95,19 +95,24 @@ takes column names or Polars expressions instead of a formula string.
|
|
|
95
95
|
|
|
96
96
|
## Features
|
|
97
97
|
|
|
98
|
-
**Fixed effects.** Any number of dimensions, including one (a within
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
(`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(`fe_dof="
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
**Fixed effects.** Any number of dimensions, including one (a within regression,
|
|
99
|
+
`y ~ x | worker_id`) and none (OLS with an intercept, `y ~ x`). Interactions
|
|
100
|
+
with `^` (`firm_id^year`). Varying slopes on the streamed dimension in fixest
|
|
101
|
+
syntax (`worker_id[t]`, `worker_id[t, t2]`). Connected components of the
|
|
102
|
+
worker–firm graph are computed and reported. The degrees-of-freedom correction
|
|
103
|
+
either removes the redundant fixed-effect levels it can identify
|
|
104
|
+
(`fe_dof="exact"`, the default) or follows pyfixest's convention
|
|
105
|
+
(`fe_dof="pyfixest"`). `"exact"` counts the connected components of each pair of
|
|
106
|
+
fixed-effect dimensions and uses the pair with the most, plus one redundant
|
|
107
|
+
level for each further dimension. With two fixed effects this is the exact
|
|
108
|
+
number of identified parameters. With more it is an upper bound, so standard
|
|
109
|
+
errors are conservative. For clustered standard errors, fixed effects nested
|
|
110
|
+
within the clusters are dropped from the count as in pyfixest, and a pair of two
|
|
111
|
+
nested fixed effects is not used. Singleton observations, those alone in their
|
|
112
|
+
level of some fixed effect, are dropped as in pyfixest, repeating until none is
|
|
113
|
+
left (`fixef_rm="singleton"`, the default; `fixef_rm="none"` keeps them).
|
|
114
|
+
Finding them costs a few extra passes over the fixed-effect columns and memory
|
|
115
|
+
for the singleton levels, not for the rows.
|
|
111
116
|
|
|
112
117
|
**Covariates.** Transformations (`I(age**2)`, `log(x)`), categoricals (`C(x)`,
|
|
113
118
|
string columns), interactions (`:`, `*`), event-study terms
|
|
@@ -188,7 +193,7 @@ demeaners, and with [xhdfe](https://github.com/reisportela/xhdfe-xfe), on its
|
|
|
188
193
|
CPU backend. Sizes run from 25,000 to 5,000,000 workers, with firms = workers /
|
|
189
194
|
15 at every size and about 8.5 rows per worker, so the largest panel has 42.5
|
|
190
195
|
million rows. Every configuration agrees with every other on the coefficients
|
|
191
|
-
to within
|
|
196
|
+
to within 3e-10, so these are measurements of equally good answers. Reproduce
|
|
192
197
|
with:
|
|
193
198
|
|
|
194
199
|
```bash
|
|
@@ -224,46 +229,48 @@ At 5 million workers:
|
|
|
224
229
|
|
|
225
230
|
| configuration | wall time | peak memory | peak disk |
|
|
226
231
|
|---|---:|---:|---:|
|
|
227
|
-
| pyfixest (MAP, its default) |
|
|
228
|
-
| pyfixest (LSMR) |
|
|
229
|
-
| xhdfe |
|
|
230
|
-
| hdfe_stream (`
|
|
231
|
-
|
|
|
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 |
|
|
232
238
|
|
|
233
239
|
### Reading the figures
|
|
234
240
|
|
|
235
241
|
**Memory is the point.** At 5 million workers the in-memory libraries peak at
|
|
236
|
-
19–
|
|
237
|
-
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
|
|
238
244
|
bucket sizes turned down ("low memory": `rows_per_bucket=250_000`,
|
|
239
|
-
`batch_rows=200_000`), for about
|
|
240
|
-
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
|
|
241
247
|
when the fit finishes. The in-memory libraries' peak grows in proportion to the
|
|
242
|
-
data. hdfe_stream's low-memory setting grows far more slowly: 0.
|
|
243
|
-
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.
|
|
244
250
|
|
|
245
|
-
**It is not slower for it.** From about
|
|
246
|
-
fastest configuration measured
|
|
247
|
-
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,
|
|
248
255
|
reducing rows to worker-firm cells before solving more than pays for the disk
|
|
249
256
|
traffic. The simulated panel has about 8.5 rows per worker and one cell per
|
|
250
257
|
row, which suits this approach; a specification where the cell table is no
|
|
251
258
|
smaller than the data, and the fixed effects are less local, may do worse.
|
|
252
259
|
|
|
253
260
|
**Below about 400,000 workers, the in-memory libraries use less memory.**
|
|
254
|
-
Running any hdfe_stream fit costs about 0.
|
|
261
|
+
Running any hdfe_stream fit costs about 0.9 GB, most of it importing polars,
|
|
255
262
|
numba and scipy and starting Polars' streaming engine and numba's thread pools.
|
|
256
|
-
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
|
|
257
264
|
pyfixest at about 0.5 GB. This is the concrete version of "use pyfixest (or
|
|
258
265
|
xhdfe) if your data fits".
|
|
259
266
|
|
|
260
267
|
**The default pyfixest demeaner is not the one to compare against.**
|
|
261
|
-
`MapDemeaner` (alternating projections) is
|
|
262
|
-
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
|
|
263
270
|
projections struggles with. If you are comparing, compare against LSMR.
|
|
264
271
|
|
|
265
|
-
**hdfe_stream's `within` solver** uses more memory at scale: 14.
|
|
266
|
-
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
|
|
267
274
|
those when memory is the constraint.
|
|
268
275
|
|
|
269
276
|
### Many covariates
|
|
@@ -300,17 +307,17 @@ Wall time and peak memory ("—": ran out of this machine's 26 GB):
|
|
|
300
307
|
|
|
301
308
|
| covariates | pyfixest (MAP) | pyfixest (LSMR) | xhdfe | hdfe_stream | hdfe_stream, sized |
|
|
302
309
|
|---:|---:|---:|---:|---:|---:|
|
|
303
|
-
| 8 |
|
|
304
|
-
| 41 |
|
|
305
|
-
| 83 |
|
|
306
|
-
| 167 | — | — | — |
|
|
307
|
-
| 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** |
|
|
308
315
|
|
|
309
316
|
**The in-memory libraries run out first.** Their peak grows with rows ×
|
|
310
|
-
covariates. At 8.5 million rows
|
|
311
|
-
at 83 indicators and out of memory at 167
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
314
321
|
that last figure, about 4.9 GB is the memory-mapped cell table: file pages the
|
|
315
322
|
kernel can drop under pressure. The memory the fit itself allocated peaked at
|
|
316
323
|
4.6 GB (a separate profile, reading the process's anonymous and file-backed
|
|
@@ -318,7 +325,7 @@ pages apart).
|
|
|
318
325
|
|
|
319
326
|
**Size the batches to the design.** At its defaults hdfe_stream holds a whole
|
|
320
327
|
bucket of up to 20 million rows, with every covariate, while it sorts; that is
|
|
321
|
-
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
|
|
322
329
|
memory at 503). Every step that holds rows holds all the covariates, so scale
|
|
323
330
|
`rows_per_bucket`, `batch_rows` and `row_group_size` down with the width. The
|
|
324
331
|
benchmark aims at about 1 GB of design per bucket and 250 MB per batch and row
|
|
@@ -333,16 +340,16 @@ fit = feols_stream(fml, data, rows_per_bucket=min(20_000_000, int(1e9 // per_row
|
|
|
333
340
|
|
|
334
341
|
**Time grows with the square of the width.** The cross-products cost k × k per
|
|
335
342
|
row; they are formed chunk by chunk as BLAS matrix products, and 503 indicators
|
|
336
|
-
take
|
|
343
|
+
take 5½ minutes where 167 take under 1½. hdfe_stream is the fastest configuration at
|
|
337
344
|
every width measured.
|
|
338
345
|
|
|
339
346
|
**Timings on this machine are noisy where memory is tight.** Each point is one
|
|
340
347
|
run. Repeated runs of the default configuration at 83 indicators, which holds
|
|
341
|
-
12.
|
|
348
|
+
12.4 GB, took between 53 and 83 s; the sized configuration varied by about 15%.
|
|
342
349
|
The differences between libraries above are far larger than that.
|
|
343
350
|
|
|
344
351
|
**Disk grows with the width too**, since the working copy of the rows carries
|
|
345
|
-
every covariate: 0.3 GB at 8 indicators,
|
|
352
|
+
every covariate: 0.3 GB at 8 indicators, 5.5 GB at 503.
|
|
346
353
|
|
|
347
354
|
**How the design is built matters.** A formula term such as `i(age_bin)` is
|
|
348
355
|
carried through the partitioning as the one column it is computed from, and the
|
|
@@ -367,12 +374,13 @@ python benchmarks/akm_benchmark.py --memory-sweep 1000000
|
|
|
367
374
|
|
|
368
375
|
| `rows_per_bucket` | `batch_rows` | wall time | peak memory | peak disk |
|
|
369
376
|
|---:|---:|---:|---:|---:|
|
|
370
|
-
| 20,000,000 (default) | 2,000,000 |
|
|
371
|
-
| 1,000,000 | 500,000 |
|
|
372
|
-
| 250,000 | 200,000 |
|
|
373
|
-
| 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 |
|
|
374
381
|
|
|
375
|
-
|
|
382
|
+
The saving comes from the first two steps down; below that, the peak rises
|
|
383
|
+
again. The defaults are tuned for a
|
|
376
384
|
machine with room to spare; if memory is the binding constraint, set
|
|
377
385
|
`rows_per_bucket` to something near what you can afford and leave the rest
|
|
378
386
|
alone (with many covariates, scale `batch_rows` and `row_group_size` down too;
|
|
@@ -385,8 +393,10 @@ Leave-out estimation has a memory knob of its own, `scratch_mb`; see
|
|
|
385
393
|
[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance).
|
|
386
394
|
|
|
387
395
|
Measured on an Intel Core Ultra 7 258V, 8 cores, 26 GB RAM, Linux (WSL2);
|
|
388
|
-
Python 3.
|
|
389
|
-
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
|
|
390
400
|
[benchmarks/results/machine.json](https://github.com/leetucker/hdfe-stream/blob/main/benchmarks/results/machine.json). Numba
|
|
391
401
|
kernels are compiled on first use and cached to disk; the benchmark runs a
|
|
392
402
|
warm-up fit so compilation is not charged to any configuration.
|
|
@@ -494,7 +504,7 @@ errors, and KSS's weak-identification diagnostic and q = 1 interval.
|
|
|
494
504
|
**[docs/kss.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md)** covers the options, the standard errors and their
|
|
495
505
|
measured coverage, weak identification, validation, reproducibility, and
|
|
496
506
|
[performance against xhdfe](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss.md#performance): at 5 million workers,
|
|
497
|
-
|
|
507
|
+
4.8 GB of memory against xhdfe's 20 GB, with standard errors in 49 minutes
|
|
498
508
|
where xhdfe's did not finish in three hours.
|
|
499
509
|
**[docs/kss_methodological_differences.md](https://github.com/leetucker/hdfe-stream/blob/main/docs/kss_methodological_differences.md)**
|
|
500
510
|
lists every way this implementation differs from LeaveOutTwoWay,
|