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.
- iaml/__init__.py +56 -0
- iaml/actionable.py +11 -0
- iaml/actionables/__init__.py +21 -0
- iaml/actionables/boosting/__init__.py +4 -0
- iaml/actionables/boosting/act_adaboost.py +59 -0
- iaml/actionables/cleaning/__init__.py +26 -0
- iaml/actionables/cleaning/act_categorical_imputer.py +124 -0
- iaml/actionables/cleaning/act_count_vectorizer.py +204 -0
- iaml/actionables/cleaning/act_drop_categorical_column.py +51 -0
- iaml/actionables/cleaning/act_drop_date_column.py +48 -0
- iaml/actionables/cleaning/act_drop_high_cardinality_categorical.py +337 -0
- iaml/actionables/cleaning/act_drop_numerical_column.py +75 -0
- iaml/actionables/cleaning/act_drop_textual_column.py +51 -0
- iaml/actionables/cleaning/act_encode_target_column.py +56 -0
- iaml/actionables/cleaning/act_frequency_encoder.py +127 -0
- iaml/actionables/cleaning/act_hashing_vectorizer.py +186 -0
- iaml/actionables/cleaning/act_knn_imputer.py +152 -0
- iaml/actionables/cleaning/act_mean_column.py +79 -0
- iaml/actionables/cleaning/act_mice.py +464 -0
- iaml/actionables/cleaning/act_missing_count_feature.py +109 -0
- iaml/actionables/cleaning/act_missing_indicator.py +124 -0
- iaml/actionables/cleaning/act_onehot.py +65 -0
- iaml/actionables/cleaning/act_ordinal_encoder.py +177 -0
- iaml/actionables/cleaning/act_rare_category_grouper.py +173 -0
- iaml/actionables/cleaning/act_simple_imputer.py +109 -0
- iaml/actionables/cleaning/act_split_date.py +68 -0
- iaml/actionables/cleaning/act_target_encoder.py +274 -0
- iaml/actionables/cleaning/act_text_normalizer.py +241 -0
- iaml/actionables/cleaning/act_tf_idf.py +80 -0
- iaml/actionables/cleaning/act_word2vec.py +150 -0
- iaml/actionables/features_precleaning/__init__.py +12 -0
- iaml/actionables/features_precleaning/act_coerce_numeric_strings.py +194 -0
- iaml/actionables/features_precleaning/act_date_converter.py +99 -0
- iaml/actionables/features_precleaning/act_drop_bad_quality_rows.py +77 -0
- iaml/actionables/features_precleaning/act_drop_duplicate_rows.py +131 -0
- iaml/actionables/features_precleaning/act_drop_high_missing_columns.py +94 -0
- iaml/actionables/features_precleaning/act_drop_id_like_columns.py +294 -0
- iaml/actionables/features_precleaning/act_normalize_column_names.py +157 -0
- iaml/actionables/features_precleaning/act_sentinel_to_na_n.py +270 -0
- iaml/actionables/features_precleaning/act_trim_space.py +79 -0
- iaml/actionables/features_preprocessing/__init__.py +18 -0
- iaml/actionables/features_preprocessing/act_cyclical_date_encoding.py +212 -0
- iaml/actionables/features_preprocessing/act_fast_ica.py +161 -0
- iaml/actionables/features_preprocessing/act_feature_agglomeration.py +90 -0
- iaml/actionables/features_preprocessing/act_k_bins_discretizer.py +207 -0
- iaml/actionables/features_preprocessing/act_k_means_features.py +296 -0
- iaml/actionables/features_preprocessing/act_kernel_pca.py +143 -0
- iaml/actionables/features_preprocessing/act_log_transformer.py +122 -0
- iaml/actionables/features_preprocessing/act_nystroem.py +100 -0
- iaml/actionables/features_preprocessing/act_pca.py +77 -0
- iaml/actionables/features_preprocessing/act_polynomial_features.py +86 -0
- iaml/actionables/features_preprocessing/act_power_transformer.py +106 -0
- iaml/actionables/features_preprocessing/act_quantile_transformer.py +114 -0
- iaml/actionables/features_preprocessing/act_rbf_sampler.py +88 -0
- iaml/actionables/features_preprocessing/act_select_percentile.py +112 -0
- iaml/actionables/features_preprocessing/act_sparse_random_projection.py +157 -0
- iaml/actionables/features_preprocessing/act_truncated_svd.py +137 -0
- iaml/actionables/features_selection/__init__.py +8 -0
- iaml/actionables/features_selection/act_permutation_importance_selector.py +421 -0
- iaml/actionables/features_selection/act_remove_high_correlated_column.py +70 -0
- iaml/actionables/features_selection/act_remove_low_variance_column.py +74 -0
- iaml/actionables/features_selection/act_rfe.py +214 -0
- iaml/actionables/features_selection/act_select_from_model.py +325 -0
- iaml/actionables/features_selection/act_select_k_best.py +181 -0
- iaml/actionables/features_selection/act_vif_selector.py +130 -0
- iaml/actionables/imbalance/__init__.py +10 -0
- iaml/actionables/imbalance/act_adasyn.py +150 -0
- iaml/actionables/imbalance/act_borderline_smote.py +171 -0
- iaml/actionables/imbalance/act_near_miss.py +158 -0
- iaml/actionables/imbalance/act_random_over_sampling.py +60 -0
- iaml/actionables/imbalance/act_random_under_sampler.py +135 -0
- iaml/actionables/imbalance/act_smote.py +162 -0
- iaml/actionables/imbalance/act_smote_tomek.py +182 -0
- iaml/actionables/imbalance/act_smoteenn.py +193 -0
- iaml/actionables/imbalance/act_tomek_links.py +138 -0
- iaml/actionables/normalize/__init__.py +6 -0
- iaml/actionables/normalize/act_max_abs_scaler.py +78 -0
- iaml/actionables/normalize/act_minmax_scaler.py +56 -0
- iaml/actionables/normalize/act_normalizer.py +95 -0
- iaml/actionables/normalize/act_robust_scaler.py +111 -0
- iaml/actionables/normalize/act_standard_scaler.py +55 -0
- iaml/actionables/predictors/__init__.py +6 -0
- iaml/actionables/predictors/_xgboost.py +16 -0
- iaml/actionables/predictors/classifier/__init__.py +26 -0
- iaml/actionables/predictors/classifier/act_bagging_classifier.py +113 -0
- iaml/actionables/predictors/classifier/act_bernoulli_nb.py +89 -0
- iaml/actionables/predictors/classifier/act_catboost_classifier.py +135 -0
- iaml/actionables/predictors/classifier/act_complement_nb.py +106 -0
- iaml/actionables/predictors/classifier/act_decision_tree_classifier.py +117 -0
- iaml/actionables/predictors/classifier/act_extra_trees_classifier.py +115 -0
- iaml/actionables/predictors/classifier/act_gaussian_nb.py +53 -0
- iaml/actionables/predictors/classifier/act_hist_gradient_boosting_classifier.py +144 -0
- iaml/actionables/predictors/classifier/act_knn.py +86 -0
- iaml/actionables/predictors/classifier/act_light_gbm_classifier.py +211 -0
- iaml/actionables/predictors/classifier/act_linear_discriminant_analysis.py +63 -0
- iaml/actionables/predictors/classifier/act_linear_svc.py +134 -0
- iaml/actionables/predictors/classifier/act_logistic_regression.py +92 -0
- iaml/actionables/predictors/classifier/act_mlp_classifier.py +107 -0
- iaml/actionables/predictors/classifier/act_multinomial_nb.py +76 -0
- iaml/actionables/predictors/classifier/act_passive_aggressive_classifier.py +141 -0
- iaml/actionables/predictors/classifier/act_quadratic_discriminant_analysis.py +72 -0
- iaml/actionables/predictors/classifier/act_randomforest.py +113 -0
- iaml/actionables/predictors/classifier/act_ridge_classifier.py +116 -0
- iaml/actionables/predictors/classifier/act_sgd_classifier.py +149 -0
- iaml/actionables/predictors/classifier/act_svm_svc.py +88 -0
- iaml/actionables/predictors/classifier/act_xgboost.py +111 -0
- iaml/actionables/predictors/regressor/__init__.py +27 -0
- iaml/actionables/predictors/regressor/act_ada_boost_regressor.py +75 -0
- iaml/actionables/predictors/regressor/act_ard_regression.py +95 -0
- iaml/actionables/predictors/regressor/act_catboost_regressor.py +134 -0
- iaml/actionables/predictors/regressor/act_decision_tree_regressor.py +111 -0
- iaml/actionables/predictors/regressor/act_elastic_net_regressor.py +109 -0
- iaml/actionables/predictors/regressor/act_extra_trees_regressor.py +113 -0
- iaml/actionables/predictors/regressor/act_gaussian_process_regressor.py +55 -0
- iaml/actionables/predictors/regressor/act_gboost_regressor.py +95 -0
- iaml/actionables/predictors/regressor/act_hist_gradient_boosting_regressor.py +105 -0
- iaml/actionables/predictors/regressor/act_huber_regressor.py +101 -0
- iaml/actionables/predictors/regressor/act_knn_regressor.py +86 -0
- iaml/actionables/predictors/regressor/act_lasso_regressor.py +103 -0
- iaml/actionables/predictors/regressor/act_light_gbm_regressor.py +201 -0
- iaml/actionables/predictors/regressor/act_linear_regression.py +43 -0
- iaml/actionables/predictors/regressor/act_mlp_regressor.py +104 -0
- iaml/actionables/predictors/regressor/act_poisson_regressor.py +111 -0
- iaml/actionables/predictors/regressor/act_quantile_regressor.py +87 -0
- iaml/actionables/predictors/regressor/act_randomforest_regressor.py +116 -0
- iaml/actionables/predictors/regressor/act_ransac_regressor.py +106 -0
- iaml/actionables/predictors/regressor/act_ridge_regressor.py +107 -0
- iaml/actionables/predictors/regressor/act_sgd_regressor.py +106 -0
- iaml/actionables/predictors/regressor/act_svm_svr.py +81 -0
- iaml/actionables/predictors/regressor/act_xgboost_regressor.py +97 -0
- iaml/actionables/predictors/survival/__init__.py +12 -0
- iaml/actionables/predictors/survival/act_aalen_additive_model.py +83 -0
- iaml/actionables/predictors/survival/act_cox.py +110 -0
- iaml/actionables/predictors/survival/act_coxnet_survival_analysis.py +134 -0
- iaml/actionables/predictors/survival/act_extra_survival_trees.py +101 -0
- iaml/actionables/predictors/survival/act_fast_survival_svm.py +102 -0
- iaml/actionables/predictors/survival/act_gradient_boosting_survival_analysis.py +93 -0
- iaml/actionables/predictors/survival/act_random_survival_forest.py +91 -0
- iaml/actionables/predictors/survival/act_survival_component_wise_gboost.py +80 -0
- iaml/actionables/predictors/survival/act_survival_tree.py +120 -0
- iaml/actionables/predictors/survival/act_survival_xgboost.py +9 -0
- iaml/actionables/predictors/survival/act_weibull_aft.py +230 -0
- iaml/cache.py +61 -0
- iaml/cache_keys.py +57 -0
- iaml/candidate.py +736 -0
- iaml/core_dispatcher.py +125 -0
- iaml/data_type.py +11 -0
- iaml/dataset.py +506 -0
- iaml/decorators/__init__.py +3 -0
- iaml/decorators/all.py +4 -0
- iaml/decorators/is_step.py +45 -0
- iaml/decorators/runner.py +100 -0
- iaml/explanation.py +112 -0
- iaml/iaml.py +1072 -0
- iaml/iaml_pipeline.py +600 -0
- iaml/logger.py +138 -0
- iaml/meta_explorer_step.py +62 -0
- iaml/meta_ordered_step.py +28 -0
- iaml/meta_partial_explorer_step.py +34 -0
- iaml/meta_singleton.py +24 -0
- iaml/metastep.py +211 -0
- iaml/metric.py +111 -0
- iaml/metric_plot.py +82 -0
- iaml/metrics/__init__.py +21 -0
- iaml/metrics/_classification.py +28 -0
- iaml/metrics/_survival_times.py +22 -0
- iaml/metrics/accuracy_metric.py +59 -0
- iaml/metrics/balanced_accuracy_metric.py +67 -0
- iaml/metrics/brier_score.py +90 -0
- iaml/metrics/classification_error_metric.py +66 -0
- iaml/metrics/concordance_index_ipcw.py +84 -0
- iaml/metrics/concordance_index_metric.py +67 -0
- iaml/metrics/cumulative_dynamic_auc.py +119 -0
- iaml/metrics/f1_score_metric.py +71 -0
- iaml/metrics/integrated_brier_score.py +98 -0
- iaml/metrics/integrated_brier_score_loss.py +41 -0
- iaml/metrics/mean_absolute_error_metric.py +46 -0
- iaml/metrics/mean_squared_error_metric.py +46 -0
- iaml/metrics/mean_squared_log_error_metric.py +49 -0
- iaml/metrics/median_absolute_error_metric.py +48 -0
- iaml/metrics/precision_metric.py +63 -0
- iaml/metrics/r2_score_metric.py +45 -0
- iaml/metrics/recall_metric.py +65 -0
- iaml/metrics/roc_auc_metric.py +50 -0
- iaml/metrics/specificity_metric.py +44 -0
- iaml/metrics/specificity_multiclass_metric.py +55 -0
- iaml/metrics/specificity_multilabel_metric.py +60 -0
- iaml/optimizers/__init__.py +5 -0
- iaml/optimizers/bayesian_optimizer.py +193 -0
- iaml/optimizers/genetic_optimizer.py +284 -0
- iaml/optimizers/optimizer.py +31 -0
- iaml/optimizers/random_optimizer.py +101 -0
- iaml/plot.py +138 -0
- iaml/plots/__init__.py +32 -0
- iaml/plots/bar_plot.py +141 -0
- iaml/plots/box_plot.py +166 -0
- iaml/plots/class_prediction_error_plot.py +37 -0
- iaml/plots/classification_report_plot.py +35 -0
- iaml/plots/confusion_matrix_plot.py +34 -0
- iaml/plots/correlation_heatmap_plot.py +201 -0
- iaml/plots/cumulative_hazard_plot.py +72 -0
- iaml/plots/density_plot.py +210 -0
- iaml/plots/histogram_plot.py +179 -0
- iaml/plots/kaplan_meier_comparison_plot.py +89 -0
- iaml/plots/line_plot.py +70 -0
- iaml/plots/missingness_heatmap_plot.py +203 -0
- iaml/plots/outlier_plot.py +217 -0
- iaml/plots/pair_plot.py +228 -0
- iaml/plots/precision_recall_curve_plot.py +86 -0
- iaml/plots/prediction_error_plot.py +34 -0
- iaml/plots/qq_plot.py +220 -0
- iaml/plots/residual_plot.py +38 -0
- iaml/plots/roc_dynamique_curve_plot.py +79 -0
- iaml/plots/rocauc_plot.py +96 -0
- iaml/plots/shap_plot.py +187 -0
- iaml/plots/target_distribution_plot.py +241 -0
- iaml/plots/violin_plot.py +206 -0
- iaml/predictor.py +139 -0
- iaml/reference.py +65 -0
- iaml/shared_cache.py +90 -0
- iaml/sklearn_preprocessor.py +74 -0
- iaml/splitters/__init__.py +3 -0
- iaml/splitters/kfold_splitter.py +32 -0
- iaml/splitters/random_splitter.py +26 -0
- iaml/stack.py +39 -0
- iaml/statistic.py +66 -0
- iaml/statistics/__init__.py +77 -0
- iaml/statistics/anova_statistic.py +80 -0
- iaml/statistics/cardinality_ratio_statistic.py +63 -0
- iaml/statistics/category_cooccurrence_statistic.py +79 -0
- iaml/statistics/chi_square_statistic.py +81 -0
- iaml/statistics/coef_variation_statistic.py +72 -0
- iaml/statistics/correlation_with_target.py +105 -0
- iaml/statistics/count.py +72 -0
- iaml/statistics/data_type_summary_statistic.py +74 -0
- iaml/statistics/duplicate_row_statistic.py +56 -0
- iaml/statistics/effect_size_statistic.py +129 -0
- iaml/statistics/entropy_statistic.py +69 -0
- iaml/statistics/event_rate_statistic.py +52 -0
- iaml/statistics/grouped_mean_statistic.py +60 -0
- iaml/statistics/iqr_statistic.py +66 -0
- iaml/statistics/kurtosis.py +50 -0
- iaml/statistics/mad_statistic.py +66 -0
- iaml/statistics/mean.py +61 -0
- iaml/statistics/median_statistic.py +61 -0
- iaml/statistics/minmax.py +60 -0
- iaml/statistics/missing_rate_statistic.py +62 -0
- iaml/statistics/mode.py +47 -0
- iaml/statistics/most_frequent_ratio.py +81 -0
- iaml/statistics/outlier_count_iqr_statistic.py +76 -0
- iaml/statistics/quantile.py +59 -0
- iaml/statistics/range.py +53 -0
- iaml/statistics/rare_category_rate.py +92 -0
- iaml/statistics/skewness.py +53 -0
- iaml/statistics/stdev.py +50 -0
- iaml/statistics/summary_table_statistic.py +60 -0
- iaml/statistics/time_by_group_statistic.py +83 -0
- iaml/statistics/time_summary_statistic.py +56 -0
- iaml/statistics/top_k_value_counts.py +68 -0
- iaml/statistics/unique_count_statistic.py +57 -0
- iaml/statistics/value_counts.py +63 -0
- iaml/statistics/variance.py +51 -0
- iaml/statistics/violin.py +63 -0
- iaml/step.py +600 -0
- iaml/step_cache.py +87 -0
- iaml/step_wrapper.py +79 -0
- iaml/timed_pool_executor.py +492 -0
- iaml/type_of_target.py +68 -0
- iaml/void_step.py +101 -0
- iaml/worker_manager.py +169 -0
- iaml/wrapper/__init__.py +4 -0
- iaml/wrapper/wrap_basic_gridsearch.py +68 -0
- iaml/wrapper/wrap_genetic_gridsearch.py +293 -0
- iaml/wrapper/wrap_iterative_gridsearch.py +399 -0
- pyiaml-1.0.0.dist-info/METADATA +802 -0
- pyiaml-1.0.0.dist-info/RECORD +279 -0
- pyiaml-1.0.0.dist-info/WHEEL +5 -0
- pyiaml-1.0.0.dist-info/licenses/LICENSE +674 -0
- 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
|