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,125 @@
1
+ """Singleton used by IAML to dispatch cores to process"""
2
+ import threading
3
+ import warnings
4
+ import multiprocess
5
+ import multiprocess.managers
6
+ import psutil
7
+ from .meta_singleton import MetaSingleton
8
+ from .logger import Logger
9
+
10
+
11
+ class CoreDispatcher(metaclass=MetaSingleton):
12
+ """Singleton used by IAML to dispatch cores to process
13
+
14
+ :param tuple, optional \\*args: Additional parameters.
15
+ :param dict, optional \\**kwargs: Additional parameters.
16
+ """
17
+ def __init__(self, *args, **kwargs) -> None: #pylint: disable=unused-argument
18
+ self.__all_cores: set[int] = set(range(psutil.cpu_count(logical=True)))
19
+ """Set of all cores available"""
20
+
21
+ self.manager: multiprocess.Manager | None = None
22
+ """Manager used to dispatch cores to process (None when not available)."""
23
+
24
+ self._lock: threading.RLock | multiprocess.managers.SyncManager.RLockProxy
25
+ """Lock protecting access to booked cores."""
26
+
27
+ self.books: list | multiprocess.managers.ListProxy
28
+ """Booked cores registry."""
29
+
30
+ try:
31
+ self.manager = multiprocess.Manager()
32
+ self._lock = self.manager.RLock()
33
+ self.books = self.manager.list([])
34
+ except Exception as exc: # pylint: disable=broad-except
35
+ warnings.warn(f"CoreDispatcher fallback to local mode (manager start failed: {exc!r})")
36
+ self.manager = None
37
+ self._lock = threading.RLock()
38
+ self.books = []
39
+
40
+ def reset_books(self) -> None:
41
+ """Set all cpu cores as available"""
42
+ with self._lock:
43
+ if hasattr(self.books, "clear"):
44
+ self.books.clear()
45
+ else:
46
+ self.books[:] = []
47
+
48
+ @property
49
+ def available_cores(self) -> set[int]:
50
+ """All cores minus booked ones
51
+
52
+ :return: Set of available cpu cores
53
+ """
54
+ avail = set(self.__all_cores)
55
+ with self._lock:
56
+ for book in list(self.books):
57
+ avail -= set(book['cores'])
58
+
59
+ return avail
60
+
61
+ def __book_cpu(self, pids: set[int], number: int) -> None:
62
+ """Affiliate CPU cores to process
63
+
64
+ :param set[int] pids: pids to set affinity with.
65
+ :param int number: Number of cores to book.
66
+ :raise RuntimeError: Not enough CPU cores available.
67
+ """
68
+ self.__free_cores()
69
+ with self._lock:
70
+ if len(self.available_cores) < number:
71
+ raise RuntimeError('Not enough CPU cores available')
72
+
73
+ to_book = list(self.available_cores)[:number]
74
+ entry = {
75
+ 'pids': pids,
76
+ 'cores': to_book
77
+ }
78
+ self.books.extend([entry])
79
+
80
+ for pid in pids:
81
+ process = psutil.Process(pid)
82
+ try:
83
+ process.cpu_affinity(to_book)
84
+ except (psutil.AccessDenied, AttributeError, NotImplementedError):
85
+ Logger().warning(
86
+ 'Unable to set CPU affinity for process '
87
+ f'{pid}. Continuing without affinity control.'
88
+ )
89
+
90
+ def affiliate(self, pids: list[int], core_number: int = 1) -> None:
91
+ """Run process with CPU affinity
92
+
93
+ :param list[int] pids: Process to affiliate with CPU cores.
94
+ :param int, optional core_number: number of process to run
95
+ (each process run the same target). Defaults to 1.
96
+ """
97
+ try:
98
+ test_process = psutil.Process(pids[0])
99
+ except psutil.Error:
100
+ Logger().warning('Unable to inspect process for CPU affinity; skipping affinity control.')
101
+ return
102
+
103
+ if hasattr(test_process, 'cpu_affinity'):
104
+ try:
105
+ self.__book_cpu(pids, core_number)
106
+ except RuntimeError as exc:
107
+ Logger().warning(str(exc))
108
+ else:
109
+ Logger().warning(
110
+ 'Your OS doesn\'t support CPU affinity. We are not able to control CPU cores access'
111
+ )
112
+
113
+ def __free_cores(self) -> None:
114
+ """Free cores that are not used anymore"""
115
+ with self._lock:
116
+ new_books = []
117
+ for book in self.books:
118
+ if any(psutil.pid_exists(pid) and psutil.Process(pid).is_running()
119
+ for pid in book['pids']):
120
+ new_books.append(book)
121
+
122
+ self.reset_books()
123
+ self.books.extend(new_books)
124
+
125
+ CoreDispatcher() # Run it a first time to init shared objects
iaml/data_type.py ADDED
@@ -0,0 +1,11 @@
1
+ """Types of data used in Dataset"""
2
+ from enum import IntEnum
3
+
4
+
5
+ class DataType(IntEnum):
6
+ """Types of data used in Dataset"""
7
+ CATEGORICAL = 0
8
+ TEXT = 1
9
+ SHORT_TEXT = 2
10
+ NUMERIC = 3
11
+ DATE = 4
iaml/dataset.py ADDED
@@ -0,0 +1,506 @@
1
+ """Encapsulate X, y data to be used by Steps
2
+ Add features like data type detection and splitting
3
+ """
4
+ import copy
5
+ from copy import deepcopy
6
+ from typing import Any, Iterator, TYPE_CHECKING
7
+ from sklearn.model_selection import StratifiedShuffleSplit, ShuffleSplit
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from .cache_keys import hash_dataset
12
+ from .data_type import DataType
13
+ from .type_of_target import type_of_target
14
+ from .logger import Logger
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from .iaml_pipeline import IAMLPipeline
19
+
20
+
21
+ class Dataset:
22
+ """Encapsulate X, y data to be used by Steps
23
+ Add features like data type detection and splitting
24
+
25
+ :param pd.DataFrame X: The dataframe used in this Dataset.
26
+ :param list, optional y: The dataframe target. Default to None.
27
+ :param pd.DataFrame, optional groups: If set, will be used when splitting to avoid
28
+ having data rows for a similar ID in two different splits. Default to None.
29
+ :param list[str], optional groups_columns: Same as groups, but with column names.
30
+ Default to None.
31
+ :param dict, optional columns_types: Specify column types instead of detecting them
32
+ automaticaly. Default to None.
33
+
34
+ """
35
+
36
+ def __init__(self,
37
+ X: pd.DataFrame,
38
+ y: list = None,
39
+ groups: pd.DataFrame = None,
40
+ groups_columns: list[str] = None,
41
+ columns_types: dict = None
42
+ ):
43
+ type_of_target_override = None
44
+ if isinstance(y, str):
45
+ type_of_target_override = y
46
+ y = []
47
+
48
+ if columns_types is None and isinstance(groups, dict) and not groups_columns:
49
+ if self._looks_like_columns_types(groups):
50
+ columns_types = groups
51
+ groups = None
52
+
53
+ if groups is not None and groups_columns:
54
+ raise ValueError("groups and groups_columns are not None. Only one must be set")
55
+
56
+ if isinstance(groups, pd.DataFrame):
57
+ if set(groups.columns).intersection(X.columns):
58
+ raise ValueError("Group columns present in dataset!")
59
+
60
+ if groups_columns is None:
61
+ groups_columns = []
62
+
63
+ self.__X: pd.DataFrame = X.drop(columns=groups_columns)
64
+ """The dataframe used in this Dataset object without groups columns if provided"""
65
+
66
+ # Make sure y is either None or single column
67
+ if isinstance(y, pd.DataFrame):
68
+ if len(y.columns) > 1:
69
+ raise ValueError("IAML only handle single column labels !")
70
+ y = y.values.ravel()
71
+ self.__y: np.array = np.array(y)
72
+ """The dataframe target"""
73
+
74
+ if groups_columns:
75
+ self.groups = X[groups_columns]
76
+ else:
77
+ if isinstance(groups, dict):
78
+ self.groups = pd.DataFrame(groups)
79
+ elif isinstance(groups, (pd.Series, list, np.ndarray)):
80
+ self.groups = pd.DataFrame(groups)
81
+ else:
82
+ self.groups = groups
83
+
84
+ if self.groups is not None and self.groups.shape[1] > 1:
85
+ # Create a combined group label by concatenating all columns into tuples
86
+ Logger().warning("You are using multiple columns as groups. \
87
+ Be careful, as these columns will serve as a composite key.")
88
+ self.groups = pd.DataFrame(pd.Series(
89
+ list(zip(*[self.groups[col] for col in self.groups.columns]))),
90
+ columns=['groups']
91
+ )
92
+
93
+ if self.groups is not None and len(self.groups) != len(self.__X):
94
+ raise ValueError("Groups must have one row per feature row")
95
+
96
+ self.columns_types: dict = self._normalize_columns_types(columns_types, self.__X)
97
+ """Columns types to be applied to our dataframe columns"""
98
+
99
+ self.__detect_columns_types()
100
+
101
+ self.type_of_target: str = None
102
+ """Type of target to predict"""
103
+
104
+ if type_of_target_override is not None:
105
+ self.type_of_target = type_of_target_override
106
+ elif y is not None:
107
+ self.type_of_target = type_of_target(self.__y)
108
+
109
+ @staticmethod
110
+ def _normalize_columns_types(columns_types: dict | None, X: pd.DataFrame) -> dict:
111
+ if not columns_types:
112
+ return {}
113
+
114
+ if all(isinstance(key, DataType) for key in columns_types.keys()):
115
+ normalized = {}
116
+ for data_type, columns in columns_types.items():
117
+ if columns is None:
118
+ continue
119
+ if isinstance(columns, (str, bytes)):
120
+ columns = [columns]
121
+ for column in columns:
122
+ if column in X.columns:
123
+ normalized[column] = (X[column].dtype, data_type)
124
+ return normalized
125
+
126
+ if all(isinstance(value, DataType) for value in columns_types.values()):
127
+ normalized = {}
128
+ for column, data_type in columns_types.items():
129
+ if column in X.columns:
130
+ normalized[column] = (X[column].dtype, data_type)
131
+ return normalized
132
+
133
+ return columns_types
134
+
135
+ @staticmethod
136
+ def _looks_like_columns_types(columns_types: dict) -> bool:
137
+ if not columns_types:
138
+ return False
139
+
140
+ keys = columns_types.keys()
141
+ values = columns_types.values()
142
+
143
+ if all(isinstance(key, DataType) for key in keys):
144
+ return True
145
+
146
+ if all(isinstance(value, DataType) for value in values):
147
+ return True
148
+
149
+ if all(
150
+ isinstance(value, tuple)
151
+ and len(value) == 2
152
+ and isinstance(value[1], DataType)
153
+ for value in values
154
+ ):
155
+ return True
156
+
157
+ return False
158
+
159
+ @property
160
+ def features(self) -> list[str]:
161
+ """List columns names of X data
162
+
163
+ :return: columns names.
164
+ """
165
+ return self.X.columns.to_list()
166
+
167
+ @property
168
+ def X(self) -> pd.DataFrame:
169
+ """X data getter
170
+
171
+ :return: X data
172
+ """
173
+ return self.__X
174
+
175
+ @property
176
+ def y(self) -> np.ndarray:
177
+ """y data getter
178
+
179
+ :return: y data
180
+ """
181
+ return self.__y
182
+
183
+ def copy(self, deep: bool = True) -> 'Dataset':
184
+ """Copy Dataset into a new instance
185
+
186
+ :param bool, optional deep: Perform a deep copy. Defaults to True.
187
+ :return: Copied Dataset.
188
+ """
189
+ if deep:
190
+ return copy.deepcopy(self)
191
+ return copy.copy(self)
192
+
193
+ def decline(self, X: pd.DataFrame, y: list, groups: pd.DataFrame = None) -> 'Dataset':
194
+ """Create a new Dataset with columns_types based on self.
195
+ Avoid time consuming columns_types computing
196
+
197
+ :param pd.DataFrame X: The dataframe to use for our newly created Dataset.
198
+ :param list y: The target to use for our newly created Dataset.
199
+ :param pd.DataFrame, optional groups: The groups to use for our newly created Dataset.
200
+ :return: New Dataset.
201
+ """
202
+ if groups is None:
203
+ groups = self.groups
204
+ return Dataset(X, y, groups=groups, columns_types=self.columns_types)
205
+
206
+ def sample(self, n: int) -> 'Dataset':
207
+ """Return a dataset with a sample of data
208
+
209
+ :param int n: Number of line in the sample dataset
210
+ :return: dataset with a sample of data
211
+ """
212
+ if isinstance(n, float):
213
+ n = int(self.X.shape[0]*n)
214
+
215
+ if n >= self.X.shape[0]:
216
+ return self.decline(self.X, self.y)
217
+
218
+ if self.type_of_target == 'continuous':
219
+ _, test_idx = next(
220
+ ShuffleSplit(n_splits=1, test_size=n, random_state=42
221
+ ).split(self.X, self.y))
222
+ else:
223
+ try:
224
+ _, test_idx = next(
225
+ StratifiedShuffleSplit(n_splits=1, test_size=n, random_state=42
226
+ ).split(self.X, self.y))
227
+ except ValueError:
228
+ _, test_idx = next(
229
+ ShuffleSplit(n_splits=1, test_size=n, random_state=42
230
+ ).split(self.X, self.y))
231
+
232
+ groups = self.groups.iloc[test_idx].copy() if self.has_groups else None
233
+ return self.decline(self.X.iloc[test_idx], self.y[test_idx], groups=groups)
234
+
235
+ def transform(self, method: callable) -> None:
236
+ """Apply transform method to X or y data based on the method signature
237
+
238
+ :param callable method: Callable to apply. Will be call with X or y as parameter.
239
+ :return: Transformed dataset.
240
+ """
241
+ self.__X = method(self.__X)
242
+ self.__detect_columns_types()
243
+
244
+ @property
245
+ def has_groups(self) -> bool:
246
+ """Groups exists ?
247
+
248
+ :return: Exists ?
249
+ """
250
+ return self.groups is not None and not self.groups.empty
251
+
252
+ def resample(self, resampler: callable) -> 'Dataset':
253
+ """Apply a resampler on X, y and groups data.
254
+
255
+ :param callable resampler: Resampler method.
256
+ :return: Resampled Dataset.
257
+ """
258
+
259
+ if self.has_groups:
260
+ # Merge groups with X
261
+ X = self.X.reset_index(drop=True).join(self.groups.reset_index(drop=True))
262
+
263
+ # Resampler
264
+ X, y = resampler(X, self.y)
265
+
266
+ # Split groups and X
267
+ return self.decline(X.drop(columns=self.groups.columns),
268
+ y,
269
+ groups=X[self.groups.columns])
270
+ return self.decline(*resampler(self.X, self.y))
271
+
272
+ def fingerprint(self) -> str:
273
+ """Hash the current features, targets, groups and interpretation metadata.
274
+
275
+ Recompute because X, y and groups are exposed as mutable objects.
276
+ """
277
+ return hash_dataset(
278
+ self.__X, self.__y, self.groups, self.columns_types, self.type_of_target
279
+ )
280
+
281
+ def split(self, splitter: callable, *args, **kwargs) -> Iterator[tuple['Dataset', 'Dataset']]:
282
+ """Use splitter to split dataset into a list of tuple (train set, test set)
283
+
284
+ :param callable splitter: The splitter function to perform.
285
+ :param tuple, optional \\*args: Additional parameters.
286
+ :param dict, optional \\**kwargs: Additional parameters.
287
+ :return: Train set and Test set iterator.
288
+ """
289
+ # Split the dataset as many times as the splitter requires it
290
+ y = self.y if self.type_of_target is not None else None
291
+ for i_train, i_test in splitter(self.X, y, *args, **kwargs):
292
+ X_train = self.X.iloc[i_train].copy()
293
+ X_test = self.X.iloc[i_test].copy()
294
+
295
+ if self.has_groups:
296
+ groups = self.groups.iloc[i_train].copy()
297
+ test_groups = self.groups.iloc[i_test].copy()
298
+ else:
299
+ groups = None
300
+ test_groups = None
301
+
302
+ if y is not None:
303
+ y_train = self.__y[i_train].copy()
304
+ y_test = self.__y[i_test].copy()
305
+ else:
306
+ y_train = None
307
+ y_test = None
308
+
309
+ yield (self.decline(X_train, y_train, groups=groups),
310
+ self.decline(X_test, y_test, groups=test_groups))
311
+
312
+ def x_with_groups(self) -> pd.DataFrame:
313
+ """Return X dataframe with groups columns if not None. Return X otherwise.
314
+
315
+ :return: The X dataframe with or without the groups columns.
316
+ """
317
+ if self.has_groups:
318
+ return self.X.reset_index(drop=True).join(self.groups.reset_index(drop=True))
319
+ return self.X
320
+
321
+ def get_columns_names_by_type(self, types: list[DataType]) -> list[str]:
322
+ """Get names of all the columns with DataType in types
323
+
324
+ :param list[DataType] types: List of Datatype to search.
325
+ :return: columns names.
326
+ """
327
+ if not isinstance(types, list):
328
+ types = [types]
329
+
330
+ return [
331
+ column
332
+ for column, (dtype, type) in self.columns_types.items()
333
+ if type in types
334
+ ]
335
+
336
+ def __detect_data_type(self, column_name: str) -> DataType:
337
+ """Detect data type of a column
338
+
339
+ :param str column_name: Name of the column to analyse.
340
+ :return Type of the columns.
341
+ """
342
+ column_value = self.X[column_name]
343
+ dtype = column_value.dtype
344
+ detected: DataType = None
345
+ if isinstance(dtype, pd.CategoricalDtype) or pd.api.types.is_bool_dtype(dtype):
346
+ detected = DataType.CATEGORICAL
347
+ elif pd.api.types.is_object_dtype(dtype) or pd.api.types.is_string_dtype(dtype):
348
+ if len(column_value) == 0:
349
+ detected = DataType.CATEGORICAL
350
+ else:
351
+ unique_count = len(column_value.unique())
352
+ if (unique_count / len(column_value) < 0.05 \
353
+ or unique_count < 7):
354
+ detected = DataType.CATEGORICAL
355
+ elif column_value.astype(str).apply(len).max() <= 85:
356
+ detected = DataType.SHORT_TEXT
357
+ else:
358
+ detected = DataType.TEXT
359
+ elif pd.api.types.is_numeric_dtype(dtype) or pd.api.types.is_timedelta64_dtype(dtype):
360
+ detected = DataType.NUMERIC
361
+ elif pd.api.types.is_datetime64_any_dtype(dtype):
362
+ detected = DataType.DATE
363
+
364
+ return dtype, detected
365
+
366
+ @property
367
+ def needed_estimator(self) -> str:
368
+ """Kind of estimator needed for this dataset
369
+
370
+ :return: Estimator type.
371
+ """
372
+ if self.type_of_target == 'continuous':
373
+ return 'regressor'
374
+
375
+ if self.type_of_target == 'survival':
376
+ return 'survival'
377
+
378
+ return 'classifier'
379
+
380
+ def __detect_columns_types(self) -> None:
381
+ """Detect column type of all features in X"""
382
+ new_types = {}
383
+ for column in self.features:
384
+ if column not in self.columns_types \
385
+ or self.columns_types[column][0] != self.X[column].dtype:
386
+ new_types[column] = self.__detect_data_type(column)
387
+ else:
388
+ new_types[column] = self.columns_types[column]
389
+
390
+ self.columns_types = new_types
391
+
392
+ def to_survival(self) -> None:
393
+ """Turn dataframe to survival compatibility"""
394
+ return Dataset.fix_survival(self.X, self.y)
395
+
396
+ @staticmethod
397
+ def _normalize_survival_pair(value: Any) -> tuple[bool, float]:
398
+ """Normalize a single survival sample to a (event, time) tuple."""
399
+ if isinstance(value, np.void):
400
+ if value.dtype.names and \
401
+ 'event' in value.dtype.names and 'time' in value.dtype.names:
402
+ return bool(value['event']), float(value['time'])
403
+ value = value.tolist()
404
+
405
+ if isinstance(value, dict):
406
+ if 'event' not in value or 'time' not in value:
407
+ raise KeyError("Survival sample dictionary must include 'event' and 'time'.")
408
+ return bool(value['event']), float(value['time'])
409
+
410
+ if isinstance(value, np.ndarray):
411
+ if value.shape == ():
412
+ return Dataset._normalize_survival_pair(value.item())
413
+ if value.ndim >= 1 and value.shape[0] >= 2:
414
+ return bool(value[0]), float(value[1])
415
+
416
+ if isinstance(value, (tuple, list)):
417
+ if len(value) < 2:
418
+ raise ValueError("Survival sample must provide event indicator and time.")
419
+ return bool(value[0]), float(value[1])
420
+
421
+ raise TypeError(f"Unsupported survival sample format: {type(value)}")
422
+
423
+ @classmethod
424
+ def normalize_survival_target(cls, y: Any) -> list[tuple[bool, float]]:
425
+ """Return survival targets as a list of (event, time) tuples."""
426
+ if y is None:
427
+ return []
428
+
429
+ if isinstance(y, pd.DataFrame):
430
+ if not len(y.columns):
431
+ return []
432
+ if {'event', 'time'}.issubset(y.columns):
433
+ iterator = zip(y['event'], y['time'])
434
+ elif len(y.columns) >= 2:
435
+ iterator = (row[:2] for row in y.itertuples(index=False, name=None))
436
+ else:
437
+ raise ValueError("Survival DataFrame must contain at least two columns.")
438
+ return [cls._normalize_survival_pair(sample) for sample in iterator]
439
+
440
+ if isinstance(y, pd.Series):
441
+ return cls.normalize_survival_target(y.to_frame())
442
+
443
+ if isinstance(y, np.ndarray):
444
+ if y.dtype.names and 'event' in y.dtype.names and 'time' in y.dtype.names:
445
+ return [cls._normalize_survival_pair((row['event'], row['time'])) for row in y]
446
+ if y.ndim == 0:
447
+ return [cls._normalize_survival_pair(y.item())]
448
+ if y.ndim == 1:
449
+ return [cls._normalize_survival_pair(sample) for sample in y.tolist()]
450
+ if y.ndim >= 2 and y.shape[1] >= 2:
451
+ return [cls._normalize_survival_pair(sample[:2]) for sample in y]
452
+
453
+ if isinstance(y, (list, tuple)):
454
+ return [cls._normalize_survival_pair(sample) for sample in y]
455
+
456
+ if hasattr(y, '__iter__'):
457
+ return cls.normalize_survival_target(list(y))
458
+
459
+ raise TypeError(f"Unsupported survival target format: {type(y)}")
460
+
461
+ @classmethod
462
+ def fix_survival(cls, X: pd.DataFrame, y: Any) -> tuple[pd.DataFrame, np.ndarray]:
463
+ """Turn dataframe to survival compatibility
464
+
465
+ :param pd.DataFrame X: The dataframe to fix.
466
+ :param Any y: The dataframe target to fix.
467
+ :return: Fixed dataframe
468
+ """
469
+ from sksurv.util import Surv
470
+
471
+ samples = cls.normalize_survival_target(y)
472
+ if samples:
473
+ events, times = zip(*samples)
474
+ y_surv = Surv.from_arrays(
475
+ event=np.asarray(events, dtype=bool),
476
+ time=np.asarray(times, dtype=float)
477
+ )
478
+ else:
479
+ y_surv = np.array([], dtype=[('event', 'bool'), ('time', 'float')])
480
+
481
+ X = deepcopy(X)
482
+ X[X.select_dtypes(include=['float64']).columns] = \
483
+ X.select_dtypes(include=['float64']).astype('float32')
484
+
485
+ return X, y_surv
486
+
487
+ @classmethod
488
+ def fix_y_survival(cls, y: Any, y_train: Any) -> list[tuple[bool, float]]:
489
+ """Adjust survival targets to avoid censoring beyond the training horizon."""
490
+ y_samples = cls.normalize_survival_target(y)
491
+ y_train_samples = cls.normalize_survival_target(y_train)
492
+
493
+ if not y_train_samples:
494
+ return y_samples
495
+
496
+ _, times = zip(*y_train_samples)
497
+ censure_time = max(times)
498
+
499
+ new_y: list[tuple[bool, float]] = []
500
+ for event, time in y_samples:
501
+ if time >= censure_time:
502
+ time = censure_time
503
+ event = False
504
+ new_y.append((event, time))
505
+
506
+ return new_y
@@ -0,0 +1,3 @@
1
+ """All IAML decorators are imported in all.py
2
+ It allow use to import decorator individually and to avoid loop import
3
+ """
iaml/decorators/all.py ADDED
@@ -0,0 +1,4 @@
1
+ """All IAML decorators"""
2
+ # pylint: disable=unused-import,import-error
3
+ from .runner import runner
4
+ from .is_step import is_step, find_steps_by_tag
@@ -0,0 +1,45 @@
1
+ """Class decorator -> Must decorate each Step inheritance"""
2
+ from ..step import Step
3
+
4
+ def is_step(*tags) -> callable:
5
+ """is_step is needed to declare new Step.
6
+ With the Step inheritance, it will setup everything to make it work smoothly
7
+
8
+ :param tuple, optional tags: Your Step will be attached to these tags.
9
+ tags are use to easily include Step into Pipeline
10
+ :return: Step decorator
11
+ """
12
+ def step_wrapper(cls) -> Step:
13
+ """Register the step with IAML.
14
+ Add call to Step.__init__() so the Sub Step developer have one to care about this
15
+
16
+ :return: Edited step class
17
+ """
18
+ Step.available_steps[cls] = tags # Declare your Step to IAML
19
+
20
+ # Help Python to find parent class
21
+ __class__ = cls # pylint: disable=unused-variable
22
+
23
+ initial_init = cls.__init__ # Keep the __init__ you have created
24
+ def __init__(self, *args, **kw):
25
+ if cls != Step:
26
+ super().__init__(*args, **kw) # All parent constructor
27
+
28
+ self.tags = set(tags)
29
+ initial_init(self, *args, **kw) # Run your __init__
30
+ self.default_configuration() # Setup default configuration
31
+
32
+ cls.__init__ = __init__ # Replace your init
33
+
34
+ return cls
35
+
36
+ return step_wrapper
37
+
38
+
39
+ def find_steps_by_tag(tag: str) -> list[Step]:
40
+ """Retrieves all registered steps with a specific tag.
41
+
42
+ :param str tag: Tag to search for
43
+ :return: Matching steps
44
+ """
45
+ return set(filter(lambda key: tag in Step.available_steps[key], Step.available_steps.keys()))