upgini 1.2.165a4244.dev2__tar.gz → 1.2.166a4253.dev1__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 (88) hide show
  1. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/PKG-INFO +1 -1
  2. upgini-1.2.166a4253.dev1/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/features_enricher.py +375 -203
  4. upgini-1.2.165a4244.dev2/src/upgini/__about__.py +0 -1
  5. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/.gitignore +0 -0
  6. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/LICENSE +0 -0
  7. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/README.md +0 -0
  8. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/pyproject.toml +0 -0
  9. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/__init__.py +0 -0
  10. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/ads.py +0 -0
  11. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/ads_management/__init__.py +0 -0
  12. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/ads_management/ads_manager.py +0 -0
  13. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/__init__.py +0 -0
  14. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/all_operators.py +0 -0
  15. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/binary.py +0 -0
  16. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/date.py +0 -0
  17. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/feature.py +0 -0
  18. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/groupby.py +0 -0
  19. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/operand.py +0 -0
  20. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/operator.py +0 -0
  21. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/__init__.py +0 -0
  22. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/base.py +0 -0
  23. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/cross.py +0 -0
  24. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/delta.py +0 -0
  25. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/lag.py +0 -0
  26. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/numpy_kernels.py +0 -0
  27. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/roll.py +0 -0
  28. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/trend.py +0 -0
  29. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/timeseries/volatility.py +0 -0
  30. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/unary.py +0 -0
  31. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/utils.py +0 -0
  32. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/autofe/vector.py +0 -0
  33. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/data_source/__init__.py +0 -0
  34. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/data_source/data_source_publisher.py +0 -0
  35. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/dataset.py +0 -0
  36. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/errors.py +0 -0
  37. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/http.py +0 -0
  38. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/mdc/__init__.py +0 -0
  39. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/mdc/context.py +0 -0
  40. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/metadata.py +0 -0
  41. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/metrics.py +0 -0
  42. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/normalizer/__init__.py +0 -0
  43. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/normalizer/normalize_utils.py +0 -0
  44. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/resource_bundle/__init__.py +0 -0
  45. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/resource_bundle/exceptions.py +0 -0
  46. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/resource_bundle/strings.properties +0 -0
  47. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  48. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/sampler/__init__.py +0 -0
  49. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/sampler/base.py +0 -0
  50. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/sampler/random_under_sampler.py +0 -0
  51. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/sampler/utils.py +0 -0
  52. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/search_task.py +0 -0
  53. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/spinner.py +0 -0
  54. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  55. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/__init__.py +0 -0
  56. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/base_search_key_detector.py +0 -0
  57. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/blocked_time_series.py +0 -0
  58. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/config.py +0 -0
  59. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/country_utils.py +0 -0
  60. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/cpu_utils.py +0 -0
  61. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/custom_loss_utils.py +0 -0
  62. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/cv_utils.py +0 -0
  63. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/datetime_utils.py +0 -0
  64. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/deduplicate_utils.py +0 -0
  65. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/display_utils.py +0 -0
  66. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/email_utils.py +0 -0
  67. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/fallback_progress_bar.py +0 -0
  68. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/feature_info.py +0 -0
  69. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/features_validator.py +0 -0
  70. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/format.py +0 -0
  71. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/hash_utils.py +0 -0
  72. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/ip_utils.py +0 -0
  73. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/mstats.py +0 -0
  74. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/one_hot_encoder.py +0 -0
  75. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/phone_utils.py +0 -0
  76. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/postal_code_utils.py +0 -0
  77. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/progress_bar.py +0 -0
  78. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/psi.py +0 -0
  79. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/pyarrow_utils.py +0 -0
  80. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/sample_utils.py +0 -0
  81. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/search_key_derivations.py +0 -0
  82. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/sklearn_ext.py +0 -0
  83. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/sort.py +0 -0
  84. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/target_utils.py +0 -0
  85. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/track_info.py +0 -0
  86. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/ts_utils.py +0 -0
  87. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/src/upgini/utils/warning_counter.py +0 -0
  88. {upgini-1.2.165a4244.dev2 → upgini-1.2.166a4253.dev1}/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.165a4244.dev2
3
+ Version: 1.2.166a4253.dev1
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.166.a4253.dev1"
@@ -132,6 +132,8 @@ DEMO_DATASET_HASHES = [
132
132
  "2519c9077c559f8975fdcdb5c50e9daae8d50b1d8a3ec72296c65ea7276f8812", # demo_salary transform
133
133
  ]
134
134
 
135
+ MATCH_HASH = "_match_hash"
136
+
135
137
 
136
138
  class FeaturesEnricher(TransformerMixin):
