upgini 1.2.166a4253.dev1__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.dev1 → upgini-1.2.167a4220.dev1}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev1/src/upgini/__about__.py +1 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/features_enricher.py +247 -50
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/metadata.py +4 -0
- 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.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/display_utils.py +31 -0
- upgini-1.2.166a4253.dev1/src/upgini/__about__.py +0 -1
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/.gitignore +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/LICENSE +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/README.md +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/pyproject.toml +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/ads.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/dataset.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/errors.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/http.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/metrics.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/search_task.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/spinner.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.166a4253.dev1 → upgini-1.2.167a4220.dev1}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.166a4253.dev1 → 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"
|
|
@@ -32,6 +32,7 @@ from sklearn.preprocessing import OrdinalEncoder
|
|
|
32
32
|
|
|
33
33
|
from upgini.autofe.feature import Feature
|
|
34
34
|
from upgini.autofe.timeseries import TimeSeriesBase
|
|
35
|
+
from upgini.autofe.utils import pydantic_dump_method
|
|
35
36
|
from upgini.autofe.vector import EnsembleModel
|
|
36
37
|
from upgini.data_source.data_source_publisher import CommercialSchema
|
|
37
38
|
from upgini.dataset import Dataset
|
|
@@ -91,10 +92,14 @@ from upgini.utils.deduplicate_utils import (
|
|
|
91
92
|
clean_full_duplicates,
|
|
92
93
|
remove_fintech_duplicates,
|
|
93
94
|
)
|
|
95
|
+
from upgini.report.assemble import assemble_report_data
|
|
96
|
+
from upgini.report.html import generate_html_report
|
|
94
97
|
from upgini.utils.display_utils import (
|
|
95
98
|
display_html_dataframe,
|
|
96
99
|
do_without_pandas_limits,
|
|
100
|
+
ipython_available,
|
|
97
101
|
prepare_and_show_report,
|
|
102
|
+
show_button_open_report,
|
|
98
103
|
show_request_quote_button,
|
|
99
104
|
)
|
|
100
105
|
from upgini.utils.email_utils import (
|
|
@@ -174,6 +179,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
174
179
|
|
|
175
180
|
shared_datasets: list of str, optional (default=None)
|
|
176
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.
|
|
177
185
|
"""
|
|
178
186
|
|
|
179
187
|
TARGET_NAME = "target"
|
|
@@ -251,6 +259,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
251
259
|
sample_config: SampleConfig | None = None,
|
|
252
260
|
print_trace_id: bool = False,
|
|
253
261
|
print_loaded_report: bool = True,
|
|
262
|
+
reports_path: str | None = None,
|
|
254
263
|
**kwargs,
|
|
255
264
|
):
|
|
256
265
|
self.bundle = get_custom_bundle(custom_bundle_config)
|
|
@@ -275,6 +284,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
275
284
|
|
|
276
285
|
self.passed_features: list[str] = []
|
|
277
286
|
self.df_with_original_index: pd.DataFrame | None = None
|
|
287
|
+
self._fit_match_search_keys: dict[str, SearchKey] | None = None
|
|
278
288
|
self.fit_columns_renaming: dict[str, str] | None = None
|
|
279
289
|
self.country_added = False
|
|
280
290
|
self.fit_generated_features: list[str] = []
|
|
@@ -308,6 +318,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
308
318
|
self.relevant_data_sources: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
309
319
|
self._relevant_data_sources_wo_links: pd.DataFrame = self.EMPTY_DATA_SOURCES
|
|
310
320
|
self.metrics: pd.DataFrame | None = None
|
|
321
|
+
self.metrics_metric_name: str | None = None
|
|
311
322
|
self.feature_names_ = []
|
|
312
323
|
self.external_source_feature_names = []
|
|
313
324
|
self.feature_importances_ = []
|
|
@@ -317,6 +328,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
317
328
|
self.disable_force_downsampling = disable_force_downsampling
|
|
318
329
|
self.print_trace_id = print_trace_id
|
|
319
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
|
|
320
334
|
|
|
321
335
|
if search_id:
|
|
322
336
|
search_task = SearchTask(search_id, rest_client=self.rest_client, logger=self.logger)
|
|
@@ -1136,18 +1150,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1136
1150
|
uplift_perc = None
|
|
1137
1151
|
enriched_estimator = None
|
|
1138
1152
|
ensemble_score_column = self._get_ensemble_score_column(fitting_X, fitting_enriched_X)
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
f"Only ensemble model returned; calculate enriched {metric} via roc_auc "
|
|
1143
|
-
f"on ensemble score column: {ensemble_score_column}"
|
|
1144
|
-
)
|
|
1145
|
-
else:
|
|
1146
|
-
self.logger.info(
|
|
1147
|
-
f"Calculate enriched {metric} on train combined "
|
|
1148
|
-
f"features: {fitting_enriched_X.columns.to_list()}"
|
|
1149
|
-
)
|
|
1150
|
-
enriched_estimator = EstimatorWrapper.create(
|
|
1153
|
+
|
|
1154
|
+
def evaluate_enriched(score_column, description: str):
|
|
1155
|
+
wrapper = EstimatorWrapper.create(
|
|
1151
1156
|
estimator,
|
|
1152
1157
|
self.logger,
|
|
1153
1158
|
model_task_type,
|
|
@@ -1159,22 +1164,48 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1159
1164
|
text_features=text_features,
|
|
1160
1165
|
has_time=has_time,
|
|
1161
1166
|
)
|
|
1162
|
-
|
|
1163
|
-
fitting_enriched_X, enriched_y_sorted,
|
|
1167
|
+
cv_result = wrapper.cross_val_predict(
|
|
1168
|
+
fitting_enriched_X, enriched_y_sorted, score_column
|
|
1164
1169
|
)
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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")
|
|
1178
1209
|
|
|
1179
1210
|
train_metrics = {
|
|
1180
1211
|
self.bundle.get("quality_metrics_segment_header"): self.bundle.get(
|
|
@@ -1321,6 +1352,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1321
1352
|
elif uplift_col in metrics_df.columns and (metrics_df[uplift_col] < 0).any():
|
|
1322
1353
|
self.logger.warning("Uplift is negative")
|
|
1323
1354
|
|
|
1355
|
+
self.metrics_metric_name = metric
|
|
1324
1356
|
return metrics_df
|
|
1325
1357
|
except Exception as e:
|
|
1326
1358
|
error_message = "Failed to calculate metrics" + (
|
|
@@ -1759,10 +1791,12 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1759
1791
|
rows_to_drop: pd.DataFrame | None = None,
|
|
1760
1792
|
left_columns: list[str] | None = None,
|
|
1761
1793
|
) -> tuple[pd.DataFrame, list[str], dict[str, SearchKey]] | None:
|
|
1762
|
-
if self.
|
|
1794
|
+
if self._search_task is None:
|
|
1763
1795
|
return None
|
|
1764
1796
|
if self.fit_columns_renaming is None or self.fit_search_keys is None:
|
|
1765
1797
|
return None
|
|
1798
|
+
if self.df_with_original_index is None and not self.__has_persisted_match_hashes():
|
|
1799
|
+
return None
|
|
1766
1800
|
|
|
1767
1801
|
fit_features = self._search_task.get_all_initial_raw_features(self._get_trace_id(), metrics_calculation=True)
|
|
1768
1802
|
if fit_features is None or len(fit_features) == 0:
|
|
@@ -1775,6 +1809,12 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1775
1809
|
]
|
|
1776
1810
|
self.logger.info(f"After dropping target outliers size: {len(fit_features)}")
|
|
1777
1811
|
|
|
1812
|
+
generated_features = [self.fit_columns_renaming.get(c, c) for c in (self.fit_generated_features or [])]
|
|
1813
|
+
search_keys = {self.fit_columns_renaming.get(k, k): v for k, v in self.fit_search_keys.items()}
|
|
1814
|
+
|
|
1815
|
+
if self.df_with_original_index is None:
|
|
1816
|
+
return fit_features.rename(columns=self.fit_columns_renaming), generated_features, search_keys
|
|
1817
|
+
|
|
1778
1818
|
left = self.df_with_original_index
|
|
1779
1819
|
if left_columns is not None:
|
|
1780
1820
|
left = left.loc[:, left_columns]
|
|
@@ -1791,10 +1831,80 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1791
1831
|
drop_system_record_id=False,
|
|
1792
1832
|
)
|
|
1793
1833
|
enriched_Xy = enriched_Xy.rename(columns=self.fit_columns_renaming)
|
|
1794
|
-
generated_features = [self.fit_columns_renaming.get(c, c) for c in (self.fit_generated_features or [])]
|
|
1795
|
-
search_keys = {self.fit_columns_renaming.get(k, k): v for k, v in self.fit_search_keys.items()}
|
|
1796
1834
|
return enriched_Xy, generated_features, search_keys
|
|
1797
1835
|
|
|
1836
|
+
def __has_persisted_match_hashes(self) -> bool:
|
|
1837
|
+
if self.add_info is None:
|
|
1838
|
+
return False
|
|
1839
|
+
entity_ids = self.add_info.transform_match_entity_ids
|
|
1840
|
+
hashes = self.add_info.transform_match_hashes
|
|
1841
|
+
return bool(entity_ids) and bool(hashes) and len(entity_ids) == len(hashes)
|
|
1842
|
+
|
|
1843
|
+
def __reuse_match_columns(self, df: pd.DataFrame, search_keys: dict[str, SearchKey]) -> list[str] | None:
|
|
1844
|
+
if self._search_task is None:
|
|
1845
|
+
return None
|
|
1846
|
+
features_for_transform = [
|
|
1847
|
+
f
|
|
1848
|
+
for f in self._search_task.get_features_for_transform()
|
|
1849
|
+
if f not in get_derived_search_key_columns(search_keys)
|
|
1850
|
+
]
|
|
1851
|
+
renaming = self.fit_columns_renaming or {}
|
|
1852
|
+
reverse = {v: k for k, v in renaming.items()}
|
|
1853
|
+
resolved: list[str] = []
|
|
1854
|
+
for feature in features_for_transform:
|
|
1855
|
+
col = feature if feature in df.columns else reverse.get(feature)
|
|
1856
|
+
if col is None or col not in df.columns:
|
|
1857
|
+
self.logger.info(f"Skip persisting transform match hashes: missing column {feature}")
|
|
1858
|
+
return None
|
|
1859
|
+
if col not in search_keys:
|
|
1860
|
+
resolved.append(col)
|
|
1861
|
+
match_cols = self.__transform_match_columns(search_keys, resolved)
|
|
1862
|
+
missing = [c for c in match_cols if c not in df.columns]
|
|
1863
|
+
if missing:
|
|
1864
|
+
self.logger.info(f"Skip persisting transform match hashes: missing columns {missing}")
|
|
1865
|
+
return None
|
|
1866
|
+
return match_cols
|
|
1867
|
+
|
|
1868
|
+
def __persist_transform_match_hashes(self) -> None:
|
|
1869
|
+
if self.add_info is None or self.df_with_original_index is None or self._search_task is None:
|
|
1870
|
+
return
|
|
1871
|
+
if ENTITY_SYSTEM_RECORD_ID not in self.df_with_original_index.columns:
|
|
1872
|
+
return
|
|
1873
|
+
match_cols = self.__reuse_match_columns(
|
|
1874
|
+
self.df_with_original_index,
|
|
1875
|
+
self._fit_match_search_keys or self.fit_search_keys or {},
|
|
1876
|
+
)
|
|
1877
|
+
if not match_cols:
|
|
1878
|
+
return
|
|
1879
|
+
match_hash = pd.util.hash_pandas_object(self.df_with_original_index[match_cols], index=False).astype("float64")
|
|
1880
|
+
pairs = pd.DataFrame(
|
|
1881
|
+
{
|
|
1882
|
+
ENTITY_SYSTEM_RECORD_ID: self.df_with_original_index[ENTITY_SYSTEM_RECORD_ID].to_numpy(),
|
|
1883
|
+
MATCH_HASH: match_hash.to_numpy(),
|
|
1884
|
+
}
|
|
1885
|
+
)
|
|
1886
|
+
pairs = self.__dedupe_on(pairs, [ENTITY_SYSTEM_RECORD_ID])
|
|
1887
|
+
pairs = self.__dedupe_on(pairs, [MATCH_HASH])
|
|
1888
|
+
self.add_info.transform_match_entity_ids = [float(v) for v in pairs[ENTITY_SYSTEM_RECORD_ID].to_numpy()]
|
|
1889
|
+
self.add_info.transform_match_hashes = [float(v) for v in pairs[MATCH_HASH].to_numpy()]
|
|
1890
|
+
try:
|
|
1891
|
+
payload = pydantic_dump_method(self.add_info)(exclude_none=True)
|
|
1892
|
+
payload.pop("autodetected_search_keys", None)
|
|
1893
|
+
self._search_task.update_add_info(self._get_trace_id(), payload)
|
|
1894
|
+
except Exception:
|
|
1895
|
+
self.logger.exception("Failed to persist transform match hashes in add_info")
|
|
1896
|
+
|
|
1897
|
+
def __persisted_hash_pairs(self) -> pd.DataFrame | None:
|
|
1898
|
+
if not self.__has_persisted_match_hashes():
|
|
1899
|
+
return None
|
|
1900
|
+
pairs = pd.DataFrame(
|
|
1901
|
+
{
|
|
1902
|
+
ENTITY_SYSTEM_RECORD_ID: self.add_info.transform_match_entity_ids,
|
|
1903
|
+
MATCH_HASH: self.add_info.transform_match_hashes,
|
|
1904
|
+
}
|
|
1905
|
+
)
|
|
1906
|
+
return self.__dedupe_on(pairs, [MATCH_HASH])
|
|
1907
|
+
|
|
1798
1908
|
@staticmethod
|
|
1799
1909
|
def __transform_match_columns(search_keys: dict[str, SearchKey], features_for_transform: list[str]) -> list[str]:
|
|
1800
1910
|
return sorted(dict.fromkeys(list(search_keys.keys()) + list(features_for_transform)))
|
|
@@ -1831,41 +1941,50 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1831
1941
|
feature_cols: list[str],
|
|
1832
1942
|
enriched_Xy: pd.DataFrame,
|
|
1833
1943
|
) -> pd.DataFrame | None:
|
|
1834
|
-
if
|
|
1944
|
+
if enriched_Xy is None or len(enriched_Xy) == 0:
|
|
1835
1945
|
return None
|
|
1836
1946
|
if not match_columns_hashed or not feature_cols:
|
|
1837
1947
|
return None
|
|
1838
1948
|
if ENTITY_SYSTEM_RECORD_ID not in enriched_Xy.columns:
|
|
1839
1949
|
return None
|
|
1840
|
-
if ENTITY_SYSTEM_RECORD_ID not in self.df_with_original_index.columns:
|
|
1841
|
-
return None
|
|
1842
1950
|
|
|
1843
1951
|
ads_cols = [c for c in feature_cols if c in enriched_Xy.columns]
|
|
1844
1952
|
if not ads_cols:
|
|
1845
1953
|
return None
|
|
1846
1954
|
|
|
1847
|
-
key_cols = [c for c in match_columns_hashed if c in self.df_with_original_index.columns]
|
|
1848
|
-
if len(key_cols) != len(match_columns_hashed):
|
|
1849
|
-
return None
|
|
1850
|
-
|
|
1851
1955
|
ads = self.__dedupe_on(enriched_Xy.loc[:, [ENTITY_SYSTEM_RECORD_ID] + ads_cols], [ENTITY_SYSTEM_RECORD_ID])
|
|
1852
1956
|
covered_ids = ads[ENTITY_SYSTEM_RECORD_ID].to_numpy()
|
|
1853
1957
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1958
|
+
if self.df_with_original_index is not None and ENTITY_SYSTEM_RECORD_ID in self.df_with_original_index.columns:
|
|
1959
|
+
key_cols = [c for c in match_columns_hashed if c in self.df_with_original_index.columns]
|
|
1960
|
+
if len(key_cols) != len(match_columns_hashed):
|
|
1961
|
+
return None
|
|
1962
|
+
fit_keys = self.df_with_original_index.loc[:, key_cols + [ENTITY_SYSTEM_RECORD_ID]]
|
|
1963
|
+
covered_mask = fit_keys[ENTITY_SYSTEM_RECORD_ID].isin(covered_ids)
|
|
1964
|
+
if not covered_mask.all():
|
|
1965
|
+
fit_keys = fit_keys.loc[covered_mask]
|
|
1966
|
+
fit_keys = self.__dedupe_on(fit_keys, [ENTITY_SYSTEM_RECORD_ID])
|
|
1967
|
+
if len(fit_keys) == 0:
|
|
1968
|
+
return None
|
|
1969
|
+
aligned = self.__align_match_frame(fit_keys, df_transform, match_columns_hashed)
|
|
1970
|
+
if aligned is None:
|
|
1971
|
+
return None
|
|
1972
|
+
match_hash = pd.util.hash_pandas_object(aligned, index=False).astype("float64")
|
|
1973
|
+
lookup = ads.set_index(ENTITY_SYSTEM_RECORD_ID).reindex(fit_keys[ENTITY_SYSTEM_RECORD_ID].to_numpy())
|
|
1974
|
+
lookup[MATCH_HASH] = match_hash.to_numpy()
|
|
1975
|
+
lookup.reset_index(drop=True, inplace=True)
|
|
1976
|
+
return self.__dedupe_on(lookup, [MATCH_HASH])
|
|
1977
|
+
|
|
1978
|
+
pairs = self.__persisted_hash_pairs()
|
|
1979
|
+
if pairs is None:
|
|
1860
1980
|
return None
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1981
|
+
covered_mask = pairs[ENTITY_SYSTEM_RECORD_ID].isin(covered_ids)
|
|
1982
|
+
if not covered_mask.all():
|
|
1983
|
+
pairs = pairs.loc[covered_mask]
|
|
1984
|
+
if len(pairs) == 0:
|
|
1864
1985
|
return None
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
lookup = ads.set_index(ENTITY_SYSTEM_RECORD_ID).reindex(fit_keys[ENTITY_SYSTEM_RECORD_ID].to_numpy())
|
|
1868
|
-
lookup[MATCH_HASH] = match_hash.to_numpy()
|
|
1986
|
+
lookup = ads.set_index(ENTITY_SYSTEM_RECORD_ID).reindex(pairs[ENTITY_SYSTEM_RECORD_ID].to_numpy())
|
|
1987
|
+
lookup[MATCH_HASH] = pairs[MATCH_HASH].to_numpy()
|
|
1869
1988
|
lookup.reset_index(drop=True, inplace=True)
|
|
1870
1989
|
return self.__dedupe_on(lookup, [MATCH_HASH])
|
|
1871
1990
|
|
|
@@ -2135,6 +2254,69 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2135
2254
|
column = new_external[0]
|
|
2136
2255
|
return column if self._is_ensemble_feature(column) else None
|
|
2137
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
|
+
|
|
2138
2320
|
def _is_ensemble_feature(self, column_name: str) -> bool:
|
|
2139
2321
|
renaming = self.fit_columns_renaming or {}
|
|
2140
2322
|
aliases = self._column_name_aliases([column_name], renaming)
|
|
@@ -4034,8 +4216,10 @@ if response.status_code == 200:
|
|
|
4034
4216
|
self._search_task = None
|
|
4035
4217
|
self.warning_counter.reset()
|
|
4036
4218
|
self.df_with_original_index = None
|
|
4219
|
+
self._fit_match_search_keys = None
|
|
4037
4220
|
self.__cached_sampled_datasets = dict()
|
|
4038
4221
|
self.metrics = None
|
|
4222
|
+
self.metrics_metric_name = None
|
|
4039
4223
|
self.fit_columns_renaming = None
|
|
4040
4224
|
self.fit_dropped_features = set()
|
|
4041
4225
|
self.fit_generated_features = []
|
|
@@ -4280,6 +4464,8 @@ if response.status_code == 200:
|
|
|
4280
4464
|
|
|
4281
4465
|
# TODO check that this is correct for enrichment
|
|
4282
4466
|
self.df_with_original_index = df.copy()
|
|
4467
|
+
# Search keys at this point are pre-explode; transform hashes the same grain.
|
|
4468
|
+
self._fit_match_search_keys = dict(self.fit_search_keys)
|
|
4283
4469
|
# TODO check maybe need to drop _time column from df_with_original_index
|
|
4284
4470
|
|
|
4285
4471
|
df, unnest_search_keys = self._explode_multiple_search_keys(df, self.fit_search_keys, self.fit_columns_renaming)
|
|
@@ -4443,6 +4629,8 @@ if response.status_code == 200:
|
|
|
4443
4629
|
raise e
|
|
4444
4630
|
|
|
4445
4631
|
self._search_task.poll_result(self._get_trace_id(), quiet=True)
|
|
4632
|
+
self.__persist_transform_match_hashes()
|
|
4633
|
+
self.search_duration_seconds = time.time() - start_time
|
|
4446
4634
|
|
|
4447
4635
|
seconds_left = time.time() - start_time
|
|
4448
4636
|
progress = SearchProgress(97.0, ProgressStage.GENERATING_REPORT, seconds_left)
|
|
@@ -5849,6 +6037,15 @@ if response.status_code == 200:
|
|
|
5849
6037
|
|
|
5850
6038
|
def __show_report_button(self, display_id: str | None = None, display_handle=None):
|
|
5851
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
|
+
|
|
5852
6049
|
eval_sets_drift_df = self._get_eval_sets_drift_summary()
|
|
5853
6050
|
|
|
5854
6051
|
return prepare_and_show_report(
|
|
@@ -395,6 +395,10 @@ class AddInfo(BaseModel):
|
|
|
395
395
|
true_one_hot_groups: Optional[Dict[str, List[str]]] = None
|
|
396
396
|
pseudo_one_hot_groups: Optional[Dict[str, List[str]]] = None
|
|
397
397
|
autodetected_search_keys: Optional[Dict[str, SearchKey]] = None
|
|
398
|
+
# Unique pre-explode match hashes for transform reuse after restore (search_id).
|
|
399
|
+
# Parallel lists: entity_system_record_id[i] <-> hash[i].
|
|
400
|
+
transform_match_entity_ids: Optional[List[float]] = None
|
|
401
|
+
transform_match_hashes: Optional[List[float]] = None
|
|
398
402
|
|
|
399
403
|
|
|
400
404
|
class AdsHintType(str, Enum):
|
|
@@ -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)
|