workbench 0.8.201__py3-none-any.whl → 0.8.204__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.
Files changed (35) hide show
  1. workbench/api/df_store.py +17 -108
  2. workbench/api/feature_set.py +41 -7
  3. workbench/api/parameter_store.py +3 -52
  4. workbench/core/artifacts/artifact.py +5 -5
  5. workbench/core/artifacts/df_store_core.py +114 -0
  6. workbench/core/artifacts/endpoint_core.py +184 -75
  7. workbench/core/artifacts/model_core.py +11 -7
  8. workbench/core/artifacts/parameter_store_core.py +98 -0
  9. workbench/core/transforms/features_to_model/features_to_model.py +27 -13
  10. workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +11 -0
  11. workbench/core/transforms/pandas_transforms/pandas_to_features.py +11 -2
  12. workbench/model_scripts/chemprop/chemprop.template +312 -293
  13. workbench/model_scripts/chemprop/generated_model_script.py +316 -297
  14. workbench/model_scripts/custom_models/uq_models/ensemble_xgb.template +11 -5
  15. workbench/model_scripts/custom_models/uq_models/meta_uq.template +11 -5
  16. workbench/model_scripts/custom_models/uq_models/ngboost.template +11 -5
  17. workbench/model_scripts/ensemble_xgb/ensemble_xgb.template +11 -5
  18. workbench/model_scripts/pytorch_model/generated_model_script.py +278 -128
  19. workbench/model_scripts/pytorch_model/pytorch.template +273 -123
  20. workbench/model_scripts/uq_models/generated_model_script.py +20 -11
  21. workbench/model_scripts/uq_models/mapie.template +17 -8
  22. workbench/model_scripts/xgb_model/generated_model_script.py +38 -9
  23. workbench/model_scripts/xgb_model/xgb_model.template +34 -5
  24. workbench/resources/open_source_api.key +1 -1
  25. workbench/utils/chemprop_utils.py +38 -1
  26. workbench/utils/pytorch_utils.py +38 -8
  27. workbench/web_interface/components/model_plot.py +7 -1
  28. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/METADATA +2 -2
  29. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/RECORD +33 -33
  30. workbench/core/cloud_platform/aws/aws_df_store.py +0 -404
  31. workbench/core/cloud_platform/aws/aws_parameter_store.py +0 -296
  32. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/WHEEL +0 -0
  33. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/entry_points.txt +0 -0
  34. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/licenses/LICENSE +0 -0
  35. {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/top_level.txt +0 -0
@@ -36,8 +36,8 @@ from workbench.utils.cache import Cache
36
36
  from workbench.utils.s3_utils import compute_s3_object_hash
37
37
  from workbench.utils.model_utils import uq_metrics
38
38
  from workbench.utils.xgboost_model_utils import cross_fold_inference as xgboost_cross_fold
39
- from workbench.utils.pytorch_utils import cross_fold_inference as pytorch_cross_fold
40
- from workbench.utils.chemprop_utils import cross_fold_inference as chemprop_cross_fold
39
+ from workbench.utils.pytorch_utils import pull_cv_results as pytorch_pull_cv
40
+ from workbench.utils.chemprop_utils import pull_cv_results as chemprop_pull_cv
41
41
  from workbench_bridges.endpoints.fast_inference import fast_inference
42
42
 
43
43
 
@@ -389,7 +389,7 @@ class EndpointCore(Artifact):
389
389
  # Grab the model features and target column
390
390
  model = ModelCore(self.model_name)
391
391
  features = model.features()
392
- target_column = model.target()
392
+ targets = model.target() # Note: We have multi-target models (so this could be a list)
393
393
 
394
394
  # Run predictions on the evaluation data
395
395
  prediction_df = self._predict(eval_df, features, drop_error_rows)
@@ -397,19 +397,26 @@ class EndpointCore(Artifact):
397
397
  self.log.warning("No predictions were made. Returning empty DataFrame.")
398
398
  return prediction_df
399
399
 
400
+ # FIXME: Multi-target support - currently uses first target for metrics
401
+ # Normalize targets to handle both string and list formats
402
+ if isinstance(targets, list):
403
+ primary_target = targets[0] if targets else None
404
+ else:
405
+ primary_target = targets
406
+
400
407
  # Sanity Check that the target column is present
401
- if target_column and (target_column not in prediction_df.columns):
402
- self.log.important(f"Target Column {target_column} not found in prediction_df!")
408
+ if primary_target and (primary_target not in prediction_df.columns):
409
+ self.log.important(f"Target Column {primary_target} not found in prediction_df!")
403
410
  self.log.important("In order to compute metrics, the target column must be present!")
404
411
  metrics = pd.DataFrame()
405
412
 
406
413
  # Compute the standard performance metrics for this model
407
414
  else:
408
415
  if model.model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
409
- prediction_df = self.residuals(target_column, prediction_df)
410
- metrics = self.regression_metrics(target_column, prediction_df)
416
+ prediction_df = self.residuals(primary_target, prediction_df)
417
+ metrics = self.regression_metrics(primary_target, prediction_df)
411
418
  elif model.model_type == ModelType.CLASSIFIER:
412
- metrics = self.classification_metrics(target_column, prediction_df)
419
+ metrics = self.classification_metrics(primary_target, prediction_df)
413
420
  else:
414
421
  # For other model types, we don't compute metrics
415
422
  self.log.info(f"Model Type: {model.model_type} doesn't have metrics...")
@@ -426,14 +433,47 @@ class EndpointCore(Artifact):
426
433
  if id_column is None:
427
434
  fs = FeatureSetCore(model.get_input())
428
435
  id_column = fs.id_column
429
- description = capture_name.replace("_", " ").title()
430
- self._capture_inference_results(
431
- capture_name, prediction_df, target_column, model.model_type, metrics, description, features, id_column
432
- )
436
+
437
+ # Normalize targets to a list for iteration
438
+ target_list = targets if isinstance(targets, list) else [targets]
439
+
440
+ # For multi-target models, use target-specific capture names (e.g., auto_target1, auto_target2)
441
+ # For single-target models, use the original capture name for backward compatibility
442
+ for target in target_list:
443
+ # Determine capture name: use prefix for multi-target, original name for single-target
444
+ if len(target_list) > 1:
445
+ prefix = "auto" if "auto" in capture_name else capture_name
446
+ target_capture_name = f"{prefix}_{target}"
447
+ else:
448
+ target_capture_name = capture_name
449
+
450
+ description = target_capture_name.replace("_", " ").title()
451
+
452
+ # Drop rows with NaN target values for metrics/plots
453
+ target_df = prediction_df.dropna(subset=[target])
454
+
455
+ # Compute per-target metrics
456
+ if model.model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
457
+ target_metrics = self.regression_metrics(target, target_df)
458
+ elif model.model_type == ModelType.CLASSIFIER:
459
+ target_metrics = self.classification_metrics(target, target_df)
460
+ else:
461
+ target_metrics = pd.DataFrame()
462
+
463
+ self._capture_inference_results(
464
+ target_capture_name,
465
+ target_df,
466
+ target,
467
+ model.model_type,
468
+ target_metrics,
469
+ description,
470
+ features,
471
+ id_column,
472
+ )
433
473
 
434
474
  # For UQ Models we also capture the uncertainty metrics
435
475
  if model.model_type in [ModelType.UQ_REGRESSOR]:
436
- metrics = uq_metrics(prediction_df, target_column)
476
+ metrics = uq_metrics(prediction_df, primary_target)
437
477
  self.param_store.upsert(f"/workbench/models/{model.name}/inference/{capture_name}", metrics)
438
478
 
439
479
  # Return the prediction DataFrame
@@ -453,12 +493,13 @@ class EndpointCore(Artifact):
453
493
  model = ModelCore(self.model_name)
454
494
 
455
495
  # Compute CrossFold (Metrics and Prediction Dataframe)
496
+ # For PyTorch and ChemProp, pull pre-computed CV results from training
456
497
  if model.model_framework in [ModelFramework.UNKNOWN, ModelFramework.XGBOOST]:
457
498
  cross_fold_metrics, out_of_fold_df = xgboost_cross_fold(model, nfolds=nfolds)
458
499
  elif model.model_framework == ModelFramework.PYTORCH_TABULAR:
459
- cross_fold_metrics, out_of_fold_df = pytorch_cross_fold(model, nfolds=nfolds)
500
+ cross_fold_metrics, out_of_fold_df = pytorch_pull_cv(model)
460
501
  elif model.model_framework == ModelFramework.CHEMPROP:
461
- cross_fold_metrics, out_of_fold_df = chemprop_cross_fold(model, nfolds=nfolds)
502
+ cross_fold_metrics, out_of_fold_df = chemprop_pull_cv(model)
462
503
  else:
463
504
  self.log.error(f"Cross-Fold Inference not supported for Model Framework: {model.model_framework}.")
464
505
  return pd.DataFrame()
@@ -475,9 +516,7 @@ class EndpointCore(Artifact):
475
516
  return out_of_fold_df
476
517
 
477
518
  # Capture the results
478
- capture_name = "full_cross_fold"
479
- description = capture_name.replace("_", " ").title()
480
- target_column = model.target()
519
+ targets = model.target() # Note: We have multi-target models (so this could be a list)
481
520
  model_type = model.model_type
482
521
 
483
522
  # Get the id_column from the model's FeatureSet
@@ -486,7 +525,7 @@ class EndpointCore(Artifact):
486
525
 
487
526
  # Is this a UQ Model? If so, run full inference and merge the results
488
527
  additional_columns = []
489
- if model_type == ModelType.UQ_REGRESSOR:
528
+ if model.model_framework == ModelFramework.XGBOOST and model_type == ModelType.UQ_REGRESSOR:
490
529
  self.log.important("UQ Regressor detected, running full inference to get uncertainty estimates...")
491
530
 
492
531
  # Get the training view dataframe for inference
@@ -495,9 +534,11 @@ class EndpointCore(Artifact):
495
534
  # Run inference on the endpoint to get UQ outputs
496
535
  uq_df = self.inference(training_df)
497
536
 
498
- # Identify UQ-specific columns (quantiles and prediction_std)
537
+ # Identify UQ-specific columns (quantiles, prediction_std, *_pred_std)
499
538
  uq_columns = [
500
- col for col in uq_df.columns if col.startswith("q_") or col == "prediction_std" or col == "confidence"
539
+ col
540
+ for col in uq_df.columns
541
+ if col.startswith("q_") or col == "prediction_std" or col.endswith("_pred_std") or col == "confidence"
501
542
  ]
502
543
 
503
544
  # Merge UQ columns with out-of-fold predictions
@@ -513,20 +554,42 @@ class EndpointCore(Artifact):
513
554
  additional_columns = uq_columns
514
555
  self.log.info(f"Added UQ columns: {', '.join(additional_columns)}")
515
556
 
516
- # Also compute UQ metrics
517
- metrics = uq_metrics(out_of_fold_df, target_column)
518
- self.param_store.upsert(f"/workbench/models/{model.name}/inference/{capture_name}", metrics)
557
+ # Also compute UQ metrics (use first target for multi-target models)
558
+ primary_target = targets[0] if isinstance(targets, list) else targets
559
+ metrics = uq_metrics(out_of_fold_df, primary_target)
560
+ self.param_store.upsert(f"/workbench/models/{model.name}/inference/full_cross_fold", metrics)
561
+
562
+ # Normalize targets to a list for iteration
563
+ target_list = targets if isinstance(targets, list) else [targets]
564
+
565
+ # For multi-target models, use target-specific capture names (e.g., cv_target1, cv_target2)
566
+ # For single-target models, use "full_cross_fold" for backward compatibility
567
+ for target in target_list:
568
+ capture_name = f"cv_{target}"
569
+ description = capture_name.replace("_", " ").title()
570
+
571
+ # Drop rows with NaN target values for metrics/plots
572
+ target_df = out_of_fold_df.dropna(subset=[target])
573
+
574
+ # Compute per-target metrics
575
+ if model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
576
+ target_metrics = self.regression_metrics(target, target_df)
577
+ elif model_type == ModelType.CLASSIFIER:
578
+ target_metrics = self.classification_metrics(target, target_df)
579
+ else:
580
+ target_metrics = pd.DataFrame()
581
+
582
+ self._capture_inference_results(
583
+ capture_name,
584
+ target_df,
585
+ target,
586
+ model_type,
587
+ target_metrics,
588
+ description,
589
+ features=additional_columns,
590
+ id_column=id_column,
591
+ )
519
592
 
520
- self._capture_inference_results(
521
- capture_name,
522
- out_of_fold_df,
523
- target_column,
524
- model_type,
525
- cross_fold_metrics,
526
- description,
527
- features=additional_columns,
528
- id_column=id_column,
529
- )
530
593
  return out_of_fold_df
531
594
 
532
595
  def fast_inference(self, eval_df: pd.DataFrame, threads: int = 4) -> pd.DataFrame:
@@ -736,19 +799,19 @@ class EndpointCore(Artifact):
736
799
  self,
737
800
  capture_name: str,
738
801
  pred_results_df: pd.DataFrame,
739
- target_column: str,
802
+ target: str,
740
803
  model_type: ModelType,
741
804
  metrics: pd.DataFrame,
742
805
  description: str,
743
806
  features: list,
744
807
  id_column: str = None,
745
808
  ):
