workbench 0.8.202__py3-none-any.whl → 0.8.220__py3-none-any.whl

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.

Potentially problematic release.


This version of workbench might be problematic. Click here for more details.

Files changed (84) hide show
  1. workbench/algorithms/dataframe/compound_dataset_overlap.py +321 -0
  2. workbench/algorithms/dataframe/feature_space_proximity.py +168 -75
  3. workbench/algorithms/dataframe/fingerprint_proximity.py +421 -85
  4. workbench/algorithms/dataframe/projection_2d.py +44 -21
  5. workbench/algorithms/dataframe/proximity.py +78 -150
  6. workbench/algorithms/graph/light/proximity_graph.py +5 -5
  7. workbench/algorithms/models/cleanlab_model.py +382 -0
  8. workbench/algorithms/models/noise_model.py +388 -0
  9. workbench/algorithms/sql/outliers.py +3 -3
  10. workbench/api/__init__.py +3 -0
  11. workbench/api/df_store.py +17 -108
  12. workbench/api/endpoint.py +13 -11
  13. workbench/api/feature_set.py +111 -8
  14. workbench/api/meta_model.py +289 -0
  15. workbench/api/model.py +45 -12
  16. workbench/api/parameter_store.py +3 -52
  17. workbench/cached/cached_model.py +4 -4
  18. workbench/core/artifacts/artifact.py +5 -5
  19. workbench/core/artifacts/df_store_core.py +114 -0
  20. workbench/core/artifacts/endpoint_core.py +228 -237
  21. workbench/core/artifacts/feature_set_core.py +185 -230
  22. workbench/core/artifacts/model_core.py +34 -26
  23. workbench/core/artifacts/parameter_store_core.py +98 -0
  24. workbench/core/pipelines/pipeline_executor.py +1 -1
  25. workbench/core/transforms/features_to_model/features_to_model.py +22 -10
  26. workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +41 -10
  27. workbench/core/transforms/pandas_transforms/pandas_to_features.py +11 -2
  28. workbench/model_script_utils/model_script_utils.py +339 -0
  29. workbench/model_script_utils/pytorch_utils.py +405 -0
  30. workbench/model_script_utils/uq_harness.py +278 -0
  31. workbench/model_scripts/chemprop/chemprop.template +428 -631
  32. workbench/model_scripts/chemprop/generated_model_script.py +432 -635
  33. workbench/model_scripts/chemprop/model_script_utils.py +339 -0
  34. workbench/model_scripts/chemprop/requirements.txt +2 -10
  35. workbench/model_scripts/custom_models/chem_info/fingerprints.py +87 -46
  36. workbench/model_scripts/custom_models/proximity/feature_space_proximity.py +194 -0
  37. workbench/model_scripts/custom_models/proximity/feature_space_proximity.template +6 -6
  38. workbench/model_scripts/custom_models/uq_models/feature_space_proximity.py +194 -0
  39. workbench/model_scripts/meta_model/generated_model_script.py +209 -0
  40. workbench/model_scripts/meta_model/meta_model.template +209 -0
  41. workbench/model_scripts/pytorch_model/generated_model_script.py +374 -613
  42. workbench/model_scripts/pytorch_model/model_script_utils.py +339 -0
  43. workbench/model_scripts/pytorch_model/pytorch.template +370 -609
  44. workbench/model_scripts/pytorch_model/pytorch_utils.py +405 -0
  45. workbench/model_scripts/pytorch_model/requirements.txt +1 -1
  46. workbench/model_scripts/pytorch_model/uq_harness.py +278 -0
  47. workbench/model_scripts/script_generation.py +6 -5
  48. workbench/model_scripts/uq_models/generated_model_script.py +65 -422
  49. workbench/model_scripts/xgb_model/generated_model_script.py +372 -395
  50. workbench/model_scripts/xgb_model/model_script_utils.py +339 -0
  51. workbench/model_scripts/xgb_model/uq_harness.py +278 -0
  52. workbench/model_scripts/xgb_model/xgb_model.template +366 -396
  53. workbench/repl/workbench_shell.py +0 -5
  54. workbench/resources/open_source_api.key +1 -1
  55. workbench/scripts/endpoint_test.py +2 -2
  56. workbench/scripts/meta_model_sim.py +35 -0
  57. workbench/scripts/training_test.py +85 -0
  58. workbench/utils/chem_utils/fingerprints.py +87 -46
  59. workbench/utils/chem_utils/projections.py +16 -6
  60. workbench/utils/chemprop_utils.py +36 -655
  61. workbench/utils/meta_model_simulator.py +499 -0
  62. workbench/utils/metrics_utils.py +256 -0
  63. workbench/utils/model_utils.py +192 -54
  64. workbench/utils/pytorch_utils.py +33 -472
  65. workbench/utils/shap_utils.py +1 -55
  66. workbench/utils/xgboost_local_crossfold.py +267 -0
  67. workbench/utils/xgboost_model_utils.py +49 -356
  68. workbench/web_interface/components/model_plot.py +7 -1
  69. workbench/web_interface/components/plugins/model_details.py +30 -68
  70. workbench/web_interface/components/plugins/scatter_plot.py +4 -8
  71. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/METADATA +6 -5
  72. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/RECORD +76 -60
  73. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/entry_points.txt +2 -0
  74. workbench/core/cloud_platform/aws/aws_df_store.py +0 -404
  75. workbench/core/cloud_platform/aws/aws_parameter_store.py +0 -296
  76. workbench/model_scripts/custom_models/meta_endpoints/example.py +0 -53
  77. workbench/model_scripts/custom_models/proximity/proximity.py +0 -410
  78. workbench/model_scripts/custom_models/uq_models/meta_uq.template +0 -377
  79. workbench/model_scripts/custom_models/uq_models/proximity.py +0 -410
  80. workbench/model_scripts/uq_models/mapie.template +0 -605
  81. workbench/model_scripts/uq_models/requirements.txt +0 -1
  82. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/WHEEL +0 -0
  83. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/licenses/LICENSE +0 -0
  84. {workbench-0.8.202.dist-info → workbench-0.8.220.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,7 @@ from workbench.utils.aws_utils import newest_path, pull_s3_data
21
21
  from workbench.utils.s3_utils import compute_s3_object_hash
22
22
  from workbench.utils.shap_utils import shap_values_data, shap_feature_importance
23
23
  from workbench.utils.deprecated_utils import deprecated
24
- from workbench.utils.model_utils import proximity_model
24
+ from workbench.utils.model_utils import published_proximity_model, get_model_hyperparameters
25
25
 
26
26
 
27
27
  class ModelType(Enum):
@@ -44,9 +44,10 @@ class ModelFramework(Enum):
44
44
  SKLEARN = "sklearn"
45
45
  XGBOOST = "xgboost"
46
46
  LIGHTGBM = "lightgbm"
47
- PYTORCH_TABULAR = "pytorch_tabular"
47
+ PYTORCH = "pytorch"
48
48
  CHEMPROP = "chemprop"
49
49
  TRANSFORMER = "transformer"
50
+ META = "meta"
50
51
  UNKNOWN = "unknown"
51
52
 
52
53
 
@@ -62,7 +63,8 @@ class ModelImages:
62
63
  "inference": "py312-general-ml-inference",
63
64
  "pytorch_training": "py312-pytorch-training",
64
65
  "pytorch_inference": "py312-pytorch-inference",
65
- "meta-endpoint": "py312-meta-endpoint",
66
+ "meta_training": "py312-meta-training",
67
+ "meta_inference": "py312-meta-inference",
66
68
  }
