SearchLibrium 0.0.261__tar.gz → 0.0.263__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.261/src/SearchLibrium.egg-info → searchlibrium-0.0.263}/PKG-INFO +1 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/pyproject.toml +1 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/Halton.py +8 -4
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/MixedLogit.py +291 -27
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/destination_distances.py +34 -11
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/latent_class.py +21 -4
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/mixed_logit.py +10 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/multinomial_logit.py +4 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/ordered_logit.py +17 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/test_lc_de.py +1 -0
- searchlibrium-0.0.263/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
- searchlibrium-0.0.261/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/LICENSE +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/MANIFEST.in +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/README.md +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/setup.cfg +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/BEHier.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/MixedLogitGSE.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/abm.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/accessibility.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/coupled_solver.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/harmony.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/jax_utils.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/larch_compat.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/larch_models.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/latent_spec.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/logistic_regression.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/mh_choice_estimator.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/numba_engine.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/numba_mxl.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/numba_rrm.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/regularization.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/search.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/skims.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/sparseea_agds.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/zero_inflated_ordered_probit.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/top_level.txt +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_destination_distances.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_jax_mask_equivalence.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_larch_integration.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_lc_smart_init.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_mh_choice_estimator.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_mxl_random_starts.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_per_alternative_random.py +0 -0
- {searchlibrium-0.0.261 → searchlibrium-0.0.263}/tests/test_zero_inflated_ordered_probit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.263
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
5
|
Author: Zeke Ahern, Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Maintainer-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
@@ -132,7 +132,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
|
132
132
|
realpython = "SearchLibrium.__main__:main"
|
|
133
133
|
|
|
134
134
|
[tool.bumpver]
|
|
135
|
-
current_version = "0.0.
|
|
135
|
+
current_version = "0.0.263"
|
|
136
136
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
137
137
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
138
138
|
commit = true
|
|
@@ -56,12 +56,14 @@ class Halton:
|
|
|
56
56
|
Set use_sobol=True to use scrambled Sobol sequences instead.
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
|
-
def __init__(self, primes=None, drop=100, shuffled=False,
|
|
59
|
+
def __init__(self, primes=None, drop=100, shuffled=False,
|
|
60
|
+
antithetic=False, use_sobol=False, seed=None):
|
|
60
61
|
self.primes = primes
|
|
61
62
|
self.drop = drop
|
|
62
63
|
self.shuffled = shuffled
|
|
63
64
|
self.antithetic = antithetic
|
|
64
65
|
self.use_sobol = use_sobol
|
|
66
|
+
self.seed = seed
|
|
65
67
|
|
|
66
68
|
# Default primes for Halton sequence
|
|
67
69
|
if self.primes is None:
|
|
@@ -81,7 +83,9 @@ class Halton:
|
|
|
81
83
|
if self.use_sobol:
|
|
82
84
|
# Use Sobol sequence (newer method)
|
|
83
85
|
base = n_draws // 2 if self.antithetic else n_draws
|
|
84
|
-
draws = _sobol_generate(
|
|
86
|
+
draws = _sobol_generate(
|
|
87
|
+
sample_size, base, n_vars, shuffled=self.shuffled,
|
|
88
|
+
seed=self.seed)
|
|
85
89
|
if self.antithetic:
|
|
86
90
|
draws = np.concatenate([draws, 1.0 - draws], axis=2)
|
|
87
91
|
else:
|
|
@@ -110,7 +114,7 @@ class Halton:
|
|
|
110
114
|
return draws
|
|
111
115
|
|
|
112
116
|
|
|
113
|
-
def _sobol_generate(sample_size, n_draws, n_vars, shuffled=False):
|
|
117
|
+
def _sobol_generate(sample_size, n_draws, n_vars, shuffled=False, seed=None):
|
|
114
118
|
"""Generate scrambled Sobol (0,1) draws.
|
|
115
119
|
|
|
116
120
|
Returns
|
|
@@ -123,7 +127,7 @@ def _sobol_generate(sample_size, n_draws, n_vars, shuffled=False):
|
|
|
123
127
|
p2 = 1
|
|
124
128
|
while p2 < total:
|
|
125
129
|
p2 <<= 1
|
|
126
|
-
sobol = Sobol(d=n_vars, scramble=True)
|
|
130
|
+
sobol = Sobol(d=n_vars, scramble=True, seed=seed)
|
|
127
131
|
flat = sobol.random(p2)[:total] # (total, n_vars)
|
|
128
132
|
draws = flat.reshape(sample_size, n_draws, n_vars).transpose(0, 2, 1)
|
|
129
133
|
else:
|
|
@@ -122,10 +122,19 @@ def _panel_weighted_shares(prob_np, panel_info):
|
|
|
122
122
|
return ind, np.mean(ind, axis=0)
|
|
123
123
|
|
|
124
124
|
class MixedLogit(DiscreteChoiceModel):
|
|
125
|
-
def __init__(self, halton_opts=None, distributions=['n', 'ln', 'nln', 't', 'tn', 'u'], _jax=None
|
|
125
|
+
def __init__(self, halton_opts=None, distributions=['n', 'ln', 'nln', 't', 'tn', 'u'], _jax=None,
|
|
126
|
+
save_prob_full=None):
|
|
126
127
|
super().__init__(_jax)
|
|
127
128
|
self.descr = "MXL"
|
|
128
129
|
self.halton_opts = halton_opts
|
|
130
|
+
# MEMORY: prob_full is the full (N, P, J, R) probability tensor built in
|
|
131
|
+
# post-process for the summary/plots. For a 13k-case/20-alt/1000-draw
|
|
132
|
+
# model that is ~2 GB, and it is never read during estimation, so it is
|
|
133
|
+
# now opt-in. Pass save_prob_full=True (or set
|
|
134
|
+
# SEARCHLIBRIUM_SAVE_PROB_FULL=1) to retain the previous behaviour.
|
|
135
|
+
if save_prob_full is None:
|
|
136
|
+
save_prob_full = bool(os.environ.get("SEARCHLIBRIUM_SAVE_PROB_FULL", ""))
|
|
137
|
+
self.save_prob_full = bool(save_prob_full)
|
|
129
138
|
self.draws_generator = Draws(k=len(distributions), halton_opts=halton_opts, rvdist=distributions)
|
|
130
139
|
self.random_parameters = RandomParameters(distributions or [])
|
|
131
140
|
self.softmax_r = True
|
|
@@ -225,7 +234,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
225
234
|
randvars=None, panels=None, base_alt=None, fit_intercept=False,
|
|
226
235
|
init_coeff=None, maxiter=1000, correlated_vars=None,
|
|
227
236
|
n_draws=1000, halton=True, minimise_func=None,
|
|
228
|
-
batch_size=None,
|
|
237
|
+
batch_size=None, person_batch_size=None, halton_opts=None,
|
|
238
|
+
draw_seed=0, ftol=1e-12,
|
|
229
239
|
gtol=1e-6, return_hess=True, return_grad=True, method="slsqp",
|
|
230
240
|
save_fitted_params=True, mnl_init=True,
|
|
231
241
|
de_init=False, de_popsize=4, de_maxiter=3, de_tol=0.5,
|
|
@@ -301,7 +311,15 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
301
311
|
self.jac = self.return_grad # scipy optimize parameter
|
|
302
312
|
self.n_draws = n_draws
|
|
303
313
|
self.batch_size = min(n_draws, batch_size) if batch_size is not None else n_draws
|
|
304
|
-
|
|
314
|
+
if person_batch_size is None:
|
|
315
|
+
self.person_batch_size = None
|
|
316
|
+
else:
|
|
317
|
+
self.person_batch_size = max(1, int(person_batch_size))
|
|
318
|
+
self._person_batch_active = False
|
|
319
|
+
self.draw_seed = int(draw_seed)
|
|
320
|
+
self.halton_opts = dict(halton_opts or {})
|
|
321
|
+
if self.person_batch_size is not None:
|
|
322
|
+
self.halton_opts.setdefault("seed", self.draw_seed)
|
|
305
323
|
|
|
306
324
|
# CRITICAL FIX: Recreate draws_generator with proper halton_opts AND rvdist
|
|
307
325
|
# The __init__ creates it with halton_opts=None, but setup() gets the actual options
|
|
@@ -309,7 +327,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
309
327
|
# AND to pass the correct random variable distributions (e.g., 'ln' for lognormal)
|
|
310
328
|
# IMPORTANT: Order rvdist by varnames order, not dictionary order
|
|
311
329
|
rvdist = [randvars[var] for var in varnames if var in randvars] if randvars else []
|
|
312
|
-
self.draws_generator = Draws(
|
|
330
|
+
self.draws_generator = Draws(
|
|
331
|
+
k=len(randvars or {}), halton_opts=self.halton_opts, rvdist=rvdist)
|
|
313
332
|
|
|
314
333
|
self.randvarsdict = randvars # random variables not transformed
|
|
315
334
|
|
|
@@ -812,7 +831,16 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
812
831
|
def fit(self):
|
|
813
832
|
# {
|
|
814
833
|
# Generate draws:
|
|
815
|
-
|
|
834
|
+
streaming_people = (
|
|
835
|
+
self.person_batch_size is not None
|
|
836
|
+
and self.person_batch_size < self.N
|
|
837
|
+
)
|
|
838
|
+
if streaming_people:
|
|
839
|
+
# Person chunks regenerate deterministic draws during each
|
|
840
|
+
# objective evaluation, avoiding an (N, Kr, R) draw bank.
|
|
841
|
+
draws, drawstrans = None, None
|
|
842
|
+
else:
|
|
843
|
+
draws, drawstrans = self.generate_draws(self.N, self.n_draws, self.halton)
|
|
816
844
|
self.draws, self.drawstrans = draws, drawstrans # Record generated values
|
|
817
845
|
# Explicit caller-supplied starts are respected verbatim (no ASC
|
|
818
846
|
# share seeding); the MNL block below may still overwrite
|
|
@@ -829,9 +857,15 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
829
857
|
from numba_engine import effective_engine as _mx_eff_eng
|
|
830
858
|
except ImportError:
|
|
831
859
|
from .numba_engine import effective_engine as _mx_eff_eng
|
|
832
|
-
|
|
860
|
+
# Numba is the default CPU engine. JAX must be selected
|
|
861
|
+
# explicitly; unsupported specifications fall back to SciPy.
|
|
862
|
+
use_numba = (_mx_eff_eng(self) != 'jax')
|
|
833
863
|
except Exception:
|
|
834
|
-
use_numba = (getattr(self, 'engine', None)
|
|
864
|
+
use_numba = (getattr(self, 'engine', None) != 'jax')
|
|
865
|
+
if streaming_people:
|
|
866
|
+
# The streaming path regenerates person-chunk draws and is not
|
|
867
|
+
# compatible with the current full-array Numba adapter.
|
|
868
|
+
use_numba = False
|
|
835
869
|
|
|
836
870
|
# 2x Kftrans - mean and lambda, 3x Krtrans - mean, s.d., lambda
|
|
837
871
|
# Kchol, Kbw - relate to random variables, non-transformed
|
|
@@ -1052,11 +1086,12 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1052
1086
|
if _v: print(f"[MXL] Differential evolution initialization failed: {e}")
|
|
1053
1087
|
|
|
1054
1088
|
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
1055
|
-
if dev.using_gpu: # {
|
|
1089
|
+
if dev.using_gpu and draws is not None: # {
|
|
1056
1090
|
self.X, self.y = dev.convert_array_gpu(self.X), dev.convert_array_gpu(self.y)
|
|
1057
1091
|
self.panel_info = dev.convert_array_gpu(self.panel_info)
|
|
1058
1092
|
draws = dev.convert_array_gpu(draws)
|
|
1059
|
-
drawstrans
|
|
1093
|
+
if drawstrans is not None:
|
|
1094
|
+
drawstrans = dev.convert_array_gpu(drawstrans)
|
|
1060
1095
|
self.weights = dev.convert_array_gpu(self.weights) if self.weights is not None else None
|
|
1061
1096
|
self.avail = dev.convert_array_gpu(self.avail) if self.avail is not None else None
|
|
1062
1097
|
# }
|
|
@@ -1104,7 +1139,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1104
1139
|
# SOLVE OPTIMISATION PROBLEM - COMPUTATIONALLY TIME CONSUMING!
|
|
1105
1140
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1106
1141
|
# ---- JAX fast path ----
|
|
1107
|
-
if (getattr(self, '_jax', False) and not
|
|
1142
|
+
if (getattr(self, '_jax', False) and not streaming_people
|
|
1143
|
+
and not use_numba
|
|
1108
1144
|
and self.minimise_func is None):
|
|
1109
1145
|
jax_result = self.optimize_jax(betas, draws, drawstrans)
|
|
1110
1146
|
if jax_result is not None:
|
|
@@ -1173,7 +1209,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1173
1209
|
self.choice_pred_prob = np.mean(p, axis=3)
|
|
1174
1210
|
self.ind_pred_prob, self.pred_prob = _panel_weighted_shares(
|
|
1175
1211
|
self.choice_pred_prob, self.panel_info)
|
|
1176
|
-
self.
|
|
1212
|
+
if self.save_prob_full:
|
|
1213
|
+
self.prob_full = p
|
|
1177
1214
|
self._refresh_corr_from_varlist()
|
|
1178
1215
|
self.post_process(jax_result, self.Xnames, self.N)
|
|
1179
1216
|
return
|
|
@@ -1234,6 +1271,16 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1234
1271
|
except Exception:
|
|
1235
1272
|
pass
|
|
1236
1273
|
|
|
1274
|
+
if draws is None:
|
|
1275
|
+
# Estimation stayed streamed; materialize full draws only for the
|
|
1276
|
+
# final fitted-parameter and prediction summaries.
|
|
1277
|
+
draws, drawstrans = self.generate_draws(
|
|
1278
|
+
self.N, self.n_draws, self.halton)
|
|
1279
|
+
self.draws, self.drawstrans = draws, drawstrans
|
|
1280
|
+
self.p = self.compute_probabilities(
|
|
1281
|
+
result['x'], self.X, self.panel_info, draws, drawstrans,
|
|
1282
|
+
self.avail, self.var_list, self.chol_mat)
|
|
1283
|
+
|
|
1237
1284
|
if self.save_fitted_params:
|
|
1238
1285
|
self.compute_fitted_params(self.y, self.p, self.panel_info, self.Br)
|
|
1239
1286
|
|
|
@@ -1251,7 +1298,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1251
1298
|
self.ind_pred_prob, self.pred_prob = _panel_weighted_shares(
|
|
1252
1299
|
self.choice_pred_prob, self.panel_info)
|
|
1253
1300
|
|
|
1254
|
-
self.
|
|
1301
|
+
if self.save_prob_full:
|
|
1302
|
+
self.prob_full = p
|
|
1255
1303
|
# }
|
|
1256
1304
|
self._refresh_corr_from_varlist()
|
|
1257
1305
|
self.post_process(result, self.Xnames, self.N)
|
|
@@ -1413,7 +1461,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1413
1461
|
|
|
1414
1462
|
sim_p = jnp.mean(pch, axis=1) # (N,)
|
|
1415
1463
|
sim_p = jnp.clip(sim_p, 1e-300, None)
|
|
1416
|
-
|
|
1464
|
+
valid_person = jnp.any(panel_info_jax > 0, axis=1)
|
|
1465
|
+
ll = -jnp.sum(jnp.where(valid_person, jnp.log(sim_p), 0.0))
|
|
1417
1466
|
# Tiny L2 penalty — makes the Hessian diagonally dominant so it
|
|
1418
1467
|
# never becomes singular, even with near-constant variables or
|
|
1419
1468
|
# simulation noise in the mixed-logit probability integration.
|
|
@@ -1499,7 +1548,11 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1499
1548
|
# process; during a heterogeneous GA search that is one retained model
|
|
1500
1549
|
# per distinct parameter-count combination.
|
|
1501
1550
|
# ------------------------------------------------------------------
|
|
1502
|
-
|
|
1551
|
+
# MEMORY: cache size is in *live models*, each of which can pin its X/y
|
|
1552
|
+
# arrays and a compiled XLA executable. The old default of 16 was tuned for
|
|
1553
|
+
# speed; 4 keeps most of the reuse benefit while capping the worst case.
|
|
1554
|
+
# Override with SEARCHLIBRIUM_JIT_CACHE_MAXSIZE (0 disables the cache).
|
|
1555
|
+
_JIT_CACHE_MAXSIZE = max(0, int(os.environ.get("SEARCHLIBRIUM_JIT_CACHE_MAXSIZE", "4")))
|
|
1503
1556
|
_mxl_jit_cache: "OrderedDict[tuple, tuple]" = OrderedDict()
|
|
1504
1557
|
|
|
1505
1558
|
@staticmethod
|
|
@@ -1562,6 +1615,17 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1562
1615
|
pass
|
|
1563
1616
|
gc.collect()
|
|
1564
1617
|
|
|
1618
|
+
def release_memory(self):
|
|
1619
|
+
"""Free per-fit caches and large post-process tensors this model holds.
|
|
1620
|
+
|
|
1621
|
+
Safe to call between metaheuristic candidates: it clears the process-
|
|
1622
|
+
wide JIT/XLA cache and drops ``prob_full`` unless it was explicitly
|
|
1623
|
+
saved. Fitted coefficients, standard errors and covariance are kept.
|
|
1624
|
+
"""
|
|
1625
|
+
if not getattr(self, "save_prob_full", False):
|
|
1626
|
+
self.prob_full = None
|
|
1627
|
+
type(self)._clear_jit_cache()
|
|
1628
|
+
|
|
1565
1629
|
def _run_jaxopt(self, betas, X_jax, y_jax, pi_jax, draws_jax,
|
|
1566
1630
|
_fn, _cache_key):
|
|
1567
1631
|
"""JAX likelihood + ``jaxopt.LBFGS`` solver (experiment).
|
|
@@ -1640,6 +1704,125 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1640
1704
|
print(f"[JAX MXL optimizer] jaxopt run failed ({_e}).")
|
|
1641
1705
|
return None
|
|
1642
1706
|
|
|
1707
|
+
def _optimize_jax_streamed(self, betas):
|
|
1708
|
+
"""Fit plain MXL with bounded person chunks and deterministic draws."""
|
|
1709
|
+
try:
|
|
1710
|
+
import os
|
|
1711
|
+
import jax
|
|
1712
|
+
import jax.numpy as jnp
|
|
1713
|
+
import numpy as _np
|
|
1714
|
+
from scipy.optimize import minimize as sp_min
|
|
1715
|
+
|
|
1716
|
+
chunk_size = int(self.person_batch_size)
|
|
1717
|
+
X_np = _np.asarray(self.X)
|
|
1718
|
+
y_np = _np.asarray(self.y)
|
|
1719
|
+
if y_np.ndim > 3:
|
|
1720
|
+
y_np = y_np[..., 0]
|
|
1721
|
+
pi_np = _np.asarray(self.panel_info)
|
|
1722
|
+
N, P, J, _ = X_np.shape
|
|
1723
|
+
Kf, Kr = int(self.Kf), int(self.Kr)
|
|
1724
|
+
Kchol, Kbw = int(self.Kchol), int(self.Kbw)
|
|
1725
|
+
fxidx = jnp.asarray(self.fxidx, dtype=bool)
|
|
1726
|
+
rvidx = jnp.asarray(self.rvidx, dtype=bool)
|
|
1727
|
+
rvdist_names = [d for d in self.rvdist if d is not False]
|
|
1728
|
+
extra = self._jax_negloglik_extra_kwargs()
|
|
1729
|
+
|
|
1730
|
+
def _pad(value, shape):
|
|
1731
|
+
result = _np.zeros(shape, dtype=_np.float64)
|
|
1732
|
+
result[tuple(slice(0, n) for n in value.shape)] = value
|
|
1733
|
+
return result
|
|
1734
|
+
|
|
1735
|
+
def _chunk_inputs(chunk_index):
|
|
1736
|
+
start = chunk_index * chunk_size
|
|
1737
|
+
end = min(start + chunk_size, N)
|
|
1738
|
+
n = end - start
|
|
1739
|
+
X_chunk = _pad(X_np[start:end], (chunk_size, P, J, X_np.shape[3]))
|
|
1740
|
+
y_chunk = _pad(y_np[start:end], (chunk_size, P, J))
|
|
1741
|
+
pi_chunk = _pad(pi_np[start:end], (chunk_size, P))
|
|
1742
|
+
|
|
1743
|
+
state = _np.random.get_state()
|
|
1744
|
+
try:
|
|
1745
|
+
_np.random.seed(self.draw_seed + chunk_index)
|
|
1746
|
+
if self.halton and hasattr(self.draws_generator, "halton"):
|
|
1747
|
+
self.draws_generator.halton.seed = (
|
|
1748
|
+
self.draw_seed + chunk_index)
|
|
1749
|
+
draws_chunk, _ = self.generate_draws(
|
|
1750
|
+
n, self.n_draws, self.halton)
|
|
1751
|
+
finally:
|
|
1752
|
+
_np.random.set_state(state)
|
|
1753
|
+
draws_chunk = _pad(
|
|
1754
|
+
_np.asarray(draws_chunk),
|
|
1755
|
+
(chunk_size, int(self.Kr), self.n_draws))
|
|
1756
|
+
return (
|
|
1757
|
+
jnp.asarray(X_chunk),
|
|
1758
|
+
jnp.asarray(y_chunk),
|
|
1759
|
+
jnp.asarray(pi_chunk),
|
|
1760
|
+
jnp.asarray(draws_chunk),
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
def _chunk_objective(b, X_j, y_j, pi_j, draws_j):
|
|
1764
|
+
return self._jax_mxl_negloglik(
|
|
1765
|
+
b, X_j, y_j, pi_j, draws_j,
|
|
1766
|
+
fxidx, rvidx, Kf, Kr, Kchol, Kbw,
|
|
1767
|
+
rvdist_names, int(self.correlationLength),
|
|
1768
|
+
**extra)
|
|
1769
|
+
|
|
1770
|
+
compiled = jax.jit(jax.value_and_grad(_chunk_objective))
|
|
1771
|
+
chunk_args = [
|
|
1772
|
+
_chunk_inputs(i)
|
|
1773
|
+
for i in range((N + chunk_size - 1) // chunk_size)
|
|
1774
|
+
]
|
|
1775
|
+
valid_counts = [
|
|
1776
|
+
min(chunk_size, N - i * chunk_size)
|
|
1777
|
+
for i in range(len(chunk_args))
|
|
1778
|
+
]
|
|
1779
|
+
|
|
1780
|
+
def _objective(betas_np):
|
|
1781
|
+
b = jnp.asarray(betas_np, dtype=jnp.float64)
|
|
1782
|
+
value = 0.0
|
|
1783
|
+
gradient = jnp.zeros_like(b)
|
|
1784
|
+
for args, valid in zip(chunk_args, valid_counts):
|
|
1785
|
+
chunk_value, chunk_gradient = compiled(b, *args)
|
|
1786
|
+
# Padded people contribute zero because their panel mask
|
|
1787
|
+
# is zero; valid is retained for clarity and future
|
|
1788
|
+
# weighted-panel support.
|
|
1789
|
+
del valid
|
|
1790
|
+
value = value + chunk_value
|
|
1791
|
+
gradient = gradient + chunk_gradient
|
|
1792
|
+
value, gradient = jax.block_until_ready((value, gradient))
|
|
1793
|
+
return float(value), _np.asarray(gradient, dtype=_np.float64)
|
|
1794
|
+
|
|
1795
|
+
options = {'maxiter': self.maxiter, 'disp': False}
|
|
1796
|
+
if self.method in ('bfgs', 'l-bfgs-b'):
|
|
1797
|
+
options['gtol'] = self.gtol
|
|
1798
|
+
options['ftol'] = self.ftol
|
|
1799
|
+
else:
|
|
1800
|
+
options['ftol'] = self.ftol
|
|
1801
|
+
result = sp_min(
|
|
1802
|
+
_objective, betas, jac=True, method=self.method,
|
|
1803
|
+
options=options)
|
|
1804
|
+
|
|
1805
|
+
if self.return_hess:
|
|
1806
|
+
def _hessian_objective(b):
|
|
1807
|
+
value = 0.0
|
|
1808
|
+
for args in chunk_args:
|
|
1809
|
+
value = value + _chunk_objective(b, *args)
|
|
1810
|
+
return value
|
|
1811
|
+
|
|
1812
|
+
hessian_fn = jax.jit(jax.hessian(_hessian_objective))
|
|
1813
|
+
hessian = _np.asarray(
|
|
1814
|
+
hessian_fn(jnp.asarray(result['x'], dtype=jnp.float64)),
|
|
1815
|
+
dtype=float)
|
|
1816
|
+
try:
|
|
1817
|
+
result['hess_inv'] = _np.linalg.inv(hessian)
|
|
1818
|
+
except _np.linalg.LinAlgError:
|
|
1819
|
+
result['hess_inv'] = _np.linalg.pinv(hessian)
|
|
1820
|
+
return result
|
|
1821
|
+
except Exception as exc:
|
|
1822
|
+
if not _SL_QUIET:
|
|
1823
|
+
print(f"[JAX streamed MXL] falling back to scipy: {exc}")
|
|
1824
|
+
return None
|
|
1825
|
+
|
|
1643
1826
|
def optimize_jax(self, betas, draws, drawstrans):
|
|
1644
1827
|
"""JAX-accelerated optimisation for the Mixed Logit model.
|
|
1645
1828
|
|
|
@@ -1655,6 +1838,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1655
1838
|
print("[JAX MXL optimizer] heterogeneity detected; falling back to scipy.")
|
|
1656
1839
|
return None
|
|
1657
1840
|
|
|
1841
|
+
if (self.person_batch_size is not None
|
|
1842
|
+
and self.person_batch_size < self.N):
|
|
1843
|
+
return self._optimize_jax_streamed(betas)
|
|
1844
|
+
|
|
1658
1845
|
try:
|
|
1659
1846
|
import os
|
|
1660
1847
|
os.environ.setdefault("JAX_ENABLE_X64", "True")
|
|
@@ -1914,12 +2101,88 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1914
2101
|
print(f"[JAX MXL masked] falling back: {e}")
|
|
1915
2102
|
return None
|
|
1916
2103
|
|
|
2104
|
+
def _person_batch_slice(self, value, start, end):
|
|
2105
|
+
"""Slice an observation-level array while preserving scalar settings."""
|
|
2106
|
+
if value is None:
|
|
2107
|
+
return None
|
|
2108
|
+
try:
|
|
2109
|
+
if np.ndim(value) > 0 and np.shape(value)[0] == self.N:
|
|
2110
|
+
return value[start:end]
|
|
2111
|
+
except (TypeError, IndexError):
|
|
2112
|
+
pass
|
|
2113
|
+
return value
|
|
2114
|
+
|
|
2115
|
+
def _stream_person_batches(self, betas, X, y, panel_info, draws,
|
|
2116
|
+
drawstrans, weights, avail, batch_size):
|
|
2117
|
+
"""Evaluate the objective in bounded person chunks.
|
|
2118
|
+
|
|
2119
|
+
When ``draws`` is absent, each chunk receives deterministic random
|
|
2120
|
+
draws derived from ``draw_seed`` and its chunk index. This keeps every
|
|
2121
|
+
optimizer evaluation reproducible without retaining all draws.
|
|
2122
|
+
"""
|
|
2123
|
+
original_n = self.N
|
|
2124
|
+
total_value = 0.0
|
|
2125
|
+
total_gradient = None
|
|
2126
|
+
has_gradient = bool(self.return_grad)
|
|
2127
|
+
try:
|
|
2128
|
+
self._person_batch_active = True
|
|
2129
|
+
for chunk_index, start in enumerate(
|
|
2130
|
+
range(0, original_n, self.person_batch_size)):
|
|
2131
|
+
end = min(start + self.person_batch_size, original_n)
|
|
2132
|
+
self.N = end - start
|
|
2133
|
+
chunk_X = X[start:end]
|
|
2134
|
+
chunk_y = y[start:end]
|
|
2135
|
+
chunk_panel = panel_info[start:end]
|
|
2136
|
+
chunk_weights = self._person_batch_slice(weights, start, end)
|
|
2137
|
+
chunk_avail = self._person_batch_slice(avail, start, end)
|
|
2138
|
+
|
|
2139
|
+
if draws is None:
|
|
2140
|
+
rng_state = np.random.get_state()
|
|
2141
|
+
try:
|
|
2142
|
+
np.random.seed(self.draw_seed + chunk_index)
|
|
2143
|
+
if self.halton and hasattr(self.draws_generator, "halton"):
|
|
2144
|
+
self.draws_generator.halton.seed = (
|
|
2145
|
+
self.draw_seed + chunk_index)
|
|
2146
|
+
chunk_draws, chunk_drawstrans = self.generate_draws(
|
|
2147
|
+
self.N, self.n_draws, self.halton)
|
|
2148
|
+
finally:
|
|
2149
|
+
np.random.set_state(rng_state)
|
|
2150
|
+
else:
|
|
2151
|
+
chunk_draws = draws[start:end]
|
|
2152
|
+
chunk_drawstrans = (
|
|
2153
|
+
drawstrans[start:end] if drawstrans is not None
|
|
2154
|
+
else drawstrans
|
|
2155
|
+
)
|
|
2156
|
+
|
|
2157
|
+
result = self.get_loglik_gradient(
|
|
2158
|
+
betas, chunk_X, chunk_y, chunk_panel,
|
|
2159
|
+
chunk_draws, chunk_drawstrans, chunk_weights,
|
|
2160
|
+
chunk_avail, batch_size)
|
|
2161
|
+
total_value += float(result[0])
|
|
2162
|
+
if has_gradient and len(result) > 1:
|
|
2163
|
+
if total_gradient is None:
|
|
2164
|
+
total_gradient = np.zeros_like(result[1], dtype=float)
|
|
2165
|
+
total_gradient += np.asarray(result[1], dtype=float)
|
|
2166
|
+
finally:
|
|
2167
|
+
self.N = original_n
|
|
2168
|
+
self._person_batch_active = False
|
|
2169
|
+
|
|
2170
|
+
if has_gradient:
|
|
2171
|
+
return total_value, total_gradient
|
|
2172
|
+
return (total_value,)
|
|
2173
|
+
|
|
1917
2174
|
def get_loglik_gradient(self, betas, X, y, panel_info, draws,
|
|
1918
2175
|
drawstrans, weights, avail, batch_size):
|
|
1919
2176
|
# {
|
|
1920
2177
|
""" Fixed and random parameters are handled separately to
|
|
1921
2178
|
speed up the estimation and the results are concatenated.
|
|
1922
2179
|
"""
|
|
2180
|
+
if (not self._person_batch_active
|
|
2181
|
+
and self.person_batch_size is not None
|
|
2182
|
+
and self.person_batch_size < self.N):
|
|
2183
|
+
return self._stream_person_batches(
|
|
2184
|
+
betas, X, y, panel_info, draws, drawstrans,
|
|
2185
|
+
weights, avail, batch_size)
|
|
1923
2186
|
# Segregating initial values to fixed betas (Bf),
|
|
1924
2187
|
# random beta means (Br_b)
|
|
1925
2188
|
# for both non-transformed and transformed variables
|
|
@@ -1999,11 +2262,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
1999
2262
|
|
|
2000
2263
|
# _, gr_b, gr_w, pch = np.zeros((N, self.Kf)), np.zeros((N, self.Kr)), np.zeros((N, self.Kr)), [] # Batch data
|
|
2001
2264
|
|
|
2002
|
-
_ = np.zeros((N, self.Kf)) # CONCEPTUAL ERROR. WHY CREATE IF NO NAME?
|
|
2003
2265
|
gr_b = np.zeros((N, self.Kr))
|
|
2004
2266
|
gr_w = np.zeros((N, self.Kr))
|
|
2005
2267
|
g_all = np.zeros((N, len(betas)))
|
|
2006
|
-
|
|
2268
|
+
lik_sum = np.zeros(N, dtype=float)
|
|
2007
2269
|
for batch in range(n_batches):
|
|
2008
2270
|
# {
|
|
2009
2271
|
a = batch * batch_size
|
|
@@ -2025,6 +2287,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
2025
2287
|
|
|
2026
2288
|
# Thresholds to avoid divide by zero warnings
|
|
2027
2289
|
pch_batch = truncate_lower(pch_batch, min_comp_val)
|
|
2290
|
+
draw_count = b - a
|
|
2291
|
+
lik_sum += np.sum(pch_batch, axis=1)
|
|
2028
2292
|
|
|
2029
2293
|
# Observed probability minus predicted probability
|
|
2030
2294
|
ymp = y - p # (N, P, J, R)
|
|
@@ -2263,12 +2527,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
2263
2527
|
g, pch_batch = dev.to_cpu(g), dev.to_cpu(pch_batch)
|
|
2264
2528
|
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2265
2529
|
|
|
2266
|
-
|
|
2267
|
-
g_all += g
|
|
2530
|
+
g_all += g * draw_count
|
|
2268
2531
|
# }
|
|
2269
2532
|
|
|
2270
|
-
|
|
2271
|
-
lik = pch.mean(axis=1) # (N,)
|
|
2533
|
+
lik = lik_sum / float(self.n_draws) # (N,)
|
|
2272
2534
|
loglik = np.log(lik)
|
|
2273
2535
|
if weights is not None: loglik = loglik * weights
|
|
2274
2536
|
loglik = loglik.sum()
|
|
@@ -2296,7 +2558,7 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
2296
2558
|
g *= weights[:, 0][:, None] # Use only the first column and convert to column vector
|
|
2297
2559
|
# }
|
|
2298
2560
|
|
|
2299
|
-
g = np.sum(
|
|
2561
|
+
g = np.sum(g_all, axis=0) / float(self.n_draws) # (K, )
|
|
2300
2562
|
g = g - self.regularize_l1_grad(betas)
|
|
2301
2563
|
# L2 ridge penalty gradient: the objective subtracts
|
|
2302
2564
|
# reg_penalty * sum(betas^2) from the log-likelihood, so its
|
|
@@ -2645,14 +2907,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
2645
2907
|
|
|
2646
2908
|
# INITIALISE
|
|
2647
2909
|
XBf = np.zeros((self.N, self.P, self.J))
|
|
2648
|
-
XBr = np.zeros((self.N, self.P, self.J, self.batch_size)) # NOT USED?
|
|
2649
|
-
V = np.zeros((self.N, self.P, self.J, self.batch_size)) # NOT USED?
|
|
2650
2910
|
|
|
2651
2911
|
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2652
2912
|
if dev.using_gpu: # {
|
|
2653
2913
|
XBf = dev.convert_array_gpu(XBf)
|
|
2654
|
-
XBr = dev.convert_array_gpu(XBr)
|
|
2655
|
-
V = dev.convert_array_gpu(V)
|
|
2656
2914
|
# }
|
|
2657
2915
|
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2658
2916
|
|
|
@@ -2692,10 +2950,16 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
2692
2950
|
Br = self.draws_generator.apply_distribution(Br, self.rvdist)
|
|
2693
2951
|
self.Br = Br # save Br to use later
|
|
2694
2952
|
Xr = X[:, :, :, self.rvidx]
|
|
2695
|
-
|
|
2696
|
-
|
|
2953
|
+
# Build the utility directly. Keeping a separate XBr and a
|
|
2954
|
+
# preallocated V needlessly retains two (N, P, J, R) work arrays.
|
|
2955
|
+
V = dev.cust_einsum('npjk,nkr -> npjr', Xr, Br).astype(float)
|
|
2956
|
+
V += XBf[:, :, :, None]
|
|
2697
2957
|
else:
|
|
2698
2958
|
self.Br = Br_b[None, :, None]
|
|
2959
|
+
V = np.broadcast_to(
|
|
2960
|
+
XBf[:, :, :, None],
|
|
2961
|
+
(self.N, self.P, self.J, draws.shape[-1]),
|
|
2962
|
+
).copy()
|
|
2699
2963
|
|
|
2700
2964
|
# transformations for variables with fixed coeffs
|
|
2701
2965
|
if self.Kftrans != 0:
|
|
@@ -167,6 +167,7 @@ def build_case_alt_distances(
|
|
|
167
167
|
dest_lat_col: str = "dest_lat",
|
|
168
168
|
dest_lon_col: str = "dest_lon",
|
|
169
169
|
offset_col: str | None = "distance_offset_km",
|
|
170
|
+
member_zones_col: str | None = "member_suburbs",
|
|
170
171
|
) -> pd.DataFrame:
|
|
171
172
|
"""Build larch-style per-(case, alternative) distances.
|
|
172
173
|
|
|
@@ -204,7 +205,12 @@ def build_case_alt_distances(
|
|
|
204
205
|
raise KeyError(f"build_case_alt_distances: alternatives missing columns {missing}")
|
|
205
206
|
|
|
206
207
|
c = cases[req_cases].copy()
|
|
207
|
-
|
|
208
|
+
extra_alt_cols = []
|
|
209
|
+
if offset_col and offset_col in alternatives.columns:
|
|
210
|
+
extra_alt_cols.append(offset_col)
|
|
211
|
+
if member_zones_col and member_zones_col in alternatives.columns:
|
|
212
|
+
extra_alt_cols.append(member_zones_col)
|
|
213
|
+
a = alternatives[req_alts + extra_alt_cols].copy()
|
|
208
214
|
c["_oz"] = c[orig_zone_col].astype(str).str.strip().str.upper()
|
|
209
215
|
a["_dz"] = a[dest_zone_col].astype(str).str.strip().str.upper()
|
|
210
216
|
if offset_col and offset_col in a.columns:
|
|
@@ -225,24 +231,41 @@ def build_case_alt_distances(
|
|
|
225
231
|
# Skim index lookup (zone label -> matrix row).
|
|
226
232
|
skim_lookup: dict[str, int] = {}
|
|
227
233
|
if skim_zone_ids is not None and skim_dist_mat is not None:
|
|
228
|
-
skim_lookup = {z: i
|
|
234
|
+
skim_lookup = {str(z).strip().upper(): i
|
|
235
|
+
for i, z in enumerate(skim_zone_ids)}
|
|
229
236
|
o_idx = np.array([skim_lookup.get(z, -1) for z in c["_oz"].to_numpy()], dtype=int)
|
|
230
237
|
d_idx = np.array([skim_lookup.get(z, -1) for z in a["_dz"].to_numpy()], dtype=int)
|
|
231
238
|
|
|
232
239
|
skim_d = np.full((n_c, n_a), np.nan, dtype=float)
|
|
233
240
|
skim_t = np.full((n_c, n_a), np.nan, dtype=float)
|
|
234
|
-
if skim_dist_mat is not None and (o_idx >= 0).any()
|
|
241
|
+
if skim_dist_mat is not None and (o_idx >= 0).any():
|
|
235
242
|
sd = np.asarray(skim_dist_mat, dtype=float)
|
|
236
243
|
st = np.asarray(skim_time_mat, dtype=float) if skim_time_mat is not None else None
|
|
237
244
|
ok_c = np.where(o_idx >= 0)[0]
|
|
238
|
-
ok_a = np.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
ok_a = np.arange(n_a) if member_zones_col and member_zones_col in a.columns \
|
|
246
|
+
else np.where(d_idx >= 0)[0]
|
|
247
|
+
if member_zones_col and member_zones_col in a.columns:
|
|
248
|
+
for ci in ok_c:
|
|
249
|
+
for ai in ok_a:
|
|
250
|
+
members = [z.strip().upper() for z in str(
|
|
251
|
+
a.iloc[ai][member_zones_col] or "").split(",") if z.strip()]
|
|
252
|
+
member_idx = [skim_lookup[z] for z in members if z in skim_lookup]
|
|
253
|
+
if not member_idx:
|
|
254
|
+
member_idx = [d_idx[ai]]
|
|
255
|
+
values = sd[o_idx[ci], member_idx]
|
|
256
|
+
valid = np.isfinite(values) & (values > 0.0) & (values < float(max_km))
|
|
257
|
+
if valid.any():
|
|
258
|
+
pick = int(np.flatnonzero(valid)[np.argmin(values[valid])])
|
|
259
|
+
skim_d[ci, ai] = values[valid][pick]
|
|
260
|
+
if st is not None and st.shape == sd.shape:
|
|
261
|
+
skim_t[ci, ai] = st[o_idx[ci], member_idx[np.flatnonzero(valid)[pick]]]
|
|
262
|
+
else:
|
|
263
|
+
sub = sd[np.ix_(o_idx[ok_c], d_idx[ok_a])]
|
|
264
|
+
usable = (sub > 0.0) & (sub < float(max_km))
|
|
265
|
+
skim_d[np.ix_(ok_c, ok_a)] = np.where(usable, sub, np.nan)
|
|
266
|
+
if st is not None and st.shape == sd.shape:
|
|
267
|
+
sub_t = st[np.ix_(o_idx[ok_c], d_idx[ok_a])]
|
|
268
|
+
skim_t[np.ix_(ok_c, ok_a)] = np.where(usable, sub_t, np.nan)
|
|
246
269
|
|
|
247
270
|
# Haversine fallback from case-specific origin coords (broadcast).
|
|
248
271
|
with np.errstate(invalid="ignore"):
|
|
@@ -1780,6 +1780,7 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
|
|
|
1780
1780
|
member_params_spec=None,
|
|
1781
1781
|
class_params_spec=None,
|
|
1782
1782
|
panels=None,
|
|
1783
|
+
retain_all=False,
|
|
1783
1784
|
ident_penalty_kwargs=None,
|
|
1784
1785
|
**kwargs,
|
|
1785
1786
|
):
|
|
@@ -1797,8 +1798,17 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
|
|
|
1797
1798
|
Variables used in the class membership equation.
|
|
1798
1799
|
member_params_spec : list or array, optional
|
|
1799
1800
|
Per-class membership variable specification.
|
|
1801
|
+
retain_all : bool
|
|
1802
|
+
MEMORY: when False (default) only the best model is kept alive and
|
|
1803
|
+
the returned list contains just that model; every intermediate
|
|
1804
|
+
class-count model is released as the search advances. Each model
|
|
1805
|
+
otherwise pins its X/y/draws design, JAX device copies and cached
|
|
1806
|
+
JIT objectives, so retaining all of them across a GA search leaks
|
|
1807
|
+
hundreds of MB per candidate. Set True to recover the previous
|
|
1808
|
+
behaviour (all models returned and stored on
|
|
1809
|
+
``best_model.search_results``).
|
|
1800
1810
|
"""
|
|
1801
|
-
fitted_models = []
|
|
1811
|
+
fitted_models = [] if retain_all else None
|
|
1802
1812
|
best_model = None
|
|
1803
1813
|
_best_nsig = float('inf')
|
|
1804
1814
|
prev_model = None
|
|
@@ -1833,7 +1843,8 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
|
|
|
1833
1843
|
de_seed=de_seed,
|
|
1834
1844
|
)
|
|
1835
1845
|
model.get_loglik_null()
|
|
1836
|
-
fitted_models
|
|
1846
|
+
if fitted_models is not None:
|
|
1847
|
+
fitted_models.append(model)
|
|
1837
1848
|
|
|
1838
1849
|
# Pareto-style significance prioritisation:
|
|
1839
1850
|
# 1) prefer fewer insignificant variable groups,
|
|
@@ -1858,8 +1869,14 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
|
|
|
1858
1869
|
_best_nsig = nsig
|
|
1859
1870
|
prev_model = model
|
|
1860
1871
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1872
|
+
if fitted_models is not None:
|
|
1873
|
+
best_model.search_results = fitted_models
|
|
1874
|
+
return best_model, fitted_models
|
|
1875
|
+
# Default path: keep only the best model alive. Drop the warm-start
|
|
1876
|
+
# chain's trailing reference so intermediate models can be collected.
|
|
1877
|
+
best_model.search_results = None
|
|
1878
|
+
prev_model = None
|
|
1879
|
+
return best_model, [best_model]
|
|
1863
1880
|
|
|
1864
1881
|
# ------------------------------------------------------------------
|
|
1865
1882
|
# Internal helpers for numerical standard errors
|
|
@@ -54,6 +54,7 @@ in a panel or longitudinal dataset.
|
|
|
54
54
|
''' LIBRARIES '''
|
|
55
55
|
''' ---------------------------------------------------------- '''
|
|
56
56
|
import itertools
|
|
57
|
+
import os
|
|
57
58
|
import numpy as np
|
|
58
59
|
import scipy.stats as ss
|
|
59
60
|
from scipy.optimize import minimize
|
|
@@ -273,6 +274,8 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
273
274
|
self.rvdist = None # Distributions for randvar
|
|
274
275
|
self.correlated_vars, self.halton_opts = None, None
|
|
275
276
|
self.batch_size, self.minimise_func = None, None
|
|
277
|
+
if not hasattr(self, "save_prob_full"):
|
|
278
|
+
self.save_prob_full = bool(os.environ.get("SEARCHLIBRIUM_SAVE_PROB_FULL", ""))
|
|
276
279
|
# }
|
|
277
280
|
|
|
278
281
|
''' -------------------------------------------------------------------- '''
|
|
@@ -781,11 +784,17 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
781
784
|
self.ind_pred_prob, self.pred_prob = _panel_weighted_shares(
|
|
782
785
|
self.choice_pred_prob, self.panel_info)
|
|
783
786
|
|
|
784
|
-
self.
|
|
787
|
+
if self.save_prob_full:
|
|
788
|
+
self.prob_full = p
|
|
785
789
|
# }
|
|
786
790
|
self.post_process(result, self.Xnames, self.N)
|
|
787
791
|
# }
|
|
788
792
|
|
|
793
|
+
def release_memory(self):
|
|
794
|
+
"""Drop prob_full if not explicitly saved. Safe between GA candidates."""
|
|
795
|
+
if not getattr(self, "save_prob_full", False):
|
|
796
|
+
self.prob_full = None
|
|
797
|
+
|
|
789
798
|
''' ---------------------------------------------------------- '''
|
|
790
799
|
''' Function. This code segments the betas array into different
|
|
791
800
|
parts and stores these parts in var_list under keys from
|
|
@@ -3,6 +3,7 @@ IMPLEMENTATION: MULTINOMIAL AND CONDITIONAL LOGIT MODEL
|
|
|
3
3
|
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
4
4
|
from sklearn.preprocessing import RobustScaler
|
|
5
5
|
from addicty import Dict
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
"""
|
|
8
9
|
BACKGROUND - MULTINOMIAL LOGIT
|
|
@@ -1019,7 +1020,9 @@ class MultinomialLogit(DiscreteChoiceModel):
|
|
|
1019
1020
|
# owning model instead of capturing ``self``, so an entry never pins a
|
|
1020
1021
|
# dead model (and its X/y arrays) for the life of the process.
|
|
1021
1022
|
# ------------------------------------------------------------------
|
|
1022
|
-
_JIT_CACHE_MAXSIZE =
|
|
1023
|
+
_JIT_CACHE_MAXSIZE = max(
|
|
1024
|
+
0, int(os.environ.get("SEARCHLIBRIUM_MNL_JIT_CACHE_MAXSIZE", "4"))
|
|
1025
|
+
)
|
|
1023
1026
|
_jit_cache: "OrderedDict[tuple, tuple]" = OrderedDict()
|
|
1024
1027
|
|
|
1025
1028
|
@classmethod
|
|
@@ -69,6 +69,8 @@ from scipy import stats
|
|
|
69
69
|
from scipy.optimize import minimize
|
|
70
70
|
|
|
71
71
|
import inspect
|
|
72
|
+
import os
|
|
73
|
+
from collections import OrderedDict
|
|
72
74
|
|
|
73
75
|
minval = 1E-30
|
|
74
76
|
|
|
@@ -2481,7 +2483,15 @@ class SA_ORDLOG():
|
|
|
2481
2483
|
self.mod = OrderedLogit(X=X, y=y, J=J, distr='logit', start=None, fit_intercept=True)
|
|
2482
2484
|
|
|
2483
2485
|
# SA specific parameters
|
|
2484
|
-
|
|
2486
|
+
# MEMORY: the archive memoises spec -> loglik so repeated specs skip
|
|
2487
|
+
# the expensive refit. It used to be an unbounded dict, so a long run
|
|
2488
|
+
# retained one entry per distinct predictor mask (2**K in the limit).
|
|
2489
|
+
# It is now an LRU-bounded OrderedDict; evicting an entry only costs a
|
|
2490
|
+
# future refit, never correctness. Set
|
|
2491
|
+
# SEARCHLIBRIUM_ORDLOG_ARCHIVE_MAX=0 to restore unbounded behaviour.
|
|
2492
|
+
self.archive = OrderedDict()
|
|
2493
|
+
self.archive_maxsize = int(
|
|
2494
|
+
os.environ.get("SEARCHLIBRIUM_ORDLOG_ARCHIVE_MAX", "50000"))
|
|
2485
2495
|
self.tI = 100
|
|
2486
2496
|
self.tF = 0.001
|
|
2487
2497
|
self.t = self.tI
|
|
@@ -2544,6 +2554,12 @@ class SA_ORDLOG():
|
|
|
2544
2554
|
print("Solution already seen")
|
|
2545
2555
|
candidate_score = self.archive[key]
|
|
2546
2556
|
|
|
2557
|
+
# LRU touch + bounded eviction (see archive_maxsize above).
|
|
2558
|
+
self.archive.move_to_end(key)
|
|
2559
|
+
_amax = getattr(self, "archive_maxsize", 0)
|
|
2560
|
+
while _amax and len(self.archive) > _amax:
|
|
2561
|
+
self.archive.popitem(last=False)
|
|
2562
|
+
|
|
2547
2563
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
2548
2564
|
|
|
2549
2565
|
if accept_change(self.current_score, candidate_score, self.t):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.263
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.263
|
|
4
4
|
Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
|
|
5
5
|
Author: Zeke Ahern, Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
|
|
6
6
|
Maintainer-email: Zeke Ahern <z.ahern@qut.edu.au>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.261
|
|
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
|
|
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.261 → searchlibrium-0.0.263}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium/zero_inflated_ordered_probit.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.261 → searchlibrium-0.0.263}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
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
|