upgini 1.2.167a4220.dev9__tar.gz → 1.2.167a4220.dev10__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.dev9 → upgini-1.2.167a4220.dev10}/PKG-INFO +1 -1
  2. upgini-1.2.167a4220.dev10/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/features_enricher.py +75 -34
  4. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/assemble.py +91 -18
  5. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/template.html +4 -3
  6. upgini-1.2.167a4220.dev9/src/upgini/__about__.py +0 -1
  7. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/.gitignore +0 -0
  8. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/LICENSE +0 -0
  9. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/README.md +0 -0
  10. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/pyproject.toml +0 -0
  11. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/__init__.py +0 -0
  12. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/ads.py +0 -0
  13. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/ads_management/__init__.py +0 -0
  14. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/ads_management/ads_manager.py +0 -0
  15. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/__init__.py +0 -0
  16. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/all_operators.py +0 -0
  17. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/binary.py +0 -0
  18. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/date.py +0 -0
  19. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/feature.py +0 -0
  20. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/groupby.py +0 -0
  21. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/operand.py +0 -0
  22. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/operator.py +0 -0
  23. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/__init__.py +0 -0
  24. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/base.py +0 -0
  25. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/cross.py +0 -0
  26. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/delta.py +0 -0
  27. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/lag.py +0 -0
  28. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  29. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/roll.py +0 -0
  30. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/trend.py +0 -0
  31. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/timeseries/volatility.py +0 -0
  32. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/unary.py +0 -0
  33. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/utils.py +0 -0
  34. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/autofe/vector.py +0 -0
  35. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/data_source/__init__.py +0 -0
  36. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/data_source/data_source_publisher.py +0 -0
  37. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/dataset.py +0 -0
  38. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/errors.py +0 -0
  39. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/http.py +0 -0
  40. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/mdc/__init__.py +0 -0
  41. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/mdc/context.py +0 -0
  42. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/metadata.py +0 -0
  43. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/metrics.py +0 -0
  44. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/normalizer/__init__.py +0 -0
  45. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/normalizer/normalize_utils.py +0 -0
  46. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/__init__.py +0 -0
  47. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/data.py +0 -0
  48. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/html.py +0 -0
  49. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/report/stats.py +0 -0
  50. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/resource_bundle/__init__.py +0 -0
  51. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/resource_bundle/exceptions.py +0 -0
  52. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/resource_bundle/strings.properties +0 -0
  53. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  54. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/sampler/__init__.py +0 -0
  55. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/sampler/base.py +0 -0
  56. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/sampler/random_under_sampler.py +0 -0
  57. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/sampler/utils.py +0 -0
  58. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/search_task.py +0 -0
  59. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/spinner.py +0 -0
  60. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  61. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/__init__.py +0 -0
  62. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/base_search_key_detector.py +0 -0
  63. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/blocked_time_series.py +0 -0
  64. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/config.py +0 -0
  65. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/country_utils.py +0 -0
  66. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/cpu_utils.py +0 -0
  67. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/custom_loss_utils.py +0 -0
  68. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/cv_utils.py +0 -0
  69. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/datetime_utils.py +0 -0
  70. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/deduplicate_utils.py +0 -0
  71. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/display_utils.py +0 -0
  72. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/email_utils.py +0 -0
  73. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/fallback_progress_bar.py +0 -0
  74. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/feature_info.py +0 -0
  75. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/features_validator.py +0 -0
  76. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/format.py +0 -0
  77. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/hash_utils.py +0 -0
  78. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/ip_utils.py +0 -0
  79. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/mstats.py +0 -0
  80. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/one_hot_encoder.py +0 -0
  81. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/phone_utils.py +0 -0
  82. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/postal_code_utils.py +0 -0
  83. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/progress_bar.py +0 -0
  84. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/psi.py +0 -0
  85. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/pyarrow_utils.py +0 -0
  86. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/sample_utils.py +0 -0
  87. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/search_key_derivations.py +0 -0
  88. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/sklearn_ext.py +0 -0
  89. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/sort.py +0 -0
  90. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/target_utils.py +0 -0
  91. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/track_info.py +0 -0
  92. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/ts_utils.py +0 -0
  93. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/src/upgini/utils/warning_counter.py +0 -0
  94. {upgini-1.2.167a4220.dev9 → upgini-1.2.167a4220.dev10}/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.dev9
