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.
- workbench/api/df_store.py +17 -108
- workbench/api/feature_set.py +41 -7
- workbench/api/parameter_store.py +3 -52
- workbench/core/artifacts/artifact.py +5 -5
- workbench/core/artifacts/df_store_core.py +114 -0
- workbench/core/artifacts/endpoint_core.py +184 -75
- workbench/core/artifacts/model_core.py +11 -7
- workbench/core/artifacts/parameter_store_core.py +98 -0
- workbench/core/transforms/features_to_model/features_to_model.py +27 -13
- workbench/core/transforms/model_to_endpoint/model_to_endpoint.py +11 -0
- workbench/core/transforms/pandas_transforms/pandas_to_features.py +11 -2
- workbench/model_scripts/chemprop/chemprop.template +312 -293
- workbench/model_scripts/chemprop/generated_model_script.py +316 -297
- workbench/model_scripts/custom_models/uq_models/ensemble_xgb.template +11 -5
- workbench/model_scripts/custom_models/uq_models/meta_uq.template +11 -5
- workbench/model_scripts/custom_models/uq_models/ngboost.template +11 -5
- workbench/model_scripts/ensemble_xgb/ensemble_xgb.template +11 -5
- workbench/model_scripts/pytorch_model/generated_model_script.py +278 -128
- workbench/model_scripts/pytorch_model/pytorch.template +273 -123
- workbench/model_scripts/uq_models/generated_model_script.py +20 -11
- workbench/model_scripts/uq_models/mapie.template +17 -8
- workbench/model_scripts/xgb_model/generated_model_script.py +38 -9
- workbench/model_scripts/xgb_model/xgb_model.template +34 -5
- workbench/resources/open_source_api.key +1 -1
- workbench/utils/chemprop_utils.py +38 -1
- workbench/utils/pytorch_utils.py +38 -8
- workbench/web_interface/components/model_plot.py +7 -1
- {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/METADATA +2 -2
- {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/RECORD +33 -33
- workbench/core/cloud_platform/aws/aws_df_store.py +0 -404
- workbench/core/cloud_platform/aws/aws_parameter_store.py +0 -296
- {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/WHEEL +0 -0
- {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/entry_points.txt +0 -0
- {workbench-0.8.201.dist-info → workbench-0.8.204.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
40
|
-
from workbench.utils.chemprop_utils import
|
|
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
|
-
|
|
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
|
|
402
|
-
self.log.important(f"Target Column {
|
|
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(
|
|
410
|
-
metrics = self.regression_metrics(
|
|
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(
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
537
|
+
# Identify UQ-specific columns (quantiles, prediction_std, *_pred_std)
|
|
499
538
|
uq_columns = [
|
|
500
|
-
col
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
783
|
-
|
|
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(
|
|
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
|
-
|
|
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[
|
|
831
|
-
self.log.warning(
|
|
832
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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[
|
|
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 = "
|
|
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
|
|
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
|
|
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 == "
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
] +
|
|
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":
|
|
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": "
|
|
195
|
-
{"Name": "
|
|
196
|
-
{"Name": "
|
|
197
|
-
{"Name": "
|
|
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 {
|
|
207
|
-
|
|
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
|