upgini 1.2.167a4220.dev4__tar.gz → 1.2.167a4220.dev6__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.dev4 → upgini-1.2.167a4220.dev6}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev6/src/upgini/__about__.py +1 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/features_enricher.py +40 -7
- upgini-1.2.167a4220.dev6/src/upgini/report/assemble.py +296 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/report/data.py +2 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/report/html.py +43 -9
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/report/stats.py +10 -3
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/report/template.html +3 -65
- upgini-1.2.167a4220.dev4/src/upgini/__about__.py +0 -1
- upgini-1.2.167a4220.dev4/src/upgini/report/assemble.py +0 -145
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/.gitignore +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/LICENSE +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/README.md +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/pyproject.toml +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/ads.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/dataset.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/errors.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/http.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/metadata.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/metrics.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/report/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/search_task.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/spinner.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/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.dev6
|
|
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.dev6"
|
|
@@ -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 (
|
|
@@ -1462,6 +1462,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1462
1462
|
progress_bar: bool = True,
|
|
1463
1463
|
progress_callback: Callable | None = None,
|
|
1464
1464
|
):
|
|
1465
|
+
if self._has_single_ensemble_score():
|
|
1466
|
+
self.logger.info("Skip OOT PSI stability check for single ensemble score")
|
|
1467
|
+
return
|
|
1465
1468
|
search_keys = self.search_keys.copy()
|
|
1466
1469
|
search_keys.update(self._get_autodetected_search_keys())
|
|
1467
1470
|
validated_X, _, validated_eval_set = self._validate_train_eval(X, y, eval_set)
|
|
@@ -2434,6 +2437,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2434
2437
|
return max(self.__cached_sampled_datasets.values(), key=lambda item: len(item[0]) if item[0] is not None else 0)
|
|
2435
2438
|
|
|
2436
2439
|
def _assemble_report_data(self):
|
|
2440
|
+
features, sources, model_shap, summary, autofe = self._report_search_results()
|
|
2437
2441
|
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2438
2442
|
return assemble_report_data(
|
|
2439
2443
|
search_id=search_id,
|
|
@@ -2443,12 +2447,30 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2443
2447
|
samples=self._report_sample_names(),
|
|
2444
2448
|
metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
|
|
2445
2449
|
metric_name=self.metrics_metric_name,
|
|
2446
|
-
model_features=
|
|
2450
|
+
model_features=summary.model_features,
|
|
2447
2451
|
is_binary=self.model_task_type == ModelTaskType.BINARY,
|
|
2448
2452
|
dataset_samples=self._report_dataset_samples(),
|
|
2449
2453
|
scored_samples=self._report_scored_samples(),
|
|
2450
2454
|
bundle=self.bundle,
|
|
2455
|
+
summary=summary,
|
|
2456
|
+
features=features,
|
|
2457
|
+
sources=sources,
|
|
2458
|
+
autofe=autofe,
|
|
2459
|
+
model_feature_shap=model_shap,
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
def _report_search_results(self):
|
|
2463
|
+
features_meta = []
|
|
2464
|
+
if self._search_task is not None:
|
|
2465
|
+
features_meta = list(self._search_task.get_all_features_metadata_v2() or [])
|
|
2466
|
+
features, sources, model_shap, summary = build_search_results(
|
|
2467
|
+
model_names=self._ensemble_model_feature_names(),
|
|
2468
|
+
features_meta=features_meta,
|
|
2469
|
+
base_columns=self._ensemble_base_columns(),
|
|
2470
|
+
is_ensemble=self._is_ensemble_feature,
|
|
2451
2471
|
)
|
|
2472
|
+
autofe = autofe_rows_from_description(self.get_autofe_features_description(), self.bundle)
|
|
2473
|
+
return features, sources, model_shap, summary, autofe
|
|
2452
2474
|
|
|
2453
2475
|
def _score_report_html(self) -> str | None:
|
|
2454
2476
|
if self._search_task is None or not self._has_single_ensemble_score():
|
|
@@ -2490,13 +2512,24 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2490
2512
|
names.add(meta.alias)
|
|
2491
2513
|
return names
|
|
2492
2514
|
|
|
2493
|
-
def
|
|
2494
|
-
|
|
2495
|
-
column.original_name
|
|
2515
|
+
def _ensemble_base_columns(self) -> list[tuple[str, str]]:
|
|
2516
|
+
return [
|
|
2517
|
+
(column.original_name, column.hashed_name)
|
|
2496
2518
|
for meta, _ in self._ensemble_generated_metadata()
|
|
2497
2519
|
for column in meta.base_columns
|
|
2498
|
-
|
|
2499
|
-
|
|
2520
|
+
]
|
|
2521
|
+
|
|
2522
|
+
def _ensemble_model_feature_names(self) -> list[str]:
|
|
2523
|
+
return sorted(
|
|
2524
|
+
{
|
|
2525
|
+
original
|
|
2526
|
+
for original, hashed in self._ensemble_base_columns()
|
|
2527
|
+
if not self._is_ensemble_feature(original) and not self._is_ensemble_feature(hashed)
|
|
2528
|
+
}
|
|
2529
|
+
)
|
|
2530
|
+
|
|
2531
|
+
def _ensemble_model_feature_count(self) -> int | None:
|
|
2532
|
+
return len(self._ensemble_model_feature_names()) or None
|
|
2500
2533
|
|
|
2501
2534
|
def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
|
|
2502
2535
|
return psi is not None and not pd.isna(psi) and psi > threshold
|
|
@@ -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
|
|
|
@@ -79,7 +78,7 @@ def _samples(data: ReportData) -> list[dict]:
|
|
|
79
78
|
{
|
|
80
79
|
"id": _slug(name),
|
|
81
80
|
"label": name,
|
|
82
|
-
"caption": "",
|
|
81
|
+
"caption": "OOF validation" if index == 0 else "",
|
|
83
82
|
"color": _SAMPLE_COLORS[index % len(_SAMPLE_COLORS)],
|
|
84
83
|
}
|
|
85
84
|
for index, name in enumerate(names)
|
|
@@ -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
|
|
|
@@ -344,10 +344,17 @@ def _score_psi(names: list[str], scored_samples: dict[str, pd.DataFrame], timeli
|
|
|
344
344
|
if not timeline or not scored_samples:
|
|
345
345
|
return []
|
|
346
346
|
train = scored_samples.get(names[0]) if names else None
|
|
347
|
-
if train is None or SCORE not in train.columns:
|
|
347
|
+
if train is None or SCORE not in train.columns or DATE not in train.columns:
|
|
348
348
|
return []
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
train_scores = pd.to_numeric(train[SCORE], errors="coerce")
|
|
350
|
+
train_months = _month_keys(train[DATE])
|
|
351
|
+
reference = None
|
|
352
|
+
for month in timeline:
|
|
353
|
+
month_scores = train_scores[train_months == month].dropna()
|
|
354
|
+
if not month_scores.empty:
|
|
355
|
+
reference = month_scores
|
|
356
|
+
break
|
|
357
|
+
if reference is None:
|
|
351
358
|
return []
|
|
352
359
|
bins = _get_bin_edges(reference, HISTOGRAM_BINS)
|
|
353
360
|
rows = []
|
|
@@ -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">
|
|
@@ -71,7 +69,7 @@
|
|
|
71
69
|
</section>
|
|
72
70
|
|
|
73
71
|
<section id="score-stability" class="section">
|
|
74
|
-
<div class="section-head"><div><h2>Score stability (PSI)</h2><p>Monthly PSI of scores · reference:
|
|
72
|
+
<div class="section-head"><div><h2>Score stability (PSI)</h2><p>Monthly PSI of scores · reference: first period (baseline month) · fixed reference bins.</p></div><div class="toolbar"><select id="psiSample" class="control" aria-label="Score stability sample"></select><button class="button ghost chart-export" data-chart="scorePsiChart">Save PNG</button></div></div>
|
|
75
73
|
<div class="panel chart-panel"><div class="threshold-legend" id="psiThresholdLegend"></div><div class="canvas-wrap"><canvas id="scorePsiChart" aria-label="Monthly score PSI" width="1596" height="560"></canvas><div class="chart-tooltip" id="psiTooltip"></div></div></div>
|
|
76
74
|
</section>
|
|
77
75
|
|
|
@@ -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);
|
|
@@ -628,15 +575,7 @@ const REPORT_DATA = __REPORT_DATA__;
|
|
|
628
575
|
bindTooltip('scorePsiChart', 'psiTooltip', 'psi', i => {
|
|
629
576
|
const cfg = D.scoreStability, s = cfg.bySample[document.getElementById('psiSample').value];
|
|
630
577
|
return `<b>${MONTHS[i]} · ${selectedLabel('psiSample')}</b><br>PSI: ${fmt3(s.psi[i])}` +
|
|
631
|
-
`<br>Rows: ${num(s.rows[i])}<br>Score coverage: ${s.scoreCoverage[i].toFixed(1)}%<br>Reference:
|
|
632
|
-
});
|
|
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)}%`;
|
|
578
|
+
`<br>Rows: ${num(s.rows[i])}<br>Score coverage: ${s.scoreCoverage[i].toFixed(1)}%<br>Reference: first period (baseline month)`;
|
|
640
579
|
});
|
|
641
580
|
}
|
|
642
581
|
|
|
@@ -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.dev4"
|
|
@@ -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.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/numpy_kernels.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/normalizer/normalize_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/__init__.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/exceptions.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/strings.properties
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/base_search_key_detector.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/src/upgini/utils/fallback_progress_bar.py
RENAMED
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev4 → upgini-1.2.167a4220.dev6}/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.dev4 → upgini-1.2.167a4220.dev6}/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
|