logit-stream 0.1.0__py3-none-any.whl

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.
@@ -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