upgini 1.2.167a4220.dev5__tar.gz → 1.2.167a4220.dev7__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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev7/src/upgini/__about__.py +1 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/features_enricher.py +69 -26
- upgini-1.2.167a4220.dev7/src/upgini/report/assemble.py +296 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/report/data.py +2 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/report/html.py +42 -8
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/report/template.html +1 -63
- upgini-1.2.167a4220.dev5/src/upgini/__about__.py +0 -1
- upgini-1.2.167a4220.dev5/src/upgini/report/assemble.py +0 -145
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/.gitignore +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/LICENSE +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/README.md +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/pyproject.toml +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/ads.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/dataset.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/errors.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/http.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/metadata.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/metrics.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/report/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/report/stats.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/search_task.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/spinner.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.
|
|
3
|
+
Version: 1.2.167a4220.dev7
|
|
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.dev7"
|
|
@@ -98,7 +98,7 @@ from upgini.utils.deduplicate_utils import (
|
|
|
98
98
|
clean_full_duplicates,
|
|
99
99
|
remove_fintech_duplicates,
|
|
100
100
|
)
|
|
101
|
-
from upgini.report.assemble import assemble_report_data
|
|
101
|
+
from upgini.report.assemble import assemble_report_data, autofe_rows_from_description, build_search_results
|
|
102
102
|
from upgini.report.html import generate_html_report
|
|
103
103
|
from upgini.report.stats import make_report_frame
|
|
104
104
|
from upgini.utils.display_utils import (
|
|
@@ -2292,17 +2292,25 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2292
2292
|
ensemble = [name for name in selected if self._is_ensemble_feature(name)]
|
|
2293
2293
|
if len(ensemble) != 1:
|
|
2294
2294
|
return None
|
|
2295
|
-
|
|
2295
|
+
renaming = self.fit_columns_renaming or {}
|
|
2296
|
+
allowed = self._column_name_aliases([ensemble[0]], renaming) | self._etalon_feature_aliases()
|
|
2297
|
+
if any(name not in allowed for name in selected):
|
|
2298
|
+
return None
|
|
2299
|
+
return ensemble[0]
|
|
2300
|
+
|
|
2301
|
+
def _etalon_feature_aliases(self) -> set[str]:
|
|
2302
|
+
renaming = self.fit_columns_renaming or {}
|
|
2303
|
+
names: set[str] = set()
|
|
2296
2304
|
if self._search_task is not None:
|
|
2297
|
-
|
|
2305
|
+
names.update(
|
|
2298
2306
|
meta.name
|
|
2299
2307
|
for meta in (self._search_task.get_all_features_metadata_v2() or [])
|
|
2300
2308
|
if meta.source == "etalon"
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
return
|
|
2309
|
+
)
|
|
2310
|
+
if isinstance(self.X, pd.DataFrame):
|
|
2311
|
+
generated = self._column_name_aliases(self.fit_generated_features or [], renaming)
|
|
2312
|
+
names.update(column for column in self.X.columns if column not in generated)
|
|
2313
|
+
return self._column_name_aliases(names, renaming)
|
|
2306
2314
|
|
|
2307
2315
|
def _reference_rows(self) -> int | None:
|
|
2308
2316
|
if self.X is None:
|
|
@@ -2437,6 +2445,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2437
2445
|
return max(self.__cached_sampled_datasets.values(), key=lambda item: len(item[0]) if item[0] is not None else 0)
|
|
2438
2446
|
|
|
2439
2447
|
def _assemble_report_data(self):
|
|
2448
|
+
features, sources, model_shap, summary, autofe = self._report_search_results()
|
|
2440
2449
|
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2441
2450
|
return assemble_report_data(
|
|
2442
2451
|
search_id=search_id,
|
|
@@ -2446,13 +2455,31 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2446
2455
|
samples=self._report_sample_names(),
|
|
2447
2456
|
metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
|
|
2448
2457
|
metric_name=self.metrics_metric_name,
|
|
2449
|
-
model_features=
|
|
2458
|
+
model_features=summary.model_features,
|
|
2450
2459
|
is_binary=self.model_task_type == ModelTaskType.BINARY,
|
|
2451
2460
|
dataset_samples=self._report_dataset_samples(),
|
|
2452
2461
|
scored_samples=self._report_scored_samples(),
|
|
2453
2462
|
bundle=self.bundle,
|
|
2463
|
+
summary=summary,
|
|
2464
|
+
features=features,
|
|
2465
|
+
sources=sources,
|
|
2466
|
+
autofe=autofe,
|
|
2467
|
+
model_feature_shap=model_shap,
|
|
2454
2468
|
)
|
|
2455
2469
|
|
|
2470
|
+
def _report_search_results(self):
|
|
2471
|
+
features_meta = []
|
|
2472
|
+
if self._search_task is not None:
|
|
2473
|
+
features_meta = list(self._search_task.get_all_features_metadata_v2() or [])
|
|
2474
|
+
features, sources, model_shap, summary = build_search_results(
|
|
2475
|
+
model_names=self._ensemble_model_feature_names(),
|
|
2476
|
+
features_meta=features_meta,
|
|
2477
|
+
base_columns=self._ensemble_base_columns(),
|
|
2478
|
+
is_ensemble=self._is_ensemble_feature,
|
|
2479
|
+
)
|
|
2480
|
+
autofe = autofe_rows_from_description(self.get_autofe_features_description(), self.bundle)
|
|
2481
|
+
return features, sources, model_shap, summary, autofe
|
|
2482
|
+
|
|
2456
2483
|
def _score_report_html(self) -> str | None:
|
|
2457
2484
|
if self._search_task is None or not self._has_single_ensemble_score():
|
|
2458
2485
|
return None
|
|
@@ -2493,13 +2520,24 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2493
2520
|
names.add(meta.alias)
|
|
2494
2521
|
return names
|
|
2495
2522
|
|
|
2496
|
-
def
|
|
2497
|
-
|
|
2498
|
-
column.original_name
|
|
2523
|
+
def _ensemble_base_columns(self) -> list[tuple[str, str]]:
|
|
2524
|
+
return [
|
|
2525
|
+
(column.original_name, column.hashed_name)
|
|
2499
2526
|
for meta, _ in self._ensemble_generated_metadata()
|
|
2500
2527
|
for column in meta.base_columns
|
|
2501
|
-
|
|
2502
|
-
|
|
2528
|
+
]
|
|
2529
|
+
|
|
2530
|
+
def _ensemble_model_feature_names(self) -> list[str]:
|
|
2531
|
+
return sorted(
|
|
2532
|
+
{
|
|
2533
|
+
original
|
|
2534
|
+
for original, hashed in self._ensemble_base_columns()
|
|
2535
|
+
if not self._is_ensemble_feature(original) and not self._is_ensemble_feature(hashed)
|
|
2536
|
+
}
|
|
2537
|
+
)
|
|
2538
|
+
|
|
2539
|
+
def _ensemble_model_feature_count(self) -> int | None:
|
|
2540
|
+
return len(self._ensemble_model_feature_names()) or None
|
|
2503
2541
|
|
|
2504
2542
|
def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
|
|
2505
2543
|
return psi is not None and not pd.isna(psi) and psi > threshold
|
|
@@ -6194,19 +6232,24 @@ if response.status_code == 200:
|
|
|
6194
6232
|
print(self._internal_features_info)
|
|
6195
6233
|
|
|
6196
6234
|
def __show_report_button(self, display_id: str | None = None, display_handle=None):
|
|
6235
|
+
if self._has_single_ensemble_score():
|
|
6236
|
+
try:
|
|
6237
|
+
report_html = self._score_report_html()
|
|
6238
|
+
if report_html:
|
|
6239
|
+
if not ipython_available():
|
|
6240
|
+
return
|
|
6241
|
+
search_id = (
|
|
6242
|
+
self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
6243
|
+
)
|
|
6244
|
+
return show_button_open_report(
|
|
6245
|
+
report_html,
|
|
6246
|
+
download_name=f"upgini-report-{search_id}.html",
|
|
6247
|
+
display_id=display_id,
|
|
6248
|
+
display_handle=display_handle,
|
|
6249
|
+
)
|
|
6250
|
+
except Exception:
|
|
6251
|
+
self.logger.exception("Failed to generate HTML report, falling back to PDF")
|
|
6197
6252
|
try:
|
|
6198
|
-
report_html = self._score_report_html() or self.report_html
|
|
6199
|
-
if report_html:
|
|
6200
|
-
if not ipython_available():
|
|
6201
|
-
return
|
|
6202
|
-
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
6203
|
-
return show_button_open_report(
|
|
6204
|
-
report_html,
|
|
6205
|
-
download_name=f"upgini-report-{search_id}.html",
|
|
6206
|
-
display_id=display_id,
|
|
6207
|
-
display_handle=display_handle,
|
|
6208
|
-
)
|
|
6209
|
-
|
|
6210
6253
|
eval_sets_drift_df = self._get_eval_sets_drift_summary()
|
|
6211
6254
|
|
|
6212
6255
|
return prepare_and_show_report(
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from upgini.__about__ import __version__
|
|
10
|
+
from upgini.metadata import FeaturesMetadataV2
|
|
11
|
+
from upgini.report.data import (
|
|
12
|
+
FeatureRow,
|
|
13
|
+
ModelFeatureShap,
|
|
14
|
+
QualitySample,
|
|
15
|
+
ReportData,
|
|
16
|
+
ReportMetadata,
|
|
17
|
+
SampleStats,
|
|
18
|
+
SearchResultsSummary,
|
|
19
|
+
SourceRow,
|
|
20
|
+
)
|
|
21
|
+
from upgini.report.stats import PSI_CRITICAL, PSI_WARNING, compute_report_charts
|
|
22
|
+
from upgini.resource_bundle import ResourceBundle
|
|
23
|
+
from upgini.utils.feature_info import FeatureInfo
|
|
24
|
+
|
|
25
|
+
UPGINI_REPORT_BRANDING_URL = "UPGINI_REPORT_BRANDING_URL"
|
|
26
|
+
_STABILITY_PSI = 0.2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_search_duration(seconds: Optional[float]) -> Optional[str]:
|
|
30
|
+
if seconds is None:
|
|
31
|
+
return None
|
|
32
|
+
total = max(0, int(round(seconds)))
|
|
33
|
+
hours, remainder = divmod(total, 3600)
|
|
34
|
+
minutes, secs = divmod(remainder, 60)
|
|
35
|
+
if hours:
|
|
36
|
+
return f"{hours} h {minutes} min {secs} sec"
|
|
37
|
+
if minutes:
|
|
38
|
+
return f"{minutes} min {secs} sec"
|
|
39
|
+
return f"{secs} sec"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assemble_report_data(
|
|
43
|
+
*,
|
|
44
|
+
search_id: str,
|
|
45
|
+
search_keys: list[str],
|
|
46
|
+
generated_at: Optional[datetime] = None,
|
|
47
|
+
pylib_version: Optional[str] = None,
|
|
48
|
+
search_duration_seconds: Optional[float] = None,
|
|
49
|
+
reference_rows: Optional[int] = None,
|
|
50
|
+
samples: Optional[list[str]] = None,
|
|
51
|
+
metrics_df: Optional[pd.DataFrame] = None,
|
|
52
|
+
metric_name: Optional[str] = None,
|
|
53
|
+
model_features: Optional[int] = None,
|
|
54
|
+
is_binary: bool = False,
|
|
55
|
+
dataset_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
56
|
+
scored_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
57
|
+
bundle: ResourceBundle,
|
|
58
|
+
summary: Optional[SearchResultsSummary] = None,
|
|
59
|
+
features: Optional[list[FeatureRow]] = None,
|
|
60
|
+
sources: Optional[list[SourceRow]] = None,
|
|
61
|
+
autofe: Optional[list[dict[str, str]]] = None,
|
|
62
|
+
model_feature_shap: Optional[list[ModelFeatureShap]] = None,
|
|
63
|
+
) -> ReportData:
|
|
64
|
+
generated_at = generated_at or datetime.now(timezone.utc)
|
|
65
|
+
sample_names = list(samples or [])
|
|
66
|
+
chart_stats, charts = compute_report_charts(
|
|
67
|
+
sample_names=sample_names,
|
|
68
|
+
dataset_samples=dataset_samples,
|
|
69
|
+
scored_samples=scored_samples,
|
|
70
|
+
metric_name=metric_name,
|
|
71
|
+
is_binary=is_binary,
|
|
72
|
+
)
|
|
73
|
+
summary = summary or SearchResultsSummary(model_features=model_features)
|
|
74
|
+
if summary.model_features is None:
|
|
75
|
+
summary.model_features = model_features
|
|
76
|
+
return ReportData(
|
|
77
|
+
metadata=ReportMetadata(
|
|
78
|
+
search_id=search_id,
|
|
79
|
+
generated_at=generated_at.strftime("%Y-%m-%d %H:%M UTC"),
|
|
80
|
+
pylib_version=pylib_version or __version__,
|
|
81
|
+
search_keys=list(search_keys or []),
|
|
82
|
+
search_duration=format_search_duration(search_duration_seconds),
|
|
83
|
+
reference_rows=reference_rows,
|
|
84
|
+
samples=sample_names,
|
|
85
|
+
logo_url=_branding_url(),
|
|
86
|
+
),
|
|
87
|
+
quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
|
|
88
|
+
summary=summary,
|
|
89
|
+
sample_stats=chart_stats or _sample_stats(sample_names, metrics_df, bundle),
|
|
90
|
+
is_binary=is_binary,
|
|
91
|
+
features=list(features or []),
|
|
92
|
+
sources=list(sources or []),
|
|
93
|
+
autofe=list(autofe or []),
|
|
94
|
+
model_feature_shap=list(model_feature_shap or []),
|
|
95
|
+
charts=charts,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_search_results(
|
|
100
|
+
*,
|
|
101
|
+
model_names: list[str],
|
|
102
|
+
features_meta: list[FeaturesMetadataV2],
|
|
103
|
+
base_columns: list[tuple[str, str]],
|
|
104
|
+
is_ensemble: Callable[[str], bool],
|
|
105
|
+
) -> tuple[list[FeatureRow], list[SourceRow], list[ModelFeatureShap], SearchResultsSummary]:
|
|
106
|
+
meta_by_name = _features_meta_index(features_meta, base_columns)
|
|
107
|
+
model_rows = [_feature_row(name, meta_by_name.get(name)) for name in model_names]
|
|
108
|
+
model_rows.sort(key=lambda row: (row.shap is None, -abs(row.shap or 0.0), row.name))
|
|
109
|
+
model_shap = [
|
|
110
|
+
ModelFeatureShap(
|
|
111
|
+
rank=index,
|
|
112
|
+
feature=row.name,
|
|
113
|
+
provider=row.provider,
|
|
114
|
+
mean_abs_shap=None if row.shap is None else abs(row.shap),
|
|
115
|
+
)
|
|
116
|
+
for index, row in enumerate(model_rows, start=1)
|
|
117
|
+
]
|
|
118
|
+
join_rows = [
|
|
119
|
+
_feature_row(meta.name, meta)
|
|
120
|
+
for meta in features_meta
|
|
121
|
+
if meta.source != "etalon" and not is_ensemble(meta.name)
|
|
122
|
+
]
|
|
123
|
+
sources = _source_rows(join_rows)
|
|
124
|
+
relevant = sum(1 for row in join_rows if _has_nonzero_shap(row))
|
|
125
|
+
contributed = len({(row.provider, row.source) for row in join_rows if row.provider and _has_nonzero_shap(row)})
|
|
126
|
+
model_count = len(model_names) or None
|
|
127
|
+
stable = sum(1 for row in model_rows if row.psi is not None and row.psi < _STABILITY_PSI)
|
|
128
|
+
summary = SearchResultsSummary(
|
|
129
|
+
relevant_features=relevant,
|
|
130
|
+
model_features=model_count,
|
|
131
|
+
data_sources=len(sources),
|
|
132
|
+
stable_features_share=(stable / len(model_rows)) if model_rows else None,
|
|
133
|
+
contributed_sources=contributed,
|
|
134
|
+
stable_features=stable if model_rows else None,
|
|
135
|
+
)
|
|
136
|
+
return model_rows, sources, model_shap, summary
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def autofe_rows_from_description(df: Optional[pd.DataFrame], bundle: ResourceBundle) -> list[dict[str, str]]:
|
|
140
|
+
if df is None or df.empty:
|
|
141
|
+
return []
|
|
142
|
+
sources_col = bundle.get("autofe_descriptions_sources")
|
|
143
|
+
name_col = bundle.get("autofe_descriptions_feature_name")
|
|
144
|
+
func_col = bundle.get("autofe_descriptions_function")
|
|
145
|
+
rows: list[dict[str, str]] = []
|
|
146
|
+
for _, row in df.iterrows():
|
|
147
|
+
source_features = []
|
|
148
|
+
for index in (1, 2):
|
|
149
|
+
col = bundle.get("autofe_descriptions_feature").format(index)
|
|
150
|
+
value = _as_str(row[col]) if col in df.columns else None
|
|
151
|
+
if value:
|
|
152
|
+
source_features.append(value)
|
|
153
|
+
rows.append(
|
|
154
|
+
{
|
|
155
|
+
"sources": (_as_str(row[sources_col]) or "") if sources_col in df.columns else "",
|
|
156
|
+
"generatedFeature": (_as_str(row[name_col]) or "") if name_col in df.columns else "",
|
|
157
|
+
"sourceFeatures": ", ".join(source_features),
|
|
158
|
+
"functions": (_as_str(row[func_col]) or "") if func_col in df.columns else "",
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
return rows
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _branding_url() -> Optional[str]:
|
|
165
|
+
env_url = os.getenv(UPGINI_REPORT_BRANDING_URL)
|
|
166
|
+
if env_url and env_url.strip():
|
|
167
|
+
return env_url.strip()
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _quality_samples(
|
|
172
|
+
metrics_df: Optional[pd.DataFrame], metric_name: Optional[str], bundle: ResourceBundle
|
|
173
|
+
) -> list[QualitySample]:
|
|
174
|
+
if metrics_df is None or metrics_df.empty:
|
|
175
|
+
return []
|
|
176
|
+
segment_col = bundle.get("quality_metrics_segment_header")
|
|
177
|
+
uplift_col = bundle.get("quality_metrics_uplift_header")
|
|
178
|
+
uplift_pct_col = bundle.get("quality_metrics_uplift_perc_header")
|
|
179
|
+
baseline_col = bundle.get("quality_metrics_baseline_header").format(metric_name) if metric_name else None
|
|
180
|
+
enriched_col = bundle.get("quality_metrics_enriched_header").format(metric_name) if metric_name else None
|
|
181
|
+
baseline_std_col = bundle.get("quality_metrics_baseline_std_header").format(metric_name) if metric_name else None
|
|
182
|
+
enriched_std_col = bundle.get("quality_metrics_enriched_std_header").format(metric_name) if metric_name else None
|
|
183
|
+
|
|
184
|
+
samples: list[QualitySample] = []
|
|
185
|
+
for _, row in metrics_df.iterrows():
|
|
186
|
+
samples.append(
|
|
187
|
+
QualitySample(
|
|
188
|
+
evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
|
|
189
|
+
metric=metric_name or "",
|
|
190
|
+
baseline=_as_float(row[baseline_col]) if baseline_col in metrics_df.columns else None,
|
|
191
|
+
baseline_std=_as_float(row[baseline_std_col]) if baseline_std_col in metrics_df.columns else None,
|
|
192
|
+
enriched=_as_float(row[enriched_col]) if enriched_col in metrics_df.columns else None,
|
|
193
|
+
enriched_std=_as_float(row[enriched_std_col]) if enriched_std_col in metrics_df.columns else None,
|
|
194
|
+
uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
|
|
195
|
+
relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return samples
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _sample_stats(
|
|
202
|
+
sample_names: list[str], metrics_df: Optional[pd.DataFrame], bundle: ResourceBundle
|
|
203
|
+
) -> list[SampleStats]:
|
|
204
|
+
segment_col = bundle.get("quality_metrics_segment_header")
|
|
205
|
+
rows_col = bundle.get("quality_metrics_rows_header")
|
|
206
|
+
mean_col = bundle.get("quality_metrics_mean_target_header")
|
|
207
|
+
by_name: dict[str, SampleStats] = {}
|
|
208
|
+
if metrics_df is not None and not metrics_df.empty and segment_col in metrics_df.columns:
|
|
209
|
+
for _, row in metrics_df.iterrows():
|
|
210
|
+
name = _as_str(row[segment_col])
|
|
211
|
+
if not name:
|
|
212
|
+
continue
|
|
213
|
+
by_name[name] = SampleStats(
|
|
214
|
+
sample=name,
|
|
215
|
+
rows=_as_int(row[rows_col]) if rows_col in metrics_df.columns else None,
|
|
216
|
+
mean_target=_as_float(row[mean_col]) if mean_col in metrics_df.columns else None,
|
|
217
|
+
)
|
|
218
|
+
names = sample_names or list(by_name)
|
|
219
|
+
stats = [by_name.get(name) or SampleStats(sample=name) for name in names]
|
|
220
|
+
seen = set(names)
|
|
221
|
+
stats.extend(sample for name, sample in by_name.items() if name not in seen)
|
|
222
|
+
return stats
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _as_str(value) -> Optional[str]:
|
|
226
|
+
return None if pd.isna(value) else str(value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _as_float(value) -> Optional[float]:
|
|
230
|
+
return None if pd.isna(value) else float(value)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _as_int(value) -> Optional[int]:
|
|
234
|
+
return None if pd.isna(value) else int(value)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _features_meta_index(
|
|
238
|
+
features_meta: list[FeaturesMetadataV2], base_columns: list[tuple[str, str]]
|
|
239
|
+
) -> dict[str, FeaturesMetadataV2]:
|
|
240
|
+
index = {meta.name: meta for meta in features_meta}
|
|
241
|
+
for original, hashed in base_columns:
|
|
242
|
+
matched = index.get(hashed) or index.get(original)
|
|
243
|
+
if matched is None:
|
|
244
|
+
continue
|
|
245
|
+
index[original] = matched
|
|
246
|
+
index[hashed] = matched
|
|
247
|
+
return index
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _feature_row(name: str, meta: Optional[FeaturesMetadataV2]) -> FeatureRow:
|
|
251
|
+
if meta is None:
|
|
252
|
+
return FeatureRow(name=name)
|
|
253
|
+
info = FeatureInfo.from_metadata(meta, None, meta.source == "etalon", False)
|
|
254
|
+
return FeatureRow(
|
|
255
|
+
name=name,
|
|
256
|
+
shap=meta.shap_value,
|
|
257
|
+
psi=meta.psi_value,
|
|
258
|
+
drift=meta.drift_score,
|
|
259
|
+
coverage=meta.hit_rate,
|
|
260
|
+
provider=info.internal_provider,
|
|
261
|
+
source=info.internal_source,
|
|
262
|
+
stability_status=_psi_status(meta.psi_value),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _source_rows(rows: list[FeatureRow]) -> list[SourceRow]:
|
|
267
|
+
grouped: dict[tuple[str, str], list[FeatureRow]] = {}
|
|
268
|
+
for row in rows:
|
|
269
|
+
if not row.provider:
|
|
270
|
+
continue
|
|
271
|
+
grouped.setdefault((row.provider, row.source), []).append(row)
|
|
272
|
+
sources = [
|
|
273
|
+
SourceRow(
|
|
274
|
+
provider=provider,
|
|
275
|
+
source=source,
|
|
276
|
+
shap_sum=sum(item.shap for item in items if item.shap is not None),
|
|
277
|
+
feature_count=len(items),
|
|
278
|
+
)
|
|
279
|
+
for (provider, source), items in grouped.items()
|
|
280
|
+
]
|
|
281
|
+
sources.sort(key=lambda row: (-(row.shap_sum or 0.0), row.provider, row.source))
|
|
282
|
+
return sources
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _psi_status(psi: Optional[float]) -> Optional[str]:
|
|
286
|
+
if psi is None or pd.isna(psi):
|
|
287
|
+
return None
|
|
288
|
+
if psi > PSI_CRITICAL:
|
|
289
|
+
return "risk"
|
|
290
|
+
if psi >= PSI_WARNING:
|
|
291
|
+
return "watch"
|
|
292
|
+
return "good"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _has_nonzero_shap(row: FeatureRow) -> bool:
|
|
296
|
+
return row.shap is not None and row.shap != 0
|
|
@@ -69,6 +69,8 @@ class SearchResultsSummary:
|
|
|
69
69
|
model_features: Optional[int] = None
|
|
70
70
|
data_sources: Optional[int] = None
|
|
71
71
|
stable_features_share: Optional[float] = None
|
|
72
|
+
contributed_sources: Optional[int] = None
|
|
73
|
+
stable_features: Optional[int] = None
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
@dataclass
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
-
from upgini.report.data import ReportCharts, ReportData, SampleStats, SearchResultsSummary
|
|
7
|
+
from upgini.report.data import FeatureRow, ReportCharts, ReportData, SampleStats, SearchResultsSummary, SourceRow
|
|
8
8
|
from upgini.report.stats import MEAN_AXIS_PADDING
|
|
9
9
|
|
|
10
10
|
_TEMPLATE_PATH = Path(__file__).with_name("template.html")
|
|
@@ -37,15 +37,14 @@ def _report_payload(data: ReportData) -> dict:
|
|
|
37
37
|
"scoreAnalysis": _score_analysis_payload(charts, samples),
|
|
38
38
|
"scoreDistribution": _score_distribution_payload(charts, samples),
|
|
39
39
|
"scoreStability": _score_stability_payload(charts, samples),
|
|
40
|
-
"features":
|
|
41
|
-
"featureStability": {"bySample": {}},
|
|
40
|
+
"features": _features_payload(data.features),
|
|
42
41
|
"shap": {"topN": 5},
|
|
43
42
|
"searchResults": {
|
|
44
43
|
"relevantFeaturesCount": _dash(data.summary.relevant_features),
|
|
45
44
|
"dataSourcesCount": _dash(data.summary.data_sources),
|
|
46
|
-
"autofeCount": _dash(len(data.autofe)
|
|
47
|
-
"sources":
|
|
48
|
-
"autofe":
|
|
45
|
+
"autofeCount": _dash(len(data.autofe)),
|
|
46
|
+
"sources": _sources_payload(data.sources),
|
|
47
|
+
"autofe": list(data.autofe),
|
|
49
48
|
},
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -141,11 +140,46 @@ def _summary_cards(summary: SearchResultsSummary) -> list[dict]:
|
|
|
141
140
|
stability = None
|
|
142
141
|
if summary.stable_features_share is not None:
|
|
143
142
|
stability = f"{summary.stable_features_share * 100:.0f}%"
|
|
143
|
+
sources_caption = ""
|
|
144
|
+
if summary.contributed_sources is not None:
|
|
145
|
+
sources_caption = f"{summary.contributed_sources} contributed"
|
|
146
|
+
stability_caption = ""
|
|
147
|
+
if summary.stable_features is not None and summary.model_features is not None:
|
|
148
|
+
stability_caption = f"{summary.stable_features} of {summary.model_features} are stable"
|
|
144
149
|
return [
|
|
145
150
|
{"label": "Features found", "value": _dash(summary.relevant_features), "caption": ""},
|
|
146
151
|
{"label": "Used in model", "value": _dash(summary.model_features), "caption": ""},
|
|
147
|
-
{"label": "Data sources", "value": _dash(summary.data_sources), "caption":
|
|
148
|
-
{"label": "Stability", "value": _dash(stability), "caption":
|
|
152
|
+
{"label": "Data sources", "value": _dash(summary.data_sources), "caption": sources_caption},
|
|
153
|
+
{"label": "Stability", "value": _dash(stability), "caption": stability_caption},
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _features_payload(features: list[FeatureRow]) -> list[dict]:
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
"name": row.name,
|
|
161
|
+
"provider": row.provider,
|
|
162
|
+
"source": row.source,
|
|
163
|
+
"importance": abs(row.shap) if row.shap is not None else 0,
|
|
164
|
+
"shap": row.shap,
|
|
165
|
+
"coverage": row.coverage,
|
|
166
|
+
"status": row.stability_status,
|
|
167
|
+
"psi": row.psi,
|
|
168
|
+
"drift": row.drift,
|
|
169
|
+
}
|
|
170
|
+
for row in features
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _sources_payload(sources: list[SourceRow]) -> list[dict]:
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
"provider": row.provider,
|
|
178
|
+
"source": row.source,
|
|
179
|
+
"aggregateShap": row.shap_sum,
|
|
180
|
+
"relevantFeatures": row.feature_count,
|
|
181
|
+
}
|
|
182
|
+
for row in sources
|
|
149
183
|
]
|
|
150
184
|
|
|
151
185
|
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
<a class="nav-link" href="#score-stability"><span class="nav-dot"></span>Score stability</a>
|
|
31
31
|
<a class="nav-link" href="#features-shap"><span class="nav-dot"></span>Features SHAP</a>
|
|
32
32
|
<a class="nav-link" href="#search-results"><span class="nav-dot"></span>Search results</a>
|
|
33
|
-
<a class="nav-link" href="#feature-stability"><span class="nav-dot"></span>Feature stability</a>
|
|
34
|
-
<div class="run-note"><strong>Search completed</strong><span id="runNoteDuration"></span><br><span id="runNoteRows"></span></div>
|
|
35
33
|
</aside>
|
|
36
34
|
<main class="main">
|
|
37
35
|
<section id="overview" class="section" style="margin-top:0">
|
|
@@ -87,11 +85,6 @@
|
|
|
87
85
|
<div class="result-pane" id="result-sources"><div class="table-actions"><button class="button ghost copy-table" data-copy="sourceTable">Copy</button><button class="button ghost csv-table" data-table="sourceTable">Download CSV</button></div><div class="panel table-wrap"><table class="data-table"><thead><tr><th>Provider</th><th>Source</th><th>Aggregate SHAP</th><th>Relevant features</th><th>Action</th></tr></thead><tbody id="sourceTable"></tbody></table></div></div>
|
|
88
86
|
<div class="result-pane" id="result-autofe"><div class="table-actions"><button class="button ghost copy-table" data-copy="autofeTable">Copy</button><button class="button ghost csv-table" data-table="autofeTable">Download CSV</button></div><div class="panel table-wrap"><table class="data-table"><thead><tr><th>Sources</th><th>Generated feature</th><th>Source features</th><th>Functions</th></tr></thead><tbody id="autofeTable"></tbody></table></div></div>
|
|
89
87
|
</section>
|
|
90
|
-
|
|
91
|
-
<section id="feature-stability" class="section">
|
|
92
|
-
<div class="section-head"><div><h2>Feature stability</h2><p>Monthly PSI and direction-invariant univariate ROC-AUC for model features only.</p></div><div class="toolbar"><select id="stabilityFeature" class="control" aria-label="Model feature"></select><button class="button ghost chart-export" data-chart="featureStabilityChart">Save PNG</button></div></div>
|
|
93
|
-
<div class="panel chart-panel"><div class="legend"><span><i style="background:#1645ee"></i>Monthly PSI</span><span><i style="background:#53cc61"></i>Univariate ROC-AUC</span></div><div class="canvas-wrap"><canvas id="featureStabilityChart" aria-label="Feature monthly PSI and ROC AUC" width="1596" height="560"></canvas><div class="chart-tooltip" id="featureTooltip"></div></div></div>
|
|
94
|
-
</section>
|
|
95
88
|
</main>
|
|
96
89
|
</div>
|
|
97
90
|
</div>
|
|
@@ -158,10 +151,6 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
158
151
|
});
|
|
159
152
|
}
|
|
160
153
|
setText('searchIdChip', 'search_id=' + m.searchId);
|
|
161
|
-
const sources = m.sourcesCount == null ? '—' : m.sourcesCount;
|
|
162
|
-
const rows = m.totalRows == null ? '—' : num(m.totalRows);
|
|
163
|
-
setText('runNoteDuration', `${m.searchDuration || '—'} · ${sources} sources`);
|
|
164
|
-
setText('runNoteRows', `${rows} rows processed`);
|
|
165
154
|
setText('statusLine', m.statusLine);
|
|
166
155
|
setText('heroTitle', m.reportTitle);
|
|
167
156
|
setText('heroSubtitle', m.reportSubtitle);
|
|
@@ -551,48 +540,6 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
551
540
|
document.getElementById('emptyState').style.display = rows.length ? 'none' : 'block';
|
|
552
541
|
}
|
|
553
542
|
|
|
554
|
-
/* ---------------------------------------------------- feature stability */
|
|
555
|
-
function renderFeatureSelect() {
|
|
556
|
-
const s = document.getElementById('stabilityFeature');
|
|
557
|
-
s.innerHTML = (D.features || []).map((f, i) => `<option value="${i}">#${i + 1} ${f.name}</option>`).join('');
|
|
558
|
-
s.addEventListener('change', drawFeatureStability);
|
|
559
|
-
}
|
|
560
|
-
function currentStabilityFeature() {
|
|
561
|
-
return D.features[+document.getElementById('stabilityFeature').value || 0];
|
|
562
|
-
}
|
|
563
|
-
function drawFeatureStability() {
|
|
564
|
-
const cfg = D.featureStability || {};
|
|
565
|
-
const { c, w, h } = setupCanvas('featureStabilityChart');
|
|
566
|
-
const feature = currentStabilityFeature();
|
|
567
|
-
const s = feature && cfg.bySample ? cfg.bySample[feature.name] : null;
|
|
568
|
-
if (!feature || !s || !MONTH_COUNT) return;
|
|
569
|
-
const psi = align(s.psi), auc = align(s.auc);
|
|
570
|
-
hits.feature = [];
|
|
571
|
-
const labelGutter = 46; // room for the ROC-AUC axis labels
|
|
572
|
-
const seriesGutter = 22; // keeps the last point off those labels
|
|
573
|
-
const plotRight = w - labelGutter - seriesGutter;
|
|
574
|
-
const x = i => 48 + (MONTH_COUNT === 1 ? (plotRight - 48) / 2 : i * (plotRight - 48) / (MONTH_COUNT - 1));
|
|
575
|
-
const yp = v => 18 + (cfg.psiAxisMax - v) / cfg.psiAxisMax * (h - 62);
|
|
576
|
-
const ya = v => 18 + (cfg.aucAxis.max - v) / cfg.aucAxis.range * (h - 62);
|
|
577
|
-
|
|
578
|
-
// grid + dual axis labels: PSI on the left, univariate ROC-AUC on the right
|
|
579
|
-
c.font = '10px Inter, sans-serif';
|
|
580
|
-
for (let i = 0; i < 5; i++) {
|
|
581
|
-
const y = 18 + i * (h - 62) / 4;
|
|
582
|
-
c.strokeStyle = '#e8eeec'; c.beginPath(); c.moveTo(40, y); c.lineTo(w - labelGutter, y); c.stroke();
|
|
583
|
-
c.fillStyle = '#5f79c9';
|
|
584
|
-
c.fillText((cfg.psiAxisMax - i * cfg.psiAxisMax / 4).toFixed(2), 4, y + 3);
|
|
585
|
-
c.fillStyle = '#5aa86a';
|
|
586
|
-
c.fillText((cfg.aucAxis.max - i * cfg.aucAxis.range / 4).toFixed(2), w - labelGutter + 10, y + 3);
|
|
587
|
-
}
|
|
588
|
-
strokeSeries(c, psi.map((v, i) => v == null ? null : { x: x(i), y: yp(v) }), '#1645ee', 4);
|
|
589
|
-
strokeSeries(c, auc.map((v, i) => v == null ? null : { x: x(i), y: ya(v) }), '#53cc61', 4);
|
|
590
|
-
MONTHS.forEach((m, i) => {
|
|
591
|
-
hits.feature.push({ x: x(i), y: 18, w: 18, h: h - 55, index: i });
|
|
592
|
-
c.fillStyle = '#7d8885'; c.fillText(m.split(' ')[0], x(i) - 8, h - 22); c.fillText(m.split(' ')[1], x(i) - 12, h - 9);
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
543
|
/* -------------------------------------------------------------- tooltips */
|
|
597
544
|
function bindTooltip(canvasId, tooltipId, hitKey, htmlFor) {
|
|
598
545
|
const cv = document.getElementById(canvasId), tip = document.getElementById(tooltipId);
|
|
@@ -630,14 +577,6 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
630
577
|
return `<b>${MONTHS[i]} · ${selectedLabel('psiSample')}</b><br>PSI: ${fmt3(s.psi[i])}` +
|
|
631
578
|
`<br>Rows: ${num(s.rows[i])}<br>Score coverage: ${s.scoreCoverage[i].toFixed(1)}%<br>Reference: first period (baseline month)`;
|
|
632
579
|
});
|
|
633
|
-
bindTooltip('featureStabilityChart', 'featureTooltip', 'feature', i => {
|
|
634
|
-
const cfg = D.featureStability, f = currentStabilityFeature(), s = cfg.bySample[f.name];
|
|
635
|
-
const auc = s.auc[i], raw = i % 3 === 0 ? 1 - auc : auc;
|
|
636
|
-
return `<b>${f.name} · ${MONTHS[i]}</b><br>SHAP rank/value: #${D.features.indexOf(f) + 1} / ${fmt3(Math.abs(f.shap))}` +
|
|
637
|
-
`<br>PSI: ${fmt3(s.psi[i])}<br>AUC: ${fmt3(auc)}<br>Raw AUC / direction: ${fmt3(raw)} / ${raw < .5 ? 'inverse' : 'direct'}` +
|
|
638
|
-
`<br>Rows: ${num(s.rows[i])}<br>Labeled: ${num(s.labeled[i])}<br>Positive count: ${num(Math.round(s.labeled[i] * D.meta.targetRate))}` +
|
|
639
|
-
`<br>Coverage: ${pct1(f.coverage)}<br>Missing rate: ${(100 - f.coverage).toFixed(1)}%`;
|
|
640
|
-
});
|
|
641
580
|
}
|
|
642
581
|
|
|
643
582
|
/* -------------------------------------------------------- sample tooltip */
|
|
@@ -712,7 +651,7 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
712
651
|
});
|
|
713
652
|
}
|
|
714
653
|
|
|
715
|
-
function drawAll() { drawChart(); drawSample(); drawScore(); drawDistribution(); drawPsi();
|
|
654
|
+
function drawAll() { drawChart(); drawSample(); drawScore(); drawDistribution(); drawPsi(); }
|
|
716
655
|
|
|
717
656
|
/* ------------------------------------------------------------------ init */
|
|
718
657
|
renderHeader();
|
|
@@ -724,7 +663,6 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
724
663
|
renderSearchResults();
|
|
725
664
|
renderFeatures();
|
|
726
665
|
renderShap();
|
|
727
|
-
renderFeatureSelect();
|
|
728
666
|
bindControls();
|
|
729
667
|
bindAllTooltips();
|
|
730
668
|
bindSampleTooltip();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.167.a4220.dev5"
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
from upgini.__about__ import __version__
|
|
10
|
-
from upgini.report.data import QualitySample, ReportData, ReportMetadata, SampleStats, SearchResultsSummary
|
|
11
|
-
from upgini.report.stats import compute_report_charts
|
|
12
|
-
from upgini.resource_bundle import ResourceBundle
|
|
13
|
-
|
|
14
|
-
UPGINI_REPORT_BRANDING_URL = "UPGINI_REPORT_BRANDING_URL"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def format_search_duration(seconds: Optional[float]) -> Optional[str]:
|
|
18
|
-
if seconds is None:
|
|
19
|
-
return None
|
|
20
|
-
total = max(0, int(round(seconds)))
|
|
21
|
-
hours, remainder = divmod(total, 3600)
|
|
22
|
-
minutes, secs = divmod(remainder, 60)
|
|
23
|
-
if hours:
|
|
24
|
-
return f"{hours} h {minutes} min {secs} sec"
|
|
25
|
-
if minutes:
|
|
26
|
-
return f"{minutes} min {secs} sec"
|
|
27
|
-
return f"{secs} sec"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def assemble_report_data(
|
|
31
|
-
*,
|
|
32
|
-
search_id: str,
|
|
33
|
-
search_keys: list[str],
|
|
34
|
-
generated_at: Optional[datetime] = None,
|
|
35
|
-
pylib_version: Optional[str] = None,
|
|
36
|
-
search_duration_seconds: Optional[float] = None,
|
|
37
|
-
reference_rows: Optional[int] = None,
|
|
38
|
-
samples: Optional[list[str]] = None,
|
|
39
|
-
metrics_df: Optional[pd.DataFrame] = None,
|
|
40
|
-
metric_name: Optional[str] = None,
|
|
41
|
-
model_features: Optional[int] = None,
|
|
42
|
-
is_binary: bool = False,
|
|
43
|
-
dataset_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
44
|
-
scored_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
45
|
-
bundle: ResourceBundle,
|
|
46
|
-
) -> ReportData:
|
|
47
|
-
generated_at = generated_at or datetime.now(timezone.utc)
|
|
48
|
-
sample_names = list(samples or [])
|
|
49
|
-
chart_stats, charts = compute_report_charts(
|
|
50
|
-
sample_names=sample_names,
|
|
51
|
-
dataset_samples=dataset_samples,
|
|
52
|
-
scored_samples=scored_samples,
|
|
53
|
-
metric_name=metric_name,
|
|
54
|
-
is_binary=is_binary,
|
|
55
|
-
)
|
|
56
|
-
return ReportData(
|
|
57
|
-
metadata=ReportMetadata(
|
|
58
|
-
search_id=search_id,
|
|
59
|
-
generated_at=generated_at.strftime("%Y-%m-%d %H:%M UTC"),
|
|
60
|
-
pylib_version=pylib_version or __version__,
|
|
61
|
-
search_keys=list(search_keys or []),
|
|
62
|
-
search_duration=format_search_duration(search_duration_seconds),
|
|
63
|
-
reference_rows=reference_rows,
|
|
64
|
-
samples=sample_names,
|
|
65
|
-
logo_url=_branding_url(),
|
|
66
|
-
),
|
|
67
|
-
quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
|
|
68
|
-
summary=SearchResultsSummary(model_features=model_features),
|
|
69
|
-
sample_stats=chart_stats or _sample_stats(sample_names, metrics_df, bundle),
|
|
70
|
-
is_binary=is_binary,
|
|
71
|
-
charts=charts,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _branding_url() -> Optional[str]:
|
|
76
|
-
env_url = os.getenv(UPGINI_REPORT_BRANDING_URL)
|
|
77
|
-
if env_url and env_url.strip():
|
|
78
|
-
return env_url.strip()
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _quality_samples(
|
|
83
|
-
metrics_df: Optional[pd.DataFrame], metric_name: Optional[str], bundle: ResourceBundle
|
|
84
|
-
) -> list[QualitySample]:
|
|
85
|
-
if metrics_df is None or metrics_df.empty:
|
|
86
|
-
return []
|
|
87
|
-
segment_col = bundle.get("quality_metrics_segment_header")
|
|
88
|
-
uplift_col = bundle.get("quality_metrics_uplift_header")
|
|
89
|
-
uplift_pct_col = bundle.get("quality_metrics_uplift_perc_header")
|
|
90
|
-
baseline_col = bundle.get("quality_metrics_baseline_header").format(metric_name) if metric_name else None
|
|
91
|
-
enriched_col = bundle.get("quality_metrics_enriched_header").format(metric_name) if metric_name else None
|
|
92
|
-
baseline_std_col = bundle.get("quality_metrics_baseline_std_header").format(metric_name) if metric_name else None
|
|
93
|
-
enriched_std_col = bundle.get("quality_metrics_enriched_std_header").format(metric_name) if metric_name else None
|
|
94
|
-
|
|
95
|
-
samples: list[QualitySample] = []
|
|
96
|
-
for _, row in metrics_df.iterrows():
|
|
97
|
-
samples.append(
|
|
98
|
-
QualitySample(
|
|
99
|
-
evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
|
|
100
|
-
metric=metric_name or "",
|
|
101
|
-
baseline=_as_float(row[baseline_col]) if baseline_col in metrics_df.columns else None,
|
|
102
|
-
baseline_std=_as_float(row[baseline_std_col]) if baseline_std_col in metrics_df.columns else None,
|
|
103
|
-
enriched=_as_float(row[enriched_col]) if enriched_col in metrics_df.columns else None,
|
|
104
|
-
enriched_std=_as_float(row[enriched_std_col]) if enriched_std_col in metrics_df.columns else None,
|
|
105
|
-
uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
|
|
106
|
-
relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
return samples
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _sample_stats(
|
|
113
|
-
sample_names: list[str], metrics_df: Optional[pd.DataFrame], bundle: ResourceBundle
|
|
114
|
-
) -> list[SampleStats]:
|
|
115
|
-
segment_col = bundle.get("quality_metrics_segment_header")
|
|
116
|
-
rows_col = bundle.get("quality_metrics_rows_header")
|
|
117
|
-
mean_col = bundle.get("quality_metrics_mean_target_header")
|
|
118
|
-
by_name: dict[str, SampleStats] = {}
|
|
119
|
-
if metrics_df is not None and not metrics_df.empty and segment_col in metrics_df.columns:
|
|
120
|
-
for _, row in metrics_df.iterrows():
|
|
121
|
-
name = _as_str(row[segment_col])
|
|
122
|
-
if not name:
|
|
123
|
-
continue
|
|
124
|
-
by_name[name] = SampleStats(
|
|
125
|
-
sample=name,
|
|
126
|
-
rows=_as_int(row[rows_col]) if rows_col in metrics_df.columns else None,
|
|
127
|
-
mean_target=_as_float(row[mean_col]) if mean_col in metrics_df.columns else None,
|
|
128
|
-
)
|
|
129
|
-
names = sample_names or list(by_name)
|
|
130
|
-
stats = [by_name.get(name) or SampleStats(sample=name) for name in names]
|
|
131
|
-
seen = set(names)
|
|
132
|
-
stats.extend(sample for name, sample in by_name.items() if name not in seen)
|
|
133
|
-
return stats
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _as_str(value) -> Optional[str]:
|
|
137
|
-
return None if pd.isna(value) else str(value)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _as_float(value) -> Optional[float]:
|
|
141
|
-
return None if pd.isna(value) else float(value)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _as_int(value) -> Optional[int]:
|
|
145
|
-
return None if pd.isna(value) else int(value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/autofe/timeseries/numpy_kernels.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/normalizer/normalize_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/__init__.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/exceptions.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/resource_bundle/strings.properties
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/base_search_key_detector.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/src/upgini/utils/fallback_progress_bar.py
RENAMED
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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.167a4220.dev5 → upgini-1.2.167a4220.dev7}/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
|