upgini 1.2.167a4220.dev1__tar.gz → 1.2.167a4220.dev2__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 (93) hide show
  1. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/PKG-INFO +1 -1
  2. upgini-1.2.167a4220.dev2/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/features_enricher.py +96 -48
  4. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/metrics.py +40 -6
  5. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/report/assemble.py +53 -4
  6. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/report/data.py +17 -2
  7. upgini-1.2.167a4220.dev2/src/upgini/report/html.py +385 -0
  8. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/strings.properties +2 -0
  9. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/display_utils.py +41 -35
  10. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/track_info.py +4 -0
  11. upgini-1.2.167a4220.dev1/src/upgini/__about__.py +0 -1
  12. upgini-1.2.167a4220.dev1/src/upgini/report/html.py +0 -281
  13. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/.gitignore +0 -0
  14. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/LICENSE +0 -0
  15. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/README.md +0 -0
  16. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/pyproject.toml +0 -0
  17. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/__init__.py +0 -0
  18. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/ads.py +0 -0
  19. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/ads_management/__init__.py +0 -0
  20. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/ads_management/ads_manager.py +0 -0
  21. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/__init__.py +0 -0
  22. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/all_operators.py +0 -0
  23. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/binary.py +0 -0
  24. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/date.py +0 -0
  25. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/feature.py +0 -0
  26. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/groupby.py +0 -0
  27. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/operand.py +0 -0
  28. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/operator.py +0 -0
  29. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/__init__.py +0 -0
  30. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/base.py +0 -0
  31. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/cross.py +0 -0
  32. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/delta.py +0 -0
  33. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/lag.py +0 -0
  34. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  35. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/roll.py +0 -0
  36. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/trend.py +0 -0
  37. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/timeseries/volatility.py +0 -0
  38. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/unary.py +0 -0
  39. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/utils.py +0 -0
  40. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/autofe/vector.py +0 -0
  41. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/data_source/__init__.py +0 -0
  42. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/data_source/data_source_publisher.py +0 -0
  43. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/dataset.py +0 -0
  44. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/errors.py +0 -0
  45. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/http.py +0 -0
  46. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/mdc/__init__.py +0 -0
  47. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/mdc/context.py +0 -0
  48. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/metadata.py +0 -0
  49. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/normalizer/__init__.py +0 -0
  50. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/normalizer/normalize_utils.py +0 -0
  51. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/report/__init__.py +0 -0
  52. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/__init__.py +0 -0
  53. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/exceptions.py +0 -0
  54. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  55. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/__init__.py +0 -0
  56. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/base.py +0 -0
  57. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/random_under_sampler.py +0 -0
  58. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/sampler/utils.py +0 -0
  59. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/search_task.py +0 -0
  60. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/spinner.py +0 -0
  61. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  62. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/__init__.py +0 -0
  63. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/base_search_key_detector.py +0 -0
  64. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/blocked_time_series.py +0 -0
  65. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/config.py +0 -0
  66. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/country_utils.py +0 -0
  67. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/cpu_utils.py +0 -0
  68. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/custom_loss_utils.py +0 -0
  69. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/cv_utils.py +0 -0
  70. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/datetime_utils.py +0 -0
  71. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/deduplicate_utils.py +0 -0
  72. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/email_utils.py +0 -0
  73. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/fallback_progress_bar.py +0 -0
  74. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/feature_info.py +0 -0
  75. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/features_validator.py +0 -0
  76. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/format.py +0 -0
  77. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/hash_utils.py +0 -0
  78. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/ip_utils.py +0 -0
  79. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/mstats.py +0 -0
  80. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/one_hot_encoder.py +0 -0
  81. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/phone_utils.py +0 -0
  82. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/postal_code_utils.py +0 -0
  83. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/progress_bar.py +0 -0
  84. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/psi.py +0 -0
  85. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/pyarrow_utils.py +0 -0
  86. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sample_utils.py +0 -0
  87. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/search_key_derivations.py +0 -0
  88. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sklearn_ext.py +0 -0
  89. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/sort.py +0 -0
  90. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/target_utils.py +0 -0
  91. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/ts_utils.py +0 -0
  92. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/src/upgini/utils/warning_counter.py +0 -0
  93. {upgini-1.2.167a4220.dev1 → upgini-1.2.167a4220.dev2}/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.dev2
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.dev2"
@@ -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
@@ -318,6 +324,7 @@ class FeaturesEnricher(TransformerMixin):
318
324
  self.relevant_data_sources: pd.DataFrame = self.EMPTY_DATA_SOURCES
