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.
Files changed (51) hide show
  1. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/PKG-INFO +2 -2
  2. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/pyproject.toml +2 -2
  3. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/harmony.py +2 -2
  4. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/latent_class.py +126 -3
  5. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/search.py +1 -1
  6. searchlibrium-0.0.115/src/SearchLibrium/version.txt +1 -0
  7. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/PKG-INFO +2 -2
  8. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/requires.txt +1 -1
  9. searchlibrium-0.0.113/src/SearchLibrium/version.txt +0 -1
  10. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/README.md +0 -0
  11. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/setup.cfg +0 -0
  12. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Halton.py +0 -0
  13. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/MixedLogit.py +0 -0
  14. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  15. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/RandomP.py +0 -0
  16. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  17. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  18. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/__init__.py +0 -0
  19. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/__main__.py +0 -0
  20. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/_choice_model.py +0 -0
  21. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/_device.py +0 -0
  22. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/banditsa.py +0 -0
  23. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/bhhh/minimize.py +0 -0
  24. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/boxcox_functions.py +0 -0
  25. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/call_meta.py +0 -0
  26. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/constraints_builder.py +0 -0
  27. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/main.py +0 -0
  28. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/main_debug.py +0 -0
  29. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mdcev.py +0 -0
  30. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/misc.py +0 -0
  31. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixed_logit.py +0 -0
  32. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixed_nested.py +0 -0
  33. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/mixedrrm.py +0 -0
  34. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_logit.py +0 -0
  35. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_nested.py +0 -0
  36. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/multinomial_probit.py +0 -0
  37. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/ordered_logit.py +0 -0
  38. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  39. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/rrm.py +0 -0
  40. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/sapbil.py +0 -0
  41. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/selection_models.py +0 -0
  42. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/setup.py +0 -0
  43. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/siman.py +0 -0
  44. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_lc_de.py +0 -0
  45. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_mario_searches.py +0 -0
  46. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
  47. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium/threshold.py +0 -0
  48. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
  49. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  50. {searchlibrium-0.0.113 → searchlibrium-0.0.115}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  51. {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.113
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.0.0
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<2.0.0",
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.113"
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.row_stack((self.param.avail, self.param.test_avail))
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.row_stack((avail_latent_ii, self.param.test_avail_latent[ii]))
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
- """Fit the latent class model via EM.
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 = self._fit_em_once(rng, betas0=init_betas, class_probs0=init_probs)
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 == 'O': # {
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.113
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.0.0
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,4 +1,4 @@
1
- numpy<2.0.0
1
+ numpy<2.3,>=1.24
2
2
  pandas>=2.0.0
3
3
  scipy>=1.10
4
4
  scikit-learn>=1.3.1
@@ -1 +0,0 @@
1
- 0.0.113