upgini 1.2.167a4220.dev5__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.dev5 → upgini-1.2.167a4220.dev6}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev6/src/upgini/__about__.py +1 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/features_enricher.py +37 -7
- upgini-1.2.167a4220.dev6/src/upgini/report/assemble.py +296 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/report/data.py +2 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/report/html.py +42 -8
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/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.dev6}/.gitignore +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/LICENSE +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/README.md +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/pyproject.toml +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/ads.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/dataset.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/errors.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/http.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/metadata.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/metrics.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/report/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/report/stats.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/search_task.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/spinner.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.167a4220.dev5 → 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 (
|
|
@@ -2437,6 +2437,7 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2437
2437
|
return max(self.__cached_sampled_datasets.values(), key=lambda item: len(item[0]) if item[0] is not None else 0)
|
|
2438
2438
|
|
|
2439
2439
|
def _assemble_report_data(self):
|
|
2440
|
+
features, sources, model_shap, summary, autofe = self._report_search_results()
|
|
2440
2441
|
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2441
2442
|
return assemble_report_data(
|
|
2442
2443
|
search_id=search_id,
|
|
@@ -2446,13 +2447,31 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2446
2447
|
samples=self._report_sample_names(),
|
|
2447
2448
|
metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
|
|
2448
2449
|
metric_name=self.metrics_metric_name,
|
|
2449
|
-
model_features=
|
|
2450
|
+
model_features=summary.model_features,
|
|
2450
2451
|
is_binary=self.model_task_type == ModelTaskType.BINARY,
|
|
2451
2452
|
dataset_samples=self._report_dataset_samples(),
|
|
2452
2453
|
scored_samples=self._report_scored_samples(),
|
|
2453
2454
|
bundle=self.bundle,
|
|
2455
|
+
summary=summary,
|
|
2456
|
+
features=features,
|
|
2457
|
+
sources=sources,
|
|
2458
|
+
autofe=autofe,
|
|
2459
|
+
model_feature_shap=model_shap,
|
|
2454
2460
|
)
|
|
2455
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,
|
|
2471
|
+
)
|
|
2472
|
+
autofe = autofe_rows_from_description(self.get_autofe_features_description(), self.bundle)
|
|
2473
|
+
return features, sources, model_shap, summary, autofe
|
|
2474
|
+
|
|
2456
2475
|
def _score_report_html(self) -> str | None:
|
|
2457
2476
|
if self._search_task is None or not self._has_single_ensemble_score():
|
|
2458
2477
|
return None
|
|
@@ -2493,13 +2512,24 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2493
2512
|
names.add(meta.alias)
|
|
2494
2513
|
return names
|
|
2495
2514
|
|
|
2496
|
-
def
|
|
2497
|
-
|
|
2498
|
-
column.original_name
|
|
2515
|
+
def _ensemble_base_columns(self) -> list[tuple[str, str]]:
|
|
2516
|
+
return [
|
|
2517
|
+
(column.original_name, column.hashed_name)
|
|
2499
2518
|
for meta, _ in self._ensemble_generated_metadata()
|
|
2500
2519
|
for column in meta.base_columns
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
|
2503
2533
|
|
|
2504
2534
|
def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
|
|
2505
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
|
|
|
@@ -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.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.dev5 → 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.dev5 → 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.dev5 → 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.dev5 → upgini-1.2.167a4220.dev6}/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.dev6}/src/upgini/resource_bundle/__init__.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/resource_bundle/exceptions.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → 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.dev5 → 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.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/base_search_key_detector.py
RENAMED
|
File without changes
|
{upgini-1.2.167a4220.dev5 → 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.dev5 → upgini-1.2.167a4220.dev6}/src/upgini/utils/fallback_progress_bar.py
RENAMED
|
File without changes
|
|
File without changes
|
{upgini-1.2.167a4220.dev5 → 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.dev5 → 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
|