319
325
  self._relevant_data_sources_wo_links: pd.DataFrame = self.EMPTY_DATA_SOURCES
320
326
  self.metrics: pd.DataFrame | None = None
327
+ self.metrics_raw: pd.DataFrame | None = None
321
328
  self.metrics_metric_name: str | None = None
322
329
  self.feature_names_ = []
323
330
  self.external_source_feature_names = []
@@ -1108,8 +1115,8 @@ class FeaturesEnricher(TransformerMixin):
1108
1115
 
1109
1116
  # 1 If client features are presented - fit and predict with KFold estimator
1110
1117
  # on etalon features and calculate baseline metric
1111
- baseline_metric = None
1112
1118
  baseline_estimator = None
1119
+ baseline_cv_result = None
1113
1120
  updating_shaps = None
1114
1121
  custom_loss_add_params = get_additional_params_custom_loss(
1115
1122
  self.loss, model_task_type, logger=self.logger
@@ -1133,22 +1140,23 @@ class FeaturesEnricher(TransformerMixin):
1133
1140
  baseline_cv_result = baseline_estimator.cross_val_predict(
1134
1141
  fitting_X, y_sorted, self.baseline_score_column
1135
1142
  )
1136
- baseline_metric = baseline_cv_result.get_display_metric()
1137
- if baseline_metric is None:
1143
+ if baseline_cv_result.metric is None:
1138
1144
  self.logger.info(
1139
1145
  f"Baseline {metric} on train client features is None (maybe all features was removed)"
1140
1146
  )
1141
1147
  baseline_estimator = None
1142
1148
  else:
1143
- self.logger.info(f"Baseline {metric} on train client features: {baseline_metric}")
1149
+ self.logger.info(
1150
+ f"Baseline {metric} on train client features: {baseline_cv_result.get_display_metric()}"
1151
+ )
1144
1152
  updating_shaps = baseline_cv_result.shap_values
1145
1153
 
1146
1154
  # 2 Fit and predict with KFold estimator on enriched tds
1147
1155
  # and calculate final metric (and uplift)
1148
- enriched_metric = None
1149
1156
  uplift = None
1150
1157
  uplift_perc = None
1151
1158
  enriched_estimator = None
1159
+ enriched_cv_result = None
1152
1160
  ensemble_score_column = self._get_ensemble_score_column(fitting_X, fitting_enriched_X)
1153
1161
 
1154
1162
  def evaluate_enriched(score_column, description: str):
@@ -1167,17 +1175,16 @@ class FeaturesEnricher(TransformerMixin):
1167
1175
  cv_result = wrapper.cross_val_predict(
1168
1176
  fitting_enriched_X, enriched_y_sorted, score_column
1169
1177
  )
1170
- display_metric = cv_result.get_display_metric()
1171
- if display_metric is None:
1178
+ if cv_result.metric is None:
1172
1179
  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}")
1180
+ return None, cv_result, None, None
1181
+ self.logger.info(f"Enriched {metric} {description}: {cv_result.get_display_metric()}")
1175
1182
  sample_uplift = None
1176
1183
  sample_uplift_perc = None
1177
- if baseline_metric is not None:
1184
+ if baseline_cv_result is not None and baseline_cv_result.metric is not None:
1178
1185
  sample_uplift = (cv_result.metric - baseline_cv_result.metric) * multiplier
1179
1186
  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
1187
+ return wrapper, cv_result, sample_uplift, sample_uplift_perc
1181
1188
 
1182
1189
  if ensemble_score_column is not None:
1183
1190
  self.logger.info(
@@ -1186,14 +1193,14 @@ class FeaturesEnricher(TransformerMixin):
1186
1193
  )
1187
1194
  (
1188
1195
  enriched_estimator,
1189
- enriched_metric,
1190
- updating_shaps,
1196
+ enriched_cv_result,
1191
1197
  uplift,
1192
1198
  uplift_perc,
1193
1199
  ) = evaluate_enriched(
1194
1200
  ensemble_score_column,
1195
1201
  f"on ensemble score column {ensemble_score_column}",
1196
1202
  )
