-
Notifications
You must be signed in to change notification settings - Fork 2
add a json output and a plot collection #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,7 @@ | ||||||
| import base64 | ||||||
| import json | ||||||
| import logging | ||||||
| import shutil | ||||||
| import tempfile | ||||||
| from pathlib import Path | ||||||
|
|
||||||
|
|
@@ -23,6 +25,40 @@ | |||||
| LOG = logging.getLogger(__name__) | ||||||
|
|
||||||
|
|
||||||
| def _json_safe(value): | ||||||
| if isinstance(value, (str, int, float, bool)) or value is None: | ||||||
| return value | ||||||
| if isinstance(value, np.generic): | ||||||
| return value.item() | ||||||
| if isinstance(value, (list, tuple, set)): | ||||||
| return [_json_safe(v) for v in value] | ||||||
| if isinstance(value, dict): | ||||||
| return {str(k): _json_safe(v) for k, v in value.items()} | ||||||
| if isinstance(value, pd.Series): | ||||||
| return [_json_safe(v) for v in value.tolist()] | ||||||
| if isinstance(value, pd.DataFrame): | ||||||
| return [ | ||||||
| {str(k): _json_safe(v) for k, v in row.items()} | ||||||
| for row in value.replace({np.nan: None}).to_dict(orient="records") | ||||||
| ] | ||||||
| if isinstance(value, np.ndarray): | ||||||
| return [_json_safe(v) for v in value.tolist()] | ||||||
| if isinstance(value, Path): | ||||||
| return str(value) | ||||||
| return str(value) | ||||||
|
|
||||||
|
|
||||||
| def _df_to_json_records(df: pd.DataFrame): | ||||||
| if df is None: | ||||||
| return [] | ||||||
| df_clean = df.replace({np.nan: None}) | ||||||
| records = df_clean.to_dict(orient="records") | ||||||
| return [ | ||||||
| {str(k): _json_safe(v) for k, v in record.items()} | ||||||
| for record in records | ||||||
| ] | ||||||
|
|
||||||
|
|
||||||
| class BaseModelTrainer: | ||||||
| def __init__( | ||||||
| self, | ||||||
|
|
@@ -258,63 +294,37 @@ def save_model(self): | |||||
| model_bytes = tmp.read() | ||||||
| f.create_dataset("model", data=np.void(model_bytes)) | ||||||
|
|
||||||
| def generate_plots(self): | ||||||
| LOG.info("Generating PyCaret diagnostic pltos") | ||||||
|
|
||||||
| # choose the right plots based on task type | ||||||
| if self.task_type == "classification": | ||||||
| plot_names = [ | ||||||
| "learning", | ||||||
| "vc", | ||||||
| "calibration", | ||||||
| "dimension", | ||||||
| "manifold", | ||||||
| "rfe", | ||||||
| "threshold", | ||||||
| "percentage_above_below", | ||||||
| "class_report", | ||||||
| "pr_auc", | ||||||
| "roc_auc", | ||||||
| ] | ||||||
| else: | ||||||
| plot_names = ["residuals", "vc", "parameter", "error", | ||||||
| "learning"] | ||||||
| for name in plot_names: | ||||||
| try: | ||||||
| ax = self.exp.plot_model( | ||||||
| self.best_model, plot=name, save=False | ||||||
| ) | ||||||
| out_path = Path(self.output_dir) / f"plot_{name}.png" | ||||||
| fig = ax.get_figure() | ||||||
| fig.savefig(out_path, bbox_inches="tight") | ||||||
| self.plots[name] = str(out_path) | ||||||
| except Exception as e: | ||||||
| LOG.warning(f"Could not generate {name} plot: {e}") | ||||||
| def _store_plot(self, src_path: str, plot_key: str): | ||||||
| if not src_path: | ||||||
| return None | ||||||
|
|
||||||
| def encode_image_to_base64(self, img_path: str) -> str: | ||||||
| with open(img_path, "rb") as img_file: | ||||||
| return base64.b64encode(img_file.read()).decode("utf-8") | ||||||
| src = Path(src_path) | ||||||
| if not src.exists(): | ||||||
| LOG.warning(f"Plot path {src_path} does not exist; skipping copy.") | ||||||
| return None | ||||||
|
|
||||||
| def save_html_report(self): | ||||||
| LOG.info("Saving HTML report") | ||||||
| ext = src.suffix or ".png" | ||||||
| dest = Path(self.output_dir) / f"plot_{plot_key}{ext}" | ||||||
|
|
||||||
| # 1) Determine best model name | ||||||
| try: | ||||||
| best_model_name = str(self.results.iloc[0]["Model"]) | ||||||
| except Exception: | ||||||
| best_model_name = type(self.best_model).__name__ | ||||||
| LOG.info(f"Best model determined as: {best_model_name}") | ||||||
| if src.resolve() != dest.resolve(): | ||||||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||||||
| shutil.copy2(src, dest) | ||||||
| stored_path = dest | ||||||
| except Exception as exc: | ||||||
| LOG.warning( | ||||||
| f"Failed to copy plot {plot_key} from {src} to {dest}: {exc}. Using original path." | ||||||
| ) | ||||||
| stored_path = src | ||||||
|
|
||||||
| # 2) Compute training sample count | ||||||
| try: | ||||||
| n_train = self.exp.X_train.shape[0] | ||||||
| except Exception: | ||||||
| n_train = getattr( | ||||||
| self.exp, "X_train_transformed", pd.DataFrame() | ||||||
| ).shape[0] | ||||||
| total_rows = self.data.shape[0] | ||||||
| self.plots[plot_key] = str(stored_path) | ||||||
| return self.plots[plot_key] | ||||||
|
|
||||||
| # 3) Build setup parameters table | ||||||
| def encode_image_to_base64(self, img_path: str) -> str: | ||||||
| with open(img_path, "rb") as img_file: | ||||||
| return base64.b64encode(img_file.read()).decode("utf-8") | ||||||
|
|
||||||
| def _prepare_setup_summary(self, n_train, total_rows): | ||||||
| all_params = self.setup_params.copy() | ||||||
| if self.task_type == "classification" and ( | ||||||
| hasattr(self, "probability_threshold") | ||||||
|
|
@@ -342,12 +352,14 @@ def save_html_report(self): | |||||
| pk = key.lower().replace(" ", "_") | ||||||
| v = all_params.get(pk) | ||||||
| if key == "Train Size": | ||||||
| frac = ( | ||||||
| float(v) | ||||||
| if v is not None | ||||||
| else (n_train / total_rows if total_rows else 0) | ||||||
| ) | ||||||
| dv = f"{frac:.2f} ({n_train} rows)" | ||||||
| if v is not None: | ||||||
| frac = float(v) | ||||||
| elif total_rows and n_train is not None: | ||||||
| frac = n_train / total_rows if total_rows else 0 | ||||||
|
||||||
| frac = n_train / total_rows if total_rows else 0 | |
| frac = n_train / total_rows |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -173,6 +173,10 @@ | |||
| <data name="model" format="h5" from_work_dir="pycaret_model.h5" label="${tool.name} best model on ${on_string}" /> | ||||
| <data name="best_model_csv" format="csv" from_work_dir="best_model.csv" label="${tool.name} The parameters of the best model on ${on_string}" hidden="true" /> | ||||
| <data name="comparison_result" format="html" from_work_dir="comparison_result.html" label="${tool.name} analysis report on ${on_string}" /> | ||||
| <data name="metrics_json" format="json" from_work_dir="metrics.json" label="${tool.name} metrics summary on ${on_string}" /> | ||||
| <collection name="plots" type="list" label="${tool.name} plots on ${on_string}"> | ||||
| <discover_datasets pattern="(?P<designation>plot_.*|shap_summary|tree_importance)\.png" format="png" directory="." /> | ||||
| </collection> | ||||
| </outputs> | ||||
| <tests> | ||||
| <test> | ||||
|
|
@@ -198,6 +202,11 @@ | |||
| </assert_contents> | ||||
| </output> | ||||
| <output name="best_model_csv" value="expected_best_model_classification_customized.csv" /> | ||||
| <output name="metrics_json"> | ||||
| <assert_contents> | ||||
| <has_text text="best_model" /> | ||||
| </assert_contents> | ||||
| </output> | ||||
| </test> | ||||
| <test> | ||||
| <param name="input_file" value="pcr.tsv"/> | ||||
|
|
@@ -282,6 +291,15 @@ | |||
| It contains the performance metrics of different models, plots of the best model | ||||
| on the testing set (or part of the training set if a separate test set is not uploaded), and feature analysis plots. | ||||
|
|
||||
|
|
||||
| - **Metrics Summary**: A JSON file that captures the experiment configuration, best model details, and evaluation metrics for train, validation, and test results. | ||||
|
|
||||
|
|
||||
| - **Plots Collection**: A dataset collection containing the generated diagnostic and feature-importance plots (PNG images) for downstream review or download. | ||||
|
|
||||
|
|
||||
| - **Markdown Report**: A concise Markdown summary suitable for embedding in downstream workflows. | ||||
|
|
||||
|
Comment on lines
+301
to
+302
|
||||
| - **Markdown Report**: A concise Markdown summary suitable for embedding in downstream workflows. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File may not be closed if an exception is raised.