logit-stream 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logit_stream-0.1.0/CHANGELOG.md +86 -0
- logit_stream-0.1.0/CITATION.cff +21 -0
- logit_stream-0.1.0/LICENSE.md +21 -0
- logit_stream-0.1.0/MANIFEST.in +2 -0
- logit_stream-0.1.0/PKG-INFO +127 -0
- logit_stream-0.1.0/README.md +72 -0
- logit_stream-0.1.0/logit_stream/__init__.py +77 -0
- logit_stream-0.1.0/logit_stream/_choice.py +64 -0
- logit_stream-0.1.0/logit_stream/_frontend.py +78 -0
- logit_stream-0.1.0/logit_stream/api.py +542 -0
- logit_stream-0.1.0/logit_stream/device/__init__.py +57 -0
- logit_stream-0.1.0/logit_stream/device/_common.py +92 -0
- logit_stream-0.1.0/logit_stream/device/fe.py +108 -0
- logit_stream-0.1.0/logit_stream/device/meat.py +210 -0
- logit_stream-0.1.0/logit_stream/device/mixed.py +168 -0
- logit_stream-0.1.0/logit_stream/device/models.py +258 -0
- logit_stream-0.1.0/logit_stream/device/nested.py +144 -0
- logit_stream-0.1.0/logit_stream/device/random_effects.py +196 -0
- logit_stream-0.1.0/logit_stream/device/store.py +96 -0
- logit_stream-0.1.0/logit_stream/estimator.py +635 -0
- logit_stream-0.1.0/logit_stream/fe.py +539 -0
- logit_stream-0.1.0/logit_stream/mixed.py +518 -0
- logit_stream-0.1.0/logit_stream/models.py +689 -0
- logit_stream-0.1.0/logit_stream/multi_fe.py +242 -0
- logit_stream-0.1.0/logit_stream/nested.py +600 -0
- logit_stream-0.1.0/logit_stream/newton.py +519 -0
- logit_stream-0.1.0/logit_stream/optimize.py +180 -0
- logit_stream-0.1.0/logit_stream/py.typed +0 -0
- logit_stream-0.1.0/logit_stream/random_effects/__init__.py +84 -0
- logit_stream-0.1.0/logit_stream/random_effects/fit.py +257 -0
- logit_stream-0.1.0/logit_stream/random_effects/linalg.py +224 -0
- logit_stream-0.1.0/logit_stream/random_effects/model.py +763 -0
- logit_stream-0.1.0/logit_stream/random_effects/sigma.py +399 -0
- logit_stream-0.1.0/logit_stream/results.py +1000 -0
- logit_stream-0.1.0/logit_stream/simulate.py +257 -0
- logit_stream-0.1.0/logit_stream/store.py +377 -0
- logit_stream-0.1.0/logit_stream.egg-info/PKG-INFO +127 -0
- logit_stream-0.1.0/logit_stream.egg-info/SOURCES.txt +59 -0
- logit_stream-0.1.0/logit_stream.egg-info/dependency_links.txt +1 -0
- logit_stream-0.1.0/logit_stream.egg-info/requires.txt +29 -0
- logit_stream-0.1.0/logit_stream.egg-info/top_level.txt +1 -0
- logit_stream-0.1.0/pyproject.toml +103 -0
- logit_stream-0.1.0/setup.cfg +4 -0
- logit_stream-0.1.0/tests/test_api.py +146 -0
- logit_stream-0.1.0/tests/test_cfelogit.py +89 -0
- logit_stream-0.1.0/tests/test_clustering.py +75 -0
- logit_stream-0.1.0/tests/test_conditional.py +170 -0
- logit_stream-0.1.0/tests/test_device.py +341 -0
- logit_stream-0.1.0/tests/test_firth_and_fit.py +116 -0
- logit_stream-0.1.0/tests/test_fixed_effects.py +118 -0
- logit_stream-0.1.0/tests/test_kernels.py +105 -0
- logit_stream-0.1.0/tests/test_mixed.py +213 -0
- logit_stream-0.1.0/tests/test_multi_fe.py +121 -0
- logit_stream-0.1.0/tests/test_multinomial.py +226 -0
- logit_stream-0.1.0/tests/test_nested.py +318 -0
- logit_stream-0.1.0/tests/test_offset_start.py +74 -0
- logit_stream-0.1.0/tests/test_ordered.py +98 -0
- logit_stream-0.1.0/tests/test_probit.py +92 -0
- logit_stream-0.1.0/tests/test_random.py +593 -0
- logit_stream-0.1.0/tests/test_simulate.py +76 -0
- logit_stream-0.1.0/tests/test_weights_scale.py +53 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
The first release: logit models by streamed Newton's method, for data that
|
|
6
|
+
do not fit in memory. Formulas are pyfixest-style (hdfe-stream's front end);
|
|
7
|
+
data are Parquet files, Polars LazyFrames or DataFrames. Pass 0 writes the
|
|
8
|
+
evaluated design as blocks of arrays, and each Newton step is one read of
|
|
9
|
+
them.
|
|
10
|
+
|
|
11
|
+
### Models
|
|
12
|
+
|
|
13
|
+
- **Binary:** `logit_stream` and `probit_stream` (hdfe-stream's `feglm_stream`),
|
|
14
|
+
with any number of absorbed fixed effects, fit statistics (`loglik`,
|
|
15
|
+
`aic`, `bic`, `pseudo_r2_within`) and the incidental-parameter bias
|
|
16
|
+
correction (`bias_correction=`).
|
|
17
|
+
- **Multinomial and ordered:** `mlogit_stream`, `ologit_stream` and
|
|
18
|
+
`oprobit_stream`.
|
|
19
|
+
- **Conditional (McFadden):** `clogit_stream`; the conditional logit drops a
|
|
20
|
+
choice set whole when an alternative has missing values (Stata's
|
|
21
|
+
`cmclogit`), or keeps the complete alternatives with `altwise=True`.
|
|
22
|
+
- **Panels:** `cfelogit_stream`, Chamberlain's conditional fixed-effects logit
|
|
23
|
+
(Stata's `xtlogit, fe`); matches statsmodels' `ConditionalLogit`.
|
|
24
|
+
- **Nested logit** (experimental): `nlogit_stream`, a tree of nests of any
|
|
25
|
+
depth, in the utility-consistent normalization (Stata's `nlogit`
|
|
26
|
+
default). Matches R's mlogit.
|
|
27
|
+
- **Mixed logit** (experimental prototype): `mixlogit_stream`, by streamed
|
|
28
|
+
maximum simulated likelihood, with normal and lognormal coefficients,
|
|
29
|
+
panels, and Halton or pseudo-random draws regenerated in every read.
|
|
30
|
+
- **Firth's penalized likelihood** (`firth=True`) for the multinomial and
|
|
31
|
+
binary logit, matching brglm2 and logistf; an optional ridge penalty
|
|
32
|
+
(`l2=`).
|
|
33
|
+
|
|
34
|
+
### Fixed and random effects
|
|
35
|
+
|
|
36
|
+
- One absorbed fixed effect for the multinomial and ordered logit, solved
|
|
37
|
+
exactly in each Newton step (`"y ~ x | puma"`); several for the
|
|
38
|
+
multinomial logit (`"y ~ x | a + b"`), fitted as the equivalent Poisson
|
|
39
|
+
regression on the data expanded by outcome level (`fepois_stream`).
|
|
40
|
+
- Random intercepts for the multinomial and ordered logit (`random=`;
|
|
41
|
+
experimental): Laplace's approximation (matching `glmer` and ordinal's
|
|
42
|
+
`clmm`) or the joint mode, with an unstructured or diagonal covariance
|
|
43
|
+
over the outcome levels. Nested groupings at any depth, optionally within
|
|
44
|
+
a fixed effect, or two crossed. `varcorr()` and `ranef()` report the
|
|
45
|
+
covariance and the predicted effects.
|
|
46
|
+
- With a fixed effect, `loglik_fe_null` and `pseudo_r2_within`.
|
|
47
|
+
|
|
48
|
+
### Inference and results
|
|
49
|
+
|
|
50
|
+
- Variances: iid, heteroskedasticity-robust and clustered (one-way,
|
|
51
|
+
multi-way, or by an interaction), with Stata's small-sample factors for
|
|
52
|
+
maximum likelihood; the cluster jackknife (`{"CRV3": var}`). Frequency
|
|
53
|
+
weights, and analytic weights rescaled to average 1 as Stata does.
|
|
54
|
+
- Results subclass hdfe-stream's `RegressionResult`: `summary`, `tidy`,
|
|
55
|
+
`confint` (pointwise or simultaneous), `wald_test`, `with_vcov`,
|
|
56
|
+
`to_pyfixest` and `etable`, and lazy `predict`, `resid` (including the
|
|
57
|
+
ordered models' probability-scale and generalized residuals), `sample` and
|
|
58
|
+
`fixef`.
|
|
59
|
+
- `offset=` and warm starts (`start=`).
|
|
60
|
+
- Convergence by the Newton decrement, with step-halving and a
|
|
61
|
+
Levenberg–Marquardt fallback, collinearity detection and warnings for
|
|
62
|
+
separated coefficients.
|
|
63
|
+
|
|
64
|
+
### Scale and speed
|
|
65
|
+
|
|
66
|
+
- **GPU** (`device="cuda"`, experimental): the Newton passes and the
|
|
67
|
+
variances' scores pass of every model, on an NVIDIA GPU through CuPy, with
|
|
68
|
+
the blocks kept on the device when they fit; `polars_engine="gpu"` runs
|
|
69
|
+
pass 0 on Polars' GPU engine. Tested on a numpy stand-in; the GPU
|
|
70
|
+
benchmark covers the hardware.
|
|
71
|
+
- 0/1 design columns are stored as one byte each, and the multinomial
|
|
72
|
+
information is computed from each row's nonzeros when the design is mostly
|
|
73
|
+
indicators.
|
|
74
|
+
- Numba kernels that accumulate per thread (the nested, mixed and
|
|
75
|
+
conditional fixed-effects logit).
|
|
76
|
+
- `storage=`, `dtype=`, `block_mb=` and `chunk_mb=` control the block store.
|
|
77
|
+
|
|
78
|
+
### Tools
|
|
79
|
+
|
|
80
|
+
- `logit_stream.simulate`: `simulate_people`, `simulate_choices` (with
|
|
81
|
+
nests, or tasks and a random coefficient) and `simulate_panel`.
|
|
82
|
+
- `examples/`: a standalone script for each page of the guide.
|
|
83
|
+
- `benchmarks/`: ACS microdata, the Swissmetro survey, simulated panels and a
|
|
84
|
+
GPU suite, against scikit-learn, statsmodels, mord, pyfixest, xlogit,
|
|
85
|
+
jaxlogit, Biogeme, and R's glm, fixest, nnet, MASS, ordinal, survival,
|
|
86
|
+
mlogit, logitr and alpaca. Resumable.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software in your research, please cite it as below."
|
|
3
|
+
type: software
|
|
4
|
+
title: "logit-stream"
|
|
5
|
+
abstract: "Out-of-core logit models: binary, multinomial, ordered, conditional, nested, mixed and panel logit, with fixed and random effects, for data that do not fit in memory."
|
|
6
|
+
authors:
|
|
7
|
+
- family-names: Tucker
|
|
8
|
+
given-names: Lee
|
|
9
|
+
email: lee.c.tucker@census.gov
|
|
10
|
+
version: 0.1.0
|
|
11
|
+
date-released: 2026-10-29
|
|
12
|
+
license: MIT
|
|
13
|
+
repository-code: "https://github.com/leetucker/logit-stream"
|
|
14
|
+
url: "https://github.com/leetucker/logit-stream"
|
|
15
|
+
keywords:
|
|
16
|
+
- econometrics
|
|
17
|
+
- logit
|
|
18
|
+
- multinomial logit
|
|
19
|
+
- discrete choice
|
|
20
|
+
- out-of-core
|
|
21
|
+
- fixed effects
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lee Tucker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logit-stream
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Out-of-core logit models by streamed Newton: binary, multinomial, ordered and conditional logit, with absorbed fixed effects
|
|
5
|
+
Author: Lee Tucker
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/leetucker/logit-stream
|
|
8
|
+
Project-URL: Source, https://github.com/leetucker/logit-stream
|
|
9
|
+
Project-URL: Issues, https://github.com/leetucker/logit-stream/issues
|
|
10
|
+
Project-URL: Documentation, https://leetucker.github.io/logit-stream/
|
|
11
|
+
Project-URL: Changelog, https://github.com/leetucker/logit-stream/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: econometrics,logit,multinomial logit,ordered logit,conditional logit,discrete choice,out-of-core,fixed effects,imputation
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE.md
|
|
28
|
+
Requires-Dist: hdfe-stream[formula]<0.7,>=0.6
|
|
29
|
+
Requires-Dist: polars!=1.41.*,>=1.37
|
|
30
|
+
Requires-Dist: numpy>=1.24
|
|
31
|
+
Requires-Dist: numba>=0.59
|
|
32
|
+
Requires-Dist: pyarrow>=14.0
|
|
33
|
+
Requires-Dist: scipy>=1.11
|
|
34
|
+
Requires-Dist: threadpoolctl>=3.1
|
|
35
|
+
Provides-Extra: benchmark
|
|
36
|
+
Requires-Dist: scikit-learn>=1.5; extra == "benchmark"
|
|
37
|
+
Requires-Dist: statsmodels>=0.14; extra == "benchmark"
|
|
38
|
+
Requires-Dist: xlogit>=0.2.7; extra == "benchmark"
|
|
39
|
+
Requires-Dist: pylogit>=1.0.1; extra == "benchmark"
|
|
40
|
+
Requires-Dist: mord>=0.7; extra == "benchmark"
|
|
41
|
+
Requires-Dist: matplotlib>=3.7; extra == "benchmark"
|
|
42
|
+
Requires-Dist: pandas>=2.0; extra == "benchmark"
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Requires-Dist: sphinx>=8.0; extra == "docs"
|
|
45
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
46
|
+
Requires-Dist: furo>=2024.8.6; extra == "docs"
|
|
47
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
48
|
+
Requires-Dist: sphinx-design>=0.6; extra == "docs"
|
|
49
|
+
Provides-Extra: test
|
|
50
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
51
|
+
Requires-Dist: scikit-learn>=1.5; extra == "test"
|
|
52
|
+
Requires-Dist: statsmodels>=0.14; extra == "test"
|
|
53
|
+
Requires-Dist: pandas>=2.0; extra == "test"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# logit-stream
|
|
57
|
+
|
|
58
|
+
Logit models for data that do not fit in memory: binary, multinomial,
|
|
59
|
+
ordered, conditional, nested and mixed logit, fitted by Newton's method with
|
|
60
|
+
the data streamed off disk, from
|
|
61
|
+
[pyfixest](https://github.com/py-econometrics/pyfixest)-style formulas, with
|
|
62
|
+
absorbed fixed effects or random intercepts.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from logit_stream import mlogit_stream
|
|
66
|
+
|
|
67
|
+
fit = mlogit_stream(
|
|
68
|
+
"race ~ C(agecat) + female + C(inccat) + C(cit) | puma",
|
|
69
|
+
"acs/*.parquet", # a Parquet path or glob, never fully loaded
|
|
70
|
+
vcov={"CRV1": "puma"},
|
|
71
|
+
)
|
|
72
|
+
fit.summary()
|
|
73
|
+
fit.predict("people/*.parquet").sink_parquet("imputed.parquet") # lazily
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| | |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `logit_stream`, `probit_stream` | binary logit and probit with any number of fixed effects ([hdfe-stream](https://github.com/leetucker/hdfe-stream)'s `feglm_stream`) |
|
|
79
|
+
| `mlogit_stream` | multinomial logit (any number of fixed effects; several through a Poisson regression) |
|
|
80
|
+
| `ologit_stream`, `oprobit_stream` | ordered logit and probit |
|
|
81
|
+
| `clogit_stream` | conditional (McFadden) logit on long-format choice data |
|
|
82
|
+
| `cfelogit_stream` | Chamberlain's conditional fixed-effects logit for panels |
|
|
83
|
+
| `mixlogit_stream`, `nlogit_stream` | mixed and nested logit (prototypes) |
|
|
84
|
+
|
|
85
|
+
Newton's method for a logit needs the data only through sums over rows -- the
|
|
86
|
+
log-likelihood, its gradient and the information matrix -- so each step is one
|
|
87
|
+
read of the data, block by block, and one dense solve; the likelihoods are
|
|
88
|
+
concave, so a handful of steps suffice. Memory grows with the number of
|
|
89
|
+
coefficients, not of rows. The multinomial and ordered logit absorb one fixed
|
|
90
|
+
effect exactly, by eliminating its per-level blocks of the information matrix
|
|
91
|
+
in every step.
|
|
92
|
+
|
|
93
|
+
Results share [hdfe-stream](https://github.com/leetucker/hdfe-stream)'s
|
|
94
|
+
reporting: `summary()`, `tidy()`, `summary_dict()`, `summary_json()`,
|
|
95
|
+
`confint()`, `wald_test()`, `with_vcov()`, `etable()`, and lazy `predict()`,
|
|
96
|
+
`resid()`, `sample()` and `fixef()` frames. Estimates and standard errors
|
|
97
|
+
match statsmodels and scikit-learn (see `tests/`).
|
|
98
|
+
|
|
99
|
+
Documentation: <https://leetucker.github.io/logit-stream/> (build locally with
|
|
100
|
+
`pip install -e ".[docs]"` and `sphinx-build -b html docs docs/_build/html`).
|
|
101
|
+
`examples/` has a runnable script for each page of its guide, on simulated
|
|
102
|
+
data.
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install logit-stream
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Python 3.10 or later. On Python 3.15, pyfixest 0.60 (which hdfe-stream's
|
|
111
|
+
formula extra brings in) has no wheels yet: it builds from source with a Rust
|
|
112
|
+
toolchain and `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` set; everything else
|
|
113
|
+
installs from wheels.
|
|
114
|
+
|
|
115
|
+
## Benchmarks
|
|
116
|
+
|
|
117
|
+
`benchmarks/` compares logit-stream with scikit-learn, statsmodels, mord and
|
|
118
|
+
R's glm, fixest, nnet, MASS and ordinal on American Community Survey
|
|
119
|
+
microdata (10,000 to 12.9 million adults); with xlogit, pylogit,
|
|
120
|
+
statsmodels, jaxlogit, Biogeme and R's mlogit and logitr on the Swissmetro
|
|
121
|
+
choice survey, resampled to 10 million choice sets; on short panels with
|
|
122
|
+
statsmodels and R's survival, fixest and alpaca; and on a GPU. See
|
|
123
|
+
`benchmarks/README.md` and the documentation's benchmark pages.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# logit-stream
|
|
2
|
+
|
|
3
|
+
Logit models for data that do not fit in memory: binary, multinomial,
|
|
4
|
+
ordered, conditional, nested and mixed logit, fitted by Newton's method with
|
|
5
|
+
the data streamed off disk, from
|
|
6
|
+
[pyfixest](https://github.com/py-econometrics/pyfixest)-style formulas, with
|
|
7
|
+
absorbed fixed effects or random intercepts.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from logit_stream import mlogit_stream
|
|
11
|
+
|
|
12
|
+
fit = mlogit_stream(
|
|
13
|
+
"race ~ C(agecat) + female + C(inccat) + C(cit) | puma",
|
|
14
|
+
"acs/*.parquet", # a Parquet path or glob, never fully loaded
|
|
15
|
+
vcov={"CRV1": "puma"},
|
|
16
|
+
)
|
|
17
|
+
fit.summary()
|
|
18
|
+
fit.predict("people/*.parquet").sink_parquet("imputed.parquet") # lazily
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| | |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `logit_stream`, `probit_stream` | binary logit and probit with any number of fixed effects ([hdfe-stream](https://github.com/leetucker/hdfe-stream)'s `feglm_stream`) |
|
|
24
|
+
| `mlogit_stream` | multinomial logit (any number of fixed effects; several through a Poisson regression) |
|
|
25
|
+
| `ologit_stream`, `oprobit_stream` | ordered logit and probit |
|
|
26
|
+
| `clogit_stream` | conditional (McFadden) logit on long-format choice data |
|
|
27
|
+
| `cfelogit_stream` | Chamberlain's conditional fixed-effects logit for panels |
|
|
28
|
+
| `mixlogit_stream`, `nlogit_stream` | mixed and nested logit (prototypes) |
|
|
29
|
+
|
|
30
|
+
Newton's method for a logit needs the data only through sums over rows -- the
|
|
31
|
+
log-likelihood, its gradient and the information matrix -- so each step is one
|
|
32
|
+
read of the data, block by block, and one dense solve; the likelihoods are
|
|
33
|
+
concave, so a handful of steps suffice. Memory grows with the number of
|
|
34
|
+
coefficients, not of rows. The multinomial and ordered logit absorb one fixed
|
|
35
|
+
effect exactly, by eliminating its per-level blocks of the information matrix
|
|
36
|
+
in every step.
|
|
37
|
+
|
|
38
|
+
Results share [hdfe-stream](https://github.com/leetucker/hdfe-stream)'s
|
|
39
|
+
reporting: `summary()`, `tidy()`, `summary_dict()`, `summary_json()`,
|
|
40
|
+
`confint()`, `wald_test()`, `with_vcov()`, `etable()`, and lazy `predict()`,
|
|
41
|
+
`resid()`, `sample()` and `fixef()` frames. Estimates and standard errors
|
|
42
|
+
match statsmodels and scikit-learn (see `tests/`).
|
|
43
|
+
|
|
44
|
+
Documentation: <https://leetucker.github.io/logit-stream/> (build locally with
|
|
45
|
+
`pip install -e ".[docs]"` and `sphinx-build -b html docs docs/_build/html`).
|
|
46
|
+
`examples/` has a runnable script for each page of its guide, on simulated
|
|
47
|
+
data.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install logit-stream
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Python 3.10 or later. On Python 3.15, pyfixest 0.60 (which hdfe-stream's
|
|
56
|
+
formula extra brings in) has no wheels yet: it builds from source with a Rust
|
|
57
|
+
toolchain and `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` set; everything else
|
|
58
|
+
installs from wheels.
|
|
59
|
+
|
|
60
|
+
## Benchmarks
|
|
61
|
+
|
|
62
|
+
`benchmarks/` compares logit-stream with scikit-learn, statsmodels, mord and
|
|
63
|
+
R's glm, fixest, nnet, MASS and ordinal on American Community Survey
|
|
64
|
+
microdata (10,000 to 12.9 million adults); with xlogit, pylogit,
|
|
65
|
+
statsmodels, jaxlogit, Biogeme and R's mlogit and logitr on the Swissmetro
|
|
66
|
+
choice survey, resampled to 10 million choice sets; on short panels with
|
|
67
|
+
statsmodels and R's survival, fixest and alpaca; and on a GPU. See
|
|
68
|
+
`benchmarks/README.md` and the documentation's benchmark pages.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""logit-stream: logit models fitted out of core by streamed Newton.
|
|
2
|
+
|
|
3
|
+
Binary (`logit_stream`, `probit_stream`: hdfe-stream's `feglm_stream`, with
|
|
4
|
+
any number of absorbed fixed effects), multinomial (`mlogit_stream`),
|
|
5
|
+
ordered (`ologit_stream`, `oprobit_stream`), conditional (`clogit_stream`),
|
|
6
|
+
conditional fixed-effects (`cfelogit_stream`), mixed (`mixlogit_stream`) and
|
|
7
|
+
nested (`nlogit_stream`) logit, from pyfixest-style formulas, on data that
|
|
8
|
+
need not fit in memory: Parquet files, Polars LazyFrames, or DataFrames.
|
|
9
|
+
|
|
10
|
+
Layout
|
|
11
|
+
api.py the estimators
|
|
12
|
+
_frontend.py formulas -> models (hdfe-stream's formula compiler)
|
|
13
|
+
store.py pass 0, into hdfe-stream's block store (hdfe_stream.core.store)
|
|
14
|
+
models.py the likelihoods: per chunk, log-likelihood, gradient,
|
|
15
|
+
information, scores
|
|
16
|
+
newton.py Newton's method, collinearity, the variance's middle matrix
|
|
17
|
+
estimator.py formula + source -> result, and the inference
|
|
18
|
+
fe.py one absorbed fixed effect (multinomial, ordered)
|
|
19
|
+
multi_fe.py several, for the multinomial logit (a Poisson regression)
|
|
20
|
+
random_effects/ random intercepts (Laplace's approximation)
|
|
21
|
+
optimize.py Newton and quasi-Newton steps from an analytic gradient,
|
|
22
|
+
for the likelihoods that are not concave
|
|
23
|
+
_choice.py what the mixed and nested logit's fits share
|
|
24
|
+
mixed.py the mixed logit (simulated likelihood)
|
|
25
|
+
nested.py the nested logit
|
|
26
|
+
device/ the passes on a GPU (CuPy)
|
|
27
|
+
results.py the result classes
|
|
28
|
+
simulate.py simulated data for examples and tests
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from hdfe_stream.core.multcomp import bonferroni, holm
|
|
34
|
+
from hdfe_stream.core.reporting import etable
|
|
35
|
+
|
|
36
|
+
from .api import (
|
|
37
|
+
cfelogit_stream,
|
|
38
|
+
clogit_stream,
|
|
39
|
+
logit_stream,
|
|
40
|
+
mixlogit_stream,
|
|
41
|
+
mlogit_stream,
|
|
42
|
+
nlogit_stream,
|
|
43
|
+
ologit_stream,
|
|
44
|
+
oprobit_stream,
|
|
45
|
+
probit_stream,
|
|
46
|
+
)
|
|
47
|
+
from .results import (
|
|
48
|
+
ConditionalFELogitResult,
|
|
49
|
+
ConditionalLogitResult,
|
|
50
|
+
LogitMulti,
|
|
51
|
+
LogitResult,
|
|
52
|
+
MixedLogitResult,
|
|
53
|
+
MultinomialLogitResult,
|
|
54
|
+
NestedLogitResult,
|
|
55
|
+
OrderedLogitResult,
|
|
56
|
+
OrderedProbitResult,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _version():
|
|
61
|
+
try:
|
|
62
|
+
from importlib.metadata import version
|
|
63
|
+
return version("logit-stream")
|
|
64
|
+
except Exception: # noqa: BLE001 -- not installed
|
|
65
|
+
return "0.1.0"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__version__ = _version()
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"logit_stream", "probit_stream", "mlogit_stream", "ologit_stream", "oprobit_stream",
|
|
72
|
+
"clogit_stream", "cfelogit_stream", "mixlogit_stream", "nlogit_stream",
|
|
73
|
+
"LogitResult", "MultinomialLogitResult", "OrderedLogitResult", "OrderedProbitResult",
|
|
74
|
+
"ConditionalLogitResult", "ConditionalFELogitResult",
|
|
75
|
+
"MixedLogitResult", "NestedLogitResult",
|
|
76
|
+
"LogitMulti", "etable", "bonferroni", "holm", "__version__",
|
|
77
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""What the fits on choice data with `optimize._GradientDriver` (the mixed
|
|
2
|
+
and the nested logit) share: the one formula, the conditional logit as the
|
|
3
|
+
starting point, and the diagnostics of the fit."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def plan_one(fml, lf, engine, log, model):
|
|
13
|
+
"""The formula's one model, without fixed effects (`model` names the
|
|
14
|
+
estimator in the errors)."""
|
|
15
|
+
from ._frontend import _plan
|
|
16
|
+
log(f"planning {fml!r}")
|
|
17
|
+
specs = _plan(fml, lf, intercept=False, collect=lambda q, what: engine.collect(q, what))
|
|
18
|
+
if len(specs) != 1:
|
|
19
|
+
raise ValueError(f"multiple estimation is not available for the {model}")
|
|
20
|
+
spec = specs[0]
|
|
21
|
+
if spec.fe:
|
|
22
|
+
raise ValueError(f"{spec.fml}: fixed effects are not available for the {model}")
|
|
23
|
+
return spec
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clogit_start(p0, xnames, spec, logger, log):
|
|
27
|
+
"""The conditional logit on the store: the starting point (it refuses
|
|
28
|
+
collinear covariates, which these fits do not drop)."""
|
|
29
|
+
from .models import _Conditional
|
|
30
|
+
from .newton import _Driver
|
|
31
|
+
log("starting values: the conditional logit")
|
|
32
|
+
K = len(xnames)
|
|
33
|
+
cdrv = _Driver(p0.store, _Conditional(xnames, K), tol=1e-8, maxiter=50, l2=0.0,
|
|
34
|
+
chunk_mb=64.0, log=lambda m: None, logger=logger)
|
|
35
|
+
cfit = cdrv.run(np.zeros(K), spec.fml)
|
|
36
|
+
if cfit.dropped:
|
|
37
|
+
raise ValueError(f"{spec.fml}: collinear covariates {cfit.dropped}; drop them (they "
|
|
38
|
+
"are not dropped for this model)")
|
|
39
|
+
return cfit
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fit_diagnostics(fit, cfit, p0, *, storage, dtype, tol, t0, device=None, drv=None,
|
|
43
|
+
**more):
|
|
44
|
+
"""The diagnostics of a `_GradientFit` started from `cfit`."""
|
|
45
|
+
out = {
|
|
46
|
+
"optimizer": {"iterations": fit.iterations, "bhhh_steps": fit.bhhh_steps,
|
|
47
|
+
"newton_steps": fit.newton_steps, "quasi_steps": fit.quasi_steps,
|
|
48
|
+
"converged": fit.converged, "decrement": fit.decrement,
|
|
49
|
+
"loglik": fit.path, "passes": fit.passes, "seconds": fit.seconds,
|
|
50
|
+
"tol": tol},
|
|
51
|
+
"start": {"loglik_clogit": float(cfit.ll), "passes": cfit.passes},
|
|
52
|
+
"gradient": [float(v) for v in fit.g],
|
|
53
|
+
"pass0_seconds": p0.seconds, "n_rows": p0.n_obs, "n_raw": p0.n_raw,
|
|
54
|
+
"store": {"storage": storage, "dtype": dtype, "blocks": len(p0.store.blocks),
|
|
55
|
+
"bytes": int(p0.store.bytes)},
|
|
56
|
+
"disk_peak_gb": round((p0.store.bytes if storage == "disk" else 0) / 1e9, 4),
|
|
57
|
+
"seconds_total": round(time.time() - t0, 2),
|
|
58
|
+
**more,
|
|
59
|
+
}
|
|
60
|
+
if device is not None:
|
|
61
|
+
ds = drv._dstore
|
|
62
|
+
out["device"] = {"name": device.name, "resident": bool(ds and ds.resident),
|
|
63
|
+
"bytes": int(ds.bytes) if ds else 0}
|
|
64
|
+
return out
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Formula front end: a pyfixest-style formula -> the models to fit.
|
|
2
|
+
|
|
3
|
+
The right-hand side is compiled by hdfe-stream's formula compiler
|
|
4
|
+
(`hdfe_stream.core.formula`), which runs pyfixest's own parser and model
|
|
5
|
+
matrix on a tiny synthetic frame and turns each column back into a Polars
|
|
6
|
+
expression. Coefficient names and the coding of categorical variables are
|
|
7
|
+
therefore exactly pyfixest's. The outcome is handled here: for the
|
|
8
|
+
multinomial and ordered models it is a categorical variable, not a number,
|
|
9
|
+
so it is kept as a raw column and coded once its levels are known.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
import polars as pl
|
|
17
|
+
from hdfe_stream.core.feterms import parse_fe
|
|
18
|
+
from hdfe_stream.core.formula import (
|
|
19
|
+
categorical_vars,
|
|
20
|
+
compile_design,
|
|
21
|
+
numeric_expr,
|
|
22
|
+
pyfixest_formula_api,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class _Spec:
|
|
28
|
+
"""One model: its formula, outcome, covariates and fixed effects."""
|
|
29
|
+
|
|
30
|
+
fml: str
|
|
31
|
+
depvar: str
|
|
32
|
+
y: pl.Expr # the outcome, uncast
|
|
33
|
+
x: list = field(default_factory=list) # [(coefficient name, Float64 expr)]
|
|
34
|
+
fe: tuple = ()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _outcome_expr(dep, schema):
|
|
38
|
+
"""A column is taken as it is (strings, categoricals, integers);
|
|
39
|
+
anything else is a numeric expression such as ``I(educ > 12)``."""
|
|
40
|
+
if dep in schema:
|
|
41
|
+
return pl.col(dep)
|
|
42
|
+
return numeric_expr(dep)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _plan(fml: str, lf: pl.LazyFrame, intercept: bool, collect) -> list[_Spec]:
|
|
46
|
+
"""Expand `fml` into its models (pyfixest's multiple-estimation syntax:
|
|
47
|
+
several outcomes, ``sw()``, ``csw()``, ...).
|
|
48
|
+
|
|
49
|
+
`intercept`: whether a model without fixed effects gets the model
|
|
50
|
+
matrix's constant (multinomial logit does; ordered and conditional logit
|
|
51
|
+
do not, their cutpoints and choice sets absorbing it). Without it the
|
|
52
|
+
categorical coding is still reduced rank, as with it.
|
|
53
|
+
`collect(lf, what)` runs a query (so that the GPU engine applies).
|
|
54
|
+
"""
|
|
55
|
+
Formula, _ = pyfixest_formula_api()
|
|
56
|
+
schema = lf.collect_schema()
|
|
57
|
+
specs = Formula.parse(fml)
|
|
58
|
+
cats = set()
|
|
59
|
+
for s in specs:
|
|
60
|
+
if s.first_stage is not None:
|
|
61
|
+
raise ValueError(f"{s.formula}: IV formulas are not available for logit models")
|
|
62
|
+
cats |= categorical_vars(s.second_stage.split("~", 1)[1], schema)
|
|
63
|
+
levels = {}
|
|
64
|
+
for v in sorted(cats):
|
|
65
|
+
u = collect(lf.select(pl.col(v).drop_nulls().unique().sort()), f"levels of {v}")
|
|
66
|
+
levels[v] = u[v].to_list()
|
|
67
|
+
out = []
|
|
68
|
+
for s in specs:
|
|
69
|
+
dep, rhs = (t.strip() for t in s.second_stage.split("~", 1))
|
|
70
|
+
fe = tuple(parse_fe(s.fixed_effects))
|
|
71
|
+
for d in fe:
|
|
72
|
+
if "[" in d:
|
|
73
|
+
raise ValueError(f"{s.formula}: varying slopes are not available for logit models")
|
|
74
|
+
_, cols = compile_design(dep, rhs, schema, levels, intercept=intercept and not fe)
|
|
75
|
+
if not intercept:
|
|
76
|
+
cols = [(nm, ex) for nm, ex in cols if nm != "Intercept"]
|
|
77
|
+
out.append(_Spec(fml=s.formula, depvar=dep, y=_outcome_expr(dep, schema), x=cols, fe=fe))
|
|
78
|
+
return out
|