1203
+ updating_shaps = enriched_cv_result.shap_values
1197
1204
  elif set(fitting_X.columns) != set(fitting_enriched_X.columns):
1198
1205
  self.logger.info(
1199
1206
  f"Calculate enriched {metric} on train combined "
@@ -1201,11 +1208,11 @@ class FeaturesEnricher(TransformerMixin):
1201
1208
  )
1202
1209
  (
1203
1210
  enriched_estimator,
1204
- enriched_metric,
1205
- updating_shaps,
1211
+ enriched_cv_result,
1206
1212
  uplift,
1207
1213
  uplift_perc,
1208
1214
  ) = evaluate_enriched(None, "on train combined features")
1215
+ updating_shaps = enriched_cv_result.shap_values
1209
1216
 
1210
1217
  train_metrics = {
1211
1218
  self.bundle.get("quality_metrics_segment_header"): self.bundle.get(
@@ -1223,14 +1230,22 @@ class FeaturesEnricher(TransformerMixin):
1223
1230
  np.mean(y_sorted),
1224
1231
  4,
1225
1232
  )
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
- )
1233
+ put_cv_metric(
1234
+ train_metrics,
1235
+ "quality_metrics_baseline_header",
1236
+ "quality_metrics_baseline_std_header",
1237
+ baseline_cv_result,
1238
+ metric,
1239
+ self.bundle,
1240
+ )
1241
+ put_cv_metric(
1242
+ train_metrics,
1243
+ "quality_metrics_enriched_header",
1244
+ "quality_metrics_enriched_std_header",
1245
+ enriched_cv_result,
1246
+ metric,
1247
+ self.bundle,
1248
+ )
1234
1249
  if uplift is not None:
1235
1250
  train_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(uplift, 3)
1236
1251
  train_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
@@ -1262,12 +1277,13 @@ class FeaturesEnricher(TransformerMixin):
1262
1277
  etalon_eval_results = baseline_estimator.calculate_metric(
1263
1278
  eval_X_sorted, eval_y_sorted, self.baseline_score_column
1264
1279
  )
1265
- etalon_eval_metric = etalon_eval_results.get_display_metric()
1266
1280
  self.logger.info(
1267
- f"Baseline {metric} on eval set {idx + 1} client features: {etalon_eval_metric}"
1281
+ "Baseline {} on eval set {} client features: {}".format(
1282
+ metric, idx + 1, etalon_eval_results.get_display_metric()
1283
+ )
1268
1284
  )
1269
1285
  else:
1270
- etalon_eval_metric = None
1286
+ etalon_eval_results = None
1271
1287
 
1272
1288
  if enriched_estimator is not None:
1273
1289
  self.logger.info(
@@ -1277,14 +1293,20 @@ class FeaturesEnricher(TransformerMixin):
1277
1293
  enriched_eval_results = enriched_estimator.calculate_metric(
1278
1294
  enriched_eval_X_sorted, enriched_eval_y_sorted, ensemble_score_column
1279
1295
  )
1280
- enriched_eval_metric = enriched_eval_results.get_display_metric()
1281
1296
  self.logger.info(
1282
- f"Enriched {metric} on eval set {idx + 1} combined features: {enriched_eval_metric}"
1297
+ "Enriched {} on eval set {} combined features: {}".format(
1298
+ metric, idx + 1, enriched_eval_results.get_display_metric()
1299
+ )
1283
1300
  )
1284
1301
  else:
1285
- enriched_eval_metric = None
1302
+ enriched_eval_results = None
1286
1303
 
1287
- if etalon_eval_metric is not None and enriched_eval_metric is not None:
1304
+ if (
1305
+ etalon_eval_results is not None
1306
+ and etalon_eval_results.metric is not None
1307
+ and enriched_eval_results is not None
1308
+ and enriched_eval_results.metric is not None
1309
+ ):
1288
1310
  eval_uplift = (enriched_eval_results.metric - etalon_eval_results.metric) * multiplier
1289
1311
  eval_uplift_perc = eval_uplift / abs(etalon_eval_results.metric) * 100
1290
1312
  else:
@@ -1308,14 +1330,22 @@ class FeaturesEnricher(TransformerMixin):
1308
1330
  np.mean(eval_y_sorted),
1309
1331
  4,
1310
1332
  )
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
- )
1333
+ put_cv_metric(
1334
+ eval_metrics,
1335
+ "quality_metrics_baseline_header",
1336
+ "quality_metrics_baseline_std_header",
1337
+ etalon_eval_results,
1338
+ metric,
1339
+ self.bundle,
1340
+ )
1341
+ put_cv_metric(
1342
+ eval_metrics,
1343
+ "quality_metrics_enriched_header",
1344
+ "quality_metrics_enriched_std_header",
1345
+ enriched_eval_results,
1346
+ metric,
1347
+ self.bundle,
1348
+ )
1319
1349
  if eval_uplift is not None:
