upgini 1.2.167a4220.dev1__tar.gz → 1.2.167a4220.dev3__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 (94) hide show
  1. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/PKG-INFO +1 -1
  2. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/pyproject.toml +3 -0
  3. upgini-1.2.167a4220.dev3/src/upgini/__about__.py +1 -0
  4. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/features_enricher.py +105 -68
  5. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/metrics.py +40 -6
  6. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/report/assemble.py +44 -4
  7. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/report/data.py +22 -2
  8. upgini-1.2.167a4220.dev3/src/upgini/report/html.py +167 -0
  9. upgini-1.2.167a4220.dev3/src/upgini/report/template.html +732 -0
  10. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/resource_bundle/strings.properties +2 -0
  11. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/display_utils.py +54 -37
  12. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/track_info.py +4 -0
  13. upgini-1.2.167a4220.dev1/src/upgini/__about__.py +0 -1
  14. upgini-1.2.167a4220.dev1/src/upgini/report/html.py +0 -281
  15. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/.gitignore +0 -0
  16. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/LICENSE +0 -0
  17. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/README.md +0 -0
  18. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/__init__.py +0 -0
  19. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/ads.py +0 -0
  20. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/ads_management/__init__.py +0 -0
  21. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/ads_management/ads_manager.py +0 -0
  22. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/__init__.py +0 -0
  23. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/all_operators.py +0 -0
  24. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/binary.py +0 -0
  25. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/date.py +0 -0
  26. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/feature.py +0 -0
  27. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/groupby.py +0 -0
  28. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/operand.py +0 -0
  29. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/operator.py +0 -0
  30. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/__init__.py +0 -0
  31. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/base.py +0 -0
  32. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/cross.py +0 -0
  33. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/delta.py +0 -0
  34. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/lag.py +0 -0
  35. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  36. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/roll.py +0 -0
  37. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/trend.py +0 -0
  38. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/timeseries/volatility.py +0 -0
  39. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/unary.py +0 -0
  40. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/utils.py +0 -0
  41. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/autofe/vector.py +0 -0
  42. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/data_source/__init__.py +0 -0
  43. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/data_source/data_source_publisher.py +0 -0
  44. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/dataset.py +0 -0
  45. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/errors.py +0 -0
  46. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/http.py +0 -0
  47. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/mdc/__init__.py +0 -0
  48. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/mdc/context.py +0 -0
  49. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/metadata.py +0 -0
  50. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/normalizer/__init__.py +0 -0
  51. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/normalizer/normalize_utils.py +0 -0
  52. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/report/__init__.py +0 -0
  53. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/resource_bundle/__init__.py +0 -0
  54. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/resource_bundle/exceptions.py +0 -0
  55. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  56. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/sampler/__init__.py +0 -0
  57. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/sampler/base.py +0 -0
  58. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/sampler/random_under_sampler.py +0 -0
  59. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/sampler/utils.py +0 -0
  60. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/search_task.py +0 -0
  61. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/spinner.py +0 -0
  62. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  63. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/__init__.py +0 -0
  64. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/base_search_key_detector.py +0 -0
  65. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/blocked_time_series.py +0 -0
  66. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/config.py +0 -0
  67. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/country_utils.py +0 -0
  68. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/cpu_utils.py +0 -0
  69. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/custom_loss_utils.py +0 -0
  70. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/cv_utils.py +0 -0
  71. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/datetime_utils.py +0 -0
  72. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/deduplicate_utils.py +0 -0
  73. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/email_utils.py +0 -0
  74. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/fallback_progress_bar.py +0 -0
  75. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/feature_info.py +0 -0
  76. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/features_validator.py +0 -0
  77. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/format.py +0 -0
  78. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/hash_utils.py +0 -0
  79. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/ip_utils.py +0 -0
  80. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/mstats.py +0 -0
  81. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/one_hot_encoder.py +0 -0
  82. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/phone_utils.py +0 -0
  83. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/postal_code_utils.py +0 -0
  84. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/progress_bar.py +0 -0
  85. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/psi.py +0 -0
  86. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/pyarrow_utils.py +0 -0
  87. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/sample_utils.py +0 -0
  88. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/search_key_derivations.py +0 -0
  89. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/sklearn_ext.py +0 -0
  90. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/sort.py +0 -0
  91. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/target_utils.py +0 -0
  92. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/ts_utils.py +0 -0
  93. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/src/upgini/utils/warning_counter.py +0 -0
  94. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev3}/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.dev1
