PyIAML 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. iaml/__init__.py +56 -0
  2. iaml/actionable.py +11 -0
  3. iaml/actionables/__init__.py +21 -0
  4. iaml/actionables/boosting/__init__.py +4 -0
  5. iaml/actionables/boosting/act_adaboost.py +59 -0
  6. iaml/actionables/cleaning/__init__.py +26 -0
  7. iaml/actionables/cleaning/act_categorical_imputer.py +124 -0
  8. iaml/actionables/cleaning/act_count_vectorizer.py +204 -0
  9. iaml/actionables/cleaning/act_drop_categorical_column.py +51 -0
  10. iaml/actionables/cleaning/act_drop_date_column.py +48 -0
  11. iaml/actionables/cleaning/act_drop_high_cardinality_categorical.py +337 -0
  12. iaml/actionables/cleaning/act_drop_numerical_column.py +75 -0
  13. iaml/actionables/cleaning/act_drop_textual_column.py +51 -0
  14. iaml/actionables/cleaning/act_encode_target_column.py +56 -0
  15. iaml/actionables/cleaning/act_frequency_encoder.py +127 -0
  16. iaml/actionables/cleaning/act_hashing_vectorizer.py +186 -0
  17. iaml/actionables/cleaning/act_knn_imputer.py +152 -0
  18. iaml/actionables/cleaning/act_mean_column.py +79 -0
  19. iaml/actionables/cleaning/act_mice.py +464 -0
  20. iaml/actionables/cleaning/act_missing_count_feature.py +109 -0
  21. iaml/actionables/cleaning/act_missing_indicator.py +124 -0
  22. iaml/actionables/cleaning/act_onehot.py +65 -0
  23. iaml/actionables/cleaning/act_ordinal_encoder.py +177 -0
  24. iaml/actionables/cleaning/act_rare_category_grouper.py +173 -0
  25. iaml/actionables/cleaning/act_simple_imputer.py +109 -0
  26. iaml/actionables/cleaning/act_split_date.py +68 -0
  27. iaml/actionables/cleaning/act_target_encoder.py +274 -0
  28. iaml/actionables/cleaning/act_text_normalizer.py +241 -0
  29. iaml/actionables/cleaning/act_tf_idf.py +80 -0
  30. iaml/actionables/cleaning/act_word2vec.py +150 -0
  31. iaml/actionables/features_precleaning/__init__.py +12 -0
  32. iaml/actionables/features_precleaning/act_coerce_numeric_strings.py +194 -0
  33. iaml/actionables/features_precleaning/act_date_converter.py +99 -0
  34. iaml/actionables/features_precleaning/act_drop_bad_quality_rows.py +77 -0
  35. iaml/actionables/features_precleaning/act_drop_duplicate_rows.py +131 -0
  36. iaml/actionables/features_precleaning/act_drop_high_missing_columns.py +94 -0
  37. iaml/actionables/features_precleaning/act_drop_id_like_columns.py +294 -0
  38. iaml/actionables/features_precleaning/act_normalize_column_names.py +157 -0
  39. iaml/actionables/features_precleaning/act_sentinel_to_na_n.py +270 -0
  40. iaml/actionables/features_precleaning/act_trim_space.py +79 -0
  41. iaml/actionables/features_preprocessing/__init__.py +18 -0
  42. iaml/actionables/features_preprocessing/act_cyclical_date_encoding.py +212 -0
  43. iaml/actionables/features_preprocessing/act_fast_ica.py +161 -0
  44. iaml/actionables/features_preprocessing/act_feature_agglomeration.py +90 -0
  45. iaml/actionables/features_preprocessing/act_k_bins_discretizer.py +207 -0
  46. iaml/actionables/features_preprocessing/act_k_means_features.py +296 -0
  47. iaml/actionables/features_preprocessing/act_kernel_pca.py +143 -0
  48. iaml/actionables/features_preprocessing/act_log_transformer.py +122 -0
  49. iaml/actionables/features_preprocessing/act_nystroem.py +100 -0
  50. iaml/actionables/features_preprocessing/act_pca.py +77 -0
  51. iaml/actionables/features_preprocessing/act_polynomial_features.py +86 -0
  52. iaml/actionables/features_preprocessing/act_power_transformer.py +106 -0
  53. iaml/actionables/features_preprocessing/act_quantile_transformer.py +114 -0
  54. iaml/actionables/features_preprocessing/act_rbf_sampler.py +88 -0
  55. iaml/actionables/features_preprocessing/act_select_percentile.py +112 -0
  56. iaml/actionables/features_preprocessing/act_sparse_random_projection.py +157 -0
  57. iaml/actionables/features_preprocessing/act_truncated_svd.py +137 -0
  58. iaml/actionables/features_selection/__init__.py +8 -0
  59. iaml/actionables/features_selection/act_permutation_importance_selector.py +421 -0
  60. iaml/actionables/features_selection/act_remove_high_correlated_column.py +70 -0
  61. iaml/actionables/features_selection/act_remove_low_variance_column.py +74 -0
  62. iaml/actionables/features_selection/act_rfe.py +214 -0
  63. iaml/actionables/features_selection/act_select_from_model.py +325 -0
  64. iaml/actionables/features_selection/act_select_k_best.py +181 -0
  65. iaml/actionables/features_selection/act_vif_selector.py +130 -0
  66. iaml/actionables/imbalance/__init__.py +10 -0
  67. iaml/actionables/imbalance/act_adasyn.py +150 -0
  68. iaml/actionables/imbalance/act_borderline_smote.py +171 -0
  69. iaml/actionables/imbalance/act_near_miss.py +158 -0
  70. iaml/actionables/imbalance/act_random_over_sampling.py +60 -0
  71. iaml/actionables/imbalance/act_random_under_sampler.py +135 -0
  72. iaml/actionables/imbalance/act_smote.py +162 -0
  73. iaml/actionables/imbalance/act_smote_tomek.py +182 -0
  74. iaml/actionables/imbalance/act_smoteenn.py +193 -0
  75. iaml/actionables/imbalance/act_tomek_links.py +138 -0
  76. iaml/actionables/normalize/__init__.py +6 -0
  77. iaml/actionables/normalize/act_max_abs_scaler.py +78 -0
  78. iaml/actionables/normalize/act_minmax_scaler.py +56 -0
  79. iaml/actionables/normalize/act_normalizer.py +95 -0
  80. iaml/actionables/normalize/act_robust_scaler.py +111 -0
  81. iaml/actionables/normalize/act_standard_scaler.py +55 -0
  82. iaml/actionables/predictors/__init__.py +6 -0
  83. iaml/actionables/predictors/_xgboost.py +16 -0
  84. iaml/actionables/predictors/classifier/__init__.py +26 -0
  85. iaml/actionables/predictors/classifier/act_bagging_classifier.py +113 -0
  86. iaml/actionables/predictors/classifier/act_bernoulli_nb.py +89 -0
  87. iaml/actionables/predictors/classifier/act_catboost_classifier.py +135 -0
  88. iaml/actionables/predictors/classifier/act_complement_nb.py +106 -0
  89. iaml/actionables/predictors/classifier/act_decision_tree_classifier.py +117 -0
  90. iaml/actionables/predictors/classifier/act_extra_trees_classifier.py +115 -0
  91. iaml/actionables/predictors/classifier/act_gaussian_nb.py +53 -0
  92. iaml/actionables/predictors/classifier/act_hist_gradient_boosting_classifier.py +144 -0
  93. iaml/actionables/predictors/classifier/act_knn.py +86 -0
  94. iaml/actionables/predictors/classifier/act_light_gbm_classifier.py +211 -0
  95. iaml/actionables/predictors/classifier/act_linear_discriminant_analysis.py +63 -0
  96. iaml/actionables/predictors/classifier/act_linear_svc.py +134 -0
  97. iaml/actionables/predictors/classifier/act_logistic_regression.py +92 -0
  98. iaml/actionables/predictors/classifier/act_mlp_classifier.py +107 -0
  99. iaml/actionables/predictors/classifier/act_multinomial_nb.py +76 -0
  100. iaml/actionables/predictors/classifier/act_passive_aggressive_classifier.py +141 -0
  101. iaml/actionables/predictors/classifier/act_quadratic_discriminant_analysis.py +72 -0
  102. iaml/actionables/predictors/classifier/act_randomforest.py +113 -0
  103. iaml/actionables/predictors/classifier/act_ridge_classifier.py +116 -0
  104. iaml/actionables/predictors/classifier/act_sgd_classifier.py +149 -0
  105. iaml/actionables/predictors/classifier/act_svm_svc.py +88 -0
  106. iaml/actionables/predictors/classifier/act_xgboost.py +111 -0
  107. iaml/actionables/predictors/regressor/__init__.py +27 -0
  108. iaml/actionables/predictors/regressor/act_ada_boost_regressor.py +75 -0
  109. iaml/actionables/predictors/regressor/act_ard_regression.py +95 -0
  110. iaml/actionables/predictors/regressor/act_catboost_regressor.py +134 -0
  111. iaml/actionables/predictors/regressor/act_decision_tree_regressor.py +111 -0
  112. iaml/actionables/predictors/regressor/act_elastic_net_regressor.py +109 -0
  113. iaml/actionables/predictors/regressor/act_extra_trees_regressor.py +113 -0
  114. iaml/actionables/predictors/regressor/act_gaussian_process_regressor.py +55 -0
  115. iaml/actionables/predictors/regressor/act_gboost_regressor.py +95 -0
  116. iaml/actionables/predictors/regressor/act_hist_gradient_boosting_regressor.py +105 -0
  117. iaml/actionables/predictors/regressor/act_huber_regressor.py +101 -0
  118. iaml/actionables/predictors/regressor/act_knn_regressor.py +86 -0
  119. iaml/actionables/predictors/regressor/act_lasso_regressor.py +103 -0
  120. iaml/actionables/predictors/regressor/act_light_gbm_regressor.py +201 -0
  121. iaml/actionables/predictors/regressor/act_linear_regression.py +43 -0
  122. iaml/actionables/predictors/regressor/act_mlp_regressor.py +104 -0
  123. iaml/actionables/predictors/regressor/act_poisson_regressor.py +111 -0
  124. iaml/actionables/predictors/regressor/act_quantile_regressor.py +87 -0
  125. iaml/actionables/predictors/regressor/act_randomforest_regressor.py +116 -0
  126. iaml/actionables/predictors/regressor/act_ransac_regressor.py +106 -0
  127. iaml/actionables/predictors/regressor/act_ridge_regressor.py +107 -0
  128. iaml/actionables/predictors/regressor/act_sgd_regressor.py +106 -0
  129. iaml/actionables/predictors/regressor/act_svm_svr.py +81 -0
  130. iaml/actionables/predictors/regressor/act_xgboost_regressor.py +97 -0
  131. iaml/actionables/predictors/survival/__init__.py +12 -0
  132. iaml/actionables/predictors/survival/act_aalen_additive_model.py +83 -0
  133. iaml/actionables/predictors/survival/act_cox.py +110 -0
  134. iaml/actionables/predictors/survival/act_coxnet_survival_analysis.py +134 -0
  135. iaml/actionables/predictors/survival/act_extra_survival_trees.py +101 -0
  136. iaml/actionables/predictors/survival/act_fast_survival_svm.py +102 -0
  137. iaml/actionables/predictors/survival/act_gradient_boosting_survival_analysis.py +93 -0
  138. iaml/actionables/predictors/survival/act_random_survival_forest.py +91 -0
  139. iaml/actionables/predictors/survival/act_survival_component_wise_gboost.py +80 -0
  140. iaml/actionables/predictors/survival/act_survival_tree.py +120 -0
  141. iaml/actionables/predictors/survival/act_survival_xgboost.py +9 -0
  142. iaml/actionables/predictors/survival/act_weibull_aft.py +230 -0
  143. iaml/cache.py +61 -0
  144. iaml/cache_keys.py +57 -0
  145. iaml/candidate.py +736 -0
  146. iaml/core_dispatcher.py +125 -0
  147. iaml/data_type.py +11 -0
  148. iaml/dataset.py +506 -0
  149. iaml/decorators/__init__.py +3 -0
  150. iaml/decorators/all.py +4 -0
  151. iaml/decorators/is_step.py +45 -0
  152. iaml/decorators/runner.py +100 -0
  153. iaml/explanation.py +112 -0
  154. iaml/iaml.py +1072 -0
  155. iaml/iaml_pipeline.py +600 -0
  156. iaml/logger.py +138 -0
  157. iaml/meta_explorer_step.py +62 -0
  158. iaml/meta_ordered_step.py +28 -0
  159. iaml/meta_partial_explorer_step.py +34 -0
  160. iaml/meta_singleton.py +24 -0
  161. iaml/metastep.py +211 -0
  162. iaml/metric.py +111 -0
  163. iaml/metric_plot.py +82 -0
  164. iaml/metrics/__init__.py +21 -0
  165. iaml/metrics/_classification.py +28 -0
  166. iaml/metrics/_survival_times.py +22 -0
  167. iaml/metrics/accuracy_metric.py +59 -0
  168. iaml/metrics/balanced_accuracy_metric.py +67 -0
  169. iaml/metrics/brier_score.py +90 -0
  170. iaml/metrics/classification_error_metric.py +66 -0
  171. iaml/metrics/concordance_index_ipcw.py +84 -0
  172. iaml/metrics/concordance_index_metric.py +67 -0
  173. iaml/metrics/cumulative_dynamic_auc.py +119 -0
  174. iaml/metrics/f1_score_metric.py +71 -0
  175. iaml/metrics/integrated_brier_score.py +98 -0
  176. iaml/metrics/integrated_brier_score_loss.py +41 -0
  177. iaml/metrics/mean_absolute_error_metric.py +46 -0
  178. iaml/metrics/mean_squared_error_metric.py +46 -0
  179. iaml/metrics/mean_squared_log_error_metric.py +49 -0
  180. iaml/metrics/median_absolute_error_metric.py +48 -0
  181. iaml/metrics/precision_metric.py +63 -0
  182. iaml/metrics/r2_score_metric.py +45 -0
  183. iaml/metrics/recall_metric.py +65 -0
  184. iaml/metrics/roc_auc_metric.py +50 -0
  185. iaml/metrics/specificity_metric.py +44 -0
  186. iaml/metrics/specificity_multiclass_metric.py +55 -0
  187. iaml/metrics/specificity_multilabel_metric.py +60 -0
  188. iaml/optimizers/__init__.py +5 -0
  189. iaml/optimizers/bayesian_optimizer.py +193 -0
  190. iaml/optimizers/genetic_optimizer.py +284 -0
  191. iaml/optimizers/optimizer.py +31 -0
  192. iaml/optimizers/random_optimizer.py +101 -0
  193. iaml/plot.py +138 -0
  194. iaml/plots/__init__.py +32 -0
  195. iaml/plots/bar_plot.py +141 -0
  196. iaml/plots/box_plot.py +166 -0
  197. iaml/plots/class_prediction_error_plot.py +37 -0
  198. iaml/plots/classification_report_plot.py +35 -0
  199. iaml/plots/confusion_matrix_plot.py +34 -0
  200. iaml/plots/correlation_heatmap_plot.py +201 -0
  201. iaml/plots/cumulative_hazard_plot.py +72 -0
  202. iaml/plots/density_plot.py +210 -0
  203. iaml/plots/histogram_plot.py +179 -0
  204. iaml/plots/kaplan_meier_comparison_plot.py +89 -0
  205. iaml/plots/line_plot.py +70 -0
  206. iaml/plots/missingness_heatmap_plot.py +203 -0
  207. iaml/plots/outlier_plot.py +217 -0
  208. iaml/plots/pair_plot.py +228 -0
  209. iaml/plots/precision_recall_curve_plot.py +86 -0
  210. iaml/plots/prediction_error_plot.py +34 -0
  211. iaml/plots/qq_plot.py +220 -0
  212. iaml/plots/residual_plot.py +38 -0
  213. iaml/plots/roc_dynamique_curve_plot.py +79 -0
  214. iaml/plots/rocauc_plot.py +96 -0
  215. iaml/plots/shap_plot.py +187 -0
  216. iaml/plots/target_distribution_plot.py +241 -0
  217. iaml/plots/violin_plot.py +206 -0
  218. iaml/predictor.py +139 -0
  219. iaml/reference.py +65 -0
  220. iaml/shared_cache.py +90 -0
  221. iaml/sklearn_preprocessor.py +74 -0
  222. iaml/splitters/__init__.py +3 -0
  223. iaml/splitters/kfold_splitter.py +32 -0
  224. iaml/splitters/random_splitter.py +26 -0
  225. iaml/stack.py +39 -0
  226. iaml/statistic.py +66 -0
  227. iaml/statistics/__init__.py +77 -0
  228. iaml/statistics/anova_statistic.py +80 -0
  229. iaml/statistics/cardinality_ratio_statistic.py +63 -0
  230. iaml/statistics/category_cooccurrence_statistic.py +79 -0
  231. iaml/statistics/chi_square_statistic.py +81 -0
  232. iaml/statistics/coef_variation_statistic.py +72 -0
  233. iaml/statistics/correlation_with_target.py +105 -0
  234. iaml/statistics/count.py +72 -0
  235. iaml/statistics/data_type_summary_statistic.py +74 -0
  236. iaml/statistics/duplicate_row_statistic.py +56 -0
  237. iaml/statistics/effect_size_statistic.py +129 -0
  238. iaml/statistics/entropy_statistic.py +69 -0
  239. iaml/statistics/event_rate_statistic.py +52 -0
  240. iaml/statistics/grouped_mean_statistic.py +60 -0
  241. iaml/statistics/iqr_statistic.py +66 -0
  242. iaml/statistics/kurtosis.py +50 -0
  243. iaml/statistics/mad_statistic.py +66 -0
  244. iaml/statistics/mean.py +61 -0
  245. iaml/statistics/median_statistic.py +61 -0
  246. iaml/statistics/minmax.py +60 -0
  247. iaml/statistics/missing_rate_statistic.py +62 -0
  248. iaml/statistics/mode.py +47 -0
  249. iaml/statistics/most_frequent_ratio.py +81 -0
  250. iaml/statistics/outlier_count_iqr_statistic.py +76 -0
  251. iaml/statistics/quantile.py +59 -0
  252. iaml/statistics/range.py +53 -0
  253. iaml/statistics/rare_category_rate.py +92 -0
  254. iaml/statistics/skewness.py +53 -0
  255. iaml/statistics/stdev.py +50 -0
  256. iaml/statistics/summary_table_statistic.py +60 -0
  257. iaml/statistics/time_by_group_statistic.py +83 -0
  258. iaml/statistics/time_summary_statistic.py +56 -0
  259. iaml/statistics/top_k_value_counts.py +68 -0
  260. iaml/statistics/unique_count_statistic.py +57 -0
  261. iaml/statistics/value_counts.py +63 -0
  262. iaml/statistics/variance.py +51 -0
  263. iaml/statistics/violin.py +63 -0
  264. iaml/step.py +600 -0
  265. iaml/step_cache.py +87 -0
  266. iaml/step_wrapper.py +79 -0
  267. iaml/timed_pool_executor.py +492 -0
  268. iaml/type_of_target.py +68 -0
  269. iaml/void_step.py +101 -0
  270. iaml/worker_manager.py +169 -0
  271. iaml/wrapper/__init__.py +4 -0
  272. iaml/wrapper/wrap_basic_gridsearch.py +68 -0
  273. iaml/wrapper/wrap_genetic_gridsearch.py +293 -0
  274. iaml/wrapper/wrap_iterative_gridsearch.py +399 -0
  275. pyiaml-1.0.0.dist-info/METADATA +802 -0
  276. pyiaml-1.0.0.dist-info/RECORD +279 -0
  277. pyiaml-1.0.0.dist-info/WHEEL +5 -0
  278. pyiaml-1.0.0.dist-info/licenses/LICENSE +674 -0
  279. pyiaml-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,293 @@
