upgini 1.2.166a4253.dev2__tar.gz → 1.2.167a4220.dev2__tar.gz
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.
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev2/src/upgini/__about__.py +1 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/features_enricher.py +213 -61
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/metrics.py +40 -6
- upgini-1.2.167a4220.dev2/src/upgini/report/__init__.py +5 -0
- upgini-1.2.167a4220.dev2/src/upgini/report/assemble.py +134 -0
- upgini-1.2.167a4220.dev2/src/upgini/report/data.py +87 -0
- upgini-1.2.167a4220.dev2/src/upgini/report/html.py +385 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/strings.properties +2 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/display_utils.py +52 -15
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/track_info.py +4 -0
- upgini-1.2.166a4253.dev2/src/upgini/__about__.py +0 -1
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/.gitignore +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/LICENSE +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/README.md +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/pyproject.toml +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/ads.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/dataset.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/errors.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/http.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/metadata.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/search_task.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/spinner.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev2}/src/upgini/version_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upgini
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.167a4220.dev2
|
|
4
4
|
Summary: Intelligent data search & enrichment for Machine Learning
|
|
5
5
|
Project-URL: Bug Reports, https://github.com/upgini/upgini/issues
|
|
6
6
|
Project-URL: Homepage, https://upgini.com/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.167.a4220.dev2"
|
|
@@ -68,7 +68,13 @@ from upgini.metadata import (
|
|
|
68
68
|
RuntimeParameters,
|
|
69
69
|
SearchKey,
|
|
70
70
|
)
|
|
71
|
-
from upgini.metrics import
|
|
71
|
+
from upgini.metrics import (
|
|
72
|
+
EstimatorWrapper,
|
|
73
|
+
define_scorer,
|
|
74
|
+
format_metrics_for_display,
|
|
75
|
+
put_cv_metric,
|
|
76
|
+
validate_scoring_argument,
|
|
77
|
+
)
|
|
72
78
|
from upgini.normalizer.normalize_utils import Normalizer
|
|
73
79
|
from upgini.resource_bundle import ResourceBundle, bundle, get_custom_bundle
|
|
74
80
|
from upgini.search_task import SearchTask
|
|
@@ -92,10 +98,14 @@ from upgini.utils.deduplicate_utils import (
|
|
|
92
98
|
clean_full_duplicates,
|
|
93
99
|
remove_fintech_duplicates,
|
|
94
100
|
)
|
|
101
|
+
from upgini.report.assemble import assemble_report_data
|
|
102
|
+
from upgini.report.html import generate_html_report
|
|
95
103
|
from upgini.utils.display_utils import (
|
|
96
104
|
display_html_dataframe,
|
|
97
105
|
do_without_pandas_limits,
|
|
106
|
+
ipython_available,
|
|
98
107
|
prepare_and_show_report,
|
|
108
|
+
show_button_open_report,
|
|
99
109
|
show_request_quote_button,
|
|
100
110
|
)
|
|
101
111
|
from upgini.utils.email_utils import (
|
|
@@ -175,6 +185,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
175
185
|
|
|
176
186
|
shared_datasets: list of str, optional (default=None)
|
|
177
187
|
list of private shared dataset ids for custom search
|
|
188
|
+
|
|
189
|
+
reports_path: str, optional (default=None)
|
|
190
|
+
Directory for the HTML search report. Defaults to a `reports` folder in the current working directory.
|
|
178
191
|
"""
|
|
179
192
|
|
|
180
193
|
TARGET_NAME = "target"
|
|
@@ -252,6 +265,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
252
265
|
sample_config: SampleConfig | None = None,
|
|
253
266
|
print_trace_id: bool = False,
|
|
254
267
|
print_loaded_report: bool = True,
|
|
268
|
+
reports_path: str | None = None,
|
|
255
269
|
**kwargs,
|
|
256
270
|
):
|
|
257
271
|
self.bundle = get_custom_bundle(custom_bundle_config)
|
|
@@ -310,6 +324,8 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
310
324
|
self.relevant_data_sources: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
311
325
|
self._relevant_data_sources_wo_links: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
312
326
|
self.metrics: pd.DataFrame | None = None
|
|
327
|
+
self.metrics_raw: pd.DataFrame | None = None
|
|
328
|
+
self.metrics_metric_name: str | None = None
|
|
313
329
|
self.feature_names_ = []
|
|
314
330
|
self.external_source_feature_names = []
|
|
315
331
|
self.feature_importances_ = []
|
|
@@ -319,6 +335,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
319
335
|
self.disable_force_downsampling = disable_force_downsampling
|
|
320
336
|
self.print_trace_id = print_trace_id
|
|
321
337
|
self.add_info: AddInfo | None = None
|
|
338
|
+
self.reports_path = reports_path
|
|
339
|
+
self.search_duration_seconds: float | None = None
|
|
340
|
+
self.report_file_path: str | None = None
|
|
322
341
|
|
|
323
342
|
if search_id:
|
|
324
343
|
search_task = SearchTask(search_id, rest_client=self.rest_client, logger=self.logger)
|
|
@@ -1096,8 +1115,8 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1096
1115
|
|
|
1097
1116
|
# 1 If client features are presented - fit and predict with KFold estimator
|
|
1098
1117
|
# on etalon features and calculate baseline metric
|
|
1099
|
-
baseline_metric = None
|
|
1100
1118
|
baseline_estimator = None
|
|
1119
|
+
baseline_cv_result = None
|
|
1101
1120
|
updating_shaps = None
|
|
1102
1121
|
custom_loss_add_params = get_additional_params_custom_loss(
|
|
1103
1122
|
self.loss, model_task_type, logger=self.logger
|
|
@@ -1121,35 +1140,27 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1121
1140
|
baseline_cv_result = baseline_estimator.cross_val_predict(
|
|
1122
1141
|
fitting_X, y_sorted, self.baseline_score_column
|
|
1123
1142
|
)
|
|
1124
|
-
|
|
1125
|
-
if baseline_metric is None:
|
|
1143
|
+
if baseline_cv_result.metric is None:
|
|
1126
1144
|
self.logger.info(
|
|
1127
1145
|
f"Baseline {metric} on train client features is None (maybe all features was removed)"
|
|
1128
1146
|
)
|
|
1129
1147
|
baseline_estimator = None
|
|
1130
1148
|
else:
|
|
1131
|
-
self.logger.info(
|
|
1149
|
+
self.logger.info(
|
|
1150
|
+
f"Baseline {metric} on train client features: {baseline_cv_result.get_display_metric()}"
|
|
1151
|
+
)
|
|
1132
1152
|
updating_shaps = baseline_cv_result.shap_values
|
|
1133
1153
|
|
|
1134
1154
|
# 2 Fit and predict with KFold estimator on enriched tds
|
|
1135
1155
|
# and calculate final metric (and uplift)
|
|
1136
|
-
enriched_metric = None
|
|
1137
1156
|
uplift = None
|
|
1138
1157
|
uplift_perc = None
|
|
1139
1158
|
enriched_estimator = None
|
|
1159
|
+
enriched_cv_result = None
|
|
1140
1160
|
ensemble_score_column = self._get_ensemble_score_column(fitting_X, fitting_enriched_X)
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
f"Only ensemble model returned; calculate enriched {metric} via roc_auc "
|
|
1145
|
-
f"on ensemble score column: {ensemble_score_column}"
|
|
1146
|
-
)
|
|
1147
|
-
else:
|
|
1148
|
-
self.logger.info(
|
|
1149
|
-
f"Calculate enriched {metric} on train combined "
|
|
1150
|
-
f"features: {fitting_enriched_X.columns.to_list()}"
|
|
1151
|
-
)
|
|
1152
|
-
enriched_estimator = EstimatorWrapper.create(
|
|
1161
|
+
|
|
1162
|
+
def evaluate_enriched(score_column, description: str):
|
|
1163
|
+
wrapper = EstimatorWrapper.create(
|
|
1153
1164
|
estimator,
|
|
1154
1165
|
self.logger,
|
|
1155
1166
|
model_task_type,
|
|
@@ -1161,22 +1172,47 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1161
1172
|
text_features=text_features,
|
|
1162
1173
|
has_time=has_time,
|
|
1163
1174
|
)
|
|
1164
|
-
|
|
1165
|
-
fitting_enriched_X, enriched_y_sorted,
|
|
1175
|
+
cv_result = wrapper.cross_val_predict(
|
|
1176
|
+
fitting_enriched_X, enriched_y_sorted, score_column
|
|
1177
|
+
)
|
|
1178
|
+
if cv_result.metric is None:
|
|
1179
|
+
self.logger.warning(f"Enriched {metric} {description} is None")
|
|
1180
|
+
return None, cv_result, None, None
|
|
1181
|
+
self.logger.info(f"Enriched {metric} {description}: {cv_result.get_display_metric()}")
|
|
1182
|
+
sample_uplift = None
|
|
1183
|
+
sample_uplift_perc = None
|
|
1184
|
+
if baseline_cv_result is not None and baseline_cv_result.metric is not None:
|
|
1185
|
+
sample_uplift = (cv_result.metric - baseline_cv_result.metric) * multiplier
|
|
1186
|
+
sample_uplift_perc = sample_uplift / abs(baseline_cv_result.metric) * 100
|
|
1187
|
+
return wrapper, cv_result, sample_uplift, sample_uplift_perc
|
|
1188
|
+
|
|
1189
|
+
if ensemble_score_column is not None:
|
|
1190
|
+
self.logger.info(
|
|
1191
|
+
f"Only ensemble model returned; calculate enriched {metric} via roc_auc "
|
|
1192
|
+
f"on ensemble score column: {ensemble_score_column}"
|
|
1193
|
+
)
|
|
1194
|
+
(
|
|
1195
|
+
enriched_estimator,
|
|
1196
|
+
enriched_cv_result,
|
|
1197
|
+
uplift,
|
|
1198
|
+
uplift_perc,
|
|
1199
|
+
) = evaluate_enriched(
|
|
1200
|
+
ensemble_score_column,
|
|
1201
|
+
f"on ensemble score column {ensemble_score_column}",
|
|
1166
1202
|
)
|
|
1167
|
-
enriched_metric = enriched_cv_result.get_display_metric()
|
|
1168
1203
|
updating_shaps = enriched_cv_result.shap_values
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1204
|
+
elif set(fitting_X.columns) != set(fitting_enriched_X.columns):
|
|
1205
|
+
self.logger.info(
|
|
1206
|
+
f"Calculate enriched {metric} on train combined "
|
|
1207
|
+
f"features: {fitting_enriched_X.columns.to_list()}"
|
|
1208
|
+
)
|
|
1209
|
+
(
|
|
1210
|
+
enriched_estimator,
|
|
1211
|
+
enriched_cv_result,
|
|
1212
|
+
uplift,
|
|
1213
|
+
uplift_perc,
|
|
1214
|
+
) = evaluate_enriched(None, "on train combined features")
|
|
1215
|
+
updating_shaps = enriched_cv_result.shap_values
|
|
1180
1216
|
|
|
1181
1217
|
train_metrics = {
|
|
1182
1218
|
self.bundle.get("quality_metrics_segment_header"): self.bundle.get(
|
|
@@ -1194,14 +1230,22 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1194
1230
|
np.mean(y_sorted),
|
|
1195
1231
|
4,
|
|
1196
1232
|
)
|
|
1197
|
-
|
|
1198
|
-
train_metrics
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1233
|
+
put_cv_metric(
|
|
1234
|
+
train_metrics,
|
|
1235
|
+
"quality_metrics_baseline_header",
|
|
1236
|
+
"quality_metrics_baseline_std_header",
|
|
1237
|
+
baseline_cv_result,
|
|
1238
|
+
metric,
|
|
1239
|
+
self.bundle,
|
|
1240
|
+
)
|
|
1241
|
+
put_cv_metric(
|
|
1242
|
+
train_metrics,
|
|
1243
|
+
"quality_metrics_enriched_header",
|
|
1244
|
+
"quality_metrics_enriched_std_header",
|
|
1245
|
+
enriched_cv_result,
|
|
1246
|
+
metric,
|
|
1247
|
+
self.bundle,
|
|
1248
|
+
)
|
|
1205
1249
|
if uplift is not None:
|
|
1206
1250
|
train_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(uplift, 3)
|
|
1207
1251
|
train_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
|
|
@@ -1233,12 +1277,13 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1233
1277
|
etalon_eval_results = baseline_estimator.calculate_metric(
|
|
1234
1278
|
eval_X_sorted, eval_y_sorted, self.baseline_score_column
|
|
1235
1279
|
)
|
|
1236
|
-
etalon_eval_metric = etalon_eval_results.get_display_metric()
|
|
1237
1280
|
self.logger.info(
|
|
1238
|
-
|
|
1281
|
+
"Baseline {} on eval set {} client features: {}".format(
|
|
1282
|
+
metric, idx + 1, etalon_eval_results.get_display_metric()
|
|
1283
|
+
)
|
|
1239
1284
|
)
|
|
1240
1285
|
else:
|
|
1241
|
-
|
|
1286
|
+
etalon_eval_results = None
|
|
1242
1287
|
|
|
1243
1288
|
if enriched_estimator is not None:
|
|
1244
1289
|
self.logger.info(
|
|
@@ -1248,14 +1293,20 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1248
1293
|
enriched_eval_results = enriched_estimator.calculate_metric(
|
|
1249
1294
|
enriched_eval_X_sorted, enriched_eval_y_sorted, ensemble_score_column
|
|
1250
1295
|
)
|
|
1251
|
-
enriched_eval_metric = enriched_eval_results.get_display_metric()
|
|
1252
1296
|
self.logger.info(
|
|
1253
|
-
|
|
1297
|
+
"Enriched {} on eval set {} combined features: {}".format(
|
|
1298
|
+
metric, idx + 1, enriched_eval_results.get_display_metric()
|
|
1299
|
+
)
|
|
1254
1300
|
)
|
|
1255
1301
|
else:
|
|
1256
|
-
|
|
1302
|
+
enriched_eval_results = None
|
|
1257
1303
|
|
|
1258
|
-
if
|
|
1304
|
+
if (
|
|
1305
|
+
etalon_eval_results is not None
|
|
1306
|
+
and etalon_eval_results.metric is not None
|
|
1307
|
+
and enriched_eval_results is not None
|
|
1308
|
+
and enriched_eval_results.metric is not None
|
|
1309
|
+
):
|
|
1259
1310
|
eval_uplift = (enriched_eval_results.metric - etalon_eval_results.metric) * multiplier
|
|
1260
1311
|
eval_uplift_perc = eval_uplift / abs(etalon_eval_results.metric) * 100
|
|
1261
1312
|
else:
|
|
@@ -1279,14 +1330,22 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1279
1330
|
np.mean(eval_y_sorted),
|
|
1280
1331
|
4,
|
|
1281
1332
|
)
|
|
1282
|
-
|
|
1283
|
-
eval_metrics
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1333
|
+
put_cv_metric(
|
|
1334
|
+
eval_metrics,
|
|
1335
|
+
"quality_metrics_baseline_header",
|
|
1336
|
+
"quality_metrics_baseline_std_header",
|
|
1337
|
+
etalon_eval_results,
|
|
1338
|
+
metric,
|
|
1339
|
+
self.bundle,
|
|
1340
|
+
)
|
|
1341
|
+
put_cv_metric(
|
|
1342
|
+
eval_metrics,
|
|
1343
|
+
"quality_metrics_enriched_header",
|
|
1344
|
+
"quality_metrics_enriched_std_header",
|
|
1345
|
+
enriched_eval_results,
|
|
1346
|
+
metric,
|
|
1347
|
+
self.bundle,
|
|
1348
|
+
)
|
|
1290
1349
|
if eval_uplift is not None:
|
|
1291
1350
|
eval_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(eval_uplift, 3)
|
|
1292
1351
|
eval_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
|
|
@@ -1323,7 +1382,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1323
1382
|
elif uplift_col in metrics_df.columns and (metrics_df[uplift_col] < 0).any():
|
|
1324
1383
|
self.logger.warning("Uplift is negative")
|
|
1325
1384
|
|
|
1326
|
-
|
|
1385
|
+
self.metrics_raw = metrics_df
|
|
1386
|
+
self.metrics_metric_name = metric
|
|
1387
|
+
return format_metrics_for_display(metrics_df, metric, self.bundle)
|
|
1327
1388
|
except Exception as e:
|
|
1328
1389
|
error_message = "Failed to calculate metrics" + (
|
|
1329
1390
|
" with validation error" if isinstance(e, ValidationError) else ""
|
|
@@ -2224,6 +2285,71 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2224
2285
|
column = new_external[0]
|
|
2225
2286
|
return column if self._is_ensemble_feature(column) else None
|
|
2226
2287
|
|
|
2288
|
+
def _has_single_ensemble_score(self) -> bool:
|
|
2289
|
+
return self._get_single_ensemble_score_name() is not None
|
|
2290
|
+
|
|
2291
|
+
def _get_single_ensemble_score_name(self) -> str | None:
|
|
2292
|
+
selected = list(self.feature_names_ or [])
|
|
2293
|
+
ensemble = [name for name in selected if self._is_ensemble_feature(name)]
|
|
2294
|
+
if len(ensemble) != 1:
|
|
2295
|
+
return None
|
|
2296
|
+
etalon_columns = set()
|
|
2297
|
+
if self._search_task is not None:
|
|
2298
|
+
etalon_columns = {
|
|
2299
|
+
meta.name
|
|
2300
|
+
for meta in (self._search_task.get_all_features_metadata_v2() or [])
|
|
2301
|
+
if meta.source == "etalon"
|
|
2302
|
+
}
|
|
2303
|
+
extras = [name for name in selected if name != ensemble[0] and name not in etalon_columns]
|
|
2304
|
+
if extras:
|
|
2305
|
+
return None
|
|
2306
|
+
return ensemble[0]
|
|
2307
|
+
|
|
2308
|
+
def _reports_dir(self) -> Path:
|
|
2309
|
+
return Path(self.reports_path) if self.reports_path else Path.cwd() / "reports"
|
|
2310
|
+
|
|
2311
|
+
def _reference_rows(self) -> int | None:
|
|
2312
|
+
if self.X is None:
|
|
2313
|
+
return None
|
|
2314
|
+
rows = _num_samples(self.X)
|
|
2315
|
+
for eval_pair in self.eval_set or []:
|
|
2316
|
+
rows += _num_samples(eval_pair[0])
|
|
2317
|
+
return rows
|
|
2318
|
+
|
|
2319
|
+
def _report_sample_names(self) -> list[str]:
|
|
2320
|
+
samples = [self.bundle.get("quality_metrics_train_segment")]
|
|
2321
|
+
for idx, _ in enumerate(self.eval_set or []):
|
|
2322
|
+
samples.append(self.bundle.get("quality_metrics_eval_segment").format(idx + 1))
|
|
2323
|
+
return samples
|
|
2324
|
+
|
|
2325
|
+
def _assemble_report_data(self):
|
|
2326
|
+
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2327
|
+
return assemble_report_data(
|
|
2328
|
+
search_id=search_id,
|
|
2329
|
+
search_keys=[str(sk) for sk in self.search_keys.values()],
|
|
2330
|
+
search_duration_seconds=self.search_duration_seconds,
|
|
2331
|
+
reference_rows=self._reference_rows(),
|
|
2332
|
+
samples=self._report_sample_names(),
|
|
2333
|
+
metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
|
|
2334
|
+
metric_name=self.metrics_metric_name,
|
|
2335
|
+
model_features=self._ensemble_model_feature_count(),
|
|
2336
|
+
is_binary=self.model_task_type == ModelTaskType.BINARY,
|
|
2337
|
+
bundle=self.bundle,
|
|
2338
|
+
)
|
|
2339
|
+
|
|
2340
|
+
def _write_score_report(self) -> str | None:
|
|
2341
|
+
if self._search_task is None or not self._has_single_ensemble_score():
|
|
2342
|
+
return None
|
|
2343
|
+
report_data = self._assemble_report_data()
|
|
2344
|
+
html = generate_html_report(report_data)
|
|
2345
|
+
reports_dir = self._reports_dir()
|
|
2346
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
2347
|
+
path = reports_dir / f"upgini-report-{report_data.metadata.search_id}.html"
|
|
2348
|
+
path.write_text(html, encoding="utf-8")
|
|
2349
|
+
self.report_file_path = str(path)
|
|
2350
|
+
self.logger.info(f"Score report saved to {path}")
|
|
2351
|
+
return self.report_file_path
|
|
2352
|
+
|
|
2227
2353
|
def _is_ensemble_feature(self, column_name: str) -> bool:
|
|
2228
2354
|
renaming = self.fit_columns_renaming or {}
|
|
2229
2355
|
aliases = self._column_name_aliases([column_name], renaming)
|
|
@@ -2232,15 +2358,20 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2232
2358
|
return False
|
|
2233
2359
|
return bool(aliases & self._column_name_aliases(list(ensemble_names), renaming))
|
|
2234
2360
|
|
|
2235
|
-
def
|
|
2361
|
+
def _ensemble_generated_metadata(self):
|
|
2236
2362
|
autofe_meta = self._search_task.get_autofe_metadata() if self._search_task else None
|
|
2237
2363
|
if not autofe_meta:
|
|
2238
|
-
return
|
|
2239
|
-
|
|
2364
|
+
return []
|
|
2365
|
+
matched = []
|
|
2240
2366
|
for meta in autofe_meta:
|
|
2241
2367
|
op = EnsembleModel.from_formula(meta.formula.split("(")[0])
|
|
2242
|
-
if op is None:
|
|
2243
|
-
|
|
2368
|
+
if op is not None:
|
|
2369
|
+
matched.append((meta, op))
|
|
2370
|
+
return matched
|
|
2371
|
+
|
|
2372
|
+
def _ensemble_feature_names_from_metadata(self) -> set[str]:
|
|
2373
|
+
names: set[str] = set()
|
|
2374
|
+
for meta, op in self._ensemble_generated_metadata():
|
|
2244
2375
|
feature = (
|
|
2245
2376
|
Feature(op, [])
|
|
2246
2377
|
.set_display_index(meta.display_index)
|
|
@@ -2252,6 +2383,14 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2252
2383
|
names.add(meta.alias)
|
|
2253
2384
|
return names
|
|
2254
2385
|
|
|
2386
|
+
def _ensemble_model_feature_count(self) -> int | None:
|
|
2387
|
+
names = {
|
|
2388
|
+
column.original_name
|
|
2389
|
+
for meta, _ in self._ensemble_generated_metadata()
|
|
2390
|
+
for column in meta.base_columns
|
|
2391
|
+
}
|
|
2392
|
+
return len(names) or None
|
|
2393
|
+
|
|
2255
2394
|
def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
|
|
2256
2395
|
return psi is not None and not pd.isna(psi) and psi > threshold
|
|
2257
2396
|
|
|
@@ -4126,6 +4265,9 @@ if response.status_code == 200:
|
|
|
4126
4265
|
self._fit_match_search_keys = None
|
|
4127
4266
|
self.__cached_sampled_datasets = dict()
|
|
4128
4267
|
self.metrics = None
|
|
4268
|
+
self.metrics_raw = None
|
|
4269
|
+
self.metrics_metric_name = None
|
|
4270
|
+
self.report_file_path = None
|
|
4129
4271
|
self.fit_columns_renaming = None
|
|
4130
4272
|
self.fit_dropped_features = set()
|
|
4131
4273
|
self.fit_generated_features = []
|
|
@@ -4536,6 +4678,7 @@ if response.status_code == 200:
|
|
|
4536
4678
|
|
|
4537
4679
|
self._search_task.poll_result(self._get_trace_id(), quiet=True)
|
|
4538
4680
|
self.__persist_transform_match_hashes()
|
|
4681
|
+
self.search_duration_seconds = time.time() - start_time
|
|
4539
4682
|
|
|
4540
4683
|
seconds_left = time.time() - start_time
|
|
4541
4684
|
progress = SearchProgress(97.0, ProgressStage.GENERATING_REPORT, seconds_left)
|
|
@@ -5942,6 +6085,15 @@ if response.status_code == 200:
|
|
|
5942
6085
|
|
|
5943
6086
|
def __show_report_button(self, display_id: str | None = None, display_handle=None):
|
|
5944
6087
|
try:
|
|
6088
|
+
report_path = self._write_score_report() or self.report_file_path
|
|
6089
|
+
if report_path:
|
|
6090
|
+
if not ipython_available():
|
|
6091
|
+
print(f"Full report saved to {report_path}")
|
|
6092
|
+
return
|
|
6093
|
+
return show_button_open_report(
|
|
6094
|
+
report_path, display_id=display_id, display_handle=display_handle
|
|
6095
|
+
)
|
|
6096
|
+
|
|
5945
6097
|
eval_sets_drift_df = self._get_eval_sets_drift_summary()
|
|
5946
6098
|
|
|
5947
6099
|
return prepare_and_show_report(
|
|
@@ -253,12 +253,46 @@ class _CrossValResults:
|
|
|
253
253
|
shap_values: Optional[Dict[str, float]]
|
|
254
254
|
|
|
255
255
|
def get_display_metric(self) -> Optional[str]:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
256
|
+
return format_display_metric(self.metric, self.metric_std)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def format_display_metric(metric: Optional[float], metric_std: Optional[float] = None) -> Optional[str]:
|
|
260
|
+
if metric is None or pd.isna(metric):
|
|
261
|
+
return None
|
|
262
|
+
if metric_std is None or pd.isna(metric_std):
|
|
263
|
+
return f"{float(metric):.3f}"
|
|
264
|
+
return f"{float(metric):.3f} ± {float(metric_std):.3f}"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def format_metrics_for_display(metrics_df: pd.DataFrame, metric_name: str, bundle) -> pd.DataFrame:
|
|
268
|
+
display = metrics_df.copy()
|
|
269
|
+
drop: list[str] = []
|
|
270
|
+
for value_key, std_key in (
|
|
271
|
+
("quality_metrics_baseline_header", "quality_metrics_baseline_std_header"),
|
|
272
|
+
("quality_metrics_enriched_header", "quality_metrics_enriched_std_header"),
|
|
273
|
+
):
|
|
274
|
+
col = bundle.get(value_key).format(metric_name)
|
|
275
|
+
std_col = bundle.get(std_key).format(metric_name)
|
|
276
|
+
if col in display.columns:
|
|
277
|
+
stds = display[std_col] if std_col in display.columns else [None] * len(display)
|
|
278
|
+
display[col] = [format_display_metric(value, std) for value, std in zip(display[col], stds)]
|
|
279
|
+
if std_col in display.columns:
|
|
280
|
+
drop.append(std_col)
|
|
281
|
+
return display.drop(columns=drop, errors="ignore")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def put_cv_metric(
|
|
285
|
+
row: dict,
|
|
286
|
+
value_key: str,
|
|
287
|
+
std_key: str,
|
|
288
|
+
result: Optional[_CrossValResults],
|
|
289
|
+
metric_name: str,
|
|
290
|
+
bundle,
|
|
291
|
+
) -> None:
|
|
292
|
+
if result is None or result.metric is None:
|
|
293
|
+
return
|
|
294
|
+
row[bundle.get(value_key).format(metric_name)] = result.metric
|
|
295
|
+
row[bundle.get(std_key).format(metric_name)] = result.metric_std
|
|
262
296
|
|
|
263
297
|
|
|
264
298
|
def is_numeric_object(x: pd.Series) -> bool:
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from upgini.report.assemble import assemble_report_data, format_search_duration
|
|
2
|
+
from upgini.report.data import ReportData
|
|
3
|
+
from upgini.report.html import generate_html_report
|
|
4
|
+
|
|
5
|
+
__all__ = ["ReportData", "assemble_report_data", "format_search_duration", "generate_html_report"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from upgini.__about__ import __version__
|
|
9
|
+
from upgini.report.data import QualitySample, ReportData, ReportMetadata, SampleStats, SearchResultsSummary
|
|
10
|
+
from upgini.resource_bundle import ResourceBundle
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_search_duration(seconds: Optional[float]) -> Optional[str]:
|
|
14
|
+
if seconds is None:
|
|
15
|
+
return None
|
|
16
|
+
total = max(0, int(round(seconds)))
|
|
17
|
+
hours, remainder = divmod(total, 3600)
|
|
18
|
+
minutes, secs = divmod(remainder, 60)
|
|
19
|
+
if hours:
|
|
20
|
+
return f"{hours} h {minutes} min {secs} sec"
|
|
21
|
+
if minutes:
|
|
22
|
+
return f"{minutes} min {secs} sec"
|
|
23
|
+
return f"{secs} sec"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def assemble_report_data(
|
|
27
|
+
*,
|
|
28
|
+
search_id: str,
|
|
29
|
+
search_keys: list[str],
|
|
30
|
+
generated_at: Optional[datetime] = None,
|
|
31
|
+
pylib_version: Optional[str] = None,
|
|
32
|
+
search_duration_seconds: Optional[float] = None,
|
|
33
|
+
reference_rows: Optional[int] = None,
|
|
34
|
+
samples: Optional[list[str]] = None,
|
|
35
|
+
metrics_df: Optional[pd.DataFrame] = None,
|
|
36
|
+
metric_name: Optional[str] = None,
|
|
37
|
+
model_features: Optional[int] = None,
|
|
38
|
+
is_binary: bool = False,
|
|
39
|
+
logo_url: Optional[str] = None,
|
|
40
|
+
bundle: ResourceBundle,
|
|
41
|
+
) -> ReportData:
|
|
42
|
+
generated_at = generated_at or datetime.now(timezone.utc)
|
|
43
|
+
sample_names = list(samples or [])
|
|
44
|
+
return ReportData(
|
|
45
|
+
metadata=ReportMetadata(
|
|
46
|
+
search_id=search_id,
|
|
47
|
+
generated_at=generated_at.strftime("%Y-%m-%d %H:%M UTC"),
|
|
48
|
+
pylib_version=pylib_version or __version__,
|
|
49
|
+
search_keys=list(search_keys or []),
|
|
50
|
+
search_duration=format_search_duration(search_duration_seconds),
|
|
51
|
+
reference_rows=reference_rows,
|
|
52
|
+
samples=sample_names,
|
|
53
|
+
logo_url=logo_url or None,
|
|
54
|
+
),
|
|
55
|
+
quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
|
|
56
|
+
summary=SearchResultsSummary(model_features=model_features),
|
|
57
|
+
sample_stats=_sample_stats(sample_names, metrics_df, bundle),
|
|
58
|
+
is_binary=is_binary,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _quality_samples(
|
|
63
|
+
metrics_df: Optional[pd.DataFrame], metric_name: Optional[str], bundle: ResourceBundle
|
|
64
|
+
) -> list[QualitySample]:
|
|
65
|
+
if metrics_df is None or metrics_df.empty:
|
|
66
|
+
return []
|
|
67
|
+
segment_col = bundle.get("quality_metrics_segment_header")
|
|
68
|
+
uplift_col = bundle.get("quality_metrics_uplift_header")
|
|
69
|
+
uplift_pct_col = bundle.get("quality_metrics_uplift_perc_header")
|
|
70
|
+
baseline_col = bundle.get("quality_metrics_baseline_header").format(metric_name) if metric_name else None
|
|
71
|
+
enriched_col = bundle.get("quality_metrics_enriched_header").format(metric_name) if metric_name else None
|
|
72
|
+
baseline_std_col = bundle.get("quality_metrics_baseline_std_header").format(metric_name) if metric_name else None
|
|
73
|
+
enriched_std_col = bundle.get("quality_metrics_enriched_std_header").format(metric_name) if metric_name else None
|
|
74
|
+
|
|
75
|
+
samples: list[QualitySample] = []
|
|
76
|
+
for _, row in metrics_df.iterrows():
|
|
77
|
+
samples.append(
|
|
78
|
+
QualitySample(
|
|
79
|
+
evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
|
|
80
|
+
metric=metric_name or "",
|
|
81
|
+
baseline=_as_float(row[baseline_col]) if baseline_col in metrics_df.columns else None,
|
|
82
|
+
enriched=_as_float(row[enriched_col]) if enriched_col in metrics_df.columns else None,
|
|
83
|
+
std=_metric_std(row, metrics_df.columns, enriched_std_col, baseline_std_col),
|
|
84
|
+
uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
|
|
85
|
+
relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return samples
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _sample_stats(
|
|
92
|
+
sample_names: list[str], metrics_df: Optional[pd.DataFrame], bundle: ResourceBundle
|
|
93
|
+
) -> list[SampleStats]:
|
|
94
|
+
segment_col = bundle.get("quality_metrics_segment_header")
|
|
95
|
+
rows_col = bundle.get("quality_metrics_rows_header")
|
|
96
|
+
mean_col = bundle.get("quality_metrics_mean_target_header")
|
|
97
|
+
by_name: dict[str, SampleStats] = {}
|
|
98
|
+
if metrics_df is not None and not metrics_df.empty and segment_col in metrics_df.columns:
|
|
99
|
+
for _, row in metrics_df.iterrows():
|
|
100
|
+
name = _as_str(row[segment_col])
|
|
101
|
+
if not name:
|
|
102
|
+
continue
|
|
103
|
+
by_name[name] = SampleStats(
|
|
104
|
+
sample=name,
|
|
105
|
+
rows=_as_int(row[rows_col]) if rows_col in metrics_df.columns else None,
|
|
106
|
+
mean_target=_as_float(row[mean_col]) if mean_col in metrics_df.columns else None,
|
|
107
|
+
)
|
|
108
|
+
names = sample_names or list(by_name)
|
|
109
|
+
stats = [by_name.get(name) or SampleStats(sample=name) for name in names]
|
|
110
|
+
seen = set(names)
|
|
111
|
+
stats.extend(sample for name, sample in by_name.items() if name not in seen)
|
|
112
|
+
return stats
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _metric_std(row, columns, enriched_std_col: Optional[str], baseline_std_col: Optional[str]) -> Optional[float]:
|
|
116
|
+
if enriched_std_col in columns:
|
|
117
|
+
std = _as_float(row[enriched_std_col])
|
|
118
|
+
if std is not None:
|
|
119
|
+
return std
|
|
120
|
+
if baseline_std_col in columns:
|
|
121
|
+
return _as_float(row[baseline_std_col])
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _as_str(value) -> Optional[str]:
|
|
126
|
+
return None if pd.isna(value) else str(value)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _as_float(value) -> Optional[float]:
|
|
130
|
+
return None if pd.isna(value) else float(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _as_int(value) -> Optional[int]:
|
|
134
|
+
return None if pd.isna(value) else int(value)
|