3
+ Version: 1.2.167a4220.dev3
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.dev3"
@@ -68,7 +68,13 @@ from upgini.metadata import (
68
68
  RuntimeParameters,
69
69
  SearchKey,
70
70
  )
71
- from upgini.metrics import EstimatorWrapper, define_scorer, validate_scoring_argument
71
+ from upgini.metrics import (
72
+ EstimatorWrapper,
73
+ define_scorer,
74
+ format_metrics_for_display,
75
+ put_cv_metric,
76
+ validate_scoring_argument,
77
+ )
72
78
  from upgini.normalizer.normalize_utils import Normalizer
73
79
  from upgini.resource_bundle import ResourceBundle, bundle, get_custom_bundle
74
80
  from upgini.search_task import SearchTask
@@ -179,9 +185,6 @@ class FeaturesEnricher(TransformerMixin):
179
185
 
180
186
  shared_datasets: list of str, optional (default=None)
181
187
  list of private shared dataset ids for custom search
182
-
183
- reports_path: str, optional (default=None)
184
- Directory for the HTML search report. Defaults to a `reports` folder in the current working directory.
185
188
  """
186
189
 
187
190
  TARGET_NAME = "target"
@@ -259,7 +262,6 @@ class FeaturesEnricher(TransformerMixin):
259
262
  sample_config: SampleConfig | None = None,
260
263
  print_trace_id: bool = False,
261
264
  print_loaded_report: bool = True,
262
- reports_path: str | None = None,
263
265
  **kwargs,
264
266
  ):
265
267
  self.bundle = get_custom_bundle(custom_bundle_config)
@@ -318,6 +320,7 @@ class FeaturesEnricher(TransformerMixin):
318
320
  self.relevant_data_sources: pd.DataFrame = self.EMPTY_DATA_SOURCES
319
321
  self._relevant_data_sources_wo_links: pd.DataFrame = self.EMPTY_DATA_SOURCES
320
322
  self.metrics: pd.DataFrame | None = None
323
+ self.metrics_raw: pd.DataFrame | None = None
321
324
  self.metrics_metric_name: str | None = None
322
325
  self.feature_names_ = []
323
326
  self.external_source_feature_names = []
@@ -328,9 +331,8 @@ class FeaturesEnricher(TransformerMixin):
328
331
  self.disable_force_downsampling = disable_force_downsampling
329
332
  self.print_trace_id = print_trace_id
330
333
  self.add_info: AddInfo | None = None
331
- self.reports_path = reports_path
332
334
  self.search_duration_seconds: float | None = None
333
- self.report_file_path: str | None = None
335
+ self.report_html: str | None = None
334
336
 
335
337
  if search_id:
336
338
  search_task = SearchTask(search_id, rest_client=self.rest_client, logger=self.logger)
@@ -1108,8 +1110,8 @@ class FeaturesEnricher(TransformerMixin):
1108
1110
 
1109
1111
  # 1 If client features are presented - fit and predict with KFold estimator
1110
1112
  # on etalon features and calculate baseline metric
1111
- baseline_metric = None
1112
1113
  baseline_estimator = None
1114
+ baseline_cv_result = None
1113
1115
  updating_shaps = None
1114
1116
  custom_loss_add_params = get_additional_params_custom_loss(
1115
1117
  self.loss, model_task_type, logger=self.logger
@@ -1133,22 +1135,23 @@ class FeaturesEnricher(TransformerMixin):
1133
1135
  baseline_cv_result = baseline_estimator.cross_val_predict(
1134
1136
  fitting_X, y_sorted, self.baseline_score_column
1135
1137
  )
1136
- baseline_metric = baseline_cv_result.get_display_metric()
1137
- if baseline_metric is None:
1138
+ if baseline_cv_result.metric is None:
1138
1139
  self.logger.info(
1139
1140
  f"Baseline {metric} on train client features is None (maybe all features was removed)"
1140
1141
  )
1141
1142
  baseline_estimator = None
1142
1143
  else:
1143
- self.logger.info(f"Baseline {metric} on train client features: {baseline_metric}")
1144
+ self.logger.info(
1145
+ f"Baseline {metric} on train client features: {baseline_cv_result.get_display_metric()}"
1146
+ )
1144
1147
  updating_shaps = baseline_cv_result.shap_values
1145
1148
 
1146
1149
  # 2 Fit and predict with KFold estimator on enriched tds
1147
1150
  # and calculate final metric (and uplift)
1148
- enriched_metric = None
1149
1151
  uplift = None
1150
1152
  uplift_perc = None
1151
1153
  enriched_estimator = None
1154
+ enriched_cv_result = None
1152
1155
  ensemble_score_column = self._get_ensemble_score_column(fitting_X, fitting_enriched_X)
1153
1156
 
1154
1157
  def evaluate_enriched(score_column, description: str):
@@ -1167,17 +1170,16 @@ class FeaturesEnricher(TransformerMixin):
1167
1170
  cv_result = wrapper.cross_val_predict(
1168
1171
  fitting_enriched_X, enriched_y_sorted, score_column
1169
1172
  )
1170
- display_metric = cv_result.get_display_metric()
1171
- if display_metric is None:
1173
+ if cv_result.metric is None:
1172
1174
  self.logger.warning(f"Enriched {metric} {description} is None")
1173
- return None, None, cv_result.shap_values, None, None
1174
- self.logger.info(f"Enriched {metric} {description}: {display_metric}")
1175
+ return None, cv_result, None, None
1176
+ self.logger.info(f"Enriched {metric} {description}: {cv_result.get_display_metric()}")
1175
1177
  sample_uplift = None
1176
1178
  sample_uplift_perc = None
1177
- if baseline_metric is not None:
1179
+ if baseline_cv_result is not None and baseline_cv_result.metric is not None:
1178
1180
  sample_uplift = (cv_result.metric - baseline_cv_result.metric) * multiplier
1179
1181
  sample_uplift_perc = sample_uplift / abs(baseline_cv_result.metric) * 100
1180
- return wrapper, display_metric, cv_result.shap_values, sample_uplift, sample_uplift_perc
1182
+ return wrapper, cv_result, sample_uplift, sample_uplift_perc
1181
1183
 
1182
1184
  if ensemble_score_column is not None:
1183
1185
  self.logger.info(
@@ -1186,14 +1188,14 @@ class FeaturesEnricher(TransformerMixin):
1186
1188
  )
1187
1189
  (
1188
1190
  enriched_estimator,
1189
- enriched_metric,
1190
- updating_shaps,
1191
+ enriched_cv_result,
1191
1192
  uplift,
1192
1193
  uplift_perc,
1193
1194
  ) = evaluate_enriched(
1194
1195
  ensemble_score_column,
1195
1196
  f"on ensemble score column {ensemble_score_column}",
1196
1197
  )
1198
+ updating_shaps = enriched_cv_result.shap_values
1197
1199
  elif set(fitting_X.columns) != set(fitting_enriched_X.columns):
1198
1200
  self.logger.info(
1199
1201
  f"Calculate enriched {metric} on train combined "
@@ -1201,11 +1203,11 @@ class FeaturesEnricher(TransformerMixin):
1201
1203
  )
1202
1204
  (
1203
1205
  enriched_estimator,
1204
- enriched_metric,
1205
- updating_shaps,
1206
+ enriched_cv_result,
1206
1207
  uplift,
1207
1208
  uplift_perc,
1208
1209
  ) = evaluate_enriched(None, "on train combined features")
1210
+ updating_shaps = enriched_cv_result.shap_values
1209
1211
 
1210
1212
  train_metrics = {
1211
1213
  self.bundle.get("quality_metrics_segment_header"): self.bundle.get(
@@ -1223,14 +1225,22 @@ class FeaturesEnricher(TransformerMixin):
1223
1225
  np.mean(y_sorted),
1224
1226
  4,
1225
1227
  )
1226
- if baseline_metric is not None:
1227
- train_metrics[self.bundle.get("quality_metrics_baseline_header").format(metric)] = (
1228
- baseline_metric
1229
- )
1230
- if enriched_metric is not None:
1231
- train_metrics[self.bundle.get("quality_metrics_enriched_header").format(metric)] = (
1232
- enriched_metric
1233
- )
1228
+ put_cv_metric(
1229
+ train_metrics,
1230
+ "quality_metrics_baseline_header",
1231
+ "quality_metrics_baseline_std_header",
1232
+ baseline_cv_result,
1233
+ metric,
1234
+ self.bundle,
1235
+ )
1236
+ put_cv_metric(
1237
+ train_metrics,
1238
+ "quality_metrics_enriched_header",
1239
+ "quality_metrics_enriched_std_header",
1240
+ enriched_cv_result,
1241
+ metric,
1242
+ self.bundle,
1243
+ )
1234
1244
  if uplift is not None:
1235
1245
  train_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(uplift, 3)
1236
1246
  train_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
@@ -1262,12 +1272,13 @@ class FeaturesEnricher(TransformerMixin):
1262
1272
  etalon_eval_results = baseline_estimator.calculate_metric(
1263
1273
  eval_X_sorted, eval_y_sorted, self.baseline_score_column
1264
1274
  )
1265
- etalon_eval_metric = etalon_eval_results.get_display_metric()
1266
1275
  self.logger.info(
1267
- f"Baseline {metric} on eval set {idx + 1} client features: {etalon_eval_metric}"
1276
+ "Baseline {} on eval set {} client features: {}".format(
1277
+ metric, idx + 1, etalon_eval_results.get_display_metric()
1278
+ )
1268
1279
  )
1269
1280
  else:
1270
- etalon_eval_metric = None
1281
+ etalon_eval_results = None
1271
1282
 
1272
1283
  if enriched_estimator is not None:
1273
1284
  self.logger.info(
@@ -1277,14 +1288,20 @@ class FeaturesEnricher(TransformerMixin):
1277
1288
  enriched_eval_results = enriched_estimator.calculate_metric(
1278
1289
  enriched_eval_X_sorted, enriched_eval_y_sorted, ensemble_score_column
1279
1290
  )
1280
- enriched_eval_metric = enriched_eval_results.get_display_metric()
1281
1291
  self.logger.info(
1282
- f"Enriched {metric} on eval set {idx + 1} combined features: {enriched_eval_metric}"
1292
+ "Enriched {} on eval set {} combined features: {}".format(
1293
+ metric, idx + 1, enriched_eval_results.get_display_metric()
1294
+ )
1283
1295
  )
1284
1296
  else:
1285
- enriched_eval_metric = None
1297
+ enriched_eval_results = None
1286
1298
 
1287
- if etalon_eval_metric is not None and enriched_eval_metric is not None:
1299
+ if (
1300
+ etalon_eval_results is not None
1301
+ and etalon_eval_results.metric is not None
1302
+ and enriched_eval_results is not None
1303
+ and enriched_eval_results.metric is not None
1304
+ ):
1288
1305
  eval_uplift = (enriched_eval_results.metric - etalon_eval_results.metric) * multiplier
1289
1306
  eval_uplift_perc = eval_uplift / abs(etalon_eval_results.metric) * 100
1290
1307
  else:
@@ -1308,14 +1325,22 @@ class FeaturesEnricher(TransformerMixin):
1308
1325
  np.mean(eval_y_sorted),
1309
1326
  4,
1310
1327
  )
1311
- if etalon_eval_metric is not None:
1312
- eval_metrics[self.bundle.get("quality_metrics_baseline_header").format(metric)] = (
1313
- etalon_eval_metric
1314
- )
1315
- if enriched_eval_metric is not None:
1316
- eval_metrics[self.bundle.get("quality_metrics_enriched_header").format(metric)] = (
1317
- enriched_eval_metric
1318
- )
1328
+ put_cv_metric(
1329
+ eval_metrics,
1330
+ "quality_metrics_baseline_header",
1331
+ "quality_metrics_baseline_std_header",
1332
+ etalon_eval_results,
1333
+ metric,
1334
+ self.bundle,
1335
+ )
1336
+ put_cv_metric(
1337
+ eval_metrics,
1338
+ "quality_metrics_enriched_header",
1339
+ "quality_metrics_enriched_std_header",
1340
+ enriched_eval_results,
1341
+ metric,
1342
+ self.bundle,
1343
+ )
1319
1344
  if eval_uplift is not None:
1320
1345
  eval_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(eval_uplift, 3)
1321
1346
  eval_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
@@ -1352,8 +1377,9 @@ class FeaturesEnricher(TransformerMixin):
1352
1377
  elif uplift_col in metrics_df.columns and (metrics_df[uplift_col] < 0).any():
1353
1378
  self.logger.warning("Uplift is negative")
1354
1379
 
1380
+ self.metrics_raw = metrics_df
1355
1381
  self.metrics_metric_name = metric
1356
- return metrics_df
1382
+ return format_metrics_for_display(metrics_df, metric, self.bundle)
1357
1383
  except Exception as e:
1358
1384
  error_message = "Failed to calculate metrics" + (
1359
1385
  " with validation error" if isinstance(e, ValidationError) else ""
@@ -2274,9 +2300,6 @@ class FeaturesEnricher(TransformerMixin):
2274
2300
  return None
2275
2301
  return ensemble[0]
2276
2302
 
2277
- def _reports_dir(self) -> Path:
2278
- return Path(self.reports_path) if self.reports_path else Path.cwd() / "reports"
2279
-
2280
2303
  def _reference_rows(self) -> int | None:
2281
2304
  if self.X is None:
2282
2305
  return None
@@ -2299,23 +2322,19 @@ class FeaturesEnricher(TransformerMixin):
2299
2322
  search_duration_seconds=self.search_duration_seconds,
2300
2323
  reference_rows=self._reference_rows(),
2301
2324
  samples=self._report_sample_names(),
2302
- metrics_df=self.metrics,
2325
+ metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
2303
2326
  metric_name=self.metrics_metric_name,
2327
+ model_features=self._ensemble_model_feature_count(),
2328
+ is_binary=self.model_task_type == ModelTaskType.BINARY,
2304
2329
  bundle=self.bundle,
2305
2330
  )
2306
2331
 
2307
- def _write_score_report(self) -> str | None:
2332
+ def _score_report_html(self) -> str | None:
2308
2333
  if self._search_task is None or not self._has_single_ensemble_score():
2309
2334
  return None
2310
2335
  report_data = self._assemble_report_data()
2311
- html = generate_html_report(report_data)
2312
- reports_dir = self._reports_dir()
2313
- reports_dir.mkdir(parents=True, exist_ok=True)
2314
- path = reports_dir / f"upgini-report-{report_data.metadata.search_id}.html"
2315
- path.write_text(html, encoding="utf-8")
2316
- self.report_file_path = str(path)
2317
- self.logger.info(f"Score report saved to {path}")
2318
- return self.report_file_path
2336
+ self.report_html = generate_html_report(report_data)
2337
+ return self.report_html
2319
2338
 
2320
2339
  def _is_ensemble_feature(self, column_name: str) -> bool:
2321
2340
  renaming = self.fit_columns_renaming or {}
@@ -2325,15 +2344,20 @@ class FeaturesEnricher(TransformerMixin):
2325
2344
  return False
2326
2345
  return bool(aliases & self._column_name_aliases(list(ensemble_names), renaming))
2327
2346
 
2328
- def _ensemble_feature_names_from_metadata(self) -> set[str]:
2347
+ def _ensemble_generated_metadata(self):
2329
2348
  autofe_meta = self._search_task.get_autofe_metadata() if self._search_task else None
2330
2349
  if not autofe_meta:
2331
- return set()
2332
- names: set[str] = set()
2350
+ return []
2351
+ matched = []
2333
2352
  for meta in autofe_meta:
2334
2353
  op = EnsembleModel.from_formula(meta.formula.split("(")[0])
2335
- if op is None:
2336
- continue
2354
+ if op is not None:
2355
+ matched.append((meta, op))
2356
+ return matched
2357
+
2358
+ def _ensemble_feature_names_from_metadata(self) -> set[str]:
2359
+ names: set[str] = set()
2360
+ for meta, op in self._ensemble_generated_metadata():
2337
2361
  feature = (
2338
2362
  Feature(op, [])
2339
2363
  .set_display_index(meta.display_index)
@@ -2345,6 +2369,14 @@ class FeaturesEnricher(TransformerMixin):
2345
2369
  names.add(meta.alias)
2346
2370
  return names
2347
2371
 
2372
+ def _ensemble_model_feature_count(self) -> int | None:
2373
+ names = {
2374
+ column.original_name
2375
+ for meta, _ in self._ensemble_generated_metadata()
2376
+ for column in meta.base_columns
2377
+ }
2378
+ return len(names) or None
2379
+
2348
2380
  def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
2349
2381
  return psi is not None and not pd.isna(psi) and psi > threshold
2350
2382
 
@@ -4219,7 +4251,9 @@ if response.status_code == 200:
4219
4251
  self._fit_match_search_keys = None
4220
4252
  self.__cached_sampled_datasets = dict()
4221
4253
  self.metrics = None
4254
+ self.metrics_raw = None
4222
4255
  self.metrics_metric_name = None
4256
+ self.report_html = None
4223
4257
  self.fit_columns_renaming = None
4224
4258
  self.fit_dropped_features = set()
4225
4259
  self.fit_generated_features = []
@@ -6037,13 +6071,16 @@ if response.status_code == 200:
6037
6071
 
6038
6072
  def __show_report_button(self, display_id: str | None = None, display_handle=None):
6039
6073
  try:
6040
- report_path = self._write_score_report()
6041
- if report_path is not None:
6074
+ report_html = self._score_report_html() or self.report_html
6075
+ if report_html:
6042
6076
  if not ipython_available():
6043
- print(f"Full report saved to {report_path}")
6044
6077
  return
6078
+ search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
6045
6079
  return show_button_open_report(
6046
- report_path, display_id=display_id, display_handle=display_handle
6080
+ report_html,
6081
+ download_name=f"upgini-report-{search_id}.html",
6082
+ display_id=display_id,
6083
+ display_handle=display_handle,
6047
6084
  )
6048
6085
 
6049
6086
  eval_sets_drift_df = self._get_eval_sets_drift_summary()
@@ -253,12 +253,46 @@ class _CrossValResults:
253
253
  shap_values: Optional[Dict[str, float]]
254
254
 
255
255
  def get_display_metric(self) -> Optional[str]:
256
- if self.metric is None:
257
- return None
258
- elif self.metric_std is None:
259
- return f"{self.metric:.3f}"
260
- else:
261
- return f"{self.metric:.3f} ± {self.metric_std:.3f}"
256
+ return format_display_metric(self.metric, self.metric_std)
257
+
258
+
259
+ def format_display_metric(metric: Optional[float], metric_std: Optional[float] = None) -> Optional[str]:
260
+ if metric is None or pd.isna(metric):
261
+ return None
262
+ if metric_std is None or pd.isna(metric_std):
263
+ return f"{float(metric):.3f}"
264
+ return f"{float(metric):.3f} ± {float(metric_std):.3f}"
265
+
266
+
267
+ def format_metrics_for_display(metrics_df: pd.DataFrame, metric_name: str, bundle) -> pd.DataFrame:
268
+ display = metrics_df.copy()
269
+ drop: list[str] = []
270
+ for value_key, std_key in (
271
+ ("quality_metrics_baseline_header", "quality_metrics_baseline_std_header"),
272
+ ("quality_metrics_enriched_header", "quality_metrics_enriched_std_header"),
273
+ ):
274
+ col = bundle.get(value_key).format(metric_name)
275
+ std_col = bundle.get(std_key).format(metric_name)
276
+ if col in display.columns:
277
+ stds = display[std_col] if std_col in display.columns else [None] * len(display)
278
+ display[col] = [format_display_metric(value, std) for value, std in zip(display[col], stds)]
279
+ if std_col in display.columns:
280
+ drop.append(std_col)
281
+ return display.drop(columns=drop, errors="ignore")
282
+
283
+
284
+ def put_cv_metric(
285
+ row: dict,
286
+ value_key: str,
287
+ std_key: str,
288
+ result: Optional[_CrossValResults],
289
+ metric_name: str,
290
+ bundle,
291
+ ) -> None:
292
+ if result is None or result.metric is None:
293
+ return
294
+ row[bundle.get(value_key).format(metric_name)] = result.metric
295
+ row[bundle.get(std_key).format(metric_name)] = result.metric_std
262
296
 
263
297
 
264
298
  def is_numeric_object(x: pd.Series) -> bool:
@@ -6,7 +6,7 @@ from typing import Optional
6
6
  import pandas as pd
7
7
 
8
8
  from upgini.__about__ import __version__
9
- from upgini.report.data import QualitySample, ReportData, ReportMetadata
9
+ from upgini.report.data import QualitySample, ReportData, ReportMetadata, SampleStats, SearchResultsSummary
10
10
  from upgini.resource_bundle import ResourceBundle
11
11
 
12
12
 
@@ -34,9 +34,13 @@ def assemble_report_data(
34
34
  samples: Optional[list[str]] = None,
35
35
  metrics_df: Optional[pd.DataFrame] = None,
36
36
  metric_name: Optional[str] = None,
37
+ model_features: Optional[int] = None,
38
+ is_binary: bool = False,
39
+ logo_url: Optional[str] = None,
37
40
  bundle: ResourceBundle,
38
41
  ) -> ReportData:
39
42
  generated_at = generated_at or datetime.now(timezone.utc)
43
+ sample_names = list(samples or [])
40
44
  return ReportData(
41
45
  metadata=ReportMetadata(
42
46
  search_id=search_id,
@@ -45,9 +49,13 @@ def assemble_report_data(
45
49
  search_keys=list(search_keys or []),
46
50
  search_duration=format_search_duration(search_duration_seconds),
47
51
  reference_rows=reference_rows,
48
- samples=list(samples or []),
52
+ samples=sample_names,
53
+ logo_url=logo_url or None,
49
54
  ),
50
55
  quality_by_sample=_quality_samples(metrics_df, metric_name, bundle),
56
+ summary=SearchResultsSummary(model_features=model_features),
57
+ sample_stats=_sample_stats(sample_names, metrics_df, bundle),
58
+ is_binary=is_binary,
51
59
  )
52
60
 
53
61
 
@@ -61,6 +69,8 @@ def _quality_samples(
61
69
  uplift_pct_col = bundle.get("quality_metrics_uplift_perc_header")
62
70
  baseline_col = bundle.get("quality_metrics_baseline_header").format(metric_name) if metric_name else None
63
71
  enriched_col = bundle.get("quality_metrics_enriched_header").format(metric_name) if metric_name else None
72
+ baseline_std_col = bundle.get("quality_metrics_baseline_std_header").format(metric_name) if metric_name else None
73
+ enriched_std_col = bundle.get("quality_metrics_enriched_std_header").format(metric_name) if metric_name else None
64
74
 
65
75
  samples: list[QualitySample] = []
66
76
  for _, row in metrics_df.iterrows():
@@ -68,8 +78,10 @@ def _quality_samples(
68
78
  QualitySample(
69
79
  evaluation_scope=(_as_str(row[segment_col]) or "") if segment_col in metrics_df.columns else "",
70
80
  metric=metric_name or "",
71
- baseline=_as_str(row[baseline_col]) if baseline_col in metrics_df.columns else None,
72
- enriched=_as_str(row[enriched_col]) if enriched_col in metrics_df.columns else None,
81
+ baseline=_as_float(row[baseline_col]) if baseline_col in metrics_df.columns else None,
82
+ baseline_std=_as_float(row[baseline_std_col]) if baseline_std_col in metrics_df.columns else None,
83
+ enriched=_as_float(row[enriched_col]) if enriched_col in metrics_df.columns else None,
84
+ enriched_std=_as_float(row[enriched_std_col]) if enriched_std_col in metrics_df.columns else None,
73
85
  uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
74
86
  relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
75
87
  )
@@ -77,9 +89,37 @@ def _quality_samples(
77
89
  return samples
78
90
 
79
91
 
92
+ def _sample_stats(
93
+ sample_names: list[str], metrics_df: Optional[pd.DataFrame], bundle: ResourceBundle
94
+ ) -> list[SampleStats]:
95
+ segment_col = bundle.get("quality_metrics_segment_header")
96
+ rows_col = bundle.get("quality_metrics_rows_header")
97
+ mean_col = bundle.get("quality_metrics_mean_target_header")
98
+ by_name: dict[str, SampleStats] = {}
99
+ if metrics_df is not None and not metrics_df.empty and segment_col in metrics_df.columns:
100
+ for _, row in metrics_df.iterrows():
101
+ name = _as_str(row[segment_col])
102
+ if not name:
103
+ continue
104
+ by_name[name] = SampleStats(
105
+ sample=name,
106
+ rows=_as_int(row[rows_col]) if rows_col in metrics_df.columns else None,
107
+ mean_target=_as_float(row[mean_col]) if mean_col in metrics_df.columns else None,
108
+ )
109
+ names = sample_names or list(by_name)
110
+ stats = [by_name.get(name) or SampleStats(sample=name) for name in names]
111
+ seen = set(names)
112
+ stats.extend(sample for name, sample in by_name.items() if name not in seen)
113
+ return stats
114
+
115
+
80
116
  def _as_str(value) -> Optional[str]:
81
117
  return None if pd.isna(value) else str(value)
82
118
 
83
119
 
84
120
  def _as_float(value) -> Optional[float]:
85
121
  return None if pd.isna(value) else float(value)
122
+
123
+
124
+ def _as_int(value) -> Optional[int]:
125
+ return None if pd.isna(value) else int(value)
@@ -13,17 +13,35 @@ class ReportMetadata:
13
13
  search_duration: Optional[str] = None
14
14
  reference_rows: Optional[int] = None
15
15
  samples: list[str] = field(default_factory=list)
16
+ logo_url: Optional[str] = None
16
17
 
17
18
 
18
19
  @dataclass
19
20
  class QualitySample:
20
21
  evaluation_scope: str
21
22
  metric: str
22
- baseline: Optional[str] = None
23
- enriched: Optional[str] = None
23
+ baseline: Optional[float] = None
24
+ baseline_std: Optional[float] = None
25
+ enriched: Optional[float] = None
26
+ enriched_std: Optional[float] = None
24
27
  uplift: Optional[float] = None
25
28
  relative_uplift: Optional[str] = None
26
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
+
34
+
35
+ @dataclass
36
+ class SampleStats:
37
+ sample: str
38
+ rows: Optional[int] = None
39
+ mean_target: Optional[float] = None
40
+ date_range: Optional[str] = None
41
+ labeled: Optional[int] = None
42
+ unlabeled: Optional[int] = None
43
+ positive: Optional[int] = None
44
+
27
45
 
28
46
  @dataclass
29
47
  class FeatureRow:
@@ -66,6 +84,8 @@ class ReportData:
66
84
  metadata: ReportMetadata
67
85
  quality_by_sample: list[QualitySample] = field(default_factory=list)
68
86
  summary: SearchResultsSummary = field(default_factory=SearchResultsSummary)
87
+ sample_stats: list[SampleStats] = field(default_factory=list)
88
+ is_binary: bool = False
69
89
  features: list[FeatureRow] = field(default_factory=list)
70
90
  sources: list[SourceRow] = field(default_factory=list)
71
91
  autofe: list[dict[str, str]] = field(default_factory=list)