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.
- {searchlibrium-0.0.142/src/SearchLibrium.egg-info → searchlibrium-0.0.144}/PKG-INFO +1 -1
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/pyproject.toml +1 -1
- searchlibrium-0.0.144/src/SearchLibrium/BEHier.py +425 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/MixedLogit.py +18 -4
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/_choice_model.py +400 -1
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/harmony.py +64 -35
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/hspbil.py +6 -1
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/latent_class.py +58 -3
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_logit.py +2 -2
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/search.py +114 -45
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/siman.py +3 -2
- searchlibrium-0.0.144/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/SOURCES.txt +1 -0
- searchlibrium-0.0.142/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/MANIFEST.in +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/README.md +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/setup.cfg +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.142 → searchlibrium-0.0.144}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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 = "
|
|
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
|
|