SearchLibrium 0.0.1__tar.gz → 0.0.83__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.
- searchlibrium-0.0.83/PKG-INFO +510 -0
- searchlibrium-0.0.83/README.md +481 -0
- searchlibrium-0.0.83/pyproject.toml +70 -0
- {searchlibrium-0.0.1 → searchlibrium-0.0.83}/setup.cfg +4 -4
- searchlibrium-0.0.83/src/SearchLibrium/Halton.py +191 -0
- searchlibrium-0.0.83/src/SearchLibrium/MixedLogit.py +1273 -0
- searchlibrium-0.0.83/src/SearchLibrium/Mode_Activity_Nested.py +85 -0
- searchlibrium-0.0.83/src/SearchLibrium/RandomP.py +49 -0
- searchlibrium-0.0.83/src/SearchLibrium/SEARCH_SM_MARIO.py +73 -0
- searchlibrium-0.0.83/src/SearchLibrium/Two_Level_Nest.py +151 -0
- searchlibrium-0.0.83/src/SearchLibrium/__init__.py +130 -0
- searchlibrium-0.0.83/src/SearchLibrium/__main__.py +5 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/_choice_model.py +1310 -1362
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/_device.py +145 -145
- searchlibrium-0.0.83/src/SearchLibrium/bhhh/minimize.py +195 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/boxcox_functions.py +115 -115
- searchlibrium-0.0.83/src/SearchLibrium/call_meta.py +453 -0
- searchlibrium-0.0.83/src/SearchLibrium/constraints_builder.py +447 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/harmony.py +1380 -1261
- searchlibrium-0.0.83/src/SearchLibrium/latent_class.py +353 -0
- searchlibrium-0.0.83/src/SearchLibrium/main.py +1196 -0
- searchlibrium-0.0.83/src/SearchLibrium/main_debug.py +550 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/misc.py +303 -303
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/mixed_logit.py +1553 -1553
- searchlibrium-0.0.83/src/SearchLibrium/mixed_nested.py +219 -0
- searchlibrium-0.0.83/src/SearchLibrium/mixedrrm.py +63 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/multinomial_logit.py +914 -558
- searchlibrium-0.0.83/src/SearchLibrium/multinomial_nested.py +1715 -0
- searchlibrium-0.0.83/src/SearchLibrium/multinomial_probit.py +165 -0
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/ordered_logit.py +1691 -1640
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/ordered_logit_mixed.py +102 -102
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/rrm.py +724 -520
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/search.py +4033 -3485
- {searchlibrium-0.0.1 → searchlibrium-0.0.83/src/SearchLibrium}/setup.py +58 -58
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/siman.py +1538 -1023
- {searchlibrium-0.0.1/old_code → searchlibrium-0.0.83/src/SearchLibrium}/threshold.py +777 -777
- searchlibrium-0.0.83/src/SearchLibrium/version.txt +1 -0
- searchlibrium-0.0.83/src/SearchLibrium.egg-info/PKG-INFO +510 -0
- searchlibrium-0.0.83/src/SearchLibrium.egg-info/SOURCES.txt +41 -0
- searchlibrium-0.0.83/src/SearchLibrium.egg-info/entry_points.txt +2 -0
- searchlibrium-0.0.83/src/SearchLibrium.egg-info/requires.txt +11 -0
- searchlibrium-0.0.83/src/SearchLibrium.egg-info/top_level.txt +1 -0
- searchlibrium-0.0.1/PKG-INFO +0 -21
- searchlibrium-0.0.1/README.md +0 -5
- searchlibrium-0.0.1/SearchLibrium.egg-info/PKG-INFO +0 -21
- searchlibrium-0.0.1/SearchLibrium.egg-info/SOURCES.txt +0 -32
- searchlibrium-0.0.1/SearchLibrium.egg-info/not-zip-safe +0 -1
- searchlibrium-0.0.1/SearchLibrium.egg-info/top_level.txt +0 -1
- searchlibrium-0.0.1/old_code/__init__.py +0 -8
- searchlibrium-0.0.1/old_code/akshay_test.py +0 -125
- searchlibrium-0.0.1/old_code/draws.py +0 -128
- searchlibrium-0.0.1/old_code/latent_class_constrained.py +0 -434
- searchlibrium-0.0.1/old_code/latent_class_mixed_model.py +0 -1566
- searchlibrium-0.0.1/old_code/latent_class_model.py +0 -1281
- searchlibrium-0.0.1/old_code/latent_main.py +0 -945
- searchlibrium-0.0.1/old_code/main.py +0 -1880
- searchlibrium-0.0.1/old_code/main_ol.py +0 -127
- searchlibrium-0.0.1/old_code/ordered_logit_multinomial.py +0 -701
- searchlibrium-0.0.1/old_code/r_ordered.py +0 -168
- searchlibrium-0.0.1/pyproject.toml +0 -18
- {searchlibrium-0.0.1 → searchlibrium-0.0.83/src}/SearchLibrium.egg-info/dependency_links.txt +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SearchLibrium
|
|
3
|
+
Version: 0.0.83
|
|
4
|
+
Summary: A Python package for econometric models driven by search
|
|
5
|
+
Author: Alexander Paz Prithvi Beeramole, Robert Burdett
|
|
6
|
+
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
|
+
Project-URL: Homepage, https://github.com/zahern/HypothesisX
|
|
8
|
+
Keywords: econometric models,search,discrete choice,logit,probit
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: numpy>2.0.0
|
|
20
|
+
Requires-Dist: pandas>=2.0.0
|
|
21
|
+
Requires-Dist: scikit-learn>=1.3.1
|
|
22
|
+
Requires-Dist: statsmodels
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: black; extra == "dev"
|
|
25
|
+
Requires-Dist: bumpver; extra == "dev"
|
|
26
|
+
Requires-Dist: isort; extra == "dev"
|
|
27
|
+
Requires-Dist: pip-tools; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# SearchLibrium
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/SearchLibrium/)
|
|
33
|
+
[](https://pypi.org/project/SearchLibrium/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
36
|
+
|
|
37
|
+
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
38
|
+
|
|
39
|
+
SearchLibrium searches over model specifications — which variables to include, whether parameters should be random, which transformations to apply, and which model class to use — and returns the best converged, all-significant model according to your chosen criterion (BIC, AIC, log-likelihood, MAE, or multi-objective combinations).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install SearchLibrium --upgrade
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Requirements:** Python ≥ 3.10, numpy ≥ 2.0, scipy ≥ 1.10, pandas ≥ 2.0, scikit-learn ≥ 1.3.1, statsmodels
|
|
50
|
+
|
|
51
|
+
### Install in Jupyter Notebook
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Run in a notebook cell
|
|
55
|
+
import subprocess
|
|
56
|
+
import sys
|
|
57
|
+
|
|
58
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "SearchLibrium", "--upgrade"])
|
|
59
|
+
|
|
60
|
+
# Then import
|
|
61
|
+
from SearchLibrium import Parameters, call_siman
|
|
62
|
+
print("✓ SearchLibrium installed and ready!")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import numpy as np
|
|
71
|
+
import pandas as pd
|
|
72
|
+
from SearchLibrium import Parameters, call_siman
|
|
73
|
+
|
|
74
|
+
df = pd.read_csv("https://raw.githubusercontent.com/zahern/HypothesisX/refs/heads/main/data/Swissmetro_final.csv")
|
|
75
|
+
varnames = ["TIME", "COST", "HEADWAY", "SEATS"]
|
|
76
|
+
choice_set = np.unique(df["alt"]).tolist()
|
|
77
|
+
|
|
78
|
+
params = Parameters(
|
|
79
|
+
criterions = [("bic", -1)], # minimise BIC
|
|
80
|
+
df = df,
|
|
81
|
+
varnames = varnames,
|
|
82
|
+
asvarnames = varnames,
|
|
83
|
+
isvarnames = [],
|
|
84
|
+
choice_set = choice_set,
|
|
85
|
+
choices = df["CHOICE"].values,
|
|
86
|
+
alt_var = df["alt"].values,
|
|
87
|
+
choice_id = df["custom_id"].values,
|
|
88
|
+
ind_id = df["ID"].values,
|
|
89
|
+
base_alt = "SM",
|
|
90
|
+
models = ["multinomial", "mixed_logit"],
|
|
91
|
+
allow_random = True,
|
|
92
|
+
p_val = 0.05,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A **run dashboard** is printed automatically at the end of every search, showing BIC, log-likelihood, AIC, MAE, variables, model type, and (if multi-objective) the full Pareto archive.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Example Notebooks
|
|
103
|
+
|
|
104
|
+
| Model | Notebook |
|
|
105
|
+
| ----- | -------- |
|
|
106
|
+
| Multinomial Logit — standalone fit + search | [notebooks/mnl_example.ipynb](src/SearchLibrium/notebooks/mnl_example.ipynb) |
|
|
107
|
+
| Mixed Logit — standalone fit + search | [notebooks/mixed_logit_example.ipynb](src/SearchLibrium/notebooks/mixed_logit_example.ipynb) |
|
|
108
|
+
| Random Regret Minimisation — standalone fit + search | [notebooks/rrm_example.ipynb](src/SearchLibrium/notebooks/rrm_example.ipynb) |
|
|
109
|
+
| Mixed Random Regret — standalone fit + search | [notebooks/mixed_rrm_example.ipynb](src/SearchLibrium/notebooks/mixed_rrm_example.ipynb) |
|
|
110
|
+
| Nested Logit — standalone fit + search | [notebooks/Data_Nest.ipynb](src/SearchLibrium/notebooks/Data_Nest.ipynb) |
|
|
111
|
+
| HPC Batch Jobs & PyPI Publishing | [notebooks/pbs_batch_jobs_guide.ipynb](src/SearchLibrium/notebooks/pbs_batch_jobs_guide.ipynb) |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## How the search works
|
|
116
|
+
|
|
117
|
+
The search uses **Simulated Annealing (SA)** to explore the space of model specifications:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
generate starting solution
|
|
121
|
+
└─ for each SA temperature step
|
|
122
|
+
└─ perturb current specification → guaranteed distinct from current
|
|
123
|
+
├─ fit model with JAX-accelerated MLE
|
|
124
|
+
├─ run backward elimination (remove insignificant vars, refit)
|
|
125
|
+
├─ accept if converged + Metropolis criterion satisfied
|
|
126
|
+
└─ update best solution
|
|
127
|
+
print dashboard
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Key guarantees:**
|
|
131
|
+
|
|
132
|
+
- Only **converged** solutions are accepted
|
|
133
|
+
- Every accepted solution has **all variables statistically significant** (p < `p_val`, backward elimination)
|
|
134
|
+
- Each perturbation is guaranteed to produce a **genuinely different specification** — a distribution-only swap (e.g. normal → lognormal) without any structural change does not count
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Data format
|
|
139
|
+
|
|
140
|
+
Your dataframe must be in **long format** — one row per alternative per observation:
|
|
141
|
+
|
|
142
|
+
| obs_id | alt | choice | TIME | COST | ... |
|
|
143
|
+
| ------ | ----- | ------ | ---- | ---- | --- |
|
|
144
|
+
| 1 | car | 1 | 35 | 12 | ... |
|
|
145
|
+
| 1 | train | 0 | 60 | 8 | ... |
|
|
146
|
+
| 1 | bus | 0 | 55 | 5 | ... |
|
|
147
|
+
| 2 | car | 0 | 40 | 14 | ... |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Model types
|
|
152
|
+
|
|
153
|
+
| Model name | Description | JAX MLE |
|
|
154
|
+
| ---------- | ----------- | ------- |
|
|
155
|
+
| `"multinomial"` | Multinomial Logit (MNL) | ✓ |
|
|
156
|
+
| `"mixed_logit"` | Mixed Logit with simulation-based integration | ✓ |
|
|
157
|
+
| `"random_regret"` | Random Regret Minimisation (RRM) | ✓ |
|
|
158
|
+
| `"mixed_random_regret"` | Mixed-RRM with random parameters | ✓ |
|
|
159
|
+
| `"nested_logit"` | Nested Logit (requires `nests=` and `lambdas=` kwargs) | ✓ |
|
|
160
|
+
| `"ordered_logit"` | Ordered Logit | ✓ |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Search examples by model type
|
|
165
|
+
|
|
166
|
+
### Multinomial Logit
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
params = Parameters(
|
|
170
|
+
criterions = [("bic", -1)],
|
|
171
|
+
df = df,
|
|
172
|
+
varnames = ["TIME", "COST", "HEADWAY"],
|
|
173
|
+
asvarnames = ["TIME", "COST", "HEADWAY"],
|
|
174
|
+
isvarnames = [],
|
|
175
|
+
choice_set = choice_set,
|
|
176
|
+
choices = df["CHOICE"].values,
|
|
177
|
+
alt_var = df["alt"].values,
|
|
178
|
+
choice_id = df["custom_id"].values,
|
|
179
|
+
base_alt = "SM",
|
|
180
|
+
models = ["multinomial"],
|
|
181
|
+
p_val = 0.05,
|
|
182
|
+
)
|
|
183
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Mixed Logit (random parameters)
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
params = Parameters(
|
|
190
|
+
criterions = [("bic", -1)],
|
|
191
|
+
df = df,
|
|
192
|
+
varnames = ["TIME", "COST", "HEADWAY"],
|
|
193
|
+
asvarnames = ["TIME", "COST", "HEADWAY"],
|
|
194
|
+
isvarnames = [],
|
|
195
|
+
choice_set = choice_set,
|
|
196
|
+
choices = df["CHOICE"].values,
|
|
197
|
+
alt_var = df["alt"].values,
|
|
198
|
+
choice_id = df["custom_id"].values,
|
|
199
|
+
ind_id = df["ID"].values,
|
|
200
|
+
base_alt = "SM",
|
|
201
|
+
models = ["mixed_logit"],
|
|
202
|
+
allow_random = True, # enable random parameters
|
|
203
|
+
allow_bcvars = True, # enable Box-Cox transformations
|
|
204
|
+
n_draws = 500, # Halton draws for simulation
|
|
205
|
+
p_val = 0.05,
|
|
206
|
+
)
|
|
207
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Random Regret Minimisation (RRM)
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
params = Parameters(
|
|
214
|
+
criterions = [("bic", -1)],
|
|
215
|
+
df = df,
|
|
216
|
+
varnames = ["TIME", "COST", "HEADWAY"],
|
|
217
|
+
asvarnames = ["TIME", "COST", "HEADWAY"],
|
|
218
|
+
isvarnames = [],
|
|
219
|
+
choice_set = choice_set,
|
|
220
|
+
choices = df["CHOICE"].values,
|
|
221
|
+
alt_var = df["alt"].values,
|
|
222
|
+
choice_id = df["custom_id"].values,
|
|
223
|
+
base_alt = "SM",
|
|
224
|
+
models = ["random_regret"],
|
|
225
|
+
p_val = 0.05,
|
|
226
|
+
)
|
|
227
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Mixed Random Regret (regret + heterogeneity)
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
params = Parameters(
|
|
234
|
+
criterions = [("bic", -1)],
|
|
235
|
+
df = df,
|
|
236
|
+
varnames = ["TIME", "COST", "HEADWAY"],
|
|
237
|
+
asvarnames = ["TIME", "COST", "HEADWAY"],
|
|
238
|
+
isvarnames = [],
|
|
239
|
+
choice_set = choice_set,
|
|
240
|
+
choices = df["CHOICE"].values,
|
|
241
|
+
alt_var = df["alt"].values,
|
|
242
|
+
choice_id = df["custom_id"].values,
|
|
243
|
+
ind_id = df["ID"].values,
|
|
244
|
+
base_alt = "SM",
|
|
245
|
+
models = ["mixed_random_regret"],
|
|
246
|
+
allow_random = True,
|
|
247
|
+
n_draws = 500,
|
|
248
|
+
p_val = 0.05,
|
|
249
|
+
)
|
|
250
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Nested Logit
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
nests = {"PublicTransport": [0, 1], "Private": [2, 3]}
|
|
257
|
+
lambdas = {"PublicTransport": 0.8, "Private": 1.0}
|
|
258
|
+
|
|
259
|
+
params = Parameters(
|
|
260
|
+
criterions = [("bic", -1)],
|
|
261
|
+
df = df,
|
|
262
|
+
varnames = ["TIME", "COST", "HEADWAY"],
|
|
263
|
+
asvarnames = ["TIME", "COST", "HEADWAY"],
|
|
264
|
+
choice_set = choice_set,
|
|
265
|
+
choices = df["CHOICE"].values,
|
|
266
|
+
alt_var = df["alt"].values,
|
|
267
|
+
choice_id = df["custom_id"].values,
|
|
268
|
+
base_alt = "SM",
|
|
269
|
+
models = ["nested_logit"],
|
|
270
|
+
nests = nests,
|
|
271
|
+
lambdas = lambdas,
|
|
272
|
+
p_val = 0.05,
|
|
273
|
+
)
|
|
274
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Multi-objective search (BIC + MAE)
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
params = Parameters(
|
|
281
|
+
criterions = [("bic", -1), ("mae", -1)], # minimise both
|
|
282
|
+
df = df,
|
|
283
|
+
df_test = df_test, # required for MAE
|
|
284
|
+
varnames = varnames,
|
|
285
|
+
asvarnames = varnames,
|
|
286
|
+
choice_set = choice_set,
|
|
287
|
+
choices = df["CHOICE"].values,
|
|
288
|
+
alt_var = df["alt"].values,
|
|
289
|
+
choice_id = df["custom_id"].values,
|
|
290
|
+
base_alt = "SM",
|
|
291
|
+
models = ["multinomial", "mixed_logit"],
|
|
292
|
+
allow_random = True,
|
|
293
|
+
)
|
|
294
|
+
best = call_siman(params, init_sol=None, id_num=1)
|
|
295
|
+
# Returns a Pareto-optimal solution; full archive is printed in the dashboard
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Key parameters
|
|
301
|
+
|
|
302
|
+
| Parameter | Type | Default | Description |
|
|
303
|
+
| --------- | ---- | ------- | ----------- |
|
|
304
|
+
| `criterions` | list of `(name, sign)` | required | Objectives: `"bic"`, `"aic"`, `"loglik"`, `"mae"`. Sign: `-1` = minimise, `+1` = maximise |
|
|
305
|
+
| `models` | list of str | all | Model classes to search over |
|
|
306
|
+
| `allow_random` | bool | `False` | Enable random parameters (required for mixed models) |
|
|
307
|
+
| `allow_bcvars` | bool | `False` | Enable Box-Cox variable transformations |
|
|
308
|
+
| `allow_corvars` | bool | `False` | Enable correlated random parameters |
|
|
309
|
+
| `p_val` | float | `0.05` | Significance threshold — variables with p > p_val are eliminated |
|
|
310
|
+
| `all_sig` | bool | `True` | Enforce all-significant via backward elimination at each evaluation |
|
|
311
|
+
| `n_draws` | int | `1000` | Halton draws for mixed model simulation |
|
|
312
|
+
| `maxiter` | int | `2000` | Maximum MLE iterations per model evaluation |
|
|
313
|
+
|
|
314
|
+
### Random parameter distributions
|
|
315
|
+
|
|
316
|
+
| Code | Distribution |
|
|
317
|
+
| ---- | ------------ |
|
|
318
|
+
| `"n"` | Normal |
|
|
319
|
+
| `"ln"` | Log-normal |
|
|
320
|
+
| `"t"` | Triangular |
|
|
321
|
+
| `"tn"` | Truncated normal |
|
|
322
|
+
| `"u"` | Uniform |
|
|
323
|
+
|
|
324
|
+
### SA control parameters
|
|
325
|
+
|
|
326
|
+
Pass `ctrl=(tI, tF, max_temp_steps, max_iter)` to `call_siman`:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
best = call_siman(params, ctrl=(500, 0.001, 100, 20), id_num=1)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
| Parameter | Description |
|
|
333
|
+
| --------- | ----------- |
|
|
334
|
+
| `tI` | Initial temperature — higher = more exploration early on |
|
|
335
|
+
| `tF` | Final temperature — lower = more exploitation at the end |
|
|
336
|
+
| `max_temp_steps` | Number of cooling steps |
|
|
337
|
+
| `max_iter` | Iterations evaluated at each temperature step |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Standalone model fitting (no search)
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from SearchLibrium import MultinomialLogit, MixedLogit, RandomRegret, MixedRandomRegret
|
|
345
|
+
|
|
346
|
+
# MNL
|
|
347
|
+
mnl = MultinomialLogit()
|
|
348
|
+
mnl.setup(X, y, varnames=varnames, alts=alts, ids=ids)
|
|
349
|
+
mnl.fit()
|
|
350
|
+
mnl.summarise()
|
|
351
|
+
|
|
352
|
+
# Mixed Logit
|
|
353
|
+
mxl = MixedLogit()
|
|
354
|
+
mxl.setup(X, y, varnames=varnames, alts=alts, ids=ids, panels=panels,
|
|
355
|
+
randvars={"TIME": "n", "COST": "ln"}, n_draws=500)
|
|
356
|
+
mxl.fit()
|
|
357
|
+
mxl.summarise()
|
|
358
|
+
|
|
359
|
+
# RRM
|
|
360
|
+
rrm = RandomRegret(df=df, short=False)
|
|
361
|
+
rrm.fit()
|
|
362
|
+
rrm.report()
|
|
363
|
+
|
|
364
|
+
# Mixed RRM
|
|
365
|
+
mrrm = MixedRandomRegret(df=df)
|
|
366
|
+
mrrm.fit()
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Interpreting the dashboard
|
|
372
|
+
|
|
373
|
+
After every `call_siman` run a dashboard is printed:
|
|
374
|
+
|
|
375
|
+
```text
|
|
376
|
+
╔══════════════════════════════════════════════════════╗
|
|
377
|
+
║ SEARCHLIBRIUM — RUN DASHBOARD ║
|
|
378
|
+
╠══════════════════════════════════════════════════════╣
|
|
379
|
+
║ Model type : mixed_logit ║
|
|
380
|
+
║ Variables : TIME, COST, HEADWAY ║
|
|
381
|
+
║ Random params: TIME~n, COST~ln ║
|
|
382
|
+
╠══════════════════════════════════════════════════════╣
|
|
383
|
+
║ Log-likelihood : -312.45 ║
|
|
384
|
+
║ AIC : 634.90 ║
|
|
385
|
+
║ BIC : 658.22 ◄ best ║
|
|
386
|
+
║ MAE : 0.1843 ║
|
|
387
|
+
╠══════════════════════════════════════════════════════╣
|
|
388
|
+
║ Evaluations : 247 Converged : 198 Accepted : 43 ║
|
|
389
|
+
╚══════════════════════════════════════════════════════╝
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
- **Lower BIC / AIC** = better fit-complexity tradeoff
|
|
393
|
+
- All retained variables are **statistically significant** (p < `p_val`)
|
|
394
|
+
- **Random parameters** indicate heterogeneity in that attribute's taste
|
|
395
|
+
- **RRM** models suit contexts where regret-avoidance drives choice behaviour
|
|
396
|
+
- For multi-objective runs the full Pareto archive is shown with one row per non-dominated solution
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Bundled datasets
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
import SearchLibrium as sl
|
|
404
|
+
sl.main.preview_dataset() # prints head of each dataset
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
| Name | Description |
|
|
408
|
+
| ---- | ----------- |
|
|
409
|
+
| `electricity` | Stated-preference electricity plan choice |
|
|
410
|
+
| `travel_mode` | Mode choice: air / train / bus / car |
|
|
411
|
+
| `swiss_metro` | Swiss Metro SP study (SM / train / car) |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## CLI
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
python -m SearchLibrium --info # print package guide
|
|
419
|
+
python -m SearchLibrium --preview_datasets # preview bundled datasets
|
|
420
|
+
python -m SearchLibrium --test_search # run MNL/MXL search on travel_mode
|
|
421
|
+
python -m SearchLibrium --test_search_nest # run nested logit search
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Search algorithms
|
|
427
|
+
|
|
428
|
+
Both algorithms share a **consistent interface** through `call_search`:
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
from SearchLibrium import call_search, estimate_ctrl
|
|
432
|
+
|
|
433
|
+
# Auto-estimate hyperparameters from problem size (recommended)
|
|
434
|
+
best = call_search(params) # SA by default
|
|
435
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
436
|
+
|
|
437
|
+
# Manual hyperparameters
|
|
438
|
+
best = call_search(params, ctrl=(1000, 0.001, 100, 20)) # SA
|
|
439
|
+
best = call_search(params, algorithm='hs',
|
|
440
|
+
ctrl=(20, 500, 0.9, 0.6, 0.85, 0.3)) # HS
|
|
441
|
+
|
|
442
|
+
# Inspect auto-estimated ctrl before running
|
|
443
|
+
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
444
|
+
print(ctrl)
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
448
|
+
|
|
449
|
+
| Parameter | Meaning |
|
|
450
|
+
| --------- | ------- |
|
|
451
|
+
| `tI` | Initial temperature — higher → more exploration |
|
|
452
|
+
| `tF` | Final temperature — lower → more exploitation |
|
|
453
|
+
| `max_temp_steps` | Number of cooling steps |
|
|
454
|
+
| `max_iter` | Evaluations per cooling step |
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
461
|
+
|
|
462
|
+
| Parameter | Meaning |
|
|
463
|
+
| --------- | ------- |
|
|
464
|
+
| `max_mem` | Harmony memory size (population) |
|
|
465
|
+
| `maxiter` | Improvisation iterations |
|
|
466
|
+
| `max_harm` | Max harmony consideration rate |
|
|
467
|
+
| `min_harm` | Min harmony consideration rate |
|
|
468
|
+
| `max_pitch` | Max pitch adjustment rate |
|
|
469
|
+
| `min_pitch` | Min pitch adjustment rate |
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Auto hyperparameter estimation
|
|
476
|
+
|
|
477
|
+
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
478
|
+
problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
from SearchLibrium import estimate_ctrl
|
|
482
|
+
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
483
|
+
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
484
|
+
print('SA ctrl:', ctrl_sa)
|
|
485
|
+
print('HS ctrl:', ctrl_hs)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Complexity buckets:
|
|
489
|
+
|
|
490
|
+
| Complexity | SA tI | SA steps | SA iter/step | HS mem | HS iters |
|
|
491
|
+
| ---------- | ----- | -------- | ------------ | ------ | -------- |
|
|
492
|
+
| < 50 | 500 | 50 | 10 | 10 | 100 |
|
|
493
|
+
| 50–200 | 1 000 | 100 | 15 | 15 | 300 |
|
|
494
|
+
| 200–600 | 2 000 | 150 | 20 | 20 | 500 |
|
|
495
|
+
| > 600 | 5 000 | 250 | 30 | 25 | 800 |
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
## License
|
|
500
|
+
|
|
501
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
502
|
+
|
|
503
|
+
## Citation
|
|
504
|
+
|
|
505
|
+
If you use SearchLibrium in academic work, please cite the repository:
|
|
506
|
+
|
|
507
|
+
```text
|
|
508
|
+
Ahern, Z. (2025). SearchLibrium: Automated discrete choice model search.
|
|
509
|
+
https://github.com/zahern/HypothesisX
|
|
510
|
+
```
|