67
69
 
68
70
  @classmethod
@@ -263,21 +265,25 @@ class ModelCore(Artifact):
263
265
  else:
264
266
  self.log.important(f"No inference data found for {self.model_name}!")
265
267
 
266
- def get_inference_metrics(self, capture_name: str = "latest") -> Union[pd.DataFrame, None]:
268
+ def get_inference_metrics(self, capture_name: str = "auto") -> Union[pd.DataFrame, None]:
267
269
  """Retrieve the inference performance metrics for this model
268
270
 
269
271
  Args:
270
- capture_name (str, optional): Specific capture_name or "training" (default: "latest")
272
+ capture_name (str, optional): Specific capture_name (default: "auto")
271
273
  Returns:
272
274
  pd.DataFrame: DataFrame of the Model Metrics
273
275
 
274
276
  Note:
275
- If a capture_name isn't specified this will try to return something reasonable
277
+ If a capture_name isn't specified this will try to the 'first' available metrics
276
278
  """
277
279
  # Try to get the auto_capture 'training_holdout' or the training
278
- if capture_name == "latest":
279
- metrics_df = self.get_inference_metrics("auto_inference")
280
- return metrics_df if metrics_df is not None else self.get_inference_metrics("model_training")
280
+ if capture_name == "auto":
281
+ metric_list = self.list_inference_runs()
282
+ if metric_list:
283
+ return self.get_inference_metrics(metric_list[0])
284
+ else:
285
+ self.log.warning(f"No performance metrics found for {self.model_name}!")
286
+ return None
281
287
 
