upgini 1.2.167a4220.dev2__tar.gz → 1.2.167a4220.dev4__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.
Files changed (96) hide show
  1. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/PKG-INFO +1 -1
  2. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/pyproject.toml +3 -0
  3. upgini-1.2.167a4220.dev4/src/upgini/__about__.py +1 -0
  4. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/features_enricher.py +135 -25
  5. upgini-1.2.167a4220.dev4/src/upgini/report/__init__.py +11 -0
  6. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/report/assemble.py +25 -14
  7. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/report/data.py +71 -1
  8. upgini-1.2.167a4220.dev4/src/upgini/report/html.py +262 -0
  9. upgini-1.2.167a4220.dev4/src/upgini/report/stats.py +426 -0
  10. upgini-1.2.167a4220.dev4/src/upgini/report/template.html +737 -0
  11. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/display_utils.py +31 -20
  12. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/psi.py +20 -1
  13. upgini-1.2.167a4220.dev2/src/upgini/__about__.py +0 -1
  14. upgini-1.2.167a4220.dev2/src/upgini/report/__init__.py +0 -5
  15. upgini-1.2.167a4220.dev2/src/upgini/report/html.py +0 -385
  16. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/.gitignore +0 -0
  17. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/LICENSE +0 -0
  18. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/README.md +0 -0
  19. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/__init__.py +0 -0
  20. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/ads.py +0 -0
  21. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/ads_management/__init__.py +0 -0
  22. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/ads_management/ads_manager.py +0 -0
  23. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/__init__.py +0 -0
  24. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/all_operators.py +0 -0
  25. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/binary.py +0 -0
  26. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/date.py +0 -0
  27. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/feature.py +0 -0
  28. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/groupby.py +0 -0
  29. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/operand.py +0 -0
  30. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/operator.py +0 -0
  31. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/__init__.py +0 -0
  32. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/base.py +0 -0
  33. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/cross.py +0 -0
  34. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/delta.py +0 -0
  35. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/lag.py +0 -0
  36. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  37. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/roll.py +0 -0
  38. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/trend.py +0 -0
  39. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/timeseries/volatility.py +0 -0
  40. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/unary.py +0 -0
  41. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/utils.py +0 -0
  42. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/autofe/vector.py +0 -0
  43. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/data_source/__init__.py +0 -0
  44. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/data_source/data_source_publisher.py +0 -0
  45. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/dataset.py +0 -0
  46. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/errors.py +0 -0
  47. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/http.py +0 -0
  48. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/mdc/__init__.py +0 -0
  49. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/mdc/context.py +0 -0
  50. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/metadata.py +0 -0
  51. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/metrics.py +0 -0
  52. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/normalizer/__init__.py +0 -0
  53. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/normalizer/normalize_utils.py +0 -0
  54. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/resource_bundle/__init__.py +0 -0
  55. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/resource_bundle/exceptions.py +0 -0
  56. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/resource_bundle/strings.properties +0 -0
  57. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  58. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/sampler/__init__.py +0 -0
  59. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/sampler/base.py +0 -0
  60. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/sampler/random_under_sampler.py +0 -0
  61. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/sampler/utils.py +0 -0
  62. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/search_task.py +0 -0
  63. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/spinner.py +0 -0
  64. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  65. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/__init__.py +0 -0
  66. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/base_search_key_detector.py +0 -0
  67. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/blocked_time_series.py +0 -0
  68. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/config.py +0 -0
  69. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/country_utils.py +0 -0
  70. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/cpu_utils.py +0 -0
  71. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/custom_loss_utils.py +0 -0
  72. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/cv_utils.py +0 -0
  73. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/datetime_utils.py +0 -0
  74. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/deduplicate_utils.py +0 -0
  75. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/email_utils.py +0 -0
  76. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/fallback_progress_bar.py +0 -0
  77. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/feature_info.py +0 -0
  78. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/features_validator.py +0 -0
  79. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/format.py +0 -0
  80. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/hash_utils.py +0 -0
  81. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/ip_utils.py +0 -0
  82. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/mstats.py +0 -0
  83. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/one_hot_encoder.py +0 -0
  84. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/phone_utils.py +0 -0
  85. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/postal_code_utils.py +0 -0
  86. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/progress_bar.py +0 -0
  87. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/pyarrow_utils.py +0 -0
  88. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/sample_utils.py +0 -0
  89. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/search_key_derivations.py +0 -0
  90. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/sklearn_ext.py +0 -0
  91. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/sort.py +0 -0
  92. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/target_utils.py +0 -0
  93. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/track_info.py +0 -0
  94. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/ts_utils.py +0 -0
  95. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/src/upgini/utils/warning_counter.py +0 -0
  96. {upgini-1.2.167a4220.dev2 → upgini-1.2.167a4220.dev4}/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.dev2
