upgini 1.2.167a4220.dev6__tar.gz → 1.2.167a4220.dev8__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.dev6 → upgini-1.2.167a4220.dev8}/PKG-INFO +1 -1
  2. upgini-1.2.167a4220.dev8/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/features_enricher.py +75 -53
  4. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/assemble.py +49 -32
  5. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/html.py +14 -0
  6. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/template.html +4 -4
  7. upgini-1.2.167a4220.dev6/src/upgini/__about__.py +0 -1
  8. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/.gitignore +0 -0
  9. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/LICENSE +0 -0
  10. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/README.md +0 -0
  11. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/pyproject.toml +0 -0
  12. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/__init__.py +0 -0
  13. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/ads.py +0 -0
  14. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/ads_management/__init__.py +0 -0
  15. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/ads_management/ads_manager.py +0 -0
  16. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/__init__.py +0 -0
  17. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/all_operators.py +0 -0
  18. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/binary.py +0 -0
  19. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/date.py +0 -0
  20. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/feature.py +0 -0
  21. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/groupby.py +0 -0
  22. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/operand.py +0 -0
  23. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/operator.py +0 -0
  24. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/__init__.py +0 -0
  25. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/base.py +0 -0
  26. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/cross.py +0 -0
  27. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/delta.py +0 -0
  28. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/lag.py +0 -0
  29. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  30. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/roll.py +0 -0
  31. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/trend.py +0 -0
  32. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/timeseries/volatility.py +0 -0
  33. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/unary.py +0 -0
  34. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/utils.py +0 -0
  35. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/autofe/vector.py +0 -0
  36. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/data_source/__init__.py +0 -0
  37. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/data_source/data_source_publisher.py +0 -0
  38. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/dataset.py +0 -0
  39. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/errors.py +0 -0
  40. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/http.py +0 -0
  41. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/mdc/__init__.py +0 -0
  42. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/mdc/context.py +0 -0
  43. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/metadata.py +0 -0
  44. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/metrics.py +0 -0
  45. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/normalizer/__init__.py +0 -0
  46. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/normalizer/normalize_utils.py +0 -0
  47. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/__init__.py +0 -0
  48. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/data.py +0 -0
  49. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/report/stats.py +0 -0
  50. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/resource_bundle/__init__.py +0 -0
  51. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/resource_bundle/exceptions.py +0 -0
  52. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/resource_bundle/strings.properties +0 -0
  53. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  54. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/sampler/__init__.py +0 -0
  55. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/sampler/base.py +0 -0
  56. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/sampler/random_under_sampler.py +0 -0
  57. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/sampler/utils.py +0 -0
  58. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/search_task.py +0 -0
  59. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/spinner.py +0 -0
  60. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  61. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/__init__.py +0 -0
  62. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/base_search_key_detector.py +0 -0
  63. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/blocked_time_series.py +0 -0
  64. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/config.py +0 -0
  65. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/country_utils.py +0 -0
  66. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/cpu_utils.py +0 -0
  67. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/custom_loss_utils.py +0 -0
  68. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/cv_utils.py +0 -0
  69. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/datetime_utils.py +0 -0
  70. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/deduplicate_utils.py +0 -0
  71. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/display_utils.py +0 -0
  72. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/email_utils.py +0 -0
  73. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/fallback_progress_bar.py +0 -0
  74. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/feature_info.py +0 -0
  75. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/features_validator.py +0 -0
  76. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/format.py +0 -0
  77. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/hash_utils.py +0 -0
  78. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/ip_utils.py +0 -0
  79. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/mstats.py +0 -0
  80. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/one_hot_encoder.py +0 -0
  81. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/phone_utils.py +0 -0
  82. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/postal_code_utils.py +0 -0
  83. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/progress_bar.py +0 -0
  84. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/psi.py +0 -0
  85. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/pyarrow_utils.py +0 -0
  86. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/sample_utils.py +0 -0
  87. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/search_key_derivations.py +0 -0
  88. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/sklearn_ext.py +0 -0
  89. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/sort.py +0 -0
  90. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/target_utils.py +0 -0
  91. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/track_info.py +0 -0
  92. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/ts_utils.py +0 -0
  93. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/src/upgini/utils/warning_counter.py +0 -0
  94. {upgini-1.2.167a4220.dev6 → upgini-1.2.167a4220.dev8}/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.dev6
3
+ Version: 1.2.167a4220.dev8
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.dev8"
@@ -98,7 +98,7 @@ from upgini.utils.deduplicate_utils import (
98
98
  clean_full_duplicates,
99
99
  remove_fintech_duplicates,
100
100
  )
101
- from upgini.report.assemble import assemble_report_data, autofe_rows_from_description, build_search_results
101
+ from upgini.report.assemble import assemble_report_data, autofe_rows_for_report, build_search_results
102
102
  from upgini.report.html import generate_html_report
103
103
  from upgini.report.stats import make_report_frame