746
- """Internal: Capture the inference results and metrics to S3
809
+ """Internal: Capture the inference results and metrics to S3 for a single target
747
810
 
748
811
  Args:
749
812
  capture_name (str): Name of the inference capture
750
813
  pred_results_df (pd.DataFrame): DataFrame with the prediction results
751
- target_column (str): Name of the target column
814
+ target (str): Target column name
752
815
  model_type (ModelType): Type of the model (e.g. REGRESSOR, CLASSIFIER)
753
816
  metrics (pd.DataFrame): DataFrame with the performance metrics
754
817
  description (str): Description of the inference results
@@ -779,26 +842,12 @@ class EndpointCore(Artifact):
779
842
  self.log.info(f"Writing metrics to {inference_capture_path}/inference_metrics.csv")
780
843
  wr.s3.to_csv(metrics, f"{inference_capture_path}/inference_metrics.csv", index=False)
781
844
 
782
- # Grab the ID column and target column if they are present
783
- output_columns = []
784
- if id_column and id_column in pred_results_df.columns:
785
- output_columns.append(id_column)
786
- if target_column in pred_results_df.columns:
787
- output_columns.append(target_column)
788
-
789
- # Grab the prediction column, any _proba columns, and UQ columns
790
- output_columns += [col for col in pred_results_df.columns if "prediction" in col]
791
- output_columns += [col for col in pred_results_df.columns if col.endswith("_proba")]
792
- output_columns += [col for col in pred_results_df.columns if col.startswith("q_") or col == "confidence"]
793
-
794
- # Write the predictions to our S3 Model Inference Folder
795
- self.log.info(f"Writing predictions to {inference_capture_path}/inference_predictions.csv")
796
- subset_df = pred_results_df[output_columns]
797
- wr.s3.to_csv(subset_df, f"{inference_capture_path}/inference_predictions.csv", index=False)
845
+ # Save the inference predictions for this target
846
+ self._save_target_inference(inference_capture_path, pred_results_df, target, id_column)
798
847
 
