SearchLibrium 0.0.83__tar.gz → 0.0.84__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 (45) hide show
  1. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/PKG-INFO +1 -1
  2. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/pyproject.toml +1 -1
  3. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/__init__.py +15 -7
  4. searchlibrium-0.0.84/src/SearchLibrium/selection_models.py +268 -0
  5. searchlibrium-0.0.84/src/SearchLibrium/version.txt +1 -0
  6. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
  7. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium.egg-info/SOURCES.txt +1 -0
  8. searchlibrium-0.0.83/src/SearchLibrium/version.txt +0 -1
  9. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/README.md +0 -0
  10. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/setup.cfg +0 -0
  11. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/Halton.py +0 -0
  12. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/MixedLogit.py +0 -0
  13. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
  14. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/RandomP.py +0 -0
  15. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
  16. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/Two_Level_Nest.py +0 -0
  17. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/__main__.py +0 -0
  18. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/_choice_model.py +0 -0
  19. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/_device.py +0 -0
  20. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/bhhh/minimize.py +0 -0
  21. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/boxcox_functions.py +0 -0
  22. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/call_meta.py +0 -0
  23. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/constraints_builder.py +0 -0
  24. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/harmony.py +0 -0
  25. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/latent_class.py +0 -0
  26. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/main.py +0 -0
  27. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/main_debug.py +0 -0
  28. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/misc.py +0 -0
  29. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/mixed_logit.py +0 -0
  30. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/mixed_nested.py +0 -0
  31. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/mixedrrm.py +0 -0
  32. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/multinomial_logit.py +0 -0
  33. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/multinomial_nested.py +0 -0
  34. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/multinomial_probit.py +0 -0
  35. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/ordered_logit.py +0 -0
  36. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
  37. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/rrm.py +0 -0
  38. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/search.py +0 -0
  39. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/setup.py +0 -0
  40. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/siman.py +0 -0
  41. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium/threshold.py +0 -0
  42. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
  43. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
  44. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/src/SearchLibrium.egg-info/requires.txt +0 -0
  45. {searchlibrium-0.0.83 → searchlibrium-0.0.84}/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.83
3
+ Version: 0.0.84
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>
@@ -59,7 +59,7 @@ Homepage = "https://github.com/zahern/HypothesisX"
59
59
  realpython = "SearchLibrium.__main__:main"
60
60
 
61
61
  [tool.bumpver]
62
- current_version = "0.0.83"
62
+ current_version = "0.0.84"
63
63
  version_pattern = "MAJOR.MINOR.PATCH"
64
64
  commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
65
65
  commit = true
@@ -86,7 +86,9 @@ try:
86
86
  from .rrm import RandomRegret
87
87
  from .mixedrrm import MixedRandomRegret
88
88
  from .ordered_logit import OrderedLogit, OrderedLogitLong
89
+ from .selection_models import BinaryProbit, HeckmanTwoStep
89
90
  from .latent_class import LatentClassMixedLogit
91
+ from .multinomial_probit import MultinomialProbit
90
92
  from .RandomP import RandomParameters
91
93
  from .constraints_builder import ConstraintBuilder, create_constraints
92
94
  from .search import Parameters
@@ -102,21 +104,27 @@ except ImportError as e:
102
104
  from rrm import RandomRegret
103
105
  from mixedrrm import MixedRandomRegret
104
106
  from ordered_logit import OrderedLogit, OrderedLogitLong
107
+ from selection_models import BinaryProbit, HeckmanTwoStep
105
108
  from latent_class import LatentClassMixedLogit
109
+ from multinomial_probit import MultinomialProbit
106
110
  from RandomP import RandomParameters
107
111
  from constraints_builder import ConstraintBuilder, create_constraints
108
112
  from search import Parameters
109
113
  from call_meta import call_siman, call_harmony, call_search, estimate_ctrl
110
114
  try:
111
115
  from .main import print_ascii_art_logo
112
- except:
113
- from main import print_ascii_art_logo
114
-
116
+ except Exception:
117
+ try:
118
+ from main import print_ascii_art_logo
119
+ except Exception:
120
+ print_ascii_art_logo = None
115
121
 
116
- try:
117
- print_ascii_art_logo()
118
- except ImportError:
119
- print("Error importing print_ascii_art_logo from main module. Continuing without logo.")
122
+
123
+ if print_ascii_art_logo is not None:
124
+ try:
125
+ print_ascii_art_logo()
126
+ except Exception:
127
+ print("SearchLibrium logo skipped; optional display dependencies are missing.")
120
128
 
121
129
  #print('loaded all')
122
130
  print('Welcome to SearchLibrium')
