SearchLibrium 0.0.126__tar.gz → 0.0.128__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.126 → searchlibrium-0.0.128}/PKG-INFO +141 -23
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/README.md +137 -22
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/pyproject.toml +4 -1
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/__init__.py +2 -2
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/constraints_builder.py +31 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/search.py +79 -2
- searchlibrium-0.0.128/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/PKG-INFO +141 -23
- searchlibrium-0.0.126/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/setup.cfg +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/harmony.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.128
|
|
4
4
|
Summary: A Python package for econometric models driven by search
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
8
|
+
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
9
|
+
Project-URL: Issues, https://github.com/zahern/HypothesisX/issues
|
|
10
|
+
Project-URL: Documentation, https://pypi.org/project/SearchLibrium/
|
|
8
11
|
Keywords: econometric models,search,discrete choice,logit,probit
|
|
9
12
|
Classifier: Development Status :: 4 - Beta
|
|
10
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -41,8 +44,9 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
41
44
|
[](https://pypi.org/project/SearchLibrium/)
|
|
42
45
|
[](LICENSE)
|
|
43
46
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/zahern/HypothesisX)
|
|
44
48
|
|
|
45
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
49
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
46
50
|
|
|
47
51
|
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).
|
|
48
52
|
|
|
@@ -122,16 +126,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
122
126
|
|
|
123
127
|
## How the search works
|
|
124
128
|
|
|
125
|
-
The search uses **
|
|
129
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
130
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
131
|
+
specifications:
|
|
126
132
|
|
|
127
133
|
```text
|
|
128
134
|
generate starting solution
|
|
129
|
-
└─ for each
|
|
135
|
+
└─ for each iteration / temperature step / improvisation
|
|
130
136
|
└─ perturb current specification → guaranteed distinct from current
|
|
131
137
|
├─ fit model with JAX-accelerated MLE
|
|
132
138
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
133
|
-
├─ accept if converged +
|
|
134
|
-
|
|
139
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
140
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
141
|
+
└─ update best solution / harmony memory
|
|
135
142
|
print dashboard
|
|
136
143
|
```
|
|
137
144
|
|
|
@@ -433,27 +440,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
433
440
|
|
|
434
441
|
## Search algorithms
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
444
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
437
445
|
|
|
438
446
|
```python
|
|
439
|
-
from SearchLibrium import call_search,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
best = call_search(params,
|
|
447
|
-
best = call_search(params, algorithm='
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# Inspect auto-estimated
|
|
447
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
448
|
+
call_banditsa, call_harmony_pbil,
|
|
449
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
450
|
+
|
|
451
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
452
|
+
best = call_search(params) # SA (default)
|
|
453
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
454
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
455
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
456
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
457
|
+
|
|
458
|
+
# Inspect auto-estimated hyperparameters before running
|
|
451
459
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
452
460
|
print(ctrl)
|
|
453
461
|
```
|
|
454
462
|
|
|
463
|
+
### Algorithm reference
|
|
464
|
+
|
|
465
|
+
| Function | Description | Population-based | Key idea |
|
|
466
|
+
| -------- | ----------- | :---: | -------- |
|
|
467
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
468
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
469
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
470
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
471
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
472
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
473
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
474
|
+
|
|
455
475
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
456
476
|
|
|
477
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
478
|
+
|
|
457
479
|
| Parameter | Meaning |
|
|
458
480
|
| --------- | ------- |
|
|
459
481
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -467,12 +489,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
467
489
|
|
|
468
490
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
469
491
|
|
|
492
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
493
|
+
|
|
470
494
|
| Parameter | Meaning |
|
|
471
495
|
| --------- | ------- |
|
|
472
496
|
| `max_mem` | Harmony memory size (population) |
|
|
473
497
|
| `maxiter` | Improvisation iterations |
|
|
474
|
-
| `max_harm` | Max harmony consideration rate |
|
|
475
|
-
| `min_harm` | Min harmony consideration rate |
|
|
498
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
499
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
476
500
|
| `max_pitch` | Max pitch adjustment rate |
|
|
477
501
|
| `min_pitch` | Min pitch adjustment rate |
|
|
478
502
|
|
|
@@ -480,6 +504,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
480
504
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
481
505
|
```
|
|
482
506
|
|
|
507
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
508
|
+
|
|
509
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
510
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
511
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
512
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
best = call_sapbil(params, id_num=1)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
519
|
+
|
|
520
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
521
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
522
|
+
add/remove decisions based on learned probabilities.
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
529
|
+
|
|
530
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
531
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
532
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
best = call_banditsa(params, id_num=1)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Parallel SA variants
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
542
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
543
|
+
```
|
|
544
|
+
|
|
483
545
|
### Auto hyperparameter estimation
|
|
484
546
|
|
|
485
547
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -489,8 +551,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
489
551
|
from SearchLibrium import estimate_ctrl
|
|
490
552
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
491
553
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
492
|
-
print('SA ctrl:', ctrl_sa)
|
|
493
|
-
print('HS ctrl:', ctrl_hs)
|
|
494
554
|
```
|
|
495
555
|
|
|
496
556
|
Complexity buckets:
|
|
@@ -504,6 +564,64 @@ Complexity buckets:
|
|
|
504
564
|
|
|
505
565
|
|
|
506
566
|
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Constraints
|
|
570
|
+
|
|
571
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
575
|
+
|
|
576
|
+
constraints = ConstraintBuilder()
|
|
577
|
+
|
|
578
|
+
# ── Inclusion / exclusion ──
|
|
579
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
580
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
581
|
+
|
|
582
|
+
# ── Mutually exclusive groups ──
|
|
583
|
+
# At most ONE variable from each group may appear in any solution.
|
|
584
|
+
# The search will never add a partner if a group member is already
|
|
585
|
+
# present, and will remove extras if they slip through.
|
|
586
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
587
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
588
|
+
|
|
589
|
+
# ── Minimum behavioural content ──
|
|
590
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
591
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
592
|
+
|
|
593
|
+
# ── Random-parameter rules ──
|
|
594
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
595
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
596
|
+
|
|
597
|
+
# ── Attach constraints to the Parameters object ──
|
|
598
|
+
params = Parameters(
|
|
599
|
+
criterions = [("bic", -1)],
|
|
600
|
+
df = df,
|
|
601
|
+
varnames = varnames,
|
|
602
|
+
asvarnames = varnames,
|
|
603
|
+
choice_set = choice_set,
|
|
604
|
+
choices = df["CHOICE"].values,
|
|
605
|
+
alt_var = df["alt"].values,
|
|
606
|
+
choice_id = df["custom_id"].values,
|
|
607
|
+
ind_id = df["ID"].values,
|
|
608
|
+
base_alt = "SM",
|
|
609
|
+
models = ["multinomial", "mixed_logit"],
|
|
610
|
+
allow_random = True,
|
|
611
|
+
p_val = 0.05,
|
|
612
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
best = call_siman(params, id_num=1)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The constraint enforcement happens at every stage:
|
|
619
|
+
- **Solution generation** — filtered so violations are never created
|
|
620
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
621
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
507
625
|
## License
|
|
508
626
|
|
|
509
627
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
[](https://pypi.org/project/SearchLibrium/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/zahern/HypothesisX)
|
|
7
8
|
|
|
8
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
9
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
9
10
|
|
|
10
11
|
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).
|
|
11
12
|
|
|
@@ -85,16 +86,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
85
86
|
|
|
86
87
|
## How the search works
|
|
87
88
|
|
|
88
|
-
The search uses **
|
|
89
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
90
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
91
|
+
specifications:
|
|
89
92
|
|
|
90
93
|
```text
|
|
91
94
|
generate starting solution
|
|
92
|
-
└─ for each
|
|
95
|
+
└─ for each iteration / temperature step / improvisation
|
|
93
96
|
└─ perturb current specification → guaranteed distinct from current
|
|
94
97
|
├─ fit model with JAX-accelerated MLE
|
|
95
98
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
96
|
-
├─ accept if converged +
|
|
97
|
-
|
|
99
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
100
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
101
|
+
└─ update best solution / harmony memory
|
|
98
102
|
print dashboard
|
|
99
103
|
```
|
|
100
104
|
|
|
@@ -396,27 +400,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
396
400
|
|
|
397
401
|
## Search algorithms
|
|
398
402
|
|
|
399
|
-
|
|
403
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
404
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
400
405
|
|
|
401
406
|
```python
|
|
402
|
-
from SearchLibrium import call_search,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
#
|
|
409
|
-
best = call_search(params,
|
|
410
|
-
best = call_search(params, algorithm='
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
# Inspect auto-estimated
|
|
407
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
408
|
+
call_banditsa, call_harmony_pbil,
|
|
409
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
410
|
+
|
|
411
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
412
|
+
best = call_search(params) # SA (default)
|
|
413
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
414
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
415
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
416
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
417
|
+
|
|
418
|
+
# Inspect auto-estimated hyperparameters before running
|
|
414
419
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
415
420
|
print(ctrl)
|
|
416
421
|
```
|
|
417
422
|
|
|
423
|
+
### Algorithm reference
|
|
424
|
+
|
|
425
|
+
| Function | Description | Population-based | Key idea |
|
|
426
|
+
| -------- | ----------- | :---: | -------- |
|
|
427
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
428
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
429
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
430
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
431
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
432
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
433
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
434
|
+
|
|
418
435
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
419
436
|
|
|
437
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
438
|
+
|
|
420
439
|
| Parameter | Meaning |
|
|
421
440
|
| --------- | ------- |
|
|
422
441
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -430,12 +449,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
430
449
|
|
|
431
450
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
432
451
|
|
|
452
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
453
|
+
|
|
433
454
|
| Parameter | Meaning |
|
|
434
455
|
| --------- | ------- |
|
|
435
456
|
| `max_mem` | Harmony memory size (population) |
|
|
436
457
|
| `maxiter` | Improvisation iterations |
|
|
437
|
-
| `max_harm` | Max harmony consideration rate |
|
|
438
|
-
| `min_harm` | Min harmony consideration rate |
|
|
458
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
459
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
439
460
|
| `max_pitch` | Max pitch adjustment rate |
|
|
440
461
|
| `min_pitch` | Min pitch adjustment rate |
|
|
441
462
|
|
|
@@ -443,6 +464,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
443
464
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
444
465
|
```
|
|
445
466
|
|
|
467
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
468
|
+
|
|
469
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
470
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
471
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
472
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
473
|
+
|
|
474
|
+
```python
|
|
475
|
+
best = call_sapbil(params, id_num=1)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
479
|
+
|
|
480
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
481
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
482
|
+
add/remove decisions based on learned probabilities.
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
489
|
+
|
|
490
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
491
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
492
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
best = call_banditsa(params, id_num=1)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Parallel SA variants
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
502
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
503
|
+
```
|
|
504
|
+
|
|
446
505
|
### Auto hyperparameter estimation
|
|
447
506
|
|
|
448
507
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -452,8 +511,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
452
511
|
from SearchLibrium import estimate_ctrl
|
|
453
512
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
454
513
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
455
|
-
print('SA ctrl:', ctrl_sa)
|
|
456
|
-
print('HS ctrl:', ctrl_hs)
|
|
457
514
|
```
|
|
458
515
|
|
|
459
516
|
Complexity buckets:
|
|
@@ -467,6 +524,64 @@ Complexity buckets:
|
|
|
467
524
|
|
|
468
525
|
|
|
469
526
|
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Constraints
|
|
530
|
+
|
|
531
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
535
|
+
|
|
536
|
+
constraints = ConstraintBuilder()
|
|
537
|
+
|
|
538
|
+
# ── Inclusion / exclusion ──
|
|
539
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
540
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
541
|
+
|
|
542
|
+
# ── Mutually exclusive groups ──
|
|
543
|
+
# At most ONE variable from each group may appear in any solution.
|
|
544
|
+
# The search will never add a partner if a group member is already
|
|
545
|
+
# present, and will remove extras if they slip through.
|
|
546
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
547
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
548
|
+
|
|
549
|
+
# ── Minimum behavioural content ──
|
|
550
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
551
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
552
|
+
|
|
553
|
+
# ── Random-parameter rules ──
|
|
554
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
555
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
556
|
+
|
|
557
|
+
# ── Attach constraints to the Parameters object ──
|
|
558
|
+
params = Parameters(
|
|
559
|
+
criterions = [("bic", -1)],
|
|
560
|
+
df = df,
|
|
561
|
+
varnames = varnames,
|
|
562
|
+
asvarnames = varnames,
|
|
563
|
+
choice_set = choice_set,
|
|
564
|
+
choices = df["CHOICE"].values,
|
|
565
|
+
alt_var = df["alt"].values,
|
|
566
|
+
choice_id = df["custom_id"].values,
|
|
567
|
+
ind_id = df["ID"].values,
|
|
568
|
+
base_alt = "SM",
|
|
569
|
+
models = ["multinomial", "mixed_logit"],
|
|
570
|
+
allow_random = True,
|
|
571
|
+
p_val = 0.05,
|
|
572
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
best = call_siman(params, id_num=1)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The constraint enforcement happens at every stage:
|
|
579
|
+
- **Solution generation** — filtered so violations are never created
|
|
580
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
581
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
470
585
|
## License
|
|
471
586
|
|
|
472
587
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -65,12 +65,15 @@ dev = [
|
|
|
65
65
|
|
|
66
66
|
[project.urls]
|
|
67
67
|
Homepage = "https://pypi.org/project/SearchLibrium/"
|
|
68
|
+
Repository = "https://github.com/zahern/HypothesisX"
|
|
69
|
+
Issues = "https://github.com/zahern/HypothesisX/issues"
|
|
70
|
+
Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
68
71
|
|
|
69
72
|
[project.scripts]
|
|
70
73
|
realpython = "SearchLibrium.__main__:main"
|
|
71
74
|
|
|
72
75
|
[tool.bumpver]
|
|
73
|
-
current_version = "0.0.
|
|
76
|
+
current_version = "0.0.128"
|
|
74
77
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
75
78
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
76
79
|
commit = true
|
|
@@ -113,7 +113,7 @@ try:
|
|
|
113
113
|
from . import misc
|
|
114
114
|
from .sapbil import SAPBIL, ProbabilityMatrix
|
|
115
115
|
from .hspbil import HSPBIL
|
|
116
|
-
from .call_meta import call_harmony, call_harmony_pbil, call_siman, call_parsa, call_search, call_sapbil, estimate_ctrl
|
|
116
|
+
from .call_meta import call_harmony, call_harmony_pbil, call_siman, call_parsa, call_search, call_sapbil, call_banditsa, call_parcopsa, estimate_ctrl
|
|
117
117
|
from .predict import DestinationPredictor, predict_destination_components, predict_aggregate_destination_flows
|
|
118
118
|
|
|
119
119
|
except ImportError as e:
|
|
@@ -135,7 +135,7 @@ except ImportError as e:
|
|
|
135
135
|
from search import Parameters
|
|
136
136
|
from sapbil import SAPBIL, ProbabilityMatrix
|
|
137
137
|
from hspbil import HSPBIL
|
|
138
|
-
from call_meta import call_siman, call_harmony, call_harmony_pbil, call_search, call_sapbil, estimate_ctrl
|
|
138
|
+
from call_meta import call_siman, call_harmony, call_harmony_pbil, call_search, call_sapbil, call_banditsa, call_parsa, call_parcopsa, estimate_ctrl
|
|
139
139
|
from predict import DestinationPredictor, predict_destination_components, predict_aggregate_destination_flows
|
|
140
140
|
try:
|
|
141
141
|
from .main import print_ascii_art_logo
|
|
@@ -104,6 +104,37 @@ class ConstraintBuilder:
|
|
|
104
104
|
self._print(f"Never include: {variables}")
|
|
105
105
|
return self
|
|
106
106
|
|
|
107
|
+
def mutually_exclusive(self, *variables: str) -> 'ConstraintBuilder':
|
|
108
|
+
"""
|
|
109
|
+
Declare that at most one variable from this group may be included
|
|
110
|
+
in any model specification. The search will never add a variable
|
|
111
|
+
from the group if another member of the group is already present,
|
|
112
|
+
and will remove extra members if they slip in.
|
|
113
|
+
|
|
114
|
+
You may call this method multiple times to define several
|
|
115
|
+
independent groups. Each call creates a separate exclusion set.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
*variables : str
|
|
120
|
+
Variable names forming a mutually exclusive group
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
ConstraintBuilder
|
|
125
|
+
Self for method chaining
|
|
126
|
+
|
|
127
|
+
Example
|
|
128
|
+
-------
|
|
129
|
+
>>> constraints.mutually_exclusive('SPEED', 'TIME')
|
|
130
|
+
>>> constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY')
|
|
131
|
+
"""
|
|
132
|
+
if 'mutually_exclusive' not in self._constraints:
|
|
133
|
+
self._constraints['mutually_exclusive'] = []
|
|
134
|
+
self._constraints['mutually_exclusive'].append(list(variables))
|
|
135
|
+
self._print(f"Mutually exclusive group: {variables}")
|
|
136
|
+
return self
|
|
137
|
+
|
|
107
138
|
def min_behavioral(self, min_count: int, *variables: str) -> 'ConstraintBuilder':
|
|
108
139
|
"""
|
|
109
140
|
Require at least ``min_count`` variables from a behavioural pool
|
|
@@ -1718,6 +1718,7 @@ class Search():
|
|
|
1718
1718
|
asvars = [self.param.avail_asvars[i] for i in asvar_select_pos]
|
|
1719
1719
|
asvars.extend(self.param.ps_asvars)
|
|
1720
1720
|
asvars = self.remove_redundant_asvars(asvars, self.param.trans_asvars, self.param.asvarnames)
|
|
1721
|
+
asvars = self._apply_mutual_exclusion_filter(asvars)
|
|
1721
1722
|
return asvars
|
|
1722
1723
|
# }
|
|
1723
1724
|
|
|
@@ -1731,7 +1732,7 @@ class Search():
|
|
|
1731
1732
|
isvar_select_pos = [i for i, x in enumerate(ind_availisvar) if x == 1]
|
|
1732
1733
|
isvars = [self.param.avail_isvars[i] for i in isvar_select_pos]
|
|
1733
1734
|
isvars.extend(self.param.ps_isvars)
|
|
1734
|
-
|
|
1735
|
+
isvars = self._apply_mutual_exclusion_filter(isvars)
|
|
1735
1736
|
return isvars
|
|
1736
1737
|
# }
|
|
1737
1738
|
|
|
@@ -2025,9 +2026,12 @@ class Search():
|
|
|
2025
2026
|
if 'randvars' in solution:
|
|
2026
2027
|
solution['randvars'] = {k: v for k, v in solution['randvars'].items() if k not in never_rand}
|
|
2027
2028
|
|
|
2029
|
+
# ── Mutual-exclusion constraint ──
|
|
2030
|
+
self._enforce_mutual_exclusion(solution)
|
|
2031
|
+
|
|
2028
2032
|
# ── Min-behavioural constraint (soft: at least n from a pool) ──
|
|
2029
2033
|
self._enforce_min_behavioral(solution)
|
|
2030
|
-
|
|
2034
|
+
|
|
2031
2035
|
return solution
|
|
2032
2036
|
|
|
2033
2037
|
def _get_forced_vars(self):
|
|
@@ -2087,6 +2091,75 @@ class Search():
|
|
|
2087
2091
|
protected.update(current)
|
|
2088
2092
|
return protected
|
|
2089
2093
|
|
|
2094
|
+
def _apply_mutual_exclusion_filter(self, varlist):
|
|
2095
|
+
"""Given a list of variables, keep at most one per mutual-exclusion group.
|
|
2096
|
+
|
|
2097
|
+
For each group the first variable encountered is kept; subsequent
|
|
2098
|
+
members are dropped.
|
|
2099
|
+
"""
|
|
2100
|
+
groups = self._get_mutual_exclusion_groups()
|
|
2101
|
+
if not groups:
|
|
2102
|
+
return varlist
|
|
2103
|
+
blocked = set()
|
|
2104
|
+
for group in groups:
|
|
2105
|
+
seen_first = False
|
|
2106
|
+
for v in varlist:
|
|
2107
|
+
if v in group:
|
|
2108
|
+
if seen_first:
|
|
2109
|
+
blocked.add(v)
|
|
2110
|
+
else:
|
|
2111
|
+
seen_first = True
|
|
2112
|
+
return [v for v in varlist if v not in blocked]
|
|
2113
|
+
|
|
2114
|
+
def _get_mutual_exclusion_groups(self):
|
|
2115
|
+
"""Return the list of mutually-exclusive variable groups, or [].
|
|
2116
|
+
|
|
2117
|
+
Each group is a list[str]; at most one member of each group may
|
|
2118
|
+
appear in a solution.
|
|
2119
|
+
"""
|
|
2120
|
+
if not hasattr(self, 'pres_spec_constr') or self.pres_spec_constr is None:
|
|
2121
|
+
return []
|
|
2122
|
+
return self.pres_spec_constr.get('mutually_exclusive', [])
|
|
2123
|
+
|
|
2124
|
+
def _get_excluded_by_mutual_group(self, already_present: set):
|
|
2125
|
+
"""Return the set of variables that are blocked because a partner
|
|
2126
|
+
from the same mutually-exclusive group is *already_present*.
|
|
2127
|
+
"""
|
|
2128
|
+
excluded = set()
|
|
2129
|
+
for group in self._get_mutual_exclusion_groups():
|
|
2130
|
+
gset = set(group)
|
|
2131
|
+
if gset & already_present:
|
|
2132
|
+
excluded.update(gset)
|
|
2133
|
+
return excluded
|
|
2134
|
+
|
|
2135
|
+
def _enforce_mutual_exclusion(self, solution):
|
|
2136
|
+
"""Ensure at most one variable per mutually-exclusive group is present.
|
|
2137
|
+
|
|
2138
|
+
If more than one is found, keep the first (arbitrary) and
|
|
2139
|
+
remove the rest from asvars, isvars, and class/member specs.
|
|
2140
|
+
"""
|
|
2141
|
+
groups = self._get_mutual_exclusion_groups()
|
|
2142
|
+
if not groups:
|
|
2143
|
+
return
|
|
2144
|
+
all_v = self._all_vars_in_solution(solution)
|
|
2145
|
+
for group in groups:
|
|
2146
|
+
present = [v for v in group if v in all_v]
|
|
2147
|
+
if len(present) <= 1:
|
|
2148
|
+
continue
|
|
2149
|
+
keep = set(present[:1])
|
|
2150
|
+
remove = set(present[1:])
|
|
2151
|
+
for v in remove:
|
|
2152
|
+
if 'asvars' in solution and v in solution['asvars']:
|
|
2153
|
+
solution['asvars'] = [x for x in solution['asvars'] if x != v]
|
|
2154
|
+
if 'isvars' in solution and v in solution['isvars']:
|
|
2155
|
+
solution['isvars'] = [x for x in solution['isvars'] if x != v]
|
|
2156
|
+
if 'randvars' in solution and v in solution['randvars']:
|
|
2157
|
+
del solution['randvars'][v]
|
|
2158
|
+
if 'bcvars' in solution and v in solution['bcvars']:
|
|
2159
|
+
solution['bcvars'] = [x for x in solution['bcvars'] if x != v]
|
|
2160
|
+
if 'corvars' in solution and v in solution['corvars']:
|
|
2161
|
+
solution['corvars'] = [x for x in solution['corvars'] if x != v]
|
|
2162
|
+
|
|
2090
2163
|
def _apply_latent_class_constraints(self, solution, lc_constraints):
|
|
2091
2164
|
"""Apply constraints specific to latent class models."""
|
|
2092
2165
|
# Example: force variables to appear only in certain classes
|
|
@@ -2415,6 +2488,8 @@ class Search():
|
|
|
2415
2488
|
def perturb_add_asfeature(self, solution):
|
|
2416
2489
|
# {
|
|
2417
2490
|
candidate = [var for var in self.param.asvarnames if var not in solution['asvars']]
|
|
2491
|
+
blocked = self._get_excluded_by_mutual_group(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2492
|
+
candidate = [v for v in candidate if v not in blocked]
|
|
2418
2493
|
if len(candidate) > 0:
|
|
2419
2494
|
new_asvar = self.random_choice(candidate)
|
|
2420
2495
|
solution = self.add_asvar(new_asvar, solution)
|
|
@@ -2488,6 +2563,8 @@ class Search():
|
|
|
2488
2563
|
def perturb_add_isfeature(self, solution):
|
|
2489
2564
|
# {
|
|
2490
2565
|
candidate = [var for var in self.param.isvarnames if var not in solution['isvars']]
|
|
2566
|
+
blocked = self._get_excluded_by_mutual_group(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2567
|
+
candidate = [v for v in candidate if v not in blocked]
|
|
2491
2568
|
if len(candidate) > 0:
|
|
2492
2569
|
# {
|
|
2493
2570
|
add_isvar = self.random_choice(candidate)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.128
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.128
|
|
4
4
|
Summary: A Python package for econometric models driven by search
|
|
5
5
|
Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Author-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
7
7
|
Project-URL: Homepage, https://pypi.org/project/SearchLibrium/
|
|
8
|
+
Project-URL: Repository, https://github.com/zahern/HypothesisX
|
|
9
|
+
Project-URL: Issues, https://github.com/zahern/HypothesisX/issues
|
|
10
|
+
Project-URL: Documentation, https://pypi.org/project/SearchLibrium/
|
|
8
11
|
Keywords: econometric models,search,discrete choice,logit,probit
|
|
9
12
|
Classifier: Development Status :: 4 - Beta
|
|
10
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -41,8 +44,9 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
41
44
|
[](https://pypi.org/project/SearchLibrium/)
|
|
42
45
|
[](LICENSE)
|
|
43
46
|
[](https://github.com/zahern/HypothesisX/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/zahern/HypothesisX)
|
|
44
48
|
|
|
45
|
-
**Automated discrete choice model search powered by Simulated Annealing, Harmony Search, and JAX-accelerated MLE.**
|
|
49
|
+
**Automated discrete choice model search powered by Simulated Annealing, Bandit-guided SA, Harmony Search, SA+PBIL, HS+PBIL, Parallel SA, and JAX-accelerated MLE.**
|
|
46
50
|
|
|
47
51
|
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).
|
|
48
52
|
|
|
@@ -122,16 +126,19 @@ A **run dashboard** is printed automatically at the end of every search, showing
|
|
|
122
126
|
|
|
123
127
|
## How the search works
|
|
124
128
|
|
|
125
|
-
The search uses **
|
|
129
|
+
The search uses **metaheuristic optimisation** (SA, BanditSA, Harmony Search,
|
|
130
|
+
SA+PBIL, HS+PBIL, or parallel variants) to explore the space of model
|
|
131
|
+
specifications:
|
|
126
132
|
|
|
127
133
|
```text
|
|
128
134
|
generate starting solution
|
|
129
|
-
└─ for each
|
|
135
|
+
└─ for each iteration / temperature step / improvisation
|
|
130
136
|
└─ perturb current specification → guaranteed distinct from current
|
|
131
137
|
├─ fit model with JAX-accelerated MLE
|
|
132
138
|
├─ run backward elimination (remove insignificant vars, refit)
|
|
133
|
-
├─ accept if converged +
|
|
134
|
-
|
|
139
|
+
├─ accept if converged + acceptance criterion satisfied
|
|
140
|
+
│ (Metropolis, dominance, or pitch adjustment)
|
|
141
|
+
└─ update best solution / harmony memory
|
|
135
142
|
print dashboard
|
|
136
143
|
```
|
|
137
144
|
|
|
@@ -433,27 +440,42 @@ python -m SearchLibrium --test_search_nest # run nested logit search
|
|
|
433
440
|
|
|
434
441
|
## Search algorithms
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+
All algorithms share a **consistent interface**. The library auto-estimates
|
|
444
|
+
hyperparameters from problem size when you omit `ctrl`:
|
|
437
445
|
|
|
438
446
|
```python
|
|
439
|
-
from SearchLibrium import call_search,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
#
|
|
446
|
-
best = call_search(params,
|
|
447
|
-
best = call_search(params, algorithm='
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# Inspect auto-estimated
|
|
447
|
+
from SearchLibrium import (call_search, call_siman, call_harmony, call_sapbil,
|
|
448
|
+
call_banditsa, call_harmony_pbil,
|
|
449
|
+
call_parsa, call_parcopsa, estimate_ctrl)
|
|
450
|
+
|
|
451
|
+
# Unified entry point — auto hyperparameters (recommended)
|
|
452
|
+
best = call_search(params) # SA (default)
|
|
453
|
+
best = call_search(params, algorithm='hs') # Harmony Search
|
|
454
|
+
best = call_search(params, algorithm='sapbil') # SA + PBIL
|
|
455
|
+
best = call_search(params, algorithm='banditsa') # Bandit-guided SA
|
|
456
|
+
best = call_search(params, algorithm='hspbil') # HS + PBIL
|
|
457
|
+
|
|
458
|
+
# Inspect auto-estimated hyperparameters before running
|
|
451
459
|
ctrl = estimate_ctrl(params, algorithm='sa')
|
|
452
460
|
print(ctrl)
|
|
453
461
|
```
|
|
454
462
|
|
|
463
|
+
### Algorithm reference
|
|
464
|
+
|
|
465
|
+
| Function | Description | Population-based | Key idea |
|
|
466
|
+
| -------- | ----------- | :---: | -------- |
|
|
467
|
+
| `call_siman` | Simulated Annealing | | Metropolis acceptance with temperature cooling |
|
|
468
|
+
| `call_banditsa` | Bandit-guided SA | | Thompson Sampling to choose perturbation actions |
|
|
469
|
+
| `call_sapbil` | SA + PBIL | ✓ | Probability matrix learns from accepted solutions to bias perturbations |
|
|
470
|
+
| `call_harmony` | Harmony Search | ✓ | Memory-based improvisation with pitch adjustment |
|
|
471
|
+
| `call_harmony_pbil` | HS + PBIL | ✓ | Harmony Search with PBIL-guided pitch perturbation |
|
|
472
|
+
| `call_parsa` | Parallel SA | | Multiple independent SA solvers running in parallel |
|
|
473
|
+
| `call_parcopsa` | Parallel Cooperative SA | ✓ | Parallel SA with periodic best-solution sharing |
|
|
474
|
+
|
|
455
475
|
### Simulated Annealing (`call_siman` / `algorithm='sa'`)
|
|
456
476
|
|
|
477
|
+
Control tuple: `(tI, tF, max_temp_steps, max_iter)`
|
|
478
|
+
|
|
457
479
|
| Parameter | Meaning |
|
|
458
480
|
| --------- | ------- |
|
|
459
481
|
| `tI` | Initial temperature — higher → more exploration |
|
|
@@ -467,12 +489,14 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
467
489
|
|
|
468
490
|
### Harmony Search (`call_harmony` / `algorithm='hs'`)
|
|
469
491
|
|
|
492
|
+
Control tuple: `(max_mem, maxiter, max_harm, min_harm, max_pitch, min_pitch)`
|
|
493
|
+
|
|
470
494
|
| Parameter | Meaning |
|
|
471
495
|
| --------- | ------- |
|
|
472
496
|
| `max_mem` | Harmony memory size (population) |
|
|
473
497
|
| `maxiter` | Improvisation iterations |
|
|
474
|
-
| `max_harm` | Max harmony consideration rate |
|
|
475
|
-
| `min_harm` | Min harmony consideration rate |
|
|
498
|
+
| `max_harm` | Max harmony memory consideration rate |
|
|
499
|
+
| `min_harm` | Min harmony memory consideration rate |
|
|
476
500
|
| `max_pitch` | Max pitch adjustment rate |
|
|
477
501
|
| `min_pitch` | Min pitch adjustment rate |
|
|
478
502
|
|
|
@@ -480,6 +504,44 @@ best = call_siman(params, ctrl=(1000, 0.001, 100, 20), id_num=1)
|
|
|
480
504
|
best = call_harmony(params, ctrl=(20, 400, 0.9, 0.6, 0.85, 0.3), id_num=1)
|
|
481
505
|
```
|
|
482
506
|
|
|
507
|
+
### SA + PBIL (`call_sapbil` / `algorithm='sapbil'`)
|
|
508
|
+
|
|
509
|
+
Population-Based Incremental Learning coupled with Simulated Annealing.
|
|
510
|
+
Maintains a probability matrix over the search decisions (inclusion,
|
|
511
|
+
randomness, distribution, correlation, Box-Cox) and updates it from
|
|
512
|
+
accepted solutions using a temperature-coupled learning rate.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
best = call_sapbil(params, id_num=1)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### HS + PBIL (`call_harmony_pbil` / `algorithm='hspbil'`)
|
|
519
|
+
|
|
520
|
+
Harmony Search with PBIL-guided pitch adjustment. Instead of uniform-random
|
|
521
|
+
perturbations during pitch adjustment, the PBIL probability matrix guides
|
|
522
|
+
add/remove decisions based on learned probabilities.
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
best = call_harmony_pbil(params, id_num=1)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Bandit-guided SA (`call_banditsa` / `algorithm='banditsa'`)
|
|
529
|
+
|
|
530
|
+
Uses Thompson Sampling over a multi-armed bandit of perturbation actions
|
|
531
|
+
(add asvar, remove asvar, change distribution, ...). Actions that
|
|
532
|
+
historically led to accepted solutions are more likely to be chosen again.
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
best = call_banditsa(params, id_num=1)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Parallel SA variants
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
best = call_parsa(params, nthrds=4) # 4 independent SA solvers
|
|
542
|
+
best = call_parcopsa(params, nthrds=8) # 8 SA solvers sharing best periodically
|
|
543
|
+
```
|
|
544
|
+
|
|
483
545
|
### Auto hyperparameter estimation
|
|
484
546
|
|
|
485
547
|
If `ctrl` is omitted, the library estimates appropriate defaults from the
|
|
@@ -489,8 +551,6 @@ problem complexity (`n_vars × n_alts × n_models`, doubled for random params):
|
|
|
489
551
|
from SearchLibrium import estimate_ctrl
|
|
490
552
|
ctrl_sa = estimate_ctrl(params, algorithm='sa')
|
|
491
553
|
ctrl_hs = estimate_ctrl(params, algorithm='hs')
|
|
492
|
-
print('SA ctrl:', ctrl_sa)
|
|
493
|
-
print('HS ctrl:', ctrl_hs)
|
|
494
554
|
```
|
|
495
555
|
|
|
496
556
|
Complexity buckets:
|
|
@@ -504,6 +564,64 @@ Complexity buckets:
|
|
|
504
564
|
|
|
505
565
|
|
|
506
566
|
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Constraints
|
|
570
|
+
|
|
571
|
+
Use `ConstraintBuilder` to enforce model structure rules during the search:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from SearchLibrium import Parameters, ConstraintBuilder, call_siman
|
|
575
|
+
|
|
576
|
+
constraints = ConstraintBuilder()
|
|
577
|
+
|
|
578
|
+
# ── Inclusion / exclusion ──
|
|
579
|
+
constraints.force_include('TIME', 'COST') # must always appear
|
|
580
|
+
constraints.never_include('ID', 'DUMMY') # must never appear
|
|
581
|
+
|
|
582
|
+
# ── Mutually exclusive groups ──
|
|
583
|
+
# At most ONE variable from each group may appear in any solution.
|
|
584
|
+
# The search will never add a partner if a group member is already
|
|
585
|
+
# present, and will remove extras if they slip through.
|
|
586
|
+
constraints.mutually_exclusive('SPEED', 'TIME') # speed OR time, never both
|
|
587
|
+
constraints.mutually_exclusive('LOG_INCOME', 'INCOME_DUMMY') # independent group
|
|
588
|
+
|
|
589
|
+
# ── Minimum behavioural content ──
|
|
590
|
+
# Require at least N variables from a pool without locking in which ones.
|
|
591
|
+
constraints.min_behavioral(2, 'PRICE', 'BIKELANE', 'DIST6', 'RECRE')
|
|
592
|
+
|
|
593
|
+
# ── Random-parameter rules ──
|
|
594
|
+
constraints.force_random('TIME', distribution='n') # must have random parameter
|
|
595
|
+
constraints.exclude_random('HEADWAY') # must never be random
|
|
596
|
+
|
|
597
|
+
# ── Attach constraints to the Parameters object ──
|
|
598
|
+
params = Parameters(
|
|
599
|
+
criterions = [("bic", -1)],
|
|
600
|
+
df = df,
|
|
601
|
+
varnames = varnames,
|
|
602
|
+
asvarnames = varnames,
|
|
603
|
+
choice_set = choice_set,
|
|
604
|
+
choices = df["CHOICE"].values,
|
|
605
|
+
alt_var = df["alt"].values,
|
|
606
|
+
choice_id = df["custom_id"].values,
|
|
607
|
+
ind_id = df["ID"].values,
|
|
608
|
+
base_alt = "SM",
|
|
609
|
+
models = ["multinomial", "mixed_logit"],
|
|
610
|
+
allow_random = True,
|
|
611
|
+
p_val = 0.05,
|
|
612
|
+
pre_spec_constraints = constraints.dict(), # ◄ attach here
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
best = call_siman(params, id_num=1)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The constraint enforcement happens at every stage:
|
|
619
|
+
- **Solution generation** — filtered so violations are never created
|
|
620
|
+
- **Perturbation / mutation** — blocked from adding violating variables
|
|
621
|
+
- **Post-evaluation cleanup** — extra members are removed if somehow present
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
507
625
|
## License
|
|
508
626
|
|
|
509
627
|
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.126
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.126 → searchlibrium-0.0.128}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|