282
288
  # Grab the metrics captured during model training (could return None)
283
289
  if capture_name == "model_training":
@@ -299,11 +305,11 @@ class ModelCore(Artifact):
299
305
  self.log.warning(f"Performance metrics {capture_name} not found for {self.model_name}!")
300
306
  return None
301
307
 
302
- def confusion_matrix(self, capture_name: str = "latest") -> Union[pd.DataFrame, None]:
308
+ def confusion_matrix(self, capture_name: str = "auto") -> Union[pd.DataFrame, None]:
303
309
  """Retrieve the confusion_matrix for this model
304
310
 
305
311
  Args:
306
- capture_name (str, optional): Specific capture_name or "training" (default: "latest")
312
+ capture_name (str, optional): Specific capture_name or "training" (default: "auto")
307
313
  Returns:
308
314
  pd.DataFrame: DataFrame of the Confusion Matrix (might be None)
309
315
  """
@@ -315,7 +321,7 @@ class ModelCore(Artifact):
315
321
  raise ValueError(error_msg)
316
322
 
317
323
  # Grab the metrics from the Workbench Metadata (try inference first, then training)
318
- if capture_name == "latest":
324
+ if capture_name == "auto":
319
325
  cm = self.confusion_matrix("auto_inference")
320
326
  return cm if cm is not None else self.confusion_matrix("model_training")
321
327
 
@@ -537,6 +543,17 @@ class ModelCore(Artifact):
537
543
  else:
538
544
  self.log.error(f"Model {self.model_name} is not a classifier!")
539
545
 
546
+ def summary(self) -> dict:
547
+ """Summary information about this Model
548
+
549
+ Returns:
550
+ dict: Dictionary of summary information about this Model
551
+ """
552
+ self.log.info("Computing Model Summary...")
553
+ summary = super().summary()
554
+ summary["hyperparameters"] = get_model_hyperparameters(self)
555
+ return summary
556
+
540
557
  def details(self) -> dict:
541
558
  """Additional Details about this Model
542
559
 
@@ -561,6 +578,7 @@ class ModelCore(Artifact):
561
578
  details["status"] = self.latest_model["ModelPackageStatus"]
562
579
  details["approval_status"] = self.latest_model.get("ModelApprovalStatus", "unknown")
563
580
  details["image"] = self.container_image().split("/")[-1] # Shorten the image uri
581
+ details["hyperparameters"] = get_model_hyperparameters(self)
564
582
 
565
583
  # Grab the inference and container info
566
584
  inference_spec = self.latest_model["InferenceSpecification"]
@@ -571,16 +589,6 @@ class ModelCore(Artifact):
571
589
  details["transform_types"] = inference_spec["SupportedTransformInstanceTypes"]
572
590
  details["content_types"] = inference_spec["SupportedContentTypes"]
573
591
  details["response_types"] = inference_spec["SupportedResponseMIMETypes"]
574
- details["model_metrics"] = self.get_inference_metrics()
575
- if self.model_type == ModelType.CLASSIFIER:
576
- details["confusion_matrix"] = self.confusion_matrix()
577
- details["predictions"] = None
578
- elif self.model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
579
- details["confusion_matrix"] = None
580
- details["predictions"] = self.get_inference_predictions()
581
- else:
582
- details["confusion_matrix"] = None
583
- details["predictions"] = None
584
592
 
585
593
  # Grab the inference metadata
586
594
  details["inference_meta"] = self.get_inference_metadata()
@@ -869,7 +877,7 @@ class ModelCore(Artifact):
869
877
  return self.df_store.get(f"/workbench/models/{self.name}/shap_data")
870
878
  else:
871
879
  # Loop over the SHAP data and return a dict of DataFrames
872
- shap_dfs = self.df_store.list_subfiles(f"/workbench/models/{self.name}/shap_data")
880
+ shap_dfs = self.df_store.list(f"/workbench/models/{self.name}/shap_data")
873
881
  shap_data = {}
874
882
  for df_location in shap_dfs:
875
883
  key = df_location.split("/")[-1]
@@ -888,19 +896,19 @@ class ModelCore(Artifact):
888
896
  except (KeyError, IndexError, TypeError):
889
897
  return None
890
898
 
891
- def publish_prox_model(self, prox_model_name: str = None, track_columns: list = None):
899
+ def publish_prox_model(self, prox_model_name: str = None, include_all_columns: bool = False):
892
900
  """Create and publish a Proximity Model for this Model