137
139
  """Retrieve external features via Upgini that are most relevant to predict your target.
@@ -1135,7 +1137,7 @@ class FeaturesEnricher(TransformerMixin):
1135
1137
  enriched_estimator = None
1136
1138
  ensemble_score_column = self._get_ensemble_score_column(fitting_X, fitting_enriched_X)
1137
1139
  if set(fitting_X.columns) != set(fitting_enriched_X.columns):
1138
- if ensemble_score_column is not None:
1140
+ if self.baseline_score_column is not None and ensemble_score_column is not None:
1139
1141
  self.logger.info(
1140
1142
  f"Only ensemble model returned; calculate enriched {metric} via roc_auc "
1141
1143
  f"on ensemble score column: {ensemble_score_column}"
@@ -1755,6 +1757,7 @@ class FeaturesEnricher(TransformerMixin):
1755
1757
  def __enrich_df_with_fit_features(
1756
1758
  self,
1757
1759
  rows_to_drop: pd.DataFrame | None = None,
1760
+ left_columns: list[str] | None = None,
1758
1761
  ) -> tuple[pd.DataFrame, list[str], dict[str, SearchKey]] | None:
1759
1762
  if self.df_with_original_index is None or self._search_task is None:
1760
1763
  return None
@@ -1772,8 +1775,17 @@ class FeaturesEnricher(TransformerMixin):
1772
1775
  ]
1773
1776
  self.logger.info(f"After dropping target outliers size: {len(fit_features)}")
1774
1777
 
1778
+ left = self.df_with_original_index
1779
+ if left_columns is not None:
1780
+ left = left.loc[:, left_columns]
1781
+ join_keys = {ENTITY_SYSTEM_RECORD_ID, SYSTEM_RECORD_ID, EVAL_SET_INDEX}
1782
+ client_cols = set(self.df_with_original_index.columns) - join_keys
1783
+ right_keep = [c for c in fit_features.columns if c not in client_cols]
1784
+ if len(right_keep) < len(fit_features.columns):
1785
+ fit_features = fit_features.loc[:, right_keep]
1786
+
1775
1787
  enriched_Xy = self.__enrich(
1776
- self.df_with_original_index,
1788
+ left,
1777
1789
  fit_features,
1778
1790
  how="inner",
1779
1791
  drop_system_record_id=False,
@@ -1783,6 +1795,125 @@ class FeaturesEnricher(TransformerMixin):
1783
1795
  search_keys = {self.fit_columns_renaming.get(k, k): v for k, v in self.fit_search_keys.items()}
1784
1796
  return enriched_Xy, generated_features, search_keys
1785
1797
 
1798
+ @staticmethod
1799
+ def __transform_match_columns(search_keys: dict[str, SearchKey], features_for_transform: list[str]) -> list[str]:
1800
+ return sorted(dict.fromkeys(list(search_keys.keys()) + list(features_for_transform)))
1801
+
1802
+ def __align_match_frame(self, source: pd.DataFrame, target: pd.DataFrame, cols: list[str]) -> pd.DataFrame | None:
1803
+ missing = [c for c in cols if c not in source.columns or c not in target.columns]
1804
+ if missing:
1805
+ self.logger.info(f"Match columns missing for fit reuse: {missing}")
1806
+ return None
1807
+ needs_cast = any(source[c].dtype != target[c].dtype for c in cols)
1808
+ # List indexing already materializes the subset; copy only when we have to astype.
1809
+ out = source[cols].copy() if needs_cast else source[cols]
1810
+ try:
1811
+ if needs_cast:
1812
+ for c in cols:
1813
+ if out[c].dtype != target[c].dtype:
1814
+ out[c] = out[c].astype(target[c].dtype)
1815
+ except (TypeError, ValueError) as e:
1816
+ self.logger.info(f"Failed to align match column dtypes for fit reuse: {e}")
1817
+ return None
1818
+ return out
1819
+
1820
+ @staticmethod
1821
+ def __dedupe_on(df: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
1822
+ duplicated = df.duplicated(subset=subset)
1823
+ if not duplicated.any():
1824
+ return df
1825
+ return df.loc[~duplicated]
1826
+
1827
+ def __fit_feature_lookup(
1828
+ self,
1829
+ df_transform: pd.DataFrame,
1830
+ match_columns_hashed: list[str],
1831
+ feature_cols: list[str],
1832
+ enriched_Xy: pd.DataFrame,
1833
+ ) -> pd.DataFrame | None:
1834
+ if self.df_with_original_index is None or enriched_Xy is None or len(enriched_Xy) == 0:
1835
+ return None
1836
+ if not match_columns_hashed or not feature_cols:
1837
+ return None
1838
+ if ENTITY_SYSTEM_RECORD_ID not in enriched_Xy.columns:
1839
+ return None
1840
+ if ENTITY_SYSTEM_RECORD_ID not in self.df_with_original_index.columns:
1841
+ return None
1842
+
1843
+ ads_cols = [c for c in feature_cols if c in enriched_Xy.columns]
1844
+ if not ads_cols:
1845
+ return None
1846
+
1847
+ key_cols = [c for c in match_columns_hashed if c in self.df_with_original_index.columns]
1848
+ if len(key_cols) != len(match_columns_hashed):
1849
+ return None
1850
+
1851
+ ads = self.__dedupe_on(enriched_Xy.loc[:, [ENTITY_SYSTEM_RECORD_ID] + ads_cols], [ENTITY_SYSTEM_RECORD_ID])
1852
+ covered_ids = ads[ENTITY_SYSTEM_RECORD_ID].to_numpy()
1853
+
1854
+ fit_keys = self.df_with_original_index.loc[:, key_cols + [ENTITY_SYSTEM_RECORD_ID]]
1855
+ covered_mask = fit_keys[ENTITY_SYSTEM_RECORD_ID].isin(covered_ids)
1856
+ if not covered_mask.all():
1857
+ fit_keys = fit_keys.loc[covered_mask]
1858
+ fit_keys = self.__dedupe_on(fit_keys, [ENTITY_SYSTEM_RECORD_ID])
1859
+ if len(fit_keys) == 0:
1860
+ return None
1861
+
1862
+ aligned = self.__align_match_frame(fit_keys, df_transform, match_columns_hashed)
1863
+ if aligned is None:
1864
+ return None
1865
+
1866
+ match_hash = pd.util.hash_pandas_object(aligned, index=False).astype("float64")
1867
+ lookup = ads.set_index(ENTITY_SYSTEM_RECORD_ID).reindex(fit_keys[ENTITY_SYSTEM_RECORD_ID].to_numpy())
1868
+ lookup[MATCH_HASH] = match_hash.to_numpy()
1869
+ lookup.reset_index(drop=True, inplace=True)
1870
+ return self.__dedupe_on(lookup, [MATCH_HASH])
1871
+
1872
+ def __unmatched_transform_mask(self, df: pd.DataFrame, lookup: pd.DataFrame | None) -> np.ndarray:
1873
+ if lookup is None or len(lookup) == 0 or MATCH_HASH not in lookup.columns:
1874
+ return np.ones(len(df), dtype=bool)
1875
+ return ~df[ENTITY_SYSTEM_RECORD_ID].isin(lookup[MATCH_HASH].to_numpy()).to_numpy()
1876
+
1877
+ def __attach_fit_lookup(
1878
+ self,
1879
+ dest: pd.DataFrame,
1880
+ lookup: pd.DataFrame,
1881
+ exclude_features_sources: list[str] | None,
1882
+ ) -> pd.DataFrame:
1883
+ feature_lookup = lookup.set_index(MATCH_HASH)
1884
+ overlap = [c for c in feature_lookup.columns if c in dest.columns]
1885
+ if overlap:
1886
+ feature_lookup = feature_lookup.drop(columns=overlap)
1887
+ if exclude_features_sources:
1888
+ feature_lookup = feature_lookup.drop(columns=exclude_features_sources, errors="ignore")
1889
+ keys = dest[ENTITY_SYSTEM_RECORD_ID]
1890
+ for col in feature_lookup.columns:
1891
+ dest[col] = keys.map(feature_lookup[col])
1892
+ return dest
1893
+
1894
+ def __check_transform_quota(self, row_count: int) -> bool:
1895
+ """Return False when transform should abort because of quota."""
1896
+ if row_count <= 0:
1897
+ return True
1898
+ transform_usage = self.rest_client.get_current_transform_usage(self._get_trace_id())
1899
+ self.logger.info(f"Current transform usage: {transform_usage}. Transforming {row_count} rows")
1900
+ if not transform_usage.has_limit:
1901
+ return True
1902
+ if row_count > transform_usage.rest_rows:
1903
+ rest_rows = max(transform_usage.rest_rows, 0)
1904
+ bundle_msg = (
1905
+ "transform_usage_warning_registered" if self.__is_registered else "transform_usage_warning_demo"
1906
+ )
1907
+ msg = self.bundle.get(bundle_msg).format(rest_rows, row_count)
1908
+ self.logger.warning(msg)
1909
+ print(msg)
1910
+ show_request_quote_button(is_registered=self.__is_registered)
1911
+ return False
1912
+ msg = self.bundle.get("transform_usage_info").format(transform_usage.limit, transform_usage.transformed_rows)
1913
+ self.logger.info(msg)
1914
+ print(msg)
1915
+ return True
1916
+
1786
1917
  def __finalize_transform_result(
1787
1918
  self,
1788
1919
  result: pd.DataFrame,
@@ -1840,13 +1971,15 @@ class FeaturesEnricher(TransformerMixin):
1840
1971
  keep_input: bool,
1841
1972
  add_fit_system_record_id: bool,
1842
1973
  metrics_calculation: bool = False,
1974
+ enriched: tuple[pd.DataFrame, list[str], dict[str, SearchKey]] | None = None,
1843
1975
  ) -> tuple[pd.DataFrame, dict[str, str], list[str], dict[str, SearchKey]] | None:
1844
1976
  """Reuse fit enrichment for transform when X was already sent on fit.