3
+ Version: 1.2.167a4220.dev4
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/
@@ -73,6 +73,9 @@ packages = [
73
73
  "src/upgini"
74
74
  ]
75
75
 
76
+ [tool.hatch.build.targets.wheel.force-include]
77
+ "src/upgini/report/template.html" = "upgini/report/template.html"
78
+
76
79
  [tool.hatch.build]
77
80
  include = [
78
81
  "/src/utils/Roboto-Regular.ttf",
@@ -0,0 +1 @@
1
+ __version__ = "1.2.167.a4220.dev4"
@@ -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,
@@ -185,9 +186,6 @@ class FeaturesEnricher(TransformerMixin):
185
186
 
186
187
  shared_datasets: list of str, optional (default=None)
187
188
  list of private shared dataset ids for custom search
188
-
189
- reports_path: str, optional (default=None)
190
- Directory for the HTML search report. Defaults to a `reports` folder in the current working directory.
191
189
  """
192
190
 
193
191
  TARGET_NAME = "target"
@@ -265,7 +263,6 @@ class FeaturesEnricher(TransformerMixin):
265
263
  sample_config: SampleConfig | None = None,
266
264
  print_trace_id: bool = False,
267
265
  print_loaded_report: bool = True,
268
- reports_path: str | None = None,
269
266
  **kwargs,
270
267
  ):
271
268
  self.bundle = get_custom_bundle(custom_bundle_config)
@@ -335,9 +332,8 @@ class FeaturesEnricher(TransformerMixin):
335
332
  self.disable_force_downsampling = disable_force_downsampling
336
333
  self.print_trace_id = print_trace_id
337
334
  self.add_info: AddInfo | None = None
338
- self.reports_path = reports_path
339
335
  self.search_duration_seconds: float | None = None
340
- self.report_file_path: str | None = None
336
+ self.report_html: str | None = None
341
337
 
342
338
  if search_id:
343
339
  search_task = SearchTask(search_id, rest_client=self.rest_client, logger=self.logger)
@@ -2305,9 +2301,6 @@ class FeaturesEnricher(TransformerMixin):
2305
2301
  return None
2306
2302
  return ensemble[0]
2307
2303
 
2308
- def _reports_dir(self) -> Path:
2309
- return Path(self.reports_path) if self.reports_path else Path.cwd() / "reports"
2310
-
2311
2304
  def _reference_rows(self) -> int | None:
2312
2305
  if self.X is None:
2313
2306
  return None
@@ -2316,17 +2309,135 @@ class FeaturesEnricher(TransformerMixin):
2316
2309
  rows += _num_samples(eval_pair[0])
2317
2310
  return rows
2318
2311
 
2312
+ def _search_key_names(self) -> list[str]:
2313
+ return [key.name for key in self.search_keys.values()]
2314
+
2319
2315
  def _report_sample_names(self) -> list[str]:
2320
2316
  samples = [self.bundle.get("quality_metrics_train_segment")]
2321
2317
  for idx, _ in enumerate(self.eval_set or []):
2322
2318
  samples.append(self.bundle.get("quality_metrics_eval_segment").format(idx + 1))
2323
2319
  return samples
2324
2320
 
2321
+ def _report_dataset_samples(self) -> dict[str, pd.DataFrame]:
2322
+ names = self._report_sample_names()
2323
+ if self.df_with_original_index is not None:
2324
+ return self._report_samples_from_combined(self.df_with_original_index, names, self.fit_search_keys)
2325
+ if self.X is None or self.y is None:
2326
+ return {}
2327
+ date_col = self._get_date_column(self.search_keys)
2328
+ frames = {names[0]: self._report_xy_frame(self.X, self.y, date_col)}
2329
+ for idx, (eval_x, eval_y) in enumerate(self.eval_set or []):
2330
+ if idx + 1 >= len(names):
2331
+ break
2332
+ frames[names[idx + 1]] = self._report_xy_frame(eval_x, eval_y, date_col)
2333
+ return frames
2334
+
2335
+ def _report_samples_from_combined(
2336
+ self, df: pd.DataFrame, names: list[str], search_keys: dict[str, SearchKey] | None
2337
+ ) -> dict[str, pd.DataFrame]:
2338
+ date_col = self._get_date_column(search_keys or {})
2339
+ if EVAL_SET_INDEX not in df.columns:
2340
+ return {names[0]: self._report_df_frame(df, date_col)} if names else {}
2341
+ frames: dict[str, pd.DataFrame] = {}
2342
+ for eval_idx in sorted(pd.unique(df[EVAL_SET_INDEX].dropna())):
2343
+ name_idx = int(eval_idx)
2344
+ if name_idx >= len(names):
2345
+ continue
2346
+ frames[names[name_idx]] = self._report_df_frame(df[df[EVAL_SET_INDEX] == eval_idx], date_col)
2347
+ return frames
2348
+
2349
+ def _report_xy_frame(self, X, y, date_col: str | None) -> pd.DataFrame:
2350
+ date = X[date_col] if isinstance(X, pd.DataFrame) and date_col and date_col in X.columns else None
2351
+ return make_report_frame(y, date=date)
2352
+
2353
+ def _report_df_frame(self, df: pd.DataFrame, date_col: str | None) -> pd.DataFrame:
2354
+ target = df[TARGET] if TARGET in df.columns else pd.Series(np.nan, index=df.index)
2355
+ return self._report_xy_frame(df, target, date_col)
2356
+
2357
+ def _report_scored_samples(self) -> dict[str, pd.DataFrame]:
2358
+ cached = self._peek_cached_sampled()
2359
+ if cached is None:
2360
+ return {}
2361
+ X_sampled, y_sampled, enriched_X, eval_set_sampled_dict, search_keys, columns_renaming, _generated = cached
2362
+ score_col = self._report_score_column(enriched_X)
2363
+ if score_col is None:
2364
+ return {}
2365
+ date_col = self._get_date_column(search_keys or {})
2366
+ names = self._report_sample_names()
2367
+ frames = {
2368
+ names[0]: self._report_scored_frame(
2369
+ X_sampled, y_sampled, enriched_X, score_col, date_col, columns_renaming
2370
+ )
2371
+ }
2372
+ for idx, (eval_X, enriched_eval_X, eval_y) in eval_set_sampled_dict.items():
2373
+ name_idx = idx + 1
2374
+ if name_idx >= len(names):
2375
+ continue
2376
+ frames[names[name_idx]] = self._report_scored_frame(
2377
+ eval_X, eval_y, enriched_eval_X, score_col, date_col, columns_renaming
2378
+ )
2379
+ return frames
2380
+
2381
+ def _report_scored_frame(
2382
+ self,
2383
+ X: pd.DataFrame,
2384
+ y,
2385
+ enriched: pd.DataFrame,
2386
+ score_col: str,
2387
+ date_col: str | None,
2388
+ columns_renaming: dict[str, str] | None,
2389
+ ) -> pd.DataFrame:
2390
+ target = y
2391
+ if target is None and TARGET in enriched.columns:
2392
+ target = enriched[TARGET]
2393
+ date_name = self._report_existing_column(date_col, columns_renaming, X, enriched)
2394
+ baseline_name = self._report_existing_column(self.baseline_score_column, columns_renaming, X, enriched)
2395
+ return make_report_frame(
2396
+ target,
2397
+ date=self._report_column_values(date_name, X, enriched),
2398
+ score=enriched[score_col] if score_col in enriched.columns else None,
2399
+ baseline=self._report_column_values(baseline_name, X, enriched),
2400
+ )
2401
+
2402
+ def _report_score_column(self, enriched_X: pd.DataFrame) -> str | None:
2403
+ name = self._get_single_ensemble_score_name()
2404
+ if name and name in enriched_X.columns:
2405
+ return name
2406
+ return None
2407
+
2408
+ def _report_existing_column(
2409
+ self, name: str | None, renaming: dict[str, str] | None, *frames: pd.DataFrame
2410
+ ) -> str | None:
2411
+ if not name:
2412
+ return None
2413
+ aliases = self._column_name_aliases([name], renaming or {})
2414
+ for frame in frames:
2415
+ if not isinstance(frame, pd.DataFrame):
2416
+ continue
2417
+ for col in frame.columns:
2418
+ if col in aliases:
2419
+ return col
2420
+ return None
2421
+
2422
+ @staticmethod
2423
+ def _report_column_values(column: str | None, *frames: pd.DataFrame):
2424
+ if not column:
2425
+ return None
2426
+ for frame in frames:
2427
+ if isinstance(frame, pd.DataFrame) and column in frame.columns:
2428
+ return frame[column]
2429
+ return None
2430
+
2431
+ def _peek_cached_sampled(self):
2432
+ if not self.__cached_sampled_datasets:
2433
+ return None
2434
+ return max(self.__cached_sampled_datasets.values(), key=lambda item: len(item[0]) if item[0] is not None else 0)
2435
+
2325
2436
  def _assemble_report_data(self):
2326
2437
  search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
2327
2438
  return assemble_report_data(
2328
2439
  search_id=search_id,
2329
- search_keys=[str(sk) for sk in self.search_keys.values()],
2440
+ search_keys=self._search_key_names(),
2330
2441
  search_duration_seconds=self.search_duration_seconds,
2331
2442
  reference_rows=self._reference_rows(),
2332
2443
  samples=self._report_sample_names(),
@@ -2334,21 +2445,17 @@ class FeaturesEnricher(TransformerMixin):
2334
2445
  metric_name=self.metrics_metric_name,
2335
2446
  model_features=self._ensemble_model_feature_count(),
2336
2447
  is_binary=self.model_task_type == ModelTaskType.BINARY,
2448
+ dataset_samples=self._report_dataset_samples(),
2449
+ scored_samples=self._report_scored_samples(),
2337
2450
  bundle=self.bundle,
2338
2451
  )
2339
2452
 
2340
- def _write_score_report(self) -> str | None:
2453
+ def _score_report_html(self) -> str | None:
2341
2454
  if self._search_task is None or not self._has_single_ensemble_score():
2342
2455
  return None
2343
2456
  report_data = self._assemble_report_data()
2344
- html = generate_html_report(report_data)
2345
- reports_dir = self._reports_dir()
2346
- reports_dir.mkdir(parents=True, exist_ok=True)
2347
- path = reports_dir / f"upgini-report-{report_data.metadata.search_id}.html"
2348
- path.write_text(html, encoding="utf-8")
2349
- self.report_file_path = str(path)
2350
- self.logger.info(f"Score report saved to {path}")
2351
- return self.report_file_path
2457
+ self.report_html = generate_html_report(report_data)
2458
+ return self.report_html
2352
2459
 
2353
2460
  def _is_ensemble_feature(self, column_name: str) -> bool:
2354
2461
  renaming = self.fit_columns_renaming or {}
@@ -4267,7 +4374,7 @@ if response.status_code == 200:
4267
4374
  self.metrics = None
4268
4375
  self.metrics_raw = None
4269
4376
  self.metrics_metric_name = None
4270
- self.report_file_path = None
4377
+ self.report_html = None
4271
4378
  self.fit_columns_renaming = None
4272
4379
  self.fit_dropped_features = set()
4273
4380
  self.fit_generated_features = []
@@ -6085,13 +6192,16 @@ if response.status_code == 200:
6085
6192
 
6086
6193
  def __show_report_button(self, display_id: str | None = None, display_handle=None):
6087
6194
  try:
6088
- report_path = self._write_score_report() or self.report_file_path
6089
- if report_path:
6195
+ report_html = self._score_report_html() or self.report_html
6196
+ if report_html:
6090
6197
  if not ipython_available():
6091
- print(f"Full report saved to {report_path}")
6092
6198
  return
6199
+ search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
6093
6200
  return show_button_open_report(
6094
- report_path, display_id=display_id, display_handle=display_handle
6201
+ report_html,
6202
+ download_name=f"upgini-report-{search_id}.html",
6203
+ display_id=display_id,
6204
+ display_handle=display_handle,
6095
6205
  )
6096
6206
 
6097
6207
  eval_sets_drift_df = self._get_eval_sets_drift_summary()
@@ -6103,7 +6213,7 @@ if response.status_code == 200:
6103
6213
  autofe_descriptions_df=self.get_autofe_features_description(),
6104
6214
  search_id=self._search_task.search_task_id,
6105
6215
  email=self.rest_client.get_current_email(),
6106
- search_keys=[str(sk) for sk in self.search_keys.values()],
6216
+ search_keys=self._search_key_names(),
6107
6217
  eval_sets_drift_df=eval_sets_drift_df,
6108
6218
  display_id=display_id,
6109
6219
  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
- logo_url: Optional[str] = None,
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=logo_url or None,
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,8 +99,9 @@ def _quality_samples(
79
99
  evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
80
100
  metric=metric_name or "",
81
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,
82
103
  enriched=_as_float(row[enriched_col]) if enriched_col in metrics_df.columns else None,
83
- std=_metric_std(row, metrics_df.columns, enriched_std_col, baseline_std_col),
104
+ enriched_std=_as_float(row[enriched_std_col]) if enriched_std_col in metrics_df.columns else None,
84
105
  uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
85
106
  relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
86
107
  )
@@ -112,16 +133,6 @@ def _sample_stats(
112
133
  return stats
113
134
 
114
135
 
115
- def _metric_std(row, columns, enriched_std_col: Optional[str], baseline_std_col: Optional[str]) -> Optional[float]:
116
- if enriched_std_col in columns:
117
- std = _as_float(row[enriched_std_col])
118
- if std is not None:
119
- return std
120
- if baseline_std_col in columns:
121
- return _as_float(row[baseline_std_col])
122
- return None
123
-
124
-
125
136
  def _as_str(value) -> Optional[str]:
126
137
  return None if pd.isna(value) else str(value)
127
138
 
@@ -21,11 +21,16 @@ class QualitySample:
21
21
  evaluation_scope: str
22
22
  metric: str
23
23
  baseline: Optional[float] = None
24
+ baseline_std: Optional[float] = None
24
25
  enriched: Optional[float] = None
25
- std: Optional[float] = None
26
+ enriched_std: Optional[float] = None
26
27
  uplift: Optional[float] = None
27
28
  relative_uplift: Optional[str] = None
28
29
 
30
+ @property
31
+ def std(self) -> Optional[float]:
32
+ return self.enriched_std if self.enriched_std is not None else self.baseline_std
33
+
29
34
 
30
35
  @dataclass
31
36
  class SampleStats:
@@ -74,6 +79,70 @@ class ModelFeatureShap:
74
79
  mean_abs_shap: Optional[float] = None
75
80
 
76
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
+
77
146
  @dataclass
78
147
  class ReportData:
79
148
  metadata: ReportMetadata
@@ -85,3 +154,4 @@ class ReportData:
85
154
  sources: list[SourceRow] = field(default_factory=list)
86
155
  autofe: list[dict[str, str]] = field(default_factory=list)
87
156
  model_feature_shap: list[ModelFeatureShap] = field(default_factory=list)
157
+ charts: ReportCharts = field(default_factory=ReportCharts)