799
848
  # CLASSIFIER: Write the confusion matrix to our S3 Model Inference Folder
800
849
  if model_type == ModelType.CLASSIFIER:
801
- conf_mtx = self.generate_confusion_matrix(target_column, pred_results_df)
850
+ conf_mtx = self.generate_confusion_matrix(target, pred_results_df)
802
851
  self.log.info(f"Writing confusion matrix to {inference_capture_path}/inference_cm.csv")
803
852
  # Note: Unlike other dataframes here, we want to write the index (labels) to the CSV
804
853
  wr.s3.to_csv(conf_mtx, f"{inference_capture_path}/inference_cm.csv", index=True)
@@ -808,6 +857,57 @@ class EndpointCore(Artifact):
808
857
  model = ModelCore(self.model_name)
809
858
  model._load_inference_metrics(capture_name)
810
859
 
860
+ def _save_target_inference(
861
+ self,
862
+ inference_capture_path: str,
863
+ pred_results_df: pd.DataFrame,
864
+ target: str,
865
+ id_column: str = None,
866
+ ):
867
+ """Save inference results for a single target.
868
+
869
+ Args:
870
+ inference_capture_path (str): S3 path for inference capture
871
+ pred_results_df (pd.DataFrame): DataFrame with prediction results
872
+ target (str): Target column name
873
+ id_column (str, optional): Name of the ID column
874
+ """
875
+ # Start with ID column if present
876
+ output_columns = []
877
+ if id_column and id_column in pred_results_df.columns:
878
+ output_columns.append(id_column)
879
+
880
+ # Add target column if present
881
+ if target and target in pred_results_df.columns:
882
+ output_columns.append(target)
883
+
884
+ # Build the output DataFrame
885
+ output_df = pred_results_df[output_columns].copy() if output_columns else pd.DataFrame()
886
+
887
+ # For multi-task: map {target}_pred -> prediction, {target}_pred_std -> prediction_std
888
+ # For single-task: just grab prediction and prediction_std columns directly
889
+ pred_col = f"{target}_pred"
890
+ std_col = f"{target}_pred_std"
891
+ if pred_col in pred_results_df.columns:
892
+ # Multi-task columns exist
893
+ output_df["prediction"] = pred_results_df[pred_col]
894
+ if std_col in pred_results_df.columns:
895
+ output_df["prediction_std"] = pred_results_df[std_col]
896
+ else:
897
+ # Single-task: grab standard prediction columns
898
+ for col in ["prediction", "prediction_std"]:
899
+ if col in pred_results_df.columns:
900
+ output_df[col] = pred_results_df[col]
901
+ # Also grab any _proba columns and UQ columns
902
+ for col in pred_results_df.columns:
903
+ if col.endswith("_proba") or col.startswith("q_") or col == "confidence":
904
+ output_df[col] = pred_results_df[col]
905
+
906
+ # Write the predictions to S3
907
+ output_file = f"{inference_capture_path}/inference_predictions.csv"
908
+ self.log.info(f"Writing predictions to {output_file}")
909
+ wr.s3.to_csv(output_df, output_file, index=False)
910
+
811
911
  def regression_metrics(self, target_column: str, prediction_df: pd.DataFrame) -> pd.DataFrame:
