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.
Files changed (51) hide show
  1. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/pyproject.toml +1 -1
  3. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Halton.py +88 -10
  4. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/MixedLogit.py +159 -46
  5. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/call_meta.py +8 -8
  6. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixed_logit.py +57 -0
  7. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/siman.py +2 -2
  8. searchlibrium-0.0.98/src/SearchLibrium/version.txt +1 -0
  9. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
  10. searchlibrium-0.0.96/src/SearchLibrium/version.txt +0 -1
  11. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/README.md +0 -0
  12. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/setup.cfg +0 -0
  13. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  14. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/RandomP.py +0 -0
  15. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  16. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  17. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/__init__.py +0 -0
  18. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/__main__.py +0 -0
  19. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/_choice_model.py +0 -0
  20. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/_device.py +0 -0
  21. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/banditsa.py +0 -0
  22. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/bhhh/minimize.py +0 -0
  23. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/boxcox_functions.py +0 -0
  24. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/constraints_builder.py +0 -0
  25. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/harmony.py +0 -0
  26. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/latent_class.py +0 -0
  27. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/main.py +0 -0
  28. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/main_debug.py +0 -0
  29. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mdcev.py +0 -0
  30. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/misc.py +0 -0
  31. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixed_nested.py +0 -0
  32. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/mixedrrm.py +0 -0
  33. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_logit.py +0 -0
  34. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_nested.py +0 -0
  35. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/multinomial_probit.py +0 -0
  36. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/ordered_logit.py +0 -0
  37. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  38. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/rrm.py +0 -0
  39. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/sapbil.py +0 -0
  40. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/search.py +0 -0
  41. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/selection_models.py +0 -0
  42. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/setup.py +0 -0
  43. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_lc_de.py +0 -0
  44. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_mario_searches.py +0 -0
  45. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
  46. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium/threshold.py +0 -0
  47. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
  48. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  49. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  50. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/requires.txt +0 -0
  51. {searchlibrium-0.0.96 → searchlibrium-0.0.98}/src/SearchLibrium.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.96
3
+ Version: 0.0.98
4
4
  Summary: A Python package for econometric models driven by search
5
5
  Author: Alexander Paz Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -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.96"
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
- """Legacy name now generates scrambled Sobol draws."""
53
+ """Generate quasi-random draws using Halton or Sobol sequences.
24
54
 
25
- def __init__(self, primes=None, drop=100, shuffled=False, antithetic=False):
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 scrambled Sobol draws for multiple variables.
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
- base = n_draws // 2 if self.antithetic else n_draws
37
- draws = _sobol_generate(sample_size, base, n_vars, shuffled=self.shuffled)
38
- # draws shape: (sample_size, n_vars, base)
39
- if self.antithetic:
40
- draws = np.concatenate([draws, 1.0 - draws], axis=2)
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 (scrambled Sobol) draws."""
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
- self.halton = Halton(**(halton_opts or {}))
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, n_vars):
45
- """
46
- Generates random draws for the mixed logit model.
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
- Parameters:
49
- sample_size (int): Number of samples.
50
- n_draws (int): Number of draws per sample.
51
- n_vars (int): Number of variables.
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 np.ndarray((1, 0, 1))
55
- # Use self.halton_opts so options like antithetic/shuffled are respected.
56
- opts = self.halton_opts or {}
57
- draws_s = Draws(k=n_vars, halton_opts=opts)
58
- draws = draws_s.generate_draws(sample_size, n_draws)
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
- # self.fn_generate_draws: DrawsFunction = self.generate_draws_halton if halton else self.generate_draws_random
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
- #print(f"self.randvars: {self.randvars}")
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
- # Better scale initialisation: use half the absolute value of the MNL
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: # CHECK ">0"
310
- # {
311
- # Similarly scale random-transformed std devs from MNL means.
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 = 50
98
- max_iter = 10
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 = 100
102
- max_iter = 15
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 = 150
106
- max_iter = 20
105
+ max_temp_steps = 250
106
+ max_iter = 40
107
107
  else:
108
108
  tI, tF = 5000, 0.0001
109
- max_temp_steps = 250
110
- max_iter = 30
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.progress_file) # Report current state
1198
- self.report_progress() # Report to the screen
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.96
3
+ Version: 0.0.98
4
4
  Summary: A Python package for econometric models driven by search
5
5
  Author: Alexander Paz Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -1 +0,0 @@
1
- 0.0.96
File without changes
File without changes