upgini 1.2.166a4253.dev2__tar.gz → 1.2.167a4220.dev1__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.dev1}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev1/src/upgini/__about__.py +1 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/features_enricher.py +131 -27
- upgini-1.2.167a4220.dev1/src/upgini/report/__init__.py +5 -0
- upgini-1.2.167a4220.dev1/src/upgini/report/assemble.py +85 -0
- upgini-1.2.167a4220.dev1/src/upgini/report/data.py +72 -0
- upgini-1.2.167a4220.dev1/src/upgini/report/html.py +281 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/display_utils.py +31 -0
- upgini-1.2.166a4253.dev2/src/upgini/__about__.py +0 -1
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/.gitignore +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/LICENSE +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/README.md +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/pyproject.toml +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/ads.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/dataset.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/errors.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/http.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/metadata.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/metrics.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/search_task.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/spinner.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/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.dev1
|
|
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.dev1"
|
|
@@ -92,10 +92,14 @@ from upgini.utils.deduplicate_utils import (
|
|
|
92
92
|
clean_full_duplicates,
|
|
93
93
|
remove_fintech_duplicates,
|
|
94
94
|
)
|
|
95
|
+
from upgini.report.assemble import assemble_report_data
|
|
96
|
+
from upgini.report.html import generate_html_report
|
|
95
97
|
from upgini.utils.display_utils import (
|
|
96
98
|
display_html_dataframe,
|
|
97
99
|
do_without_pandas_limits,
|
|
100
|
+
ipython_available,
|
|
98
101
|
prepare_and_show_report,
|
|
102
|
+
show_button_open_report,
|
|
99
103
|
show_request_quote_button,
|
|
100
104
|
)
|
|
101
105
|
from upgini.utils.email_utils import (
|
|
@@ -175,6 +179,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
175
179
|
|
|
176
180
|
shared_datasets: list of str, optional (default=None)
|
|
177
181
|
list of private shared dataset ids for custom search
|
|
182
|
+
|
|
183
|
+
reports_path: str, optional (default=None)
|
|
184
|
+
Directory for the HTML search report. Defaults to a `reports` folder in the current working directory.
|
|
178
185
|
"""
|
|
179
186
|
|
|
180
187
|
TARGET_NAME = "target"
|
|
@@ -252,6 +259,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
252
259
|
sample_config: SampleConfig | None = None,
|
|
253
260
|
print_trace_id: bool = False,
|
|
254
261
|
print_loaded_report: bool = True,
|
|
262
|
+
reports_path: str | None = None,
|
|
255
263
|
**kwargs,
|
|
256
264
|
):
|
|
257
265
|
self.bundle = get_custom_bundle(custom_bundle_config)
|
|
@@ -310,6 +318,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
310
318
|
self.relevant_data_sources: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
311
319
|
self._relevant_data_sources_wo_links: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
312
320
|
self.metrics: pd.DataFrame | None = None
|
|
321
|
+
self.metrics_metric_name: str | None = None
|
|
313
322
|
self.feature_names_ = []
|
|
314
323
|
self.external_source_feature_names = []
|
|
315
324
|
self.feature_importances_ = []
|
|
@@ -319,6 +328,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
319
328
|
self.disable_force_downsampling = disable_force_downsampling
|
|
320
329
|
self.print_trace_id = print_trace_id
|
|
321
330
|
self.add_info: AddInfo | None = None
|
|
331
|
+
self.reports_path = reports_path
|
|
332
|
+
self.search_duration_seconds: float | None = None
|
|
333
|
+
self.report_file_path: str | None = None
|
|
322
334
|
|
|
323
335
|
if search_id:
|
|
324
336
|
search_task = SearchTask(search_id, rest_client=self.rest_client, logger=self.logger)
|
|
@@ -1138,18 +1150,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1138
1150
|
uplift_perc = None
|
|
1139
1151
|
enriched_estimator = None
|
|
1140
1152
|
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(
|
|
1153
|
+
|
|
1154
|
+
def evaluate_enriched(score_column, description: str):
|
|
1155
|
+
wrapper = EstimatorWrapper.create(
|
|
1153
1156
|
estimator,
|
|
1154
1157
|
self.logger,
|
|
1155
1158
|
model_task_type,
|
|
@@ -1161,22 +1164,48 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1161
1164
|
text_features=text_features,
|
|
1162
1165
|
has_time=has_time,
|
|
1163
1166
|
)
|
|
1164
|
-
|
|
1165
|
-
fitting_enriched_X, enriched_y_sorted,
|
|
1167
|
+
cv_result = wrapper.cross_val_predict(
|
|
1168
|
+
fitting_enriched_X, enriched_y_sorted, score_column
|
|
1166
1169
|
)
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1170
|
+
display_metric = cv_result.get_display_metric()
|
|
1171
|
+
if display_metric is None:
|
|
1172
|
+
self.logger.warning(f"Enriched {metric} {description} is None")
|
|
1173
|
+
return None, None, cv_result.shap_values, None, None
|
|
1174
|
+
self.logger.info(f"Enriched {metric} {description}: {display_metric}")
|
|
1175
|
+
sample_uplift = None
|
|
1176
|
+
sample_uplift_perc = None
|
|
1177
|
+
if baseline_metric is not None:
|
|
1178
|
+
sample_uplift = (cv_result.metric - baseline_cv_result.metric) * multiplier
|
|
1179
|
+
sample_uplift_perc = sample_uplift / abs(baseline_cv_result.metric) * 100
|
|
1180
|
+
return wrapper, display_metric, cv_result.shap_values, sample_uplift, sample_uplift_perc
|
|
1181
|
+
|
|
1182
|
+
if ensemble_score_column is not None:
|
|
1183
|
+
self.logger.info(
|
|
1184
|
+
f"Only ensemble model returned; calculate enriched {metric} via roc_auc "
|
|
1185
|
+
f"on ensemble score column: {ensemble_score_column}"
|
|
1186
|
+
)
|
|
1187
|
+
(
|
|
1188
|
+
enriched_estimator,
|
|
1189
|
+
enriched_metric,
|
|
1190
|
+
updating_shaps,
|
|
1191
|
+
uplift,
|
|
1192
|
+
uplift_perc,
|
|
1193
|
+
) = evaluate_enriched(
|
|
1194
|
+
ensemble_score_column,
|
|
1195
|
+
f"on ensemble score column {ensemble_score_column}",
|
|
1196
|
+
)
|
|
1197
|
+
elif set(fitting_X.columns) != set(fitting_enriched_X.columns):
|
|
1198
|
+
self.logger.info(
|
|
1199
|
+
f"Calculate enriched {metric} on train combined "
|
|
1200
|
+
f"features: {fitting_enriched_X.columns.to_list()}"
|
|
1201
|
+
)
|
|
1202
|
+
(
|
|
1203
|
+
enriched_estimator,
|
|
1204
|
+
enriched_metric,
|
|
1205
|
+
updating_shaps,
|
|
1206
|
+
uplift,
|
|
1207
|
+
uplift_perc,
|
|
1208
|
+
) = evaluate_enriched(None, "on train combined features")
|
|
1180
1209
|
|
|
1181
1210
|
train_metrics = {
|
|
1182
1211
|
self.bundle.get("quality_metrics_segment_header"): self.bundle.get(
|
|
@@ -1323,6 +1352,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1323
1352
|
elif uplift_col in metrics_df.columns and (metrics_df[uplift_col] < 0).any():
|
|
1324
1353
|
self.logger.warning("Uplift is negative")
|
|
1325
1354
|
|
|
1355
|
+
self.metrics_metric_name = metric
|
|
1326
1356
|
return metrics_df
|
|
1327
1357
|
except Exception as e:
|
|
1328
1358
|
error_message = "Failed to calculate metrics" + (
|
|
@@ -2224,6 +2254,69 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2224
2254
|
column = new_external[0]
|
|
2225
2255
|
return column if self._is_ensemble_feature(column) else None
|
|
2226
2256
|
|
|
2257
|
+
def _has_single_ensemble_score(self) -> bool:
|
|
2258
|
+
return self._get_single_ensemble_score_name() is not None
|
|
2259
|
+
|
|
2260
|
+
def _get_single_ensemble_score_name(self) -> str | None:
|
|
2261
|
+
selected = list(self.feature_names_ or [])
|
|
2262
|
+
ensemble = [name for name in selected if self._is_ensemble_feature(name)]
|
|
2263
|
+
if len(ensemble) != 1:
|
|
2264
|
+
return None
|
|
2265
|
+
etalon_columns = set()
|
|
2266
|
+
if self._search_task is not None:
|
|
2267
|
+
etalon_columns = {
|
|
2268
|
+
meta.name
|
|
2269
|
+
for meta in (self._search_task.get_all_features_metadata_v2() or [])
|
|
2270
|
+
if meta.source == "etalon"
|
|
2271
|
+
}
|
|
2272
|
+
extras = [name for name in selected if name != ensemble[0] and name not in etalon_columns]
|
|
2273
|
+
if extras:
|
|
2274
|
+
return None
|
|
2275
|
+
return ensemble[0]
|
|
2276
|
+
|
|
2277
|
+
def _reports_dir(self) -> Path:
|
|
2278
|
+
return Path(self.reports_path) if self.reports_path else Path.cwd() / "reports"
|
|
2279
|
+
|
|
2280
|
+
def _reference_rows(self) -> int | None:
|
|
2281
|
+
if self.X is None:
|
|
2282
|
+
return None
|
|
2283
|
+
rows = _num_samples(self.X)
|
|
2284
|
+
for eval_pair in self.eval_set or []:
|
|
2285
|
+
rows += _num_samples(eval_pair[0])
|
|
2286
|
+
return rows
|
|
2287
|
+
|
|
2288
|
+
def _report_sample_names(self) -> list[str]:
|
|
2289
|
+
samples = [self.bundle.get("quality_metrics_train_segment")]
|
|
2290
|
+
for idx, _ in enumerate(self.eval_set or []):
|
|
2291
|
+
samples.append(self.bundle.get("quality_metrics_eval_segment").format(idx + 1))
|
|
2292
|
+
return samples
|
|
2293
|
+
|
|
2294
|
+
def _assemble_report_data(self):
|
|
2295
|
+
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2296
|
+
return assemble_report_data(
|
|
2297
|
+
search_id=search_id,
|
|
2298
|
+
search_keys=[str(sk) for sk in self.search_keys.values()],
|
|
2299
|
+
search_duration_seconds=self.search_duration_seconds,
|
|
2300
|
+
reference_rows=self._reference_rows(),
|
|
2301
|
+
samples=self._report_sample_names(),
|
|
2302
|
+
metrics_df=self.metrics,
|
|
2303
|
+
metric_name=self.metrics_metric_name,
|
|
2304
|
+
bundle=self.bundle,
|
|
2305
|
+
)
|
|
2306
|
+
|
|
2307
|
+
def _write_score_report(self) -> str | None:
|
|
2308
|
+
if self._search_task is None or not self._has_single_ensemble_score():
|
|
2309
|
+
return None
|
|
2310
|
+
report_data = self._assemble_report_data()
|
|
2311
|
+
html = generate_html_report(report_data)
|
|
2312
|
+
reports_dir = self._reports_dir()
|
|
2313
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
2314
|
+
path = reports_dir / f"upgini-report-{report_data.metadata.search_id}.html"
|
|
2315
|
+
path.write_text(html, encoding="utf-8")
|
|
2316
|
+
self.report_file_path = str(path)
|
|
2317
|
+
self.logger.info(f"Score report saved to {path}")
|
|
2318
|
+
return self.report_file_path
|
|
2319
|
+
|
|
2227
2320
|
def _is_ensemble_feature(self, column_name: str) -> bool:
|
|
2228
2321
|
renaming = self.fit_columns_renaming or {}
|
|
2229
2322
|
aliases = self._column_name_aliases([column_name], renaming)
|
|
@@ -4126,6 +4219,7 @@ if response.status_code == 200:
|
|
|
4126
4219
|
self._fit_match_search_keys = None
|
|
4127
4220
|
self.__cached_sampled_datasets = dict()
|
|
4128
4221
|
self.metrics = None
|
|
4222
|
+
self.metrics_metric_name = None
|
|
4129
4223
|
self.fit_columns_renaming = None
|
|
4130
4224
|
self.fit_dropped_features = set()
|
|
4131
4225
|
self.fit_generated_features = []
|
|
@@ -4536,6 +4630,7 @@ if response.status_code == 200:
|
|
|
4536
4630
|
|
|
4537
4631
|
self._search_task.poll_result(self._get_trace_id(), quiet=True)
|
|
4538
4632
|
self.__persist_transform_match_hashes()
|
|
4633
|
+
self.search_duration_seconds = time.time() - start_time
|
|
4539
4634
|
|
|
4540
4635
|
seconds_left = time.time() - start_time
|
|
4541
4636
|
progress = SearchProgress(97.0, ProgressStage.GENERATING_REPORT, seconds_left)
|
|
@@ -5942,6 +6037,15 @@ if response.status_code == 200:
|
|
|
5942
6037
|
|
|
5943
6038
|
def __show_report_button(self, display_id: str | None = None, display_handle=None):
|
|
5944
6039
|
try:
|
|
6040
|
+
report_path = self._write_score_report()
|
|
6041
|
+
if report_path is not None:
|
|
6042
|
+
if not ipython_available():
|
|
6043
|
+
print(f"Full report saved to {report_path}")
|
|
6044
|
+
return
|
|
6045
|
+
return show_button_open_report(
|
|
6046
|
+
report_path, display_id=display_id, display_handle=display_handle
|
|
6047
|
+
)
|
|
6048
|
+
|
|
5945
6049
|
eval_sets_drift_df = self._get_eval_sets_drift_summary()
|
|
5946
6050
|
|
|
5947
6051
|
return prepare_and_show_report(
|
|
@@ -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,85 @@
|
|
|
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
|
|
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
|
+
bundle: ResourceBundle,
|
|
38
|
+
) -> ReportData:
|
|
39
|
+
generated_at = generated_at or datetime.now(timezone.utc)
|
|
40
|
+
return ReportData(
|
|
41
|
+
metadata=ReportMetadata(
|
|
42
|
+
search_id=search_id,
|
|
43
|
+
generated_at=generated_at.strftime("%Y-%m-%d %H:%M UTC"),
|
|
44
|
+
pylib_version=pylib_version or __version__,
|
|
45
|
+
search_keys=list(search_keys or []),
|
|
46
|
+
search_duration=format_search_duration(search_duration_seconds),
|
|
47
|
+
reference_rows=reference_rows,
|
|
48
|
+
samples=list(samples or []),
|
|
49
|
+
),
|
|
50
|
+
quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _quality_samples(
|
|
55
|
+
metrics_df: Optional[pd.DataFrame], metric_name: Optional[str], bundle: ResourceBundle
|
|
56
|
+
) -> list[QualitySample]:
|
|
57
|
+
if metrics_df is None or metrics_df.empty:
|
|
58
|
+
return []
|
|
59
|
+
segment_col = bundle.get("quality_metrics_segment_header")
|
|
60
|
+
uplift_col = bundle.get("quality_metrics_uplift_header")
|
|
61
|
+
uplift_pct_col = bundle.get("quality_metrics_uplift_perc_header")
|
|
62
|
+
baseline_col = bundle.get("quality_metrics_baseline_header").format(metric_name) if metric_name else None
|
|
63
|
+
enriched_col = bundle.get("quality_metrics_enriched_header").format(metric_name) if metric_name else None
|
|
64
|
+
|
|
65
|
+
samples: list[QualitySample] = []
|
|
66
|
+
for _, row in metrics_df.iterrows():
|
|
67
|
+
samples.append(
|
|
68
|
+
QualitySample(
|
|
69
|
+
evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
|
|
70
|
+
metric=metric_name or "",
|
|
71
|
+
baseline=_as_str(row[baseline_col]) if baseline_col in metrics_df.columns else None,
|
|
72
|
+
enriched=_as_str(row[enriched_col]) if enriched_col in metrics_df.columns else None,
|
|
73
|
+
uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
|
|
74
|
+
relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return samples
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _as_str(value) -> Optional[str]:
|
|
81
|
+
return None if pd.isna(value) else str(value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _as_float(value) -> Optional[float]:
|
|
85
|
+
return None if pd.isna(value) else float(value)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ReportMetadata:
|
|
9
|
+
search_id: str
|
|
10
|
+
generated_at: str
|
|
11
|
+
pylib_version: str
|
|
12
|
+
search_keys: list[str] = field(default_factory=list)
|
|
13
|
+
search_duration: Optional[str] = None
|
|
14
|
+
reference_rows: Optional[int] = None
|
|
15
|
+
samples: list[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class QualitySample:
|
|
20
|
+
evaluation_scope: str
|
|
21
|
+
metric: str
|
|
22
|
+
baseline: Optional[str] = None
|
|
23
|
+
enriched: Optional[str] = None
|
|
24
|
+
uplift: Optional[float] = None
|
|
25
|
+
relative_uplift: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class FeatureRow:
|
|
30
|
+
name: str
|
|
31
|
+
shap: Optional[float] = None
|
|
32
|
+
psi: Optional[float] = None
|
|
33
|
+
drift: Optional[float] = None
|
|
34
|
+
coverage: Optional[float] = None
|
|
35
|
+
provider: str = ""
|
|
36
|
+
source: str = ""
|
|
37
|
+
stability_status: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SourceRow:
|
|
42
|
+
provider: str
|
|
43
|
+
source: str
|
|
44
|
+
shap_sum: Optional[float] = None
|
|
45
|
+
feature_count: Optional[int] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SearchResultsSummary:
|
|
50
|
+
relevant_features: Optional[int] = None
|
|
51
|
+
model_features: Optional[int] = None
|
|
52
|
+
data_sources: Optional[int] = None
|
|
53
|
+
stable_features_share: Optional[float] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ModelFeatureShap:
|
|
58
|
+
rank: int
|
|
59
|
+
feature: str
|
|
60
|
+
provider: str = ""
|
|
61
|
+
mean_abs_shap: Optional[float] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ReportData:
|
|
66
|
+
metadata: ReportMetadata
|
|
67
|
+
quality_by_sample: list[QualitySample] = field(default_factory=list)
|
|
68
|
+
summary: SearchResultsSummary = field(default_factory=SearchResultsSummary)
|
|
69
|
+
features: list[FeatureRow] = field(default_factory=list)
|
|
70
|
+
sources: list[SourceRow] = field(default_factory=list)
|
|
71
|
+
autofe: list[dict[str, str]] = field(default_factory=list)
|
|
72
|
+
model_feature_shap: list[ModelFeatureShap] = field(default_factory=list)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from upgini.report.data import FeatureRow, QualitySample, ReportData, SearchResultsSummary, SourceRow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_html_report(data: ReportData) -> str:
|
|
10
|
+
meta = data.metadata
|
|
11
|
+
keys = ", ".join(meta.search_keys) if meta.search_keys else "—"
|
|
12
|
+
samples = ", ".join(meta.samples) if meta.samples else "—"
|
|
13
|
+
duration = meta.search_duration or "—"
|
|
14
|
+
rows = f"{meta.reference_rows:,}" if meta.reference_rows is not None else "—"
|
|
15
|
+
return f"""<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>Upgini search report {escape(meta.search_id)}</title>
|
|
21
|
+
<style>
|
|
22
|
+
:root {{
|
|
23
|
+
--bg: #f4f1ea;
|
|
24
|
+
--ink: #1c1917;
|
|
25
|
+
--muted: #57534e;
|
|
26
|
+
--card: #fffdf8;
|
|
27
|
+
--line: #e7e0d4;
|
|
28
|
+
--accent: #0f766e;
|
|
29
|
+
--accent-ink: #134e4a;
|
|
30
|
+
--chip: #ecfdf5;
|
|
31
|
+
}}
|
|
32
|
+
* {{ box-sizing: border-box; }}
|
|
33
|
+
body {{
|
|
34
|
+
margin: 0;
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
font-family: "Source Sans 3", "Segoe UI", Helvetica, Arial, sans-serif;
|
|
38
|
+
line-height: 1.45;
|
|
39
|
+
}}
|
|
40
|
+
header {{
|
|
41
|
+
background: #1c1917;
|
|
42
|
+
color: #fafaf9;
|
|
43
|
+
padding: 28px 32px 24px;
|
|
44
|
+
}}
|
|
45
|
+
header .kicker {{
|
|
46
|
+
letter-spacing: 0.12em;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
font-size: 12px;
|
|
49
|
+
color: #a8a29e;
|
|
50
|
+
margin-bottom: 8px;
|
|
51
|
+
}}
|
|
52
|
+
h1 {{ margin: 0 0 12px; font-size: 28px; font-weight: 600; }}
|
|
53
|
+
.chips {{ display: flex; flex-wrap: wrap; gap: 8px; }}
|
|
54
|
+
.chip {{
|
|
55
|
+
background: #292524;
|
|
56
|
+
color: #e7e5e4;
|
|
57
|
+
border-radius: 999px;
|
|
58
|
+
padding: 4px 10px;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
}}
|
|
61
|
+
main {{ max-width: 1080px; margin: 0 auto; padding: 24px 20px 48px; }}
|
|
62
|
+
section {{
|
|
63
|
+
background: var(--card);
|
|
64
|
+
border: 1px solid var(--line);
|
|
65
|
+
border-radius: 12px;
|
|
66
|
+
padding: 20px 22px;
|
|
67
|
+
margin-bottom: 16px;
|
|
68
|
+
}}
|
|
69
|
+
h2 {{ margin: 0 0 14px; font-size: 18px; }}
|
|
70
|
+
.meta-grid, .cards {{
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
73
|
+
gap: 12px;
|
|
74
|
+
}}
|
|
75
|
+
.meta-item .label, .card .label {{
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
letter-spacing: 0.04em;
|
|
80
|
+
}}
|
|
81
|
+
.meta-item .value, .card .value {{
|
|
82
|
+
font-size: 20px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
margin-top: 4px;
|
|
85
|
+
}}
|
|
86
|
+
table {{
|
|
87
|
+
width: 100%;
|
|
88
|
+
border-collapse: collapse;
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
}}
|
|
91
|
+
th, td {{
|
|
92
|
+
text-align: left;
|
|
93
|
+
padding: 8px 10px;
|
|
94
|
+
border-bottom: 1px solid var(--line);
|
|
95
|
+
vertical-align: top;
|
|
96
|
+
}}
|
|
97
|
+
th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; }}
|
|
98
|
+
.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
|
|
99
|
+
.uplift {{ color: var(--accent-ink); font-weight: 600; }}
|
|
100
|
+
.empty {{ color: var(--muted); }}
|
|
101
|
+
</style>
|
|
102
|
+
</head>
|
|
103
|
+
<body>
|
|
104
|
+
<header>
|
|
105
|
+
<div class="kicker">Upgini search report</div>
|
|
106
|
+
<h1>FIT completed successfully</h1>
|
|
107
|
+
<div class="chips">
|
|
108
|
+
<span class="chip">search_id={escape(meta.search_id)}</span>
|
|
109
|
+
<span class="chip">{escape(keys)}</span>
|
|
110
|
+
</div>
|
|
111
|
+
</header>
|
|
112
|
+
<main>
|
|
113
|
+
<section>
|
|
114
|
+
<h2>Overview</h2>
|
|
115
|
+
<div class="meta-grid">
|
|
116
|
+
{_meta_item("Generated", meta.generated_at)}
|
|
117
|
+
{_meta_item("PyLib version", meta.pylib_version)}
|
|
118
|
+
{_meta_item("Search duration", duration)}
|
|
119
|
+
{_meta_item("Rows processed", rows)}
|
|
120
|
+
{_meta_item("Samples", samples)}
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
{_summary_section(data.summary)}
|
|
124
|
+
{_quality_section(data.quality_by_sample)}
|
|
125
|
+
{_features_section(data.features)}
|
|
126
|
+
{_sources_section(data.sources)}
|
|
127
|
+
{_autofe_section(data.autofe)}
|
|
128
|
+
</main>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _meta_item(label: str, value: str) -> str:
|
|
135
|
+
return (
|
|
136
|
+
f'<div class="meta-item"><div class="label">{escape(label)}</div>'
|
|
137
|
+
f'<div class="value">{escape(str(value))}</div></div>'
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _card(label: str, value) -> str:
|
|
142
|
+
return (
|
|
143
|
+
f'<div class="card"><div class="label">{escape(label)}</div>'
|
|
144
|
+
f'<div class="value">{escape(str(value))}</div></div>'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _dash(value: Optional[object]) -> str:
|
|
149
|
+
if value is None or value == "":
|
|
150
|
+
return "—"
|
|
151
|
+
return str(value)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _summary_section(summary: SearchResultsSummary) -> str:
|
|
155
|
+
if all(
|
|
156
|
+
value is None
|
|
157
|
+
for value in (
|
|
158
|
+
summary.relevant_features,
|
|
159
|
+
summary.model_features,
|
|
160
|
+
summary.data_sources,
|
|
161
|
+
summary.stable_features_share,
|
|
162
|
+
)
|
|
163
|
+
):
|
|
164
|
+
return ""
|
|
165
|
+
stable = (
|
|
166
|
+
f"{summary.stable_features_share * 100:.0f}%"
|
|
167
|
+
if summary.stable_features_share is not None
|
|
168
|
+
else "—"
|
|
169
|
+
)
|
|
170
|
+
return f"""<section>
|
|
171
|
+
<h2>Summary</h2>
|
|
172
|
+
<div class="cards">
|
|
173
|
+
{_card("Features found", _dash(summary.relevant_features))}
|
|
174
|
+
{_card("Used in model", _dash(summary.model_features))}
|
|
175
|
+
{_card("Data sources", _dash(summary.data_sources))}
|
|
176
|
+
{_card("Stable features", stable)}
|
|
177
|
+
</div>
|
|
178
|
+
</section>"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _quality_section(samples: list[QualitySample]) -> str:
|
|
182
|
+
if not samples:
|
|
183
|
+
return '<section><h2>Key result</h2><p class="empty">Metrics were not calculated for this search.</p></section>'
|
|
184
|
+
metric = samples[0].metric or "score"
|
|
185
|
+
rows = []
|
|
186
|
+
for sample in samples:
|
|
187
|
+
rows.append(
|
|
188
|
+
"<tr>"
|
|
189
|
+
f"<td>{escape(sample.evaluation_scope)}</td>"
|
|
190
|
+
f"<td class='num'>{escape(_dash(sample.baseline))}</td>"
|
|
191
|
+
f"<td class='num'>{escape(_dash(sample.enriched))}</td>"
|
|
192
|
+
f"<td class='num uplift'>{escape(_dash(sample.uplift))}</td>"
|
|
193
|
+
f"<td class='num uplift'>{escape(_dash(sample.relative_uplift))}</td>"
|
|
194
|
+
"</tr>"
|
|
195
|
+
)
|
|
196
|
+
return f"""<section>
|
|
197
|
+
<h2>Key result · {escape(metric)}</h2>
|
|
198
|
+
<table>
|
|
199
|
+
<thead>
|
|
200
|
+
<tr>
|
|
201
|
+
<th>Scope</th>
|
|
202
|
+
<th class="num">Baseline</th><th class="num">Enriched</th>
|
|
203
|
+
<th class="num">Uplift, abs</th><th class="num">Uplift, %</th>
|
|
204
|
+
</tr>
|
|
205
|
+
</thead>
|
|
206
|
+
<tbody>{''.join(rows)}</tbody>
|
|
207
|
+
</table>
|
|
208
|
+
</section>"""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _features_section(features: list[FeatureRow]) -> str:
|
|
212
|
+
if not features:
|
|
213
|
+
return ""
|
|
214
|
+
rows = []
|
|
215
|
+
for idx, feature in enumerate(features, start=1):
|
|
216
|
+
rows.append(
|
|
217
|
+
"<tr>"
|
|
218
|
+
f"<td class='num'>{idx}</td>"
|
|
219
|
+
f"<td>{escape(feature.name)}</td>"
|
|
220
|
+
f"<td>{escape(feature.provider)}</td>"
|
|
221
|
+
f"<td>{escape(feature.source)}</td>"
|
|
222
|
+
f"<td class='num'>{escape(_dash(feature.shap))}</td>"
|
|
223
|
+
f"<td class='num'>{escape(_dash(feature.psi))}</td>"
|
|
224
|
+
f"<td class='num'>{escape(_dash(feature.drift))}</td>"
|
|
225
|
+
f"<td class='num'>{escape(_dash(feature.coverage))}</td>"
|
|
226
|
+
"</tr>"
|
|
227
|
+
)
|
|
228
|
+
return f"""<section>
|
|
229
|
+
<h2>Search results · features</h2>
|
|
230
|
+
<table>
|
|
231
|
+
<thead>
|
|
232
|
+
<tr>
|
|
233
|
+
<th class="num">#</th><th>Feature</th><th>Provider</th><th>Source</th>
|
|
234
|
+
<th class="num">SHAP</th><th class="num">PSI</th>
|
|
235
|
+
<th class="num">Drift</th><th class="num">Coverage %</th>
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody>{''.join(rows)}</tbody>
|
|
239
|
+
</table>
|
|
240
|
+
</section>"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _sources_section(sources: list[SourceRow]) -> str:
|
|
244
|
+
if not sources:
|
|
245
|
+
return ""
|
|
246
|
+
rows = []
|
|
247
|
+
for source in sources:
|
|
248
|
+
rows.append(
|
|
249
|
+
"<tr>"
|
|
250
|
+
f"<td>{escape(source.provider)}</td>"
|
|
251
|
+
f"<td>{escape(source.source)}</td>"
|
|
252
|
+
f"<td class='num'>{escape(_dash(source.shap_sum))}</td>"
|
|
253
|
+
f"<td class='num'>{escape(_dash(source.feature_count))}</td>"
|
|
254
|
+
"</tr>"
|
|
255
|
+
)
|
|
256
|
+
return f"""<section>
|
|
257
|
+
<h2>Search results · sources</h2>
|
|
258
|
+
<table>
|
|
259
|
+
<thead>
|
|
260
|
+
<tr><th>Provider</th><th>Source</th><th class="num">SHAP sum</th><th class="num">Features</th></tr>
|
|
261
|
+
</thead>
|
|
262
|
+
<tbody>{''.join(rows)}</tbody>
|
|
263
|
+
</table>
|
|
264
|
+
</section>"""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _autofe_section(autofe: list[dict[str, str]]) -> str:
|
|
268
|
+
if not autofe:
|
|
269
|
+
return ""
|
|
270
|
+
columns = list(autofe[0].keys())
|
|
271
|
+
head = "".join(f"<th>{escape(col)}</th>" for col in columns)
|
|
272
|
+
rows = []
|
|
273
|
+
for item in autofe:
|
|
274
|
+
rows.append("<tr>" + "".join(f"<td>{escape(item.get(col, ''))}</td>" for col in columns) + "</tr>")
|
|
275
|
+
return f"""<section>
|
|
276
|
+
<h2>AutoFE</h2>
|
|
277
|
+
<table>
|
|
278
|
+
<thead><tr>{head}</tr></thead>
|
|
279
|
+
<tbody>{''.join(rows)}</tbody>
|
|
280
|
+
</table>
|
|
281
|
+
</section>"""
|
|
@@ -327,6 +327,37 @@ def prepare_and_show_report(
|
|
|
327
327
|
return show_button_download_pdf(report, display_id=display_id, display_handle=display_handle)
|
|
328
328
|
|
|
329
329
|
|
|
330
|
+
def show_button_open_report(
|
|
331
|
+
path: str, title="Open full report", display_id: Optional[str] = None, display_handle=None
|
|
332
|
+
):
|
|
333
|
+
import webbrowser
|
|
334
|
+
from pathlib import Path
|
|
335
|
+
|
|
336
|
+
from IPython.display import HTML, display
|
|
337
|
+
from ipywidgets import Button, Layout
|
|
338
|
+
|
|
339
|
+
resolved = Path(path).resolve()
|
|
340
|
+
path_html = f'<div style="margin-top:6px;font-size:12px;color:#57534e">{resolved}</div>'
|
|
341
|
+
link_html = f"""<div>
|
|
342
|
+
<a href="{resolved.as_uri()}" target="_blank" rel="noopener noreferrer">
|
|
343
|
+
<button type="button">{title}</button>
|
|
344
|
+
</a>
|
|
345
|
+
{path_html}
|
|
346
|
+
</div>"""
|
|
347
|
+
if display_handle is not None:
|
|
348
|
+
display_handle.update(HTML(link_html))
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
button = Button(description=title, layout=Layout(width="auto"))
|
|
352
|
+
|
|
353
|
+
def on_click(b):
|
|
354
|
+
webbrowser.open(resolved.as_uri())
|
|
355
|
+
|
|
356
|
+
button.on_click(on_click)
|
|
357
|
+
display(button)
|
|
358
|
+
return display(HTML(path_html), display_id=display_id)
|
|
359
|
+
|
|
360
|
+
|
|
330
361
|
def show_button_download_pdf(
|
|
331
362
|
source: str, title="\U0001f4ca Download PDF report", display_id: Optional[str] = None, display_handle=None
|
|
332
363
|
):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.166.a4253.dev2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/ads_management/ads_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/numpy_kernels.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/volatility.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/normalizer/normalize_utils.py
RENAMED
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/__init__.py
RENAMED
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/exceptions.py
RENAMED
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/strings.properties
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/random_under_sampler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/base_search_key_detector.py
RENAMED
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/blocked_time_series.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/fallback_progress_bar.py
RENAMED
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/features_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.166a4253.dev2 → upgini-1.2.167a4220.dev1}/src/upgini/utils/search_key_derivations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|