893
901
 
894
902
  Args:
895
903
  prox_model_name (str, optional): Name of the Proximity Model (if not specified, a name will be generated)
896
- track_columns (list, optional): List of columns to track in the Proximity Model.
904
+ include_all_columns (bool): Include all DataFrame columns in results (default: False)
897
905
 
898
906
  Returns:
899
907
  Model: The published Proximity Model
900
908
  """
901
909
  if prox_model_name is None:
902
910
  prox_model_name = self.model_name + "-prox"
903
- return proximity_model(self, prox_model_name, track_columns=track_columns)
911
+ return published_proximity_model(self, prox_model_name, include_all_columns=include_all_columns)
904
912
 
905
913
  def delete(self):
906
914
  """Delete the Model Packages and the Model Group"""
@@ -0,0 +1,98 @@
1
+ """ParameterStoreCore: Manages Workbench parameters in a Cloud Based Parameter Store."""
2
+
3
+ import logging
4
+
5
+ # Workbench Imports
6
+ from workbench.core.cloud_platform.aws.aws_account_clamp import AWSAccountClamp
7
+
8
+ # Workbench Bridges Import
9
+ from workbench_bridges.api import ParameterStore as BridgesParameterStore
10
+
11
+
12
+ class ParameterStoreCore(BridgesParameterStore):
13
+ """ParameterStoreCore: Manages Workbench parameters in a Cloud Based Parameter Store.
14
+
15
+ Common Usage:
16
+ ```python
17
+ params = ParameterStoreCore()
18
+
19
+ # List Parameters
20
+ params.list()
21
+
22
+ ['/workbench/abalone_info',
23
+ '/workbench/my_data',
24
+ '/workbench/test',
25
+ '/workbench/pipelines/my_pipeline']
26
+
27
+ # Add Key
28
+ params.upsert("key", "value")
29
+ value = params.get("key")
30
+
31
+ # Add any data (lists, dictionaries, etc..)
32
+ my_data = {"key": "value", "number": 4.2, "list": [1,2,3]}
33
+ params.upsert("my_data", my_data)
34
+
35
+ # Retrieve data
36
+ return_value = params.get("my_data")
37
+ pprint(return_value)
38
+
39
+ {'key': 'value', 'list': [1, 2, 3], 'number': 4.2}
40
+
41
+ # Delete parameters
42
+ param_store.delete("my_data")
43
+ ```
44
+ """
45
+
46
+ def __init__(self):
47
+ """ParameterStoreCore Init Method"""
48
+ session = AWSAccountClamp().boto3_session
49
+
50
+ # Initialize parent with workbench config
51
+ super().__init__(boto3_session=session)
52
+ self.log = logging.getLogger("workbench")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ """Exercise the ParameterStoreCore Class"""
57
+
58
+ # Create a ParameterStoreCore manager
59
+ param_store = ParameterStoreCore()
60
+
61
+ # List the parameters
62
+ print("Listing Parameters...")
63
+ print(param_store.list())
64
+
65
+ # Add a new parameter
66
+ param_store.upsert("/workbench/test", "value")
67
+
68
+ # Get the parameter
69
+ print(f"Getting parameter 'test': {param_store.get('/workbench/test')}")
70
+
71
+ # Add a dictionary as a parameter
72
+ sample_dict = {"key": "str_value", "awesome_value": 4.2}
73
+ param_store.upsert("/workbench/my_data", sample_dict)
74
+
75
+ # Retrieve the parameter as a dictionary
76
+ retrieved_value = param_store.get("/workbench/my_data")
77
+ print("Retrieved value:", retrieved_value)
78
+
79
+ # List the parameters
80
+ print("Listing Parameters...")
81
+ print(param_store.list())
82
+
83
+ # List the parameters with a prefix
84
+ print("Listing Parameters with prefix '/workbench':")
85
+ print(param_store.list("/workbench"))
86
+
87
+ # Delete the parameters
88
+ param_store.delete("/workbench/test")
89
+ param_store.delete("/workbench/my_data")
90
+
91
+ # Out of scope tests
92
+ param_store.upsert("test", "value")
93
+ param_store.delete("test")
94
+
95
+ # Recursive delete test
96
+ param_store.upsert("/workbench/test/test1", "value1")
97
+ param_store.upsert("/workbench/test/test2", "value2")
98
+ param_store.delete_recursive("workbench/test/")
@@ -123,7 +123,7 @@ class PipelineExecutor:
123
123
  if "model" in workbench_objects and (not subset or "endpoint" in subset):
124
124
  workbench_objects["model"].to_endpoint(**kwargs)
125
125
  endpoint = Endpoint(kwargs["name"])
126
- endpoint.auto_inference(capture=True)
126
+ endpoint.auto_inference()
127
127
 
128
128
  # Found something weird
129
129
  else:
@@ -1,6 +1,7 @@
1
1
  """FeaturesToModel: Train/Create a Model from a Feature Set"""
2
2
 
3
3
  from pathlib import Path
4
+ from typing import Union
4
5
  from sagemaker.estimator import Estimator
5
6
  import awswrangler as wr
6
7
  from datetime import datetime, timezone
@@ -83,12 +84,17 @@ class FeaturesToModel(Transform):
83
84
  self.inference_arch = inference_arch
84
85
 
85
86
  def transform_impl(
86
- self, target_column: str, description: str = None, feature_list: list = None, train_all_data=False, **kwargs
87
+ self,
88
+ target_column: Union[str, list[str]],
89
+ description: str = None,
90
+ feature_list: list = None,
91
+ train_all_data=False,
92
+ **kwargs,
87
93
  ):
88
94
  """Generic Features to Model: Note you should create a new class and inherit from
