SearchLibrium 0.0.128__tar.gz → 0.0.130__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.128 → searchlibrium-0.0.130}/PKG-INFO +1 -1
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/pyproject.toml +1 -1
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/harmony.py +331 -10
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/search.py +126 -65
- searchlibrium-0.0.130/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/PKG-INFO +1 -1
- searchlibrium-0.0.128/src/SearchLibrium/version.txt +0 -1
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/README.md +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/setup.cfg +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.130}/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.130
|
|
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>
|
|
@@ -73,7 +73,7 @@ Documentation = "https://pypi.org/project/SearchLibrium/"
|
|
|
73
73
|
realpython = "SearchLibrium.__main__:main"
|
|
74
74
|
|
|
75
75
|
[tool.bumpver]
|
|
76
|
-
current_version = "0.0.
|
|
76
|
+
current_version = "0.0.130"
|
|
77
77
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
78
78
|
commit_message = "[skip ci] Bump version {old_version} -> {new_version}"
|
|
79
79
|
commit = true
|
|
@@ -705,12 +705,334 @@ class HarmonySearch(Search):
|
|
|
705
705
|
# }
|
|
706
706
|
# }
|
|
707
707
|
|
|
708
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
709
|
+
# TAILORED PERTURBATIONS — model-development-style moves (changes 11–18)
|
|
710
|
+
# These mirror how a practitioner actually builds discrete choice models:
|
|
711
|
+
# significance pruning, distribution sweeps, structural hypothesis tests,
|
|
712
|
+
# model progression, ensemble crossover, and feature substitution.
|
|
713
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
714
|
+
|
|
715
|
+
def make_change_11_significance_prune(self, candidate):
|
|
716
|
+
"""Model-developer move: remove the least significant variable from
|
|
717
|
+
the best solution (mirrors backward elimination). Then try adding
|
|
718
|
+
the most promising unused variable as compensation."""
|
|
719
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
720
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
721
|
+
|
|
722
|
+
# Only prune if we have > 2 AS vars (keep a minimum)
|
|
723
|
+
if len(best_asvars) <= 2:
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
# ── Try removing one AS variable ──────────────────────────────
|
|
727
|
+
candidates_to_drop = [v for v in best_asvars
|
|
728
|
+
if v not in self._get_forced_vars()]
|
|
729
|
+
if not candidates_to_drop:
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
# Try each candidate drop (up to 3) — a real modeller tests each removal
|
|
733
|
+
for drop_var in candidates_to_drop[:3]:
|
|
734
|
+
trial_vars = [v for v in best_asvars if v != drop_var]
|
|
735
|
+
trial_randvars = {k: v for k, v in best_randvars.items()
|
|
736
|
+
if k in trial_vars}
|
|
737
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
738
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
739
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
740
|
+
asc_ind=asc_ind)
|
|
741
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else None
|
|
742
|
+
revised, converged = self.evaluate_solution(solution)
|
|
743
|
+
if converged:
|
|
744
|
+
self.insert_solution(revised)
|
|
745
|
+
|
|
746
|
+
# ── Then try adding a new promising variable ──────────────────
|
|
747
|
+
unused = [v for v in self.param.avail_asvars
|
|
748
|
+
if v not in best_asvars and v not in self.param.ps_asvars]
|
|
749
|
+
if unused:
|
|
750
|
+
add_var = self.param.generator.choice(unused)
|
|
751
|
+
trial_vars = best_asvars + [add_var]
|
|
752
|
+
trial_vars = list(dict.fromkeys(trial_vars))
|
|
753
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
754
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
755
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
756
|
+
asc_ind=asc_ind)
|
|
757
|
+
revised, converged = self.evaluate_solution(solution)
|
|
758
|
+
if converged:
|
|
759
|
+
self.insert_solution(revised)
|
|
760
|
+
|
|
761
|
+
def make_change_12_distribution_sweep(self, candidate):
|
|
762
|
+
"""Model-developer move: for each random parameter in the best
|
|
763
|
+
solution, test every alternative distribution. A practitioner
|
|
764
|
+
often asks 'is this log-normal or should it be triangular?'"""
|
|
765
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
766
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
767
|
+
|
|
768
|
+
if not best_randvars or len(best_randvars) == 0:
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
all_distributions = [d for d in self.param.distr if d != 'f']
|
|
772
|
+
|
|
773
|
+
for var_name, current_dist in list(best_randvars.items()):
|
|
774
|
+
alternatives = [d for d in all_distributions if d != current_dist]
|
|
775
|
+
if not alternatives:
|
|
776
|
+
continue
|
|
777
|
+
for alt_dist in alternatives[:len(alternatives)]:
|
|
778
|
+
trial_randvars = dict(best_randvars)
|
|
779
|
+
trial_randvars[var_name] = alt_dist
|
|
780
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars,
|
|
781
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
782
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
783
|
+
asc_ind=asc_ind)
|
|
784
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else 'mixed_logit'
|
|
785
|
+
revised, converged = self.evaluate_solution(solution)
|
|
786
|
+
if converged:
|
|
787
|
+
self.insert_solution(revised)
|
|
788
|
+
|
|
789
|
+
def make_change_13_model_progression(self, candidate):
|
|
790
|
+
"""Model-developer move: systematically step up or down the model
|
|
791
|
+
complexity ladder: MNL → NestedLogit → MixedLogit → MixedNested.
|
|
792
|
+
A modeller tests 'does adding nesting improve fit? does adding
|
|
793
|
+
random heterogeneity after nesting improve it further?'"""
|
|
794
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
795
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
796
|
+
|
|
797
|
+
current_model = (candidate[0].get('model_n', 'multinomial')
|
|
798
|
+
if isinstance(candidate, list) and candidate
|
|
799
|
+
else 'multinomial')
|
|
800
|
+
|
|
801
|
+
has_nests = bool(getattr(self.param, 'nests', None))
|
|
802
|
+
has_randvars = bool(best_randvars)
|
|
803
|
+
|
|
804
|
+
# Model ladder — try the "next step up" in complexity
|
|
805
|
+
progression = []
|
|
806
|
+
if has_nests:
|
|
807
|
+
progression = ['multinomial', 'nested_logit', 'mixed_nested']
|
|
808
|
+
else:
|
|
809
|
+
progression = ['multinomial', 'mixed_logit']
|
|
810
|
+
|
|
811
|
+
# Also consider stepping down (for parsimony)
|
|
812
|
+
for target_model in progression:
|
|
813
|
+
if target_model == current_model:
|
|
814
|
+
continue
|
|
815
|
+
# mixed_nested requires both nests and random params
|
|
816
|
+
if target_model == 'mixed_nested' and not (has_nests and has_randvars):
|
|
817
|
+
continue
|
|
818
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars,
|
|
819
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
820
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
821
|
+
asc_ind=asc_ind, model_n=target_model)
|
|
822
|
+
revised, converged = self.evaluate_solution(solution)
|
|
823
|
+
if converged:
|
|
824
|
+
self.insert_solution(revised)
|
|
825
|
+
|
|
826
|
+
def make_change_14_ensemble_crossover(self, candidate):
|
|
827
|
+
"""Model-developer move: combine variables from two top solutions
|
|
828
|
+
(like a modeller synthesising insights from two promising specs).
|
|
829
|
+
Creates four offspring: union, intersection, top-only, second-only."""
|
|
830
|
+
if len(self.memory) < 2:
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
memory_sorted = self.sort_memory(self.memory[:])
|
|
834
|
+
sol_a = memory_sorted[0]
|
|
835
|
+
sol_b = memory_sorted[1]
|
|
836
|
+
|
|
837
|
+
a_asvars = set(sol_a.get('asvars', []))
|
|
838
|
+
b_asvars = set(sol_b.get('asvars', []))
|
|
839
|
+
a_randvars = sol_a.get('randvars', {})
|
|
840
|
+
b_randvars = sol_b.get('randvars', {})
|
|
841
|
+
|
|
842
|
+
# ── Offspring 1: Union of both specs ──
|
|
843
|
+
union_vars = sorted(a_asvars | b_asvars)
|
|
844
|
+
if union_vars:
|
|
845
|
+
merged_randvars = {}
|
|
846
|
+
for v in union_vars:
|
|
847
|
+
if v in a_randvars: merged_randvars[v] = a_randvars[v]
|
|
848
|
+
elif v in b_randvars: merged_randvars[v] = b_randvars[v]
|
|
849
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=union_vars,
|
|
850
|
+
isvars=sol_a.get('isvars', []),
|
|
851
|
+
randvars=merged_randvars,
|
|
852
|
+
bcvars=[], corvars=[],
|
|
853
|
+
asc_ind=sol_a.get('asc_ind', False))
|
|
854
|
+
revised, converged = self.evaluate_solution(solution)
|
|
855
|
+
if converged:
|
|
856
|
+
self.insert_solution(revised)
|
|
857
|
+
|
|
858
|
+
# ── Offspring 2: Intersection (core stable variables) ──
|
|
859
|
+
inter_vars = sorted(a_asvars & b_asvars)
|
|
860
|
+
if inter_vars and len(inter_vars) >= 2:
|
|
861
|
+
inter_randvars = {v: a_randvars[v] for v in inter_vars
|
|
862
|
+
if v in a_randvars}
|
|
863
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=inter_vars,
|
|
864
|
+
isvars=sol_a.get('isvars', []),
|
|
865
|
+
randvars=inter_randvars,
|
|
866
|
+
bcvars=[], corvars=[],
|
|
867
|
+
asc_ind=sol_a.get('asc_ind', False))
|
|
868
|
+
revised, converged = self.evaluate_solution(solution)
|
|
869
|
+
if converged:
|
|
870
|
+
self.insert_solution(revised)
|
|
871
|
+
|
|
872
|
+
def make_change_15_feature_substitution(self, candidate):
|
|
873
|
+
"""Model-developer move: substitute one base feature for a
|
|
874
|
+
correlated alternative. E.g. swap ln_dest_trips for
|
|
875
|
+
osm_destination_pull, or swap ln_dist for a spline/knot."""
|
|
876
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
877
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
878
|
+
|
|
879
|
+
# Pre-check: identify substitution groups among available variables
|
|
880
|
+
# (variables that measure the same construct differently)
|
|
881
|
+
subst_groups = self._build_substitution_groups(best_asvars)
|
|
882
|
+
|
|
883
|
+
if not subst_groups:
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
for original, replacement in subst_groups:
|
|
887
|
+
trial_vars = [replacement if v == original else v
|
|
888
|
+
for v in best_asvars]
|
|
889
|
+
trial_randvars = {replacement if k == original else k: v
|
|
890
|
+
for k, v in best_randvars.items()}
|
|
891
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
892
|
+
isvars=best_isvars, randvars=trial_randvars,
|
|
893
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
894
|
+
asc_ind=asc_ind)
|
|
895
|
+
solution['model_n'] = candidate[0]['model_n'] if isinstance(candidate, list) and candidate else None
|
|
896
|
+
revised, converged = self.evaluate_solution(solution)
|
|
897
|
+
if converged:
|
|
898
|
+
self.insert_solution(revised)
|
|
899
|
+
|
|
900
|
+
def make_change_16_structural_hypothesis(self, candidate):
|
|
901
|
+
"""Model-developer move: test a structural hypothesis by taking
|
|
902
|
+
one origin profile and sweeping across ALL destination attributes.
|
|
903
|
+
'Does bike ownership moderate sensitivity to every destination
|
|
904
|
+
feature, or just distance?'"""
|
|
905
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
906
|
+
asc_ind, _, _ = self.get_best_features(candidate)
|
|
907
|
+
|
|
908
|
+
# Find which origin profiles are currently used via interactions
|
|
909
|
+
active_origins = set()
|
|
910
|
+
for v in best_asvars:
|
|
911
|
+
if v.startswith('inter__prop_'):
|
|
912
|
+
parts = v.replace('inter__', '').split('__')
|
|
913
|
+
if len(parts) >= 1:
|
|
914
|
+
active_origins.add(parts[0])
|
|
915
|
+
|
|
916
|
+
if not active_origins:
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
# Pick one active origin profile to sweep
|
|
920
|
+
origins_list = sorted(active_origins)
|
|
921
|
+
origin = self.param.generator.choice(origins_list)
|
|
922
|
+
|
|
923
|
+
# For this origin, add interactions with every OTHER destination attr
|
|
924
|
+
# that isn't already present (structural completeness hypothesis)
|
|
925
|
+
for var_name in self.param.avail_asvars:
|
|
926
|
+
if not var_name.startswith('inter__'):
|
|
927
|
+
continue
|
|
928
|
+
if var_name in best_asvars:
|
|
929
|
+
continue
|
|
930
|
+
if origin not in var_name:
|
|
931
|
+
continue
|
|
932
|
+
|
|
933
|
+
trial_vars = best_asvars + [var_name]
|
|
934
|
+
trial_vars = list(dict.fromkeys(trial_vars))
|
|
935
|
+
trial_vars = self.remove_collinear_vars(trial_vars)
|
|
936
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=trial_vars,
|
|
937
|
+
isvars=best_isvars, randvars=best_randvars,
|
|
938
|
+
bcvars=best_bcvars, corvars=best_corvars,
|
|
939
|
+
asc_ind=asc_ind)
|
|
940
|
+
revised, converged = self.evaluate_solution(solution)
|
|
941
|
+
if converged:
|
|
942
|
+
self.insert_solution(revised)
|
|
943
|
+
|
|
944
|
+
def _build_substitution_groups(self, asvars):
|
|
945
|
+
"""Identify groups of variables that measure similar constructs
|
|
946
|
+
and can be substituted for one another."""
|
|
947
|
+
groups = []
|
|
948
|
+
# Group 1: destination size measures
|
|
949
|
+
size_measures = [v for v in asvars
|
|
950
|
+
if v in {'ln_dest_trips', 'osm_destination_pull',
|
|
951
|
+
'acdc_aggl', 'osm_poi_intensity'}]
|
|
952
|
+
for i in range(len(size_measures)):
|
|
953
|
+
for j in range(i + 1, len(size_measures)):
|
|
954
|
+
groups.append((size_measures[i], size_measures[j]))
|
|
955
|
+
|
|
956
|
+
# Group 2: distance-related measures
|
|
957
|
+
dist_measures = [v for v in asvars
|
|
958
|
+
if v in {'ln_dist', 'dist_km',
|
|
959
|
+
'acdc_nearest_dest_km', 'acdc_weighted_mean_dist_km'}]
|
|
960
|
+
for i in range(len(dist_measures)):
|
|
961
|
+
for j in range(i + 1, len(dist_measures)):
|
|
962
|
+
groups.append((dist_measures[i], dist_measures[j]))
|
|
963
|
+
|
|
964
|
+
# Group 3: OSM amenity measures
|
|
965
|
+
osm_measures = [v for v in asvars
|
|
966
|
+
if v.startswith('osm_') and not v.startswith('osm_dest')]
|
|
967
|
+
for i in range(len(osm_measures)):
|
|
968
|
+
for j in range(i + 1, len(osm_measures)):
|
|
969
|
+
groups.append((osm_measures[i], osm_measures[j]))
|
|
970
|
+
|
|
971
|
+
return groups
|
|
972
|
+
|
|
973
|
+
def _get_forced_vars(self):
|
|
974
|
+
"""Return list of variables that must never be removed."""
|
|
975
|
+
forced = set()
|
|
976
|
+
forced.update(getattr(self.param, 'ps_asvars', []))
|
|
977
|
+
forced.update(getattr(self.param, 'ps_isvars', []))
|
|
978
|
+
if hasattr(self.param, 'pres_spec_constr') and self.param.pres_spec_constr:
|
|
979
|
+
forced.update(self.param.pres_spec_constr.get('force_include', []))
|
|
980
|
+
return forced
|
|
981
|
+
|
|
982
|
+
def local_search_developer(self, iter: int, phase: str = 'early'):
|
|
983
|
+
"""
|
|
984
|
+
Run developer-style local improvement moves on the top solutions.
|
|
985
|
+
Phase determines which moves are used:
|
|
986
|
+
- early (first 40%): broad exploration (structural sweep, substitutions)
|
|
987
|
+
- mid (40-70%): refinement (significance pruning, distribution sweep)
|
|
988
|
+
- late (70-100%): convergence (model progression, ensemble crossover)
|
|
989
|
+
"""
|
|
990
|
+
if len(self.memory) < 2:
|
|
991
|
+
return
|
|
992
|
+
|
|
993
|
+
memory_sorted = self.sort_memory(self.memory[:])
|
|
994
|
+
top_n = min(5, len(memory_sorted))
|
|
995
|
+
candidates = memory_sorted[:top_n]
|
|
996
|
+
|
|
997
|
+
# Weight schedule: more moves in mid/late phases
|
|
998
|
+
if phase == 'early':
|
|
999
|
+
moves = [self.make_change_16_structural_hypothesis,
|
|
1000
|
+
self.make_change_15_feature_substitution]
|
|
1001
|
+
per_candidate = 1
|
|
1002
|
+
elif phase == 'mid':
|
|
1003
|
+
moves = [self.make_change_11_significance_prune,
|
|
1004
|
+
self.make_change_12_distribution_sweep,
|
|
1005
|
+
self.make_change_15_feature_substitution]
|
|
1006
|
+
per_candidate = 2
|
|
1007
|
+
else:
|
|
1008
|
+
moves = [self.make_change_13_model_progression,
|
|
1009
|
+
self.make_change_14_ensemble_crossover,
|
|
1010
|
+
self.make_change_11_significance_prune]
|
|
1011
|
+
per_candidate = 3
|
|
1012
|
+
|
|
1013
|
+
for cand in candidates[:per_candidate]:
|
|
1014
|
+
for move in moves:
|
|
1015
|
+
try:
|
|
1016
|
+
move(cand)
|
|
1017
|
+
except Exception:
|
|
1018
|
+
pass
|
|
1019
|
+
|
|
708
1020
|
def local_search(self):
|
|
709
1021
|
# {
|
|
710
|
-
#
|
|
711
|
-
|
|
1022
|
+
# ── Phase-aware local search ─────────────────────────────────
|
|
1023
|
+
# Uses the current improvisation progress to switch between
|
|
1024
|
+
# exploration (early), refinement (mid), and convergence (late).
|
|
1025
|
+
progress = getattr(self, '_improv_iter', 0) / max(self.maxiter, 1)
|
|
1026
|
+
if progress < 0.4:
|
|
1027
|
+
phase = 'early'
|
|
1028
|
+
elif progress < 0.7:
|
|
1029
|
+
phase = 'mid'
|
|
1030
|
+
else:
|
|
1031
|
+
phase = 'late'
|
|
1032
|
+
self.local_search_developer(0, phase=phase)
|
|
712
1033
|
|
|
713
|
-
#
|
|
1034
|
+
# Also run the original local search changes (1-10) at reduced rate
|
|
1035
|
+
candidate = [sol for sol in self.memory if abs(sol.obj[0]) < BOUND]
|
|
714
1036
|
# }
|
|
715
1037
|
|
|
716
1038
|
''' ---------------------------------------------------------- '''
|
|
@@ -755,6 +1077,8 @@ class HarmonySearch(Search):
|
|
|
755
1077
|
best, current = [], []
|
|
756
1078
|
for iter in range(self.maxiter):
|
|
757
1079
|
# {
|
|
1080
|
+
self._improv_iter = iter # track for phase-aware local search
|
|
1081
|
+
|
|
758
1082
|
# Compute consideration rate and pitch value
|
|
759
1083
|
# This code introduces oscillations (a.k.a., variations) based on the iteration number.
|
|
760
1084
|
# The result is scaled by the sine function only when its value is non-negative.
|
|
@@ -768,13 +1092,10 @@ class HarmonySearch(Search):
|
|
|
768
1092
|
# {
|
|
769
1093
|
self.insert_solution(curr_sol)
|
|
770
1094
|
|
|
771
|
-
#
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
#best.append(best_sol.obj[0])
|
|
776
|
-
#current.append(curr_sol.obj[0])
|
|
777
|
-
# }
|
|
1095
|
+
# Phase-aware local search: every 10 iterations, run
|
|
1096
|
+
# developer-style moves on the top solutions
|
|
1097
|
+
if iter > 0 and iter % 10 == 0 and len(self.memory) >= 2:
|
|
1098
|
+
self.local_search()
|
|
778
1099
|
# }
|
|
779
1100
|
# }
|
|
780
1101
|
|
|
@@ -1223,6 +1223,20 @@ class Search():
|
|
|
1223
1223
|
self.idnum = idnum
|
|
1224
1224
|
self.local_impr = 0
|
|
1225
1225
|
|
|
1226
|
+
# ── Banlist: specifications that have already failed ────────────
|
|
1227
|
+
# Signatures (SHA-256 hashes) of specs that crashed or returned
|
|
1228
|
+
# non-converged / infinite results. These are never visited again.
|
|
1229
|
+
self._banlist = set()
|
|
1230
|
+
|
|
1231
|
+
# ── Variable-level failure tracker ──────────────────────────────
|
|
1232
|
+
# {varname: count} — how many times each variable appeared in a
|
|
1233
|
+
# failed (non-convergent, exception, infinite-LL) specification.
|
|
1234
|
+
# When count exceeds _var_attrition_limit the variable is removed
|
|
1235
|
+
# from the available lists so it never appears again for the
|
|
1236
|
+
# remainder of the search.
|
|
1237
|
+
self._var_failures = {}
|
|
1238
|
+
self._var_attrition_limit = 15 # failures before permanent removal
|
|
1239
|
+
|
|
1226
1240
|
# ── Latent class feature toggles ────────────────────────────────
|
|
1227
1241
|
self.optimise_class = kwargs.get('optimise_class', False)
|
|
1228
1242
|
self.optimise_membership = kwargs.get('optimise_membership', False)
|
|
@@ -2160,6 +2174,24 @@ class Search():
|
|
|
2160
2174
|
if 'corvars' in solution and v in solution['corvars']:
|
|
2161
2175
|
solution['corvars'] = [x for x in solution['corvars'] if x != v]
|
|
2162
2176
|
|
|
2177
|
+
def _cull_attrited_vars(self):
|
|
2178
|
+
"""Remove variables from the available pools that have exceeded the
|
|
2179
|
+
failure threshold. Once removed they stay out for the rest of
|
|
2180
|
+
the search, freeing the algorithm to focus on viable variables.
|
|
2181
|
+
"""
|
|
2182
|
+
limit = self._var_attrition_limit
|
|
2183
|
+
forced = set(self._get_forced_vars())
|
|
2184
|
+
to_kill = {v for v, cnt in self._var_failures.items()
|
|
2185
|
+
if cnt >= limit and v not in forced}
|
|
2186
|
+
if not to_kill:
|
|
2187
|
+
return
|
|
2188
|
+
for v in sorted(to_kill):
|
|
2189
|
+
if hasattr(self.param, 'avail_asvars') and v in self.param.avail_asvars:
|
|
2190
|
+
self.param.avail_asvars.remove(v)
|
|
2191
|
+
if hasattr(self.param, 'avail_isvars') and v in self.param.avail_isvars:
|
|
2192
|
+
self.param.avail_isvars.remove(v)
|
|
2193
|
+
print(f" [attrition] '{v}' removed from search after {self._var_failures[v]} failures")
|
|
2194
|
+
|
|
2163
2195
|
def _apply_latent_class_constraints(self, solution, lc_constraints):
|
|
2164
2196
|
"""Apply constraints specific to latent class models."""
|
|
2165
2197
|
# Example: force variables to appear only in certain classes
|
|
@@ -3685,6 +3717,11 @@ class Search():
|
|
|
3685
3717
|
''' ---------------------------------------------------------- '''
|
|
3686
3718
|
def evaluate_solution(self, sol):
|
|
3687
3719
|
# {
|
|
3720
|
+
sig = self.setup_signature(sol)
|
|
3721
|
+
if sig in self._banlist:
|
|
3722
|
+
sol['converged'] = False
|
|
3723
|
+
return (sol, False)
|
|
3724
|
+
|
|
3688
3725
|
as_vars, is_vars, rand_vars, bc_vars, corvars, asc_ind = self.get_components(sol)
|
|
3689
3726
|
all_vars = is_vars + as_vars
|
|
3690
3727
|
all_vars = [var for var in self.param.varnames if var in all_vars]
|
|
@@ -3692,17 +3729,32 @@ class Search():
|
|
|
3692
3729
|
# Estimate model if input variables are present in specification
|
|
3693
3730
|
if not all_vars:
|
|
3694
3731
|
sol['converged'] = False
|
|
3732
|
+
self._banlist.add(sig)
|
|
3695
3733
|
return (sol, False)
|
|
3696
3734
|
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3735
|
+
try:
|
|
3736
|
+
# Run backward elimination if enabled (default True), otherwise a single fit
|
|
3737
|
+
all_sig = getattr(self.param, 'all_sig', True)
|
|
3738
|
+
if all_sig:
|
|
3739
|
+
sol, converged = self.backward_eliminate(sol)
|
|
3740
|
+
else:
|
|
3741
|
+
result = self.evaluate_model(sol)
|
|
3742
|
+
aic, bic, loglik, mae, asvars, isvars, randvars, bcvars, corvars, converged, sol = result
|
|
3743
|
+
sol['bcvars'] = bcvars
|
|
3744
|
+
sol['aic'], sol['bic'], sol['loglik'], sol['mae'] = aic, bic, loglik, mae
|
|
3745
|
+
except Exception:
|
|
3746
|
+
sol['converged'] = False
|
|
3747
|
+
self._banlist.add(sig)
|
|
3748
|
+
if hasattr(self, 'best_solution') and self.best_solution is not None:
|
|
3749
|
+
fail_vars = set(as_vars + is_vars)
|
|
3750
|
+
base_as = set(self.best_solution.get('asvars', []))
|
|
3751
|
+
base_is = set(self.best_solution.get('isvars', []))
|
|
3752
|
+
new_vars = fail_vars - (base_as | base_is)
|
|
3753
|
+
if new_vars:
|
|
3754
|
+
for v in new_vars:
|
|
3755
|
+
self._var_failures[v] = self._var_failures.get(v, 0) + 1
|
|
3756
|
+
self._cull_attrited_vars()
|
|
3757
|
+
return (sol, False)
|
|
3706
3758
|
|
|
3707
3759
|
if converged or (isinstance(sol.get('loglik'), float) and math.isfinite(sol.get('loglik', float('nan')))):
|
|
3708
3760
|
# {
|
|
@@ -3720,6 +3772,20 @@ class Search():
|
|
|
3720
3772
|
# {
|
|
3721
3773
|
self.not_converged += 1
|
|
3722
3774
|
sol['converged'] = False
|
|
3775
|
+
# ── Banlist: never visit this exact specification again
|
|
3776
|
+
self._banlist.add(sig)
|
|
3777
|
+
# ── Variable attrition: only blame vars ADDED vs the
|
|
3778
|
+
# currently-accepted best (neighbour) model.
|
|
3779
|
+
# Skip entirely on init / startup — no baseline yet.
|
|
3780
|
+
if hasattr(self, 'best_solution') and self.best_solution is not None:
|
|
3781
|
+
fail_vars = set(as_vars + is_vars)
|
|
3782
|
+
base_as = set(self.best_solution.get('asvars', []))
|
|
3783
|
+
base_is = set(self.best_solution.get('isvars', []))
|
|
3784
|
+
new_vars = fail_vars - (base_as | base_is)
|
|
3785
|
+
if new_vars:
|
|
3786
|
+
for v in new_vars:
|
|
3787
|
+
self._var_failures[v] = self._var_failures.get(v, 0) + 1
|
|
3788
|
+
self._cull_attrited_vars()
|
|
3723
3789
|
# ── Convergence diagnostic: explain why the model did not converge
|
|
3724
3790
|
self._diagnose_nonconvergence(sol, model_n=sol.get('model_n', ''))
|
|
3725
3791
|
# }
|
|
@@ -4009,17 +4075,8 @@ class Search():
|
|
|
4009
4075
|
model.fit()
|
|
4010
4076
|
|
|
4011
4077
|
|
|
4012
|
-
#
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
def fit_random_regret(self, df):
|
|
4016
|
-
model = RandomRegret(df=df, short=False, normalize=True)
|
|
4017
|
-
#a = (df, s)
|
|
4018
|
-
# model.setup_long()
|
|
4019
|
-
# model.setup(X, y, varnames,alts , isvars, transvars, ids, weights, panels, avail, base_alt)
|
|
4020
|
-
model.fit()
|
|
4021
|
-
model.report()
|
|
4022
|
-
return model
|
|
4078
|
+
# fit_random_regret moved below with transvars support
|
|
4079
|
+
# (see evaluate_rrm section)
|
|
4023
4080
|
|
|
4024
4081
|
|
|
4025
4082
|
|
|
@@ -4166,40 +4223,27 @@ class Search():
|
|
|
4166
4223
|
|
|
4167
4224
|
def evaluate_nested_logit(self, sol):
|
|
4168
4225
|
"""Evaluates a Nested Logit model."""
|
|
4169
|
-
|
|
4170
|
-
sol =self.apply_constraints(sol)
|
|
4226
|
+
sol = self.apply_constraints(sol)
|
|
4171
4227
|
as_vars, is_vars, asc_ind = sol['asvars'], sol['isvars'], sol['asc_ind']
|
|
4172
|
-
|
|
4173
|
-
state = sol['state']
|
|
4174
|
-
|
|
4175
|
-
#state['nest_vars']
|
|
4176
|
-
#sol = self.apply_constraints(sol)
|
|
4177
|
-
#sol = self.repair_solution(sol)
|
|
4178
|
-
# Define nests and lambda values (adjust based on your data)
|
|
4179
|
-
# TODO NEED TO FEED IN THE NESTS FROM params
|
|
4228
|
+
bc_vars = self.define_bc_vars(sol)
|
|
4180
4229
|
nests = self.param.nests
|
|
4181
4230
|
lambdas = self.param.lambdas
|
|
4182
|
-
#lambdas_mapping = self.param.lambdas_mapping
|
|
4183
4231
|
|
|
4184
|
-
# Filter the variables to include in the model
|
|
4185
4232
|
all_vars = as_vars + is_vars
|
|
4186
4233
|
if len(all_vars) == 0:
|
|
4187
4234
|
raise ValueError('need a variable: todo debug why')
|
|
4188
4235
|
all_vars = [var for var in self.param.varnames if var in all_vars]
|
|
4189
4236
|
nest_vars = [var for var in self.param.varnest if var in all_vars]
|
|
4190
4237
|
|
|
4191
|
-
#to do if nest_vars is None repair
|
|
4192
|
-
# if nest vars)not in all_var repair
|
|
4193
|
-
|
|
4194
|
-
# Prepare data for the nested logit model
|
|
4195
4238
|
X, y = self.param.df[all_vars].values, self.param.choices
|
|
4196
4239
|
X_nest = self.param.df[nest_vars]
|
|
4197
4240
|
|
|
4198
|
-
# Fit the Nested Logit model
|
|
4199
4241
|
model = NestedLogit(_jax=getattr(self.param, '_jax', True))
|
|
4200
|
-
model.setup(X=X, X_nest
|
|
4242
|
+
model.setup(X=X, X_nest=X_nest, y=y, varnames=all_vars, isvars=is_vars,
|
|
4201
4243
|
alts=self.param.alt_var, ids=self.param.choice_id,
|
|
4202
|
-
nests=nests, lambdas=lambdas, fit_intercept=asc_ind,
|
|
4244
|
+
nests=nests, lambdas=lambdas, fit_intercept=asc_ind,
|
|
4245
|
+
transvars=bc_vars,
|
|
4246
|
+
return_grad=self.param.grad, return_hess=self.param.hess)
|
|
4203
4247
|
|
|
4204
4248
|
model.fit()
|
|
4205
4249
|
|
|
@@ -4234,6 +4278,7 @@ class Search():
|
|
|
4234
4278
|
sol = self.apply_constraints(sol)
|
|
4235
4279
|
as_vars, is_vars, asc_ind = sol['asvars'], sol['isvars'], sol['asc_ind']
|
|
4236
4280
|
randvars = sol.get('randvars', {})
|
|
4281
|
+
bc_vars = self.define_bc_vars(sol)
|
|
4237
4282
|
|
|
4238
4283
|
nests = self.param.nests
|
|
4239
4284
|
lambdas = self.param.lambdas
|
|
@@ -4257,6 +4302,7 @@ class Search():
|
|
|
4257
4302
|
nests=nests,
|
|
4258
4303
|
lambdas=lambdas,
|
|
4259
4304
|
randvars=randvars,
|
|
4305
|
+
transvars=bc_vars,
|
|
4260
4306
|
panels=self.param.ind_id,
|
|
4261
4307
|
fit_intercept=asc_ind,
|
|
4262
4308
|
n_draws=n_draws,
|
|
@@ -4277,30 +4323,26 @@ class Search():
|
|
|
4277
4323
|
|
|
4278
4324
|
|
|
4279
4325
|
def evaluate_nested_logit_ml(self, sol):
|
|
4280
|
-
"""Evaluates a Nested Logit model."""
|
|
4281
|
-
# Extract relevant model parameters
|
|
4326
|
+
"""Evaluates a Multi-Layer Nested Logit model."""
|
|
4282
4327
|
as_vars, is_vars, asc_ind = sol['asvars'], sol['isvars'], sol['asc_ind']
|
|
4328
|
+
bc_vars = self.define_bc_vars(sol)
|
|
4283
4329
|
|
|
4284
|
-
# Define nests and lambda values (adjust based on your data)
|
|
4285
|
-
#TODO NEED TO FEED IN THE NESTS FROM params
|
|
4286
4330
|
nests = self.param.nests
|
|
4287
4331
|
lambdas = self.param.lambdas
|
|
4288
4332
|
lambdas_mapping = self.param.lambdas_mapping
|
|
4289
4333
|
|
|
4290
|
-
# Filter the variables to include in the model
|
|
4291
4334
|
all_vars = as_vars + is_vars
|
|
4292
|
-
if len(all_vars) ==0:
|
|
4335
|
+
if len(all_vars) == 0:
|
|
4293
4336
|
raise ValueError('need a variable: todo debug why')
|
|
4294
4337
|
all_vars = [var for var in self.param.varnames if var in all_vars]
|
|
4295
4338
|
|
|
4296
|
-
# Prepare data for the nested logit model
|
|
4297
4339
|
X, y = self.param.df[all_vars].values, self.param.choices
|
|
4298
4340
|
|
|
4299
|
-
# Fit the Nested Logit model
|
|
4300
4341
|
model = MultiLayerNestedLogit()
|
|
4301
4342
|
model.setup(X=X, y=y, varnames=all_vars, isvars=is_vars,
|
|
4302
|
-
|
|
4303
|
-
|
|
4343
|
+
alts=self.param.alt_var, ids=self.param.choice_id,
|
|
4344
|
+
nests=nests, lambdas=lambdas, lambdas_mapping=lambdas_mapping,
|
|
4345
|
+
transvars=bc_vars, fit_intercept=asc_ind, return_grad=False)
|
|
4304
4346
|
|
|
4305
4347
|
model.fit()
|
|
4306
4348
|
|
|
@@ -4383,26 +4425,42 @@ class Search():
|
|
|
4383
4425
|
|
|
4384
4426
|
return sub, all_attr_vars
|
|
4385
4427
|
|
|
4386
|
-
def fit_random_regret(self, df, use_jax=True):
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
model
|
|
4428
|
+
def fit_random_regret(self, df, use_jax=True, transvars=None):
|
|
4429
|
+
if transvars:
|
|
4430
|
+
# Build model via setup() so transvars flow through pre_process
|
|
4431
|
+
# RRM uses attribute_vars as the model variables
|
|
4432
|
+
all_vars = list(df.columns.difference(['id', 'alt', 'choice', 'weight']))
|
|
4433
|
+
X = df[all_vars].values
|
|
4434
|
+
y = df['choice'].values.astype(np.int32)
|
|
4435
|
+
alts = df['alt'].values.astype(np.int32)
|
|
4436
|
+
ids = df['id'].values.astype(np.int32)
|
|
4437
|
+
model = RandomRegret()
|
|
4438
|
+
model.setup(X=X, y=y, varnames=all_vars, alts=alts, ids=ids,
|
|
4439
|
+
transvars=[v for v in transvars if v in all_vars])
|
|
4440
|
+
if use_jax:
|
|
4441
|
+
model.fit_jax()
|
|
4442
|
+
else:
|
|
4443
|
+
model.fit()
|
|
4390
4444
|
else:
|
|
4391
|
-
model
|
|
4445
|
+
model = RandomRegret(df=df, short=False, normalize=True)
|
|
4446
|
+
if use_jax:
|
|
4447
|
+
model.fit_jax()
|
|
4448
|
+
else:
|
|
4449
|
+
model.fit()
|
|
4392
4450
|
model.report()
|
|
4393
4451
|
return model
|
|
4394
4452
|
|
|
4395
4453
|
def evaluate_rrm(self, sol):
|
|
4396
4454
|
as_vars, is_vars, asc_ind = sol['asvars'], sol['isvars'], sol['asc_ind']
|
|
4455
|
+
bc_vars = self.define_bc_vars(sol)
|
|
4456
|
+
bc_vars = [v for v in bc_vars if v not in self.param.isvarnames]
|
|
4397
4457
|
|
|
4398
4458
|
df, attr_vars = self._build_rrm_df(self.param.df, as_vars, is_vars)
|
|
4399
|
-
model = self.fit_random_regret(df=df)
|
|
4459
|
+
model = self.fit_random_regret(df=df, transvars=bc_vars)
|
|
4400
4460
|
sol['model'] = model
|
|
4401
4461
|
sol['coeff'] = model.coeff_est if hasattr(model, 'coeff_est') else model.beta
|
|
4402
4462
|
converged = model.converged
|
|
4403
4463
|
aic, bic, loglik = model.aic, model.bic, model.loglik
|
|
4404
|
-
bc_vars = self.define_bc_vars(sol)
|
|
4405
|
-
bc_vars = [v for v in bc_vars if v not in self.param.isvarnames]
|
|
4406
4464
|
rand_vars, cor_vars = {}, []
|
|
4407
4465
|
|
|
4408
4466
|
if self.mae_is_an_objective():
|
|
@@ -4432,7 +4490,8 @@ class Search():
|
|
|
4432
4490
|
try:
|
|
4433
4491
|
model.setup(X=X, y=y, varnames=all_vars, alts=self.param.alt_var,
|
|
4434
4492
|
isvars=is_vars, ids=self.param.choice_id,
|
|
4435
|
-
randvars=rand_vars,
|
|
4493
|
+
randvars=rand_vars, transvars=bc_vars,
|
|
4494
|
+
panels=self.param.ind_id,
|
|
4436
4495
|
avail=self.param.avail, base_alt=self.param.base_alt,
|
|
4437
4496
|
maxiter=self.param.maxiter, ftol=self.param.ftol,
|
|
4438
4497
|
gtol=self.param.gtol)
|
|
@@ -4457,12 +4516,13 @@ class Search():
|
|
|
4457
4516
|
def evaluate_ordered_logit(self,sol):
|
|
4458
4517
|
|
|
4459
4518
|
as_vars, is_vars, asc_ind = sol['asvars'], sol['isvars'], sol['asc_ind']
|
|
4519
|
+
bc_vars = self.define_bc_vars(sol)
|
|
4520
|
+
bc_vars = [var for var in bc_vars if var not in self.param.isvarnames]
|
|
4460
4521
|
|
|
4461
4522
|
|
|
4462
4523
|
all_vars = as_vars + is_vars
|
|
4463
4524
|
|
|
4464
4525
|
all_vars = [var for var in self.param.varnames if var in all_vars]
|
|
4465
|
-
all_vars = all_vars
|
|
4466
4526
|
|
|
4467
4527
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4468
4528
|
#df_long = misc.wide_to_long(self.param.df, id_col='id', alt_list=self.param.alt_var, alt_name='alt')
|
|
@@ -4470,13 +4530,13 @@ class Search():
|
|
|
4470
4530
|
#y = df_long['choice']
|
|
4471
4531
|
X, y = self.param.df[all_vars], self.param.choices
|
|
4472
4532
|
J = len(np.unique(self.param.alt_var))
|
|
4473
|
-
model = self.fit_ordered_logit(X=X, y=y, ids
|
|
4474
|
-
|
|
4533
|
+
model = self.fit_ordered_logit(X=X, y=y, ids=self.param.choice_id,
|
|
4534
|
+
varnames=all_vars, choices=J,
|
|
4535
|
+
transvars=bc_vars)
|
|
4536
|
+
sol['model'] = model
|
|
4475
4537
|
sol['coeff'] = model.coeff_est
|
|
4476
4538
|
converged = model.converged
|
|
4477
4539
|
aic, bic, loglik = model.aic, model.bic, model.loglik
|
|
4478
|
-
bc_vars = self.define_bc_vars(sol)
|
|
4479
|
-
bc_vars = [var for var in bc_vars if var not in self.param.isvarnames]
|
|
4480
4540
|
alts = self.param.alt_var
|
|
4481
4541
|
rand_vars, cor_vars = {}, []
|
|
4482
4542
|
|
|
@@ -4498,7 +4558,7 @@ class Search():
|
|
|
4498
4558
|
return tuple
|
|
4499
4559
|
|
|
4500
4560
|
|
|
4501
|
-
def fit_ordered_logit(self, X, y, ids, varnames, choices):
|
|
4561
|
+
def fit_ordered_logit(self, X, y, ids, varnames, choices, transvars=None):
|
|
4502
4562
|
|
|
4503
4563
|
|
|
4504
4564
|
moll = OrderedLogitLong(X=X.values,
|
|
@@ -4509,7 +4569,8 @@ class Search():
|
|
|
4509
4569
|
distr='logit',
|
|
4510
4570
|
start=None,
|
|
4511
4571
|
normalize=False,
|
|
4512
|
-
fit_intercept=False
|
|
4572
|
+
fit_intercept=False,
|
|
4573
|
+
transvars=transvars or [])
|
|
4513
4574
|
|
|
4514
4575
|
moll.fit(method='BFGS')
|
|
4515
4576
|
moll.report()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.130
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.130
|
|
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>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.0.128
|
|
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.128 → searchlibrium-0.0.130}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.128 → searchlibrium-0.0.130}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|