1
+ """[WRAPPER] Genetic Grid Search implementation
2
+ This wrapper is inspired by genetic algorithms.
3
+ It'll randomly create and mutate generations Step configurations to find the best parameters
4
+ Each new generation will learn from the previous one
5
+ """
6
+ from copy import deepcopy
7
+ from math import isfinite
8
+ import random
9
+ from typing import Callable
10
+ from ..step_wrapper import StepWrapper
11
+ from ..step import Step
12
+ from ..decorators.all import is_step, runner
13
+ from ..candidate import Candidate
14
+ from ..meta_explorer_step import MetaExplorerStep
15
+ from ..logger import Logger
16
+
17
+
18
+ @is_step('wrapper')
19
+ class WrapGeneticGridSearch(StepWrapper):
20
+ """[WRAPPER] Genetic Grid Search implementation
21
+ This wrapper is inspired by genetic algorithms.
22
+ It'll randomly create and mutate generations Step configurations
23
+ to find the best parameters
24
+ Each new generation will learn from the previous one
25
+ """
26
+ name = "Wrap : Genetic GridSearch"
27
+ _usage: str = "Use when you need adaptive, multi-generation search over larger spaces and want broader exploration than WrapBasicGridSearch or WrapIterativeGridSearch. Applicable to numeric, categorical, and boolean hyperparameters. Avoid when budget is tight or a fixed grid suffices."
28
+
29
+ def __init__(self, step: Step, *, evaluator: Callable[[Candidate], dict] = None):
30
+ # Set of configuration key to ignore.
31
+ # For example, random_state is not a parameter to optimize
32
+ self.ignored_configs: set[str] = {'random_state'}
33
+ # An evaluator can capture a dataset or splitter that changes between runs.
34
+ # Recompute the search rather than cache results by input identity alone.
35
+ self.use_cache = False
36
+ self.evaluator = evaluator
37
+ """Optional callback returning metric scores for each generated candidate.
38
+
39
+ Use Candidate.training_evaluate with the original dataset and the desired
40
+ splitter. Without a callback, multiple generations require scores already
41
+ supplied by the wrapped step.
42
+ """
43
+
44
+ self.configuration = {
45
+ 'initial_modificator': {
46
+ 'description': 'Maximum multiplier of default value to generate \
47
+ the first generation of steps',
48
+ 'default': 5
49
+ },
50
+ 'nb_generations': {
51
+ 'description': 'Number of generations to create, train and test',
52
+ 'default': 5,
53
+ 'range': [1, float('inf')]
54
+ },
55
+ 'nb_estimators': {
56
+ 'description': 'Number of Steps by generations',
57
+ 'default': 15,
58
+ 'range': [1, float('inf')]
59
+ },
60
+ 'mutation_power': {
61
+ 'description': 'Maximum multiplier of current value when mutating',
62
+ 'default': 0.1,
63
+ 'range': [0.001, 1]
64
+ }
65
+ }
66
+ """Dictionnary of genetic configuration"""
67
+
68
+ # pylint: disable=too-many-locals
69
+ @runner
70
+ def run(self, candidate: Candidate) -> list[Candidate]:
71
+ """Will iterate over generation to find best parameters
72
+
73
+ Genetic GridSearch
74
+ First generation
75
+ Generate nb_estimator Step randomly
76
+ Run and get result
77
+ Next generations
78
+ Keep 1/4 BEST
79
+ Mutation 2/4 BEST to New steps
80
+ Generate totally new steps
81
+
82
+ :param Candidate candidate: Candidate data
83
+ :return: All generated Candidate
84
+ """
85
+ # If no configuration, let's run the step once. Nothing to optimize here
86
+ if not self.__config_keys():
87
+ return self.step.run(candidate)
88
+
89
+ population_size = self.get_config('nb_estimators')
90
+ generations = self.get_config('nb_generations')
91
+ for name, value in [('nb_estimators', population_size), ('nb_generations', generations)]:
92
+ if type(value) is not int or value < 1:
93
+ raise ValueError(f'{name} must be a positive integer')
94
+
95
+ generation = [self.random_generation() for _ in range(population_size)]
96
+ candidates = []
97
+ for i_gen in range(generations):
98
+ Logger().info(f"created new generation: [b]{self.step.__class__.__name__}[/] \
99
+ (generation={i_gen})")
100
+
101
+ meta = MetaExplorerStep() # Use MetaExplorer to run all our generation easily
102
+ meta.add_steps(generation) # Give all steps to MetaExplorer
103
+ for step in generation:
104
+ step.candidate = []
105
+ meta.run(candidate)
106
+
107
+ # The runner keeps each step's outputs. No legacy Stack history is needed.
108
+ ranked = []
109
+ for step in generation:
110
+ for result in step.candidate or []:
111
+ result.main_metric = candidate.get_main_metric()
112
+ if self.evaluator is not None:
113
+ result.computed_metrics = self.evaluator(result) or {}
114
+ score = result.computed_metrics.get(result.main_metric)
115
+ if score is None or not isfinite(score):
116
+ continue
117
+ elif i_gen + 1 < generations and (
118
+ result.main_metric not in result.computed_metrics
119
+ or not isfinite(result.get_main_metric_value())
120
+ ):
121
+ raise ValueError(
122
+ 'WrapGeneticGridSearch needs an evaluator returning the main '
123
+ 'metric to select subsequent generations. Pass evaluator=... '
124
+ 'using Candidate.training_evaluate on the original dataset, '
125
+ 'or use GeneticOptimizer in IAML.'
126
+ )
127
+ ranked.append((result, step))
128
+
129
+ candidates = [result for result, _ in ranked]
130
+ if not candidates or i_gen + 1 == generations:
131
+ break
132
+
133
+ ranked.sort(key=lambda item: item[0], reverse=True)
134
+ nb_to_keep = max(1, population_size // 4)
135
+ steps_to_keep = []
136
+ seen = set()
137
+ for _, step in ranked:
138
+ fingerprint = step.fingerprint()
139
+ if fingerprint in seen:
140
+ continue
141
+ seen.add(fingerprint)
142
+ kept = deepcopy(step)
143
+ kept.parents_steps.remove(id(meta))
144
+ steps_to_keep.append(kept)
145
+ if len(steps_to_keep) == nb_to_keep:
146
+ break
147
+
148
+ new_generation = list(steps_to_keep)
149
+ for _ in range(min(2 * nb_to_keep, population_size - len(new_generation))):
150
+ new_generation.append(self.random_mutation(random.choice(steps_to_keep)))
151
+ while len(new_generation) < population_size:
152
+ new_generation.append(self.random_generation())
153
+
154
+ # Compare the complete effective configuration, including every key.
155
+ generation = []
156
+ seen = set()
157
+ for step in new_generation:
158
+ fingerprint = step.fingerprint()
159
+ if fingerprint not in seen:
160
+ seen.add(fingerprint)
161
+ generation.append(step)
162
+
163
+ return candidates
164
+
165
+ # Return Step with random configuration
166
+ def random_generation(self) -> Step:
167
+ """Randomly generate a new Step
168
+
169
+ :return: Generated step with random configuration
170
+ """
171
+ # Deepcopy to avoid editing other Steps of the same generation
172
+ new_step: Step = deepcopy(self.step)
173
+
174
+ for key in self.__config_keys(): # For each configuration key, we'll choose a random value
175
+ config = new_step.configuration[key]
176
+
177
+ if 'categorical' in config:
178
+ new_value = random.choice(config['categorical'])
179
+ elif type(config['value']) in [int, float]: # Numeric value ? Let's apply multiplier
180
+ is_int = isinstance(config['value'], int)
181
+
182
+ new_value = None
183
+ # Randomly choose a positive or negative editing
184
+ if bool(random.getrandbits(1)):
185
+ # Negative -> Multiply value by something between 0.01 and 1
186
+ change_rate = random.uniform(0.01, 1)
187
+ new_value = config['value']*change_rate
188
+ else:
189
+ # Positive -> Multiply vaoue by something between 1
190
+ # and the max modificator in configuration
191
+ change_rate = random.uniform(1, self.get_config('initial_modificator'))
192
+ new_value = config['value']*change_rate
193
+
194
+ # Value was a int ? Round it to keep it int
195
+ if is_int:
196
+ new_value = round(new_value)
197
+
198
+ if not self.__valide_config(config, new_value):
199
+ # Cancel is the new value is not correct.
200
+ new_value = config['value']
201
+
202
+ elif isinstance(config['value'], bool):
203
+ # Bool value, choose randomly beetwen True and False
204
+ new_value = random.choice([True, False])
205
+ else: # Other value ? Just keep it
206
+ new_value = config['value']
207
+
208
+ new_step.configure(key, new_value) # Set new configuration in the step
209
+
210
+ return new_step
211
+
212
+ # Randomly mutate Step
213
+ def random_mutation(self, step: Step) -> Step:
214
+ """Randomly mutate some parameters of the step
215
+
216
+ :param Step step: Step to mutate.
217
+ :return: Mutated Step
218
+ """
219
+ new_step: Step = deepcopy(step) # Deepcopy to avoid editing another Step
220
+
221
+ keys = self.__config_keys()
222
+ if not keys:
223
+ return new_step
224
+ random_key = random.choice(keys) # Choose a random key to mutate
225
+ random_item = new_step.configuration[random_key] # Get value of the random key
226
+ new_value = None
227
+
228
+ if 'categorical' in random_item:
229
+ new_value = random.choice(random_item['categorical'])
230
+ elif type(random_item['value']) in [int, float]: # Numeric value ? Apply multiplier
231
+ is_int = isinstance(random_item['value'], int)
232
+
233
+ # Find a multiplier between - mutation_power & + mutation_power
234
+ change_rate = random.uniform(-self.get_config('mutation_power'), \
235
+ self.get_config('mutation_power'))
236
+ new_value = random_item['value']*(1+change_rate) # Apply random multiplier
237
+
238
+ # Value was a int ? Round it to keep it int
239
+ if is_int:
240
+ new_value = round(new_value)
241
+
242
+ if new_value == random_item['value']: # To be sure there is a mutation
243
+ new_value += random.choice([-1, 1])
244
+
245
+ if not self.__valide_config(random_item, new_value):
246
+ # Cancel is the new value is not correct.
247
+ new_value = random_item['value']
248
+
249
+ elif isinstance(random_item['value'], bool): # Bool -> Choose between True and False
250
+ new_value = random.choice([True, False])
251
+ else: # Other -> Keep it
252
+ new_value = random_item['value']
253
+
254
+ new_step.configure(random_key, new_value) # Apply configuration
255
+ return new_step
256
+
257
+ def conf_to_rich_str_list(self) -> list[str]:
258
+ """List of String to Rich logger
259
+
260
+ :return: list of stirng
261
+ """
262
+ l = [f'step={self.step.__class__.__name__}']
263
+ l.extend(super().conf_to_rich_str_list())
264
+
265
+ return l
266
+
267
+ def count_steps(self) -> int:
268
+ """Estimation of remaining step count
269
+
270
+ :return: Number of steps
271
+ """
272
+ estimators = self.get_config('nb_estimators')
273
+ generations = self.get_config('nb_generations')
274
+
275
+ return self.step.count_steps() * estimators * generations
276
+
277
+ def __valide_config(self, config: dict, value: float) -> bool:
278
+ """Is the configuration valid or not.
279
+
280
+ :param dict config: The configuration to check.
281
+ :param float value: The value to check in range.
282
+ :return: Valid ?
283
+ """
284
+ if 'range' not in config.keys():
285
+ return True
286
+ lower, upper = config['range']
287
+ return (lower is None or lower <= value) and (upper is None or value <= upper)
288
+
289
+ # Get configurable keys (without ignored keys)
290
+ def __config_keys(self):
291
+ """Get configurable keys (without ignored keys)."""
292
+ return [key for key, config in self.step.configuration.items()
293
+ if key not in self.ignored_configs and not config.get('no_gridsearch', False)]
@@ -0,0 +1,399 @@
1
+ """[WRAPPER] Wrap a step to apply an Iterative Grid Search implementation"""
2
+ from copy import deepcopy
3
+ from math import isfinite
4
+ from typing import Any, Callable
5
+ from ..step_wrapper import StepWrapper
6
+ from ..candidate import Candidate
7
+ from ..step import Step
8
+ from ..decorators.all import is_step, runner
9
+
10
+
11
+ # Wrapper : Implementation of an interative GridSearch
12
+ @is_step('wrapper')
13
+ class WrapIterativeGridSearch(StepWrapper):
14
+ """[WRAPPER] Wrap a step to apply an Iterative Grid Search implementation
15
+
16
+ :param Step step: Step to apply grid search on.
17
+ :param evaluator: Callback returning metric scores, for example using
18
+ Candidate.training_evaluate with the original dataset and desired splitter.
19
+ Required unless the wrapped step supplies evaluation scores itself.
20
+ """
21
+
22
+ name = "Wrap : Iterative GridSearch"
23
+ _usage = "Use when you need iterative narrowing of numeric grids, faster than WrapBasicGridSearch. Applicable to numeric-heavy configs with ordered ranges and small categorical/bool sets. Avoid when spaces are huge or unstructured; consider WrapGeneticGridSearch."
24
+ def __init__(self, step: Step, *, evaluator: Callable[[Candidate], dict] = None) -> None:
25
+ self.evaluator = evaluator
26
+ # Evaluation callbacks may capture data that changes between runs.
27
+ self.use_cache = False
28
+ self.configuration = {
29
+ 'modificator': {
30
+ 'description': 'Value modificator for each iteration',
31
+ 'default': 0.5
32
+ },
33
+ 'max_iterations': {
34
+ 'description': 'Maximum number of iterations',
35
+ 'default': 10
36
+ },
37
+ 'patience': {
38
+ 'description': 'Stop iterations after N tries without improvements',
39
+ 'default': 3
40
+ }
41
+ }
42
+ """Iterative grid search configuration"""
43
+
44
+ self.step: Step = step
45
+ """Step to perform grid search on"""
46
+
47
+ to_avoid = ['random_state']
48
+
49
+ @runner
50
+ def run(self, candidate: Candidate) -> list[Candidate]:
51
+ """Iterative GridSearch.
52
+
53
+ Search numeric parameters as follows::
54
+
55
+ Numeric values
56
+ -> First run -> 100% of the value
57
+ -> Next runs -> +10% and - 10% (100% * modificator value)
58
+ -> RUN
59
+ -> Result is better ? Keep best result and try again with modificator
60
+ -> Result is worst ? Keep previous result, update modificator
61
+ -> STOP Conditions ? -> Number of iterations OR no improvement since X interations
62
+ -> Remember the range and then do a dichotomous to find the best parameters
63
+
64
+ Use the basic grid search for other parameter types::
65
+
66
+ Categorical values -> 1 run each
67
+ Boolean values -> Run with True and False
68
+ Other -> keep current value
69
+
70
+ :param Candidate candidate: The candidate on which we run the grid search.
71
+ :return: All generated Candidates.
72
+ """
73
+ step = deepcopy(self.step)
74
+ config = deepcopy({key: item for key, item in step.configuration.items()
75
+ if key not in self.to_avoid and not item.get('no_gridsearch', False)})
76
+ pending = [GridIteration(
77
+ step, self.get_config('modificator'), copy_config=config,
78
+ patience=self.get_config('patience'), max_iterations=self.get_config('max_iterations'),
79
+ evaluator=self.evaluator,
80
+ )]
81
+ results = []
82
+ while pending:
83
+ iteration = pending.pop(0)
84
+ current_results, siblings = iteration.run(candidate)
85
+ results.extend(current_results)
86
+ pending.extend(siblings)
87
+ return sorted(results, reverse=True)[:10]
88
+
89
+
90
+ class GridIteration: # pylint: disable=too-many-instance-attributes
91
+ """One iteration of Iterative grid search
92
+
93
+ :param Step step: The step we are working on.
94
+ :param float modificator_rate: The modification ratio
95
+ :param list, optional value_range: Range used to compute modificator. Default to None.
96
+ :param int, optional patience: Patience. Default to 5.
97
+ :param dict, optional copy_config: Config to copy. Default to None.
98
+ :param str, optional key: Key to perform gridsearch on. Default to None.
99
+ :param Any, optional value: Value of the key. Default to None.
100
+ :param int, optional max_iterations: Maximum number of iterations. Default to 10.
101
+ :param int, optional number_of_results: Maximum number of results. Default to 10.
102
+ :param float, optional minimal_range_diff: Minimal range diff. Default to None.
103
+ :param float, optional best_result: Best ranking score. Default to negative infinity.
104
+ """
105
+ # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,R0915,R0917
106
+ def __init__(
107
+ self,
108
+ step: Step,
109
+ modificator_rate: float,
110
+ value_range: list = None,
111
+ patience: int = 5,
112
+ copy_config: dict = None,
113
+ key: str = None,
114
+ value: Any = None,
115
+ max_iterations: int = 10,
116
+ number_of_results: int = 10,
117
+ minimal_range_diff: float = None,
118
+ best_result: float = float('-inf'),
119
+ evaluator: Callable[[Candidate], dict] = None) -> None:
120
+ self.step: Step = step
121
+ """The step we are working on"""
122
+ self.evaluator = evaluator
123
+
124
+ self.modificator_rate: float = modificator_rate
125
+ """Modificator ratio"""
126
+
127
+ self.patience: int = patience
128
+ """Patience"""
129
+
130
+ self.config: dict = deepcopy(step.configuration) if copy_config is None else copy_config
131
+ """Config used"""
132
+
133
+ if key:
134
+ self.key = key
135
+ elif any(self.config.keys()):
136
+ self.key = list(self.config.keys())[0]
137
+ else:
138
+ self.key = None
139
+
140
+ self.max_iterations: int = max_iterations
141
+ """Maximum number of iterations"""
142
+
143
+ self.count_iterations: int = 0
144
+ """Iteration counter"""
145
+
146
+ self.iterations_without_improvement: int = 0
147
+ """Iterations without improvement counter"""
148
+
149
+ self.children: list = []
150
+ """Childrens"""
151
+
152
+ self.ways: list = []
153
+ """List of ways to obtain possible values"""
154
+
155
+ self.values: list = []
156
+ """List of possible values"""
157
+
158
+ if self.key:
159
+ self.value = self.config[self.key]['value'] if value is None else value
160
+
161
+ self.modificator = None
162
+ self.minimal_range_diff = None
163
+ if minimal_range_diff is not None:
164
+ self.minimal_range_diff = minimal_range_diff
165
+
166
+ # Type
167
+ self.can_generate_sibling = False
168
+ if 'categorical' in self.config[self.key]:
169
+ self.values = list(self.config[self.key]['categorical'])
170
+ elif type(self.value) in [int, float]:
171
+ self.can_generate_sibling = True
172
+ if value_range:
173
+ self.modificator = abs(value_range[1]-value_range[0])/2 * modificator_rate
174
+ else:
175
+ self.modificator = abs(self.value) * self.modificator_rate
176
+
177
+ for way_ind, way in enumerate([1, -1]):
178
+ way_values = [self.value+(self.modificator*ind*way) \
179
+ for ind in range(way_ind, self.max_iterations)]
180
+ if isinstance(self.value, int):
181
+ way_values = [round(v) for v in way_values]
182
+
183
+ if ('range' in self.config[self.key]) or value_range:
184
+ limits = value_range or self.config[self.key]['range']
185
+ way_values = [v for v in way_values
186
+ if (limits[0] is None or limits[0] <= v)
187
+ and (limits[1] is None or v <= limits[1])]
188
+
189
+ self.ways.append(list(dict.fromkeys(way_values)))
190
+
191
+ self.__next_way()
192
+
193
+ if self.minimal_range_diff is None:
194
+ self.minimal_range_diff = self.modificator * 0.1 # TODO improve this
195
+
196
+ elif isinstance(self.value, bool): # Bool
197
+ self.values = [True, False]
198
+ else: # Other
199
+ self.values = [self.value]
200
+
201
+ self.candidates: list[Candidate] = []
202
+ """List of returned candidates"""
203
+
204
+ self.results: list = []
205
+ """List of results"""
206
+
207
+ self.number_of_results: int = number_of_results
208
+ """Number of results"""
209
+
210
+ self.best_result: float = best_result
211
+ """Best result"""
212
+
213
+ def done(self) -> bool:
214
+ """Iteration finished ?
215
+
216
+ :return: Finished ?
217
+ """
218
+ if not self.can_generate_sibling:
219
+ return self.count_iterations >= len(self.values)
220
+ return (self.iterations_without_improvement >= self.patience) \
221
+ or (self.count_iterations >= self.max_iterations) \
222
+ or (len(self.values)-1 < self.count_iterations)
223
+
224
+ def go_deeper(self) -> bool:
225
+ """Need to go deeper ? (Remains parameter to optimize)
226
+
227
+ :return: Deeper ?
228
+ """
229
+ return len(self.config.keys()) > 1
230
+
231
+ def __generate_child(self):
232
+ """Generate new childs"""
233
+ child_config = deepcopy(self.config)
234
+ del child_config[self.key]
235
+
236
+ self.children.append(self.__class__(
237
+ self.step,
238
+ self.modificator_rate,
239
+ patience=self.patience,
240
+ best_result = self.best_result,
241
+ copy_config=child_config,
242
+ max_iterations=self.max_iterations,
243
+ evaluator=self.evaluator))
244
+
245
+ def __get_best_range(self) -> tuple:
246
+ """Get the best range using previous results.
247
+ The best range will be the higher results + his highest neighbour.
248
+
249
+ :return: best range.
250
+ """
251
+ if len(self.results) < 2:
252
+ return None, None
253
+
254
+ ordered = sorted(self.results, key=lambda result: result['value'])
255
+ max_index = max(range(len(ordered)), key=lambda index: ordered[index]['result'])
256
+ around = ordered[max(0, max_index-1):(max_index+2)]
257
+ around = sorted(around, key=lambda x: x['result'])
258
+ return tuple(sorted((around[-2]['value'], around[-1]['value'])))
259
+
260
+ def __generate_siblings(self) -> list:
261
+ """Generate siblings
262
+ Siblings will be next iterator at the same level (same key, same step).
263
+ They only explore the best range.
264
+
265
+ :return: list of siblings
266
+ """
267
+ if not self.can_generate_sibling:
268
+ return []
269
+
270
+ mini, maxi = self.__get_best_range()
271
+
272
+ if maxi is None or mini is None:
273
+ return []
274
+
275
+ current_range = [mini, maxi]
276
+ middle = mini+(maxi-mini)/2
277
+
278
+ if self.minimal_range_diff >= (maxi-mini):
279
+ return []
280
+
281
+ if isinstance(maxi, int):
282
+ if (maxi-mini) <= 1:
283
+ return []
284
+
285
+ middle = round(middle)
286
+
287
+ next_iter = self.__class__(
288
+ self.step,
289
+ self.modificator_rate,
290
+ value=middle,
291
+ value_range=current_range,
292
+ patience=self.patience,
293
+ copy_config=self.config,
294
+ best_result = self.best_result,
295
+ key=self.key,
296
+ max_iterations=self.max_iterations,
297
+ minimal_range_diff=self.minimal_range_diff,
298
+ evaluator=self.evaluator)
299
+
300
+ return [next_iter]
301
+
302
+ def __next_way(self) -> None:
303
+ """Go to the next direction.
304
+ Iterator values will go up and then go down.
305
+ """
306
+ while self.ways:
307
+ self.values = self.ways.pop(0)
308
+ self.count_iterations = 0
309
+ self.iterations_without_improvement = 0
310
+ if self.values:
311
+ break
312
+
313
+ def next_iteration(self) -> None:
314
+ """
315
+ Go to the next iteration or the next way if current one is finished
316
+ """
317
+ self.count_iterations = self.count_iterations + 1
318
+ if self.done():
319
+ self.__next_way()
320
+
321
+ def current_value(self) -> Any:
322
+ """Get current value
323
+
324
+ :return: Current value
325
+ """
326
+ return self.values[self.count_iterations]
327
+
328
+ def __stack_results(self, results: list[Candidate]) -> None:
329
+ """Stack, compute and save results"""
330
+ if not any(results):
331
+ return None
332
+
333
+ best = max(results, key=lambda result: result.get_main_metric_score())
334
+ best_val = best.get_main_metric_score()
335
+
336
+ self.results.append({'value': self.current_value(), 'result': best_val})
337
+
338
+ if best_val <= self.best_result:
339
+ self.iterations_without_improvement = self.iterations_without_improvement + 1
340
+ else:
341
+ self.best_result = best_val
342
+ self.iterations_without_improvement = 0
343
+
344
+ self.candidates.append(best)
345
+
346
+ # Keep only n best
347
+ self.candidates.sort(reverse=True)
348
+ self.candidates = self.candidates[:self.number_of_results]
349
+
350
+ return None
351
+
352
+ def run(
353
+ self,
354
+ candidate: Candidate) -> tuple[list[Candidate], list['GridIteration']]:
355
+ """Run and Stack results
356
+
357
+ :param Candidate candidate: The candidate we run.
358
+ :return: tuple of lists of candidates and GridIteration
359
+ """
360
+ # Run and Stack results
361
+ if not self.key:
362
+ return self.step.run(candidate.to_input()), []
363
+
364
+ while not self.done():
365
+ results = []
366
+ self.step.configure(self.key, self.current_value())
367
+
368
+ if self.go_deeper():
369
+ self.__generate_child()
370
+ results = []
371
+ while self.children:
372
+ child = self.children.pop(0)
373
+ current_results, siblings = child.run(candidate)
374
+ results = results + current_results
375
+
376
+ if siblings:
377
+ self.children = self.children + siblings
378
+ else:
379
+ for result in deepcopy(self.step).run(candidate.to_input()):
380
+ result.main_metric = candidate.get_main_metric()
381
+ if self.evaluator is not None:
382
+ result.computed_metrics = self.evaluator(result) or {}
383
+ score = result.computed_metrics.get(result.main_metric)
384
+ if score is None or not isfinite(score):
385
+ if self.evaluator is None:
386
+ raise ValueError(
387
+ 'WrapIterativeGridSearch needs an evaluator returning the main '
388
+ 'metric. Pass evaluator=... using Candidate.training_evaluate '
389
+ 'on the original dataset.'
390
+ )
391
+ continue
392
+ results.append(result)
393
+
394
+ self.__stack_results(results)
395
+ self.next_iteration()
396
+
397
+ siblings = self.__generate_siblings()
398
+
399
+ return self.candidates, siblings