89
95
  this one to include specific logic for your Feature Set/Model
90
96
  Args:
91
- target_column (str): Column name of the target variable
97
+ target_column (str or list[str]): Column name(s) of the target variable(s)
92
98
  description (str): Description of the model (optional)
93
99
  feature_list (list[str]): A list of columns for the features (default None, will try to guess)
94
100
  train_all_data (bool): Train on ALL (100%) of the data (default False)
@@ -105,9 +111,11 @@ class FeaturesToModel(Transform):
105
111
  s3_training_path = feature_set.create_s3_training_data()
106
112
  self.log.info(f"Created new training data {s3_training_path}...")
107
113
 
108
- # Report the target column
114
+ # Report the target column(s)
109
115
  self.target_column = target_column
110
- self.log.info(f"Target column: {self.target_column}")
116
+ # Normalize target_column to a list for internal use
117
+ target_list = [target_column] if isinstance(target_column, str) else (target_column or [])
118
+ self.log.info(f"Target column(s): {self.target_column}")
111
119
 
112
120
  # Did they specify a feature list?
113
121
  if feature_list:
@@ -134,7 +142,7 @@ class FeaturesToModel(Transform):
134
142
  "is_deleted",
135
143
  "event_time",
136
144
  "training",
137
- ] + [self.target_column]
145
+ ] + target_list
138
146
  feature_list = [c for c in all_columns if c not in filter_list]
139
147
 