1320
1350
  eval_metrics[self.bundle.get("quality_metrics_uplift_header")] = round(eval_uplift, 3)
1321
1351
  eval_metrics[self.bundle.get("quality_metrics_uplift_perc_header")] = (
@@ -1352,8 +1382,9 @@ class FeaturesEnricher(TransformerMixin):
1352
1382
  elif uplift_col in metrics_df.columns and (metrics_df[uplift_col] < 0).any():
1353
1383
  self.logger.warning("Uplift is negative")
1354
1384
 
1385
+ self.metrics_raw = metrics_df
1355
1386
  self.metrics_metric_name = metric
1356
- return metrics_df
1387
+ return format_metrics_for_display(metrics_df, metric, self.bundle)
1357
1388
  except Exception as e:
1358
1389
  error_message = "Failed to calculate metrics" + (
1359
1390
  " with validation error" if isinstance(e, ValidationError) else ""
@@ -2299,8 +2330,10 @@ class FeaturesEnricher(TransformerMixin):
2299
2330
  search_duration_seconds=self.search_duration_seconds,
2300
2331
  reference_rows=self._reference_rows(),
2301
2332
  samples=self._report_sample_names(),
2302
- metrics_df=self.metrics,
2333
+ metrics_df=self.metrics_raw if self.metrics_raw is not None else self.metrics,
2303
2334
  metric_name=self.metrics_metric_name,
2335
+ model_features=self._ensemble_model_feature_count(),
2336
+ is_binary=self.model_task_type == ModelTaskType.BINARY,
2304
2337
  bundle=self.bundle,
2305
2338
  )
2306
2339
 
@@ -2325,15 +2358,20 @@ class FeaturesEnricher(TransformerMixin):
2325
2358
  return False
2326
2359
  return bool(aliases & self._column_name_aliases(list(ensemble_names), renaming))
2327
2360
 
2328
- def _ensemble_feature_names_from_metadata(self) -> set[str]:
2361
+ def _ensemble_generated_metadata(self):
2329
2362
  autofe_meta = self._search_task.get_autofe_metadata() if self._search_task else None
2330
2363
  if not autofe_meta:
2331
- return set()
2332
- names: set[str] = set()
2364
+ return []
2365
+ matched = []
2333
2366
  for meta in autofe_meta:
2334
2367
  op = EnsembleModel.from_formula(meta.formula.split("(")[0])
2335
- if op is None:
2336
- continue
2368
+ if op is not None:
2369
+ matched.append((meta, op))
2370
+ return matched
2371
+
2372
+ def _ensemble_feature_names_from_metadata(self) -> set[str]:
2373
+ names: set[str] = set()
2374
+ for meta, op in self._ensemble_generated_metadata():
2337
2375
  feature = (
2338
2376
  Feature(op, [])
2339
2377
  .set_display_index(meta.display_index)
@@ -2345,6 +2383,14 @@ class FeaturesEnricher(TransformerMixin):
2345
2383
  names.add(meta.alias)
2346
2384
  return names
2347
2385
 
2386
+ def _ensemble_model_feature_count(self) -> int | None:
2387
+ names = {
2388
+ column.original_name
2389
+ for meta, _ in self._ensemble_generated_metadata()
2390
+ for column in meta.base_columns
2391
+ }
2392
+ return len(names) or None
2393
+
2348
2394
  def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
2349
2395
  return psi is not None and not pd.isna(psi) and psi > threshold
2350
2396
 
@@ -4219,7 +4265,9 @@ if response.status_code == 200:
4219
4265
  self._fit_match_search_keys = None
4220
4266
  self.__cached_sampled_datasets = dict()
4221
4267
  self.metrics = None
4268
+ self.metrics_raw = None
4222
4269
  self.metrics_metric_name = None
4270
+ self.report_file_path = None
4223
4271
  self.fit_columns_renaming = None
4224
4272
  self.fit_dropped_features = set()
4225
4273
  self.fit_generated_features = []
@@ -6037,8 +6085,8 @@ if response.status_code == 200:
6037
6085
 
6038
6086
  def __show_report_button(self, display_id: str | None = None, display_handle=None):
6039
6087
  try:
6040
- report_path = self._write_score_report()
6041
- if report_path is not None:
6088
+ report_path = self._write_score_report() or self.report_file_path
6089
+ if report_path:
6042
6090
  if not ipython_available():
6043
6091
  print(f"Full report saved to {report_path}")
6044
6092
  return
@@ -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,9 @@ 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
+ 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),
73
84
  uplift=_as_float(row[uplift_col]) if uplift_col in metrics_df.columns else None,
74
85
  relative_uplift=_as_str(row[uplift_pct_col]) if uplift_pct_col in metrics_df.columns else None,
75
86
  )
