SearchLibrium 0.0.142__tar.gz → 0.0.144__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.

Potentially problematic release.


This version of SearchLibrium might be problematic. Click here for more details.

Files changed (59) hide show
  1. {searchlibrium-0.0.142/src/SearchLibrium.egg-info → searchlibrium-0.0.144}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/pyproject.toml +1 -1
  3. searchlibrium-0.0.144/src/SearchLibrium/BEHier.py +425 -0
  4. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/MixedLogit.py +18 -4
  5. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/_choice_model.py +400 -1
  6. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/harmony.py +64 -35
  7. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/hspbil.py +6 -1
  8. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/latent_class.py +58 -3
  9. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_logit.py +2 -2
  10. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/search.py +114 -45
  11. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/siman.py +3 -2
  12. searchlibrium-0.0.144/src/SearchLibrium/version.txt +1 -0
  13. {searchlibrium-0.0.142 → searchlibrium-0.0.144/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
  14. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/SOURCES.txt +1 -0
  15. searchlibrium-0.0.142/src/SearchLibrium/version.txt +0 -1
  16. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/MANIFEST.in +0 -0
  17. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/README.md +0 -0
  18. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/setup.cfg +0 -0
  19. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Halton.py +0 -0
  20. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  21. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/RandomP.py +0 -0
  22. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  23. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  24. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/__init__.py +0 -0
  25. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/__main__.py +0 -0
  26. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/_device.py +0 -0
  27. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/banditsa.py +0 -0
  28. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/bhhh/minimize.py +0 -0
  29. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/boxcox_functions.py +0 -0
  30. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/call_meta.py +0 -0
  31. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/constraints_builder.py +0 -0
  32. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
  33. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/TravelMode.csv +0 -0
  34. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/electricity.csv +0 -0
  35. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/main.py +0 -0
  36. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/main_debug.py +0 -0
  37. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mdcev.py +0 -0
  38. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/misc.py +0 -0
  39. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixed_logit.py +0 -0
  40. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixed_nested.py +0 -0
  41. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixedrrm.py +0 -0
  42. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_nested.py +0 -0
  43. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_probit.py +0 -0
  44. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/ordered_logit.py +0 -0
  45. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  46. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/predict.py +0 -0
  47. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/rrm.py +0 -0
  48. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/sample_data.py +0 -0
  49. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/sapbil.py +0 -0
  50. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/selection_models.py +0 -0
  51. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/setup.py +0 -0
  52. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_lc_de.py +0 -0
  53. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_mario_searches.py +0 -0
  54. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
  55. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/threshold.py +0 -0
  56. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  57. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  58. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/requires.txt +0 -0
  59. {searchlibrium-0.0.142 → searchlibrium-0.0.144}/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.142
3
+ Version: 0.0.144
4
4
  Summary: A Python package for econometric models driven by search
5
5
  Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -82,7 +82,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
82
82
  realpython = "SearchLibrium.__main__:main"
83
83
 
84
84
  [tool.bumpver]
85
- current_version = "0.0.142"
85
+ current_version = "0.0.144"
86
86
  version_pattern = "MAJOR.MINOR.PATCH"
87
87
  commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
88
88
  commit = true
@@ -0,0 +1,425 @@
1
+ """
2
+ BEHier — Backward Elimination with Hierarchical Rules
3
+ =======================================================
4
+
5
+ In simple terms, what this module does:
6
+
7
+ 1. Takes an already-estimated solution and looks at which coefficients
8
+ are NOT statistically significant.
9
+ 2. Removes the least significant one first — but respecting hierarchical
10
+ groups (an intercept, a random parameter's mean/sd pair, a correlation
11
+ block, a Box-Cox transform) so that related pieces are removed or kept
12
+ together instead of breaking a group apart incorrectly.
13
+ 3. Re-estimates the model and checks whether the goodness-of-fit (BIC)
14
+ improved. If it did, the change is kept and the process repeats with
15
+ the next worst coefficient. If it did not improve, the change is
16
+ reverted and that coefficient is skipped instead.
17
+ 4. Anything the user explicitly prespecified (forced to stay in the
18
+ model, i.e. any `ps_*` setting) is NEVER removed, no matter how
19
+ insignificant it tests.
20
+ 5. This repeats until every remaining coefficient is significant (or a
21
+ prespecified exception), or `max_passes` is reached.
22
+
23
+ This module is self-contained: it can be added or removed without touching
24
+ anything else. Its only external dependency is `search_instance.evaluate_model(sol)`,
25
+ used to re-fit the model after each change — that fitting engine is not
26
+ duplicated here on purpose.
27
+ """
28
+
29
+ import copy
30
+ import math
31
+ import random
32
+ import numpy as np
33
+
34
+
35
+ # ------------------------------------------------------------------------
36
+ # Read-only helpers
37
+ # ------------------------------------------------------------------------
38
+
39
+ def get_components(sol):
40
+ return sol['asvars'], sol['isvars'], sol['randvars'], sol['bcvars'], \
41
+ sol['corvars'], sol['asc_ind']
42
+
43
+
44
+ def update_objectives(param, sol):
45
+ for i in range(param.nb_crit):
46
+ metric = param.criterions[i][0]
47
+ sol.update_objective(i, sol[metric])
48
+
49
+
50
+ # ------------------------------------------------------------------------
51
+ # Removal helpers — uniform naming: all "remove" functions use a leading
52
+ # underscore, all take `param` explicitly instead of relying on `self`.
53
+ # Each one respects any variable marked as prespecified (`ps_*`): those
54
+ # are never dropped, regardless of significance.
55
+ # ------------------------------------------------------------------------
56
+
57
+ def _remove_insig_asvars(asvars, insig, bcvars, pval, pval_member,
58
+ class_params_spec, member_params_spec, param):
59
+ # Keep significant as-variables, i.e., those with significant pvals
60
+ asvars_sig = [var for var in asvars if var not in insig]
61
+ asvars_sig.extend(param.ps_asvars) # prespecified: always kept
62
+
63
+ # Replace insignificant alt-spec coefficient with generic coefficient
64
+ insig_altspec = []
65
+ for var in param.asvarnames:
66
+ altspec = [name for name in insig if name.startswith(var)]
67
+ insig_altspec.extend(altspec)
68
+ insig_altspec_vars = [var for var in insig_altspec if var not in param.asvarnames]
69
+
70
+ rem_asvars = []
71
+
72
+ if insig_altspec_vars:
73
+ gen_var = [var for sublist in insig_altspec_vars for var in sublist.split("_")]
74
+ gen_coeff = [var for var in param.asvarnames if var in gen_var]
75
+
76
+ if asvars_sig:
77
+ redund_vars = [var for var in gen_coeff if any(var in sublist for sublist in asvars_sig)]
78
+ asvars_sig.extend([var for var in gen_coeff if var not in redund_vars])
79
+ rem_asvars = sorted(list(set(asvars_sig)))
80
+ else:
81
+ rem_asvars = gen_coeff
82
+ if not rem_asvars:
83
+ rem_asvars = sorted(list(set(asvars_sig)))
84
+
85
+ rem_class_params_spec = copy.deepcopy(class_params_spec)
86
+ rem_member_params_spec = copy.deepcopy(member_params_spec)
87
+
88
+ return rem_asvars, rem_class_params_spec, rem_member_params_spec
89
+
90
+
91
+ def _remove_insig_isvars(isvars, insig, param):
92
+ insig_isvars = []
93
+ for var in param.isvarnames:
94
+ insig_isvar = [name for name in insig if name.startswith(var)]
95
+ insig_isvars.extend(insig_isvar)
96
+
97
+ remove_isvars = []
98
+ remove_isvars.extend(part.split(".") for part in insig_isvars)
99
+
100
+ remove_isvar = [var for var in remove_isvars if var in isvars]
101
+
102
+ dict_insig_isvar = {var: remove_isvar.count(var) for var in remove_isvar}
103
+
104
+ rem_isvar = [k for k, v in dict_insig_isvar.items() if v == (len(param.choice_set) - 1)]
105
+
106
+ isvars_revised = [var for var in isvars if var not in rem_isvar]
107
+ isvars_revised.extend(param.ps_isvars) # prespecified: always kept
108
+
109
+ rem_isvars = sorted(list(set(isvars_revised)))
110
+ return rem_isvars
111
+
112
+
113
+ def _remove_insig_randvars(insig, randvars, rem_asvars, param):
114
+ insig_sd = [var for var in insig if var.startswith('sd.')]
115
+ insig_sd_rem = [str(var).replace('sd.', '') for var in insig_sd]
116
+
117
+ remove_rv = [var for var in insig_sd_rem if
118
+ var not in param.ps_randvars.keys() or var not in rem_asvars]
119
+
120
+ rem_rand_vars = {var: val for var, val in randvars.items() if var in rem_asvars and var not in remove_rv}
121
+ rem_rand_vars.update({var: val for var, val in param.ps_randvars.items()
122
+ if var in rem_asvars and val != 'f'}) # prespecified: always kept
123
+
124
+ for var in param.ps_corvars:
125
+ if var in rem_asvars and var not in rem_rand_vars.keys():
126
+ if remove_rv:
127
+ rem_rand_vars.update({var: np.random.choice(remove_rv)})
128
+
129
+ return rem_rand_vars
130
+
131
+
132
+ def _remove_insig_bcvars(insig, bcvars, rem_asvars, param):
133
+ ns_lambda = [x for x in insig if x.startswith('lambda.')]
134
+ ns_bctransvar = [str(i).replace('lambda.', '') for i in ns_lambda
135
+ if str(i).replace('lambda.', '') not in param.ps_bcvars] # prespecified: never marked for removal
136
+ rem_bcvars = [var for var in bcvars if var in rem_asvars and var not in ns_bctransvar
137
+ and var not in param.ps_corvars]
138
+ return rem_bcvars
139
+
140
+
141
+ def _remove_insig_corvars(insig, corvars, rem_randvars, rem_bcvars, all_pvalues, all_coeff_names, param):
142
+ """
143
+ Remove insignificant correlated variables using a row-based Cholesky
144
+ criterion: a variable is removed from corvars only if NONE of its
145
+ off-diagonal correlations with other corvars are significant. If at
146
+ least one correlation is significant, the variable stays. Prespecified
147
+ (`ps_corvars`) variables are always kept regardless of significance.
148
+ """
149
+ if not corvars or len(corvars) < 2:
150
+ return []
151
+
152
+ Kc = len(corvars)
153
+
154
+ pval_dict = dict(zip(all_coeff_names, all_pvalues))
155
+
156
+ matrix = np.ones((Kc, Kc))
157
+ for i, vi in enumerate(corvars):
158
+ for j, vj in enumerate(corvars):
159
+ if j <= i:
160
+ key1 = f"chol.{vi}.{vj}"
161
+ key2 = f"chol.{vj}.{vi}"
162
+ pval = pval_dict.get(key1, pval_dict.get(key2, 1.0))
163
+ matrix[i][j] = pval
164
+ matrix[j][i] = pval
165
+
166
+ sig_value = param.p_val
167
+ matrix_diag = np.diag(matrix).copy()
168
+ np.fill_diagonal(matrix, sig_value)
169
+
170
+ rows_insig = ~np.any(matrix < sig_value, axis=1)
171
+
172
+ np.fill_diagonal(matrix, matrix_diag)
173
+
174
+ if np.all(rows_insig):
175
+ # Even if every row is insignificant, prespecified corvars must stay
176
+ rem_corvars = [var for var in corvars if var in param.ps_corvars
177
+ and var in rem_randvars.keys() and var not in rem_bcvars]
178
+ return rem_corvars if len(rem_corvars) >= 2 else []
179
+
180
+ rem_corvars = [var for var, insig_row in zip(corvars, rows_insig)
181
+ if (not insig_row or var in param.ps_corvars) # prespecified: always kept
182
+ and var in rem_randvars.keys()
183
+ and var not in rem_bcvars]
184
+
185
+ return rem_corvars if len(rem_corvars) >= 2 else []
186
+
187
+
188
+ # ------------------------------------------------------------------------
189
+ # Main entry point
190
+ # ------------------------------------------------------------------------
191
+
192
+ def BEHier(search_instance, sol, max_passes=10):
193
+ """
194
+ Backward Elimination with Hierarchical Rules.
195
+
196
+ Iteratively removes the least significant coefficient — respecting
197
+ hierarchical groups (intercepts, random mean/sd pairs, correlation
198
+ blocks, Box-Cox transforms) and any prespecified (`ps_*`) exception —
199
+ re-estimating after each removal and keeping the change only if it
200
+ improves BIC. Stops when every remaining coefficient is significant
201
+ (or prespecified) or when `max_passes` is reached.
202
+
203
+ `search_instance` is only used to call `evaluate_model(sol)`.
204
+ """
205
+ param = search_instance.param
206
+
207
+ np_state = np.random.get_state()
208
+ rnd_state = random.getstate()
209
+
210
+ if sol['pvalues'] is None or len(sol['pvalues']) == 0 or len(sol['coeff_names']) == 0:
211
+ print("No p-values or coefficient names available for BEHier. Returning original solution.")
212
+ return sol
213
+
214
+ as_vars, is_vars, rand_vars, bc_vars, corvars, asc_ind = get_components(sol)
215
+ asvars, isvars, randvars, bcvars = as_vars, is_vars, rand_vars, bc_vars
216
+
217
+ intercept_names = [f'intercept.{a}' for a in param.choice_set if a != param.base_alt]
218
+
219
+ cleanup_pass = 0
220
+ skipped = set()
221
+ skipped_vars = set()
222
+ best_spec = {k: sol[k] for k in ['asvars','isvars','randvars','bcvars','corvars',
223
+ 'bic','pvalues','coeff_names','model','aic','loglik','mae','asc_ind']}
224
+
225
+ print(f"\n{'='*60}")
226
+ print(f"BEHier — Solution {sol['sol_num']} | {sol['model_n']} | BIC: {round(float(sol['bic']), 4)}")
227
+ print(f"{'='*60}")
228
+
229
+ while cleanup_pass < max_passes and sol['pvalues'] is not None:
230
+
231
+ pvalues = sol['pvalues']
232
+ coeff_names = sol['coeff_names']
233
+ pval_dict = dict(zip(coeff_names, pvalues))
234
+
235
+ # Pick worst insignificant coefficient not already skipped
236
+ sorted_insig = sorted(
237
+ [(coeff_names[i], float(pvalues[i])) for i in range(len(pvalues))
238
+ if float(pvalues[i]) > param.p_val and coeff_names[i] not in skipped],
239
+ key=lambda x: -x[1])
240
+
241
+ if not sorted_insig:
242
+ break
243
+
244
+ worst_name, max_pval = sorted_insig[0]
245
+ var_name = worst_name.replace('sd.', '').replace('chol.', '').split('.')[0]
246
+
247
+ all_insig = [(str(coeff_names[i]), round(float(pvalues[i]), 3))
248
+ for i in range(len(pvalues)) if float(pvalues[i]) > param.p_val]
249
+
250
+ print(f"\n--- BEHier pass {cleanup_pass + 1} ---")
251
+ print(f" BIC before : {round(float(sol['bic']), 4)}")
252
+ print(f" Insignificant: {all_insig}")
253
+ print(f" Worst : {worst_name} (p={round(max_pval, 3)})")
254
+
255
+ insig = []
256
+
257
+ # ----------------------------------------------------------------
258
+ # Case 1 — Intercepts: remove all jointly or not at all
259
+ # ----------------------------------------------------------------
260
+ if worst_name in intercept_names:
261
+ all_intercepts_insig = all(pval_dict.get(n, 1.0) > param.p_val for n in intercept_names)
262
+ if all_intercepts_insig:
263
+ insig = intercept_names
264
+ print(f" [Intercept] : all intercepts insignificant — removing all jointly.")
265
+ else:
266
+ sig = [n for n in intercept_names if pval_dict.get(n, 1.0) <= param.p_val]
267
+ print(f" [Intercept] : {sig} still significant — keeping all intercepts.")
268
+ skipped.update(intercept_names)
269
+ continue
270
+
271
+ # ----------------------------------------------------------------
272
+ # Case 2 — Fixed parameter
273
+ # ----------------------------------------------------------------
274
+ elif worst_name in asvars and var_name not in randvars and var_name not in bcvars:
275
+ insig = [worst_name]
276
+ print(f" [Fixed] : removing {worst_name} (p={round(max_pval, 3)}).")
277
+
278
+ # ----------------------------------------------------------------
279
+ # Case 3 — Random parameter (mean or sd)
280
+ # ----------------------------------------------------------------
281
+ elif var_name in randvars and not worst_name.startswith('chol.') and var_name not in corvars:
282
+ sd_name = f'sd.{var_name}'
283
+ chol_diag = f'chol.{var_name}.{var_name}'
284
+ sd_pval = pval_dict.get(sd_name, pval_dict.get(chol_diag, 0.0))
285
+ mean_pval = pval_dict.get(var_name, 0.0)
286
+ sd_insig = sd_pval > param.p_val
287
+ mean_insig= mean_pval > param.p_val
288
+
289
+ if sd_insig:
290
+ # Mean sig, sd insig — OR — Mean insig, sd insig: remove sd
291
+ insig = [sd_name] if sd_name in pval_dict else [chol_diag]
292
+ print(f" [Random] : sd insignificant (p={round(sd_pval,3)}) — removing sd of {var_name}.")
293
+ else:
294
+ # Mean insig, sd sig — keep both
295
+ print(f" [Random] : mean of {var_name} insig but sd significant (p={round(sd_pval,3)}) — keeping both.")
296
+ skipped.add(worst_name)
297
+ continue
298
+
299
+ # ----------------------------------------------------------------
300
+ # Case 4 — Cholesky off-diagonal: remove var from corvars if all cross-chol insig
301
+ # ----------------------------------------------------------------
302
+ elif worst_name.startswith('chol.'):
303
+ parts = worst_name.split('.')
304
+ v, u = parts[1], parts[2]
305
+ is_diag = (v == u)
306
+
307
+ if not is_diag:
308
+ if v in skipped_vars:
309
+ skipped.add(worst_name)
310
+ print(f" [Chol] : {v} already decided — skipping {worst_name}.")
311
+ continue
312
+ cross_chol = [k for k in pval_dict if k.startswith('chol.') and
313
+ k != f'chol.{v}.{v}' and
314
+ (f'.{v}.' in k or k.endswith(f'.{v}'))]
315
+ all_cross_insig = all(pval_dict[k] > param.p_val for k in cross_chol) if cross_chol else True
316
+ if all_cross_insig:
317
+ insig = cross_chol if cross_chol else [worst_name]
318
+ print(f" [Chol] : all off-diagonal chol for {v} insignificant — removing {v} from corvars.")
319
+ else:
320
+ sig_cross = [k for k in cross_chol if pval_dict[k] <= param.p_val]
321
+ print(f" [Chol] : {sig_cross} still significant — keeping {v} in corvars.")
322
+ skipped_vars.add(v)
323
+ skipped.add(worst_name)
324
+ continue
325
+ else:
326
+ # Diagonal chol — treat as sd
327
+ sd_insig = pval_dict.get(worst_name, 0.0) > param.p_val
328
+ if sd_insig:
329
+ insig = [f'sd.{v}']
330
+ print(f" [Chol diag] : {worst_name} insignificant — removing sd of {v}.")
331
+ else:
332
+ skipped.add(worst_name)
333
+ continue
334
+
335
+ # ----------------------------------------------------------------
336
+ # Case 5 — Boxcox: remove from bcvars, keep in randvars if applicable
337
+ # ----------------------------------------------------------------
338
+ elif var_name in bcvars:
339
+ insig = [worst_name]
340
+ if var_name in randvars:
341
+ print(f" [Boxcox] : lambda of {var_name} insig — removing from bcvars, remains in randvars.")
342
+ else:
343
+ print(f" [Boxcox] : lambda of {var_name} insig — removing from bcvars, reverting to fixed.")
344
+
345
+ else:
346
+ skipped.add(worst_name)
347
+ print(f" [Skip] : {worst_name} — no matching case, skipping.")
348
+ continue
349
+
350
+ # ----------------------------------------------------------------
351
+ # Apply removal and check if spec changed
352
+ # ----------------------------------------------------------------
353
+ if not insig:
354
+ skipped.add(worst_name)
355
+ continue
356
+
357
+ rem_asvars, _, _ = _remove_insig_asvars(asvars, insig, bcvars, pvalues, [], None, None, param)
358
+ if len(rem_asvars) == 0:
359
+ rem_asvars = list(asvars)
360
+ rem_isvars = _remove_insig_isvars(isvars, insig, param)
361
+ rem_randvars = _remove_insig_randvars(insig, randvars, rem_asvars, param)
362
+ rem_bcvars = _remove_insig_bcvars(insig, bcvars, rem_asvars, param)
363
+ rem_corvars = _remove_insig_corvars(
364
+ insig, corvars, rem_randvars, rem_bcvars,
365
+ sol['pvalues'], sol['coeff_names'], param) if worst_name.startswith('chol.') else list(corvars)
366
+
367
+ if param.ps_intercept is not None:
368
+ rem_asc_ind = param.ps_intercept # prespecified: never overridden by significance
369
+ else:
370
+ rem_asc_ind = False if (insig and all(n in intercept_names for n in insig)) else asc_ind
371
+
372
+ spec_changed = (
373
+ sorted(rem_asvars) != sorted(asvars) or
374
+ sorted(rem_isvars) != sorted(isvars) or
375
+ dict(sorted(rem_randvars.items())) != dict(sorted(randvars.items())) or
376
+ sorted(rem_bcvars) != sorted(bcvars) or
377
+ sorted(rem_corvars) != sorted(corvars) or
378
+ rem_asc_ind != asc_ind
379
+ )
380
+
381
+ if not spec_changed:
382
+ skipped.add(worst_name)
383
+ print(f" Result : no spec change — skipping.")
384
+ continue
385
+
386
+ if spec_changed and sorted(rem_corvars) != sorted(corvars):
387
+ skipped_vars = set()
388
+
389
+ sol['asvars'] = rem_asvars
390
+ sol['isvars'] = rem_isvars
391
+ sol['randvars']= rem_randvars
392
+ sol['bcvars'] = rem_bcvars
393
+ sol['corvars'] = rem_corvars
394
+ sol['asc_ind'] = rem_asc_ind
395
+
396
+ aic, bic, loglik, mae, asvars, isvars, randvars, bcvars, corvars, converged, pvalues, coeff_names, sol = \
397
+ search_instance.evaluate_model(sol)
398
+
399
+ asc_ind = sol['asc_ind']
400
+ sol['aic'], sol['bic'], sol['loglik'], sol['mae'] = aic, bic, loglik, mae
401
+ sol['pvalues'], sol['coeff_names'] = pvalues, coeff_names
402
+ passes = isinstance(loglik, float) and math.isfinite(loglik)
403
+
404
+ print(f" BIC after : {round(float(bic), 4)} | converged: {converged} | valid: {passes}")
405
+
406
+ if passes and float(bic) < float(best_spec['bic']):
407
+ print(f" Result : improved — new best BIC: {round(float(bic), 4)}")
408
+ best_spec = {k: sol[k] for k in ['asvars','isvars','randvars','bcvars','corvars',
409
+ 'bic','pvalues','coeff_names','model','aic','loglik','mae','asc_ind']}
410
+ skipped = set()
411
+ skipped_vars = set()
412
+ else:
413
+ sol.update(best_spec)
414
+ asc_ind = sol['asc_ind']
415
+ asvars, isvars, randvars, bcvars, corvars = \
416
+ sol['asvars'], sol['isvars'], sol['randvars'], sol['bcvars'], sol['corvars']
417
+ skipped.update(intercept_names if worst_name in intercept_names else [worst_name])
418
+ print(f" Result : BIC did not improve — reverting, skipping {worst_name}.")
419
+
420
+ cleanup_pass += 1
421
+
422
+ update_objectives(param, sol)
423
+ np.random.set_state(np_state)
424
+ random.setstate(rnd_state)
425
+ return sol
@@ -36,7 +36,7 @@ infinity = float('inf')
36
36
  class MixedLogit(DiscreteChoiceModel):
37
37
  def __init__(self, halton_opts=None, distributions=['n', 'ln', 't', 'tn', 'u'], _jax=True):
38
38
  super().__init__(_jax)
39
- self.descr = "Mixed Logit"
39
+ self.descr = "MXL"
40
40
  self.halton_opts = halton_opts
41
41
  self.draws_generator = Draws(k=len(distributions), halton_opts=halton_opts, rvdist=distributions)
42
42
  self.random_parameters = RandomParameters(distributions or []) # Initialize RandomParameters
@@ -459,7 +459,7 @@ class MixedLogit(DiscreteChoiceModel):
459
459
  bnds = [[bound[1][0]] * bound[1][1] for bound in bound_dict.items() if bound[1][1] > 0]
460
460
  bnds = list(itertools.chain.from_iterable(bnds))
461
461
 
462
- print(f"[MXL] Starting beta seed length={n_coeff}, first_values={betas[:min(8, len(betas))]!r}")
462
+ #print(f"[MXL] Starting beta seed length={n_coeff}, first_values={betas[:min(8, len(betas))]!r}")
463
463
 
464
464
  if self.de_init:
465
465
  _v = getattr(self, 'verbose_de', False)
@@ -527,12 +527,12 @@ class MixedLogit(DiscreteChoiceModel):
527
527
  # }
528
528
  # '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
529
529
 
530
- print(f"[MXL] Minimization start with betas first_values={betas[:min(8, len(betas))]!r}")
530
+ #print(f"[MXL] Minimization start with betas first_values={betas[:min(8, len(betas))]!r}")
531
531
  try:
532
532
  before_fun = self.get_loglik_gradient(betas, self.X, self.y, self.panel_info,
533
533
  draws, drawstrans, self.weights,
534
534
  self.avail, self.batch_size)[0]
535
- print(f"[MXL] Minimization obj before={before_fun:.6g}")
535
+ #print(f"[MXL] Minimization obj before={before_fun:.6g}")
536
536
  except Exception as e:
537
537
  print(f"[MXL] Could not evaluate initial objective before minimization: {e}")
538
538
 
@@ -570,6 +570,20 @@ class MixedLogit(DiscreteChoiceModel):
570
570
  if getattr(self, '_jax', False):
571
571
  jax_result = self.optimize_jax(betas, draws, drawstrans)
572
572
  if jax_result is not None:
573
+ beta_segment_names = ["Bf", "Br_b", "chol", "Br_w", "Bftrans",
574
+ "flmbda", "Brtrans_b", "Brtrans_w", "rlmda"]
575
+ iterations = [self.Kf, self.Kr, self.Kchol, self.Kbw, self.Kftrans,
576
+ self.Kftrans, self.Krtrans, self.Krtrans, self.Krtrans]
577
+ self.var_list = self.split_betas(jax_result['x'], iterations, beta_segment_names)
578
+ self.chol_mat = self.construct_chol_mat(
579
+ self.var_list['chol'], self.var_list['Br_w'], self.var_list['Brtrans_w'])
580
+
581
+ p = self.compute_probabilities(jax_result['x'], self.X, self.panel_info,
582
+ draws, drawstrans, self.avail, self.var_list, self.chol_mat)
583
+ self.choice_pred_prob = np.mean(p, axis=3)
584
+ self.ind_pred_prob = np.mean(self.choice_pred_prob, axis=1)
585
+ self.pred_prob = np.mean(self.ind_pred_prob, axis=0)
586
+ self.prob_full = p
573
587
  self.post_process(jax_result, self.Xnames, self.N)
574
588
  return
575
589