812
912
  """Compute the performance metrics for this Endpoint
813
913
  Args:
@@ -822,24 +922,23 @@ class EndpointCore(Artifact):
822
922
  self.log.warning("No predictions were made. Returning empty DataFrame.")
823
923
  return pd.DataFrame()
824
924
 
925
+ # Check for prediction column
926
+ if "prediction" not in prediction_df.columns:
927
+ self.log.warning("No 'prediction' column found in DataFrame")
928
+ return pd.DataFrame()
929
+
825
930
  # Check for NaN values in target or prediction columns
826
- prediction_col = "prediction" if "prediction" in prediction_df.columns else "predictions"
827
- if prediction_df[target_column].isnull().any() or prediction_df[prediction_col].isnull().any():
828
- # Compute the number of NaN values in each column
931
+ if prediction_df[target_column].isnull().any() or prediction_df["prediction"].isnull().any():
829
932
  num_nan_target = prediction_df[target_column].isnull().sum()
830
- num_nan_prediction = prediction_df[prediction_col].isnull().sum()
831
- self.log.warning(
832
- f"NaNs Found: {target_column} {num_nan_target} and {prediction_col}: {num_nan_prediction}."
833
- )
834
- self.log.warning(
835
- "NaN values found in target or prediction columns. Dropping NaN rows for metric computation."
836
- )
837
- prediction_df = prediction_df.dropna(subset=[target_column, prediction_col])
933
+ num_nan_prediction = prediction_df["prediction"].isnull().sum()
934
+ self.log.warning(f"NaNs Found: {target_column} {num_nan_target} and prediction: {num_nan_prediction}.")
935
+ self.log.warning("Dropping NaN rows for metric computation.")
936
+ prediction_df = prediction_df.dropna(subset=[target_column, "prediction"])
838
937
 
839
938
  # Compute the metrics
840
939
  try:
841
940
  y_true = prediction_df[target_column]
842
- y_pred = prediction_df[prediction_col]
941
+ y_pred = prediction_df["prediction"]
843
942
 
844
943
  mae = mean_absolute_error(y_true, y_pred)
845
944
  rmse = np.sqrt(mean_squared_error(y_true, y_pred))
@@ -871,11 +970,13 @@ class EndpointCore(Artifact):
871
970
  Returns:
872
971
  pd.DataFrame: DataFrame with two new columns called 'residuals' and 'residuals_abs'
873
972
  """
