upgini 1.2.167a4220.dev3__tar.gz → 1.2.167a4220.dev5__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.dev3 → upgini-1.2.167a4220.dev5}/PKG-INFO +1 -1
- upgini-1.2.167a4220.dev5/src/upgini/__about__.py +1 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/features_enricher.py +126 -2
- upgini-1.2.167a4220.dev5/src/upgini/report/__init__.py +11 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/report/assemble.py +23 -3
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/report/data.py +65 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/report/html.py +106 -11
- upgini-1.2.167a4220.dev5/src/upgini/report/stats.py +433 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/report/template.html +13 -8
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/psi.py +20 -1
- upgini-1.2.167a4220.dev3/src/upgini/__about__.py +0 -1
- upgini-1.2.167a4220.dev3/src/upgini/report/__init__.py +0 -5
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/.gitignore +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/LICENSE +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/README.md +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/pyproject.toml +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/ads.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/date.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/dataset.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/errors.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/http.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/metadata.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/metrics.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/search_task.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/spinner.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/cpu_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/pyarrow_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/search_key_derivations.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/src/upgini/utils/warning_counter.py +0 -0
- {upgini-1.2.167a4220.dev3 → upgini-1.2.167a4220.dev5}/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.dev5
|
|
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.dev5"
|
|
@@ -100,6 +100,7 @@ from upgini.utils.deduplicate_utils import (
|
|
|
100
100
|
)
|
|
101
101
|
from upgini.report.assemble import assemble_report_data
|
|
102
102
|
from upgini.report.html import generate_html_report
|
|
103
|
+
from upgini.report.stats import make_report_frame
|
|
103
104
|
from upgini.utils.display_utils import (
|
|
104
105
|
display_html_dataframe,
|
|
105
106
|
do_without_pandas_limits,
|
|
@@ -1461,6 +1462,9 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
1461
1462
|
progress_bar: bool = True,
|
|
1462
1463
|
progress_callback: Callable | None = None,
|
|
1463
1464
|
):
|
|
1465
|
+
if self._has_single_ensemble_score():
|
|
1466
|
+
self.logger.info("Skip OOT PSI stability check for single ensemble score")
|
|
1467
|
+
return
|
|
1464
1468
|
search_keys = self.search_keys.copy()
|
|
1465
1469
|
search_keys.update(self._get_autodetected_search_keys())
|
|
1466
1470
|
validated_X, _, validated_eval_set = self._validate_train_eval(X, y, eval_set)
|
|
@@ -2308,17 +2312,135 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2308
2312
|
rows += _num_samples(eval_pair[0])
|
|
2309
2313
|
return rows
|
|
2310
2314
|
|
|
2315
|
+
def _search_key_names(self) -> list[str]:
|
|
2316
|
+
return [key.name for key in self.search_keys.values()]
|
|
2317
|
+
|
|
2311
2318
|
def _report_sample_names(self) -> list[str]:
|
|
2312
2319
|
samples = [self.bundle.get("quality_metrics_train_segment")]
|
|
2313
2320
|
for idx, _ in enumerate(self.eval_set or []):
|
|
2314
2321
|
samples.append(self.bundle.get("quality_metrics_eval_segment").format(idx + 1))
|
|
2315
2322
|
return samples
|
|
2316
2323
|
|
|
2324
|
+
def _report_dataset_samples(self) -> dict[str, pd.DataFrame]:
|
|
2325
|
+
names = self._report_sample_names()
|
|
2326
|
+
if self.df_with_original_index is not None:
|
|
2327
|
+
return self._report_samples_from_combined(self.df_with_original_index, names, self.fit_search_keys)
|
|
2328
|
+
if self.X is None or self.y is None:
|
|
2329
|
+
return {}
|
|
2330
|
+
date_col = self._get_date_column(self.search_keys)
|
|
2331
|
+
frames = {names[0]: self._report_xy_frame(self.X, self.y, date_col)}
|
|
2332
|
+
for idx, (eval_x, eval_y) in enumerate(self.eval_set or []):
|
|
2333
|
+
if idx + 1 >= len(names):
|
|
2334
|
+
break
|
|
2335
|
+
frames[names[idx + 1]] = self._report_xy_frame(eval_x, eval_y, date_col)
|
|
2336
|
+
return frames
|
|
2337
|
+
|
|
2338
|
+
def _report_samples_from_combined(
|
|
2339
|
+
self, df: pd.DataFrame, names: list[str], search_keys: dict[str, SearchKey] | None
|
|
2340
|
+
) -> dict[str, pd.DataFrame]:
|
|
2341
|
+
date_col = self._get_date_column(search_keys or {})
|
|
2342
|
+
if EVAL_SET_INDEX not in df.columns:
|
|
2343
|
+
return {names[0]: self._report_df_frame(df, date_col)} if names else {}
|
|
2344
|
+
frames: dict[str, pd.DataFrame] = {}
|
|
2345
|
+
for eval_idx in sorted(pd.unique(df[EVAL_SET_INDEX].dropna())):
|
|
2346
|
+
name_idx = int(eval_idx)
|
|
2347
|
+
if name_idx >= len(names):
|
|
2348
|
+
continue
|
|
2349
|
+
frames[names[name_idx]] = self._report_df_frame(df[df[EVAL_SET_INDEX] == eval_idx], date_col)
|
|
2350
|
+
return frames
|
|
2351
|
+
|
|
2352
|
+
def _report_xy_frame(self, X, y, date_col: str | None) -> pd.DataFrame:
|
|
2353
|
+
date = X[date_col] if isinstance(X, pd.DataFrame) and date_col and date_col in X.columns else None
|
|
2354
|
+
return make_report_frame(y, date=date)
|
|
2355
|
+
|
|
2356
|
+
def _report_df_frame(self, df: pd.DataFrame, date_col: str | None) -> pd.DataFrame:
|
|
2357
|
+
target = df[TARGET] if TARGET in df.columns else pd.Series(np.nan, index=df.index)
|
|
2358
|
+
return self._report_xy_frame(df, target, date_col)
|
|
2359
|
+
|
|
2360
|
+
def _report_scored_samples(self) -> dict[str, pd.DataFrame]:
|
|
2361
|
+
cached = self._peek_cached_sampled()
|
|
2362
|
+
if cached is None:
|
|
2363
|
+
return {}
|
|
2364
|
+
X_sampled, y_sampled, enriched_X, eval_set_sampled_dict, search_keys, columns_renaming, _generated = cached
|
|
2365
|
+
score_col = self._report_score_column(enriched_X)
|
|
2366
|
+
if score_col is None:
|
|
2367
|
+
return {}
|
|
2368
|
+
date_col = self._get_date_column(search_keys or {})
|
|
2369
|
+
names = self._report_sample_names()
|
|
2370
|
+
frames = {
|
|
2371
|
+
names[0]: self._report_scored_frame(
|
|
2372
|
+
X_sampled, y_sampled, enriched_X, score_col, date_col, columns_renaming
|
|
2373
|
+
)
|
|
2374
|
+
}
|
|
2375
|
+
for idx, (eval_X, enriched_eval_X, eval_y) in eval_set_sampled_dict.items():
|
|
2376
|
+
name_idx = idx + 1
|
|
2377
|
+
if name_idx >= len(names):
|
|
2378
|
+
continue
|
|
2379
|
+
frames[names[name_idx]] = self._report_scored_frame(
|
|
2380
|
+
eval_X, eval_y, enriched_eval_X, score_col, date_col, columns_renaming
|
|
2381
|
+
)
|
|
2382
|
+
return frames
|
|
2383
|
+
|
|
2384
|
+
def _report_scored_frame(
|
|
2385
|
+
self,
|
|
2386
|
+
X: pd.DataFrame,
|
|
2387
|
+
y,
|
|
2388
|
+
enriched: pd.DataFrame,
|
|
2389
|
+
score_col: str,
|
|
2390
|
+
date_col: str | None,
|
|
2391
|
+
columns_renaming: dict[str, str] | None,
|
|
2392
|
+
) -> pd.DataFrame:
|
|
2393
|
+
target = y
|
|
2394
|
+
if target is None and TARGET in enriched.columns:
|
|
2395
|
+
target = enriched[TARGET]
|
|
2396
|
+
date_name = self._report_existing_column(date_col, columns_renaming, X, enriched)
|
|
2397
|
+
baseline_name = self._report_existing_column(self.baseline_score_column, columns_renaming, X, enriched)
|
|
2398
|
+
return make_report_frame(
|
|
2399
|
+
target,
|
|
2400
|
+
date=self._report_column_values(date_name, X, enriched),
|
|
2401
|
+
score=enriched[score_col] if score_col in enriched.columns else None,
|
|
2402
|
+
baseline=self._report_column_values(baseline_name, X, enriched),
|
|
2403
|
+
)
|
|
2404
|
+
|
|
2405
|
+
def _report_score_column(self, enriched_X: pd.DataFrame) -> str | None:
|
|
2406
|
+
name = self._get_single_ensemble_score_name()
|
|
2407
|
+
if name and name in enriched_X.columns:
|
|
2408
|
+
return name
|
|
2409
|
+
return None
|
|
2410
|
+
|
|
2411
|
+
def _report_existing_column(
|
|
2412
|
+
self, name: str | None, renaming: dict[str, str] | None, *frames: pd.DataFrame
|
|
2413
|
+
) -> str | None:
|
|
2414
|
+
if not name:
|
|
2415
|
+
return None
|
|
2416
|
+
aliases = self._column_name_aliases([name], renaming or {})
|
|
2417
|
+
for frame in frames:
|
|
2418
|
+
if not isinstance(frame, pd.DataFrame):
|
|
2419
|
+
continue
|
|
2420
|
+
for col in frame.columns:
|
|
2421
|
+
if col in aliases:
|
|
2422
|
+
return col
|
|
2423
|
+
return None
|
|
2424
|
+
|
|
2425
|
+
@staticmethod
|
|
2426
|
+
def _report_column_values(column: str | None, *frames: pd.DataFrame):
|
|
2427
|
+
if not column:
|
|
2428
|
+
return None
|
|
2429
|
+
for frame in frames:
|
|
2430
|
+
if isinstance(frame, pd.DataFrame) and column in frame.columns:
|
|
2431
|
+
return frame[column]
|
|
2432
|
+
return None
|
|
2433
|
+
|
|
2434
|
+
def _peek_cached_sampled(self):
|
|
2435
|
+
if not self.__cached_sampled_datasets:
|
|
2436
|
+
return None
|
|
2437
|
+
return max(self.__cached_sampled_datasets.values(), key=lambda item: len(item[0]) if item[0] is not None else 0)
|
|
2438
|
+
|
|
2317
2439
|
def _assemble_report_data(self):
|
|
2318
2440
|
search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
|
|
2319
2441
|
return assemble_report_data(
|
|
2320
2442
|
search_id=search_id,
|
|
2321
|
-
search_keys=
|
|
2443
|
+
search_keys=self._search_key_names(),
|
|
2322
2444
|
search_duration_seconds=self.search_duration_seconds,
|
|
2323
2445
|
reference_rows=self._reference_rows(),
|
|
2324
2446
|
samples=self._report_sample_names(),
|
|
@@ -2326,6 +2448,8 @@ class FeaturesEnricher(TransformerMixin):
|
|
|
2326
2448
|
metric_name=self.metrics_metric_name,
|
|
2327
2449
|
model_features=self._ensemble_model_feature_count(),
|
|
2328
2450
|
is_binary=self.model_task_type == ModelTaskType.BINARY,
|
|
2451
|
+
dataset_samples=self._report_dataset_samples(),
|
|
2452
|
+
scored_samples=self._report_scored_samples(),
|
|
2329
2453
|
bundle=self.bundle,
|
|
2330
2454
|
)
|
|
2331
2455
|
|
|
@@ -6092,7 +6216,7 @@ if response.status_code == 200:
|
|
|
6092
6216
|
autofe_descriptions_df=self.get_autofe_features_description(),
|
|
6093
6217
|
search_id=self._search_task.search_task_id,
|
|
6094
6218
|
email=self.rest_client.get_current_email(),
|
|
6095
|
-
search_keys=
|
|
6219
|
+
search_keys=self._search_key_names(),
|
|
6096
6220
|
eval_sets_drift_df=eval_sets_drift_df,
|
|
6097
6221
|
display_id=display_id,
|
|
6098
6222
|
display_handle=display_handle,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from upgini.report.assemble import UPGINI_REPORT_BRANDING_URL, assemble_report_data, format_search_duration
|
|
2
|
+
from upgini.report.data import ReportData
|
|
3
|
+
from upgini.report.html import generate_html_report
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ReportData",
|
|
7
|
+
"UPGINI_REPORT_BRANDING_URL",
|
|
8
|
+
"assemble_report_data",
|
|
9
|
+
"format_search_duration",
|
|
10
|
+
"generate_html_report",
|
|
11
|
+
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from datetime import datetime, timezone
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
@@ -7,8 +8,11 @@ import pandas as pd
|
|
|
7
8
|
|
|
8
9
|
from upgini.__about__ import __version__
|
|
9
10
|
from upgini.report.data import QualitySample, ReportData, ReportMetadata, SampleStats, SearchResultsSummary
|
|
11
|
+
from upgini.report.stats import compute_report_charts
|
|
10
12
|
from upgini.resource_bundle import ResourceBundle
|
|
11
13
|
|
|
14
|
+
UPGINI_REPORT_BRANDING_URL = "UPGINI_REPORT_BRANDING_URL"
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
def format_search_duration(seconds: Optional[float]) -> Optional[str]:
|
|
14
18
|
if seconds is None:
|
|
@@ -36,11 +40,19 @@ def assemble_report_data(
|
|
|
36
40
|
metric_name: Optional[str] = None,
|
|
37
41
|
model_features: Optional[int] = None,
|
|
38
42
|
is_binary: bool = False,
|
|
39
|
-
|
|
43
|
+
dataset_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
44
|
+
scored_samples: Optional[dict[str, pd.DataFrame]] = None,
|
|
40
45
|
bundle: ResourceBundle,
|
|
41
46
|
) -> ReportData:
|
|
42
47
|
generated_at = generated_at or datetime.now(timezone.utc)
|
|
43
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
|
+
)
|
|
44
56
|
return ReportData(
|
|
45
57
|
metadata=ReportMetadata(
|
|
46
58
|
search_id=search_id,
|
|
@@ -50,15 +62,23 @@ def assemble_report_data(
|
|
|
50
62
|
search_duration=format_search_duration(search_duration_seconds),
|
|
51
63
|
reference_rows=reference_rows,
|
|
52
64
|
samples=sample_names,
|
|
53
|
-
logo_url=
|
|
65
|
+
logo_url=_branding_url(),
|
|
54
66
|
),
|
|
55
67
|
quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
|
|
56
68
|
summary=SearchResultsSummary(model_features=model_features),
|
|
57
|
-
sample_stats=_sample_stats(sample_names, metrics_df, bundle),
|
|
69
|
+
sample_stats=chart_stats or _sample_stats(sample_names, metrics_df, bundle),
|
|
58
70
|
is_binary=is_binary,
|
|
71
|
+
charts=charts,
|
|
59
72
|
)
|
|
60
73
|
|
|
61
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
|
+
|
|
62
82
|
def _quality_samples(
|
|
63
83
|
metrics_df: Optional[pd.DataFrame], metric_name: Optional[str], bundle: ResourceBundle
|
|
64
84
|
) -> list[QualitySample]:
|
|
@@ -79,6 +79,70 @@ class ModelFeatureShap:
|
|
|
79
79
|
mean_abs_shap: Optional[float] = None
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class MonthlySamplePoint:
|
|
84
|
+
month: str
|
|
85
|
+
sample: str
|
|
86
|
+
total: int
|
|
87
|
+
labeled: int
|
|
88
|
+
unlabeled: int
|
|
89
|
+
positive: int
|
|
90
|
+
mean: Optional[float] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class QualityMonthly:
|
|
95
|
+
evaluation_scope: str
|
|
96
|
+
metric: str
|
|
97
|
+
baseline: list[Optional[float]] = field(default_factory=list)
|
|
98
|
+
enriched: list[Optional[float]] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ScoreMonthlyStats:
|
|
103
|
+
sample: str
|
|
104
|
+
mean_score: list[Optional[float]] = field(default_factory=list)
|
|
105
|
+
mean_target: list[Optional[float]] = field(default_factory=list)
|
|
106
|
+
labeled: list[Optional[int]] = field(default_factory=list)
|
|
107
|
+
unlabeled: list[Optional[int]] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class ScoreHistogram:
|
|
112
|
+
sample: str
|
|
113
|
+
target_0: list[float] = field(default_factory=list)
|
|
114
|
+
target_1: list[float] = field(default_factory=list)
|
|
115
|
+
n_target_0: int = 0
|
|
116
|
+
n_target_1: int = 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class ScorePsi:
|
|
121
|
+
sample: str
|
|
122
|
+
psi: list[Optional[float]] = field(default_factory=list)
|
|
123
|
+
rows: list[Optional[int]] = field(default_factory=list)
|
|
124
|
+
coverage: list[Optional[float]] = field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class ReportCharts:
|
|
129
|
+
timeline_months: list[str] = field(default_factory=list)
|
|
130
|
+
period_options: list[int] = field(default_factory=list)
|
|
131
|
+
default_period: int = 0
|
|
132
|
+
sample_monthly: list[MonthlySamplePoint] = field(default_factory=list)
|
|
133
|
+
monthly_count_max: int = 0
|
|
134
|
+
quality_monthly: list[QualityMonthly] = field(default_factory=list)
|
|
135
|
+
score_monthly: list[ScoreMonthlyStats] = field(default_factory=list)
|
|
136
|
+
score_count_axis_max: int = 0
|
|
137
|
+
histogram_bin_edges: list[float] = field(default_factory=list)
|
|
138
|
+
histograms: list[ScoreHistogram] = field(default_factory=list)
|
|
139
|
+
histogram_density_max: float = 0.0
|
|
140
|
+
score_psi: list[ScorePsi] = field(default_factory=list)
|
|
141
|
+
psi_axis_max: float = 0.32
|
|
142
|
+
psi_warning: float = 0.1
|
|
143
|
+
psi_critical: float = 0.25
|
|
144
|
+
|
|
145
|
+
|
|
82
146
|
@dataclass
|
|
83
147
|
class ReportData:
|
|
84
148
|
metadata: ReportMetadata
|
|
@@ -90,3 +154,4 @@ class ReportData:
|
|
|
90
154
|
sources: list[SourceRow] = field(default_factory=list)
|
|
91
155
|
autofe: list[dict[str, str]] = field(default_factory=list)
|
|
92
156
|
model_feature_shap: list[ModelFeatureShap] = field(default_factory=list)
|
|
157
|
+
charts: ReportCharts = field(default_factory=ReportCharts)
|
|
@@ -4,7 +4,8 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
-
from upgini.report.data import ReportData, SampleStats, SearchResultsSummary
|
|
7
|
+
from upgini.report.data import ReportCharts, ReportData, SampleStats, SearchResultsSummary
|
|
8
|
+
from upgini.report.stats import MEAN_AXIS_PADDING
|
|
8
9
|
|
|
9
10
|
_TEMPLATE_PATH = Path(__file__).with_name("template.html")
|
|
10
11
|
_SAMPLE_COLORS = ("#1645ee", "#20aab4", "#53cc61", "#e19132", "#d45b5b")
|
|
@@ -21,16 +22,21 @@ def generate_html_report(data: ReportData) -> str:
|
|
|
21
22
|
|
|
22
23
|
def _report_payload(data: ReportData) -> dict:
|
|
23
24
|
samples = _samples(data)
|
|
25
|
+
charts = data.charts or ReportCharts()
|
|
24
26
|
return {
|
|
25
27
|
"meta": _meta(data, samples),
|
|
26
|
-
"timeline": {
|
|
28
|
+
"timeline": {
|
|
29
|
+
"months": list(charts.timeline_months),
|
|
30
|
+
"periodOptions": list(charts.period_options),
|
|
31
|
+
"defaultPeriod": charts.default_period,
|
|
32
|
+
},
|
|
27
33
|
"samples": samples,
|
|
28
|
-
"keyResult": _key_result(data),
|
|
34
|
+
"keyResult": _key_result(data, charts),
|
|
29
35
|
"summaryCards": _summary_cards(data.summary),
|
|
30
|
-
"sampleStats": _sample_stats_payload(data, samples),
|
|
31
|
-
"scoreAnalysis":
|
|
32
|
-
"scoreDistribution":
|
|
33
|
-
"scoreStability":
|
|
36
|
+
"sampleStats": _sample_stats_payload(data, samples, charts),
|
|
37
|
+
"scoreAnalysis": _score_analysis_payload(charts, samples),
|
|
38
|
+
"scoreDistribution": _score_distribution_payload(charts, samples),
|
|
39
|
+
"scoreStability": _score_stability_payload(charts, samples),
|
|
34
40
|
"features": [],
|
|
35
41
|
"featureStability": {"bySample": {}},
|
|
36
42
|
"shap": {"topN": 5},
|
|
@@ -73,23 +79,43 @@ def _samples(data: ReportData) -> list[dict]:
|
|
|
73
79
|
{
|
|
74
80
|
"id": _slug(name),
|
|
75
81
|
"label": name,
|
|
76
|
-
"caption": "",
|
|
82
|
+
"caption": "OOF validation" if index == 0 else "",
|
|
77
83
|
"color": _SAMPLE_COLORS[index % len(_SAMPLE_COLORS)],
|
|
78
84
|
}
|
|
79
85
|
for index, name in enumerate(names)
|
|
80
86
|
]
|
|
81
87
|
|
|
82
88
|
|
|
83
|
-
def _key_result(data: ReportData) -> dict:
|
|
89
|
+
def _key_result(data: ReportData, charts: ReportCharts) -> dict:
|
|
90
|
+
series = {
|
|
91
|
+
(_slug(row.metric or "score"), _slug(row.evaluation_scope)): row for row in charts.quality_monthly
|
|
92
|
+
}
|
|
84
93
|
metrics: dict[str, dict] = {}
|
|
85
94
|
for sample in data.quality_by_sample:
|
|
86
95
|
key = _slug(sample.metric or "score")
|
|
87
96
|
bucket = metrics.setdefault(key, {"name": sample.metric or "score", "bySample": {}})
|
|
97
|
+
monthly = series.get((key, _slug(sample.evaluation_scope)))
|
|
88
98
|
bucket["bySample"][_slug(sample.evaluation_scope)] = {
|
|
89
99
|
"baseline": sample.baseline,
|
|
90
100
|
"baselineCi": sample.baseline_std,
|
|
91
101
|
"enriched": sample.enriched,
|
|
92
102
|
"enrichedCi": sample.enriched_std,
|
|
103
|
+
"baselineSeries": list(monthly.baseline) if monthly else [],
|
|
104
|
+
"enrichedSeries": list(monthly.enriched) if monthly else [],
|
|
105
|
+
}
|
|
106
|
+
for monthly in charts.quality_monthly:
|
|
107
|
+
key = _slug(monthly.metric or "score")
|
|
108
|
+
sample_id = _slug(monthly.evaluation_scope)
|
|
109
|
+
bucket = metrics.setdefault(key, {"name": monthly.metric or "score", "bySample": {}})
|
|
110
|
+
if sample_id in bucket["bySample"]:
|
|
111
|
+
continue
|
|
112
|
+
bucket["bySample"][sample_id] = {
|
|
113
|
+
"baseline": None,
|
|
114
|
+
"baselineCi": None,
|
|
115
|
+
"enriched": None,
|
|
116
|
+
"enrichedCi": None,
|
|
117
|
+
"baselineSeries": list(monthly.baseline),
|
|
118
|
+
"enrichedSeries": list(monthly.enriched),
|
|
93
119
|
}
|
|
94
120
|
default_metric = next(iter(metrics), "")
|
|
95
121
|
has_std = any(
|
|
@@ -123,7 +149,7 @@ def _summary_cards(summary: SearchResultsSummary) -> list[dict]:
|
|
|
123
149
|
]
|
|
124
150
|
|
|
125
151
|
|
|
126
|
-
def _sample_stats_payload(data: ReportData, samples: list[dict]) -> dict:
|
|
152
|
+
def _sample_stats_payload(data: ReportData, samples: list[dict], charts: ReportCharts) -> dict:
|
|
127
153
|
by_id = {_slug(stats.sample): stats for stats in data.sample_stats}
|
|
128
154
|
rows = [
|
|
129
155
|
_stat_row("Date range", samples, by_id, lambda stats: stats.date_range),
|
|
@@ -138,7 +164,76 @@ def _sample_stats_payload(data: ReportData, samples: list[dict]) -> dict:
|
|
|
138
164
|
_stat_row("Count (unlabeled)", samples, by_id, lambda stats: _format_int(stats.unlabeled)),
|
|
139
165
|
]
|
|
140
166
|
)
|
|
141
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
"rows": rows,
|
|
169
|
+
"monthlyAxis": list(charts.timeline_months),
|
|
170
|
+
"monthlyCountMax": charts.monthly_count_max or 1,
|
|
171
|
+
"monthlyMeanAxisPadding": MEAN_AXIS_PADDING,
|
|
172
|
+
"monthlyPoints": [
|
|
173
|
+
{
|
|
174
|
+
"month": point.month,
|
|
175
|
+
"sample": _slug(point.sample),
|
|
176
|
+
"total": point.total,
|
|
177
|
+
"labeled": point.labeled,
|
|
178
|
+
"unlabeled": point.unlabeled,
|
|
179
|
+
"positive": point.positive,
|
|
180
|
+
"mean": point.mean,
|
|
181
|
+
}
|
|
182
|
+
for point in charts.sample_monthly
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _score_analysis_payload(charts: ReportCharts, samples: list[dict]) -> dict:
|
|
188
|
+
by_id = {_slug(row.sample): row for row in charts.score_monthly}
|
|
189
|
+
return {
|
|
190
|
+
"countAxisMax": charts.score_count_axis_max or 1,
|
|
191
|
+
"bySample": {
|
|
192
|
+
sample["id"]: {
|
|
193
|
+
"meanScore": list(row.mean_score),
|
|
194
|
+
"meanTarget": list(row.mean_target),
|
|
195
|
+
"labeled": list(row.labeled),
|
|
196
|
+
"unlabeled": list(row.unlabeled),
|
|
197
|
+
}
|
|
198
|
+
for sample in samples
|
|
199
|
+
if (row := by_id.get(sample["id"])) is not None
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _score_distribution_payload(charts: ReportCharts, samples: list[dict]) -> dict:
|
|
205
|
+
by_id = {_slug(row.sample): row for row in charts.histograms}
|
|
206
|
+
train = next((by_id.get(sample["id"]) for sample in samples if sample["id"] in by_id), None)
|
|
207
|
+
return {
|
|
208
|
+
"binEdges": list(charts.histogram_bin_edges),
|
|
209
|
+
"densityAxisMax": charts.histogram_density_max or 1,
|
|
210
|
+
"countScale": {
|
|
211
|
+
"target0": train.n_target_0 if train else 0,
|
|
212
|
+
"target1": train.n_target_1 if train else 0,
|
|
213
|
+
},
|
|
214
|
+
"bySample": {
|
|
215
|
+
sample["id"]: {"target0": list(row.target_0), "target1": list(row.target_1)}
|
|
216
|
+
for sample in samples
|
|
217
|
+
if (row := by_id.get(sample["id"])) is not None
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _score_stability_payload(charts: ReportCharts, samples: list[dict]) -> dict:
|
|
223
|
+
by_id = {_slug(row.sample): row for row in charts.score_psi}
|
|
224
|
+
return {
|
|
225
|
+
"psiAxisMax": charts.psi_axis_max or charts.psi_critical,
|
|
226
|
+
"thresholds": {"warning": charts.psi_warning, "critical": charts.psi_critical},
|
|
227
|
+
"bySample": {
|
|
228
|
+
sample["id"]: {
|
|
229
|
+
"psi": list(row.psi),
|
|
230
|
+
"rows": list(row.rows),
|
|
231
|
+
"scoreCoverage": list(row.coverage),
|
|
232
|
+
}
|
|
233
|
+
for sample in samples
|
|
234
|
+
if (row := by_id.get(sample["id"])) is not None
|
|
235
|
+
},
|
|
236
|
+
}
|
|
142
237
|
|
|
143
238
|
|
|
144
239
|
def _stat_row(label: str, samples: list[dict], by_id: dict[str, SampleStats], value) -> dict:
|