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,63 @@
1
+ """[STATISTIC] Violin."""
2
+ from __future__ import annotations
3
+
4
+ import textwrap
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy import stats
8
+
9
+ from ..dataset import Dataset
10
+ from ..data_type import DataType
11
+ from ..statistic import Statistic
12
+
13
+
14
+ class ViolinStatistic(Statistic):
15
+ """[STATISTIC] Violin."""
16
+
17
+ name: str = "Violin"
18
+ _description: str = textwrap.dedent("""\
19
+ Violin statistics summarize target distributions per categorical value.
20
+ """)
21
+ _description_long: str = textwrap.dedent("""\
22
+ Violin statistics summarize target distributions per categorical value
23
+ to support violin-style plots.
24
+ """)
25
+ refs: list[dict] = []
26
+
27
+ def __str__(self) -> str:
28
+ return 'violin'
29
+
30
+ def compute(self, dataset: Dataset, **kwargs) -> pd.DataFrame:
31
+ """Compute violin statistics for categorical features."""
32
+ if dataset.type_of_target != 'continuous':
33
+ return pd.DataFrame()
34
+
35
+ data = []
36
+ columns = []
37
+ for col in dataset.X.columns:
38
+ if dataset.columns_types[col][1] not in (DataType.TEXT, DataType.SHORT_TEXT):
39
+ columns.append(col)
40
+ if dataset.columns_types[col][1] == DataType.CATEGORICAL:
41
+ categorical_values = dataset.X[col].unique()
42
+ stats_dict = {}
43
+ for cat in categorical_values:
44
+ cond = (dataset.X[col] == cat).to_numpy()
45
+ tmp = dataset.y[cond]
46
+ if len(tmp) > 1:
47
+ kernel = stats.gaussian_kde(tmp)
48
+ support = np.linspace(min(tmp), max(tmp), 100)
49
+ density = kernel(support)
50
+ quartiles = np.percentile(tmp, [25, 50, 75])
51
+ stats_dict[cat] = {
52
+ 'density': density,
53
+ 'support': support,
54
+ 'quartiles': quartiles
55
+ }
56
+ data.append(stats_dict)
57
+ else:
58
+ data.append(None)
59
+ return pd.DataFrame([data], index=[str(self)], columns=columns)
60
+
61
+ def suitable(self, dataset: Dataset) -> bool: # pylint: disable=unused-argument
62
+ """Does this statistic apply to the dataset?"""
63
+ return dataset.type_of_target == 'continuous'
iaml/step.py ADDED
@@ -0,0 +1,600 @@
1
+ """
2
+ Step class is a brick used to create pipelines.
3
+ This class is not really use in Pipeline, run function doesn't do anything.
4
+ Step class is use to create new kinds of steps by inheritance and give all
5
+ needed attributes and methods to children classes.
6
+
7
+ There are also decorators needed to create a Step. See it under Step class.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ import json
14
+ import textwrap
15
+ import uuid
16
+ from hashlib import md5
17
+ from typing import TYPE_CHECKING
18
+ from typing import Any
19
+ from copy import deepcopy
20
+ from multipledispatch import dispatch
21
+ from .dataset import Dataset
22
+ from .decorators.runner import runner
23
+ from .reference import Reference
24
+ from .step_cache import StepCache
25
+
26
+ if TYPE_CHECKING:
27
+ from .candidate import Candidate
28
+ from ..iaml.reference import Reference
29
+
30
+
31
+ class Step: # pylint: disable=too-many-public-methods, too-many-instance-attributes
32
+ """This class is the base for all IAML steps.
33
+
34
+ :param bool, optional use_cache: Whether to enable caching for this step.
35
+ """
36
+ available_steps: dict[Step, tuple[str]] = {}
37
+ """Static list of all registered steps within IAML."""
38
+
39
+ name: str = 'Step'
40
+ """Step's name."""
41
+
42
+ refs: list[dict[str, Any]] = None
43
+ """List of references for this step."""
44
+
45
+ _description: str = ''
46
+ """Short description of the step."""
47
+
48
+ _description_long: str = ''
49
+ """Longer description of the step."""
50
+
51
+ _usage: str = ''
52
+ """Concise guidance on when to use (and avoid) this step."""
53
+
54
+ can_be_disabled: bool = True
55
+ """Whether this step can be disabled."""
56
+
57
+ def __init__(self, *args, use_cache: bool = True, **kwargs): # pylint: disable=unused-argument
58
+ self.tags: set = None
59
+ """Step's tags. Tags are equivalent to categories of steps."""
60
+
61
+ self.__use_cache: bool = use_cache
62
+ """Whether this step should benefit from the cache."""
63
+
64
+ self._cache_id: str = uuid.uuid4().hex
65
+ """Shared cache namespace id for this step across deep copies."""
66
+
67
+ self._config_version: int = 0
68
+ """Incremented when configuration changes to invalidate fingerprints."""
69
+
70
+ self.explanations: list[str] = []
71
+ """List of explanations that were computed during the step's execution."""
72
+
73
+ self.is_interchangeable: bool = False
74
+ """Whether this step can be mutated into another step with the same tags."""
75
+
76
+ self.enable: bool = True
77
+ """Whether this step is enabled. If it is disabled, the step will simply return the input
78
+ candidate."""
79
+
80
+ self.optimizable: bool = False
81
+ """Whether the configuration's parameters should be considered optimizable."""
82
+
83
+ self.configuration: dict = {}
84
+ """Configuration of this step (equivalent to hyperparameters for models)."""
85
+
86
+ self.parents_steps: list[Step] = []
87
+ """If this step is a child of another one, lists all the parents of this step."""
88
+
89
+ self.references: list[Reference] = []
90
+ """References for this step."""
91
+
92
+ # Build a list of References from Step's references list
93
+ if self.refs is not None:
94
+ self.references = [ Reference(ref, type(self).__name__) for ref in self.refs ]
95
+
96
+ self.default_configuration() # Loads the default configuration
97
+
98
+ def __deepcopy__(self, memo: dict) -> 'Step':
99
+ """Custom deepcopy to avoid copying per-step runtime caches."""
100
+ cls = self.__class__
101
+ result = cls.__new__(cls)
102
+ memo[id(self)] = result
103
+ for key, value in self.__dict__.items():
104
+ if key == 'candidate':
105
+ setattr(result, key, None)
106
+ continue
107
+ if key == 'caches':
108
+ setattr(result, key, [])
109
+ continue
110
+ setattr(result, key, deepcopy(value, memo))
111
+ return result
112
+
113
+ @classmethod
114
+ def from_pipeline(cls, pipeline: dict[str, Any], *args, **kwargs) -> 'Step':
115
+ """Loads any kind of Step (Step, MetaStep, Wrapper, etc.) from an imported pipeline.
116
+
117
+ :param dict pipeline: Pipeline to import.
118
+ :param optional \\*args: Args to pass to the Step's constructor.
119
+ :param optional \\**kwargs: Kwargs to pass to the Step's constructor.
120
+ :raise TypeError: invalid pipeline: missing step attribute
121
+ :raise TypeError: invalid pipeline: step does not exist
122
+ :return: A step
123
+ """
124
+ step = None
125
+ if 'step' not in pipeline:
126
+ raise TypeError("invalid pipeline: missing step attribute")
127
+
128
+ step_class = getattr(sys.modules['iaml'], pipeline['step']) # Get class from string
129
+ if Step in step_class.__mro__:
130
+ if step_class == cls:
131
+ step = cls(*args, **kwargs)
132
+
133
+ if 'enable' in pipeline:
134
+ step.enable = pipeline['enable']
135
+
136
+ if 'configuration' in pipeline:
137
+ for name, value in pipeline['configuration'].items():
138
+ step.configure(name, value['value']) # pylint: disable=too-many-function-args
139
+ else:
140
+ step = step_class.from_pipeline(pipeline)
141
+ else:
142
+ raise TypeError('invalid pipeline: step does not exist')
143
+
144
+ return step
145
+
146
+ @property
147
+ def enable(self) -> bool:
148
+ """Tells whether the step is enabled.
149
+
150
+ :return: Whether the step is enabled.
151
+ """
152
+ return self.__enable
153
+
154
+ @enable.setter
155
+ def enable(self, value: bool) -> None:
156
+ """Sets the state of the step.
157
+
158
+ :param value: State of the step (True if enabled, False if disabled).
159
+ """
160
+ # If can_be_disabled is False, value will always be True
161
+ self.__enable = value or not self.can_be_disabled
162
+
163
+ def __str__(self) -> str:
164
+ """Returns the name of this step.
165
+
166
+ :return: Name of this step.
167
+ """
168
+ return self.name
169
+
170
+ def suitable(self, dataset: Dataset) -> bool: # pylint: disable=unused-argument
171
+ """Evaluates whether the step is suitable to run with the given dataset.
172
+
173
+ :param Dataset dataset: Dataset to evaluate on.
174
+ :return: Whether the step is suitable to run.
175
+ """
176
+ return True
177
+
178
+ def configure_child(self, child: 'Step') -> 'Step':
179
+ """When working with steps that can have children, setups the child with the parent.
180
+
181
+ :param Step child: Step to configure with the parent.
182
+ :return: Configured step.
183
+ """
184
+ child.configure_parents(self)
185
+
186
+ return child
187
+
188
+ def configure_parents(self, *parents: list['Step']) -> None:
189
+ """Backpropagates the parents to the children.
190
+
191
+ :param list[Step] parents: Parent steps to add to the children.
192
+ """
193
+ self.parents_steps.extend(map(id, parents))
194
+
195
+ def step_with_same_tags(self) -> list['Step']:
196
+ """Returns the steps that have the same tags as this step.
197
+
198
+ :return: List of steps.
199
+ """
200
+ return [ key for key, tags in Step.available_steps.items() if self.tags == set(tags) ]
201
+
202
+ ################
203
+ # Configurable #
204
+ ################
205
+ #
206
+ # A actionable step can be configured by the user.
207
+ # For example, we can configure learning rate of a machine learning step.
208
+ #
209
+ # Each parameters have a name, a description and a default value.
210
+ # Default value can be fixed or computed based on dataset
211
+
212
+ @dispatch(str, object)
213
+ def configure(self, key: str, value: Any) -> None:
214
+ """Configures a parameter.
215
+
216
+ :param str key: Name (or key) of the parameter to set.
217
+ :param Any value: New value for the parameter.
218
+ :raise AttributeError: When the key is invalid.
219
+ """
220
+ if key in self.configuration:
221
+ self.configuration[key]['value'] = value
222
+ self._config_version += 1
223
+ else:
224
+ raise AttributeError(f"Configurable Key '{key}' does not exist.")
225
+
226
+ @dispatch(dict)
227
+ def configure(self, config: dict[str, Any]) -> None: # pylint: disable=function-redefined
228
+ """Configures several parameters at once.
229
+
230
+ :param dict[str, Any] config: Dictionary of parameters' names and values.
231
+ """
232
+ for key, value in config.items():
233
+ self.configure(key, value) # pylint: disable=too-many-function-args
234
+
235
+ def passthrough_parameters(self, default: bool = True) -> dict[str, Any]:
236
+ """Constructs a dictionary of parameters' names and values.
237
+
238
+ :param bool default: Default behavior when trying to passthrough configurations from
239
+ one step which have no "passthrough" key. When "passthrough" is undefined and "default"
240
+ is set to False, the configuration will not be returned; otherwise, the default value
241
+ for that configuration will be returned.
242
+ :return: A dictionary view of the configuration with parameters' names and values.
243
+ """
244
+ parameters = {}
245
+ for key, value in self.configuration.items():
246
+ if "passthrough" in value:
247
+ if value['passthrough']:
248
+ parameters[key] = value['value']
249
+ elif default:
250
+ parameters[key] = value['value']
251
+
252
+ return parameters
253
+
254
+ def all_configurations(self) -> list[dict[str, Any]]:
255
+ """Retrieves the signature (id and configuration) of the step. This should be overridden in
256
+ steps which have children to also return the children's configuration.
257
+
258
+ :return: List of all configurations as objects.
259
+ """
260
+ return [{
261
+ 'step_id': id(self),
262
+ 'configuration': self.configuration
263
+ }]
264
+
265
+ def resume_configuration(self) -> dict:
266
+ """Summarizes the configuration of this step as keys and values.
267
+
268
+ :return: Configuration summary.
269
+ """
270
+ return Step.__resume_a_configuration(self.configuration)
271
+
272
+ def serializable_resume_configuration(self) -> dict:
273
+ """Generates a serializable configuration of this step.
274
+
275
+ :return: Serializable dictionary.
276
+ """
277
+ return {key: value.__name__ if callable(value) else value \
278
+ for key, value in self.resume_configuration().items()}
279
+
280
+ @classmethod
281
+ def __resume_a_configuration(cls, config: dict[str, Any]) -> dict:
282
+ """Summarizes a configuration, and uses the default value for each configuration which have
283
+ no value yet.
284
+
285
+ :param dict[str, Any] config: Step's configuration.
286
+ :return: The configuration to make a "summary" for.
287
+ """
288
+ return { k: Step.__get_a_value(v) for k, v in config.items() }
289
+
290
+ @classmethod
291
+ def __get_a_value(cls, elem: dict[str, Any]) -> Any:
292
+ """Retrieves a value from the configuration, and uses the default value for that
293
+ configuration if it has no value.
294
+
295
+ :param dict[str, Any] elem: Step's configuration item.
296
+ :return: Value or default value.
297
+ """
298
+ return elem['value'] if 'value' in elem.keys() else elem['default']
299
+
300
+ def get_config(self, key: str) -> Any:
301
+ """Retrieves a value from the configuration, and uses the default value for that
302
+ configuration if it has no value.
303
+
304
+ :param str key: Step's configuration item.
305
+ :return: Value or default value.
306
+ """
307
+ param: dict = self.configuration[key]
308
+
309
+ return param['value'] if 'value' in param else param['default']
310
+
311
+ def default_configuration(self) -> None:
312
+ """Gives the default value to all configuration items."""
313
+ for param in self.configuration.values():
314
+ param['value'] = param['default']
315
+
316
+ def check_configuration(self, fix: bool = True) -> bool:
317
+ """Checks whether the configuration's format is valid.
318
+
319
+ :param bool fix: Whether to try to fix the configuration if something is wrong.
320
+ :return: Whether the configuration is wrong. Fixed issues return True.
321
+ """
322
+ for key, config in self.configuration.items():
323
+ if 'categorical' in config:
324
+ if self.get_config(key) not in config['categorical']:
325
+ if fix:
326
+ self.configure(key, config['categorical'][0]) # pylint: disable=too-many-function-args
327
+ else:
328
+ return False
329
+ elif 'range' in config:
330
+ if self.get_config(key) < config['range'][0] \
331
+ or self.get_config(key) > config['range'][1]:
332
+ if fix:
333
+ self.configure(key, (config['range'][0] + config['range'][1]) / 2) # pylint: disable=too-many-function-args
334
+ else:
335
+ return False
336
+ return True
337
+
338
+ def all_steps(self) -> list[Step]:
339
+ """Retrieves all steps of a pipeline. This should be overridden in steps which may run
340
+ other steps, such as children steps.
341
+
342
+ :return: List of steps.
343
+ """
344
+ return [self]
345
+ #####################
346
+ ## CACHING RESULTS ##
347
+ #####################
348
+ # Results of run() can be stored in cache to avoid recomputing the same candidate.
349
+
350
+ def from_cache(self, candidate: Candidate) -> Candidate:
351
+ """Checks whether this step was already run with the same input candidate and the same
352
+ configuration, and return the cached result if that is the case. Otherwise, return None.
353
+
354
+ :param Candidate candidate: Candidate to look for in cache.
355
+ :return: Cached candidate or None.
356
+ """
357
+ if not self.use_cache:
358
+ return None
359
+
360
+ cache_key = self._cache_key(candidate)
361
+ cached = StepCache().get(cache_key, candidate)
362
+ if cached is None:
363
+ return None
364
+
365
+ return self._clone_output(cached)
366
+
367
+ def add_cache(self, input_candidate: Candidate, output_candidate: Candidate) -> bool:
368
+ """Adds a candidate in the cache of this step.
369
+
370
+ :param Candidate input_candidate: Input candidate.
371
+ :param Candidate output_candidate: Output candidate to cache.
372
+ :return: Whether the candidate was cached.
373
+ """
374
+ if not self.use_cache:
375
+ return False
376
+
377
+ cache_key = self._cache_key(input_candidate)
378
+ frozen_output = self._clone_output(output_candidate)
379
+ StepCache().put(cache_key, frozen_output, self._cache_id, input_candidate)
380
+
381
+ return True
382
+
383
+ def reset_cache(self) -> None:
384
+ """Removes all cached candidates from the cache."""
385
+ StepCache().clear(self._cache_id)
386
+
387
+ @property
388
+ def caches(self) -> list:
389
+ """Deprecated view of cached results backed by StepCache."""
390
+ return StepCache().values_for_step(self._cache_id)
391
+
392
+ @caches.setter
393
+ def caches(self, value: list | None) -> None:
394
+ if not value:
395
+ StepCache().clear(self._cache_id)
396
+
397
+ def _cache_key(self, candidate: Candidate) -> tuple:
398
+ """Index by address; StepCache also verifies the input through a weak reference."""
399
+ candidate_id = id(candidate) if candidate is not None else None
400
+ return (self._cache_id, self.fingerprint(), candidate_id)
401
+
402
+ def _clone_output(self, output: Any) -> Any:
403
+ """Return cached output without cloning to preserve identity semantics."""
404
+ return output
405
+
406
+ @property
407
+ def use_cache(self) -> bool:
408
+ """Tells whether the step is using the cache.
409
+
410
+ :return: True if caching is enabled, False otherwise.
411
+ """
412
+ return self.__use_cache
413
+
414
+ @use_cache.setter
415
+ def use_cache(self, value: bool) -> bool:
416
+ """Enables or disables caching.
417
+
418
+ :param bool value: If True, enables caching; if False, disables caching.
419
+ :raise ValueError: When `value` is not a boolean.
420
+ """
421
+ if not isinstance(value, bool):
422
+ raise ValueError('Value must be a boolean')
423
+ self.__use_cache = value
424
+
425
+ def json_pipeline(self) -> dict[str, Any]:
426
+ """Exports a representation of this step as a dictionary.
427
+
428
+ :return: Dictionary view of this step's signature.
429
+ """
430
+ return {
431
+ 'step': self.__class__.__name__,
432
+ 'name': self.name,
433
+ 'description': self.description,
434
+ 'enable': self.enable,
435
+ 'can_be_disable': self.can_be_disabled,
436
+ 'configuration': {
437
+ k: { **v, 'description': v['description'].replace('\n', ' ') }
438
+ for k, v in self.configuration.items() },
439
+ 'children': []
440
+ }
441
+
442
+ def conf_to_rich_str_list(self) -> list[str]:
443
+ """Renders the configuration of this step as a list of rich-formatted strings.
444
+
445
+ :return: List of rich-formatted strings.
446
+ """
447
+ conf = [ f'{name}={conf["value"]}' for name, conf in self.configuration.items() ]
448
+
449
+ return conf
450
+
451
+ def to_rich_str(self) -> str:
452
+ """Renders the signature of this step as a rich-formatted string.
453
+
454
+ :return: Rich-formatted string.
455
+ """
456
+ conf = self.conf_to_rich_str_list()
457
+
458
+ if len(conf) > 0:
459
+ name = f'[b]{self.__class__.__name__}[/] ({", ".join(conf)})'
460
+ else:
461
+ name = f'[b]{self.__class__.__name__}[/]'
462
+
463
+ return name
464
+
465
+ def count_steps(self) -> int:
466
+ """Returns a rough estimation of the total count of steps for a given pipeline.
467
+
468
+ :return: Number of steps.
469
+ """
470
+ return 1
471
+
472
+ ############
473
+ # Priorize #
474
+ ############
475
+ def priorize(self, candidate: Candidate = None) -> float: # pylint: disable=unused-argument
476
+ """Evaluates the priority of the step within a pipeline. 0 means the execution of the step
477
+ should not be prioritized, and 1 means it should be executed early in the pipeline.
478
+
479
+ :param Candidate, optional candidate: Candidate on which the priority should be evaluated.
480
+ :return: Value between 0 and 1.
481
+ """
482
+ return 0.0
483
+
484
+ #######
485
+ # RUN #
486
+ #######
487
+ def fit(self, dataset: Dataset) -> 'Step': # pylint: disable=unused-argument
488
+ """Fits the step on the given dataset.
489
+
490
+ :param Dataset dataset: Features and labels.
491
+ :return: Fitted step.
492
+ """
493
+ return self
494
+
495
+ @runner
496
+ def run(self, candidate: Candidate) -> Candidate:
497
+ """Runs the step for a given candidate.
498
+
499
+ :param Candidate candidate: Candidate to run the step for.
500
+ :return: Run candidate.
501
+ """
502
+
503
+ # Never fit predictor during generation of candidates
504
+ if self.tags and 'predictor' in self.tags:
505
+ return candidate.add_to_pipeline(self)
506
+
507
+ self.fit(candidate.dataset)
508
+
509
+ return candidate.add_to_pipeline(self)
510
+
511
+ def fingerprint(self) -> str:
512
+ """Returns a MD5 hash that can be used to distinguish steps.
513
+
514
+ :return: MD5 string.
515
+ """
516
+ to_hash = f"{str(self.__class__)} = \
517
+ {json.dumps(self.serializable_resume_configuration(), sort_keys=True)}"
518
+
519
+ return md5(to_hash.encode()).hexdigest()
520
+
521
+ ####################
522
+ ### Explanations ###
523
+ ####################
524
+ def __format_description(self, description: str) -> str:
525
+ return description \
526
+ .replace('\n', ' ') \
527
+ .format(**{ k: v['value'] for k, v in self.configuration.items() })
528
+
529
+ @property
530
+ def description(self) -> str:
531
+ """Formats the description of a step with its configuration.
532
+
533
+ :return: Formatted description.
534
+ """
535
+ return self.__format_description(self._description)
536
+
537
+ @property
538
+ def description_long(self) -> str:
539
+ """Formats the longer description of a step with its configuration.
540
+
541
+ :return: Formatted description.
542
+ """
543
+ return self.__format_description(self._description_long)
544
+
545
+ def explain(self, processings_limit: int = 20) -> str:
546
+ """Renders the step configurations as Markdown text.
547
+
548
+ :param int processings_limit: Maximum number of processings to render.
549
+ :return: Markdown document.
550
+ """
551
+ confs = '\n '.join([
552
+ f'| **{k}** | {v["description"]} | {v["value"]} |'.replace('\n', '')
553
+ for k, v in self.configuration.items()
554
+ ])
555
+
556
+ processings = [ f' - {p}' for p in self.explanations[:processings_limit] ]
557
+ explanations = '\n '.join(processings)
558
+ processings_left = len(self.explanations) - processings_limit
559
+
560
+ if len(explanations) == 0 and (hasattr(self, 'transform') or hasattr(self, 'resample')):
561
+ return None
562
+
563
+ markdown_conf = textwrap.dedent(f"""\
564
+ ### Configuration
565
+ | Name | Description | Value |
566
+ | ---- | ----------- | ----- |
567
+ {confs}
568
+ """) if len(confs) > 0 else ""
569
+
570
+ markdown_processings = textwrap.dedent(f'''\
571
+ ### Processings
572
+ {explanations}
573
+ {f" - *and **{processings_left}** more explanations...*" if processings_left > 0 else ""}
574
+ ''') if len(explanations) > 0 else ""
575
+
576
+ return '\n'.join([
577
+ f'## {self.name}',
578
+ f'**{self.description}**\n',
579
+ markdown_conf,
580
+ markdown_processings,
581
+ ])
582
+
583
+ def same_types(a: dict, b: dict) -> bool:
584
+ """Recursively checks whether the two provided dictionaries are the exact same.
585
+
586
+ :param dict a: First dictionary.
587
+ :param dict b: Second dictionary.
588
+ :return: Whether the two dictionaries are the same.
589
+ """
590
+ if len(a.keys()) != len(b.keys()):
591
+ return False
592
+
593
+ for key, value in a.items():
594
+ if isinstance(value, dict):
595
+ return same_types(value, b[key])
596
+
597
+ if key not in b or value != b[key]:
598
+ return False
599
+
600
+ return True