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
iaml/candidate.py
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
"""Candidate is used to exchange data between Steps """
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import traceback
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from math import isfinite
|
|
8
|
+
from numbers import Real
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
from copy import copy, deepcopy
|
|
11
|
+
from hashlib import md5
|
|
12
|
+
import textwrap
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from .dataset import Dataset
|
|
16
|
+
from .cache import Cache
|
|
17
|
+
from .cache_keys import hash_evaluation_context
|
|
18
|
+
from .splitters import random_splitter
|
|
19
|
+
from .iaml_pipeline import IAMLPipeline
|
|
20
|
+
from .metric_plot import MetricPlot
|
|
21
|
+
from .logger import Logger
|
|
22
|
+
from .step_cache import StepCache
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .metric import Metric
|
|
26
|
+
from .step import Step
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Candidate:
|
|
30
|
+
"""Candidate of every IAML Step
|
|
31
|
+
|
|
32
|
+
:param Dataset, optional dataset: Dataset being built. Default to None.
|
|
33
|
+
:param list[Metric], optional metrics: List of metrics used to evaluate models. Default to None.
|
|
34
|
+
:param IAMLPipeline, optional iaml_pipeline: Pipeline being built. Default to None.
|
|
35
|
+
:param list, optional stacked_path: Stack of all steps used to build this Candidate.
|
|
36
|
+
Default to None.
|
|
37
|
+
:param main_metric: Metric or metric name used to rank candidates.
|
|
38
|
+
If None, use the default metric for the task.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
dataset: Dataset = None,
|
|
44
|
+
metrics: list[Metric] = None,
|
|
45
|
+
iaml_pipeline: IAMLPipeline = None,
|
|
46
|
+
stacked_path: list = None,
|
|
47
|
+
main_metric: Metric | str | None = None) -> None:
|
|
48
|
+
|
|
49
|
+
self.dataset: Dataset = dataset
|
|
50
|
+
"""Dataset used for this candidate"""
|
|
51
|
+
|
|
52
|
+
self.metrics: list[Metric] = copy(metrics) if metrics is not None else []
|
|
53
|
+
"""List of metrics used to evaluate the model"""
|
|
54
|
+
|
|
55
|
+
if iaml_pipeline is not None:
|
|
56
|
+
self.pipeline = iaml_pipeline
|
|
57
|
+
else:
|
|
58
|
+
self.pipeline = IAMLPipeline(
|
|
59
|
+
estimator_type=dataset.needed_estimator,
|
|
60
|
+
original_dataset=dataset.X.copy()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if main_metric is None:
|
|
64
|
+
if self.pipeline.estimator_type == "classifier":
|
|
65
|
+
self.main_metric = 'balanced_accuracy'
|
|
66
|
+
elif self.pipeline.estimator_type == "survival":
|
|
67
|
+
self.main_metric = 'concordance_index_ipcw'
|
|
68
|
+
else:
|
|
69
|
+
self.main_metric = 'r2_score'
|
|
70
|
+
else:
|
|
71
|
+
self.main_metric = main_metric
|
|
72
|
+
|
|
73
|
+
self.computed_metrics: dict = {}
|
|
74
|
+
"""Result dictionnary for all the metrics computed"""
|
|
75
|
+
|
|
76
|
+
self.fold_metrics: list[dict[str, Any]] = []
|
|
77
|
+
"""Per-fold metrics computed during the latest internal cross-validation."""
|
|
78
|
+
|
|
79
|
+
self.training_audit: dict[str, Any] | None = None
|
|
80
|
+
"""Structured audit payload for the latest training evaluation."""
|
|
81
|
+
|
|
82
|
+
self.stacked_path: list = copy(stacked_path) if stacked_path is not None else []
|
|
83
|
+
"""Stack of all steps used to build this Candidate"""
|
|
84
|
+
|
|
85
|
+
def add_stack(self, stack: 'Step') -> None:
|
|
86
|
+
"""Add a step to the stack
|
|
87
|
+
|
|
88
|
+
:param Step stack: Step to add
|
|
89
|
+
"""
|
|
90
|
+
self.stacked_path.append(stack)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def main_metric(self) -> str:
|
|
94
|
+
"""Name of the metric used to rank candidates."""
|
|
95
|
+
return str(self._main_metric)
|
|
96
|
+
|
|
97
|
+
@main_metric.setter
|
|
98
|
+
def main_metric(self, metric: Metric | str) -> None:
|
|
99
|
+
self._main_metric = metric
|
|
100
|
+
|
|
101
|
+
def get_main_metric(self) -> Metric | str:
|
|
102
|
+
"""Return the metric definition, or its name when no definition is available."""
|
|
103
|
+
if isinstance(self._main_metric, str):
|
|
104
|
+
return next(
|
|
105
|
+
(metric for metric in self.metrics if str(metric) == self.main_metric),
|
|
106
|
+
self._main_metric,
|
|
107
|
+
)
|
|
108
|
+
return self._main_metric
|
|
109
|
+
|
|
110
|
+
def get_main_metric_value(self) -> float:
|
|
111
|
+
"""Get computed value of the main metric from saved metrics
|
|
112
|
+
|
|
113
|
+
:return: Main metric value
|
|
114
|
+
"""
|
|
115
|
+
if self.computed_metrics and self.main_metric in self.computed_metrics:
|
|
116
|
+
return self.computed_metrics[self.main_metric]
|
|
117
|
+
return -1
|
|
118
|
+
|
|
119
|
+
def get_main_metric_score(self) -> float:
|
|
120
|
+
"""Return a ranking score where higher is better, preserving raw metrics.
|
|
121
|
+
|
|
122
|
+
Metric names are resolved against the candidate's evaluation metrics.
|
|
123
|
+
A candidate without its main metric always ranks below an evaluated one.
|
|
124
|
+
"""
|
|
125
|
+
if self.main_metric not in self.computed_metrics:
|
|
126
|
+
return float('-inf')
|
|
127
|
+
metric = self.get_main_metric()
|
|
128
|
+
value = self.get_main_metric_value()
|
|
129
|
+
return value if getattr(metric, 'greater_is_better', True) else -value
|
|
130
|
+
|
|
131
|
+
def __gt__(self, other: 'Candidate') -> bool:
|
|
132
|
+
"""Check if a candidate is greater than another by various methods.
|
|
133
|
+
|
|
134
|
+
- Main Metric Value
|
|
135
|
+
- Presence of computed metrics
|
|
136
|
+
- id of the candidate
|
|
137
|
+
|
|
138
|
+
:param Candidate other: The other Candidate to compare to.
|
|
139
|
+
:return: Greater than?
|
|
140
|
+
"""
|
|
141
|
+
if self.computed_metrics and other.computed_metrics:
|
|
142
|
+
return self.get_main_metric_score() > other.get_main_metric_score()
|
|
143
|
+
if self.computed_metrics:
|
|
144
|
+
return True
|
|
145
|
+
if other.computed_metrics:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
return id(self) > id(other)
|
|
149
|
+
|
|
150
|
+
def __lt__(self, other: 'Candidate'):
|
|
151
|
+
"""Check if a candidate is less than another by various methods.
|
|
152
|
+
|
|
153
|
+
- Main Metric Value
|
|
154
|
+
- Presence of computed metrics
|
|
155
|
+
- id of the candidate
|
|
156
|
+
|
|
157
|
+
:param Candidate other: The other Candidate to compare to.
|
|
158
|
+
:return: Less than?
|
|
159
|
+
"""
|
|
160
|
+
if self.computed_metrics and other.computed_metrics:
|
|
161
|
+
return self.get_main_metric_score() < other.get_main_metric_score()
|
|
162
|
+
if self.computed_metrics:
|
|
163
|
+
return False
|
|
164
|
+
if other.computed_metrics:
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
return id(self) < id(other)
|
|
168
|
+
|
|
169
|
+
def __eq__(self, other: 'Candidate'):
|
|
170
|
+
"""Check if a candidate is equal to another by various methods.
|
|
171
|
+
|
|
172
|
+
- Main Metric Value
|
|
173
|
+
- Presence of computed metrics
|
|
174
|
+
- id of the candidate
|
|
175
|
+
|
|
176
|
+
:param Candidate other: The other Candidate to compare to.
|
|
177
|
+
:return: Equal to?
|
|
178
|
+
"""
|
|
179
|
+
if self.computed_metrics and other.computed_metrics:
|
|
180
|
+
return self.get_main_metric_score() == other.get_main_metric_score()
|
|
181
|
+
|
|
182
|
+
return id(self) == id(other)
|
|
183
|
+
|
|
184
|
+
def to_output(
|
|
185
|
+
self,
|
|
186
|
+
dataset: Dataset = None,
|
|
187
|
+
metrics: list[Metric] = None,
|
|
188
|
+
iaml_pipeline: IAMLPipeline = None) -> 'Candidate':
|
|
189
|
+
"""Create a copy of the current instance, preserving its ranking metric.
|
|
190
|
+
|
|
191
|
+
:param Dataset, optional dataset: Replace current dataset. Defaults to None.
|
|
192
|
+
:param Metric, optional metrics: Replace current metrics. Defaults to None.
|
|
193
|
+
:param IAMLPipeline, optional iaml_pipeline: Replace current pipeline. Defaults to None.
|
|
194
|
+
:return: New Candidate
|
|
195
|
+
"""
|
|
196
|
+
logger = None
|
|
197
|
+
diag_enabled = os.environ.get("IAML_DIAG", "").lower() in ["1", "true", "yes"]
|
|
198
|
+
if diag_enabled:
|
|
199
|
+
from .logger import Logger # pylint: disable=import-outside-toplevel
|
|
200
|
+
logger = Logger()
|
|
201
|
+
if logger.verbose <= 1:
|
|
202
|
+
logger = None
|
|
203
|
+
|
|
204
|
+
pipeline_copy_time = 0.0
|
|
205
|
+
dataset_copy_time = 0.0
|
|
206
|
+
if iaml_pipeline is None:
|
|
207
|
+
start = time.perf_counter()
|
|
208
|
+
iaml_pipeline = self.pipeline.copy()
|
|
209
|
+
pipeline_copy_time = time.perf_counter() - start
|
|
210
|
+
|
|
211
|
+
if dataset is None:
|
|
212
|
+
start = time.perf_counter()
|
|
213
|
+
dataset = deepcopy(self.dataset)
|
|
214
|
+
dataset_copy_time = time.perf_counter() - start
|
|
215
|
+
|
|
216
|
+
if logger is not None:
|
|
217
|
+
try:
|
|
218
|
+
steps = self.pipeline.training_steps
|
|
219
|
+
step_count = len(steps)
|
|
220
|
+
cache_count = 0
|
|
221
|
+
step_cache = StepCache()
|
|
222
|
+
for _, step in steps:
|
|
223
|
+
if hasattr(step, "_cache_id"):
|
|
224
|
+
cache_count += step_cache.size_for_step(step._cache_id)
|
|
225
|
+
elif hasattr(step, "caches") and step.caches is not None:
|
|
226
|
+
cache_count += len(step.caches)
|
|
227
|
+
candidate_refs = 0
|
|
228
|
+
for _, step in steps:
|
|
229
|
+
if hasattr(step, "candidate") and step.candidate is not None:
|
|
230
|
+
if isinstance(step.candidate, list):
|
|
231
|
+
candidate_refs += len(step.candidate)
|
|
232
|
+
else:
|
|
233
|
+
candidate_refs += 1
|
|
234
|
+
except Exception: # pylint: disable=broad-except
|
|
235
|
+
step_count = None
|
|
236
|
+
cache_count = None
|
|
237
|
+
candidate_refs = None
|
|
238
|
+
|
|
239
|
+
logger.info(
|
|
240
|
+
"diag: to_output copy pipeline=%.3fs dataset=%.3fs steps=%s caches=%s candidates=%s",
|
|
241
|
+
pipeline_copy_time,
|
|
242
|
+
dataset_copy_time,
|
|
243
|
+
step_count,
|
|
244
|
+
cache_count,
|
|
245
|
+
candidate_refs,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return Candidate(
|
|
249
|
+
dataset,
|
|
250
|
+
metrics or copy(self.metrics),
|
|
251
|
+
iaml_pipeline=iaml_pipeline,
|
|
252
|
+
stacked_path=self.stacked_path,
|
|
253
|
+
main_metric=self._main_metric)
|
|
254
|
+
|
|
255
|
+
def to_input(self,
|
|
256
|
+
dataset: Dataset = None,
|
|
257
|
+
metrics: list[Metric] = None,
|
|
258
|
+
iaml_pipeline: IAMLPipeline = None) -> 'Candidate':
|
|
259
|
+
"""Create a copy of current instance and assign parameters values to attributes
|
|
260
|
+
|
|
261
|
+
:param Dataset, optional dataset: Replace current dataset. Defaults to None.
|
|
262
|
+
:param Metric, optional metrics: Replace current metrics. Defaults to None.
|
|
263
|
+
:param IAMLPipeline, optional iaml_pipeline: Replace current pipeline. Defaults to None.
|
|
264
|
+
:return: New Candidate
|
|
265
|
+
"""
|
|
266
|
+
return self.to_output(dataset, metrics, iaml_pipeline)
|
|
267
|
+
|
|
268
|
+
def add_to_pipeline(self, instance: 'Step') -> 'Candidate':
|
|
269
|
+
"""Add a Step to prediction Pipeline.
|
|
270
|
+
|
|
271
|
+
The instance must implement one of these methods:
|
|
272
|
+
|
|
273
|
+
- ``transform(X)``: Apply column transformations to the dataset.
|
|
274
|
+
- ``predict(X)``: Predict values with an AI model.
|
|
275
|
+
- ``resample(X, y)``: Apply row transformations to the training dataset.
|
|
276
|
+
|
|
277
|
+
:param Step instance: Add a step to the pipeline.
|
|
278
|
+
:return: New Candidate
|
|
279
|
+
"""
|
|
280
|
+
if self.pipeline is not None:
|
|
281
|
+
if hasattr(instance, 'transform') and callable(instance.transform):
|
|
282
|
+
self.dataset.transform(instance.transform)
|
|
283
|
+
self.pipeline.add_transform(instance)
|
|
284
|
+
elif hasattr(instance, 'predict') and callable(instance.predict):
|
|
285
|
+
self.pipeline.set_model(instance)
|
|
286
|
+
elif hasattr(instance, 'resample') and callable(instance.resample):
|
|
287
|
+
self.pipeline.add_resample(instance)
|
|
288
|
+
# Resample in Dataset used in pipeline generation step
|
|
289
|
+
self.dataset = self.dataset.resample(instance.resample)
|
|
290
|
+
|
|
291
|
+
return self.to_output()
|
|
292
|
+
|
|
293
|
+
def add_metric(self, metric: 'Metric') -> None:
|
|
294
|
+
"""Add a new Metric to evaluate models
|
|
295
|
+
|
|
296
|
+
:param Metric metric: metric to add
|
|
297
|
+
"""
|
|
298
|
+
self.metrics.append(metric)
|
|
299
|
+
|
|
300
|
+
def __str__(self) -> str:
|
|
301
|
+
"""String representation of the Candidate
|
|
302
|
+
|
|
303
|
+
:return: String representatio n of the candidate.
|
|
304
|
+
"""
|
|
305
|
+
name = [name for name, _ in self.pipeline.steps]
|
|
306
|
+
main_metric = self.get_main_metric_value()
|
|
307
|
+
if main_metric:
|
|
308
|
+
name = f"{main_metric} : {name}"
|
|
309
|
+
return name
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def __serialize_audit_value(cls, value: Any) -> Any:
|
|
313
|
+
"""Convert runtime values into JSON-friendly audit payloads."""
|
|
314
|
+
if isinstance(value, np.generic):
|
|
315
|
+
return value.item()
|
|
316
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
317
|
+
return value
|
|
318
|
+
if isinstance(value, dict):
|
|
319
|
+
return {
|
|
320
|
+
str(key): cls.__serialize_audit_value(current)
|
|
321
|
+
for key, current in value.items()
|
|
322
|
+
}
|
|
323
|
+
if isinstance(value, (list, tuple, set)):
|
|
324
|
+
return [cls.__serialize_audit_value(current) for current in value]
|
|
325
|
+
if callable(value):
|
|
326
|
+
return getattr(value, "__name__", str(value))
|
|
327
|
+
return str(value)
|
|
328
|
+
|
|
329
|
+
def __serialize_metric_values(self, values: dict[str, Any]) -> dict[str, Any]:
|
|
330
|
+
"""Convert metric outputs into a stable, serializable format."""
|
|
331
|
+
return {
|
|
332
|
+
str(name): self.__serialize_audit_value(value)
|
|
333
|
+
for name, value in values.items()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def __valid_main_metric(self, scores: dict[str, Any]) -> bool:
|
|
337
|
+
"""A main metric must be a finite numeric scalar; zero remains valid."""
|
|
338
|
+
value = scores.get(self.main_metric)
|
|
339
|
+
try:
|
|
340
|
+
return isinstance(value, Real) and isfinite(value)
|
|
341
|
+
except (TypeError, ValueError, OverflowError):
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
def __aggregate_metrics(self, fold_results: list[dict[str, Any]]) -> dict[str, float]:
|
|
345
|
+
"""Aggregate fold-level metric dictionaries into global scores."""
|
|
346
|
+
computed_metrics: dict[str, float] = {}
|
|
347
|
+
for metric in self.metrics:
|
|
348
|
+
name = str(metric)
|
|
349
|
+
values = [
|
|
350
|
+
metric_values.get(name)
|
|
351
|
+
for metric_values in fold_results
|
|
352
|
+
if name in metric_values
|
|
353
|
+
]
|
|
354
|
+
if values:
|
|
355
|
+
computed_metrics[name] = float(np.mean([
|
|
356
|
+
value if value is not None else 0 for value in values
|
|
357
|
+
]))
|
|
358
|
+
else:
|
|
359
|
+
computed_metrics[name] = 0
|
|
360
|
+
return computed_metrics
|
|
361
|
+
|
|
362
|
+
def pipeline_audit_summary(self) -> dict[str, Any]:
|
|
363
|
+
"""Return a serializable summary of the pipeline steps and their config."""
|
|
364
|
+
summarized_steps: list[dict[str, Any]] = []
|
|
365
|
+
for name, step in self.pipeline.training_steps:
|
|
366
|
+
if self.pipeline.predictor is not None and step is self.pipeline.predictor[1]:
|
|
367
|
+
role = "predictor"
|
|
368
|
+
elif callable(getattr(step, 'transform', None)):
|
|
369
|
+
role = "transformer"
|
|
370
|
+
else:
|
|
371
|
+
role = "resampler"
|
|
372
|
+
summarized_steps.append(
|
|
373
|
+
{
|
|
374
|
+
"role": role,
|
|
375
|
+
"name": name,
|
|
376
|
+
"class": step.__class__.__name__,
|
|
377
|
+
"tags": sorted(step.tags) if step.tags else [],
|
|
378
|
+
"configuration": self.__serialize_audit_value(
|
|
379
|
+
step.resume_configuration()
|
|
380
|
+
),
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
"fingerprint": self.pipeline.fingerprint(),
|
|
386
|
+
"estimator_type": self.pipeline.estimator_type,
|
|
387
|
+
"steps": summarized_steps,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
def build_training_audit(
|
|
391
|
+
self,
|
|
392
|
+
dataset: Dataset,
|
|
393
|
+
fold_metrics: list[dict[str, Any]],
|
|
394
|
+
aggregated_metrics: dict[str, Any],
|
|
395
|
+
status: str,
|
|
396
|
+
error: str | None = None,
|
|
397
|
+
) -> dict[str, Any]:
|
|
398
|
+
"""Build a structured record for later audit on the IAML object."""
|
|
399
|
+
return {
|
|
400
|
+
"pipeline_fingerprint": self.pipeline.fingerprint(),
|
|
401
|
+
"dataset_fingerprint": dataset.fingerprint(),
|
|
402
|
+
"dataset_shape": {
|
|
403
|
+
"rows": int(dataset.X.shape[0]),
|
|
404
|
+
"columns": int(dataset.X.shape[1]),
|
|
405
|
+
},
|
|
406
|
+
"main_metric": self.main_metric,
|
|
407
|
+
"status": status,
|
|
408
|
+
"error": error,
|
|
409
|
+
"metrics": self.__serialize_metric_values(aggregated_metrics),
|
|
410
|
+
"fold_metrics": deepcopy(fold_metrics),
|
|
411
|
+
"pipeline": self.pipeline_audit_summary(),
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
def training_evaluate(
|
|
415
|
+
self,
|
|
416
|
+
dataset: Dataset,
|
|
417
|
+
splitter: callable = random_splitter,
|
|
418
|
+
cache_split: bool = True,
|
|
419
|
+
store_audit: bool = False) -> dict:
|
|
420
|
+
"""Evaluate pipeline model with self.metrics on dataset
|
|
421
|
+
If evaluate is called in training process, result will be cached in
|
|
422
|
+
self.computed_metrics.
|
|
423
|
+
|
|
424
|
+
:param Dataset dataset: Dataset used to compute metrics results
|
|
425
|
+
:param callable, optional splitter: The split method to be used. Default to random_splitter.
|
|
426
|
+
:return: Metric name as key and result as value
|
|
427
|
+
"""
|
|
428
|
+
self.computed_metrics = {}
|
|
429
|
+
self.fold_metrics = []
|
|
430
|
+
self.training_audit = None
|
|
431
|
+
if not self.pipeline.have_model:
|
|
432
|
+
return None
|
|
433
|
+
metrics: list[dict] = []
|
|
434
|
+
fold_metrics: list[dict[str, Any]] = []
|
|
435
|
+
|
|
436
|
+
splitter_fingerprint = (
|
|
437
|
+
hash_evaluation_context(splitter) if cache_split else None
|
|
438
|
+
)
|
|
439
|
+
cache_key = (
|
|
440
|
+
f"splits_{self.fingerprint()}_{splitter_fingerprint}"
|
|
441
|
+
if splitter_fingerprint is not None else None
|
|
442
|
+
)
|
|
443
|
+
dataset_key = dataset.fingerprint() if cache_key else None
|
|
444
|
+
from_cache = False
|
|
445
|
+
to_cache: list[tuple[Dataset, Dataset]] = []
|
|
446
|
+
splitted_datasets = Cache().from_cache(cache_key, dataset_key) if cache_key else None
|
|
447
|
+
if splitted_datasets:
|
|
448
|
+
from_cache = True
|
|
449
|
+
else:
|
|
450
|
+
splitted_datasets = splitter(dataset)
|
|
451
|
+
|
|
452
|
+
for fold_index, (train_ds, test_ds) in enumerate(splitted_datasets, start=1):
|
|
453
|
+
copied_pipe = deepcopy(self.pipeline)
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
# Fit in two steps to allow caching.
|
|
457
|
+
if not from_cache:
|
|
458
|
+
train_ds = train_ds.decline(*copied_pipe.fit_transform(train_ds.X, train_ds.y))
|
|
459
|
+
test_ds = test_ds.decline(copied_pipe.transform(test_ds.X), test_ds.y)
|
|
460
|
+
|
|
461
|
+
copied_pipe.fit(train_ds.X, train_ds.y, only_predictor=True)
|
|
462
|
+
except (ValueError, np.linalg.LinAlgError) as exc:
|
|
463
|
+
Logger().warning(
|
|
464
|
+
f"Skip candidate {self._pipeline_signature()} after training failure: {exc!r}"
|
|
465
|
+
)
|
|
466
|
+
if store_audit:
|
|
467
|
+
self.training_audit = self.build_training_audit(
|
|
468
|
+
dataset=dataset,
|
|
469
|
+
fold_metrics=fold_metrics,
|
|
470
|
+
aggregated_metrics={},
|
|
471
|
+
status="failed",
|
|
472
|
+
error=f"training failure: {exc!r}",
|
|
473
|
+
)
|
|
474
|
+
return {}
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
fold_result = self.__compute_metrics(
|
|
478
|
+
test_ds.X,
|
|
479
|
+
test_ds.y,
|
|
480
|
+
pipeline=copied_pipe,
|
|
481
|
+
X_train=train_ds.X,
|
|
482
|
+
y_train=train_ds.y,
|
|
483
|
+
model_only=True)
|
|
484
|
+
if not self.__valid_main_metric(fold_result):
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"Main metric '{self.main_metric}' is missing or invalid on fold {fold_index}"
|
|
487
|
+
)
|
|
488
|
+
metrics.append(fold_result)
|
|
489
|
+
if store_audit:
|
|
490
|
+
fold_metrics.append(
|
|
491
|
+
{
|
|
492
|
+
"fold": fold_index,
|
|
493
|
+
"train_shape": {
|
|
494
|
+
"rows": int(train_ds.X.shape[0]),
|
|
495
|
+
"columns": int(train_ds.X.shape[1]),
|
|
496
|
+
},
|
|
497
|
+
"test_shape": {
|
|
498
|
+
"rows": int(test_ds.X.shape[0]),
|
|
499
|
+
"columns": int(test_ds.X.shape[1]),
|
|
500
|
+
},
|
|
501
|
+
"metrics": self.__serialize_metric_values(fold_result),
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
if cache_key and not from_cache:
|
|
505
|
+
to_cache.append((train_ds, test_ds))
|
|
506
|
+
except ValueError as exc:
|
|
507
|
+
Logger().warning(
|
|
508
|
+
f"Skip candidate {self._pipeline_signature()} after metric failure: {exc!r}"
|
|
509
|
+
)
|
|
510
|
+
if store_audit:
|
|
511
|
+
self.training_audit = self.build_training_audit(
|
|
512
|
+
dataset=dataset,
|
|
513
|
+
fold_metrics=fold_metrics,
|
|
514
|
+
aggregated_metrics={},
|
|
515
|
+
status="failed",
|
|
516
|
+
error=f"metric failure: {exc!r}",
|
|
517
|
+
)
|
|
518
|
+
return {}
|
|
519
|
+
|
|
520
|
+
if cache_key and not from_cache:
|
|
521
|
+
Cache().add_to_cache(cache_key, dataset_key, to_cache)
|
|
522
|
+
|
|
523
|
+
computed_metrics = self.__aggregate_metrics(metrics) if metrics else {}
|
|
524
|
+
if not self.__valid_main_metric(computed_metrics):
|
|
525
|
+
if store_audit:
|
|
526
|
+
self.training_audit = self.build_training_audit(
|
|
527
|
+
dataset, fold_metrics, {}, status="failed",
|
|
528
|
+
error=f"Main metric '{self.main_metric}' has no valid aggregate",
|
|
529
|
+
)
|
|
530
|
+
return {}
|
|
531
|
+
self.computed_metrics = computed_metrics
|
|
532
|
+
if store_audit:
|
|
533
|
+
self.fold_metrics = deepcopy(fold_metrics)
|
|
534
|
+
self.training_audit = self.build_training_audit(
|
|
535
|
+
dataset=dataset,
|
|
536
|
+
fold_metrics=fold_metrics,
|
|
537
|
+
aggregated_metrics=computed_metrics,
|
|
538
|
+
status="success",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return self.computed_metrics
|
|
542
|
+
|
|
543
|
+
def evaluate(self, X: pd.DataFrame, y: np.ndarray) -> dict:
|
|
544
|
+
"""Evaluate pipeline performances with self.metrics
|
|
545
|
+
|
|
546
|
+
:param pd.DataFrame X: Features.
|
|
547
|
+
:param np.ndarray y: label.
|
|
548
|
+
:return: Computed metrics.
|
|
549
|
+
"""
|
|
550
|
+
if not self.pipeline.have_model:
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
return self.__compute_metrics(X, np.array(y))
|
|
554
|
+
|
|
555
|
+
def __compute_metrics(
|
|
556
|
+
self,
|
|
557
|
+
X_test: pd.DataFrame,
|
|
558
|
+
y_test: np.ndarray,
|
|
559
|
+
pipeline : IAMLPipeline = None,
|
|
560
|
+
X_train: pd.DataFrame = None,
|
|
561
|
+
y_train: np.ndarray = None,
|
|
562
|
+
**kwargs) -> dict:
|
|
563
|
+
"""Perform metrics computation
|
|
564
|
+
|
|
565
|
+
:param pd.DataFrame X_test: The dataframe to compute metrics on.
|
|
566
|
+
:param np.ndarray y_test: The dataframe labels used for evaluation.
|
|
567
|
+
:param IAMLPipeline, optional: The pipeline to use for evaludation. Default to None.
|
|
568
|
+
:param pd.DataFrame, optional X_train: DataFrame used for metrics comparison.
|
|
569
|
+
Default to None.
|
|
570
|
+
:param np.ndarray, optional y_train: DataFrame labels used for metrics comparison.
|
|
571
|
+
Default to None.
|
|
572
|
+
:param dict, optional \\**kwargs: Additional parameters.
|
|
573
|
+
:return: computed metrics
|
|
574
|
+
"""
|
|
575
|
+
if pipeline is None: # Is no pipeline in args -> Use the main one
|
|
576
|
+
pipeline = self.pipeline
|
|
577
|
+
|
|
578
|
+
X_test = X_test.copy(deep=True)
|
|
579
|
+
y_test = deepcopy(y_test)
|
|
580
|
+
|
|
581
|
+
if y_train is None or X_train is None:
|
|
582
|
+
y_train=self.dataset.y
|
|
583
|
+
X_train=self.dataset.X
|
|
584
|
+
|
|
585
|
+
computed = {}
|
|
586
|
+
needs = {metric.needed_prediction for metric in self.metrics}
|
|
587
|
+
|
|
588
|
+
for need in needs:
|
|
589
|
+
try:
|
|
590
|
+
method = getattr(pipeline, need)
|
|
591
|
+
y_pred = method(X_test, **kwargs)
|
|
592
|
+
for metric in self.metrics:
|
|
593
|
+
try:
|
|
594
|
+
if metric.needed_prediction == need:
|
|
595
|
+
computed[str(metric)] = metric.compute(
|
|
596
|
+
y_test,
|
|
597
|
+
y_pred,
|
|
598
|
+
y_train=y_train,
|
|
599
|
+
X_train=X_train
|
|
600
|
+
)
|
|
601
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
602
|
+
Logger().error(traceback.format_exc())
|
|
603
|
+
except AttributeError:
|
|
604
|
+
Logger().warning(
|
|
605
|
+
f"Pipeline {self._pipeline_signature()} does not expose '{need}' needed by metrics."
|
|
606
|
+
)
|
|
607
|
+
return computed
|
|
608
|
+
|
|
609
|
+
def __metric_value(self, metric: Metric) -> float | None:
|
|
610
|
+
"""Get a specific metric value
|
|
611
|
+
|
|
612
|
+
:param Metric metric: The metric we want to get the value.
|
|
613
|
+
:return: The metric value or None if this Metric doesn't exsists.
|
|
614
|
+
"""
|
|
615
|
+
for key, value in self.computed_metrics.items():
|
|
616
|
+
if key == str(metric):
|
|
617
|
+
return value
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
def bibliography(self, structured: bool = False) -> str | list[dict]:
|
|
621
|
+
"""Format a string with all step's referencies
|
|
622
|
+
|
|
623
|
+
:param bool, optional structured: Str or JSON serializable bibliography. Default to Str.
|
|
624
|
+
:return: Formatted bibliography or structured bibliography.
|
|
625
|
+
"""
|
|
626
|
+
return self.pipeline.bibliography(structured)
|
|
627
|
+
|
|
628
|
+
def fingerprint(self) -> str:
|
|
629
|
+
"""Generate a fingerprint to identify this instance
|
|
630
|
+
|
|
631
|
+
:return: String fingerprint
|
|
632
|
+
"""
|
|
633
|
+
if hasattr(self.pipeline, "transformers_resamplers_fingerprint"):
|
|
634
|
+
return self.pipeline.transformers_resamplers_fingerprint()
|
|
635
|
+
|
|
636
|
+
to_hash = "\n".join([
|
|
637
|
+
step.fingerprint()
|
|
638
|
+
for _, step in [*self.pipeline.transformers, *self.pipeline.resamplers]
|
|
639
|
+
])
|
|
640
|
+
return md5(to_hash.encode()).hexdigest()
|
|
641
|
+
|
|
642
|
+
def _pipeline_signature(self) -> str:
|
|
643
|
+
"""Return a short, logging-safe pipeline identifier."""
|
|
644
|
+
try:
|
|
645
|
+
names = [name for name, _ in self.pipeline.training_steps if name]
|
|
646
|
+
if names:
|
|
647
|
+
return " -> ".join(names)
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
650
|
+
return getattr(self.pipeline, "name", self.pipeline.__class__.__name__)
|
|
651
|
+
|
|
652
|
+
def describe_metrics(self) -> str:
|
|
653
|
+
"""Explain all metrics
|
|
654
|
+
|
|
655
|
+
:return: Markdown table of all metrics.
|
|
656
|
+
"""
|
|
657
|
+
metrics = '\n '.join([
|
|
658
|
+
f'| `{m}` | **{self.__metric_value(m):.4f}** | *{m.explain()}* |'
|
|
659
|
+
for m in self.metrics
|
|
660
|
+
])
|
|
661
|
+
|
|
662
|
+
return textwrap.dedent(f'''\
|
|
663
|
+
### Results
|
|
664
|
+
| Metric name | Computed value | Description |
|
|
665
|
+
| ----------- | -------------- | ----------- |
|
|
666
|
+
{metrics}
|
|
667
|
+
''') if len(metrics) > 0 else ""
|
|
668
|
+
|
|
669
|
+
def describe_steps(self) -> list[str]:
|
|
670
|
+
"""Explain all steps
|
|
671
|
+
|
|
672
|
+
:return: List of explanation strings for each step.
|
|
673
|
+
"""
|
|
674
|
+
return self.pipeline.explanations
|
|
675
|
+
|
|
676
|
+
def explain_model_performance(
|
|
677
|
+
self,
|
|
678
|
+
X_test: pd.DataFrame,
|
|
679
|
+
y_test: list,
|
|
680
|
+
X_train: pd.DataFrame = None,
|
|
681
|
+
y_train: list = None,
|
|
682
|
+
**kwargs) -> list[MetricPlot]:
|
|
683
|
+
"""Return a list of plot that explain models performances
|
|
684
|
+
|
|
685
|
+
:param pd.DataFrame X_test: Features.
|
|
686
|
+
:param list y_test: Target.
|
|
687
|
+
:param pd.DataFrame, optional X_train: Train features. Default to None.
|
|
688
|
+
:param list, optional y_train: Train target. Default to None.
|
|
689
|
+
:param dict, optional \\**kwargs: Additional Parameters.
|
|
690
|
+
:return: List of plot instances.
|
|
691
|
+
"""
|
|
692
|
+
if isinstance(y_test, pd.DataFrame):
|
|
693
|
+
y_test = y_test[y_test.columns[0]]
|
|
694
|
+
if isinstance(y_train, pd.DataFrame):
|
|
695
|
+
y_train = y_train[y_train.columns[0]]
|
|
696
|
+
|
|
697
|
+
plots = []
|
|
698
|
+
for plot_sub_class in MetricPlot.__subclasses__():
|
|
699
|
+
# Verify if a subclass is suitable or not
|
|
700
|
+
if plot_sub_class.suitable(self.dataset.type_of_target):
|
|
701
|
+
plot = plot_sub_class().compute(self.pipeline,
|
|
702
|
+
deepcopy(X_test), deepcopy(y_test),
|
|
703
|
+
X_train=deepcopy(X_train), y_train=deepcopy(y_train), **kwargs)
|
|
704
|
+
plots.append(plot)
|
|
705
|
+
|
|
706
|
+
return plots
|
|
707
|
+
|
|
708
|
+
def explain_feature_importance(self, X: pd.DataFrame, nsamples: int = 20):
|
|
709
|
+
"""
|
|
710
|
+
Explains the model by computing SHAP values on the fitted model. Uses
|
|
711
|
+
the train set as the masker, and the provided set as prediction.
|
|
712
|
+
|
|
713
|
+
:param pd.DataFrame X: Prediction set to compute SHAP values for.
|
|
714
|
+
:param int, optional nsamples: Number of samples to pick from the masker to pick feature
|
|
715
|
+
data from for each row in the provided prediction dataset. More samples means more
|
|
716
|
+
accurate SHAP values and longer computing times. Defaults to 20.
|
|
717
|
+
:return: Model explanation, with an overview of the most important features, and graphs.
|
|
718
|
+
"""
|
|
719
|
+
return self.pipeline.explain_model(X, nsamples)
|
|
720
|
+
|
|
721
|
+
def predict(self, X: pd.DataFrame) -> list:
|
|
722
|
+
"""Run all the steps to predict labels from candidate data
|
|
723
|
+
|
|
724
|
+
:param pd.DataFrame X: Features used as candidate of the pipeline.
|
|
725
|
+
:return: Predicted values
|
|
726
|
+
"""
|
|
727
|
+
return self.pipeline.predict(X)
|
|
728
|
+
|
|
729
|
+
def predict_proba(self, X: pd.DataFrame) -> list:
|
|
730
|
+
"""Run all pipeline steps to predict class probabilities.
|
|
731
|
+
|
|
732
|
+
:param pd.DataFrame X: Features used as candidate of the pipeline.
|
|
733
|
+
:return: Class probabilities in the predictor's class order.
|
|
734
|
+
:raise AttributeError: The model does not support probability predictions.
|
|
735
|
+
"""
|
|
736
|
+
return self.pipeline.predict_proba(X)
|