104
104
  from upgini.utils.display_utils import (
@@ -2288,22 +2288,32 @@ class FeaturesEnricher(TransformerMixin):
2288
2288
  return self._get_single_ensemble_score_name() is not None
2289
2289
 
2290
2290
  def _get_single_ensemble_score_name(self) -> str | None:
2291
+ if len(self._ensemble_generated_metadata()) != 1:
2292
+ return None
2291
2293
  selected = list(self.feature_names_ or [])
2292
2294
  ensemble = [name for name in selected if self._is_ensemble_feature(name)]
2293
2295
  if len(ensemble) != 1:
2294
2296
  return None
2295
- etalon_columns = set()
2296
- if self._search_task is not None:
2297
- etalon_columns = {
2298
- meta.name
2299
- for meta in (self._search_task.get_all_features_metadata_v2() or [])
2300
- if meta.source == "etalon"
2301
- }
2302
- extras = [name for name in selected if name != ensemble[0] and name not in etalon_columns]
2303
- if extras:
2297
+ if any(self._is_extra_enriched_feature(name) for name in selected):
2304
2298
  return None
2305
2299
  return ensemble[0]
2306
2300
 
2301
+ def _is_extra_enriched_feature(self, name: str) -> bool:
2302
+ if self._is_ensemble_feature(name):
2303
+ return False
2304
+ renaming = self.fit_columns_renaming or {}
2305
+ aliases = self._column_name_aliases([name], renaming)
2306
+ if aliases & set(self.external_source_feature_names or []):
2307
+ return True
2308
+ if self._search_task is None:
2309
+ return False
2310
+ for meta in self._search_task.get_all_features_metadata_v2() or []:
2311
+ if meta.source == "etalon" or self._is_ensemble_feature(meta.name):
2312
+ continue
2313
+ if aliases & self._column_name_aliases([meta.name], renaming):
2314
+ return True
2315
+ return False
2316
+
2307
2317
  def _reference_rows(self) -> int | None:
2308
2318
  if self.X is None:
2309
2319
  return None
@@ -2464,12 +2474,18 @@ class FeaturesEnricher(TransformerMixin):
2464
2474
  if self._search_task is not None:
2465
2475
  features_meta = list(self._search_task.get_all_features_metadata_v2() or [])
2466
2476
  features, sources, model_shap, summary = build_search_results(
2467
- model_names=self._ensemble_model_feature_names(),
2468
2477
  features_meta=features_meta,
2469
- base_columns=self._ensemble_base_columns(),
2470
2478
  is_ensemble=self._is_ensemble_feature,
2479
+ generated_names=self._column_name_aliases(
2480
+ self.fit_generated_features or [], self.fit_columns_renaming or {}
2481
+ ),
2482
+ )
2483
+ autofe = autofe_rows_for_report(
2484
+ self.get_autofe_features_description(features_meta=features_meta),
2485
+ features_meta,
2486
+ self._is_ensemble_feature,
2487
+ self.bundle,
2471
2488
  )
2472
- autofe = autofe_rows_from_description(self.get_autofe_features_description(), self.bundle)
2473
2489
  return features, sources, model_shap, summary, autofe
2474
2490
 
2475
2491
  def _score_report_html(self) -> str | None:
@@ -2512,25 +2528,6 @@ class FeaturesEnricher(TransformerMixin):
2512
2528
  names.add(meta.alias)
2513
2529
  return names
2514
2530
 
2515
- def _ensemble_base_columns(self) -> list[tuple[str, str]]:
2516
- return [
2517
- (column.original_name, column.hashed_name)
2518
- for meta, _ in self._ensemble_generated_metadata()
2519
- for column in meta.base_columns
2520
- ]
2521
-
2522
- def _ensemble_model_feature_names(self) -> list[str]:
2523
- return sorted(
2524
- {
2525
- original
2526
- for original, hashed in self._ensemble_base_columns()
2527
- if not self._is_ensemble_feature(original) and not self._is_ensemble_feature(hashed)
2528
- }
2529
- )
2530
-
2531
- def _ensemble_model_feature_count(self) -> int | None:
2532
- return len(self._ensemble_model_feature_names()) or None
2533
-
2534
2531
  def _psi_exceeds_threshold(self, psi: float | None, threshold: float) -> bool:
2535
2532
  return psi is not None and not pd.isna(psi) and psi > threshold
2536
2533
 
@@ -5942,12 +5939,12 @@ if response.status_code == 200:
5942
5939
  else:
5943
5940
  self.logger.warning("Empty features info")
5944
5941
 
5945
- def get_autofe_features_description(self):
5942
+ def get_autofe_features_description(self, features_meta: list[FeaturesMetadataV2] | None = None):
5946
5943
  try:
5947
5944
  autofe_meta = self._search_task.get_autofe_metadata()
5948
5945
  if autofe_meta is None:
5949
5946
  return None
5950
- if len(self._internal_features_info) != 0:
5947
+ if features_meta is None and len(self._internal_features_info) != 0:
5951
5948
 
5952
5949
  def to_feature_meta(row):
5953
5950
  fm = FeaturesMetadataV2(
@@ -5961,13 +5958,16 @@ if response.status_code == 200:
5961
5958
  return fm
5962
5959
 
5963
5960
  features_meta = self._internal_features_info.apply(to_feature_meta, axis=1).to_list()
5964
- else:
5961
+ elif features_meta is None:
5965
5962
  features_meta = self._search_task.get_all_features_metadata_v2()
5966
5963
 
5967
5964
  def get_feature_by_name(name: str):
5968
- for m in features_meta:
5969
- if m.name == name:
5970
- return m
5965
+ if not name or not features_meta:
5966
+ return None
5967
+ for candidate in features_meta:
5968
+ if candidate.name == name:
5969
+ return candidate
5970
+ return None
5971
5971
 
5972
5972
  descriptions = []
5973
5973
  for m in autofe_meta:
@@ -5988,11 +5988,28 @@ if response.status_code == 200:
5988
5988
 
5989
5989
  description = {}
5990
5990
 
5991
- feature_meta = get_feature_by_name(autofe_feature.get_display_name(shorten=True, unhash=True))
5992
- if feature_meta is None:
5991
+ lookup_names = [autofe_feature.get_display_name(shorten=True, unhash=True)]
5992
+ if m.alias:
5993
+ lookup_names.extend([m.alias, f"f_autofe_{m.alias}"])
5994
+ feature_meta = None
5995
+ for lookup_name in lookup_names:
5996
+ feature_meta = get_feature_by_name(lookup_name)
5997
+ if feature_meta is not None:
5998
+ break
5999
+ if feature_meta is None and m.alias and features_meta:
6000
+ prefix = f"f_autofe_{m.alias}"
6001
+ matches = [
6002
+ candidate
6003
+ for candidate in features_meta
6004
+ if candidate.name == prefix or candidate.name.startswith(prefix + "_")
6005
+ ]
6006
+ if len(matches) == 1:
6007
+ feature_meta = matches[0]
6008
+ if feature_meta is None or not feature_meta.shap_value:
5993
6009
  continue
5994
6010
  description["shap"] = feature_meta.shap_value
5995
- description[self.bundle.get("autofe_descriptions_sources")] = feature_meta.data_source.replace(
6011
+ source = feature_meta.data_source or ""
6012
+ description[self.bundle.get("autofe_descriptions_sources")] = source.replace(
5996
6013
  "AutoFE: features from ", ""
5997
6014
  ).replace("AutoFE: feature from ", "")
5998
6015
  description[self.bundle.get("autofe_descriptions_feature_name")] = feature_meta.name
@@ -6224,19 +6241,24 @@ if response.status_code == 200:
6224
6241
  print(self._internal_features_info)
6225
6242
 
6226
6243
  def __show_report_button(self, display_id: str | None = None, display_handle=None):
6244
+ if self._has_single_ensemble_score():
6245
+ try:
6246
+ report_html = self._score_report_html()
6247
+ if report_html:
6248
+ if not ipython_available():
6249
+ return
6250
+ search_id = (
6251
+ self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
6252
+ )
6253
+ return show_button_open_report(
6254
+ report_html,
6255
+ download_name=f"upgini-report-{search_id}.html",
6256
+ display_id=display_id,
6257
+ display_handle=display_handle,
6258
+ )
6259
+ except Exception:
6260
+ self.logger.exception("Failed to generate HTML report, falling back to PDF")
6227
6261
  try:
6228
- report_html = self._score_report_html() or self.report_html
6229
- if report_html:
6230
- if not ipython_available():
6231
- return
6232
- search_id = self._search_task.search_task_id if self._search_task is not None else (self.search_id or "")
6233
- return show_button_open_report(
6234
- report_html,
6235
- download_name=f"upgini-report-{search_id}.html",
6236
- display_id=display_id,
6237
- display_handle=display_handle,
6238
- )
6239
-
6240
6262
  eval_sets_drift_df = self._get_eval_sets_drift_summary()
6241
6263
 
6242
6264
  return prepare_and_show_report(
@@ -98,13 +98,20 @@ def assemble_report_data(
98
98
 
99
99
  def build_search_results(
100
100
  *,
101
- model_names: list[str],
102
101
  features_meta: list[FeaturesMetadataV2],
103
- base_columns: list[tuple[str, str]],
104
102
  is_ensemble: Callable[[str], bool],
103
+ generated_names: Optional[set[str]] = None,
105
104
  ) -> tuple[list[FeatureRow], list[SourceRow], list[ModelFeatureShap], SearchResultsSummary]:
106
- meta_by_name = _features_meta_index(features_meta, base_columns)
107
- model_rows = [_feature_row(name, meta_by_name.get(name)) for name in model_names]
105
+ generated_names = generated_names or set()
106
+ model_rows: list[FeatureRow] = []
107
+ found_rows: list[FeatureRow] = []
108
+ for meta in features_meta:
109
+ if is_ensemble(meta.name) or not _has_nonzero_shap_value(meta.shap_value):
110
+ continue
111
+ row = _feature_row(meta.name, meta, generated_names)
112
+ model_rows.append(row)
113
+ if meta.source != "etalon":
114
+ found_rows.append(row)
108
115
  model_rows.sort(key=lambda row: (row.shap is None, -abs(row.shap or 0.0), row.name))
109
116
  model_shap = [
110
117
  ModelFeatureShap(
@@ -115,19 +122,12 @@ def build_search_results(
115
122
  )
116
123
  for index, row in enumerate(model_rows, start=1)
117
124
  ]
118
- join_rows = [
119
- _feature_row(meta.name, meta)
120
- for meta in features_meta
121
- if meta.source != "etalon" and not is_ensemble(meta.name)
122
- ]
123
- sources = _source_rows(join_rows)
124
- relevant = sum(1 for row in join_rows if _has_nonzero_shap(row))
125
- contributed = len({(row.provider, row.source) for row in join_rows if row.provider and _has_nonzero_shap(row)})
126
- model_count = len(model_names) or None
125
+ sources = _source_rows(found_rows)
126
+ contributed = len({(row.provider, row.source) for row in found_rows if row.provider})
127
127
  stable = sum(1 for row in model_rows if row.psi is not None and row.psi < _STABILITY_PSI)
128
128
  summary = SearchResultsSummary(
129
- relevant_features=relevant,
130
- model_features=model_count,
129
+ relevant_features=len(found_rows),
130
+ model_features=len(model_rows) or None,
131
131
  data_sources=len(sources),
132
132
  stable_features_share=(stable / len(model_rows)) if model_rows else None,
133
133
  contributed_sources=contributed,
@@ -136,6 +136,31 @@ def build_search_results(
136
136
  return model_rows, sources, model_shap, summary
137
137
 
138
138
 
139
+ def autofe_rows_for_report(
140
+ df: Optional[pd.DataFrame],
141
+ features_meta: list[FeaturesMetadataV2],
142
+ is_ensemble: Callable[[str], bool],
143
+ bundle: ResourceBundle,
144
+ ) -> list[dict[str, str]]:
145
+ rows = autofe_rows_from_description(df, bundle)
146
+ listed = {row["generatedFeature"] for row in rows if row.get("generatedFeature")}
147
+ for meta in features_meta:
148
+ if is_ensemble(meta.name) or meta.source != "generated":
149
+ continue
150
+ if not _has_nonzero_shap_value(meta.shap_value) or meta.name in listed:
151
+ continue
152
+ row = _feature_row(meta.name, meta)
153
+ rows.append(
154
+ {
155
+ "sources": row.source,
156
+ "generatedFeature": row.name,
157
+ "sourceFeatures": "",
158
+ "functions": "",
159
+ }
160
+ )
161
+ return rows
162
+
163
+
139
164
  def autofe_rows_from_description(df: Optional[pd.DataFrame], bundle: ResourceBundle) -> list[dict[str, str]]:
140
165
  if df is None or df.empty:
141
166
  return []
@@ -234,23 +259,15 @@ def _as_int(value) -> Optional[int]:
234
259
  return None if pd.isna(value) else int(value)
235
260
 
236
261
 
237
- def _features_meta_index(
238
- features_meta: list[FeaturesMetadataV2], base_columns: list[tuple[str, str]]
239
- ) -> dict[str, FeaturesMetadataV2]:
240
- index = {meta.name: meta for meta in features_meta}
241
- for original, hashed in base_columns:
242
- matched = index.get(hashed) or index.get(original)
243
- if matched is None:
244
- continue
245
- index[original] = matched
246
- index[hashed] = matched
247
- return index
248
-
249
-
250
- def _feature_row(name: str, meta: Optional[FeaturesMetadataV2]) -> FeatureRow:
262
+ def _feature_row(
263
+ name: str, meta: Optional[FeaturesMetadataV2], generated_names: Optional[set[str]] = None
264
+ ) -> FeatureRow:
251
265
  if meta is None:
252
266
  return FeatureRow(name=name)
253
- info = FeatureInfo.from_metadata(meta, None, meta.source == "etalon", False)
267
+ generated_names = generated_names or set()
268
+ is_generated = name in generated_names or meta.source == "generated"
269
+ is_client = meta.source == "etalon" and not is_generated
270
+ info = FeatureInfo.from_metadata(meta, None, is_client, is_generated)
254
271
  return FeatureRow(
255
272
  name=name,
256
273
  shap=meta.shap_value,
@@ -292,5 +309,5 @@ def _psi_status(psi: Optional[float]) -> Optional[str]:
292
309
  return "good"
293
310
 
294
311
 
295
- def _has_nonzero_shap(row: FeatureRow) -> bool:
296
- return row.shap is not None and row.shap != 0
312
+ def _has_nonzero_shap_value(shap: Optional[float]) -> bool:
313
+ return shap is not None and not pd.isna(shap) and shap != 0
@@ -6,6 +6,7 @@ from typing import Optional
6
6
 
7
7
  from upgini.report.data import FeatureRow, ReportCharts, ReportData, SampleStats, SearchResultsSummary, SourceRow
8
8
  from upgini.report.stats import MEAN_AXIS_PADDING
9
+ from upgini.utils.feature_info import CLIENT_SOURCE, GENERATED_SOURCE
9
10
 
10
11
  _TEMPLATE_PATH = Path(__file__).with_name("template.html")
11
12
  _SAMPLE_COLORS = ("#1645ee", "#20aab4", "#53cc61", "#e19132", "#d45b5b")
@@ -160,6 +161,7 @@ def _features_payload(features: list[FeatureRow]) -> list[dict]:
160
161
  "name": row.name,
161
162
  "provider": row.provider,
162
163
  "source": row.source,
164
+ "shapClass": _shap_fill_class(row),
163
165
  "importance": abs(row.shap) if row.shap is not None else 0,
164
166
  "shap": row.shap,
165
167
  "coverage": row.coverage,
@@ -171,6 +173,18 @@ def _features_payload(features: list[FeatureRow]) -> list[dict]:
171
173
  ]
172
174
 
173
175
 
176
+ def _shap_fill_class(row: FeatureRow) -> str:
177
+ source = (row.source or "").lower()
178
+ provider = (row.provider or "").lower()
179
+ if source.startswith("autofe") or provider == "autofe" or source == GENERATED_SOURCE.lower():
180
+ return "autofe"
181
+ if source == CLIENT_SOURCE.lower() or provider == "user":
182
+ return "user"
183
+ if provider in {"", "upgini"}:
184
+ return "upgini"
185
+ return "external"
186
+
187
+
174
188
  def _sources_payload(sources: list[SourceRow]) -> list[dict]:
175
189
  return [
176
190
  {
@@ -7,7 +7,7 @@
7
7
  <style>
8
8
  :root{--ink:#15211f;--muted:#6f7976;--line:#dce4e1;--paper:#fff;--bg:#f3f6f5;--blue:#1645ee;--green:#53cc61;--cyan:#20aab4;--warn:#e19132;--danger:#d45b5b;--shadow:0 18px 60px rgba(18,45,39,.08)}
9
9
  *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.45}.shell{max-width:1440px;margin:auto;min-height:100vh}.topbar{height:72px;padding:0 42px;display:flex;align-items:center;gap:25px;background:rgba(255,255,255,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}.brand{display:flex;align-items:center;gap:14px}.wordmark{font-size:25px;font-weight:800;letter-spacing:-.07em;line-height:1;background:linear-gradient(135deg,#0138f8,#5bd357);-webkit-background-clip:text;background-clip:text;color:transparent}.wordmark b{font-weight:850}.wordmark span{font-weight:650}.wordmark.small{font-size:20px;min-width:66px}.brand-divider{width:1px;height:25px;background:var(--line)}.brand-label{font-weight:700;color:#45514f}.top-meta{display:flex;gap:8px;margin-left:auto;align-items:center}.chip{background:#eef2f1;border:1px solid #e0e7e5;border-radius:999px;padding:7px 11px;color:#53605d;font-size:12px}.button{border:0;border-radius:10px;padding:10px 15px;font:inherit;font-weight:700;cursor:pointer;transition:.18s ease}.button:hover{transform:translateY(-1px)}.button.primary{background:var(--ink);color:white}.button.ghost{background:white;border:1px solid var(--line);color:var(--ink)}.layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:0}.sidenav{position:sticky;top:72px;height:calc(100vh - 72px);padding:30px 22px;border-right:1px solid var(--line);background:#f7f9f8}.nav-title{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:#98a29f;margin:0 10px 12px}.nav-link{display:flex;align-items:center;gap:10px;color:#66716f;text-decoration:none;padding:10px;border-radius:9px;margin:2px 0;font-weight:600}.nav-link:hover,.nav-link.active{background:white;color:var(--ink);box-shadow:0 4px 18px rgba(25,49,43,.06)}.nav-dot{width:7px;height:7px;border-radius:50%;background:#c4cecb}.nav-link.active .nav-dot{background:linear-gradient(135deg,var(--blue),var(--green))}.run-note{position:absolute;bottom:24px;left:22px;right:22px;padding:14px;background:white;border:1px solid var(--line);border-radius:12px;font-size:11px;color:var(--muted)}.run-note strong{display:block;color:var(--ink);font-size:12px;margin-bottom:4px}.main{min-width:0;padding:38px 44px 80px}.eyebrow{display:flex;align-items:center;gap:8px;text-transform:uppercase;letter-spacing:.12em;color:#74807d;font-size:11px;font-weight:800}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px #e7f8e9}.hero-title{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;margin:12px 0 28px}.hero-title h1{font-size:36px;line-height:1.08;letter-spacing:-.04em;margin:0;max-width:720px}.hero-title p{color:var(--muted);margin:10px 0 0;max-width:690px}.run-stamp{text-align:right;min-width:190px;color:var(--muted);font-size:12px}.run-stamp strong{display:block;color:var(--ink);font-size:14px;margin-bottom:3px}.metric-hero{position:relative;overflow:hidden;background:#172421;color:white;border-radius:22px;padding:28px 30px;box-shadow:var(--shadow);margin-bottom:18px}.metric-hero:after{content:"";position:absolute;width:400px;height:400px;right:-150px;top:-250px;border-radius:50%;background:radial-gradient(circle,rgba(71,215,117,.24),transparent 68%)}.metric-head{position:relative;z-index:1;display:flex;justify-content:space-between;gap:20px}.metric-tabs{display:flex;gap:5px;background:rgba(255,255,255,.08);border-radius:11px;padding:4px}.metric-tab{border:0;background:transparent;color:#aebbb7;padding:8px 12px;border-radius:8px;font-weight:700;cursor:pointer}.metric-tab.active{background:white;color:#13201e}.scores{position:relative;z-index:1;display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:25px;margin-top:32px;align-items:end}.score-label{text-transform:uppercase;letter-spacing:.11em;color:#8fa09b;font-size:10px;font-weight:800}.score-value{font-size:39px;line-height:1;margin-top:8px;font-variant-numeric:tabular-nums;letter-spacing:-.04em}.uplift .score-value{color:#73e57b}.uplift p{color:#aebcb8;margin:8px 0 0;font-size:12px}.summary-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0 34px}.summary{background:white;border:1px solid var(--line);border-radius:15px;padding:17px 18px}.summary .k{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.09em;font-weight:800}.summary .v{font-size:23px;font-weight:750;margin-top:5px;letter-spacing:-.02em}.summary .d{font-size:11px;color:#88928f;margin-top:4px}.section{scroll-margin-top:94px;margin-top:36px}.section-head{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:15px}.section-head h2{font-size:22px;letter-spacing:-.025em;margin:0}.section-head p{color:var(--muted);margin:5px 0 0;font-size:12px}.panel{background:white;border:1px solid var(--line);border-radius:17px;box-shadow:0 4px 20px rgba(18,45,39,.035)}.chart-panel{padding:20px}.chart-head{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:10px}.legend{display:flex;gap:17px;flex-wrap:wrap;font-size:11px;color:var(--muted)}.legend i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px}.control{border:1px solid var(--line);background:#fafcfb;color:var(--ink);border-radius:9px;padding:8px 10px;font:inherit;font-size:12px}.canvas-wrap{height:280px;position:relative}canvas{width:100%;height:100%}.callout{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;background:#fff8ed;border:1px solid #f1dcc0;border-radius:13px;color:#6b4b20}.callout strong{display:block;color:#4c361a;margin-bottom:2px}.callout-icon{width:27px;height:27px;display:grid;place-items:center;background:#f4c77f;border-radius:8px;font-weight:900;color:#65410c;flex:none}.stability-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,.5fr);gap:14px}.heatmap{overflow:auto;padding:16px}.heat-table{border-collapse:separate;border-spacing:5px;width:100%;font-size:11px}.heat-table th{color:var(--muted);font-weight:600;padding:2px 6px;white-space:nowrap}.heat-table th:first-child{text-align:left}.heat-table td:first-child{font-weight:700;padding-right:12px;white-space:nowrap}.heat{height:30px;min-width:47px;text-align:center;border-radius:6px;font-weight:750}.s1{background:#dbf4de;color:#276b2f}.s2{background:#e9f4cf;color:#526826}.s3{background:#fff0c7;color:#7f641b}.s4{background:#f8d5c8;color:#914634}.stability-side{padding:20px}.donut{width:140px;height:140px;border-radius:50%;margin:8px auto 17px;background:conic-gradient(var(--green) 0 72%,#e6b556 72% 91%,#df7474 91%);position:relative}.donut:after{content:"";position:absolute;inset:25px;background:white;border-radius:50%}.donut-label{position:absolute;inset:0;z-index:1;display:grid;place-items:center;text-align:center;font-weight:800;font-size:21px}.donut-label small{display:block;color:var(--muted);font-size:10px;font-weight:600}.stat-list{display:grid;gap:8px}.stat-row{display:flex;justify-content:space-between;padding-top:8px;border-top:1px solid #edf1ef;color:var(--muted);font-size:12px}.stat-row b{color:var(--ink)}.toolbar{display:flex;gap:9px;flex-wrap:wrap}.search{min-width:230px;border:1px solid var(--line);background:white;border-radius:9px;padding:9px 11px;font:inherit}.table-wrap{overflow:auto}.data-table{width:100%;border-collapse:collapse;min-width:860px}.data-table th{text-align:left;padding:12px 14px;background:#f8faf9;color:#77827f;text-transform:uppercase;letter-spacing:.07em;font-size:10px;border-bottom:1px solid var(--line)}.data-table td{padding:13px 14px;border-bottom:1px solid #edf1ef;font-size:12px}.data-table tr:last-child td{border-bottom:0}.data-table tbody tr:hover{background:#f8fbfa}.feature-name{font-weight:750;color:#25312f}.source-name{color:#74807d}.bar{height:6px;width:90px;background:#edf1ef;border-radius:99px;overflow:hidden}.bar i{height:100%;display:block;background:linear-gradient(90deg,var(--blue),#43c878);border-radius:99px}.tag{display:inline-flex;align-items:center;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:750}.tag.good{background:#e8f7ea;color:#337139}.tag.watch{background:#fff1d9;color:#855d18}.tag.risk{background:#fde7e4;color:#984a40}.source-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.source-card{padding:19px}.source-top{display:flex;justify-content:space-between;gap:12px}.source-icon{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;background:linear-gradient(135deg,#e8eeff,#e8f9e8);font-weight:850;color:var(--blue)}.source-card h3{font-size:14px;margin:12px 0 4px}.source-card p{font-size:11px;color:var(--muted);margin:0}.source-metrics{display:flex;gap:18px;margin-top:17px;padding-top:13px;border-top:1px solid #edf1ef}.source-metrics b{display:block;font-size:16px}.source-metrics span{color:var(--muted);font-size:10px}.notebook{padding:0;overflow:hidden}.cell-head{background:#f8faf9;border-bottom:1px solid var(--line);padding:10px 14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#68736f;font-size:11px}.cell-output{padding:18px;display:flex;align-items:center;gap:25px;flex-wrap:wrap}.cell-quality{display:flex;gap:24px}.cell-quality span{font-size:11px;color:var(--muted)}.cell-quality b{font-size:17px;color:var(--ink);display:block}.footer{text-align:center;color:#8a9592;font-size:11px;padding:40px 0 10px}.empty{display:none;padding:24px;text-align:center;color:var(--muted)}
10
- .sample-table{min-width:700px}.toggle-legend{margin-bottom:10px}.series-toggle{border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:11px;padding:5px 7px;border-radius:6px}.series-toggle:not(.active){opacity:.35;text-decoration:line-through}.threshold-legend{display:flex;gap:8px;justify-content:flex-end;margin-bottom:10px}.shap-panel{padding:16px}.shap-row{display:grid;grid-template-columns:minmax(170px,1fr) 2fr 60px;gap:12px;align-items:center;padding:9px 4px;border-bottom:1px solid #edf1ef}.shap-row:last-child{border:0}.provider{font-size:9px;margin-left:6px;padding:3px 6px;border-radius:99px;border:1px solid var(--line);color:var(--muted)}.shap-track{height:10px;background:#edf1ef;border-radius:99px;overflow:hidden}.shap-fill{display:block;height:100%;border-radius:99px}.shap-fill.upgini{background:#1645ee}.shap-fill.user{background:#20aab4}.shap-fill.autofe{background:#53cc61}.result-tabs{display:flex;gap:4px;margin-bottom:12px}.result-tab{border:1px solid var(--line);background:#eef2f1;padding:9px 13px;border-radius:9px;font:inherit;cursor:pointer;color:var(--muted)}.result-tab.active{background:var(--ink);color:#fff}.result-tab b{font-size:10px;margin-left:5px}.result-pane{display:none}.result-pane.active{display:block}.table-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-bottom:10px}.chart-tooltip{position:absolute;display:none;pointer-events:none;background:#172421;color:white;padding:9px 11px;border-radius:8px;font-size:10px;box-shadow:var(--shadow);white-space:nowrap}.sidenav{overflow:auto}.run-note{position:static;margin-top:22px}.nav-link{font-size:12px;padding:8px 10px}
10
+ .sample-table{min-width:700px}.toggle-legend{margin-bottom:10px}.series-toggle{border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:11px;padding:5px 7px;border-radius:6px}.series-toggle:not(.active){opacity:.35;text-decoration:line-through}.threshold-legend{display:flex;gap:8px;justify-content:flex-end;margin-bottom:10px}.shap-panel{padding:16px}.shap-row{display:grid;grid-template-columns:minmax(170px,1fr) 2fr 60px;gap:12px;align-items:center;padding:9px 4px;border-bottom:1px solid #edf1ef}.shap-row:last-child{border:0}.provider{font-size:9px;margin-left:6px;padding:3px 6px;border-radius:99px;border:1px solid var(--line);color:var(--muted)}.shap-track{height:10px;background:#edf1ef;border-radius:99px;overflow:hidden}.shap-fill{display:block;height:100%;border-radius:99px}.shap-fill.upgini{background:#1645ee}.shap-fill.user{background:#20aab4}.shap-fill.autofe{background:#53cc61}.shap-fill.external{background:#7b5cff}.result-tabs{display:flex;gap:4px;margin-bottom:12px}.result-tab{border:1px solid var(--line);background:#eef2f1;padding:9px 13px;border-radius:9px;font:inherit;cursor:pointer;color:var(--muted)}.result-tab.active{background:var(--ink);color:#fff}.result-tab b{font-size:10px;margin-left:5px}.result-pane{display:none}.result-pane.active{display:block}.table-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-bottom:10px}.chart-tooltip{position:absolute;display:none;pointer-events:none;background:#172421;color:white;padding:9px 11px;border-radius:8px;font-size:10px;box-shadow:var(--shadow);white-space:nowrap}.sidenav{overflow:auto}.run-note{position:static;margin-top:22px}.nav-link{font-size:12px;padding:8px 10px}
11
11
  .metric-sample-table{position:relative;z-index:1;margin-top:26px}.metric-sample-head,.metric-sample-row{display:grid;grid-template-columns:minmax(140px,.9fr) 1fr 1fr .8fr .8fr;gap:18px;align-items:center}.metric-sample-ci{display:block;margin-top:6px;color:#8fa09b;font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.metric-note{color:#8fa09b;font-size:11px;margin-top:6px}.partner-logo{height:30px;width:auto;display:block}.metric-sample-head{padding:0 14px 8px;color:#8fa09b;text-transform:uppercase;letter-spacing:.11em;font-size:10px;font-weight:800}.metric-sample-row{padding:15px 14px;border-top:1px solid rgba(255,255,255,.12)}.metric-sample-name{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:760}.metric-sample-name i{width:9px;height:9px;border-radius:50%;background:var(--blue);box-shadow:0 0 0 4px rgba(22,69,238,.18)}.metric-sample-row[data-sample="eval_1"] .metric-sample-name i{background:var(--cyan);box-shadow:0 0 0 4px rgba(32,170,180,.18)}.metric-sample-row[data-sample="eval_2"] .metric-sample-name i{background:var(--green);box-shadow:0 0 0 4px rgba(83,204,97,.18)}.metric-sample-name small{display:block;color:#8fa09b;font-size:10px;font-weight:650;margin-top:2px}.metric-sample-value{font-size:27px;line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-.035em}.metric-sample-uplift .metric-sample-value{color:#73e57b}.metric-sample-uplift p{margin:5px 0 0;color:#aebcb8;font-size:11px}.tall-chart{height:340px}.chart-tooltip b{font-size:11px}.chart-tooltip .tip-label{color:#aebbb7}.canvas-wrap canvas{cursor:crosshair}.search-id-chip{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
12
12
  @media(max-width:980px){.layout{grid-template-columns:1fr}.sidenav{display:none}.main{padding:28px 22px 70px}.topbar{padding:0 22px}.scores{grid-template-columns:1fr 1fr}.uplift{grid-column:1/-1}.summary-grid{grid-template-columns:1fr 1fr}.stability-grid,.sample-grid{grid-template-columns:1fr}.source-cards{grid-template-columns:1fr 1fr}}
13
13
  @media(max-width:650px){.top-meta .chip,.brand-divider,.brand-label{display:none}.topbar{gap:10px}.button{padding:9px 10px}.hero-title{display:block}.run-stamp{text-align:left;margin-top:16px}.hero-title h1{font-size:30px}.metric-head{display:block}.metric-tabs{margin-top:15px;width:max-content}.summary-grid,.source-cards{grid-template-columns:1fr}.scores{gap:14px}.score-value{font-size:32px}.metric-sample-head{display:none}.metric-sample-row{grid-template-columns:1fr 1fr;gap:14px;padding:16px 2px}.metric-sample-name{grid-column:1/-1}.metric-sample-value{font-size:24px}.metric-sample-row>div:not(.metric-sample-name):before{display:block;color:#8fa09b;text-transform:uppercase;letter-spacing:.08em;font-size:8px;font-weight:800;margin-bottom:6px}.metric-sample-row>div:nth-child(2):before{content:"Baseline"}.metric-sample-row>div:nth-child(3):before{content:"Enriched"}.metric-sample-row>div:nth-child(4):before{content:"Uplift, abs"}.metric-sample-row>div:nth-child(5):before{content:"Uplift, %"}.main{padding-left:15px;padding-right:15px}.cell-output{gap:14px}}
@@ -507,8 +507,8 @@ const REPORT_DATA = __REPORT_DATA__;
507
507
  const maxImportance = Math.max(...features.map(f => f.importance)) || 1;
508
508
  const list = shapAll ? features : features.slice(0, (D.shap && D.shap.topN) || features.length);
509
509
  document.getElementById('shapList').innerHTML = list.map((f, i) =>
510
- `<div class="shap-row"><div><b>#${i + 1} ${f.name}</b><span class="provider">${f.provider}</span></div>` +
511
- `<div class="shap-track"><i class="shap-fill ${f.provider.toLowerCase()}" style="width:${(f.importance / maxImportance * 100).toFixed(2)}%"></i></div>` +
510
+ `<div class="shap-row"><div><b>#${i + 1} ${f.name}</b>${f.provider ? `<span class="provider">${f.provider}</span>` : ''}</div>` +
511
+ `<div class="shap-track"><i class="shap-fill ${f.shapClass || 'upgini'}" style="width:${(f.importance / maxImportance * 100).toFixed(2)}%"></i></div>` +
512
512
  `<b>${fmt3(Math.abs(f.shap))}</b></div>`).join('');
513
513
  document.getElementById('showAllShap').textContent = shapAll ? `Show top ${(D.shap && D.shap.topN) || features.length}` : `Show all ${features.length}`;
514
514
  }
@@ -534,7 +534,7 @@ const REPORT_DATA = __REPORT_DATA__;
534
534
  const body = document.getElementById('featureBody');
535
535
  body.innerHTML = rows.map(f =>
536
536
  `<tr data-name="${f.name}"><td><span class="feature-name" title="${f.name}">${f.name}</span></td>` +
537
- `<td><span class="provider">${f.provider}</span></td><td class="source-name">${f.source}</td>` +
537
+ `<td>${f.provider ? `<span class="provider">${f.provider}</span>` : ''}</td><td class="source-name">${f.source}</td>` +
538
538
  `<td>${shapLabel(f.shap)}</td><td><span class="tag ${f.status}">${fmt2(f.psi)}</span></td>` +
539
539
  `<td>${f.drift == null ? '—' : fmt2(f.drift)}</td><td>${pct1(f.coverage)}</td></tr>`).join('');
540
540
  document.getElementById('emptyState').style.display = rows.length ? 'none' : 'block';
@@ -1 +0,0 @@
1
- __version__ = "1.2.167.a4220.dev6"