mnlite 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.
Files changed (36) hide show
  1. mnlite-0.1.0/CHANGELOG.md +24 -0
  2. mnlite-0.1.0/CITATION.cff +21 -0
  3. mnlite-0.1.0/LICENSE +21 -0
  4. mnlite-0.1.0/MANIFEST.in +9 -0
  5. mnlite-0.1.0/PKG-INFO +271 -0
  6. mnlite-0.1.0/README.md +231 -0
  7. mnlite-0.1.0/examples/bal_binary.csv +22 -0
  8. mnlite-0.1.0/examples/quickstart.py +48 -0
  9. mnlite-0.1.0/pyproject.toml +117 -0
  10. mnlite-0.1.0/setup.cfg +4 -0
  11. mnlite-0.1.0/src/mnlite/__init__.py +46 -0
  12. mnlite-0.1.0/src/mnlite/__main__.py +7 -0
  13. mnlite-0.1.0/src/mnlite/_stats.py +86 -0
  14. mnlite-0.1.0/src/mnlite/_version.py +1 -0
  15. mnlite-0.1.0/src/mnlite/cli.py +246 -0
  16. mnlite-0.1.0/src/mnlite/data.py +522 -0
  17. mnlite-0.1.0/src/mnlite/datasets.py +156 -0
  18. mnlite-0.1.0/src/mnlite/estimation.py +658 -0
  19. mnlite-0.1.0/src/mnlite/models.py +332 -0
  20. mnlite-0.1.0/src/mnlite/optimize.py +173 -0
  21. mnlite-0.1.0/src/mnlite/py.typed +0 -0
  22. mnlite-0.1.0/src/mnlite/spec.py +280 -0
  23. mnlite-0.1.0/src/mnlite.egg-info/PKG-INFO +271 -0
  24. mnlite-0.1.0/src/mnlite.egg-info/SOURCES.txt +34 -0
  25. mnlite-0.1.0/src/mnlite.egg-info/dependency_links.txt +1 -0
  26. mnlite-0.1.0/src/mnlite.egg-info/entry_points.txt +2 -0
  27. mnlite-0.1.0/src/mnlite.egg-info/requires.txt +13 -0
  28. mnlite-0.1.0/src/mnlite.egg-info/top_level.txt +1 -0
  29. mnlite-0.1.0/tests/conftest.py +73 -0
  30. mnlite-0.1.0/tests/test_cli.py +149 -0
  31. mnlite-0.1.0/tests/test_data.py +190 -0
  32. mnlite-0.1.0/tests/test_mnl.py +209 -0
  33. mnlite-0.1.0/tests/test_nested.py +119 -0
  34. mnlite-0.1.0/tests/test_optimize.py +79 -0
  35. mnlite-0.1.0/tests/test_post.py +177 -0
  36. mnlite-0.1.0/tests/test_stats_spec.py +153 -0
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - Formula-style `Spec`: one linear-in-parameters utility per alternative, generic and
10
+ alternative-specific coefficients, constants, interactions, fixed parameters, nests.
11
+ - `ChoiceData` container with availability and weights; wide and long conversion;
12
+ stdlib CSV reader/writer; pandas DataFrames accepted (optional).
13
+ - Multinomial logit with analytic score and Hessian; Newton-Raphson with step halving,
14
+ BFGS fallback, optional scipy backend.
15
+ - Nested logit (one level, FIML) with analytic gradient and numerical Hessian.
16
+ - Classical and robust (sandwich) standard errors, t-statistics, p-values, confidence
17
+ intervals, LL(0), LL(C), rho-squared, adjusted rho-squared, AIC, BIC, likelihood
18
+ ratio tests, Wald test of nest parameters against 1, model comparison table.
19
+ - Direct and cross elasticities (disaggregate and probability-weighted aggregate).
20
+ - Parameter ratios and value of travel time with delta-method standard errors.
21
+ - Prediction, market shares and scenario simulation (attribute changes, availability).
22
+ - Validation data: Ben-Akiva & Lerman (1985) 21-person car/transit example with
23
+ published estimates; synthetic car/bus/rail generator with known parameters.
24
+ - `mnlite` command line tool: `estimate`, `synth`, `convert`, `example`.
@@ -0,0 +1,21 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "mnlite"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://gitlab.com/geophilo1/mnlite_sdk"
13
+ url: "https://pypi.org/project/mnlite/"
14
+ abstract: "Lightweight discrete choice modelling for travel mode choice in numpy: multinomial and nested logit estimated by maximum likelihood with analytic gradients, classical and robust standard errors, rho-squared, AIC/BIC, likelihood ratio tests, elasticities, value of travel time with delta-method standard errors and market-share scenario simulation."
15
+ keywords:
16
+ - discrete choice
17
+ - multinomial logit
18
+ - nested logit
19
+ - mode choice
20
+ - travel demand
21
+ - value of time
mnlite-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/mnlite/py.typed
7
+ recursive-include src/mnlite *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.csv
mnlite-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,271 @@
1
+ Metadata-Version: 2.4
2
+ Name: mnlite
3
+ Version: 0.1.0
4
+ Summary: Lightweight discrete choice modelling for travel mode choice in numpy: multinomial and nested logit by maximum likelihood, robust standard errors, rho-squared, LR tests, elasticities, value of travel time and scenario simulation.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/geophilo1/mnlite_sdk
8
+ Project-URL: Repository, https://gitlab.com/geophilo1/mnlite_sdk
9
+ Project-URL: Issues, https://gitlab.com/geophilo1/mnlite_sdk/-/work_items
10
+ Project-URL: Changelog, https://gitlab.com/geophilo1/mnlite_sdk/-/blob/main/CHANGELOG.md
11
+ Keywords: discrete-choice,multinomial-logit,nested-logit,mode-choice,travel-demand,transport-planning,maximum-likelihood,elasticity,value-of-time,econometrics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Education
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.21
30
+ Provides-Extra: pandas
31
+ Requires-Dist: pandas>=1.3.0; extra == "pandas"
32
+ Provides-Extra: scipy
33
+ Requires-Dist: scipy>=1.7; extra == "scipy"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
36
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # mnlite
42
+
43
+ [![PyPI version](https://img.shields.io/pypi/v/mnlite.svg)](https://pypi.org/project/mnlite/)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/mnlite.svg)](https://pypi.org/project/mnlite/)
45
+ [![pipeline](https://gitlab.com/geophilo1/mnlite_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/mnlite_sdk/-/pipelines)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://gitlab.com/geophilo1/mnlite_sdk/-/blob/main/LICENSE)
47
+
48
+ **Lightweight discrete choice modelling for travel mode choice, in numpy.**
49
+
50
+ `mnlite` estimates multinomial logit (MNL) and one-level nested logit (NL) models
51
+ by maximum likelihood and gives you what a mode choice study needs afterwards:
52
+ standard errors (classical and robust), rho-squared, likelihood ratio tests,
53
+ elasticities, the value of travel time with a delta-method standard error, and
54
+ market-share scenarios. It depends on numpy only; scipy and pandas are optional.
55
+
56
+ It is meant for teaching, quick studies and pipelines where a full econometrics
57
+ package is too heavy. For mixed logit, latent classes, panel data or large
58
+ production models use a fuller package: [Biogeme](https://biogeme.epfl.ch/),
59
+ [PyLogit](https://github.com/timothyb0912/pylogit),
60
+ [xlogit](https://github.com/arteagac/xlogit) (GPU mixed logit) or
61
+ [Apollo](http://www.apollochoicemodelling.com/) in R.
62
+
63
+ ```bash
64
+ pip install mnlite # numpy only
65
+ pip install "mnlite[pandas]" # + DataFrame export
66
+ pip install "mnlite[scipy]" # + scipy optimiser backend
67
+ ```
68
+
69
+ Requires Python 3.9 or newer.
70
+
71
+ ## Quickstart
72
+
73
+ ```python
74
+ import mnlite
75
+ from mnlite import Spec, estimate
76
+
77
+ # Ben-Akiva & Lerman (1985, p. 88): 21 travellers, car vs transit.
78
+ data = mnlite.ben_akiva_lerman()
79
+ res = estimate({"car": "b_time*time", "transit": "asc_transit + b_time*time"}, data)
80
+ print(res.summary())
81
+ # asc_transit 0.2376 (s.e. 0.7505), b_time -0.0531 (s.e. 0.0206),
82
+ # LL -6.166, rho2 0.576, adjusted rho2 0.439 -> identical to the published values
83
+
84
+ # Three modes, availability, generic and alternative-specific coefficients.
85
+ modes = mnlite.synthetic_mode_choice(n=3000) # known true parameters
86
+ spec = Spec({
87
+ "car": "b_time*time + b_cost*cost + b_inc_car*income",
88
+ "bus": "asc_bus + b_time*time + b_cost*cost",
89
+ "rail": "asc_rail + b_time*time + b_cost*cost",
90
+ })
91
+ mnl = estimate(spec, modes)
92
+ print(mnl.vtt("b_time", "b_cost", scale=60)) # currency per hour, delta-method s.e.
93
+ print(mnl.elasticities(modes, "cost", "car")) # direct (car) and cross elasticities
94
+ print(mnl.simulate(modes, {"cost_bus": "*1.2", "time_rail": "-5"}).table())
95
+
96
+ # Nested logit: bus and rail in a public transport nest (FIML).
97
+ nl = estimate(Spec(spec.utilities, nests={"pt": ["bus", "rail"]}), modes)
98
+ print(mnlite.lr_test(mnl, nl))
99
+ print(mnlite.compare([mnl, nl], ["MNL", "NL"]))
100
+ ```
101
+
102
+ See `examples/quickstart.py` for a complete script.
103
+
104
+ ## Data
105
+
106
+ `ChoiceData` holds one row per choice observation (wide form). Alternative-specific
107
+ attributes are columns named `{var}_{alt}` (`time_car`, `time_bus`); other columns
108
+ (`income`) are individual-specific. The utility term `b_time*time` for alternative
109
+ `bus` uses column `time_bus` if it exists, otherwise `time`.
110
+
111
+ ```python
112
+ d = mnlite.read_csv("trips.csv", alternatives=["car", "bus", "rail"],
113
+ choice="mode", avail="{alt}_av", weights="w") # wide
114
+ d = mnlite.read_csv("trips_long.csv", form="long", id_col="id", alt_col="alt",
115
+ choice_col="chosen", avail_col="av") # long
116
+ d = mnlite.ChoiceData.from_wide(df, ["car", "bus", "rail"]) # pandas
117
+ d.to_long(); d.to_wide(); d.write_csv("out.csv", "long"); d.to_pandas()
118
+ ```
119
+
120
+ In long form, missing rows mean "unavailable"; a variable that is constant across an
121
+ observation's alternatives is also kept as an individual-specific column.
122
+
123
+ ## Specification mini-language
124
+
125
+ | Written | Meaning |
126
+ |---|---|
127
+ | `asc_bus` | alternative-specific constant |
128
+ | `b_time*time` | parameter times variable; the **first** name is always the parameter |
129
+ | `b_inc_rail*income` | individual-specific variable (needs an alternative-specific parameter) |
130
+ | `b*time*income` | interaction (product of variables) |
131
+ | `b_cost*cost*0.01` | numeric scaling |
132
+ | `- b*x` | negative sign |
133
+ | `0` or `""` | utility fixed at zero (reference alternative) |
134
+
135
+ A parameter name used in several utilities is **generic**; used once it is
136
+ **alternative-specific**. `Spec(..., fixed={"asc_rail": 0})` holds a parameter fixed;
137
+ `nests={"pt": ["bus", "rail"]}` adds the nest parameter `lambda_pt`.
138
+ `Spec.to_dict()` / `Spec.from_dict()` round-trip through JSON.
139
+
140
+ ## API overview
141
+
142
+ | Object | Purpose |
143
+ |---|---|
144
+ | `Spec(utilities, nests=None, fixed=None, column_pattern="{var}_{alt}")` | model specification |
145
+ | `ChoiceData(columns, alternatives, choice, avail=None, weights=None)` | data; `.from_wide`, `.from_long`, `.to_long`, `.to_wide`, `.write_csv`, `.to_pandas` |
146
+ | `read_csv(path, form="wide" or "long", **kw)` | CSV reader (stdlib `csv`) |
147
+ | `estimate(spec, data, start=None, method="auto")` | MLE; returns `EstimationResults` |
148
+ | `res.params`, `.std_errors(robust=)`, `.tstats()`, `.pvalues()`, `.conf_int()` | inference |
149
+ | `res.loglik`, `.loglik_null`, `.loglik_constants`, `.rho2`, `.rho2_bar`, `.aic`, `.bic` | fit |
150
+ | `res.lr_test_null()`, `lr_test(restricted, unrestricted)`, `res.wald_test(name, value)` | tests |
151
+ | `res.ratio(num, den, scale)`, `res.vtt(time, cost, scale=60)` | ratios, VTT with delta-method s.e. |
152
+ | `res.elasticities(data, var, alt, aggregate=True)` | direct / cross point elasticities |
153
+ | `res.predict(data)`, `.market_shares(data)`, `.simulate(data, changes, avail)` | prediction and scenarios |
154
+ | `res.summary()`, `.to_dict()`, `.to_json()`, `compare([...])` | reporting |
155
+ | `ben_akiva_lerman()`, `synthetic_mode_choice(n, seed, nested)` | validation data |
156
+
157
+ ### Command line
158
+
159
+ ```bash
160
+ mnlite example # reproduces Ben-Akiva & Lerman (1985)
161
+ mnlite synth trips.csv --n 2000 [--nested] [--long]
162
+ mnlite estimate trips.csv --avail "{alt}_av" \
163
+ -u "car=b_time*time + b_cost*cost + b_inc_car*income" \
164
+ -u "bus=asc_bus + b_time*time + b_cost*cost" \
165
+ -u "rail=asc_rail + b_time*time + b_cost*cost" \
166
+ --nest pt:bus,rail --ratio "b_time/b_cost*60" \
167
+ --elasticity cost:car --scenario "cost_bus*1.2" --json results.json
168
+ mnlite estimate trips.csv --spec spec.json # {"utilities": {...}, "nests": {...}, "fixed": {...}}
169
+ mnlite convert trips_long.csv trips_wide.csv --long --avail avail --to wide
170
+ ```
171
+
172
+ Scenario changes: `COL*1.2`, `COL+5`, `COL-5`, `COL/2`, `COL=0`.
173
+
174
+ ## Method and sources
175
+
176
+ * **MNL.** `P_ni = a_ni exp(V_ni) / sum_j a_nj exp(V_nj)` with availability `a_nj`
177
+ (McFadden 1974; Train 2009, ch. 3). The weighted log-likelihood
178
+ `sum_n w_n ln P_n,i(n)` is globally concave; its score
179
+ `x_ni - sum_j P_nj x_nj` and Hessian `-sum_n w_n sum_j P_nj (x_nj - x̄_n)(x_nj - x̄_n)'`
180
+ are analytic (Train 2009, sec. 3.7). Newton-Raphson with step halving, stopping on the
181
+ scaled gradient `g' H^-1 g` (Train 2009, sec. 8.3-8.4); BFGS (Nocedal & Wright 2006)
182
+ when the Hessian is not negative definite; `method="scipy"` uses
183
+ `scipy.optimize.minimize(method="BFGS")`.
184
+ * **Nested logit**, one level, normalised at the top (the random-utility-consistent form
185
+ of Koppelman & Bhat 2006 and Train 2009, sec. 4.2):
186
+ `P_ni = exp(V_ni/λ_m) / exp(I_m) · exp(λ_m I_m) / sum_l exp(λ_l I_l)`,
187
+ `I_m = ln sum_{j in m} a_nj exp(V_nj/λ_m)`. Estimation is **FIML** (all parameters
188
+ jointly, started from the MNL estimates), not the sequential two-step method. The
189
+ gradient is analytic; the Hessian for standard errors is a central difference of the
190
+ analytic gradient. `0 < λ ≤ 1` is required for consistency with utility
191
+ maximisation (McFadden 1978); the summary flags estimates outside that range and
192
+ reports a t-test against 1. Single-alternative nests have λ fixed at 1.
193
+ * **Inference.** Covariance `(-H)^-1`; robust sandwich `H^-1 (sum w_n² s_n s_n') H^-1`
194
+ (White 1982). With non-unit weights (e.g. WESML for choice-based samples, Manski &
195
+ Lerman 1977) use the robust standard errors. p-values use the normal approximation.
196
+ * **Fit.** `rho2 = 1 - LL(β)/LL(0)`, `rho2_bar = 1 - (LL(β) - K)/LL(0)` (Ben-Akiva &
197
+ Lerman 1985, sec. 7.4); `LL(0)` gives equal probability to the available alternatives;
198
+ `LL(C)` is from a constants-only MNL. `AIC = 2K - 2LL` (Akaike 1974),
199
+ `BIC = K ln N - 2LL` (Schwarz 1978). LR test `-2(LL_r - LL_u) ~ χ²(K_u - K_r)`; the
200
+ χ² tail uses the regularised incomplete gamma function (Press et al. 2007, sec. 6.2),
201
+ so scipy is not needed.
202
+ * **Elasticities.** Point elasticities of `P_nj` with respect to a data column, by central
203
+ difference of the model probabilities; this equals `β x_ni (1 - P_ni)` (direct) and
204
+ `-β x_ni P_ni` (cross) for MNL and the nested-logit expressions with the
205
+ `(1/λ - 1)` terms, both checked in the tests. Aggregate elasticities are
206
+ probability-weighted, `Σ w_n P_nj E_nj / Σ w_n P_nj`: the elasticity of the predicted
207
+ share (Ben-Akiva & Lerman 1985, sec. 5.5).
208
+ * **Value of travel time** `scale · β_time / β_cost`, with the delta-method variance
209
+ `∇g' Σ ∇g`, `∇g = scale · (1/β_c, -β_t/β_c²)` (e.g. Daly, Hess & de Jong 2012, who also
210
+ discuss the alternatives to the delta method).
211
+ * **Scenarios** use sample enumeration: probabilities recomputed for every observation
212
+ with the changed attributes or availability, then averaged with the weights.
213
+
214
+ ### Validation
215
+
216
+ * `ben_akiva_lerman()` is the 21-person car/transit example of Ben-Akiva & Lerman (1985,
217
+ p. 88), as reproduced in the EPFL Transport and Mobility Laboratory course slides
218
+ (binary choice lecture). mnlite reproduces the published constant 0.2376 (s.e. 0.7505),
219
+ time coefficient -0.0531 (s.e. 0.0206), LL(β) = -6.166, LL(0) = -14.556,
220
+ ρ² = 0.576 and adjusted ρ² = 0.439 (`tests/test_mnl.py`).
221
+ * `synthetic_mode_choice()` simulates car/bus/rail choices (with rail availability) from
222
+ known parameters, by MNL or nested logit; the tests check that the estimates recover
223
+ them within their standard errors, that the analytic gradients match finite
224
+ differences, that NL with λ = 1 equals MNL, and that the scipy backend agrees.
225
+
226
+ ### References
227
+
228
+ * Akaike, H. (1974). A new look at the statistical model identification. *IEEE Transactions on Automatic Control*, 19(6), 716-723.
229
+ * Ben-Akiva, M., & Lerman, S. R. (1985). *Discrete Choice Analysis: Theory and Application to Travel Demand*. MIT Press.
230
+ * Daly, A., Hess, S., & de Jong, G. (2012). Calculating errors for measures derived from choice modelling estimates. *Transportation Research Part B*, 46(2), 333-341.
231
+ * Koppelman, F. S., & Bhat, C. (2006). *A Self Instructing Course in Mode Choice Modeling: Multinomial and Nested Logit Models*. US DOT, Federal Transit Administration.
232
+ * Manski, C. F., & Lerman, S. R. (1977). The estimation of choice probabilities from choice based samples. *Econometrica*, 45(8), 1977-1988.
233
+ * McFadden, D. (1974). Conditional logit analysis of qualitative choice behavior. In P. Zarembka (Ed.), *Frontiers in Econometrics*. Academic Press.
234
+ * McFadden, D. (1978). Modelling the choice of residential location. In A. Karlqvist et al. (Eds.), *Spatial Interaction Theory and Planning Models*. North-Holland.
235
+ * Nocedal, J., & Wright, S. J. (2006). *Numerical Optimization* (2nd ed.). Springer.
236
+ * Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007). *Numerical Recipes* (3rd ed.). Cambridge University Press.
237
+ * Schwarz, G. (1978). Estimating the dimension of a model. *Annals of Statistics*, 6(2), 461-464.
238
+ * Train, K. E. (2009). *Discrete Choice Methods with Simulation* (2nd ed.). Cambridge University Press.
239
+ * White, H. (1982). Maximum likelihood estimation of misspecified models. *Econometrica*, 50(1), 1-25.
240
+
241
+ ## Limitations
242
+
243
+ * Utilities must be linear in parameters (transform variables beforehand, e.g. log cost).
244
+ * One nesting level only; no cross-nesting, mixed logit, latent class or panel
245
+ (repeated choice) correlation. Use Biogeme, PyLogit, xlogit or Apollo for those.
246
+ * The nested logit Hessian is numerical (finite differences of the analytic gradient),
247
+ so its standard errors are accurate to a few significant digits rather than to
248
+ machine precision.
249
+ * Elasticities are with respect to a data column: if the same column enters several
250
+ utilities (for example `income`), all of them change together.
251
+ * The ratio/VTT confidence interval is the symmetric delta-method interval; for ratios
252
+ with an imprecise denominator a simulation or Fieller interval may be more suitable
253
+ (Daly, Hess & de Jong 2012).
254
+ * Designed for datasets that fit in memory as an `(observations × alternatives ×
255
+ parameters)` array; tens of thousands of observations are fine.
256
+
257
+ ## Academic citation
258
+
259
+ ```bibtex
260
+ @software{eminoglu_mnlite,
261
+ author = {Eminoğlu, Yusuf},
262
+ title = {mnlite},
263
+ year = {2026},
264
+ version = {0.1.0},
265
+ url = {https://gitlab.com/geophilo1/mnlite_sdk}
266
+ }
267
+ ```
268
+
269
+ ## License
270
+
271
+ MIT License, Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
mnlite-0.1.0/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # mnlite
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/mnlite.svg)](https://pypi.org/project/mnlite/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/mnlite.svg)](https://pypi.org/project/mnlite/)
5
+ [![pipeline](https://gitlab.com/geophilo1/mnlite_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/mnlite_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://gitlab.com/geophilo1/mnlite_sdk/-/blob/main/LICENSE)
7
+
8
+ **Lightweight discrete choice modelling for travel mode choice, in numpy.**
9
+
10
+ `mnlite` estimates multinomial logit (MNL) and one-level nested logit (NL) models
11
+ by maximum likelihood and gives you what a mode choice study needs afterwards:
12
+ standard errors (classical and robust), rho-squared, likelihood ratio tests,
13
+ elasticities, the value of travel time with a delta-method standard error, and
14
+ market-share scenarios. It depends on numpy only; scipy and pandas are optional.
15
+
16
+ It is meant for teaching, quick studies and pipelines where a full econometrics
17
+ package is too heavy. For mixed logit, latent classes, panel data or large
18
+ production models use a fuller package: [Biogeme](https://biogeme.epfl.ch/),
19
+ [PyLogit](https://github.com/timothyb0912/pylogit),
20
+ [xlogit](https://github.com/arteagac/xlogit) (GPU mixed logit) or
21
+ [Apollo](http://www.apollochoicemodelling.com/) in R.
22
+
23
+ ```bash
24
+ pip install mnlite # numpy only
25
+ pip install "mnlite[pandas]" # + DataFrame export
26
+ pip install "mnlite[scipy]" # + scipy optimiser backend
27
+ ```
28
+
29
+ Requires Python 3.9 or newer.
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ import mnlite
35
+ from mnlite import Spec, estimate
36
+
37
+ # Ben-Akiva & Lerman (1985, p. 88): 21 travellers, car vs transit.
38
+ data = mnlite.ben_akiva_lerman()
39
+ res = estimate({"car": "b_time*time", "transit": "asc_transit + b_time*time"}, data)
40
+ print(res.summary())
41
+ # asc_transit 0.2376 (s.e. 0.7505), b_time -0.0531 (s.e. 0.0206),
42
+ # LL -6.166, rho2 0.576, adjusted rho2 0.439 -> identical to the published values
43
+
44
+ # Three modes, availability, generic and alternative-specific coefficients.
45
+ modes = mnlite.synthetic_mode_choice(n=3000) # known true parameters
46
+ spec = Spec({
47
+ "car": "b_time*time + b_cost*cost + b_inc_car*income",
48
+ "bus": "asc_bus + b_time*time + b_cost*cost",
49
+ "rail": "asc_rail + b_time*time + b_cost*cost",
50
+ })
51
+ mnl = estimate(spec, modes)
52
+ print(mnl.vtt("b_time", "b_cost", scale=60)) # currency per hour, delta-method s.e.
53
+ print(mnl.elasticities(modes, "cost", "car")) # direct (car) and cross elasticities
54
+ print(mnl.simulate(modes, {"cost_bus": "*1.2", "time_rail": "-5"}).table())
55
+
56
+ # Nested logit: bus and rail in a public transport nest (FIML).
57
+ nl = estimate(Spec(spec.utilities, nests={"pt": ["bus", "rail"]}), modes)
58
+ print(mnlite.lr_test(mnl, nl))
59
+ print(mnlite.compare([mnl, nl], ["MNL", "NL"]))
60
+ ```
61
+
62
+ See `examples/quickstart.py` for a complete script.
63
+
64
+ ## Data
65
+
66
+ `ChoiceData` holds one row per choice observation (wide form). Alternative-specific
67
+ attributes are columns named `{var}_{alt}` (`time_car`, `time_bus`); other columns
68
+ (`income`) are individual-specific. The utility term `b_time*time` for alternative
69
+ `bus` uses column `time_bus` if it exists, otherwise `time`.
70
+
71
+ ```python
72
+ d = mnlite.read_csv("trips.csv", alternatives=["car", "bus", "rail"],
73
+ choice="mode", avail="{alt}_av", weights="w") # wide
74
+ d = mnlite.read_csv("trips_long.csv", form="long", id_col="id", alt_col="alt",
75
+ choice_col="chosen", avail_col="av") # long
76
+ d = mnlite.ChoiceData.from_wide(df, ["car", "bus", "rail"]) # pandas
77
+ d.to_long(); d.to_wide(); d.write_csv("out.csv", "long"); d.to_pandas()
78
+ ```
79
+
80
+ In long form, missing rows mean "unavailable"; a variable that is constant across an
81
+ observation's alternatives is also kept as an individual-specific column.
82
+
83
+ ## Specification mini-language
84
+
85
+ | Written | Meaning |
86
+ |---|---|
87
+ | `asc_bus` | alternative-specific constant |
88
+ | `b_time*time` | parameter times variable; the **first** name is always the parameter |
89
+ | `b_inc_rail*income` | individual-specific variable (needs an alternative-specific parameter) |
90
+ | `b*time*income` | interaction (product of variables) |
91
+ | `b_cost*cost*0.01` | numeric scaling |
92
+ | `- b*x` | negative sign |
93
+ | `0` or `""` | utility fixed at zero (reference alternative) |
94
+
95
+ A parameter name used in several utilities is **generic**; used once it is
96
+ **alternative-specific**. `Spec(..., fixed={"asc_rail": 0})` holds a parameter fixed;
97
+ `nests={"pt": ["bus", "rail"]}` adds the nest parameter `lambda_pt`.
98
+ `Spec.to_dict()` / `Spec.from_dict()` round-trip through JSON.
99
+
100
+ ## API overview
101
+
102
+ | Object | Purpose |
103
+ |---|---|
104
+ | `Spec(utilities, nests=None, fixed=None, column_pattern="{var}_{alt}")` | model specification |
105
+ | `ChoiceData(columns, alternatives, choice, avail=None, weights=None)` | data; `.from_wide`, `.from_long`, `.to_long`, `.to_wide`, `.write_csv`, `.to_pandas` |
106
+ | `read_csv(path, form="wide" or "long", **kw)` | CSV reader (stdlib `csv`) |
107
+ | `estimate(spec, data, start=None, method="auto")` | MLE; returns `EstimationResults` |
108
+ | `res.params`, `.std_errors(robust=)`, `.tstats()`, `.pvalues()`, `.conf_int()` | inference |
109
+ | `res.loglik`, `.loglik_null`, `.loglik_constants`, `.rho2`, `.rho2_bar`, `.aic`, `.bic` | fit |
110
+ | `res.lr_test_null()`, `lr_test(restricted, unrestricted)`, `res.wald_test(name, value)` | tests |
111
+ | `res.ratio(num, den, scale)`, `res.vtt(time, cost, scale=60)` | ratios, VTT with delta-method s.e. |
112
+ | `res.elasticities(data, var, alt, aggregate=True)` | direct / cross point elasticities |
113
+ | `res.predict(data)`, `.market_shares(data)`, `.simulate(data, changes, avail)` | prediction and scenarios |
114
+ | `res.summary()`, `.to_dict()`, `.to_json()`, `compare([...])` | reporting |
115
+ | `ben_akiva_lerman()`, `synthetic_mode_choice(n, seed, nested)` | validation data |
116
+
117
+ ### Command line
118
+
119
+ ```bash
120
+ mnlite example # reproduces Ben-Akiva & Lerman (1985)
121
+ mnlite synth trips.csv --n 2000 [--nested] [--long]
122
+ mnlite estimate trips.csv --avail "{alt}_av" \
123
+ -u "car=b_time*time + b_cost*cost + b_inc_car*income" \
124
+ -u "bus=asc_bus + b_time*time + b_cost*cost" \
125
+ -u "rail=asc_rail + b_time*time + b_cost*cost" \
126
+ --nest pt:bus,rail --ratio "b_time/b_cost*60" \
127
+ --elasticity cost:car --scenario "cost_bus*1.2" --json results.json
128
+ mnlite estimate trips.csv --spec spec.json # {"utilities": {...}, "nests": {...}, "fixed": {...}}
129
+ mnlite convert trips_long.csv trips_wide.csv --long --avail avail --to wide
130
+ ```
131
+
132
+ Scenario changes: `COL*1.2`, `COL+5`, `COL-5`, `COL/2`, `COL=0`.
133
+
134
+ ## Method and sources
135
+
136
+ * **MNL.** `P_ni = a_ni exp(V_ni) / sum_j a_nj exp(V_nj)` with availability `a_nj`
137
+ (McFadden 1974; Train 2009, ch. 3). The weighted log-likelihood
138
+ `sum_n w_n ln P_n,i(n)` is globally concave; its score
139
+ `x_ni - sum_j P_nj x_nj` and Hessian `-sum_n w_n sum_j P_nj (x_nj - x̄_n)(x_nj - x̄_n)'`
140
+ are analytic (Train 2009, sec. 3.7). Newton-Raphson with step halving, stopping on the
141
+ scaled gradient `g' H^-1 g` (Train 2009, sec. 8.3-8.4); BFGS (Nocedal & Wright 2006)
142
+ when the Hessian is not negative definite; `method="scipy"` uses
143
+ `scipy.optimize.minimize(method="BFGS")`.
144
+ * **Nested logit**, one level, normalised at the top (the random-utility-consistent form
145
+ of Koppelman & Bhat 2006 and Train 2009, sec. 4.2):
146
+ `P_ni = exp(V_ni/λ_m) / exp(I_m) · exp(λ_m I_m) / sum_l exp(λ_l I_l)`,
147
+ `I_m = ln sum_{j in m} a_nj exp(V_nj/λ_m)`. Estimation is **FIML** (all parameters
148
+ jointly, started from the MNL estimates), not the sequential two-step method. The
149
+ gradient is analytic; the Hessian for standard errors is a central difference of the
150
+ analytic gradient. `0 < λ ≤ 1` is required for consistency with utility
151
+ maximisation (McFadden 1978); the summary flags estimates outside that range and
152
+ reports a t-test against 1. Single-alternative nests have λ fixed at 1.
153
+ * **Inference.** Covariance `(-H)^-1`; robust sandwich `H^-1 (sum w_n² s_n s_n') H^-1`
154
+ (White 1982). With non-unit weights (e.g. WESML for choice-based samples, Manski &
155
+ Lerman 1977) use the robust standard errors. p-values use the normal approximation.
156
+ * **Fit.** `rho2 = 1 - LL(β)/LL(0)`, `rho2_bar = 1 - (LL(β) - K)/LL(0)` (Ben-Akiva &
157
+ Lerman 1985, sec. 7.4); `LL(0)` gives equal probability to the available alternatives;
158
+ `LL(C)` is from a constants-only MNL. `AIC = 2K - 2LL` (Akaike 1974),
159
+ `BIC = K ln N - 2LL` (Schwarz 1978). LR test `-2(LL_r - LL_u) ~ χ²(K_u - K_r)`; the
160
+ χ² tail uses the regularised incomplete gamma function (Press et al. 2007, sec. 6.2),
161
+ so scipy is not needed.
162
+ * **Elasticities.** Point elasticities of `P_nj` with respect to a data column, by central
163
+ difference of the model probabilities; this equals `β x_ni (1 - P_ni)` (direct) and
164
+ `-β x_ni P_ni` (cross) for MNL and the nested-logit expressions with the
165
+ `(1/λ - 1)` terms, both checked in the tests. Aggregate elasticities are
166
+ probability-weighted, `Σ w_n P_nj E_nj / Σ w_n P_nj`: the elasticity of the predicted
167
+ share (Ben-Akiva & Lerman 1985, sec. 5.5).
168
+ * **Value of travel time** `scale · β_time / β_cost`, with the delta-method variance
169
+ `∇g' Σ ∇g`, `∇g = scale · (1/β_c, -β_t/β_c²)` (e.g. Daly, Hess & de Jong 2012, who also
170
+ discuss the alternatives to the delta method).
171
+ * **Scenarios** use sample enumeration: probabilities recomputed for every observation
172
+ with the changed attributes or availability, then averaged with the weights.
173
+
174
+ ### Validation
175
+
176
+ * `ben_akiva_lerman()` is the 21-person car/transit example of Ben-Akiva & Lerman (1985,
177
+ p. 88), as reproduced in the EPFL Transport and Mobility Laboratory course slides
178
+ (binary choice lecture). mnlite reproduces the published constant 0.2376 (s.e. 0.7505),
179
+ time coefficient -0.0531 (s.e. 0.0206), LL(β) = -6.166, LL(0) = -14.556,
180
+ ρ² = 0.576 and adjusted ρ² = 0.439 (`tests/test_mnl.py`).
181
+ * `synthetic_mode_choice()` simulates car/bus/rail choices (with rail availability) from
182
+ known parameters, by MNL or nested logit; the tests check that the estimates recover
183
+ them within their standard errors, that the analytic gradients match finite
184
+ differences, that NL with λ = 1 equals MNL, and that the scipy backend agrees.
185
+
186
+ ### References
187
+
188
+ * Akaike, H. (1974). A new look at the statistical model identification. *IEEE Transactions on Automatic Control*, 19(6), 716-723.
189
+ * Ben-Akiva, M., & Lerman, S. R. (1985). *Discrete Choice Analysis: Theory and Application to Travel Demand*. MIT Press.
190
+ * Daly, A., Hess, S., & de Jong, G. (2012). Calculating errors for measures derived from choice modelling estimates. *Transportation Research Part B*, 46(2), 333-341.
191
+ * Koppelman, F. S., & Bhat, C. (2006). *A Self Instructing Course in Mode Choice Modeling: Multinomial and Nested Logit Models*. US DOT, Federal Transit Administration.
192
+ * Manski, C. F., & Lerman, S. R. (1977). The estimation of choice probabilities from choice based samples. *Econometrica*, 45(8), 1977-1988.
193
+ * McFadden, D. (1974). Conditional logit analysis of qualitative choice behavior. In P. Zarembka (Ed.), *Frontiers in Econometrics*. Academic Press.
194
+ * McFadden, D. (1978). Modelling the choice of residential location. In A. Karlqvist et al. (Eds.), *Spatial Interaction Theory and Planning Models*. North-Holland.
195
+ * Nocedal, J., & Wright, S. J. (2006). *Numerical Optimization* (2nd ed.). Springer.
196
+ * Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007). *Numerical Recipes* (3rd ed.). Cambridge University Press.
197
+ * Schwarz, G. (1978). Estimating the dimension of a model. *Annals of Statistics*, 6(2), 461-464.
198
+ * Train, K. E. (2009). *Discrete Choice Methods with Simulation* (2nd ed.). Cambridge University Press.
199
+ * White, H. (1982). Maximum likelihood estimation of misspecified models. *Econometrica*, 50(1), 1-25.
200
+
201
+ ## Limitations
202
+
203
+ * Utilities must be linear in parameters (transform variables beforehand, e.g. log cost).
204
+ * One nesting level only; no cross-nesting, mixed logit, latent class or panel
205
+ (repeated choice) correlation. Use Biogeme, PyLogit, xlogit or Apollo for those.
206
+ * The nested logit Hessian is numerical (finite differences of the analytic gradient),
207
+ so its standard errors are accurate to a few significant digits rather than to
208
+ machine precision.
209
+ * Elasticities are with respect to a data column: if the same column enters several
210
+ utilities (for example `income`), all of them change together.
211
+ * The ratio/VTT confidence interval is the symmetric delta-method interval; for ratios
212
+ with an imprecise denominator a simulation or Fieller interval may be more suitable
213
+ (Daly, Hess & de Jong 2012).
214
+ * Designed for datasets that fit in memory as an `(observations × alternatives ×
215
+ parameters)` array; tens of thousands of observations are fine.
216
+
217
+ ## Academic citation
218
+
219
+ ```bibtex
220
+ @software{eminoglu_mnlite,
221
+ author = {Eminoğlu, Yusuf},
222
+ title = {mnlite},
223
+ year = {2026},
224
+ version = {0.1.0},
225
+ url = {https://gitlab.com/geophilo1/mnlite_sdk}
226
+ }
227
+ ```
228
+
229
+ ## License
230
+
231
+ MIT License, Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,22 @@
1
+ id,choice,time_car,time_transit
2
+ 1,transit,52.9,4.4
3
+ 2,transit,4.1,28.5
4
+ 3,car,4.1,86.9
5
+ 4,transit,56.2,31.6
6
+ 5,transit,51.8,20.2
7
+ 6,car,0.2,91.2
8
+ 7,car,27.6,79.7
9
+ 8,transit,89.9,2.2
10
+ 9,transit,41.5,24.5
11
+ 10,transit,95,43.5
12
+ 11,transit,99.1,8.4
13
+ 12,car,18.5,84
14
+ 13,car,82,38
15
+ 14,transit,8.6,1.6
16
+ 15,car,22.5,74.1
17
+ 16,car,51.4,83.8
18
+ 17,transit,81,19.2
19
+ 18,car,51,85
20
+ 19,car,62.2,90.1
21
+ 20,transit,95.1,22.2
22
+ 21,car,41.6,91.5
@@ -0,0 +1,48 @@
1
+ """mnlite quickstart: validation example, MNL, nested logit, VTT, elasticities, scenarios.
2
+
3
+ Run with ``python examples/quickstart.py``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import mnlite
11
+ from mnlite import Spec, estimate, lr_test
12
+
13
+ HERE = Path(__file__).resolve().parent
14
+
15
+ # 1. Published textbook example (Ben-Akiva & Lerman 1985, p. 88), read from CSV.
16
+ data = mnlite.read_csv(HERE / "bal_binary.csv", alternatives=["car", "transit"])
17
+ res = estimate({"car": "b_time*time", "transit": "asc_transit + b_time*time"}, data)
18
+ print(res.summary())
19
+ print("published: asc_transit 0.2376 (0.7505), b_time -0.0531 (0.0206), LL -6.166\n")
20
+
21
+ # 2. Three-mode synthetic sample with known true parameters.
22
+ modes = mnlite.synthetic_mode_choice(n=3000, seed=1)
23
+ spec = Spec(
24
+ {
25
+ "car": "b_time*time + b_cost*cost + b_inc_car*income",
26
+ "bus": "asc_bus + b_time*time + b_cost*cost",
27
+ "rail": "asc_rail + b_time*time + b_cost*cost",
28
+ }
29
+ )
30
+ mnl = estimate(spec, modes)
31
+ print(mnl.summary())
32
+ print("true values:", mnlite.datasets.SYNTHETIC_TRUE_PARAMS)
33
+
34
+ # Value of travel time (time in minutes, cost in currency) per hour, delta-method s.e.
35
+ print(mnl.vtt("b_time", "b_cost", scale=60))
36
+
37
+ # Aggregate direct and cross elasticities with respect to car cost.
38
+ print("elasticities w.r.t. cost_car:", mnl.elasticities(modes, "cost", "car"))
39
+
40
+ # 3. Nested logit (bus and rail in one public transport nest), FIML.
41
+ nl = estimate(Spec(spec.utilities, nests={"pt": ["bus", "rail"]}), modes)
42
+ print(nl.summary())
43
+ print("MNL vs NL:", lr_test(mnl, nl))
44
+ print(mnlite.compare([mnl, nl], ["MNL", "NL"]))
45
+
46
+ # 4. Scenario: bus fares +20 %, rail 5 minutes faster.
47
+ scenario = mnl.simulate(modes, {"cost_bus": "*1.2", "time_rail": "-5"})
48
+ print(scenario.table())