@@ -77,9 +88,47 @@ def _quality_samples(
77
88
  return samples
78
89
 
79
90
 
91
+ def _sample_stats(
92
+ sample_names: list[str], metrics_df: Optional[pd.DataFrame], bundle: ResourceBundle
93
+ ) -> list[SampleStats]:
94
+ segment_col = bundle.get("quality_metrics_segment_header")
95
+ rows_col = bundle.get("quality_metrics_rows_header")
96
+ mean_col = bundle.get("quality_metrics_mean_target_header")
97
+ by_name: dict[str, SampleStats] = {}
98
+ if metrics_df is not None and not metrics_df.empty and segment_col in metrics_df.columns:
99
+ for _, row in metrics_df.iterrows():
100
+ name = _as_str(row[segment_col])
101
+ if not name:
102
+ continue
103
+ by_name[name] = SampleStats(
104
+ sample=name,
105
+ rows=_as_int(row[rows_col]) if rows_col in metrics_df.columns else None,
106
+ mean_target=_as_float(row[mean_col]) if mean_col in metrics_df.columns else None,
107
+ )
108
+ names = sample_names or list(by_name)
109
+ stats = [by_name.get(name) or SampleStats(sample=name) for name in names]
110
+ seen = set(names)
111
+ stats.extend(sample for name, sample in by_name.items() if name not in seen)
112
+ return stats
113
+
114
+
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
+
80
125
  def _as_str(value) -> Optional[str]:
81
126
  return None if pd.isna(value) else str(value)
82
127
 
83
128
 
84
129
  def _as_float(value) -> Optional[float]:
85
130
  return None if pd.isna(value) else float(value)
131
+
132
+
133
+ def _as_int(value) -> Optional[int]:
134
+ return None if pd.isna(value) else int(value)
@@ -13,18 +13,31 @@ 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
+ enriched: Optional[float] = None
25
+ std: Optional[float] = None
24
26
  uplift: Optional[float] = None
25
27
  relative_uplift: Optional[str] = None
26
28
 
27
29
 
30
+ @dataclass
31
+ class SampleStats:
32
+ sample: str
33
+ rows: Optional[int] = None
34
+ mean_target: Optional[float] = None
35
+ date_range: Optional[str] = None
36
+ labeled: Optional[int] = None
37
+ unlabeled: Optional[int] = None
38
+ positive: Optional[int] = None
39
+
40
+
28
41
  @dataclass
29
42
  class FeatureRow:
30
43
  name: str
@@ -66,6 +79,8 @@ class ReportData:
66
79
  metadata: ReportMetadata
67
80
  quality_by_sample: list[QualitySample] = field(default_factory=list)
68
81
  summary: SearchResultsSummary = field(default_factory=SearchResultsSummary)
82
+ sample_stats: list[SampleStats] = field(default_factory=list)
83
+ is_binary: bool = False
69
84
  features: list[FeatureRow] = field(default_factory=list)
70
85
  sources: list[SourceRow] = field(default_factory=list)
71
86
  autofe: list[dict[str, str]] = field(default_factory=list)