973
+ # Check for prediction column
974
+ if "prediction" not in prediction_df.columns:
975
+ self.log.warning("No 'prediction' column found. Cannot compute residuals.")
976
+ return prediction_df
874
977
 
875
- # Compute the residuals
876
978
  y_true = prediction_df[target_column]
877
- prediction_col = "prediction" if "prediction" in prediction_df.columns else "predictions"
878
- y_pred = prediction_df[prediction_col]
979
+ y_pred = prediction_df["prediction"]
879
980
 
880
981
  # Check for classification scenario
881
982
  if not pd.api.types.is_numeric_dtype(y_true) or not pd.api.types.is_numeric_dtype(y_pred):
@@ -916,9 +1017,13 @@ class EndpointCore(Artifact):
916
1017
  Returns:
917
1018
  pd.DataFrame: DataFrame with the performance metrics
918
1019
  """
1020
+ # Check for prediction column
1021
+ if "prediction" not in prediction_df.columns:
1022
+ self.log.warning("No 'prediction' column found in DataFrame")
1023
+ return pd.DataFrame()
1024
+
919
1025
  # Drop rows with NaN predictions (can't compute metrics on missing predictions)
920
- prediction_col = "prediction" if "prediction" in prediction_df.columns else "predictions"
921
- nan_mask = prediction_df[prediction_col].isna()
1026
+ nan_mask = prediction_df["prediction"].isna()
922
1027
  if nan_mask.any():
923
1028
  n_nan = nan_mask.sum()
924
1029
  self.log.warning(f"Dropping {n_nan} rows with NaN predictions for metrics calculation")
@@ -938,7 +1043,7 @@ class EndpointCore(Artifact):
938
1043
  # Calculate precision, recall, f1, and support, handling zero division
939
1044
  scores = precision_recall_fscore_support(
940
1045
  prediction_df[target_column],
941
- prediction_df[prediction_col],
1046
+ prediction_df["prediction"],
942
1047
  average=None,
943
1048
  labels=class_labels,
944
1049
  zero_division=0,
@@ -986,16 +1091,20 @@ class EndpointCore(Artifact):
986
1091
  Returns:
987
1092
  pd.DataFrame: DataFrame with the confusion matrix
988
1093
  """
