SearchLibrium 0.0.113__tar.gz → 0.0.115__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.113 → searchlibrium-0.0.115}/PKG-INFO +2 -2
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/pyproject.toml +2 -2
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/harmony.py +2 -2
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/latent_class.py +126 -3
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/search.py +1 -1
- searchlibrium-0.0.115/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/PKG-INFO +2 -2
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/requires.txt +1 -1
- searchlibrium-0.0.113/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/README.md +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/setup.cfg +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.113 → searchlibrium-0.0.115}/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.115
|
|
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>
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
17
17
|
Requires-Python: >=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
-
Requires-Dist: numpy<2.
|
|
19
|
+
Requires-Dist: numpy<2.3,>=1.24
|
|
20
20
|
Requires-Dist: pandas>=2.0.0
|
|
21
21
|
Requires-Dist: scipy>=1.10
|
|
22
22
|
Requires-Dist: scikit-learn>=1.3.1
|
|
@@ -37,7 +37,7 @@ classifiers = [
|
|
|
37
37
|
"Topic :: Scientific/Engineering :: Mathematics",
|
|
38
38
|
]
|
|
39
39
|
dependencies = [
|
|
40
|
-
"numpy
|
|
40
|
+
"numpy>=1.24,<2.3",
|
|
41
41
|
"pandas>=2.0.0",
|
|
42
42
|
"scipy>=1.10",
|
|
43
43
|
"scikit-learn>=1.3.1",
|
|
@@ -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.115"
|
|
64
64
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
65
65
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
66
66
|
commit = true
|
|
@@ -815,7 +815,7 @@ class HarmonySearch(Search):
|
|
|
815
815
|
if self.nb_crit > 1:
|
|
816
816
|
# {
|
|
817
817
|
if self.param.avail is not None:
|
|
818
|
-
avail = np.
|
|
818
|
+
avail = np.vstack((self.param.avail, self.param.test_avail))
|
|
819
819
|
|
|
820
820
|
if self.param.avail_latent is not None:
|
|
821
821
|
# {
|
|
@@ -823,7 +823,7 @@ class HarmonySearch(Search):
|
|
|
823
823
|
for ii, avail_latent_ii in enumerate(self.param.avail_latent):
|
|
824
824
|
# {
|
|
825
825
|
if avail_latent_ii is not None:
|
|
826
|
-
avail_latent[ii] = np.
|
|
826
|
+
avail_latent[ii] = np.vstack((avail_latent_ii, self.param.test_avail_latent[ii]))
|
|
827
827
|
# }
|
|
828
828
|
# }
|
|
829
829
|
|
|
@@ -300,6 +300,25 @@ class LatentClassMixedLogit:
|
|
|
300
300
|
raise ValueError("class_probs0 must have length n_classes.")
|
|
301
301
|
return self._normalize_class_probs(class_probs0)
|
|
302
302
|
|
|
303
|
+
def _em_step(self, betas, class_probs):
|
|
304
|
+
"""Single E+M step. Returns (new_betas, new_class_probs, loglik, posterior)."""
|
|
305
|
+
log_choice, _ = self._log_choice_probs_np(betas)
|
|
306
|
+
log_joint = log_choice + np.log(np.clip(class_probs, 1e-300, None))[None, :]
|
|
307
|
+
log_denom = logsumexp(log_joint, axis=1, keepdims=True)
|
|
308
|
+
posterior = np.exp(log_joint - log_denom)
|
|
309
|
+
loglik = float(log_denom.sum())
|
|
310
|
+
new_class_probs = self._normalize_class_probs(posterior.mean(axis=0))
|
|
311
|
+
new_betas = betas.copy()
|
|
312
|
+
for c in range(self.n_classes):
|
|
313
|
+
new_betas[c] = self._weighted_m_step(betas[c], posterior[:, c])
|
|
314
|
+
return new_betas, new_class_probs, loglik, posterior
|
|
315
|
+
|
|
316
|
+
def _squarem_loglik(self, betas, class_probs):
|
|
317
|
+
"""Log-likelihood at (betas, class_probs) without running the M-step."""
|
|
318
|
+
log_choice, _ = self._log_choice_probs_np(betas)
|
|
319
|
+
log_joint = log_choice + np.log(np.clip(class_probs, 1e-300, None))[None, :]
|
|
320
|
+
return float(logsumexp(log_joint, axis=1).sum())
|
|
321
|
+
|
|
303
322
|
def _fit_em_once(self, rng, betas0=None, class_probs0=None):
|
|
304
323
|
betas = self._make_initial_betas(rng, betas0=betas0)
|
|
305
324
|
class_probs = self._make_initial_class_probs(class_probs0=class_probs0)
|
|
@@ -332,9 +351,105 @@ class LatentClassMixedLogit:
|
|
|
332
351
|
"iterations": iteration,
|
|
333
352
|
}
|
|
334
353
|
|
|
354
|
+
def _fit_squarem_once(self, rng, betas0=None, class_probs0=None):
|
|
355
|
+
"""Fit via SQUAREM-accelerated EM (Varadhan & Roland 2008).
|
|
356
|
+
|
|
357
|
+
Each outer iteration uses two EM steps to build a squared extrapolation
|
|
358
|
+
that typically reaches the fixed-point in far fewer EM calls than
|
|
359
|
+
standard EM. Convergence is monitored via log-likelihood change.
|
|
360
|
+
|
|
361
|
+
Returns the same dict as ``_fit_em_once``, plus key ``em_calls`` (total
|
|
362
|
+
number of E+M steps executed, for fair comparison with standard EM).
|
|
363
|
+
"""
|
|
364
|
+
betas = self._make_initial_betas(rng, betas0=betas0)
|
|
365
|
+
class_probs = self._make_initial_class_probs(class_probs0=class_probs0)
|
|
366
|
+
|
|
367
|
+
n_beta = self.n_classes * self.K
|
|
368
|
+
|
|
369
|
+
def _pack(b, cp):
|
|
370
|
+
return np.concatenate([b.ravel(), cp])
|
|
371
|
+
|
|
372
|
+
def _unpack(theta):
|
|
373
|
+
b = theta[:n_beta].reshape(self.n_classes, self.K)
|
|
374
|
+
cp = self._normalize_class_probs(theta[n_beta:])
|
|
375
|
+
return b, cp
|
|
376
|
+
|
|
377
|
+
theta = _pack(betas, class_probs)
|
|
378
|
+
prev_loglik = -np.inf
|
|
379
|
+
converged = False
|
|
380
|
+
posterior = np.full((self.N, self.n_classes), 1.0 / self.n_classes)
|
|
381
|
+
em_calls = 0
|
|
382
|
+
|
|
383
|
+
for outer_iter in range(1, self.maxiter + 1):
|
|
384
|
+
b0, cp0 = _unpack(theta)
|
|
385
|
+
|
|
386
|
+
b1, cp1, ll1, _p1 = self._em_step(b0, cp0)
|
|
387
|
+
em_calls += 1
|
|
388
|
+
theta1 = _pack(b1, cp1)
|
|
389
|
+
|
|
390
|
+
b2, cp2, ll2, post2 = self._em_step(b1, cp1)
|
|
391
|
+
em_calls += 1
|
|
392
|
+
theta2 = _pack(b2, cp2)
|
|
393
|
+
|
|
394
|
+
r = theta1 - theta
|
|
395
|
+
v = theta2 - 2.0 * theta1 + theta
|
|
396
|
+
norm_v = np.linalg.norm(v)
|
|
397
|
+
|
|
398
|
+
if norm_v < 1e-14:
|
|
399
|
+
# v negligible — no extrapolation gain; accept two standard steps
|
|
400
|
+
theta = theta2
|
|
401
|
+
loglik = ll2
|
|
402
|
+
posterior = post2
|
|
403
|
+
else:
|
|
404
|
+
alpha = min(-np.linalg.norm(r) / norm_v, -1.0)
|
|
405
|
+
|
|
406
|
+
# Step-halving: shrink |α − (−1)| by half each attempt
|
|
407
|
+
accepted = False
|
|
408
|
+
b_p, cp_p, ll_p = b2, cp2, ll2 # default fallback
|
|
409
|
+
for _ in range(10):
|
|
410
|
+
theta_prop = theta - 2.0 * alpha * r + alpha ** 2 * v
|
|
411
|
+
b_cand, cp_cand = _unpack(theta_prop)
|
|
412
|
+
ll_cand = self._squarem_loglik(b_cand, cp_cand)
|
|
413
|
+
if np.isfinite(ll_cand) and ll_cand >= ll1:
|
|
414
|
+
b_p, cp_p, ll_p = b_cand, cp_cand, ll_cand
|
|
415
|
+
accepted = True
|
|
416
|
+
break
|
|
417
|
+
alpha = (alpha + (-1.0)) / 2.0 # halve toward α = −1
|
|
418
|
+
|
|
419
|
+
if accepted:
|
|
420
|
+
theta = _pack(b_p, cp_p)
|
|
421
|
+
loglik = ll_p
|
|
422
|
+
# recompute posterior at the accepted extrapolated point
|
|
423
|
+
log_choice, _ = self._log_choice_probs_np(b_p)
|
|
424
|
+
log_joint = log_choice + np.log(np.clip(cp_p, 1e-300, None))[None, :]
|
|
425
|
+
log_denom = logsumexp(log_joint, axis=1, keepdims=True)
|
|
426
|
+
posterior = np.exp(log_joint - log_denom)
|
|
427
|
+
else:
|
|
428
|
+
# Fall back to two standard EM steps
|
|
429
|
+
theta = theta2
|
|
430
|
+
loglik = ll2
|
|
431
|
+
posterior = post2
|
|
432
|
+
|
|
433
|
+
if abs(loglik - prev_loglik) < self.tol:
|
|
434
|
+
converged = True
|
|
435
|
+
break
|
|
436
|
+
prev_loglik = loglik
|
|
437
|
+
|
|
438
|
+
b_final, cp_final = _unpack(theta)
|
|
439
|
+
return {
|
|
440
|
+
"betas": b_final,
|
|
441
|
+
"class_probs": cp_final,
|
|
442
|
+
"posterior": posterior,
|
|
443
|
+
"loglik": loglik,
|
|
444
|
+
"converged": converged,
|
|
445
|
+
"iterations": em_calls, # EM-equivalent step count for fair comparison
|
|
446
|
+
"em_calls": em_calls,
|
|
447
|
+
}
|
|
448
|
+
|
|
335
449
|
def fit(self, betas0=None, class_probs0=None,
|
|
336
|
-
de_init=False, de_popsize=6, de_maxiter=20, de_tol=0.01, de_seed=None
|
|
337
|
-
|
|
450
|
+
de_init=False, de_popsize=6, de_maxiter=20, de_tol=0.01, de_seed=None,
|
|
451
|
+
em_method="squarem"):
|
|
452
|
+
"""Fit the latent class model via EM or SQUAREM-accelerated EM.
|
|
338
453
|
|
|
339
454
|
Parameters
|
|
340
455
|
----------
|
|
@@ -347,7 +462,13 @@ class LatentClassMixedLogit:
|
|
|
347
462
|
``betas0`` when True).
|
|
348
463
|
de_popsize, de_maxiter, de_tol, de_seed
|
|
349
464
|
DE hyper-parameters forwarded to :meth:`_de_warm_start`.
|
|
465
|
+
em_method : {'standard', 'squarem'}
|
|
466
|
+
EM solver. ``'squarem'`` applies the Squared Extrapolation Method
|
|
467
|
+
(Varadhan & Roland 2008) to accelerate convergence.
|
|
350
468
|
"""
|
|
469
|
+
if em_method not in ("standard", "squarem"):
|
|
470
|
+
raise ValueError(f"em_method must be 'standard' or 'squarem', got {em_method!r}")
|
|
471
|
+
|
|
351
472
|
if de_init:
|
|
352
473
|
betas0 = self._de_warm_start(
|
|
353
474
|
popsize=de_popsize,
|
|
@@ -357,13 +478,14 @@ class LatentClassMixedLogit:
|
|
|
357
478
|
)
|
|
358
479
|
|
|
359
480
|
best_result = None
|
|
481
|
+
_fit_once = self._fit_squarem_once if em_method == "squarem" else self._fit_em_once
|
|
360
482
|
|
|
361
483
|
for init_idx in range(self.n_init):
|
|
362
484
|
seed = self.random_state + init_idx
|
|
363
485
|
rng = np.random.default_rng(seed)
|
|
364
486
|
init_betas = betas0 if init_idx == 0 else None
|
|
365
487
|
init_probs = class_probs0 if init_idx == 0 else None
|
|
366
|
-
result =
|
|
488
|
+
result = _fit_once(rng, betas0=init_betas, class_probs0=init_probs)
|
|
367
489
|
if best_result is None or result["loglik"] > best_result["loglik"]:
|
|
368
490
|
best_result = result
|
|
369
491
|
|
|
@@ -373,6 +495,7 @@ class LatentClassMixedLogit:
|
|
|
373
495
|
self.loglik = best_result["loglik"]
|
|
374
496
|
self.converged = best_result["converged"]
|
|
375
497
|
self.total_iter = best_result["iterations"]
|
|
498
|
+
self.em_method = em_method
|
|
376
499
|
self.coeff_est = self.class_betas.ravel()
|
|
377
500
|
self.coeff_names = [
|
|
378
501
|
f"class_{class_idx + 1}_{name}"
|
|
@@ -2212,7 +2212,7 @@ class Search():
|
|
|
2212
2212
|
for sol in solutions: # {
|
|
2213
2213
|
bool_arr = []
|
|
2214
2214
|
for i, val_i in copied_new_sol.items(): # {
|
|
2215
|
-
if hasattr(sol[i], 'dtype') and sol[i].dtype ==
|
|
2215
|
+
if hasattr(sol[i], 'dtype') and sol[i].dtype == np.object_: # {
|
|
2216
2216
|
obj_arr1 = np.concatenate(sol[i])
|
|
2217
2217
|
obj_arr2 = np.concatenate(val_i)
|
|
2218
2218
|
bool_arr.append(len(obj_arr1) == len(obj_arr2) and np.all(obj_arr1 == obj_arr2))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.115
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.115
|
|
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>
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
17
17
|
Requires-Python: >=3.9
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
|
-
Requires-Dist: numpy<2.
|
|
19
|
+
Requires-Dist: numpy<2.3,>=1.24
|
|
20
20
|
Requires-Dist: pandas>=2.0.0
|
|
21
21
|
Requires-Dist: scipy>=1.10
|
|
22
22
|
Requires-Dist: scikit-learn>=1.3.1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.113
|
|
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.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|