SearchLibrium 0.0.150__tar.gz → 0.0.152__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.150/src/SearchLibrium.egg-info → searchlibrium-0.0.152}/PKG-INFO +1 -1
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/pyproject.toml +1 -1
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/constraints_builder.py +73 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/predict.py +15 -2
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/search.py +227 -36
- searchlibrium-0.0.152/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152/src/SearchLibrium.egg-info}/PKG-INFO +1 -1
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/SOURCES.txt +2 -1
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/top_level.txt +1 -0
- searchlibrium-0.0.152/src/data_generator/latent_class_gen.py +174 -0
- searchlibrium-0.0.150/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/MANIFEST.in +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/README.md +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/setup.cfg +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/BEHier.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/data/Swissmetro_final.csv +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/data/TravelMode.csv +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/data/electricity.csv +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/harmony.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/sample_data.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/requires.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.152
|
|
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.152"
|
|
86
86
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
87
87
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
88
88
|
commit = true
|
|
@@ -135,6 +135,79 @@ class ConstraintBuilder:
|
|
|
135
135
|
self._print(f"Mutually exclusive group: {variables}")
|
|
136
136
|
return self
|
|
137
137
|
|
|
138
|
+
# ========================
|
|
139
|
+
# Alternative-Specific Variable Constraints
|
|
140
|
+
# ========================
|
|
141
|
+
|
|
142
|
+
def asvar_alt_spec(self, variable: str, alts: List[str]) -> 'ConstraintBuilder':
|
|
143
|
+
"""
|
|
144
|
+
Break alternative-specific variables into subsets so they don't all
|
|
145
|
+
apply uniformly to the model. For each variable, creates
|
|
146
|
+
alternative-specific dummy columns only for the given *alts*, while
|
|
147
|
+
keeping the original variable for the remaining alternatives as a
|
|
148
|
+
generic coefficient. The search will then select among these
|
|
149
|
+
alt-specific columns as part of the variable search.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
variable : str
|
|
154
|
+
Base variable name (e.g. 'TIME', 'COST').
|
|
155
|
+
alts : list of str
|
|
156
|
+
Alternatives for which alt-specific coefficients should be
|
|
157
|
+
created (e.g. ['Car', 'Train']). Other alternatives share a
|
|
158
|
+
single generic coefficient.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
ConstraintBuilder
|
|
163
|
+
Self for method chaining
|
|
164
|
+
|
|
165
|
+
Example
|
|
166
|
+
-------
|
|
167
|
+
>>> constraints.asvar_alt_spec('TIME', ['Car', 'Train'])
|
|
168
|
+
>>> constraints.asvar_alt_spec('COST', ['Bus', 'Car'])
|
|
169
|
+
|
|
170
|
+
This creates ``TIME_Car``, ``TIME_Train``, ``COST_Bus``, ``COST_Car``
|
|
171
|
+
in the active variable pool while keeping ``TIME`` and ``COST`` as
|
|
172
|
+
generic columns for the remaining alternatives.
|
|
173
|
+
"""
|
|
174
|
+
if 'asvar_alt_spec' not in self._constraints:
|
|
175
|
+
self._constraints['asvar_alt_spec'] = {}
|
|
176
|
+
self._constraints['asvar_alt_spec'][variable] = list(alts)
|
|
177
|
+
self._print(f"Alt-specific subset: '{variable}' → {alts}")
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def incompatible_specs(self, *alt_var_names: str) -> 'ConstraintBuilder':
|
|
181
|
+
"""
|
|
182
|
+
Declare that certain alternative-specific variable names (e.g.,
|
|
183
|
+
``TIME_Car`` and ``COST_Car``) are incompatible and must not appear
|
|
184
|
+
together in the same model. Each call creates one exclusion group.
|
|
185
|
+
|
|
186
|
+
This is applied **after** the alternative-specific dummies are
|
|
187
|
+
created, so you can declare constraints on the resulting column
|
|
188
|
+
names.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
*alt_var_names : str
|
|
193
|
+
Alt-specific column names forming a mutually exclusive group.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
ConstraintBuilder
|
|
198
|
+
Self for method chaining
|
|
199
|
+
|
|
200
|
+
Example
|
|
201
|
+
-------
|
|
202
|
+
>>> constraints.incompatible_specs('TIME_Car', 'COST_Car')
|
|
203
|
+
>>> constraints.incompatible_specs('FARE_Train', 'TIME_Train')
|
|
204
|
+
"""
|
|
205
|
+
if 'incompatible_specs' not in self._constraints:
|
|
206
|
+
self._constraints['incompatible_specs'] = []
|
|
207
|
+
self._constraints['incompatible_specs'].append(list(alt_var_names))
|
|
208
|
+
self._print(f"Incompatible alt-specific group: {alt_var_names}")
|
|
209
|
+
return self
|
|
210
|
+
|
|
138
211
|
def min_behavioral(self, min_count: int, *variables: str) -> 'ConstraintBuilder':
|
|
139
212
|
"""
|
|
140
213
|
Require at least ``min_count`` variables from a behavioural pool
|
|
@@ -88,6 +88,14 @@ class DestinationPredictor:
|
|
|
88
88
|
if prob is None:
|
|
89
89
|
prob = getattr(self.model, 'choice_pred_prob', None)
|
|
90
90
|
if prob is not None and hasattr(prob, 'shape') and len(prob.shape) == 2:
|
|
91
|
+
# JAX-fitted models (_jax=True, the default) leave this as a
|
|
92
|
+
# jax.Array. Downstream code (predict_destinations()) runs plain
|
|
93
|
+
# numpy ops (argmax/argsort) per row in a Python loop; numpy
|
|
94
|
+
# dispatching those against an un-converted jax.Array is
|
|
95
|
+
# catastrophically slow (~4s per argsort call observed, i.e.
|
|
96
|
+
# hours instead of seconds for a few thousand trips) rather than
|
|
97
|
+
# erroring, so convert once here up front.
|
|
98
|
+
prob = np.asarray(prob)
|
|
91
99
|
self._probs = prob
|
|
92
100
|
return prob
|
|
93
101
|
|
|
@@ -106,8 +114,9 @@ class DestinationPredictor:
|
|
|
106
114
|
avail.reshape(self.n_trips, self.n_dests),
|
|
107
115
|
return_logsum=True,
|
|
108
116
|
)
|
|
117
|
+
probs = np.asarray(probs) # see conversion note above
|
|
109
118
|
self._probs = probs
|
|
110
|
-
self._logsums = logsums
|
|
119
|
+
self._logsums = np.asarray(logsums)
|
|
111
120
|
return probs
|
|
112
121
|
|
|
113
122
|
# ------------------------------------------------------------------
|
|
@@ -147,7 +156,11 @@ class DestinationPredictor:
|
|
|
147
156
|
(self.idca[self.trip_id_col] == tid)
|
|
148
157
|
& (self.idca[self.choice_col] == 1)
|
|
149
158
|
]
|
|
150
|
-
|
|
159
|
+
# dest_col isn't always integer-coded (e.g. string alt labels
|
|
160
|
+
# like 'CAR'/'SM'/'TRAIN') -- keep the raw value so it compares
|
|
161
|
+
# correctly against dest_codes/best_code, which retain whatever
|
|
162
|
+
# dtype the source data used.
|
|
163
|
+
actual_code = actual[self.dest_col].iloc[0] if len(actual) else -1
|
|
151
164
|
|
|
152
165
|
rows.append({
|
|
153
166
|
self.trip_id_col: tid,
|
|
@@ -1703,6 +1703,26 @@ class Search():
|
|
|
1703
1703
|
''' ---------------------------------------------------------- '''
|
|
1704
1704
|
def remove_redundant_asvars(self, asvars, transasvars, asvarnames):
|
|
1705
1705
|
# {
|
|
1706
|
+
# Gather variables that are part of an asvar_alt_spec constraint so
|
|
1707
|
+
# that both the generic column AND its alt-specific dummies are kept
|
|
1708
|
+
# together (the constraint requires both forms to coexist).
|
|
1709
|
+
constraints = getattr(self.param, 'pres_spec_constr', None) or {}
|
|
1710
|
+
alt_spec_map = constraints.get('asvar_alt_spec', {})
|
|
1711
|
+
constrained_base = set()
|
|
1712
|
+
constrained_dummies = set()
|
|
1713
|
+
if alt_spec_map:
|
|
1714
|
+
for base_var, alts in alt_spec_map.items():
|
|
1715
|
+
if base_var in asvars:
|
|
1716
|
+
constrained_base.add(base_var)
|
|
1717
|
+
for alt in alts:
|
|
1718
|
+
dummy_name = f"{base_var}_{alt}"
|
|
1719
|
+
if dummy_name in asvars:
|
|
1720
|
+
constrained_dummies.add(dummy_name)
|
|
1721
|
+
|
|
1722
|
+
# Merge constrained vars -- they always travel together so exclude
|
|
1723
|
+
# them from the either/or logic below.
|
|
1724
|
+
constrained_merged = constrained_base | constrained_dummies
|
|
1725
|
+
|
|
1706
1726
|
# Filter out elements from asvars that contain any substring present in transasvars.
|
|
1707
1727
|
redundant_asvars = [var for var in asvars if any(subvar in var for subvar in transasvars)]
|
|
1708
1728
|
unique_vars = [var for var in asvars if var not in redundant_asvars]
|
|
@@ -1710,8 +1730,10 @@ class Search():
|
|
|
1710
1730
|
# When transformations are not applied, the redundancy is created
|
|
1711
1731
|
# if a variable has both generic & alt-spec coeffs
|
|
1712
1732
|
if len(transasvars) == 0: # {
|
|
1713
|
-
gen_var_select = [var for var in asvars
|
|
1714
|
-
|
|
1733
|
+
gen_var_select = [var for var in asvars
|
|
1734
|
+
if var in asvarnames and var not in constrained_merged]
|
|
1735
|
+
alspec_final = [var for var in asvars
|
|
1736
|
+
if var not in asvarnames and var not in constrained_merged]
|
|
1715
1737
|
# }
|
|
1716
1738
|
else:
|
|
1717
1739
|
# {
|
|
@@ -1719,12 +1741,16 @@ class Search():
|
|
|
1719
1741
|
for var in transasvars:
|
|
1720
1742
|
# {
|
|
1721
1743
|
redun_vars = [item for item in asvars if var in item]
|
|
1722
|
-
gen_var = [
|
|
1744
|
+
gen_var = [v for v in redun_vars
|
|
1745
|
+
if v in asvarnames and v not in constrained_merged]
|
|
1723
1746
|
if gen_var:
|
|
1724
1747
|
gen_var_select.append(np.random.choice(gen_var))
|
|
1725
|
-
alspec_redun_vars = [item for item in asvars
|
|
1726
|
-
|
|
1727
|
-
|
|
1748
|
+
alspec_redun_vars = [item for item in asvars
|
|
1749
|
+
if var in item and item not in asvarnames
|
|
1750
|
+
and item not in constrained_merged]
|
|
1751
|
+
trans_alspec = [item for item in alspec_redun_vars
|
|
1752
|
+
if any(sub_item in item for sub_item in boxc_l)]
|
|
1753
|
+
lin_alspec = [v for v in alspec_redun_vars if v not in trans_alspec]
|
|
1728
1754
|
choice = np.random.randint(2) # Chooses a 0 or 1
|
|
1729
1755
|
ref = lin_alspec if choice else trans_alspec
|
|
1730
1756
|
alspec_final.extend(ref)
|
|
@@ -1738,6 +1764,8 @@ class Search():
|
|
|
1738
1764
|
else:
|
|
1739
1765
|
final_asvars = alspec_final
|
|
1740
1766
|
|
|
1767
|
+
# Append constrained vars (both generic base and alt-specific dummies)
|
|
1768
|
+
final_asvars.extend(list(constrained_merged))
|
|
1741
1769
|
final_asvars.extend(unique_vars) # Extend the list
|
|
1742
1770
|
|
|
1743
1771
|
# Remove duplicates while preserving the order of elements
|
|
@@ -1771,6 +1799,7 @@ class Search():
|
|
|
1771
1799
|
asvars.extend(self.param.ps_asvars)
|
|
1772
1800
|
asvars = self.remove_redundant_asvars(asvars, self.param.trans_asvars, self.param.asvarnames)
|
|
1773
1801
|
asvars = self._apply_mutual_exclusion_filter(asvars)
|
|
1802
|
+
asvars = self._apply_incompatible_specs_filter(asvars)
|
|
1774
1803
|
return asvars
|
|
1775
1804
|
# }
|
|
1776
1805
|
|
|
@@ -1785,6 +1814,7 @@ class Search():
|
|
|
1785
1814
|
isvars = [self.param.avail_isvars[i] for i in isvar_select_pos]
|
|
1786
1815
|
isvars.extend(self.param.ps_isvars)
|
|
1787
1816
|
isvars = self._apply_mutual_exclusion_filter(isvars)
|
|
1817
|
+
isvars = self._apply_incompatible_specs_filter(isvars)
|
|
1788
1818
|
return isvars
|
|
1789
1819
|
# }
|
|
1790
1820
|
|
|
@@ -2084,6 +2114,9 @@ class Search():
|
|
|
2084
2114
|
# ── Mutual-exclusion constraint ──
|
|
2085
2115
|
self._enforce_mutual_exclusion(solution)
|
|
2086
2116
|
|
|
2117
|
+
# ── Incompatible alt-specific specs constraint ──
|
|
2118
|
+
self._enforce_incompatible_specs(solution)
|
|
2119
|
+
|
|
2087
2120
|
# ── Min-behavioural constraint (soft: at least n from a pool) ──
|
|
2088
2121
|
self._enforce_min_behavioral(solution)
|
|
2089
2122
|
|
|
@@ -2176,6 +2209,17 @@ class Search():
|
|
|
2176
2209
|
return []
|
|
2177
2210
|
return self.param.pres_spec_constr.get('mutually_exclusive', [])
|
|
2178
2211
|
|
|
2212
|
+
def _get_incompatible_specs_groups(self):
|
|
2213
|
+
"""Return the list of incompatible alt-specific variable groups, or [].
|
|
2214
|
+
|
|
2215
|
+
Each group is a list[str] of alt-specific column names (e.g.,
|
|
2216
|
+
``TIME_Car``, ``COST_Car``); at most one member of each group
|
|
2217
|
+
may appear in a solution.
|
|
2218
|
+
"""
|
|
2219
|
+
if not hasattr(self.param, 'pres_spec_constr') or self.param.pres_spec_constr is None:
|
|
2220
|
+
return []
|
|
2221
|
+
return self.param.pres_spec_constr.get('incompatible_specs', [])
|
|
2222
|
+
|
|
2179
2223
|
def _get_excluded_by_mutual_group(self, already_present: set):
|
|
2180
2224
|
"""Return the set of variables that are blocked because a partner
|
|
2181
2225
|
from the same mutually-exclusive group is *already_present*.
|
|
@@ -2187,6 +2231,37 @@ class Search():
|
|
|
2187
2231
|
excluded.update(gset)
|
|
2188
2232
|
return excluded
|
|
2189
2233
|
|
|
2234
|
+
def _get_excluded_by_incompatible_specs(self, already_present: set):
|
|
2235
|
+
"""Return the set of alt-specific column names that are blocked
|
|
2236
|
+
because a partner from the same incompatible-spec group is
|
|
2237
|
+
*already_present*.
|
|
2238
|
+
"""
|
|
2239
|
+
excluded = set()
|
|
2240
|
+
for group in self._get_incompatible_specs_groups():
|
|
2241
|
+
gset = set(group)
|
|
2242
|
+
if gset & already_present:
|
|
2243
|
+
excluded.update(gset)
|
|
2244
|
+
return excluded
|
|
2245
|
+
|
|
2246
|
+
def _apply_incompatible_specs_filter(self, varlist):
|
|
2247
|
+
"""Given a list of variables, keep at most one per incompatible-spec
|
|
2248
|
+
group. For each group the first variable encountered is kept;
|
|
2249
|
+
subsequent members are dropped.
|
|
2250
|
+
"""
|
|
2251
|
+
groups = self._get_incompatible_specs_groups()
|
|
2252
|
+
if not groups:
|
|
2253
|
+
return varlist
|
|
2254
|
+
blocked = set()
|
|
2255
|
+
for group in groups:
|
|
2256
|
+
seen_first = False
|
|
2257
|
+
for v in varlist:
|
|
2258
|
+
if v in group:
|
|
2259
|
+
if seen_first:
|
|
2260
|
+
blocked.add(v)
|
|
2261
|
+
else:
|
|
2262
|
+
seen_first = True
|
|
2263
|
+
return [v for v in varlist if v not in blocked]
|
|
2264
|
+
|
|
2190
2265
|
def _enforce_mutual_exclusion(self, solution):
|
|
2191
2266
|
"""Ensure at most one variable per mutually-exclusive group is present.
|
|
2192
2267
|
|
|
@@ -2215,6 +2290,34 @@ class Search():
|
|
|
2215
2290
|
if 'corvars' in solution and v in solution['corvars']:
|
|
2216
2291
|
solution['corvars'] = [x for x in solution['corvars'] if x != v]
|
|
2217
2292
|
|
|
2293
|
+
def _enforce_incompatible_specs(self, solution):
|
|
2294
|
+
"""Ensure at most one alt-specific variable per incompatible group.
|
|
2295
|
+
|
|
2296
|
+
Operates on the alt-specific dummy column names (e.g.
|
|
2297
|
+
``TIME_Car``, ``COST_Car``). If multiple members of a group
|
|
2298
|
+
are present, keep the first and remove the rest from asvars.
|
|
2299
|
+
"""
|
|
2300
|
+
groups = self._get_incompatible_specs_groups()
|
|
2301
|
+
if not groups:
|
|
2302
|
+
return
|
|
2303
|
+
all_v = self._all_vars_in_solution(solution)
|
|
2304
|
+
for group in groups:
|
|
2305
|
+
present = [v for v in group if v in all_v]
|
|
2306
|
+
if len(present) <= 1:
|
|
2307
|
+
continue
|
|
2308
|
+
remove = set(present[1:])
|
|
2309
|
+
for v in remove:
|
|
2310
|
+
if 'asvars' in solution and v in solution['asvars']:
|
|
2311
|
+
solution['asvars'] = [x for x in solution['asvars'] if x != v]
|
|
2312
|
+
if 'isvars' in solution and v in solution['isvars']:
|
|
2313
|
+
solution['isvars'] = [x for x in solution['isvars'] if x != v]
|
|
2314
|
+
if 'randvars' in solution and v in solution['randvars']:
|
|
2315
|
+
del solution['randvars'][v]
|
|
2316
|
+
if 'bcvars' in solution and v in solution['bcvars']:
|
|
2317
|
+
solution['bcvars'] = [x for x in solution['bcvars'] if x != v]
|
|
2318
|
+
if 'corvars' in solution and v in solution['corvars']:
|
|
2319
|
+
solution['corvars'] = [x for x in solution['corvars'] if x != v]
|
|
2320
|
+
|
|
2218
2321
|
def _cull_attrited_vars(self):
|
|
2219
2322
|
"""Remove variables from the available pools that have exceeded the
|
|
2220
2323
|
failure threshold. Once removed they stay out for the rest of
|
|
@@ -2234,13 +2337,50 @@ class Search():
|
|
|
2234
2337
|
print(f" [attrition] '{v}' removed from search after {self._var_failures[v]} failures")
|
|
2235
2338
|
|
|
2236
2339
|
def _apply_latent_class_constraints(self, solution, lc_constraints):
|
|
2237
|
-
"""Apply constraints specific to latent class models.
|
|
2238
|
-
|
|
2340
|
+
"""Apply constraints specific to latent class models.
|
|
2341
|
+
|
|
2342
|
+
Populates solution['class_params_spec'] from the constraint builder's
|
|
2343
|
+
class_specific_vars dict, so that the search respects per-class
|
|
2344
|
+
variable assignments.
|
|
2345
|
+
"""
|
|
2346
|
+
n_classes = lc_constraints.get('n_classes', 2)
|
|
2347
|
+
|
|
2348
|
+
# --- class-specific variable assignments ---
|
|
2239
2349
|
if 'class_specific_vars' in lc_constraints:
|
|
2240
|
-
|
|
2241
|
-
#
|
|
2242
|
-
|
|
2243
|
-
|
|
2350
|
+
class_spec_vars = lc_constraints['class_specific_vars']
|
|
2351
|
+
# Build / update class_params_spec
|
|
2352
|
+
existing = solution.data.get('class_params_spec', None)
|
|
2353
|
+
if existing is None or len(existing) != n_classes:
|
|
2354
|
+
existing = np.empty(n_classes, dtype=object)
|
|
2355
|
+
for c in range(n_classes):
|
|
2356
|
+
existing[c] = np.array([], dtype='<U64')
|
|
2357
|
+
|
|
2358
|
+
for c in range(n_classes):
|
|
2359
|
+
key = f'class_{c}'
|
|
2360
|
+
if key in class_spec_vars:
|
|
2361
|
+
vars_c = class_spec_vars[key]
|
|
2362
|
+
current = list(existing[c]) if len(existing[c]) > 0 else []
|
|
2363
|
+
merged = list(set(current + vars_c))
|
|
2364
|
+
existing[c] = np.array(merged, dtype='<U64')
|
|
2365
|
+
|
|
2366
|
+
solution['class_params_spec'] = existing
|
|
2367
|
+
|
|
2368
|
+
# Also ensure all class-specific vars are in asvars (the flat list used
|
|
2369
|
+
# by variable-selection operators for discovery)
|
|
2370
|
+
all_class_vars = []
|
|
2371
|
+
for vars_c in class_spec_vars.values():
|
|
2372
|
+
all_class_vars.extend(vars_c)
|
|
2373
|
+
solution['asvars'] = list(set(all_class_vars + solution['asvars']))
|
|
2374
|
+
|
|
2375
|
+
# --- membership variable constraints ---
|
|
2376
|
+
if 'membership_vars' in lc_constraints:
|
|
2377
|
+
member_vars = list(lc_constraints['membership_vars'].keys())
|
|
2378
|
+
existing_member = solution.data.get('member_params_spec', None)
|
|
2379
|
+
if existing_member is None or len(existing_member) == 0:
|
|
2380
|
+
solution['member_params_spec'] = np.array(member_vars, dtype='<U64')
|
|
2381
|
+
else:
|
|
2382
|
+
merged = list(set(list(existing_member) + member_vars))
|
|
2383
|
+
solution['member_params_spec'] = np.array(merged, dtype='<U64')
|
|
2244
2384
|
|
|
2245
2385
|
def _apply_mixed_model_constraints(self, solution, mm_constraints):
|
|
2246
2386
|
"""Apply constraints specific to mixed models."""
|
|
@@ -2562,6 +2702,7 @@ class Search():
|
|
|
2562
2702
|
# {
|
|
2563
2703
|
candidate = [var for var in self.param.asvarnames if var not in solution['asvars']]
|
|
2564
2704
|
blocked = self._get_excluded_by_mutual_group(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2705
|
+
blocked |= self._get_excluded_by_incompatible_specs(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2565
2706
|
candidate = [v for v in candidate if v not in blocked]
|
|
2566
2707
|
if len(candidate) > 0:
|
|
2567
2708
|
new_asvar = self.random_choice(candidate)
|
|
@@ -2637,6 +2778,7 @@ class Search():
|
|
|
2637
2778
|
# {
|
|
2638
2779
|
candidate = [var for var in self.param.isvarnames if var not in solution['isvars']]
|
|
2639
2780
|
blocked = self._get_excluded_by_mutual_group(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2781
|
+
blocked |= self._get_excluded_by_incompatible_specs(set(solution.get('asvars', [])) | set(solution.get('isvars', [])))
|
|
2640
2782
|
candidate = [v for v in candidate if v not in blocked]
|
|
2641
2783
|
if len(candidate) > 0:
|
|
2642
2784
|
# {
|
|
@@ -3883,47 +4025,72 @@ class Search():
|
|
|
3883
4025
|
def create_dummy_column(self, asvars):
|
|
3884
4026
|
# {
|
|
3885
4027
|
"""
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
the
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
The new asvar includes the new dummy columns
|
|
3896
|
-
variables
|
|
4028
|
+
Generates alternative-specific dummy columns for variables that
|
|
4029
|
+
receive alternative-specific coefficients. For variables listed
|
|
4030
|
+
in the ``asvar_alt_spec`` constraint, dummies are created *only*
|
|
4031
|
+
for the specified alternatives (the original variable is kept
|
|
4032
|
+
for the remaining alternatives as a generic coefficient). For
|
|
4033
|
+
all other variables the existing random-selection behaviour is
|
|
4034
|
+
preserved: a random subset is turned into full alt-specific
|
|
4035
|
+
dummies and the remaining variables stay generic.
|
|
4036
|
+
|
|
4037
|
+
The new asvar list includes the new dummy columns alongside the
|
|
4038
|
+
variables that were not chosen for alternative-specific
|
|
4039
|
+
coefficients.
|
|
3897
4040
|
"""
|
|
3898
4041
|
|
|
3899
|
-
|
|
3900
|
-
|
|
4042
|
+
constraints = getattr(self.param, 'pres_spec_constr', None) or {}
|
|
4043
|
+
alt_spec_map = constraints.get('asvar_alt_spec', {})
|
|
4044
|
+
|
|
4045
|
+
# Separate constrained vars from free vars
|
|
4046
|
+
constrained_asvars = [v for v in asvars if v in alt_spec_map]
|
|
4047
|
+
free_asvars = [v for v in asvars if v not in alt_spec_map]
|
|
4048
|
+
|
|
4049
|
+
# Generate a random boolean array for free vars only
|
|
4050
|
+
rand_array = np.random.choice([True, False], len(free_asvars))
|
|
3901
4051
|
|
|
3902
|
-
# Initialize a list for storing new alternative-specific variables
|
|
3903
4052
|
asvars_new = []
|
|
3904
4053
|
|
|
3905
|
-
#
|
|
3906
|
-
|
|
4054
|
+
# 1. Process constrained variables (subset alt-specific)
|
|
4055
|
+
for alt_var in constrained_asvars:
|
|
4056
|
+
# {
|
|
4057
|
+
specified_alts = alt_spec_map[alt_var]
|
|
4058
|
+
for choice in specified_alts:
|
|
4059
|
+
# {
|
|
4060
|
+
col_name = f"{alt_var}_{choice}"
|
|
4061
|
+
if col_name not in self.param.df.columns:
|
|
4062
|
+
self.param.df[col_name] = self.param.df[alt_var] * (self.param.alt_var == choice)
|
|
4063
|
+
|
|
4064
|
+
if self.param.nb_crit > 1:
|
|
4065
|
+
self.param.df_test[col_name] = self.param.df_test[alt_var] * (self.param.test_alt_var == choice)
|
|
4066
|
+
|
|
4067
|
+
asvars_new.append(col_name)
|
|
4068
|
+
# }
|
|
4069
|
+
# Keep the original variable as a generic coefficient for
|
|
4070
|
+
# the alternatives NOT listed in the constraint
|
|
4071
|
+
asvars_new.append(alt_var)
|
|
4072
|
+
# }
|
|
4073
|
+
|
|
4074
|
+
# 2. Process free variables (existing random behaviour)
|
|
4075
|
+
alt_spec_vars = [var for var, bool in zip(free_asvars, rand_array) if bool]
|
|
4076
|
+
generic_vars = [var for var, bool in zip(free_asvars, rand_array) if not bool]
|
|
3907
4077
|
|
|
3908
|
-
# Create dummy columns for the selected variables
|
|
3909
4078
|
for alt_var in alt_spec_vars:
|
|
3910
4079
|
# {
|
|
3911
4080
|
for choice in self.param.choice_set:
|
|
3912
4081
|
# {
|
|
3913
4082
|
col_name = f"{alt_var}_{choice}"
|
|
3914
|
-
|
|
4083
|
+
if col_name not in self.param.df.columns:
|
|
4084
|
+
self.param.df[col_name] = self.param.df[alt_var] * (self.param.alt_var == choice)
|
|
3915
4085
|
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
# full training self.param.alt_var (shape mismatch else).
|
|
3919
|
-
self.param.df_test[col_name] = self.param.df_test[alt_var] * (self.param.test_alt_var == choice)
|
|
4086
|
+
if self.param.nb_crit > 1:
|
|
4087
|
+
self.param.df_test[col_name] = self.param.df_test[alt_var] * (self.param.test_alt_var == choice)
|
|
3920
4088
|
|
|
3921
4089
|
asvars_new.append(col_name)
|
|
3922
4090
|
# }
|
|
3923
4091
|
# }
|
|
4092
|
+
asvars_new.extend(generic_vars)
|
|
3924
4093
|
|
|
3925
|
-
# Add the remaining variables that were not selected for alternative-specific coefficients
|
|
3926
|
-
asvars_new.extend(var for var, bool in zip(asvars, rand_array) if not bool)
|
|
3927
4094
|
return asvars_new
|
|
3928
4095
|
|
|
3929
4096
|
# }
|
|
@@ -4148,6 +4315,7 @@ class Search():
|
|
|
4148
4315
|
avail=avail,
|
|
4149
4316
|
membership_vars=membership_vars,
|
|
4150
4317
|
member_params_spec=member_params_spec,
|
|
4318
|
+
class_params_spec=class_params_spec,
|
|
4151
4319
|
)
|
|
4152
4320
|
model.fit(em_method="squarem")
|
|
4153
4321
|
model.get_loglik_null()
|
|
@@ -4344,6 +4512,29 @@ class Search():
|
|
|
4344
4512
|
member_params_spec = sol.get('member_params_spec', None)
|
|
4345
4513
|
num_classes = getattr(self.param, 'num_classes', 2)
|
|
4346
4514
|
|
|
4515
|
+
# Build all_vars as the UNION of all per-class variable specs
|
|
4516
|
+
# (plus membership vars, plus asvars/isvars fallback).
|
|
4517
|
+
if class_params_spec is not None and len(class_params_spec) > 0:
|
|
4518
|
+
all_vars_set = set()
|
|
4519
|
+
for c in range(len(class_params_spec)):
|
|
4520
|
+
if class_params_spec[c] is not None and len(class_params_spec[c]) > 0:
|
|
4521
|
+
for v in class_params_spec[c]:
|
|
4522
|
+
all_vars_set.add(str(v))
|
|
4523
|
+
# Also include member params
|
|
4524
|
+
if member_params_spec is not None:
|
|
4525
|
+
if hasattr(member_params_spec, 'flat'):
|
|
4526
|
+
for v in member_params_spec.flat:
|
|
4527
|
+
all_vars_set.add(str(v))
|
|
4528
|
+
elif hasattr(member_params_spec, '__iter__'):
|
|
4529
|
+
for v in member_params_spec:
|
|
4530
|
+
all_vars_set.add(str(v))
|
|
4531
|
+
# Fallback to asvars+isvars for any vars not captured
|
|
4532
|
+
for v in (as_vars + is_vars):
|
|
4533
|
+
all_vars_set.add(str(v))
|
|
4534
|
+
all_vars = [v for v in self.param.varnames if v in all_vars_set]
|
|
4535
|
+
else:
|
|
4536
|
+
all_vars = [var for var in self.param.varnames if var in (as_vars + is_vars)]
|
|
4537
|
+
|
|
4347
4538
|
X = self.param.df[all_vars].values
|
|
4348
4539
|
y = self.param.choices
|
|
4349
4540
|
ids = self.param.choice_id if self.param.choice_id is not None else self.param.ind_id
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.152
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.152
|
|
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>
|
|
@@ -53,4 +53,5 @@ src/SearchLibrium.egg-info/top_level.txt
|
|
|
53
53
|
src/SearchLibrium/bhhh/minimize.py
|
|
54
54
|
src/SearchLibrium/data/Swissmetro_final.csv
|
|
55
55
|
src/SearchLibrium/data/TravelMode.csv
|
|
56
|
-
src/SearchLibrium/data/electricity.csv
|
|
56
|
+
src/SearchLibrium/data/electricity.csv
|
|
57
|
+
src/data_generator/latent_class_gen.py
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy.special import softmax
|
|
4
|
+
|
|
5
|
+
class AdvancedLatentClassGenerator:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
n_classes=3,
|
|
9
|
+
n_alternatives=3,
|
|
10
|
+
n_individuals=2000,
|
|
11
|
+
n_choice_tasks=5,
|
|
12
|
+
class_probs=None,
|
|
13
|
+
scale_separation=2.5,
|
|
14
|
+
n_noise_vars=5,
|
|
15
|
+
n_weak_vars=3,
|
|
16
|
+
n_collinear_vars=3,
|
|
17
|
+
rare_class=False,
|
|
18
|
+
random_state=42
|
|
19
|
+
):
|
|
20
|
+
self.rng = np.random.default_rng(random_state)
|
|
21
|
+
|
|
22
|
+
self.K = n_classes
|
|
23
|
+
self.J = n_alternatives
|
|
24
|
+
self.N = n_individuals
|
|
25
|
+
self.T = n_choice_tasks
|
|
26
|
+
|
|
27
|
+
if class_probs is None:
|
|
28
|
+
if rare_class:
|
|
29
|
+
probs = np.ones(n_classes)
|
|
30
|
+
probs[-1] = 0.05
|
|
31
|
+
probs[:-1] = (1 - 0.05) / (n_classes - 1)
|
|
32
|
+
self.class_probs = probs
|
|
33
|
+
else:
|
|
34
|
+
self.class_probs = np.ones(n_classes) / n_classes
|
|
35
|
+
else:
|
|
36
|
+
self.class_probs = np.array(class_probs)
|
|
37
|
+
|
|
38
|
+
self.scale_separation = scale_separation
|
|
39
|
+
self.n_noise_vars = n_noise_vars
|
|
40
|
+
self.n_weak_vars = n_weak_vars
|
|
41
|
+
self.n_collinear_vars = n_collinear_vars
|
|
42
|
+
|
|
43
|
+
self._define_variables()
|
|
44
|
+
self._generate_parameters()
|
|
45
|
+
|
|
46
|
+
# --------------------------------
|
|
47
|
+
# Variable Names
|
|
48
|
+
# --------------------------------
|
|
49
|
+
def _define_variables(self):
|
|
50
|
+
|
|
51
|
+
self.shared_vars = [
|
|
52
|
+
"price",
|
|
53
|
+
"travel_time",
|
|
54
|
+
"waiting_time"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
self.class_specific_vars = {
|
|
58
|
+
0: ["comfort_level", "seat_space"],
|
|
59
|
+
1: ["brand_reputation", "loyalty_points"],
|
|
60
|
+
2: ["eco_rating", "carbon_emissions"]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
self.noise_vars = [f"random_noise_{i}" for i in range(self.n_noise_vars)]
|
|
64
|
+
self.weak_vars = [f"weak_signal_{i}" for i in range(self.n_weak_vars)]
|
|
65
|
+
self.collinear_vars = [f"collinear_var_{i}" for i in range(self.n_collinear_vars)]
|
|
66
|
+
|
|
67
|
+
# --------------------------------
|
|
68
|
+
# Generate True Parameters
|
|
69
|
+
# --------------------------------
|
|
70
|
+
def _generate_parameters(self):
|
|
71
|
+
|
|
72
|
+
self.parameters = {}
|
|
73
|
+
|
|
74
|
+
for k in range(self.K):
|
|
75
|
+
params = {}
|
|
76
|
+
|
|
77
|
+
# Shared variables
|
|
78
|
+
for var in self.shared_vars:
|
|
79
|
+
params[var] = self.rng.normal(
|
|
80
|
+
loc=k * self.scale_separation,
|
|
81
|
+
scale=1.0
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Class-specific variables
|
|
85
|
+
for var in self.class_specific_vars.get(k, []):
|
|
86
|
+
params[var] = self.rng.normal(
|
|
87
|
+
loc=(k + 1) * self.scale_separation,
|
|
88
|
+
scale=1.0
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Weak effect variables
|
|
92
|
+
for var in self.weak_vars:
|
|
93
|
+
params[var] = self.rng.normal(
|
|
94
|
+
loc=0,
|
|
95
|
+
scale=0.05
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Noise variables → TRUE beta = 0
|
|
99
|
+
for var in self.noise_vars:
|
|
100
|
+
params[var] = 0.0
|
|
101
|
+
|
|
102
|
+
self.parameters[k] = params
|
|
103
|
+
|
|
104
|
+
# --------------------------------
|
|
105
|
+
# Feature Generation
|
|
106
|
+
# --------------------------------
|
|
107
|
+
def _generate_features(self):
|
|
108
|
+
|
|
109
|
+
all_vars = set(self.shared_vars)
|
|
110
|
+
for v in self.class_specific_vars.values():
|
|
111
|
+
all_vars.update(v)
|
|
112
|
+
|
|
113
|
+
all_vars.update(self.noise_vars)
|
|
114
|
+
all_vars.update(self.weak_vars)
|
|
115
|
+
all_vars.update(self.collinear_vars)
|
|
116
|
+
|
|
117
|
+
X = {}
|
|
118
|
+
|
|
119
|
+
for var in all_vars:
|
|
120
|
+
X[var] = self.rng.normal(size=(self.N, self.T, self.J))
|
|
121
|
+
|
|
122
|
+
# Create collinearity
|
|
123
|
+
for i, var in enumerate(self.collinear_vars):
|
|
124
|
+
X[var] = 0.8 * X["price"] + 0.2 * self.rng.normal(
|
|
125
|
+
size=(self.N, self.T, self.J)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return X
|
|
129
|
+
|
|
130
|
+
# --------------------------------
|
|
131
|
+
# Data Generation
|
|
132
|
+
# --------------------------------
|
|
133
|
+
def generate(self):
|
|
134
|
+
|
|
135
|
+
classes = self.rng.choice(
|
|
136
|
+
self.K,
|
|
137
|
+
size=self.N,
|
|
138
|
+
p=self.class_probs
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
X = self._generate_features()
|
|
142
|
+
rows = []
|
|
143
|
+
|
|
144
|
+
for n in range(self.N):
|
|
145
|
+
k = classes[n]
|
|
146
|
+
|
|
147
|
+
for t in range(self.T):
|
|
148
|
+
|
|
149
|
+
utilities = np.zeros(self.J)
|
|
150
|
+
|
|
151
|
+
for j in range(self.J):
|
|
152
|
+
for var, beta in self.parameters[k].items():
|
|
153
|
+
utilities[j] += beta * X[var][n, t, j]
|
|
154
|
+
|
|
155
|
+
probs = softmax(utilities)
|
|
156
|
+
choice = self.rng.choice(self.J, p=probs)
|
|
157
|
+
|
|
158
|
+
for j in range(self.J):
|
|
159
|
+
|
|
160
|
+
row = {
|
|
161
|
+
"individual": n,
|
|
162
|
+
"choice_task": t,
|
|
163
|
+
"alternative": j,
|
|
164
|
+
"choice": 1 if j == choice else 0,
|
|
165
|
+
"true_class": k
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for var in X:
|
|
169
|
+
row[var] = X[var][n, t, j]
|
|
170
|
+
|
|
171
|
+
rows.append(row)
|
|
172
|
+
|
|
173
|
+
df = pd.DataFrame(rows)
|
|
174
|
+
return df
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.150
|
|
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
|
|
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.150 → searchlibrium-0.0.152}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.150 → searchlibrium-0.0.152}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|