SearchLibrium 0.0.239__tar.gz → 0.0.240__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 (78) hide show
  1. {searchlibrium-0.0.239/src/SearchLibrium.egg-info → searchlibrium-0.0.240}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/pyproject.toml +1 -1
  3. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/latent_class.py +42 -16
  4. searchlibrium-0.0.240/src/SearchLibrium/version.txt +1 -0
  5. {searchlibrium-0.0.239 → searchlibrium-0.0.240/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
  6. searchlibrium-0.0.239/src/SearchLibrium/version.txt +0 -1
  7. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/LICENSE +0 -0
  8. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/MANIFEST.in +0 -0
  9. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/README.md +0 -0
  10. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/setup.cfg +0 -0
  11. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/BEHier.py +0 -0
  12. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/Halton.py +0 -0
  13. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/MixedLogit.py +0 -0
  14. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/MixedLogitGSE.py +0 -0
  15. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  16. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/RandomP.py +0 -0
  17. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  18. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  19. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/__init__.py +0 -0
  20. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/__main__.py +0 -0
  21. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/_choice_model.py +0 -0
  22. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/_device.py +0 -0
  23. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/abm.py +0 -0
  24. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/accessibility.py +0 -0
  25. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/banditsa.py +0 -0
  26. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/boxcox_functions.py +0 -0
  27. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/call_meta.py +0 -0
  28. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/constraints_builder.py +0 -0
  29. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
  30. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/data/TravelMode.csv +0 -0
  31. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/data/electricity.csv +0 -0
  32. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/harmony.py +0 -0
  33. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/hspbil.py +0 -0
  34. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/jax_utils.py +0 -0
  35. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/larch_compat.py +0 -0
  36. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/larch_models.py +0 -0
  37. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/logistic_regression.py +0 -0
  38. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/main.py +0 -0
  39. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/main_debug.py +0 -0
  40. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/mdcev.py +0 -0
  41. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/misc.py +0 -0
  42. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/mixed_logit.py +0 -0
  43. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/mixed_nested.py +0 -0
  44. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/mixedrrm.py +0 -0
  45. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/multinomial_logit.py +0 -0
  46. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/multinomial_nested.py +0 -0
  47. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/multinomial_probit.py +0 -0
  48. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/numba_engine.py +0 -0
  49. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/numba_mxl.py +0 -0
  50. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/numba_rrm.py +0 -0
  51. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/ordered_logit.py +0 -0
  52. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  53. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/predict.py +0 -0
  54. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/regularization.py +0 -0
  55. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/rrm.py +0 -0
  56. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/sample_data.py +0 -0
  57. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/sapbil.py +0 -0
  58. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/search.py +0 -0
  59. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/selection_models.py +0 -0
  60. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/setup.py +0 -0
  61. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/siman.py +0 -0
  62. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/skims.py +0 -0
  63. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/sparseea_agds.py +0 -0
  64. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/test_lc_de.py +0 -0
  65. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/test_mario_searches.py +0 -0
  66. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
  67. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/threshold.py +0 -0
  68. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium/zero_inflated_ordered_probit.py +0 -0
  69. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
  70. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  71. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  72. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium.egg-info/requires.txt +0 -0
  73. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/src/SearchLibrium.egg-info/top_level.txt +0 -0
  74. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/tests/test_jax_mask_equivalence.py +0 -0
  75. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/tests/test_larch_integration.py +0 -0
  76. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/tests/test_lc_smart_init.py +0 -0
  77. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/tests/test_per_alternative_random.py +0 -0
  78. {searchlibrium-0.0.239 → searchlibrium-0.0.240}/tests/test_zero_inflated_ordered_probit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.239
3
+ Version: 0.0.240
4
4
  Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
5
5
  Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -129,7 +129,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
129
129
  realpython = "SearchLibrium.__main__:main"
130
130
 
131
131
  [tool.bumpver]
132
- current_version = "0.0.239"
132
+ current_version = "0.0.240"
133
133
  version_pattern = "MAJOR.MINOR.PATCH"
134
134
  commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
135
135
  commit = true
@@ -507,7 +507,7 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
507
507
  if hasattr(self, cache_key):
508
508
  return getattr(self, cache_key)
509
509
 
510
- if not self._jax_enabled or len(set(self._Ks)) != 1 or self._panelled:
510
+ if not self._jax_enabled or len(set(self._Ks)) != 1:
511
511
  setattr(self, cache_key, None)
512
512
  return None
513
513
 
@@ -531,6 +531,13 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
531
531
  l2 = self.l2_penalty
532
532
  l1 = self.l1_penalty
533
533
 
534
+ # Panel aggregation: the class marginalisation is per decision-maker, so
535
+ # per-task log-choice is segment-summed into M = n_panels person totals.
536
+ panelled = bool(self._panelled)
537
+ M = int(self.n_panels)
538
+ panel_idx_b = self.jnp.asarray(self.panel_idx) if panelled else None
539
+ seg_sum = self.jax.ops.segment_sum
540
+
534
541
  def _negloglik_flat(params):
535
542
  phi = params[:n_phi]
536
543
  beta_flat = params[n_phi:n_phi + C * K]
@@ -543,11 +550,15 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
543
550
  denom = jnp.clip(exp_u.sum(axis=2, keepdims=True), 1e-300)
544
551
  probs = exp_u / denom
545
552
  chosen = jnp.clip((probs * y_b[:, None, :]).sum(axis=2), 1e-300)
546
- log_chosen = jnp.log(chosen)
553
+ log_chosen_task = jnp.log(chosen)
554
+ if panelled:
555
+ log_chosen = seg_sum(log_chosen_task, panel_idx_b, num_segments=M)
556
+ else:
557
+ log_chosen = log_chosen_task
547
558
 
548
559
  if has_memb:
549
560
  gammas = params[n_phi + C * K:].reshape(C - 1, Km)
550
- logits = jnp.zeros((N, C))
561
+ logits = jnp.zeros((M, C))
551
562
  for c in range(C - 1):
552
563
  logits = logits.at[:, c].set(X_memb @ gammas[c])
553
564
  logits = logits - jnp.max(logits, axis=1, keepdims=True)
@@ -557,7 +568,7 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
557
568
  else:
558
569
  phi_full = jnp.concatenate([phi, jnp.zeros(1)])
559
570
  log_priors_raw = phi_full - self.jax_logsumexp(phi_full)
560
- log_prior = jnp.broadcast_to(log_priors_raw[None, :], (N, C))
571
+ log_prior = jnp.broadcast_to(log_priors_raw[None, :], (M, C))
561
572
 
562
573
  log_joint = log_chosen + log_prior
563
574
  log_marg = self.jax_logsumexp(log_joint, axis=1)
@@ -1595,6 +1606,9 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
1595
1606
  ll = float(logsumexp(log_joint, axis=1).sum())
1596
1607
  ll -= self._regularize_l2_betas(betas)
1597
1608
  ll -= self._regularize_l1_betas(betas)
1609
+ if has_gamma:
1610
+ ll -= self._regularize_l2_gammas(gammas)
1611
+ ll -= self._regularize_l1_gammas(gammas)
1598
1612
  return ll
1599
1613
 
1600
1614
  def _autograd_hessian(self, params: np.ndarray) -> np.ndarray | None:
@@ -1612,10 +1626,6 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
1612
1626
  if len(set(self._Ks)) != 1:
1613
1627
  print("[LC] Autograd Hessian requires all classes to share the same variable set.")
1614
1628
  return None
1615
- if self._panelled:
1616
- # The JIT objective marginalises per task, not per person; use the
1617
- # panel-aware finite-difference Hessian instead.
1618
- return None
1619
1629
 
1620
1630
  cache_key = "_cached_autograd_hessian_fn"
1621
1631
  if hasattr(self, cache_key):
@@ -1782,25 +1792,41 @@ class LatentClassMixedLogit(DiscreteChoiceModel):
1782
1792
  info = -H_num # observed info = -hessian(loglik)
1783
1793
  se_method = "numerical-hessian (finite differences)"
1784
1794
 
1795
+ # When a membership equation is active the class-share logits (phi) do
1796
+ # not enter the likelihood — they are exactly unidentified, which makes
1797
+ # the observed-information matrix exactly singular in those directions
1798
+ # (an artefact, not a genuinely ill-conditioned model). Invert only the
1799
+ # identified block; phi rows/cols are reinstated with zero covariance.
1800
+ P = len(params)
1801
+ if has_gamma and n_phi > 0:
1802
+ keep = np.arange(n_phi, P)
1803
+ else:
1804
+ keep = np.arange(P)
1805
+ info_id = info[np.ix_(keep, keep)]
1806
+
1785
1807
  cond_number = np.nan
1786
- cov = None
1808
+ cov_id = None
1787
1809
  try:
1788
- eigvals = np.linalg.eigvalsh(info)
1810
+ eigvals = np.linalg.eigvalsh(info_id)
1789
1811
  cond_number = float(eigvals.max() / max(eigvals.min(), 1e-300))
1790
1812
 
1791
1813
  if eigvals.min() < 1e-8 * eigvals.max():
1792
1814
  ridge = 1e-6 * eigvals.max()
1793
- info_reg = info + ridge * np.eye(len(params))
1794
- cov = np.linalg.inv(info_reg)
1815
+ info_reg = info_id + ridge * np.eye(len(keep))
1816
+ cov_id = np.linalg.inv(info_reg)
1795
1817
  se_method = "hessian (ridge-regularised)"
1796
1818
  else:
1797
- cov = np.linalg.inv(info)
1819
+ cov_id = np.linalg.inv(info_id)
1798
1820
  except np.linalg.LinAlgError:
1799
- cov = np.linalg.pinv(info)
1821
+ cov_id = np.linalg.pinv(info_id)
1800
1822
  se_method = "hessian (pinv fallback)"
1801
1823
 
1802
- if cov is None:
1803
- cov = np.linalg.pinv(info)
1824
+ if cov_id is None:
1825
+ cov_id = np.linalg.pinv(info_id)
1826
+
1827
+ # Re-embed the identified covariance into the full parameter space.
1828
+ cov = np.zeros((P, P))
1829
+ cov[np.ix_(keep, keep)] = cov_id
1804
1830
 
1805
1831
  se = np.sqrt(np.clip(np.diag(cov), 0.0, None))
1806
1832
 
@@ -0,0 +1 @@
1
+ 0.0.240
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.239
3
+ Version: 0.0.240
4
4
  Summary: Automated specification search for discrete choice models (logit, mixed logit, nested logit, RRM and more) using metaheuristic optimisation
5
5
  Author: Mario Fernando Taco Morales, Alexander Paz, Prithvi Beeramole, Robert Burdett
6
6
  Author-email: Zeke Ahern <z.ahern@qut.edu.au>
@@ -1 +0,0 @@
1
- 0.0.239
File without changes