1094
+ # Check for prediction column
1095
+ if "prediction" not in prediction_df.columns:
1096
+ self.log.warning("No 'prediction' column found in DataFrame")
1097
+ return pd.DataFrame()
1098
+
989
1099
  # Drop rows with NaN predictions (can't include in confusion matrix)
990
- prediction_col = "prediction" if "prediction" in prediction_df.columns else "predictions"
991
- nan_mask = prediction_df[prediction_col].isna()
1100
+ nan_mask = prediction_df["prediction"].isna()
992
1101
  if nan_mask.any():
993
1102
  n_nan = nan_mask.sum()
994
1103
  self.log.warning(f"Dropping {n_nan} rows with NaN predictions for confusion matrix")
995
1104
  prediction_df = prediction_df[~nan_mask].copy()
996
1105
 
997
1106
  y_true = prediction_df[target_column]
998
- y_pred = prediction_df[prediction_col]
1107
+ y_pred = prediction_df["prediction"]
999
1108
 
1000
1109
  # Get model class labels
1001
1110
  model_class_labels = ModelCore(self.model_name).class_labels()
@@ -263,21 +263,25 @@ class ModelCore(Artifact):
263
263
  else:
264
264
  self.log.important(f"No inference data found for {self.model_name}!")
265
265
 