3
+ Version: 1.2.167a4220.dev10
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.dev10"
@@ -100,6 +100,7 @@ from upgini.utils.deduplicate_utils import (
100
100
  remove_fintech_duplicates,
101
101
  )
102
102
  from upgini.report.assemble import (
103
+ _fold_model_nodes,
103
104
  _generated_feature_names,
104
105
  _parse_generated_feature,
105
106
  assemble_report_data,
@@ -2513,20 +2514,48 @@ class FeaturesEnricher(TransformerMixin):
2513
2514
  return False
2514
2515
  return bool(aliases & self._column_name_aliases(list(ensemble_names), renaming))
2515
2516
 
2516
- def _match_autofe_feature_meta(
2517
- self,
2518
- generated_meta: GeneratedFeatureMetadata,
2517
+ @staticmethod
2518
+ def _match_autofe_meta_by_names(
2519
+ names: set[str],
2519
2520
  features_meta: list[FeaturesMetadataV2],
2520
2521
  used_names: set[str],
2521
2522
  ) -> FeaturesMetadataV2 | None:
2522
2523
  available = [meta for meta in features_meta or [] if meta.name not in used_names]
2523
2524
  by_name = {meta.name: meta for meta in available}
2524
- for name in _generated_feature_names(generated_meta):
2525
+ for name in names:
2525
2526
  matched = by_name.get(name)
2526
2527
  if matched is not None:
2527
2528
  return matched
2528
2529
  return None
2529
2530
 
2531
+ def _add_autofe_description(
2532
+ self,
2533
+ descriptions: list[dict],
2534
+ used_names: set[str],
2535
+ feature_meta: FeaturesMetadataV2 | None,
2536
+ source_names: list[str],
2537
+ operand_names: Iterable[str],
2538
+ ) -> None:
2539
+ if feature_meta is None or not feature_meta.shap_value:
2540
+ return
2541
+ description: dict = {}
2542
+ description["shap"] = feature_meta.shap_value
2543
+ source = feature_meta.data_source or ""
2544
+ description[self.bundle.get("autofe_descriptions_sources")] = source.replace(
2545
+ "AutoFE: features from ", ""
2546
+ ).replace("AutoFE: feature from ", "")
2547
+ description[self.bundle.get("autofe_descriptions_feature_name")] = feature_meta.name
2548
+ feature_idx = 1
2549
+ for hashed in source_names:
2550
+ description[self.bundle.get("autofe_descriptions_feature").format(feature_idx)] = hashed
2551
+ feature_idx += 1
2552
+ # Don't show features with more than 2 base columns
2553
+ if feature_idx > 3:
2554
+ return
2555
+ description[self.bundle.get("autofe_descriptions_function")] = ",".join(sorted(operand_names))
2556
+ used_names.add(feature_meta.name)
2557
+ descriptions.append(description)
2558
+
2530
2559
  def _ensemble_generated_metadata(self):
2531
2560
  autofe_meta = self._search_task.get_autofe_metadata() if self._search_task else None
2532
2561
  if not autofe_meta:
@@ -5995,9 +6024,11 @@ if response.status_code == 200:
5995
6024
  orig_to_hashed = {
5996
6025
  base_column.original_name: base_column.hashed_name for base_column in m.base_columns
5997
6026
  }
6027
+ parsed = Feature.from_formula(m.formula)
6028
+ if not isinstance(parsed, Feature):
6029
+ continue
5998
6030
  autofe_feature = (
5999
- Feature.from_formula(m.formula)
6000
- .set_display_index(m.display_index or None)
6031
+ parsed.set_display_index(m.display_index or None)
6001
6032
  .set_alias(m.alias)
6002
6033
  .set_op_params(m.operator_params or {})
6003
6034
  .rename_columns(orig_to_hashed)
@@ -6006,40 +6037,48 @@ if response.status_code == 200:
6006
6037
  self.logger.exception(f"Failed to parse AutoFE formula: {m.formula}")
6007
6038
  continue
6008
6039
 
6009
- is_ts = isinstance(autofe_feature.op, TimeSeriesBase)
6010
-
6011
- if autofe_feature.op.is_vector and not is_ts:
6012
- continue
6013
-
6014
- feature_meta = self._match_autofe_feature_meta(m, features_meta, used_names)
6015
- if feature_meta is None or not feature_meta.shap_value:
6040
+ fold_autofe = [
6041
+ child
6042
+ for fold in _fold_model_nodes(autofe_feature)
6043
+ for child in fold.children
6044
+ if isinstance(child, Feature)
6045
+ ]
6046
+ if fold_autofe:
6047
+ hashed_to_orig = {hashed: orig for orig, hashed in orig_to_hashed.items()}
6048
+ for node in fold_autofe:
6049
+ is_ts = isinstance(node.op, TimeSeriesBase)
6050
+ source_names = [
6051
+ hashed
6052
+ for hashed in node.get_columns()
6053
+ if not is_ts or hashed_to_orig.get(hashed, hashed) not in self.fit_search_keys
6054
+ ]
6055
+ name = node.get_display_name(shorten=True, unhash=True, cache=False)
6056
+ self._add_autofe_description(
6057
+ descriptions,
6058
+ used_names,
6059
+ self._match_autofe_meta_by_names({name} if name else set(), features_meta, used_names),
6060
+ source_names,
6061
+ node.get_all_operand_names(),
6062
+ )
6016
6063
  continue
6017
6064
 
6018
- description = {}
6019
- description["shap"] = feature_meta.shap_value
6020
- source = feature_meta.data_source or ""
6021
- description[self.bundle.get("autofe_descriptions_sources")] = source.replace(
6022
- "AutoFE: features from ", ""
6023
- ).replace("AutoFE: feature from ", "")
6024
- description[self.bundle.get("autofe_descriptions_feature_name")] = feature_meta.name
6025
-
6026
- feature_idx = 1
6027
- for bc in m.base_columns:
6028
- if not is_ts or bc.original_name not in self.fit_search_keys:
6029
- description[self.bundle.get("autofe_descriptions_feature").format(feature_idx)] = bc.hashed_name
6030
- feature_idx += 1
6031
-
6032
- # Don't show features with more than 2 base columns
6033
- if feature_idx > 3:
6065
+ is_ts = isinstance(autofe_feature.op, TimeSeriesBase)
6066
+ if autofe_feature.op.is_vector and not is_ts:
6034
6067
  continue
6035
6068
 
6036
- description[self.bundle.get("autofe_descriptions_function")] = ",".join(
6037
- sorted(autofe_feature.get_all_operand_names())
6069
+ source_names = [
6070
+ bc.hashed_name
6071
+ for bc in m.base_columns
6072
+ if not is_ts or bc.original_name not in self.fit_search_keys
6073
+ ]
6074
+ self._add_autofe_description(
6075
+ descriptions,
6076
+ used_names,
6077
+ self._match_autofe_meta_by_names(_generated_feature_names(m), features_meta, used_names),
6078
+ source_names,
6079
+ autofe_feature.get_all_operand_names(),
6038
6080
  )
6039
6081
 
6040
- used_names.add(feature_meta.name)
6041
- descriptions.append(description)
6042
-
6043
6082
  if len(descriptions) == 0:
6044
6083
  return None
6045
6084
 
@@ -6216,6 +6255,8 @@ if response.status_code == 200:
6216
6255
  display_html_dataframe(self.metrics, self.metrics, msg)
6217
6256
 
6218
6257
  def __show_selected_features(self):
6258
+ if self._has_single_ensemble_score():
6259
+ return
6219
6260
  try:
6220
6261
  _ = get_ipython() # type: ignore
6221
6262
 
@@ -7,7 +7,7 @@ from typing import Callable, Optional
7
7
  import pandas as pd
8
8
 
9
9
  from upgini.__about__ import __version__
10
- from upgini.autofe.feature import Feature
10
+ from upgini.autofe.feature import Column, Feature
11
11
  from upgini.autofe.vector import CatboostModel, EnsembleModel, OnnxModel
12
12
  from upgini.metadata import BaseColumnMetadata, FeaturesMetadataV2, GeneratedFeatureMetadata
13
13
  from upgini.report.data import (
@@ -313,26 +313,30 @@ def _overview_summary(
313
313
  joined_ads_features_count: Optional[int],
314
314
  joined_ads_count: Optional[int],
315
315
  ) -> SearchResultsSummary:
316
- top_level = _unique_fold_model_columns(generated_features)
316
+ top_level = _unique_fold_model_inputs(generated_features)
317
317
  features_by_name = {meta.name: meta for meta in features_meta}
318
318
  autofe_by_name = _autofe_generated_by_name(generated_features, is_ensemble)
319
+ columns_by_name = _base_columns_by_name(generated_features)
320
+ ads_ids = _contributed_ads_ids(generated_features, autofe_by_name)
319
321
  if top_level:
320
322
  counts = {"external": 0, "original": 0, "autofe": 0}
321
- ads_ids: set[str] = set()
322
323
  psis: list[Optional[float]] = []
323
- for column in top_level:
324
- kind = _top_level_kind(column, features_by_name, autofe_by_name, is_ensemble)
324
+ for node in top_level:
325
+ kind = _top_level_kind(node, features_by_name, columns_by_name, autofe_by_name, is_ensemble)
325
326
  counts[kind] += 1
326
- ads_ids.update(_leaf_ads_ids(column, autofe_by_name))
327
- meta = features_by_name.get(column.hashed_name) or features_by_name.get(column.original_name)
327
+ meta = _feature_meta_for_node(node, features_by_name, columns_by_name)
328
328
  psis.append(None if meta is None else meta.psi_value)
329
329
  model_features = len(top_level)
330
330
  external, original, autofe = counts["external"], counts["original"], counts["autofe"]
331
- contributed = len(ads_ids)
331
+ contributed = len(ads_ids or set())
332
332
  else:
333
333
  model_features = len(model_rows) or None
334
334
  external = original = autofe = None
335
- contributed = len({(row.provider, row.source) for row in found_rows if row.provider})
335
+ contributed = (
336
+ len(ads_ids)
337
+ if ads_ids is not None
338
+ else len({(row.provider, row.source) for row in found_rows if row.provider})
339
+ )
336
340
  psis = [row.psi for row in model_rows]
337
341
  share, stable, psi_count = _stability_from_psi(psis)
338
342
  return SearchResultsSummary(
@@ -351,17 +355,66 @@ def _overview_summary(
351
355
  )
352
356
 
353
357
 
354
- def _unique_fold_model_columns(generated_features: list[GeneratedFeatureMetadata]) -> list[BaseColumnMetadata]:
355
- unique: dict[str, BaseColumnMetadata] = {}
358
+ def _unique_fold_model_inputs(generated_features: list[GeneratedFeatureMetadata]) -> list[Column | Feature]:
359
+ unique: dict[str, Column | Feature] = {}
356
360
  for meta in generated_features:
357
361
  feature = _parse_generated_feature(meta)
358
- if feature is None or not isinstance(feature.op, (CatboostModel, OnnxModel)):
362
+ if feature is None:
359
363
  continue
360
- for column in meta.base_columns:
361
- unique.setdefault(column.hashed_name or column.original_name, column)
364
+ for fold in _fold_model_nodes(feature):
365
+ for child in fold.children:
366
+ unique.setdefault(_top_level_key(child), child)
362
367
  return list(unique.values())
363
368
 
364
369
 
370
+ def _fold_model_nodes(feature: Feature) -> list[Feature]:
371
+ if isinstance(feature.op, (CatboostModel, OnnxModel)):
372
+ return [feature]
373
+ nodes: list[Feature] = []
374
+ for child in feature.children:
375
+ if isinstance(child, Feature):
376
+ nodes.extend(_fold_model_nodes(child))
377
+ return nodes
378
+
379
+
380
+ def _top_level_key(node: Column | Feature) -> str:
381
+ if isinstance(node, Column):
382
+ return f"col:{node.name}"
383
+ return f"fe:{node.to_formula()}"
384
+
385
+
386
+ def _base_columns_by_name(generated_features: list[GeneratedFeatureMetadata]) -> dict[str, BaseColumnMetadata]:
387
+ by_name: dict[str, BaseColumnMetadata] = {}
388
+ for meta in generated_features:
389
+ for column in meta.base_columns:
390
+ if column.hashed_name:
391
+ by_name.setdefault(column.hashed_name, column)
392
+ if column.original_name:
393
+ by_name.setdefault(column.original_name, column)
394
+ return by_name
395
+
396
+
397
+ def _column_names(node: Column, columns_by_name: dict[str, BaseColumnMetadata]) -> list[str]:
398
+ names = [node.name]
399
+ column = columns_by_name.get(node.name)
400
+ if column is not None:
401
+ names.extend([column.hashed_name, column.original_name])
402
+ return [name for name in names if name]
403
+
404
+
405
+ def _feature_meta_for_node(
406
+ node: Column | Feature,
407
+ features_by_name: dict[str, FeaturesMetadataV2],
408
+ columns_by_name: dict[str, BaseColumnMetadata],
409
+ ) -> Optional[FeaturesMetadataV2]:
410
+ if not isinstance(node, Column):
411
+ return None
412
+ return next(
413
+ (features_by_name[name] for name in _column_names(node, columns_by_name) if name in features_by_name),
414
+ None,
415
+ )
416
+
417
+
365
418
  def _autofe_generated_by_name(
366
419
  generated_features: list[GeneratedFeatureMetadata], is_ensemble: Callable[[str], bool]
367
420
  ) -> dict[str, GeneratedFeatureMetadata]:
@@ -400,13 +453,16 @@ def _generated_feature_names(meta: GeneratedFeatureMetadata) -> set[str]:
400
453
 
401
454
 
402
455
  def _top_level_kind(
403
- column: BaseColumnMetadata,
456
+ node: Column | Feature,
404
457
  features_by_name: dict[str, FeaturesMetadataV2],
458
+ columns_by_name: dict[str, BaseColumnMetadata],
405
459
  autofe_by_name: dict[str, GeneratedFeatureMetadata],
406
460
  is_ensemble: Callable[[str], bool],
407
461
  ) -> str:
408
- names = [column.hashed_name, column.original_name]
409
- if any(name in autofe_by_name for name in names if name):
462
+ if isinstance(node, Feature):
463
+ return "autofe"
464
+ names = _column_names(node, columns_by_name)
465
+ if any(name in autofe_by_name for name in names):
410
466
  return "autofe"
411
467
  meta = next((features_by_name[name] for name in names if name in features_by_name), None)
412
468
  if meta is not None and not is_ensemble(meta.name):
@@ -416,11 +472,28 @@ def _top_level_kind(
416
472
  return "external"
417
473
  if meta.source == "etalon":
418
474
  return "original"
419
- if column.ads_definition_id:
475
+ column = next((columns_by_name[name] for name in names if name in columns_by_name), None)
476
+ if column is not None and column.ads_definition_id:
420
477
  return "external"
421
478
  return "original"
422
479
 
423
480
 
481
+ def _contributed_ads_ids(
482
+ generated_features: list[GeneratedFeatureMetadata],
483
+ autofe_by_name: dict[str, GeneratedFeatureMetadata],
484
+ ) -> Optional[set[str]]:
485
+ ads_ids: Optional[set[str]] = None
486
+ for meta in generated_features:
487
+ feature = _parse_generated_feature(meta)
488
+ if feature is None or not _fold_model_nodes(feature):
489
+ continue
490
+ if ads_ids is None:
491
+ ads_ids = set()
492
+ for column in meta.base_columns:
493
+ ads_ids.update(_leaf_ads_ids(column, autofe_by_name))
494
+ return ads_ids
495
+
496
+
424
497
  def _leaf_ads_ids(column: BaseColumnMetadata, autofe_by_name: dict[str, GeneratedFeatureMetadata]) -> set[str]:
425
498
  autofe = autofe_by_name.get(column.hashed_name) or autofe_by_name.get(column.original_name)
426
499
  leaves = autofe.base_columns if autofe is not None else [column]
@@ -7,8 +7,9 @@
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{display:inline-block;font-size:9px;margin-left:6px;padding:3px 7px;border-radius:8px;border:1px solid var(--line);color:var(--muted);line-height:1.3;text-align:center;vertical-align:middle;white-space:normal}.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}
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:fit-content(55%) minmax(90px,1fr) 64px;gap:12px;align-items:center;padding:9px 4px;border-bottom:1px solid #edf1ef}.shap-row:last-child{border:0}.shap-name{min-width:0;overflow-wrap:anywhere;word-break:break-word}.shap-value{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}.provider{display:inline-block;font-size:9px;margin-left:6px;padding:3px 7px;border-radius:8px;border:1px solid var(--line);color:var(--muted);line-height:1.3;text-align:center;vertical-align:middle;white-space:normal}.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
+ @supports (grid-template-columns:subgrid){.shap-panel{display:grid;grid-template-columns:fit-content(55%) minmax(90px,1fr) 64px;column-gap:12px}.shap-row{grid-template-columns:subgrid;grid-column:1/-1;padding-left:0;padding-right:0}}
12
13
  @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
14
  @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}}
14
15
  </style>
@@ -510,9 +511,9 @@ const REPORT_DATA = __REPORT_DATA__;
510
511
  const maxImportance = Math.max(...features.map(f => f.importance)) || 1;
511
512
  const list = shapAll ? features : features.slice(0, (D.shap && D.shap.topN) || features.length);
512
513
  document.getElementById('shapList').innerHTML = list.map((f, i) =>
513
- `<div class="shap-row"><div><b>#${i + 1} ${f.name}</b>${providerBadge(f.provider)}</div>` +
514
+ `<div class="shap-row"><div class="shap-name"><b>#${i + 1} ${f.name}</b>${providerBadge(f.provider)}</div>` +
514
515
  `<div class="shap-track"><i class="shap-fill ${f.shapClass || 'upgini'}" style="width:${(f.importance / maxImportance * 100).toFixed(2)}%"></i></div>` +
515
- `<b>${fmt3(Math.abs(f.shap))}</b></div>`).join('');
516
+ `<b class="shap-value">${fmt3(Math.abs(f.shap))}</b></div>`).join('');
516
517
  document.getElementById('showAllShap').textContent = shapAll ? `Show top ${(D.shap && D.shap.topN) || features.length}` : `Show all ${features.length}`;
517
518
  }
518
519
 
@@ -1 +0,0 @@
1
- __version__ = "1.2.167.a4220.dev9"