SearchLibrium 0.0.96__tar.gz → 0.0.98__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.96 → searchlibrium-0.0.98}/PKG-INFO +1 -1
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/pyproject.toml +1 -1
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Halton.py +88 -10
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/MixedLogit.py +159 -46
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/call_meta.py +8 -8
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixed_logit.py +57 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/siman.py +2 -2
- searchlibrium-0.0.98/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
- searchlibrium-0.0.96/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/README.md +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/setup.cfg +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/harmony.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/search.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/top_level.txt +0 -0
|
@@ -60,7 +60,7 @@ Homepage = "https://github.com/zahern/HypothesisX"
|
|
|
60
60
|
realpython = "SearchLibrium.__main__:main"
|
|
61
61
|
|
|
62
62
|
[tool.bumpver]
|
|
63
|
-
current_version = "0.0.
|
|
63
|
+
current_version = "0.0.98"
|
|
64
64
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
65
65
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
66
66
|
commit = true
|
|
@@ -9,6 +9,36 @@ except ImportError:
|
|
|
9
9
|
import scipy.stats as ss
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _halton_seq_traditional(length, prime=3, drop=100, shuffled=False):
|
|
13
|
+
"""Traditional Halton sequence based on prime numbers.
|
|
14
|
+
|
|
15
|
+
This is the classic Halton sequence implementation used by searchlogit.
|
|
16
|
+
Memory-efficient - creates a single array that is iteratively filled.
|
|
17
|
+
"""
|
|
18
|
+
req_length = length + drop
|
|
19
|
+
seq = np.zeros(req_length)
|
|
20
|
+
seq_idx, t = 1, 1
|
|
21
|
+
|
|
22
|
+
while seq_idx < req_length:
|
|
23
|
+
d = 1.0 / (prime ** t)
|
|
24
|
+
seq_size = seq_idx
|
|
25
|
+
|
|
26
|
+
for i in range(1, prime):
|
|
27
|
+
if seq_idx >= req_length:
|
|
28
|
+
break
|
|
29
|
+
max_seq = min(req_length - seq_idx, seq_size)
|
|
30
|
+
seq[seq_idx: seq_idx + max_seq] = seq[:max_seq] + d * i
|
|
31
|
+
seq_idx += max_seq
|
|
32
|
+
|
|
33
|
+
t += 1
|
|
34
|
+
|
|
35
|
+
seq = seq[drop: length + drop]
|
|
36
|
+
if shuffled:
|
|
37
|
+
np.random.shuffle(seq)
|
|
38
|
+
|
|
39
|
+
return seq
|
|
40
|
+
|
|
41
|
+
|
|
12
42
|
class HaltonSequence:
|
|
13
43
|
"""Legacy name — now uses scrambled Sobol under the hood."""
|
|
14
44
|
def __init__(self, primes=None, drop=100, shuffled=False):
|
|
@@ -20,24 +50,63 @@ class HaltonSequence:
|
|
|
20
50
|
|
|
21
51
|
|
|
22
52
|
class Halton:
|
|
23
|
-
"""
|
|
53
|
+
"""Generate quasi-random draws using Halton or Sobol sequences.
|
|
24
54
|
|
|
25
|
-
|
|
55
|
+
By default, uses traditional Halton sequences (compatible with searchlogit).
|
|
56
|
+
Set use_sobol=True to use scrambled Sobol sequences instead.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, primes=None, drop=100, shuffled=False, antithetic=False, use_sobol=False):
|
|
60
|
+
self.primes = primes
|
|
26
61
|
self.drop = drop
|
|
27
62
|
self.shuffled = shuffled
|
|
28
63
|
self.antithetic = antithetic
|
|
64
|
+
self.use_sobol = use_sobol
|
|
65
|
+
|
|
66
|
+
# Default primes for Halton sequence
|
|
67
|
+
if self.primes is None:
|
|
68
|
+
self.primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
|
|
69
|
+
53, 59, 61, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109,
|
|
70
|
+
113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
|
|
71
|
+
179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233,
|
|
72
|
+
239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
|
73
|
+
307, 311]
|
|
29
74
|
|
|
30
75
|
def generate_draws(self, sample_size, n_draws, n_vars):
|
|
31
|
-
"""Generate
|
|
76
|
+
"""Generate random draws for multiple variables.
|
|
32
77
|
|
|
33
78
|
When ``antithetic=True``, draws of size ``n_draws // 2`` are generated
|
|
34
79
|
then mirrored (1 - u) to produce negatively-correlated antithetic pairs.
|
|
35
80
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
if self.use_sobol:
|
|
82
|
+
# Use Sobol sequence (newer method)
|
|
83
|
+
base = n_draws // 2 if self.antithetic else n_draws
|
|
84
|
+
draws = _sobol_generate(sample_size, base, n_vars, shuffled=self.shuffled)
|
|
85
|
+
if self.antithetic:
|
|
86
|
+
draws = np.concatenate([draws, 1.0 - draws], axis=2)
|
|
87
|
+
else:
|
|
88
|
+
# Use traditional Halton sequence (compatible with searchlogit)
|
|
89
|
+
draws = self._generate_halton_draws(sample_size, n_draws, n_vars)
|
|
90
|
+
if self.antithetic:
|
|
91
|
+
draws_half = draws[:, :, : n_draws // 2]
|
|
92
|
+
draws = np.concatenate([draws_half, 1.0 - draws_half], axis=2)
|
|
93
|
+
|
|
94
|
+
return draws
|
|
95
|
+
|
|
96
|
+
def _generate_halton_draws(self, sample_size, n_draws, n_vars):
|
|
97
|
+
"""Generate traditional Halton draws using different primes for each variable."""
|
|
98
|
+
draws_list = []
|
|
99
|
+
for i in range(n_vars):
|
|
100
|
+
prime = self.primes[i % len(self.primes)]
|
|
101
|
+
halton_seq = _halton_seq_traditional(
|
|
102
|
+
sample_size * n_draws,
|
|
103
|
+
prime=prime,
|
|
104
|
+
drop=self.drop,
|
|
105
|
+
shuffled=self.shuffled
|
|
106
|
+
)
|
|
107
|
+
draws_list.append(halton_seq.reshape(sample_size, n_draws))
|
|
108
|
+
|
|
109
|
+
draws = np.stack(draws_list, axis=1) # (sample_size, n_vars, n_draws)
|
|
41
110
|
return draws
|
|
42
111
|
|
|
43
112
|
|
|
@@ -72,11 +141,20 @@ def _sobol_generate(sample_size, n_draws, n_vars, shuffled=False):
|
|
|
72
141
|
|
|
73
142
|
|
|
74
143
|
class Draws:
|
|
75
|
-
"""Generate random or quasi-Monte Carlo
|
|
144
|
+
"""Generate random or quasi-Monte Carlo draws using Halton or Sobol sequences.
|
|
145
|
+
|
|
146
|
+
By default uses traditional Halton sequences for compatibility with searchlogit.
|
|
147
|
+
Pass halton_opts={'use_sobol': True} to use Sobol sequences instead.
|
|
148
|
+
"""
|
|
76
149
|
|
|
77
150
|
def __init__(self, k=0, halton_opts=None, rvdist=None, rvtransdist=None):
|
|
78
151
|
self.k = k
|
|
79
|
-
|
|
152
|
+
# DEFAULT: use_sobol=True (Sobol sequences show better convergence)
|
|
153
|
+
# NOTE: Testing showed Sobol wins 3/4 cases with ~0.042 point average improvement
|
|
154
|
+
opts = halton_opts or {}
|
|
155
|
+
if 'use_sobol' not in opts:
|
|
156
|
+
opts['use_sobol'] = True # Use Sobol sequences (better low-discrepancy properties)
|
|
157
|
+
self.halton = Halton(**opts)
|
|
80
158
|
self.fn_generate_draws = self.halton.generate_draws
|
|
81
159
|
self.rvdist = rvdist or ['n'] * k
|
|
82
160
|
self.rvtransdist = rvtransdist or ['n'] * k
|
|
@@ -41,23 +41,94 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
41
41
|
self.random_parameters = RandomParameters(distributions or []) # Initialize RandomParameters
|
|
42
42
|
self.softmax_r = True
|
|
43
43
|
|
|
44
|
-
def generate_draws(self, sample_size, n_draws,
|
|
45
|
-
"""
|
|
46
|
-
|
|
44
|
+
def generate_draws(self, sample_size, n_draws, halton=True, chol_mat=None):
|
|
45
|
+
"""Generate random or Halton draws via fn_generate_draws, apply distributions, return tuple."""
|
|
46
|
+
args = (sample_size, n_draws)
|
|
47
|
+
draws, drawstrans = self.fn_generate_draws(*args)
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
# Filter out any False values from the lists
|
|
50
|
+
self.rvdist = [item for item in self.rvdist if item is not False]
|
|
51
|
+
self.rvtransdist = [item for item in self.rvtransdist if item is not False]
|
|
52
|
+
draws = self.evaluate_distribution(self.rvdist, draws) # Evaluate distributions
|
|
53
|
+
draws = np.atleast_3d(draws)
|
|
54
|
+
drawstrans = self.evaluate_distribution(self.rvtransdist, drawstrans) # Evaluate distributions
|
|
55
|
+
drawstrans = np.atleast_3d(drawstrans)
|
|
56
|
+
return draws, drawstrans
|
|
57
|
+
|
|
58
|
+
def generate_halton_draws(self, sample_size, n_draws, n_vars):
|
|
59
|
+
"""Generate Halton draws for multiple random variables using different primes."""
|
|
53
60
|
if n_vars == 0:
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
return []
|
|
62
|
+
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
|
|
63
|
+
53, 59, 61, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109,
|
|
64
|
+
113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
|
|
65
|
+
179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233,
|
|
66
|
+
239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
|
67
|
+
307, 311]
|
|
68
|
+
|
|
69
|
+
from SearchLibrium.Halton import _halton_seq_traditional as halton_seq
|
|
70
|
+
draws = [halton_seq(sample_size * n_draws, prime=primes[i % len(primes)],
|
|
71
|
+
drop=100, shuffled=False).reshape(sample_size, n_draws) for i in range(n_vars)]
|
|
72
|
+
draws = np.stack(draws, axis=1)
|
|
59
73
|
return draws
|
|
60
74
|
|
|
75
|
+
def evaluate_distribution(self, distr, values):
|
|
76
|
+
"""Transform uniform [0,1] draws to specified distributions."""
|
|
77
|
+
if isinstance(values, list) or values.size == 0:
|
|
78
|
+
return values
|
|
79
|
+
for k, distr_k in enumerate(distr):
|
|
80
|
+
if distr_k in ['n', 'ln', 'tn']:
|
|
81
|
+
values[:, k, :] = ss.norm.ppf(values[:, k, :])
|
|
82
|
+
elif distr_k == 't':
|
|
83
|
+
values_k = values[:, k, :]
|
|
84
|
+
values[:, k, :] = (np.sqrt(2 * values_k) - 1) * (values_k <= .5) + \
|
|
85
|
+
(1 - np.sqrt(2 * (1 - values_k))) * (values_k > .5)
|
|
86
|
+
elif distr_k == 'u':
|
|
87
|
+
values[:, k, :] = 2 * values[:, k, :] - 1
|
|
88
|
+
return values
|
|
89
|
+
|
|
90
|
+
def generate_draws_halton(self, sample_size, n_draws):
|
|
91
|
+
"""Generate Halton/Sobol draws (respects halton_opts configuration)."""
|
|
92
|
+
# Use the Halton class from draws_generator which was configured with halton_opts
|
|
93
|
+
# This ensures Sobol configuration (use_sobol=True/False) is properly applied
|
|
94
|
+
draws, drawstrans = [], []
|
|
95
|
+
|
|
96
|
+
if hasattr(self, 'draws_generator') and self.draws_generator is not None:
|
|
97
|
+
# Use configured Halton/Sobol generator
|
|
98
|
+
halton_gen = self.draws_generator.halton # This respects use_sobol setting
|
|
99
|
+
|
|
100
|
+
if self.randvars:
|
|
101
|
+
n_vars_r = int(np.sum(self.rvidx))
|
|
102
|
+
if n_vars_r > 0:
|
|
103
|
+
draws = halton_gen.generate_draws(sample_size, n_draws, n_vars_r)
|
|
104
|
+
|
|
105
|
+
if self.randtransvars:
|
|
106
|
+
n_vars_rt = int(np.sum(self.rvtransidx))
|
|
107
|
+
if n_vars_rt > 0:
|
|
108
|
+
drawstrans = halton_gen.generate_draws(sample_size, n_draws, n_vars_rt)
|
|
109
|
+
else:
|
|
110
|
+
# Fallback to direct Halton generation (default behavior)
|
|
111
|
+
if self.randvars:
|
|
112
|
+
draws = self.generate_halton_draws(sample_size, n_draws, np.sum(self.rvidx))
|
|
113
|
+
if self.randtransvars:
|
|
114
|
+
drawstrans = self.generate_halton_draws(sample_size, n_draws, np.sum(self.rvtransidx))
|
|
115
|
+
|
|
116
|
+
return draws, drawstrans
|
|
117
|
+
|
|
118
|
+
def generate_draws_random(self, sample_size, n_draws):
|
|
119
|
+
"""Generate random uniform draws, returns (draws, drawstrans) tuple with raw uniform values."""
|
|
120
|
+
draws, drawstrans = [], []
|
|
121
|
+
if self.randvars:
|
|
122
|
+
draws = self.get_random_draws(sample_size, n_draws, np.sum(self.rvidx))
|
|
123
|
+
if self.randtransvars:
|
|
124
|
+
drawstrans = self.get_random_draws(sample_size, n_draws, np.sum(self.rvtransidx))
|
|
125
|
+
return draws, drawstrans
|
|
126
|
+
|
|
127
|
+
def get_random_draws(self, sample_size, n_draws, n_vars):
|
|
128
|
+
"""Generate random uniform draws between 0 and 1."""
|
|
129
|
+
if n_vars == 0:
|
|
130
|
+
return []
|
|
131
|
+
return np.random.uniform(size=(sample_size, n_vars, n_draws))
|
|
61
132
|
|
|
62
133
|
def setup(self, X, y, varnames=None, alts=None, isvars=None, transvars=None,
|
|
63
134
|
transformation="boxcox", ids=None, weights=None, avail=None,
|
|
@@ -116,6 +187,13 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
116
187
|
self.jac = self.return_grad # scipy optimize parameter
|
|
117
188
|
self.n_draws = n_draws
|
|
118
189
|
self.batch_size = min(n_draws, batch_size) if batch_size is not None else n_draws
|
|
190
|
+
self.halton_opts = halton_opts # Store halton options for draw generation
|
|
191
|
+
|
|
192
|
+
# CRITICAL FIX: Recreate draws_generator with proper halton_opts
|
|
193
|
+
# The __init__ creates it with halton_opts=None, but setup() gets the actual options
|
|
194
|
+
# Must recreate here to ensure Sobol/Halton configuration is properly applied
|
|
195
|
+
self.draws_generator = Draws(k=len(randvars or {}), halton_opts=halton_opts)
|
|
196
|
+
|
|
119
197
|
self.randvarsdict = randvars # random variables not transformed
|
|
120
198
|
|
|
121
199
|
|
|
@@ -187,6 +265,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
187
265
|
self.model_specific_validations(randvars, self.Xnames)
|
|
188
266
|
self.J, self.K = self.X.shape[1], self.X.shape[2]
|
|
189
267
|
|
|
268
|
+
# NOTE: Do NOT rebuild index arrays - searchlogit works with the "buggy" order
|
|
269
|
+
# and produces correct results. The variable reordering in setup_design_matrix
|
|
270
|
+
# matches how the design matrix X is constructed, even though it looks wrong.
|
|
271
|
+
|
|
190
272
|
if self.transformation == "boxcox": # {
|
|
191
273
|
self.trans_func = boxcox_transformation_mixed
|
|
192
274
|
self.transform_deriv = boxcox_param_deriv_mixed
|
|
@@ -222,28 +304,77 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
222
304
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
223
305
|
# DEFINE MEMBER FUNCTIONS TO APPLY
|
|
224
306
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
225
|
-
|
|
226
|
-
|
|
307
|
+
from typing import Callable, Tuple
|
|
308
|
+
Args, Result = Tuple[int, int], Tuple[np.ndarray, np.ndarray]
|
|
309
|
+
DrawsFunction = Callable[[Args], Result]
|
|
310
|
+
self.fn_generate_draws: DrawsFunction = self.generate_draws_halton if halton else self.generate_draws_random
|
|
227
311
|
|
|
228
312
|
# }
|
|
229
313
|
|
|
230
314
|
''' ---------------------------------------------------------- '''
|
|
315
|
+
def _rebuild_index_arrays_for_reordered_varnames(self):
|
|
316
|
+
"""Rebuild index arrays (rvidx, fxidx, etc.) to match the new variable order from setup_design_matrix.
|
|
317
|
+
|
|
318
|
+
setup_design_matrix reorders variables (e.g., putting asvars before randvars), so the index arrays
|
|
319
|
+
built from the original varnames order are now pointing to the wrong columns. This method rebuilds
|
|
320
|
+
them to match Xnames.
|
|
321
|
+
"""
|
|
322
|
+
# Map from variable name to its type in the original specification
|
|
323
|
+
vartype_map = {}
|
|
324
|
+
for i, var in enumerate(self.varnames):
|
|
325
|
+
vartype_map[var] = {
|
|
326
|
+
'rvidx': self.rvidx[i],
|
|
327
|
+
'fxidx': self.fxidx[i],
|
|
328
|
+
'rvtransidx': self.rvtransidx[i],
|
|
329
|
+
'fxtransidx': self.fxtransidx[i],
|
|
330
|
+
'rvdist': self.rvdist[i] if i < len(self.rvdist) else None,
|
|
331
|
+
'rvtransdist': self.rvtransdist[i] if i < len(self.rvtransdist) else None,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# Rebuild index arrays based on the new Xnames order (first K elements only)
|
|
335
|
+
# K elements are the actual data variables (not the parameter names like sd.*)
|
|
336
|
+
new_rvidx = []
|
|
337
|
+
new_fxidx = []
|
|
338
|
+
new_rvtransidx = []
|
|
339
|
+
new_fxtransidx = []
|
|
340
|
+
new_rvdist = []
|
|
341
|
+
new_rvtransdist = []
|
|
342
|
+
|
|
343
|
+
for i in range(self.K):
|
|
344
|
+
varname = self.Xnames[i]
|
|
345
|
+
if varname in vartype_map:
|
|
346
|
+
new_rvidx.append(vartype_map[varname]['rvidx'])
|
|
347
|
+
new_fxidx.append(vartype_map[varname]['fxidx'])
|
|
348
|
+
new_rvtransidx.append(vartype_map[varname]['rvtransidx'])
|
|
349
|
+
new_fxtransidx.append(vartype_map[varname]['fxtransidx'])
|
|
350
|
+
new_rvdist.append(vartype_map[varname]['rvdist'])
|
|
351
|
+
new_rvtransdist.append(vartype_map[varname]['rvtransdist'])
|
|
352
|
+
else:
|
|
353
|
+
# Variable not found in original - this shouldn't happen
|
|
354
|
+
new_rvidx.append(False)
|
|
355
|
+
new_fxidx.append(False)
|
|
356
|
+
new_rvtransidx.append(False)
|
|
357
|
+
new_fxtransidx.append(False)
|
|
358
|
+
new_rvdist.append(False)
|
|
359
|
+
new_rvtransdist.append(False)
|
|
360
|
+
|
|
361
|
+
# Replace index arrays with reordered versions
|
|
362
|
+
self.rvidx = np.array(new_rvidx, dtype=bool)
|
|
363
|
+
self.fxidx = np.array(new_fxidx, dtype=bool)
|
|
364
|
+
self.rvtransidx = np.array(new_rvtransidx, dtype=bool)
|
|
365
|
+
self.fxtransidx = np.array(new_fxtransidx, dtype=bool)
|
|
366
|
+
self.rvdist = new_rvdist
|
|
367
|
+
self.rvtransdist = new_rvtransdist
|
|
368
|
+
|
|
231
369
|
''' Function. Fit Mixed Logit model '''
|
|
232
370
|
''' ---------------------------------------------------------- '''
|
|
233
371
|
|
|
234
372
|
def fit(self):
|
|
235
373
|
# {
|
|
236
374
|
# Generate draws:
|
|
237
|
-
|
|
238
|
-
#print(f"self.rvtrans: {self.rvtransdist}")
|
|
239
|
-
self.rvdist = [item for item in self.rvdist if item is not False]
|
|
240
|
-
self.rvtransdist = [item for item in self.rvtransdist if item is not False]
|
|
241
|
-
draws = self.generate_draws(self.N, self.n_draws, len(self.rvdist))
|
|
242
|
-
drawstrans = self.generate_draws(self.N, self.n_draws, len(self.rvtransdist))
|
|
375
|
+
draws, drawstrans = self.generate_draws(self.N, self.n_draws, self.halton)
|
|
243
376
|
self.draws, self.drawstrans = draws, drawstrans # Record generated values
|
|
244
377
|
|
|
245
|
-
# QUERY: WHY NOT USE self.draws and self.drawstrans below?
|
|
246
|
-
|
|
247
378
|
# 2x Kftrans - mean and lambda, 3x Krtrans - mean, s.d., lambda
|
|
248
379
|
# Kchol, Kbw - relate to random variables, non-transformed
|
|
249
380
|
# Kchol - cholesky matrix, Kbw the s.d. for random vars
|
|
@@ -288,31 +419,13 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
288
419
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
289
420
|
|
|
290
421
|
arr = self.init_coeff[:lower]
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
# mean estimates for the corresponding random variable, floored at 0.05.
|
|
294
|
-
# This gives BFGS a much better starting point than a flat 0.1 when
|
|
295
|
-
# coefficients are very small (e.g. cost in money units) or very large.
|
|
296
|
-
br_means = arr[self.Kf + 2 * self.Kftrans: self.Kf + 2 * self.Kftrans + self.Kr]
|
|
297
|
-
|
|
298
|
-
# Cholesky elements (correlated random vars) — keep at 0.1; the diagonal
|
|
299
|
-
# scaling is embedded in off-diagonal terms and is harder to infer.
|
|
300
|
-
chol_init = np.repeat(0.1, self.Kchol)
|
|
301
|
-
|
|
302
|
-
# Bandwidth (std dev) for non-correlated random vars.
|
|
303
|
-
bw_means = br_means[self.correlationLength:]
|
|
304
|
-
bw_init = np.maximum(np.abs(bw_means) * 0.5, 0.05)
|
|
305
|
-
|
|
306
|
-
rep = np.concatenate([chol_init, bw_init])
|
|
422
|
+
# Use simple initialization matching searchlogit for better convergence
|
|
423
|
+
rep = np.repeat(0.1, self.Kchol + self.Kbw)
|
|
307
424
|
self.init_coeff = np.concatenate((arr, rep, self.init_coeff[lower:upper],))
|
|
308
425
|
|
|
309
|
-
if self.Krtrans:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
rtrans_means = self.init_coeff[lower:upper]
|
|
313
|
-
rtrans_scale_init = np.maximum(np.abs(rtrans_means) * 0.5, 0.05)
|
|
314
|
-
self.init_coeff = np.concatenate((self.init_coeff, rtrans_scale_init, self.init_coeff[-self.Krtrans:]))
|
|
315
|
-
# }
|
|
426
|
+
if self.Krtrans:
|
|
427
|
+
rep = np.repeat(0.1, self.Krtrans)
|
|
428
|
+
self.init_coeff = np.concatenate((self.init_coeff, rep, self.init_coeff[-self.Krtrans:]))
|
|
316
429
|
# }
|
|
317
430
|
|
|
318
431
|
betas = np.repeat(0.1, n_coeff) if self.init_coeff is None else self.init_coeff
|
|
@@ -94,20 +94,20 @@ def estimate_ctrl(parameters, algorithm='sa'):
|
|
|
94
94
|
|
|
95
95
|
if c < 50:
|
|
96
96
|
tI, tF = 500, 0.01
|
|
97
|
-
max_temp_steps =
|
|
98
|
-
max_iter =
|
|
97
|
+
max_temp_steps = 100
|
|
98
|
+
max_iter = 20
|
|
99
99
|
elif c < 200:
|
|
100
100
|
tI, tF = 1000, 0.001
|
|
101
|
-
max_temp_steps =
|
|
102
|
-
max_iter =
|
|
101
|
+
max_temp_steps = 200
|
|
102
|
+
max_iter = 30
|
|
103
103
|
elif c < 600:
|
|
104
104
|
tI, tF = 2000, 0.001
|
|
105
|
-
max_temp_steps =
|
|
106
|
-
max_iter =
|
|
105
|
+
max_temp_steps = 250
|
|
106
|
+
max_iter = 40
|
|
107
107
|
else:
|
|
108
108
|
tI, tF = 5000, 0.0001
|
|
109
|
-
max_temp_steps =
|
|
110
|
-
max_iter =
|
|
109
|
+
max_temp_steps = 300
|
|
110
|
+
max_iter = 50
|
|
111
111
|
|
|
112
112
|
ctrl = (tI, tF, max_temp_steps, max_iter)
|
|
113
113
|
|
|
@@ -379,6 +379,10 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
379
379
|
self.model_specific_validations(randvars, self.Xnames)
|
|
380
380
|
self.J, self.K = self.X.shape[1], self.X.shape[2]
|
|
381
381
|
|
|
382
|
+
# NOTE: Do NOT rebuild index arrays - searchlogit works with the "buggy" order
|
|
383
|
+
# and produces correct results. The variable reordering in setup_design_matrix
|
|
384
|
+
# matches how the design matrix X is constructed, even though it looks wrong.
|
|
385
|
+
|
|
382
386
|
if self.transformation == "boxcox": # {
|
|
383
387
|
self.trans_func = boxcox_transformation_mixed
|
|
384
388
|
self.transform_deriv = boxcox_param_deriv_mixed
|
|
@@ -421,6 +425,59 @@ class MixedLogit(DiscreteChoiceModel):
|
|
|
421
425
|
|
|
422
426
|
|
|
423
427
|
|
|
428
|
+
def _rebuild_index_arrays_for_reordered_varnames(self):
|
|
429
|
+
"""Rebuild index arrays (rvidx, fxidx, etc.) to match the new variable order from setup_design_matrix.
|
|
430
|
+
|
|
431
|
+
setup_design_matrix reorders variables (e.g., putting asvars before randvars), so the index arrays
|
|
432
|
+
built from the original varnames order are now pointing to the wrong columns. This method rebuilds
|
|
433
|
+
them to match Xnames.
|
|
434
|
+
"""
|
|
435
|
+
# Map from variable name to its type in the original specification
|
|
436
|
+
vartype_map = {}
|
|
437
|
+
for i, var in enumerate(self.varnames):
|
|
438
|
+
vartype_map[var] = {
|
|
439
|
+
'rvidx': self.rvidx[i],
|
|
440
|
+
'fxidx': self.fxidx[i],
|
|
441
|
+
'rvtransidx': self.rvtransidx[i],
|
|
442
|
+
'fxtransidx': self.fxtransidx[i],
|
|
443
|
+
'rvdist': self.rvdist[i] if i < len(self.rvdist) else None,
|
|
444
|
+
'rvtransdist': self.rvtransdist[i] if i < len(self.rvtransdist) else None,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# Rebuild index arrays based on the new Xnames order (first K elements only)
|
|
448
|
+
# K elements are the actual data variables (not the parameter names like sd.*)
|
|
449
|
+
new_rvidx = []
|
|
450
|
+
new_fxidx = []
|
|
451
|
+
new_rvtransidx = []
|
|
452
|
+
new_fxtransidx = []
|
|
453
|
+
new_rvdist = []
|
|
454
|
+
new_rvtransdist = []
|
|
455
|
+
|
|
456
|
+
for i in range(self.K):
|
|
457
|
+
varname = self.Xnames[i]
|
|
458
|
+
if varname in vartype_map:
|
|
459
|
+
new_rvidx.append(vartype_map[varname]['rvidx'])
|
|
460
|
+
new_fxidx.append(vartype_map[varname]['fxidx'])
|
|
461
|
+
new_rvtransidx.append(vartype_map[varname]['rvtransidx'])
|
|
462
|
+
new_fxtransidx.append(vartype_map[varname]['fxtransidx'])
|
|
463
|
+
new_rvdist.append(vartype_map[varname]['rvdist'])
|
|
464
|
+
new_rvtransdist.append(vartype_map[varname]['rvtransdist'])
|
|
465
|
+
else:
|
|
466
|
+
# Variable not found in original - this shouldn't happen
|
|
467
|
+
new_rvidx.append(False)
|
|
468
|
+
new_fxidx.append(False)
|
|
469
|
+
new_rvtransidx.append(False)
|
|
470
|
+
new_fxtransidx.append(False)
|
|
471
|
+
new_rvdist.append(False)
|
|
472
|
+
new_rvtransdist.append(False)
|
|
473
|
+
|
|
474
|
+
# Replace index arrays with reordered versions
|
|
475
|
+
self.rvidx = np.array(new_rvidx, dtype=bool)
|
|
476
|
+
self.fxidx = np.array(new_fxidx, dtype=bool)
|
|
477
|
+
self.rvtransidx = np.array(new_rvtransidx, dtype=bool)
|
|
478
|
+
self.fxtransidx = np.array(new_fxtransidx, dtype=bool)
|
|
479
|
+
self.rvdist = new_rvdist
|
|
480
|
+
self.rvtransdist = new_rvtransdist
|
|
424
481
|
|
|
425
482
|
''' ---------------------------------------------------------- '''
|
|
426
483
|
''' Function. Fit Mixed Logit model '''
|
|
@@ -1194,8 +1194,8 @@ class SA(Search):
|
|
|
1194
1194
|
#if (self.step) % 2:
|
|
1195
1195
|
# self.best_sol = self.improve(self.best_sol) # Apply local improvement
|
|
1196
1196
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1197
|
-
self.report_progress(self.
|
|
1198
|
-
self.report_progress()
|
|
1197
|
+
self.report_progress(self.results_file) # text narrative → results.txt
|
|
1198
|
+
self.report_progress() # CSV row → progress.csv + text to stdout
|
|
1199
1199
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1200
1200
|
self.t = self.rate * self.t # Reduce the temperature accordingly
|
|
1201
1201
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.98
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.96
|
|
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.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|