1845
1977
 
1846
1978
  Returns None when coverage is incomplete (sampling/dedup) so the caller can
1847
1979
  fall back to a normal validation search.
1848
1980
  """
1849
- enriched = self.__enrich_df_with_fit_features()
1981
+ if enriched is None:
1982
+ enriched = self.__enrich_df_with_fit_features()
1850
1983
  if enriched is None:
1851
1984
  return None
1852
1985
  enriched_Xy, generated_features, search_keys = enriched
@@ -1992,9 +2125,6 @@ class FeaturesEnricher(TransformerMixin):
1992
2125
  return self.baseline_score_column
1993
2126
 
1994
2127
  def _get_ensemble_score_column(self, fitting_X: pd.DataFrame, fitting_enriched_X: pd.DataFrame) -> str | None:
1995
- """Ensemble score column when baseline_score_column is set and it is the only returned extra feature."""
1996
- if self.baseline_score_column is None:
1997
- return None
1998
2128
  renaming = self.fit_columns_renaming or {}
1999
2129
  generated_aliases = self._column_name_aliases(self.fit_generated_features or [], renaming)
2000
2130
  new_external = [
@@ -3284,6 +3414,15 @@ if response.status_code == 200:
3284
3414
  self.__display_support_link(msg)
3285
3415
  return None, {}, [], search_keys
3286
3416
 
3417
+ fit_left_columns = [ENTITY_SYSTEM_RECORD_ID]
3418
+ if self.df_with_original_index is not None:
3419
+ if EVAL_SET_INDEX in self.df_with_original_index.columns:
3420
+ fit_left_columns.append(EVAL_SET_INDEX)
3421
+ for col in self.fit_generated_features or []:
3422
+ if col in self.df_with_original_index.columns:
3423
+ fit_left_columns.append(col)
3424
+ fit_enriched = self.__enrich_df_with_fit_features(left_columns=fit_left_columns)
3425
+
3287
3426
  # Reuse fit enrichment when transforming the same train / eval / OOT object
3288
3427
  # that was already uploaded on fit (avoids a second backend search + quota use).
3289
3428
  fit_dataset_index = self._find_fit_dataset_index(X)
@@ -3296,6 +3435,7 @@ if response.status_code == 200:
3296
3435
  keep_input,
3297
3436
  add_fit_system_record_id,
3298
3437
  metrics_calculation=metrics_calculation,
3438
+ enriched=fit_enriched,
3299
3439
  )
3300
3440
  if from_fit is not None:
3301
3441
  return from_fit
@@ -3310,27 +3450,6 @@ if response.status_code == 200:
3310
3450
  print(msg)
3311
3451
  self.display_transactional_transform_api(only_online_sources=True)
3312
3452
 
3313
- if not metrics_calculation:
3314
- transform_usage = self.rest_client.get_current_transform_usage(self._get_trace_id())
3315
- self.logger.info(f"Current transform usage: {transform_usage}. Transforming {len(X)} rows")
3316
- if transform_usage.has_limit:
3317
- if len(X) > transform_usage.rest_rows:
3318
- rest_rows = max(transform_usage.rest_rows, 0)
3319
- bundle_msg = (
3320
- "transform_usage_warning_registered" if self.__is_registered else "transform_usage_warning_demo"
3321
- )
3322
- msg = self.bundle.get(bundle_msg).format(rest_rows, len(X))
3323
- self.logger.warning(msg)
3324
- print(msg)
3325
- show_request_quote_button(is_registered=self.__is_registered)
3326
- return None, {}, [], {}
3327
- else:
3328
- msg = self.bundle.get("transform_usage_info").format(
3329
- transform_usage.limit, transform_usage.transformed_rows
3330
- )
3331
- self.logger.info(msg)
3332
- print(msg)
3333
-
3334
3453
  is_demo_dataset = hash_input(df) in DEMO_DATASET_HASHES
3335
3454
 
3336
3455
  columns_to_drop = [
@@ -3448,7 +3567,7 @@ if response.status_code == 200:
3448
3567
  runtime_parameters.properties["features_for_embeddings"] = ",".join(features_for_embeddings)
3449
3568
  features_for_transform = [f for f in features_for_transform if f not in search_keys.keys()]
3450
3569
 
3451
- columns_for_system_record_id = sorted(list(search_keys.keys()) + features_for_transform)
3570
+ columns_for_system_record_id = self.__transform_match_columns(search_keys, features_for_transform)
3452
3571
 
3453
3572
  df[ENTITY_SYSTEM_RECORD_ID] = pd.util.hash_pandas_object(df[columns_for_system_record_id], index=False).astype(
3454
3573
  "float64"
@@ -3471,212 +3590,265 @@ if response.status_code == 200:
3471
3590
  df = df.rename(columns={SYSTEM_RECORD_ID: SORT_ID})
3472
3591
  features_not_to_pass.append(SORT_ID)
3473
3592
 
3593
+ lookup = None
3594
+ unmatched_mask = np.ones(len(df), dtype=bool)
3595
+ if fit_enriched is not None:
3596
+ enriched_Xy, fit_generated, _ = fit_enriched
3597
+ lookup_exclude = {
3598
+ TARGET,
3599
+ EVAL_SET_INDEX,
3600
+ SYSTEM_RECORD_ID,
3601
+ ENTITY_SYSTEM_RECORD_ID,
3602
+ SORT_ID,
3603
+ DateTimeConverter.DATETIME_COL,
3604
+ MATCH_HASH,
3605
+ }
3606
+ generated_set = set(fit_generated)
3607
+ feature_cols = [c for c in enriched_Xy.columns if c not in lookup_exclude and c not in generated_set]
3608
+ lookup = self.__fit_feature_lookup(df, columns_for_system_record_id, feature_cols, enriched_Xy)
3609
+ if lookup is not None and len(lookup) > 0:
3610
+ unmatched_mask = self.__unmatched_transform_mask(df, lookup)
3611
+ del enriched_Xy
3612
+ del fit_enriched
3613
+
3614
+ reused_count = int((~unmatched_mask).sum())
3615
+ unmatched_count = int(unmatched_mask.sum())
3616
+ self.logger.info(f"Fit enrichment reuse: reused={reused_count}, send_to_transform={unmatched_count}")
3617
+
3618
+ if not metrics_calculation and unmatched_count > 0:
3619
+ if not self.__check_transform_quota(unmatched_count):
3620
+ return None, {}, [], {}
3621
+
3474
3622
  system_columns_with_original_index = [ENTITY_SYSTEM_RECORD_ID] + generated_features
3475
3623
  if add_fit_system_record_id:
3476
3624
  system_columns_with_original_index.append(SORT_ID)
3477
3625
 
3478
- df_before_explode = df[system_columns_with_original_index].copy()
3626
+ df_before_explode = df[system_columns_with_original_index]
3479
3627
 
3480
- # Explode multiple search keys
3481
- df, unnest_search_keys = self._explode_multiple_search_keys(df, search_keys, columns_renaming)
3628
+ result_features = None
3629
+ unnest_search_keys: dict[str, list[str]] = {}
3630
+ if unmatched_count > 0:
3631
+ work_df = df if unmatched_mask.all() else df.iloc[unmatched_mask].copy()
3482
3632
 
3483
- # Convert search keys and generate features on them
3633
+ # Explode multiple search keys
3634
+ work_df, unnest_search_keys = self._explode_multiple_search_keys(work_df, search_keys, columns_renaming)
3484
3635
 
3485
- email_column = self._get_email_column(search_keys)
3486
- hem_column = self._get_hem_column(search_keys)
3487
- if email_column:
3488
- converter = EmailSearchKeyConverter(
3489
- email_column,
3490
- hem_column,
3491
- search_keys,
3492
- columns_renaming,
3493
- list(unnest_search_keys.keys()),
3494
- self.logger,
3495
- )
3496
- df = converter.convert(df)
3636
+ # Convert search keys and generate features on them
3497
3637
 
3498
- ip_column = self._get_ip_column(search_keys)
3499
- if ip_column:
3500
- converter = IpSearchKeyConverter(
3501
- ip_column,
3502
- search_keys,
3503
- columns_renaming,
3504
- list(unnest_search_keys.keys()),
3505
- self.bundle,
3506
- self.logger,
3638
+ email_column = self._get_email_column(search_keys)
3639
+ hem_column = self._get_hem_column(search_keys)
3640
+ if email_column:
3641
+ converter = EmailSearchKeyConverter(
3642
+ email_column,
3643
+ hem_column,
3644
+ search_keys,
3645
+ columns_renaming,
3646
+ list(unnest_search_keys.keys()),
3647
+ self.logger,
3648
+ )
3649
+ work_df = converter.convert(work_df)
3650
+
3651
+ ip_column = self._get_ip_column(search_keys)
3652
+ if ip_column:
3653
+ converter = IpSearchKeyConverter(
3654
+ ip_column,
3655
+ search_keys,
3656
+ columns_renaming,
3657
+ list(unnest_search_keys.keys()),
3658
+ self.bundle,
3659
+ self.logger,
3660
+ )
3661
+ work_df = converter.convert(work_df)
3662
+
3663
+ date_features = []
3664
+ for col in features_for_transform:
3665
+ if DateTimeConverter(col).is_datetime(work_df):
3666
+ work_df[col] = DateTimeConverter(col).to_date_string(work_df)
3667
+ date_features.append(col)
3668
+
3669
+ meaning_types = {}
3670
+ meaning_types.update(
3671
+ {
3672
+ col: FileColumnMeaningType.FEATURE
3673
+ for col in features_for_transform
3674
+ if col not in date_features and col not in generated_features
3675
+ }
3507
3676
  )
3508
- df = converter.convert(df)
3677
+ meaning_types.update({col: FileColumnMeaningType.GENERATED_FEATURE for col in generated_features})
3678
+ meaning_types.update({col: FileColumnMeaningType.DATE_FEATURE for col in date_features})
3679
+ meaning_types.update({col: key.value for col, key in search_keys.items()})
3509
3680
 
3510
- date_features = []
3511
- for col in features_for_transform:
3512
- if DateTimeConverter(col).is_datetime(df):
3513
- df[col] = DateTimeConverter(col).to_date_string(df)
3514
- date_features.append(col)
3681
+ features_not_to_pass.extend(
3682
+ [
3683
+ c
3684
+ for c in work_df.columns
3685
+ if c not in search_keys.keys()
3686
+ and c not in features_for_transform
3687
+ and c not in [ENTITY_SYSTEM_RECORD_ID, SEARCH_KEY_UNNEST]
3688
+ ]
3689
+ )
3515
3690
 
3516
- meaning_types = {}
3517
- meaning_types.update(
3518
- {
3519
- col: FileColumnMeaningType.FEATURE
3520
- for col in features_for_transform
3521
- if col not in date_features and col not in generated_features
3522
- }
3523
- )
3524
- meaning_types.update({col: FileColumnMeaningType.GENERATED_FEATURE for col in generated_features})
3525
- meaning_types.update({col: FileColumnMeaningType.DATE_FEATURE for col in date_features})
3526
- meaning_types.update({col: key.value for col, key in search_keys.items()})
3691
+ if DateTimeConverter.DATETIME_COL in work_df.columns:
3692
+ work_df = work_df.drop(columns=DateTimeConverter.DATETIME_COL)
3527
3693
 
3528
- features_not_to_pass.extend(
3529
- [
3530
- c
3531
- for c in df.columns
3532
- if c not in search_keys.keys()
3533
- and c not in features_for_transform
3534
- and c not in [ENTITY_SYSTEM_RECORD_ID, SEARCH_KEY_UNNEST]
3535
- ]
3536
- )
3694
+ # search keys might be changed after explode
3695
+ columns_for_system_record_id = self.__transform_match_columns(search_keys, features_for_transform)
3696
+ work_df[SYSTEM_RECORD_ID] = pd.util.hash_pandas_object(
3697
+ work_df[columns_for_system_record_id], index=False
3698
+ ).astype("float64")
3699
+ meaning_types[SYSTEM_RECORD_ID] = FileColumnMeaningType.SYSTEM_RECORD_ID
3700
+ meaning_types[ENTITY_SYSTEM_RECORD_ID] = FileColumnMeaningType.ENTITY_SYSTEM_RECORD_ID
3701
+ if SEARCH_KEY_UNNEST in work_df.columns:
3702
+ meaning_types[SEARCH_KEY_UNNEST] = FileColumnMeaningType.UNNEST_KEY
3537
3703
 
3538
- if DateTimeConverter.DATETIME_COL in df.columns:
3539
- df = df.drop(columns=DateTimeConverter.DATETIME_COL)
3704
+ work_df = work_df.reset_index(drop=True)
3540
3705
 
3541
- # search keys might be changed after explode
3542
- columns_for_system_record_id = sorted(list(search_keys.keys()) + features_for_transform)
3543
- df[SYSTEM_RECORD_ID] = pd.util.hash_pandas_object(df[columns_for_system_record_id], index=False).astype(
3544
- "float64"
3545
- )
3546
- meaning_types[SYSTEM_RECORD_ID] = FileColumnMeaningType.SYSTEM_RECORD_ID
3547
- meaning_types[ENTITY_SYSTEM_RECORD_ID] = FileColumnMeaningType.ENTITY_SYSTEM_RECORD_ID
3548
- if SEARCH_KEY_UNNEST in df.columns:
3549
- meaning_types[SEARCH_KEY_UNNEST] = FileColumnMeaningType.UNNEST_KEY
3706
+ combined_search_keys = combine_search_keys(search_keys.keys())
3550
3707
 
3551
- df = df.reset_index(drop=True)
3708
+ df_without_features = work_df.drop(columns=features_not_to_pass, errors="ignore")
3552
3709
 
3553
- combined_search_keys = combine_search_keys(search_keys.keys())
3710
+ df_without_features, full_duplicates_warning = clean_full_duplicates(
3711
+ df_without_features, is_transform=True, logger=self.logger, bundle=self.bundle
3712
+ )
3713
+ if not silent_mode and full_duplicates_warning:
3714
+ self.__log_warning(full_duplicates_warning)
3554
3715
 
3555
- df_without_features = df.drop(columns=features_not_to_pass, errors="ignore")
3716
+ del work_df
3717
+ gc.collect()
3556
3718
 
3557
- df_without_features, full_duplicates_warning = clean_full_duplicates(
3558
- df_without_features, is_transform=True, logger=self.logger, bundle=self.bundle
3559
- )
3560
- if not silent_mode and full_duplicates_warning:
3561
- self.__log_warning(full_duplicates_warning)
3719
+ def invoke_validation(validation_df: pd.DataFrame):
3720
+
3721
+ dataset = Dataset(
3722
+ "sample_" + str(uuid.uuid4()),
3723
+ df=validation_df,
3724
+ meaning_types=meaning_types,
3725
+ search_keys=combined_search_keys,
3726
+ unnest_search_keys=unnest_search_keys,
3727
+ id_columns=self.__get_renamed_id_columns(columns_renaming),
3728
+ date_column=self._get_date_column(search_keys),
3729
+ date_format=self.date_format,
3730
+ sample_config=self.sample_config,
3731
+ rest_client=self.rest_client,
3732
+ logger=self.logger,
3733
+ bundle=self.bundle,
3734
+ warning_callback=self.__log_warning,
3735
+ )
3736
+ dataset.columns_renaming = columns_renaming
3562
3737
 
3563
- del df
3564
- gc.collect()
3738
+ validation_task = self._search_task.validation(
3739
+ self._get_trace_id(),
3740
+ dataset,
3741
+ start_time=start_time,
3742
+ extract_features=True,
3743
+ runtime_parameters=runtime_parameters,
3744
+ exclude_features_sources=exclude_features_sources,
3745
+ metrics_calculation=metrics_calculation,
3746
+ silent_mode=silent_mode,
3747
+ progress_bar=progress_bar,
3748
+ progress_callback=progress_callback,
3749
+ )
3565
3750
 
3566
- def invoke_validation(df: pd.DataFrame):
3567
-
3568
- dataset = Dataset(
3569
- "sample_" + str(uuid.uuid4()),
3570
- df=df,
3571
- meaning_types=meaning_types,
3572
- search_keys=combined_search_keys,
3573
- unnest_search_keys=unnest_search_keys,
3574
- id_columns=self.__get_renamed_id_columns(columns_renaming),
3575
- date_column=self._get_date_column(search_keys),
3576
- date_format=self.date_format,
3577
- sample_config=self.sample_config,
3578
- rest_client=self.rest_client,
3579
- logger=self.logger,
3580
- bundle=self.bundle,
3581
- warning_callback=self.__log_warning,
3582
- )
3583
- dataset.columns_renaming = columns_renaming
3584
-
3585
- validation_task = self._search_task.validation(
3586
- self._get_trace_id(),
3587
- dataset,
3588
- start_time=start_time,
3589
- extract_features=True,
3590
- runtime_parameters=runtime_parameters,
3591
- exclude_features_sources=exclude_features_sources,
3592
- metrics_calculation=metrics_calculation,
3593
- silent_mode=silent_mode,
3594
- progress_bar=progress_bar,
3595
- progress_callback=progress_callback,
3596
- )
3751
+ del validation_df, dataset
3752
+ gc.collect()
3597
3753
 
3598
- del df, dataset
3599
- gc.collect()
3754
+ if not silent_mode:
3755
+ print(self.bundle.get("polling_transform_task").format(validation_task.search_task_id))
3756
+ if not self.__is_registered:
3757
+ print(self.bundle.get("polling_unregister_information"))
3600
3758
 
3601
- if not silent_mode:
3602
- print(self.bundle.get("polling_transform_task").format(validation_task.search_task_id))
3603
- if not self.__is_registered:
3604
- print(self.bundle.get("polling_unregister_information"))
3759
+ progress = self.get_progress(validation_task)
3760
+ progress.recalculate_eta(time.time() - start_time)
3761
+ if progress_bar is not None:
3762
+ progress_bar.progress = progress.to_progress_bar()
3763
+ if progress_callback is not None:
3764
+ progress_callback(progress)
3765
+ prev_progress: SearchProgress | None = None
3766
+ polling_period_seconds = 1
3767
+ try:
3768
+ while progress.stage != ProgressStage.DOWNLOADING.value:
3769
+ if prev_progress is None or prev_progress.percent != progress.percent:
3770
+ progress.recalculate_eta(time.time() - start_time)
3771
+ else:
3772
+ progress.update_eta(prev_progress.eta - polling_period_seconds)
3773
+ prev_progress = progress
3774
+ if progress_bar is not None:
3775
+ progress_bar.progress = progress.to_progress_bar()
3776
+ if progress_callback is not None:
3777
+ progress_callback(progress)
3778
+ if progress.stage == ProgressStage.FAILED.value:
3779
+ raise Exception(progress.error_message)
3780
+ time.sleep(polling_period_seconds)
3781
+ progress = self.get_progress(validation_task)
3782
+ except KeyboardInterrupt as e:
3783
+ print(self.bundle.get("search_stopping"))
3784
+ self.rest_client.stop_search_task_v2(self._get_trace_id(), validation_task.search_task_id)
3785
+ self.logger.warning(f"Search {validation_task.search_task_id} stopped by user")
3786
+ print(self.bundle.get("search_stopped"))
3787
+ raise e
3605
3788
 
3606
- progress = self.get_progress(validation_task)
3607
- progress.recalculate_eta(time.time() - start_time)
3608
- if progress_bar is not None:
3609
- progress_bar.progress = progress.to_progress_bar()
3610
- if progress_callback is not None:
3611
- progress_callback(progress)
3612
- prev_progress: SearchProgress | None = None
3613
- polling_period_seconds = 1
3614
- try:
3615
- while progress.stage != ProgressStage.DOWNLOADING.value:
3616
- if prev_progress is None or prev_progress.percent != progress.percent:
3617
- progress.recalculate_eta(time.time() - start_time)
3618
- else:
3619
- progress.update_eta(prev_progress.eta - polling_period_seconds)
3620
- prev_progress = progress
3621
- if progress_bar is not None:
3622
- progress_bar.progress = progress.to_progress_bar()
3623
- if progress_callback is not None:
3624
- progress_callback(progress)
3625
- if progress.stage == ProgressStage.FAILED.value:
3626
- raise Exception(progress.error_message)
3627
- time.sleep(polling_period_seconds)
3628
- progress = self.get_progress(validation_task)
3629
- except KeyboardInterrupt as e:
3630
- print(self.bundle.get("search_stopping"))
3631
- self.rest_client.stop_search_task_v2(self._get_trace_id(), validation_task.search_task_id)
3632
- self.logger.warning(f"Search {validation_task.search_task_id} stopped by user")
3633
- print(self.bundle.get("search_stopped"))
3634
- raise e
3789
+ validation_task.poll_result(self._get_trace_id(), quiet=True)
3635
3790
 
3636
- validation_task.poll_result(self._get_trace_id(), quiet=True)
3791
+ seconds_left = time.time() - start_time
3792
+ progress = SearchProgress(97.0, ProgressStage.DOWNLOADING, seconds_left)
3793
+ if progress_bar is not None:
3794
+ progress_bar.progress = progress.to_progress_bar()
3795
+ if progress_callback is not None:
3796
+ progress_callback(progress)
3637
3797
 
3638
- seconds_left = time.time() - start_time
3639
- progress = SearchProgress(97.0, ProgressStage.DOWNLOADING, seconds_left)
3640
- if progress_bar is not None:
3641
- progress_bar.progress = progress.to_progress_bar()
3642
- if progress_callback is not None:
3643
- progress_callback(progress)
3798
+ if not silent_mode:
3799
+ print(self.bundle.get("transform_start"))
3644
3800
 
3645
- if not silent_mode:
3646
- print(self.bundle.get("transform_start"))
3801
+ return validation_task.get_all_validation_raw_features(self._get_trace_id(), metrics_calculation)
3647
3802
 
3648
- return validation_task.get_all_validation_raw_features(self._get_trace_id(), metrics_calculation)
3803
+ if len(df_without_features) <= Dataset.MAX_ROWS:
3804
+ result_features = invoke_validation(df_without_features)
3805
+ else:
3806
+ self.logger.warning(
3807
+ f"Dataset has more than {Dataset.MAX_ROWS} rows: {len(df_without_features)}, "
3808
+ f"splitting into chunks of {Dataset.MAX_ROWS} rows"
3809
+ )
3810
+ result_features_list = []
3649
3811
 
3650
- if len(df_without_features) <= Dataset.MAX_ROWS:
3651
- result_features = invoke_validation(df_without_features)
3652
- else:
3653
- self.logger.warning(
3654
- f"Dataset has more than {Dataset.MAX_ROWS} rows: {len(df_without_features)}, "
3655
- f"splitting into chunks of {Dataset.MAX_ROWS} rows"
3656
- )
3657
- result_features_list = []
3812
+ for i in range(0, len(df_without_features), Dataset.MAX_ROWS):
3813
+ chunk = df_without_features.iloc[i : i + Dataset.MAX_ROWS]
3814
+ result_features_list.append(invoke_validation(chunk))
3815
+ result_features = pd.concat(result_features_list)
3658
3816
 
3659
- for i in range(0, len(df_without_features), Dataset.MAX_ROWS):
3660
- chunk = df_without_features.iloc[i : i + Dataset.MAX_ROWS]
3661
- result_features_list.append(invoke_validation(chunk))
3662
- result_features = pd.concat(result_features_list)
3817
+ del df
3818
+ gc.collect()
3663
3819
 
3664
- # Prepare input DataFrame for __enrich by concatenating generated ids and client features
3665
- df_before_explode = df_before_explode.rename(columns=columns_renaming)
3820
+ # Attach generated ids without a second full-width copy of validated_Xy.
3821
+ df_before_explode.rename(columns=columns_renaming, inplace=True)
3666
3822
  generated_features = [columns_renaming.get(c, c) for c in generated_features]
3667
- combined_df = pd.concat(
3668
- [
3669
- validated_Xy.reset_index(drop=True),
3670
- df_before_explode.reset_index(drop=True),
3671
- ],
3672
- axis=1,
3673
- ).set_index(validated_Xy.index)
3674
-
3675
- result = self.__enrich(
3676
- combined_df,
3677
- result_features,
3678
- how="left",
3679
- )
3823
+ combined_df = validated_Xy.copy()
3824
+ for col in df_before_explode.columns:
3825
+ combined_df[col] = df_before_explode[col].to_numpy()
3826
+ del df_before_explode
3827
+
3828
+ unmatched_part = None
3829
+ if unmatched_count > 0:
3830
+ unmatched_part = self.__enrich(
3831
+ combined_df.iloc[unmatched_mask],
3832
+ result_features,
3833
+ how="left",
3834
+ )
3835
+
3836
+ if unmatched_mask.all() and unmatched_part is not None:
3837
+ result = unmatched_part
3838
+ else:
3839
+ base_cols = set(combined_df.columns)
3840
+ if reused_count > 0 and lookup is not None:
3841
+ self.__attach_fit_lookup(combined_df, lookup, exclude_features_sources)
3842
+ if unmatched_part is not None:
3843
+ ads_cols = [c for c in unmatched_part.columns if c not in base_cols]
3844
+ pos = np.flatnonzero(unmatched_mask)
3845
+ for col in ads_cols:
3846
+ if col not in combined_df.columns:
3847
+ combined_df[col] = np.nan
3848
+ combined_df.iloc[pos, combined_df.columns.get_loc(col)] = unmatched_part[col].to_numpy()
3849
+ result = combined_df
3850
+ result.index = validated_Xy.index
3851
+ result.index.name = validated_Xy.index.name
3680
3852
 
3681
3853
  return self.__finalize_transform_result(
3682
3854
  result,
@@ -1 +0,0 @@
1
- __version__ = "1.2.165.a4244.dev2"