140
148
  # AWS Feature Store has 3 user column types (String, Integral, Fractional)
@@ -157,12 +165,14 @@ class FeaturesToModel(Transform):
157
165
  self.log.important(f"Feature List for Modeling: {self.model_feature_list}")
158
166
 
159
167
  # Set up our parameters for the model script
168
+ # ChemProp expects target_column as a list; other templates expect a string
169
+ target_for_template = target_list if self.model_framework == ModelFramework.CHEMPROP else self.target_column
160
170
  template_params = {
161
171
  "model_imports": self.model_import_str,
162
172
  "model_type": self.model_type,
163
173
  "model_framework": self.model_framework,
164
174
  "model_class": self.model_class,
165
- "target_column": self.target_column,
175
+ "target_column": target_for_template,
166
176
  "feature_list": self.model_feature_list,
167
177
  "compressed_features": feature_set.get_compressed_features(),
168
178
  "model_metrics_s3_path": self.model_training_root,
@@ -202,11 +212,13 @@ class FeaturesToModel(Transform):
202
212
  # Metric Definitions for Classification
203
213
  elif self.model_type == ModelType.CLASSIFIER:
204
214
  # We need to get creative with the Classification Metrics
215
+ # Note: Classification only supports single target
216
+ class_target = target_list[0] if target_list else self.target_column
205
217
 
206
218
  # Grab all the target column class values (class labels)
207
219
  table = feature_set.data_source.table
208
- self.class_labels = feature_set.query(f'select DISTINCT {self.target_column} FROM "{table}"')[
209
- self.target_column
220
+ self.class_labels = feature_set.query(f'select DISTINCT {class_target} FROM "{table}"')[
221
+ class_target
210
222
  ].to_list()
211
223
 
212
224
  # Sanity check on the targets
@@ -216,7 +228,7 @@ class FeaturesToModel(Transform):
216
228
  raise ValueError(msg)
217
229
 
218
230
  # Dynamically create the metric definitions
219
- metrics = ["precision", "recall", "f1"]
231
+ metrics = ["precision", "recall", "f1", "support"]
220
232
  metric_definitions = []
221
233
  for t in self.class_labels:
222
234
  for m in metrics:
@@ -242,7 +254,7 @@ class FeaturesToModel(Transform):
242
254
  image = ModelImages.get_image_uri(self.sm_session.boto_region_name, self.training_image)
243
255
 
244
256
  # Use GPU instance for ChemProp/PyTorch, CPU for others
245
- if self.model_framework in [ModelFramework.CHEMPROP, ModelFramework.PYTORCH_TABULAR]:
257
+ if self.model_framework in [ModelFramework.CHEMPROP, ModelFramework.PYTORCH]:
246
258
  train_instance_type = "ml.g6.xlarge" # NVIDIA L4 GPU, ~$0.80/hr
247
259
  self.log.important(f"Using GPU instance {train_instance_type} for {self.model_framework.value}")
248
260
  else:
@@ -1,6 +1,7 @@
1
1
  """ModelToEndpoint: Deploy an Endpoint for a Model"""
2
2
 
3
3
  import time
4
+ from botocore.exceptions import ClientError
4
5
  from sagemaker import ModelPackage
5
6
  from sagemaker.serializers import CSVSerializer
6
7
  from sagemaker.deserializers import CSVDeserializer
@@ -102,10 +103,21 @@ class ModelToEndpoint(Transform):
102
103
  # Is this a serverless deployment?
103
104
  serverless_config = None
104
105
  if self.serverless:
106
+ # For PyTorch or ChemProp we need at least 4GB of memory
107
+ from workbench.api import ModelFramework
108
+
109
+ self.log.info(f"Model Framework: {workbench_model.model_framework}")
110
+ if workbench_model.model_framework in [ModelFramework.PYTORCH, ModelFramework.CHEMPROP]:
111
+ if mem_size < 4096:
112
+ self.log.important(
113
+ f"{workbench_model.model_framework} needs at least 4GB of memory (setting to 4GB)"
114
+ )
115
+ mem_size = 4096
105
116
  serverless_config = ServerlessInferenceConfig(
106
117
  memory_size_in_mb=mem_size,
107
118
  max_concurrency=max_concurrency,
108
119
  )
120
+ self.log.important(f"Serverless Config: Memory={mem_size}MB, MaxConcurrency={max_concurrency}")
109
121
 
110
122
  # Configure data capture if requested (and not serverless)
111
123
  data_capture_config = None
@@ -126,16 +138,35 @@ class ModelToEndpoint(Transform):
126
138
 
127
139
  # Deploy the Endpoint
128
140
  self.log.important(f"Deploying the Endpoint {self.output_name}...")
129
- model_package.deploy(
130
- initial_instance_count=1,
131
- instance_type=self.instance_type,
132
- serverless_inference_config=serverless_config,
133
- endpoint_name=self.output_name,
134
- serializer=CSVSerializer(),
135
- deserializer=CSVDeserializer(),
136
- data_capture_config=data_capture_config,
137
- tags=aws_tags,
138
- )
141
+ try:
142
+ model_package.deploy(
143
+ initial_instance_count=1,
144
+ instance_type=self.instance_type,
145
+ serverless_inference_config=serverless_config,
146
+ endpoint_name=self.output_name,
147
+ serializer=CSVSerializer(),
148
+ deserializer=CSVDeserializer(),
149
+ data_capture_config=data_capture_config,
150
+ tags=aws_tags,
151
+ )
152
+ except ClientError as e:
153
+ # Check if this is the "endpoint config already exists" error
154
+ if "Cannot create already existing endpoint configuration" in str(e):
155
+ self.log.warning("Endpoint config already exists, deleting and retrying...")
156
+ self.sm_client.delete_endpoint_config(EndpointConfigName=self.output_name)
157
+ # Retry the deploy
158
+ model_package.deploy(
159
+ initial_instance_count=1,
160
+ instance_type=self.instance_type,
161
+ serverless_inference_config=serverless_config,
162
+ endpoint_name=self.output_name,
163
+ serializer=CSVSerializer(),
164
+ deserializer=CSVDeserializer(),
165
+ data_capture_config=data_capture_config,
166
+ tags=aws_tags,
167
+ )
168
+ else:
169
+ raise
139
170
 
140
171
  def post_transform(self, **kwargs):
141
172
  """Post-Transform: Calling onboard() for the Endpoint"""
@@ -68,6 +68,15 @@ class PandasToFeatures(Transform):
68
68
  self.output_df = input_df.copy()
69
69
  self.one_hot_columns = one_hot_columns or []
70
70
 
71
+ # Warn about known AWS Iceberg bug with event_time_column
72
+ if event_time_column is not None:
73
+ self.log.warning(
74
+ f"event_time_column='{event_time_column}' specified. Note: AWS has a known bug with "
75
+ "Iceberg FeatureGroups where varying event times across multiple days can cause "
76
+ "duplicate rows in the offline store. Setting event_time_column=None."
77
+ )
78
+ self.event_time_column = None
79
+
71
80
  # Now Prepare the DataFrame for its journey into an AWS FeatureGroup
72
81
  self.prep_dataframe()
73
82
 
@@ -400,7 +409,7 @@ class PandasToFeatures(Transform):
400
409
 
401
410
  # Set Hold Out Ids (if we got them during creation)
402
411
  if self.incoming_hold_out_ids:
403
- self.output_feature_set.set_training_holdouts(self.id_column, self.incoming_hold_out_ids)
412
+ self.output_feature_set.set_training_holdouts(self.incoming_hold_out_ids)
404
413
 
405
414
  def ensure_feature_group_created(self, feature_group):
406
415
  status = feature_group.describe().get("FeatureGroupStatus")
@@ -462,7 +471,7 @@ if __name__ == "__main__":
462
471
 
463
472
  # Create my DF to Feature Set Transform (with one-hot encoding)
464
473
  df_to_features = PandasToFeatures("test_features")
465
- df_to_features.set_input(data_df, id_column="id", one_hot_columns=["food"])
474
+ df_to_features.set_input(data_df, id_column="id", event_time_column="date", one_hot_columns=["food"])
466
475
  df_to_features.set_output_tags(["test", "small"])
467
476
  df_to_features.transform()
468
477