266
- def get_inference_metrics(self, capture_name: str = "latest") -> Union[pd.DataFrame, None]:
266
+ def get_inference_metrics(self, capture_name: str = "any") -> Union[pd.DataFrame, None]:
267
267
  """Retrieve the inference performance metrics for this model
268
268
 
269
269
  Args:
270
- capture_name (str, optional): Specific capture_name or "training" (default: "latest")
270
+ capture_name (str, optional): Specific capture_name (default: "any")
271
271
  Returns:
272
272
  pd.DataFrame: DataFrame of the Model Metrics
273
273
 
274
274
  Note:
275
- If a capture_name isn't specified this will try to return something reasonable
275
+ If a capture_name isn't specified this will try to the 'first' available metrics
276
276
  """
277
277
  # 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")
278
+ if capture_name == "any":
279
+ metric_list = self.list_inference_runs()
280
+ if metric_list:
281
+ return self.get_inference_metrics(metric_list[0])
282
+ else:
283
+ self.log.warning(f"No performance metrics found for {self.model_name}!")
284
+ return None
281
285
 
282
286
  # Grab the metrics captured during model training (could return None)
283
287
  if capture_name == "model_training":
@@ -869,7 +873,7 @@ class ModelCore(Artifact):
869
873
  return self.df_store.get(f"/workbench/models/{self.name}/shap_data")
870
874
  else:
871
875
  # 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")
876
+ shap_dfs = self.df_store.list(f"/workbench/models/{self.name}/shap_data")
873
877
  shap_data = {}
874
878
  for df_location in shap_dfs:
875
879
  key = df_location.split("/")[-1]
@@ -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/")
@@ -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,
@@ -188,23 +198,27 @@ class FeaturesToModel(Transform):
188
198
  # Generate our model script
189
199
  script_path = generate_model_script(template_params)
190
200
 
191
- # Metric Definitions for Regression
201
+ # Metric Definitions for Regression (matches model script output format)
192
202
  if self.model_type in [ModelType.REGRESSOR, ModelType.UQ_REGRESSOR, ModelType.ENSEMBLE_REGRESSOR]:
193
203
  metric_definitions = [
194
- {"Name": "RMSE", "Regex": "RMSE: ([0-9.]+)"},
195
- {"Name": "MAE", "Regex": "MAE: ([0-9.]+)"},
196
- {"Name": "R2", "Regex": "R2: ([0-9.]+)"},
197
- {"Name": "NumRows", "Regex": "NumRows: ([0-9]+)"},
204
+ {"Name": "rmse", "Regex": r"rmse: ([0-9.]+)"},
205
+ {"Name": "mae", "Regex": r"mae: ([0-9.]+)"},
206
+ {"Name": "medae", "Regex": r"medae: ([0-9.]+)"},
207
+ {"Name": "r2", "Regex": r"r2: ([0-9.-]+)"},
208
+ {"Name": "spearmanr", "Regex": r"spearmanr: ([0-9.-]+)"},
209
+ {"Name": "support", "Regex": r"support: ([0-9]+)"},
198
210
  ]
199
211
 
200
212
  # Metric Definitions for Classification
201
213
  elif self.model_type == ModelType.CLASSIFIER:
202
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
203
217
 
204
218
  # Grab all the target column class values (class labels)
205
219
  table = feature_set.data_source.table
206
- self.class_labels = feature_set.query(f'select DISTINCT {self.target_column} FROM "{table}"')[
207
- self.target_column
220
+ self.class_labels = feature_set.query(f'select DISTINCT {class_target} FROM "{table}"')[
221
+ class_target
208
222
  ].to_list()
209
223
 
210
224
  # Sanity check on the targets