SearchLibrium 0.0.128__tar.gz → 0.0.129__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.129}/PKG-INFO +1 -1
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/pyproject.toml +1 -1
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/harmony.py +331 -10
- searchlibrium-0.0.129/src/SearchLibrium/version.txt +1 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/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.129}/README.md +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/setup.cfg +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/Halton.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/MixedLogit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/Mode_Activity_Nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/RandomP.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/SEARCH_SM_MARIO.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/Two_Level_Nest.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/__init__.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/__main__.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/_choice_model.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/_device.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/banditsa.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/bhhh/minimize.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/boxcox_functions.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/call_meta.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/constraints_builder.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/hspbil.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/latent_class.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/main.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/main_debug.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/mdcev.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/misc.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/mixed_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/mixed_nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/mixedrrm.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_nested.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/multinomial_probit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/ordered_logit.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/ordered_logit_mixed.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/predict.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/rrm.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/sapbil.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/search.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/selection_models.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/setup.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/siman.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/test_lc_de.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/test_mario_searches.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/test_sapbil_vs_banditsa.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/threshold.py +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/SOURCES.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/dependency_links.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/entry_points.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/requires.txt +0 -0
- {searchlibrium-0.0.128 → searchlibrium-0.0.129}/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.129
|
|
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.129"
|
|
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
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.129
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SearchLibrium
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.129
|
|
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
|
|
File without changes
|
{searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium/test_sapbil_vs_banditsa.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{searchlibrium-0.0.128 → searchlibrium-0.0.129}/src/SearchLibrium.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|