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,464 @@
1
+ """[STEP] Impute missing values with MICE (miceforest/LightGBM)"""
2
+ from copy import deepcopy
3
+ import os
4
+ import pickle
5
+ import textwrap
6
+ import pandas as pd
7
+ import numpy as np
8
+ import types
9
+
10
+ from ...actionable import Actionable
11
+ from ...dataset import Dataset
12
+ from ...candidate import Candidate
13
+ from ...decorators.all import is_step
14
+ from ...data_type import DataType
15
+
16
+ from ...logger import Logger
17
+
18
+ # miceforest
19
+ try:
20
+ import miceforest as mf
21
+ except ImportError as e:
22
+ raise ImportError(
23
+ "miceforest is required for ActMICEForestImputer. Install with: pip install miceforest lightgbm"
24
+ ) from e
25
+
26
+ try:
27
+ from lightgbm.basic import LightGBMError # type: ignore
28
+ _LGBM_ERRORS: tuple[type[Exception], ...] = (LightGBMError,)
29
+ except Exception:
30
+ _LGBM_ERRORS = tuple()
31
+
32
+
33
+ @is_step('cleaning')
34
+ class ActMICEForestImputer(Actionable):
35
+ """[STEP] Impute missing values with MICE (miceforest/LightGBM).
36
+
37
+ Copies and serialized steps retain fitted state without rebuilding models.
38
+ Accessing ``kernel`` or transforming data restores a private kernel; refitting
39
+ replaces that state directly.
40
+ """
41
+
42
+ name: str = 'Impute missing values (MICE - miceforest)'
43
+ _description: str = textwrap.dedent('''\
44
+ Impute missing values using MICE (chained equations) powered by LightGBM,
45
+ leveraging multivariate relations between features (numeric only by default).''')
46
+ _description_long: str = textwrap.dedent('''\
47
+ Uses miceforest.ImputationKernel to iteratively impute missing values.
48
+ By default works on numeric columns. Optionally auto-categorizes low-cardinality
49
+ object columns to allow categorical imputation by LightGBM.''')
50
+ _usage: str = 'Use when multivariate imputation is needed for missing numeric data instead of ActDropNumericalColumn. Applicable to datasets with correlated numeric features (and low-cardinality categoricals if auto_categorize). Avoid when missingness is tiny or ActCategoricalImputer is a better fit.'
51
+ can_be_disabled: bool = False
52
+
53
+ def __init__(self):
54
+ self.columns: list[str] = None
55
+ self._kernel: mf.ImputationKernel | None = None
56
+ self._kernel_snapshot: bytes | None = None
57
+ self._nan_stats: dict[str, tuple[int, int, float]] = {}
58
+ self._all_nan_cols: list[str] = []
59
+ self._prefill_values: dict[str, object] = {}
60
+
61
+ self.configuration: dict = {
62
+ 'max_iter': {
63
+ 'description': 'Number of MICE iterations to run.',
64
+ 'default': 5
65
+ },
66
+ 'random_state': {
67
+ 'description': 'Random seed for reproducibility (None for stochastic).',
68
+ 'default': 0
69
+ },
70
+ 'auto_categorize': {
71
+ 'description': 'If True, cast low-cardinality object columns to category.',
72
+ 'default': False
73
+ },
74
+ 'auto_categorize_max_cardinality': {
75
+ 'description': 'Max unique values to auto-cast object->category when auto_categorize=True.',
76
+ 'default': 30
77
+ }
78
+ }
79
+ self._n_jobs = self._detect_parallel_jobs()
80
+
81
+ @property
82
+ def kernel(self) -> mf.ImputationKernel | None:
83
+ """Restore a private kernel only when this copy needs its fitted state."""
84
+ snapshot = self._kernel_snapshot
85
+ if snapshot is not None:
86
+ self._kernel = pickle.loads(snapshot)
87
+ # A live kernel can mutate, so its previous snapshot must not be reused.
88
+ self._kernel_snapshot = None
89
+ return self._kernel
90
+
91
+ @kernel.setter
92
+ def kernel(self, value: mf.ImputationKernel | None) -> None:
93
+ self._kernel = value
94
+ self._kernel_snapshot = None
95
+
96
+ def __getstate__(self) -> dict:
97
+ """Transport fitted state without rebuilding Parquet tables or LightGBM models."""
98
+ state = self.__dict__.copy()
99
+ kernel = state.pop('_kernel')
100
+ if kernel is not None:
101
+ state['_kernel_snapshot'] = pickle.dumps(kernel, protocol=pickle.HIGHEST_PROTOCOL)
102
+ return state
103
+
104
+ def __setstate__(self, state: dict) -> None:
105
+ state = state.copy()
106
+ # Previously saved steps stored the live kernel directly as a public attribute.
107
+ kernel = state.pop('kernel', None)
108
+ self.__dict__.update(state)
109
+ self._kernel = kernel
110
+ self._kernel_snapshot = state.get('_kernel_snapshot')
111
+
112
+ def __deepcopy__(self, memo: dict) -> 'ActMICEForestImputer':
113
+ copied = type(self).__new__(type(self))
114
+ memo[id(self)] = copied
115
+ state = {}
116
+ for key, value in self.__getstate__().items():
117
+ # Preserve Step's treatment of runtime references and caches.
118
+ if key == 'candidate':
119
+ state[key] = None
120
+ elif key == 'caches':
121
+ state[key] = []
122
+ else:
123
+ state[key] = deepcopy(value, memo)
124
+ copied.__setstate__(state)
125
+ return copied
126
+
127
+ # --- helpers -----------------------------------------------------------------
128
+ def _select_columns(self, df: pd.DataFrame) -> list[str]:
129
+ # Base : colonnes numériques
130
+ cols = list(df.columns.intersection(df.select_dtypes(include=[np.number]).columns))
131
+
132
+ if self.configuration['auto_categorize']['default']:
133
+ max_card = int(self.configuration['auto_categorize_max_cardinality']['default'])
134
+ obj_cols = df.select_dtypes(include=['object']).columns
135
+ for c in obj_cols:
136
+ nuniq = df[c].nunique(dropna=True)
137
+ if 1 < nuniq <= max_card:
138
+ df[c] = df[c].astype('category')
139
+ cols.append(c)
140
+ cat_cols = df.select_dtypes(include=['category']).columns
141
+ for c in cat_cols:
142
+ if c not in cols:
143
+ cols.append(c)
144
+
145
+ # Préserver l'ordre d’origine
146
+ cols_ordered = [c for c in df.columns if c in set(cols)]
147
+ return cols_ordered
148
+
149
+ # --- core API ----------------------------------------------------------------
150
+ def fit(self, dataset: Dataset) -> Actionable:
151
+ # Refitting (including skipped fits) replaces old state without restoring it.
152
+ self.kernel = None
153
+ X = dataset.X.copy()
154
+ self.columns = self._select_columns(X)
155
+ if not self.columns:
156
+ self.explanations = []
157
+ self.kernel = None
158
+ return self
159
+
160
+ Logger().info("MICE FIT")
161
+
162
+
163
+ X_fit = X[self.columns].copy()
164
+
165
+ # Exclure colonnes entièrement NaN (miceforest ne peut pas les initialiser)
166
+ nonnull_counts = X_fit.notna().sum(axis=0)
167
+ valid_cols = [c for c in self.columns if nonnull_counts[c] > 0]
168
+ self._all_nan_cols = [c for c in self.columns if nonnull_counts[c] == 0]
169
+
170
+ # Pré-remplissages par défaut pour ces colonnes (appliqués en transform)
171
+ self._prefill_values = {}
172
+ for c in self._all_nan_cols:
173
+ if pd.api.types.is_numeric_dtype(X_fit[c]):
174
+ self._prefill_values[c] = 0
175
+ elif pd.api.types.is_categorical_dtype(X_fit[c]):
176
+ if 'missing' not in X_fit[c].cat.categories:
177
+ X_fit[c] = X_fit[c].cat.add_categories(['missing'])
178
+ self._prefill_values[c] = 'missing'
179
+ else:
180
+ self._prefill_values[c] = 'missing'
181
+
182
+ if not valid_cols:
183
+ self.explanations = [
184
+ "Skipped MICE: all selected columns had 0 observed values."
185
+ ]
186
+ self.kernel = None
187
+ return self
188
+
189
+ X_fit_valid = X_fit[valid_cols].copy().reset_index(drop=True)
190
+ if X_fit_valid.empty:
191
+ self.explanations = [
192
+ "Skipped MICE: dataset had 0 lignes après prétraitements (nothing to impute)."
193
+ ]
194
+ self.kernel = None
195
+ return self
196
+ if len(X_fit_valid) < 5:
197
+ self.explanations = [
198
+ f"Skipped MICE: dataset trop petit ({len(X_fit_valid)} lignes) pour miceforest."
199
+ ]
200
+ self.kernel = None
201
+ return self
202
+
203
+ self._nan_stats = {}
204
+ for c in valid_cols:
205
+ n_missing = int(X_fit_valid[c].isna().sum())
206
+ n_total = int(len(X_fit_valid[c]))
207
+ pct = (n_missing / n_total * 100.0) if n_total > 0 else 0.0
208
+ self._nan_stats[c] = (n_missing, n_total, pct)
209
+ rs = self.configuration['random_state']['default']
210
+
211
+ default_mmc = 5
212
+ mean_match_candidates = {
213
+ c: max(0, min(default_mmc, int(nonnull_counts[c]) - 1))
214
+ for c in valid_cols
215
+ }
216
+
217
+ kernel_kwargs = dict(
218
+ data=X_fit_valid,
219
+ random_state=rs,
220
+ num_datasets=1,
221
+ save_all_iterations_data=True,
222
+ mean_match_candidates=mean_match_candidates,
223
+ )
224
+ self.kernel = mf.ImputationKernel(**kernel_kwargs)
225
+
226
+ def _run_kernel() -> None:
227
+ self.kernel.mice(
228
+ int(self.configuration['max_iter']['default']),
229
+ n_jobs=self._n_jobs,
230
+ verbose=False,
231
+ seed=rs,
232
+ random_state=rs
233
+ )
234
+
235
+ fallback_errors: tuple[type[Exception], ...] = (IndexError,) + _LGBM_ERRORS
236
+
237
+ try:
238
+ _run_kernel()
239
+ except ValueError as exc:
240
+ Logger().warning(
241
+ "MICE fitting failed (%s). Step skipped; columns left untouched.", exc
242
+ )
243
+ raise RuntimeError(f"MICE fitting failed: {exc}") from exc
244
+ except fallback_errors as exc: # type: ignore[misc]
245
+ Logger().warning(
246
+ "MICE mean-matching failed (%s). Retrying without predictive mean matching.",
247
+ exc,
248
+ )
249
+ fallback_kernel_kwargs = dict(kernel_kwargs, mean_match_candidates=0)
250
+ self.kernel = mf.ImputationKernel(**fallback_kernel_kwargs)
251
+ try:
252
+ _run_kernel()
253
+ except Exception as exc2: # noqa: BLE001
254
+ Logger().warning(
255
+ "MICE fallback without predictive mean matching failed (%s). Step skipped; columns left untouched.",
256
+ exc2,
257
+ )
258
+ raise RuntimeError(
259
+ f"MICE fallback without predictive mean matching failed: {exc2}"
260
+ ) from exc2
261
+ except Exception as exc: # noqa: BLE001
262
+ Logger().warning(
263
+ "MICE fitting failed (%s). Step skipped; columns left untouched.", exc
264
+ )
265
+ raise RuntimeError(f"MICE fitting failed: {exc}") from exc
266
+
267
+ self._ensure_seed_on_kernel_models(rs)
268
+ self._ensure_parallelism_on_kernel_models(self._n_jobs)
269
+
270
+ # Explications
271
+ expl = [
272
+ f"Imputed missing values of column **`{c}`** using **MICE (miceforest)** "
273
+ f"(**{n}** / **{t}**; **{pct:.2f}%** missing in train data)."
274
+ for c, (n, t, pct) in self._nan_stats.items() if n > 0
275
+ ]
276
+ if self._all_nan_cols:
277
+ expl.append(
278
+ "Skipped MICE for all-NaN columns: " +
279
+ ", ".join(f"`{c}`" for c in self._all_nan_cols) +
280
+ " (cannot initialize with miceforest)."
281
+ )
282
+ self.explanations = expl
283
+
284
+ # Conserver toutes les colonnes (ordre : valides puis all-NaN)
285
+ self.columns = valid_cols + self._all_nan_cols
286
+ return self
287
+
288
+ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
289
+ """Apply trained MICEForest kernel to new data."""
290
+ if not self.columns:
291
+ return X
292
+
293
+ Logger().info("MICE TRANSFORM")
294
+
295
+ X_out = X.copy()
296
+
297
+ # Harmoniser les types si auto_categorize activé
298
+ if self.configuration['auto_categorize']['default']:
299
+ max_card = int(self.configuration['auto_categorize_max_cardinality']['default'])
300
+ for c in X_out.columns:
301
+ if c in self.columns and X_out[c].dtype == 'object':
302
+ nuniq = X_out[c].nunique(dropna=True)
303
+ if 1 < nuniq <= max_card:
304
+ X_out[c] = X_out[c].astype('category')
305
+
306
+ # 1) Imputer les colonnes "valides" via MICE (celles non all-NaN au fit)
307
+ valid_cols = [c for c in self.columns if c not in self._all_nan_cols]
308
+ if self.kernel is not None and valid_cols:
309
+ X_sub = X_out[valid_cols].copy()
310
+
311
+ # miceforest attend un RangeIndex
312
+ X_sub_reset = X_sub.reset_index(drop=True)
313
+
314
+ # Sécuriser les modèles du kernel : s'assurer que params['seed'] existe
315
+ _ = self._ensure_seed_on_kernel_models(self.configuration['random_state']['default'])
316
+ self._ensure_parallelism_on_kernel_models(self._n_jobs)
317
+
318
+ # Appel principal à impute_new_data ; en cas de KeyError 'seed', on coupe le PMM
319
+ try:
320
+ imputed_data = self.kernel.impute_new_data(
321
+ new_data=X_sub_reset,
322
+ datasets=[0],
323
+ iterations=int(self.configuration['max_iter']['default'])
324
+ )
325
+ except KeyError as e:
326
+ if str(e) == "'seed'":
327
+ # Fallback : désactiver le PMM pour l'imputation "new data"
328
+ imputed_data = self.kernel.impute_new_data(
329
+ new_data=X_sub_reset,
330
+ datasets=[0],
331
+ iterations=int(self.configuration['max_iter']['default']),
332
+ mean_match_candidates=0 # imputation par prédiction directe
333
+ # exact=True # <- alternative possible selon versions de miceforest
334
+ )
335
+ else:
336
+ raise
337
+
338
+ imputed = imputed_data.complete_data(dataset=0)
339
+
340
+ # Réinjection en respectant l'index d'origine de X_out
341
+ X_out.loc[:, valid_cols] = imputed[valid_cols].values
342
+
343
+ # 2) Pré-remplir les colonnes all-NaN (impossibles à traiter par miceforest)
344
+ for c in self._all_nan_cols:
345
+ if c in X_out.columns:
346
+ fill_val = self._prefill_values.get(c, np.nan)
347
+ if pd.api.types.is_categorical_dtype(X_out[c]) and str(fill_val) not in X_out[c].cat.categories:
348
+ X_out[c] = X_out[c].cat.add_categories([fill_val])
349
+ X_out[c] = X_out[c].fillna(fill_val)
350
+
351
+ # 3) Nettoyer les types numériques
352
+ for col in X_out.columns:
353
+ if pd.api.types.is_numeric_dtype(X_out[col]):
354
+ X_out[col] = pd.to_numeric(X_out[col], errors='coerce').infer_objects(copy=False)
355
+
356
+ return X_out
357
+
358
+
359
+ def suitable(self, dataset: Dataset) -> bool:
360
+ if dataset.X.empty:
361
+ return False
362
+
363
+ X = dataset.X.copy()
364
+ columns = self._select_columns(X)
365
+ if not columns:
366
+ return False
367
+
368
+ X_fit = X[columns]
369
+ if X_fit.empty:
370
+ return False
371
+
372
+ if not X_fit.isna().any().any():
373
+ return False
374
+
375
+ nonnull_counts = X_fit.notna().sum(axis=0)
376
+ valid_cols = [c for c in columns if nonnull_counts[c] > 0]
377
+ if not valid_cols:
378
+ return False
379
+
380
+ X_fit_valid = X_fit[valid_cols]
381
+ if X_fit_valid.empty or len(X_fit_valid) < 5:
382
+ return False
383
+
384
+ return True
385
+
386
+ def priorize(self, candidate: Candidate = None) -> float:
387
+ alpha = 0.01
388
+ return 1 - (candidate.dataset.X.isnull().sum().min() / len(candidate.dataset.X)) + alpha
389
+
390
+ def _yield_kernel_models(self):
391
+ """Itère de manière sécurisée sur les modèles LGBM stockés dans le kernel miceforest,
392
+ sans introspection profonde qui casse sur pandas."""
393
+ if self.kernel is None:
394
+ return
395
+ containers = []
396
+ for attr in ("imputation_models", "models", "model_dict", "model_dicts"):
397
+ if hasattr(self.kernel, attr):
398
+ containers.append(getattr(self.kernel, attr))
399
+
400
+ def walk(obj):
401
+ if obj is None:
402
+ return
403
+ if isinstance(obj, dict):
404
+ for v in obj.values():
405
+ yield from walk(v)
406
+ elif isinstance(obj, (list, tuple)):
407
+ for v in obj:
408
+ yield from walk(v)
409
+ else:
410
+ # Cible : objets LightGBM-like avec un dict .params
411
+ p = getattr(obj, "params", None)
412
+ if isinstance(p, dict):
413
+ yield obj
414
+
415
+ for c in containers:
416
+ yield from walk(c)
417
+
418
+ def _ensure_seed_on_kernel_models(self, seed: int) -> int:
419
+ """Ajoute params['seed'] aux modèles si absent. Retourne le nombre patché."""
420
+ patched = 0
421
+ for m in self._yield_kernel_models():
422
+ p = getattr(m, "params", None)
423
+ if isinstance(p, dict) and "seed" not in p:
424
+ p["seed"] = p.get("random_state", int(seed) if seed is not None else 0)
425
+ patched += 1
426
+ return patched
427
+
428
+ def _ensure_parallelism_on_kernel_models(self, threads: int) -> int:
429
+ if threads is None or threads < 1:
430
+ return 0
431
+ patched = 0
432
+ for m in self._yield_kernel_models():
433
+ params = getattr(m, "params", None)
434
+ if isinstance(params, dict):
435
+ updated = False
436
+ for key in ("num_threads", "n_jobs", "nthread"):
437
+ if params.get(key) != threads:
438
+ params[key] = threads
439
+ updated = True
440
+ if updated:
441
+ patched += 1
442
+ return patched
443
+
444
+ def _detect_parallel_jobs(self) -> int:
445
+ manual = os.environ.get("IAML_MICE_JOBS")
446
+ if manual:
447
+ try:
448
+ jobs = int(manual)
449
+ if jobs >= 1:
450
+ return jobs
451
+ except ValueError:
452
+ pass
453
+ cpu_count = os.cpu_count() or 1
454
+ if cpu_count <= 2:
455
+ return 1
456
+ if cpu_count <= 4:
457
+ return 2
458
+ if cpu_count <= 8:
459
+ return 4
460
+ if cpu_count <= 16:
461
+ return 6
462
+ if cpu_count <= 32:
463
+ return 8
464
+ return min(16, max(8, cpu_count // 2))
@@ -0,0 +1,109 @@
1
+ """[STEP] Add missing value count feature."""
2
+ import textwrap
3
+
4
+ import pandas as pd
5
+
6
+ from ...actionable import Actionable
7
+ from ...candidate import Candidate
8
+ from ...data_type import DataType
9
+ from ...dataset import Dataset
10
+ from ...decorators.all import is_step
11
+
12
+
13
+ @is_step('cleaning')
14
+ class ActMissingCountFeature(Actionable):
15
+ """[STEP] Add missing value count feature."""
16
+
17
+ name: str = 'Add missing count feature'
18
+ _usage: str = 'Use when per-row missingness may carry signal; Applicable to datasets with any column types that contain missing values; Avoid when you should impute with ActCategoricalImputer or drop fields with ActDropNumericalColumn.'
19
+ _description: str = textwrap.dedent('''\
20
+ Add a "{feature_name}" column with the number of missing values per row.''')
21
+ _description_long: str = textwrap.dedent('''\
22
+ Count missing values across selected columns and append the count as a single
23
+ numeric feature for each row. This captures the global missingness signal
24
+ that can be useful for downstream models.''')
25
+
26
+ def __init__(self) -> None:
27
+ self.configuration = {
28
+ 'feature_name': {
29
+ 'description': 'Name of the missing count feature.',
30
+ 'default': 'missing_count'
31
+ }
32
+ }
33
+ self.columns: list[str] = []
34
+ self.feature_name: str | None = None
35
+
36
+ def fit(self, dataset: Dataset) -> Actionable:
37
+ self.columns = self._select_columns(dataset)
38
+ self.feature_name = None
39
+ self.explanations = []
40
+
41
+ if not self.columns or dataset.X.empty:
42
+ return self
43
+
44
+ feature_name = self.get_config('feature_name')
45
+ if not feature_name:
46
+ feature_name = 'missing_count'
47
+ feature_name = str(feature_name)
48
+
49
+ reserved = set(dataset.X.columns)
50
+ self.feature_name = self._unique_name(feature_name, reserved)
51
+
52
+ missing_counts = dataset.X[self.columns].isna().sum()
53
+ total_missing = int(missing_counts.sum())
54
+ if total_missing > 0:
55
+ total_values = int(dataset.X[self.columns].size)
56
+ missing_columns = int((missing_counts > 0).sum())
57
+ ratio = (total_missing / total_values) if total_values else 0.0
58
+ self.explanations.append(
59
+ f"Added `{self.feature_name}` counting missing values per row "
60
+ f"({total_missing} missing across {missing_columns} columns, "
61
+ f"{ratio:.2%} of values)."
62
+ )
63
+
64
+ return self
65
+
66
+ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
67
+ if not self.columns or not self.feature_name:
68
+ return X
69
+
70
+ columns = [column for column in self.columns if column in X.columns]
71
+ if not columns:
72
+ return X
73
+
74
+ X[self.feature_name] = X[columns].isna().sum(axis=1)
75
+
76
+ return X
77
+
78
+ def suitable(self, dataset: Dataset) -> bool:
79
+ columns = self._select_columns(dataset)
80
+ if not columns or dataset.X.empty:
81
+ return False
82
+ return bool(dataset.X[columns].isna().any().any())
83
+
84
+ def priorize(self, candidate: Candidate = None) -> float:
85
+ if candidate is None or candidate.dataset.X.empty:
86
+ return 0.0
87
+ columns = self._select_columns(candidate.dataset)
88
+ if not columns:
89
+ return 0.0
90
+ missing = candidate.dataset.X[columns].isna().sum().sum()
91
+ total = candidate.dataset.X[columns].size or 1
92
+ return min(1.0, missing / total)
93
+
94
+ def _select_columns(self, dataset: Dataset) -> list[str]:
95
+ columns = dataset.get_columns_names_by_type(list(DataType))
96
+ if len(columns) != dataset.X.shape[1]:
97
+ missing = [column for column in dataset.X.columns if column not in columns]
98
+ columns.extend(missing)
99
+ return columns
100
+
101
+ def _unique_name(self, name: str, reserved: set[str]) -> str:
102
+ if name not in reserved:
103
+ return name
104
+ idx = 1
105
+ candidate = f"{name}_{idx}"
106
+ while candidate in reserved:
107
+ idx += 1
108
+ candidate = f"{name}_{idx}"
109
+ return candidate
@@ -0,0 +1,124 @@
1
+ """[STEP] Add missing value indicator columns."""
2
+ import textwrap
3
+
4
+ import pandas as pd
5
+
6
+ from ...actionable import Actionable
7
+ from ...candidate import Candidate
8
+ from ...data_type import DataType
9
+ from ...dataset import Dataset
10
+ from ...decorators.all import is_step
11
+
12
+
13
+ @is_step('cleaning')
14
+ class ActMissingIndicator(Actionable):
15
+ """[STEP] Add missing value indicator columns."""
16
+
17
+ name: str = 'Add missing indicators'
18
+ _description: str = textwrap.dedent('''\
19
+ Add binary "{suffix}" indicator columns to flag missing values.''')
20
+ _description_long: str = textwrap.dedent('''\
21
+ For each selected column, create a companion column named
22
+ "<column>{suffix}" that contains 1 when the value is missing and 0 otherwise.
23
+ The "features" option controls whether indicators are added for all columns
24
+ or only for columns that contain missing values (current: {features}).''')
25
+ _usage: str = "Use when missingness may be predictive and you want to keep columns rather than ActDropNumericalColumn or ActDropCategoricalColumn. Applicable to numerical or categorical columns with NaNs. Avoid when missingness is negligible or you plan to drop columns instead."
26
+
27
+ def __init__(self) -> None:
28
+ self.configuration = {
29
+ 'suffix': {
30
+ 'description': 'Suffix appended to indicator columns.',
31
+ 'default': '_is_missing'
32
+ },
33
+ 'features': {
34
+ 'description': textwrap.dedent('''\
35
+ Create indicators for all columns or only those with missing values.'''),
36
+ 'default': 'all',
37
+ 'categorical': ['all', 'missing-only']
38
+ }
39
+ }
40
+ self.columns: list[str] = []
41
+ self.indicator_columns: dict[str, str] = {}
42
+
43
+ def fit(self, dataset: Dataset) -> Actionable:
44
+ self.columns = self._select_columns(dataset)
45
+ self.indicator_columns = {}
46
+ self.explanations = []
47
+
48
+ if not self.columns or dataset.X.empty:
49
+ return self
50
+
51
+ missing_counts = dataset.X[self.columns].isna().sum()
52
+ feature_mode = self.get_config('features')
53
+ suffix = self.get_config('suffix')
54
+ if suffix is None:
55
+ suffix = '_is_missing'
56
+ suffix = str(suffix)
57
+
58
+ if feature_mode == 'missing-only':
59
+ selected = missing_counts[missing_counts > 0].index.tolist()
60
+ else:
61
+ selected = self.columns
62
+
63
+ if not selected:
64
+ return self
65
+
66
+ reserved = set(dataset.X.columns)
67
+ for column in selected:
68
+ indicator_name = self._unique_name(column, suffix, reserved)
69
+ reserved.add(indicator_name)
70
+ self.indicator_columns[column] = indicator_name
71
+
72
+ missing = int(missing_counts.get(column, 0))
73
+ if missing > 0:
74
+ self.explanations.append(
75
+ f"Added missing indicator `{indicator_name}` for `{column}` "
76
+ f"({missing} missing)."
77
+ )
78
+
79
+ return self
80
+
81
+ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
82
+ if not self.indicator_columns:
83
+ return X
84
+
85
+ present_columns = [column for column in self.indicator_columns if column in X.columns]
86
+ if not present_columns:
87
+ return X
88
+
89
+ mapping = {column: self.indicator_columns[column] for column in present_columns}
90
+ indicators = X[present_columns].isna().rename(columns=mapping).astype('int8')
91
+ X[indicators.columns] = indicators
92
+
93
+ return X
94
+
95
+ def suitable(self, dataset: Dataset) -> bool:
96
+ columns = self._select_columns(dataset)
97
+ if not columns or dataset.X.empty:
98
+ return False
99
+ return bool(dataset.X[columns].isna().any().any())
100
+
101
+ def priorize(self, candidate: Candidate = None) -> float:
102
+ if candidate is None or candidate.dataset.X.empty:
103
+ return 0.0
104
+ missing = candidate.dataset.X.isna().sum().sum()
105
+ total = candidate.dataset.X.size or 1
106
+ return min(1.0, missing / total)
107
+
108
+ def _select_columns(self, dataset: Dataset) -> list[str]:
109
+ columns = dataset.get_columns_names_by_type(list(DataType))
110
+ if len(columns) != dataset.X.shape[1]:
111
+ missing = [column for column in dataset.X.columns if column not in columns]
112
+ columns.extend(missing)
113
+ return columns
114
+
115
+ def _unique_name(self, column: str, suffix: str, reserved: set[str]) -> str:
116
+ base = f"{column}{suffix}"
117
+ if base not in reserved:
118
+ return base
119
+ idx = 1
120
+ candidate = f"{base}_{idx}"
121
+ while candidate in reserved:
122
+ idx += 1
123
+ candidate = f"{base}_{idx}"
124
+ return candidate