@@ -0,0 +1,268 @@
1
+ import math
2
+ from dataclasses import dataclass
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy.optimize import minimize
7
+ from scipy.stats import norm, t as student_t
8
+
9
+ try:
10
+ import jax
11
+ import jax.numpy as jnp
12
+ from jax.scipy.special import ndtr as jax_ndtr
13
+ except ImportError: # pragma: no cover
14
+ jax = None
15
+ jnp = None
16
+ jax_ndtr = None
17
+
18
+ try:
19
+ from ._choice_model import DiscreteChoiceModel
20
+ except ImportError:
21
+ from _choice_model import DiscreteChoiceModel
22
+
23
+
24
+ class BinaryProbit(DiscreteChoiceModel):
25
+ """Binary probit estimated with JAX autodiff and scipy L-BFGS-B."""
26
+
27
+ def __init__(self, _jax=False):
28
+ super(BinaryProbit, self).__init__(_jax)
29
+ self.descr = "Binary Probit"
30
+ self.result = None
31
+ self._X_design = None
32
+
33
+ def setup(self, X, y, varnames=None, fit_intercept=True):
34
+ X = np.asarray(X)
35
+ y = np.asarray(y).reshape(-1)
36
+ if varnames is None:
37
+ varnames = [f"x{i}" for i in range(X.shape[1])]
38
+ self.X = X
39
+ self.y = y
40
+ self.varnames = np.asarray(varnames, dtype="<U64")
41
+ self.fit_intercept = bool(fit_intercept)
42
+ self.sample_size = int(X.shape[0])
43
+ if self.fit_intercept:
44
+ self._X_design = np.column_stack([np.ones((X.shape[0], 1)), X])
45
+ self._design_names = np.asarray(["intercept", *self.varnames], dtype="<U64")
46
+ else:
47
+ self._X_design = X.copy()
48
+ self._design_names = self.varnames.copy()
49
+ return self
50
+
51
+ def _negloglik_jax(self, params, X, y):
52
+ xb = X @ params
53
+ p = jnp.clip(jax_ndtr(xb), 1e-10, 1.0 - 1e-10)
54
+ ll = y * jnp.log(p) + (1.0 - y) * jnp.log(1.0 - p)
55
+ return -jnp.sum(ll)
56
+
57
+ def fit(self, disp=False, **fit_kwargs):
58
+ if jax is None or jnp is None or jax_ndtr is None:
59
+ raise ImportError("JAX is required for BinaryProbit")
60
+
61
+ X = jnp.asarray(self._X_design)
62
+ y = jnp.asarray(self.y)
63
+ init = np.zeros(X.shape[1], dtype=float)
64
+
65
+ val_grad = jax.jit(jax.value_and_grad(self._negloglik_jax))
66
+
67
+ def _obj(params_np):
68
+ val, grad = val_grad(jnp.asarray(params_np), X, y)
69
+ return float(val), np.asarray(grad, dtype=float)
70
+
71
+ res = minimize(
72
+ fun=lambda p: _obj(p)[0],
73
+ x0=init,
74
+ jac=lambda p: _obj(p)[1],
75
+ method="L-BFGS-B",
76
+ options={"disp": bool(disp), "maxiter": int(fit_kwargs.pop("maxiter", 1000))},
77
+ )
78
+ self.result = res
79
+ self.coeff_names = self._design_names.copy()
80
+ self.coeff_est = np.asarray(res.x, dtype=float)
81
+ self.loglik = float(-res.fun)
82
+ self.converged = bool(res.success)
83
+ self.total_fun_eval = int(getattr(res, "nfev", 0))
84
+
85
+ hess_inv = getattr(res, "hess_inv", None)
86
+ if hess_inv is not None:
87
+ if hasattr(hess_inv, "todense"):
88
+ cov = np.asarray(hess_inv.todense(), dtype=float)
89
+ else:
90
+ cov = np.asarray(hess_inv, dtype=float)
91
+ stderr = np.sqrt(np.clip(np.diag(cov), 1e-12, None))
92
+ else:
93
+ stderr = np.full_like(self.coeff_est, np.nan, dtype=float)
94
+
95
+ self.stderr = stderr
96
+ self.zvalues = self.coeff_est / np.where(stderr > 0, stderr, np.nan)
97
+ self.pvalues = 2.0 * (1.0 - norm.cdf(np.abs(self.zvalues)))
98
+ k = len(self.coeff_est)
99
+ n = max(int(self.sample_size), 1)
100
+ self.aic = float(2 * k - 2 * self.loglik)
101
+ self.bic = float(k * np.log(n) - 2 * self.loglik)
102
+ return res
103
+
104
+ def predict_proba(self, X=None):
105
+ if self.coeff_est is None:
106
+ raise RuntimeError("BinaryProbit must be fit before prediction")
107
+ X_arr = self.X if X is None else np.asarray(X)
108
+ if self.fit_intercept:
109
+ X_arr = np.column_stack([np.ones((X_arr.shape[0], 1)), X_arr])
110
+ xb = X_arr @ self.coeff_est
111
+ return norm.cdf(xb)
112
+
113
+ def summary_frame(self):
114
+ if self.coeff_est is None:
115
+ return pd.DataFrame()
116
+ return pd.DataFrame({
117
+ "coef": self.coeff_est,
118
+ "stderr": self.stderr,
119
+ "z": self.zvalues,
120
+ "pvalue": self.pvalues,
121
+ }, index=self.coeff_names)
122
+
123
+
124
+ @dataclass
125
+ class _OLSResult:
126
+ params: pd.Series
127
+ bse: pd.Series
128
+ tvalues: pd.Series
129
+ pvalues: pd.Series
130
+ llf: float
131
+
132
+
133
+ class HeckmanTwoStep(DiscreteChoiceModel):
134
+ """Heckman selection model using JAX probit + closed-form OLS second stage."""
135
+
136
+ def __init__(self, _jax=False):
137
+ super(HeckmanTwoStep, self).__init__(_jax)
138
+ self.descr = "Heckman Two-Step"
139
+ self.selection_result = None
140
+ self.outcome_result = None
141
+ self.params_table = pd.DataFrame()
142
+
143
+ def setup(
144
+ self,
145
+ selection_X,
146
+ selection_y,
147
+ outcome_X,
148
+ outcome_y,
149
+ selection_varnames=None,
150
+ outcome_varnames=None,
151
+ fit_intercept=True,
152
+ ):
153
+ selection_X = np.asarray(selection_X)
154
+ selection_y = np.asarray(selection_y).reshape(-1)
155
+ outcome_X = np.asarray(outcome_X)
156
+ outcome_y = np.asarray(outcome_y).reshape(-1)
157
+ if selection_varnames is None:
158
+ selection_varnames = [f"s{i}" for i in range(selection_X.shape[1])]
159
+ if outcome_varnames is None:
160
+ outcome_varnames = [f"o{i}" for i in range(outcome_X.shape[1])]
161
+ self.selection_X = selection_X
162
+ self.selection_y = selection_y
163
+ self.outcome_X = outcome_X
164
+ self.outcome_y = outcome_y
165
+ self.selection_varnames = np.asarray(selection_varnames, dtype="<U64")
166
+ self.outcome_varnames = np.asarray(outcome_varnames, dtype="<U64")
167
+ self.fit_intercept = bool(fit_intercept)
168
+ self.sample_size = int(selection_X.shape[0])
169
+ return self
170
+
171
+ def fit(self, disp=False, **fit_kwargs):
172
+ sel_X = np.asarray(self.selection_X, dtype=float)
173
+ out_X = np.asarray(self.outcome_X, dtype=float)
174
+ if self.fit_intercept:
175
+ sel_X = np.column_stack([np.ones((sel_X.shape[0], 1)), sel_X])
176
+ out_X = np.column_stack([np.ones((out_X.shape[0], 1)), out_X])
177
+
178
+ probit_model = BinaryProbit(_jax=True)
179
+ sel_names = (["intercept"] if self.fit_intercept else []) + list(self.selection_varnames)
180
+ probit_model.setup(sel_X[:, 1:] if self.fit_intercept else sel_X,
181
+ self.selection_y,
182
+ varnames=sel_names[1:] if self.fit_intercept else sel_names,
183
+ fit_intercept=self.fit_intercept)
184
+ probit_model.fit(disp=disp, **fit_kwargs)
185
+
186
+ xb = sel_X @ probit_model.coeff_est
187
+ mills = norm.pdf(xb) / np.clip(norm.cdf(xb), 1e-10, None)
188
+
189
+ mask = self.selection_y == 1
190
+ out_design = np.column_stack([out_X[mask], mills[mask]])
191
+ out_y = self.outcome_y[mask]
192
+
193
+ xtx = out_design.T @ out_design
194
+ xtx_inv = np.linalg.pinv(xtx)
195
+ beta = xtx_inv @ (out_design.T @ out_y)
196
+ resid = out_y - out_design @ beta
197
+ dof = max(out_design.shape[0] - out_design.shape[1], 1)
198
+ sigma2 = float((resid @ resid) / dof)
199
+ cov = sigma2 * xtx_inv
200
+ se = np.sqrt(np.clip(np.diag(cov), 1e-12, None))
201
+ tvals = beta / np.where(se > 0, se, np.nan)
202
+ pvals = 2.0 * (1.0 - student_t.cdf(np.abs(tvals), df=dof))
203
+ ll_ols = -0.5 * out_design.shape[0] * (math.log(2.0 * math.pi * sigma2) + 1.0)
204
+
205
+ out_names = (["intercept"] if self.fit_intercept else []) + list(self.outcome_varnames) + ["IMR"]
206
+ ols = _OLSResult(
207
+ params=pd.Series(beta, index=out_names),
208
+ bse=pd.Series(se, index=out_names),
209
+ tvalues=pd.Series(tvals, index=out_names),
210
+ pvalues=pd.Series(pvals, index=out_names),
211
+ llf=float(ll_ols),
212
+ )
213
+
214
+ self.selection_result = probit_model
215
+ self.outcome_result = ols
216
+ self.loglik = float(probit_model.loglik + ll_ols)
217
+ total_k = len(probit_model.coeff_est) + len(beta)
218
+ self.aic = float(2 * total_k - 2 * self.loglik)
219
+ self.bic = float(total_k * np.log(max(self.sample_size, 1)) - 2 * self.loglik)
220
+ self.converged = bool(probit_model.converged)
221
+
222
+ selection_tbl = pd.DataFrame({
223
+ "coef": probit_model.coeff_est,
224
+ "stderr": probit_model.stderr,
225
+ "z": probit_model.zvalues,
226
+ "pvalue": probit_model.pvalues,
227
+ }, index=probit_model.coeff_names)
228
+ outcome_tbl = pd.DataFrame({
229
+ "coef": ols.params,
230
+ "stderr": ols.bse,
231
+ "z": ols.tvalues,
232
+ "pvalue": ols.pvalues,
233
+ })
234
+ self.params_table = pd.concat(
235
+ {"selection": selection_tbl, "outcome": outcome_tbl},
236
+ names=["equation", "term"],
237
+ )
238
+
239
+ coeff_names = [f"selection::{name}" for name in selection_tbl.index]
240
+ coeff_names += [f"outcome::{name}" for name in outcome_tbl.index]
241
+ self.coeff_names = np.asarray(coeff_names, dtype="<U128")
242
+ self.coeff_est = np.concatenate([selection_tbl["coef"].values, outcome_tbl["coef"].values])
243
+ self.stderr = np.concatenate([selection_tbl["stderr"].values, outcome_tbl["stderr"].values])
244
+ self.zvalues = np.concatenate([selection_tbl["z"].values, outcome_tbl["z"].values])
245
+ self.pvalues = np.concatenate([selection_tbl["pvalue"].values, outcome_tbl["pvalue"].values])
246
+ return {"probit": probit_model, "ols": ols}
247
+
248
+ def predict_selection_proba(self, X=None):
249
+ if self.selection_result is None:
250
+ raise RuntimeError("HeckmanTwoStep must be fit before prediction")
251
+ X_arr = self.selection_X if X is None else np.asarray(X)
252
+ return self.selection_result.predict_proba(X_arr)
253
+
254
+ def predict_outcome(self, X=None, selection_probability=None):
255
+ if self.outcome_result is None:
256
+ raise RuntimeError("HeckmanTwoStep must be fit before prediction")
257
+ X_arr = self.outcome_X if X is None else np.asarray(X)
258
+ if self.fit_intercept:
259
+ X_arr = np.column_stack([np.ones((X_arr.shape[0], 1)), X_arr])
260
+ if selection_probability is None:
261
+ selection_probability = np.clip(self.predict_selection_proba(), 1e-10, 1 - 1e-10)
262
+ xb = norm.ppf(np.clip(selection_probability, 1e-10, 1 - 1e-10))
263
+ imr = norm.pdf(xb) / np.clip(norm.cdf(xb), 1e-10, None)
264
+ X_aug = np.column_stack([X_arr, imr])
265
+ return X_aug @ self.outcome_result.params.values
266
+
267
+ def summary_frame(self):
268
+ return self.params_table.copy()
@@ -0,0 +1 @@
1
+ 0.0.84
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SearchLibrium
3
- Version: 0.0.83
3
+ Version: 0.0.84
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>
@@ -28,6 +28,7 @@ src/SearchLibrium/ordered_logit.py
28
28
  src/SearchLibrium/ordered_logit_mixed.py
29
29
  src/SearchLibrium/rrm.py
30
30
  src/SearchLibrium/search.py
31
+ src/SearchLibrium/selection_models.py
31
32
  src/SearchLibrium/setup.py
32
33
  src/SearchLibrium/siman.py
33
34
  src/SearchLibrium/threshold.py
@@ -1 +0,0 @@
1
- 0.0.83
File without changes
File without changes