From 289b41cd4831f3a600e91a47c5a742ae6010de5a Mon Sep 17 00:00:00 2001 From: mducducd Date: Thu, 21 May 2026 15:25:29 +0200 Subject: [PATCH 1/4] refactor --- src/stamp/modeling/data.py | 8 +++----- src/stamp/modeling/models/cox.py | 13 ------------- src/stamp/statistics/__init__.py | 30 ++++++++++++++++++++--------- src/stamp/statistics/categorical.py | 12 ++++++++---- src/stamp/statistics/regression.py | 7 +++++-- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/stamp/modeling/data.py b/src/stamp/modeling/data.py index 3604ce82..19c9bb4b 100755 --- a/src/stamp/modeling/data.py +++ b/src/stamp/modeling/data.py @@ -1104,11 +1104,9 @@ def filter_complete_patient_data_( ) } - _logger.info( - f"Total patients in clinical table: {len(patient_to_ground_truth)}\n" - f"Patients appearing in slide table: {len(patient_to_slides)}\n" - f"Final usable patients (complete data): {len(patients)}\n" - ) + _logger.info("Total patients in clinical table: %d", len(patient_to_ground_truth)) + _logger.info("Patients appearing in slide table: %d", len(patient_to_slides)) + _logger.info("Final usable patients (complete data): %d", len(patients)) return patients diff --git a/src/stamp/modeling/models/cox.py b/src/stamp/modeling/models/cox.py index 48b88a6b..94da2cb6 100644 --- a/src/stamp/modeling/models/cox.py +++ b/src/stamp/modeling/models/cox.py @@ -4,7 +4,6 @@ # pylint: disable=C0103 # pylint: disable=C0301 -import sys import warnings import torch @@ -268,15 +267,3 @@ def neg_partial_log_likelihood( ) ) return loss - - -if __name__ == "__main__": - import doctest - - # Run doctest - results = doctest.testmod() - if results.failed == 0: - print("All tests passed.") - else: - print("Some doctests failed.") - sys.exit(1) diff --git a/src/stamp/statistics/__init__.py b/src/stamp/statistics/__init__.py index 0a7eedef..c10d0998 100644 --- a/src/stamp/statistics/__init__.py +++ b/src/stamp/statistics/__init__.py @@ -6,6 +6,7 @@ from __future__ import annotations +import re from collections.abc import Sequence from pathlib import Path from typing import NewType @@ -31,7 +32,20 @@ from stamp.statistics.survival import _plot_km, _survival_stats_for_csv from stamp.types import PandasLabel, Task -__all__ = ["StatsConfig", "compute_stats_"] +__all__ = ["StatsConfig", "compute_stats_", "path_safe"] + + +_PATH_SEP_RE = re.compile(r"[/\\]+") + + +def path_safe(label: str) -> str: + """Make a label safe to embed in a filename. + + Replaces path separators with ``_`` so labels like + ``"Parameter(0=mod/well,1=poor)"`` don't get interpreted as a + subdirectory when passed to ``pathlib.Path``. + """ + return _PATH_SEP_RE.sub("_", label) __author__ = "Marko van Treeck, Minh Duc Nguyen" @@ -146,7 +160,8 @@ def _compute_multitarget_classification_stats( ) fig.tight_layout() - fig.savefig(output_dir / f"roc-curve_{target_label}={true_class}.svg") + safe_target = path_safe(target_label) + fig.savefig(output_dir / f"roc-curve_{safe_target}={true_class}.svg") plt.close(fig) # Plot PRC curve @@ -172,7 +187,7 @@ def _compute_multitarget_classification_stats( ) fig.tight_layout() - fig.savefig(output_dir / f"pr-curve_{target_label}={true_class}.svg") + fig.savefig(output_dir / f"pr-curve_{safe_target}={true_class}.svg") plt.close(fig) # Compute aggregated statistics for all targets @@ -291,9 +306,8 @@ def compute_stats_( fig.tight_layout() output_dir.mkdir(parents=True, exist_ok=True) - fig.savefig( - output_dir / f"roc-curve_{ground_truth_label}={true_class}.svg" - ) + safe_label = path_safe(ground_truth_label) + fig.savefig(output_dir / f"roc-curve_{safe_label}={true_class}.svg") plt.close(fig) fig, ax = plt.subplots( @@ -320,9 +334,7 @@ def compute_stats_( ) fig.tight_layout() - fig.savefig( - output_dir / f"pr-curve_{ground_truth_label}={true_class}.svg" - ) + fig.savefig(output_dir / f"pr-curve_{safe_label}={true_class}.svg") plt.close(fig) categorical_aggregated_( diff --git a/src/stamp/statistics/categorical.py b/src/stamp/statistics/categorical.py index a267f5ca..35d6876e 100755 --- a/src/stamp/statistics/categorical.py +++ b/src/stamp/statistics/categorical.py @@ -7,6 +7,8 @@ import scipy.stats as st from sklearn import metrics +from stamp.statistics import path_safe + __author__ = "Marko van Treeck" __copyright__ = "Copyright (C) 2022-2025 Marko van Treeck" __license__ = "MIT" @@ -137,9 +139,10 @@ def categorical_aggregated_( ) preds_df = pd.concat(preds_dfs).sort_index() - preds_df.to_csv(outpath / f"{ground_truth_label}_categorical-stats_individual.csv") + safe_label = path_safe(ground_truth_label) + preds_df.to_csv(outpath / f"{safe_label}_categorical-stats_individual.csv") stats_df = _aggregate_categorical_stats(preds_df.reset_index()) - stats_df.to_csv(outpath / f"{ground_truth_label}_categorical-stats_aggregated.csv") + stats_df.to_csv(outpath / f"{safe_label}_categorical-stats_aggregated.csv") def categorical_aggregated_multitarget_( @@ -181,11 +184,12 @@ def categorical_aggregated_multitarget_( # Concatenate and save individual stats for this target preds_df = pd.concat(preds_dfs).sort_index() - preds_df.to_csv(outpath / f"{target_label}_categorical-stats_individual.csv") + safe_target = path_safe(target_label) + preds_df.to_csv(outpath / f"{safe_target}_categorical-stats_individual.csv") # Aggregate stats for this target stats_df = _aggregate_categorical_stats(preds_df.reset_index()) - stats_df.to_csv(outpath / f"{target_label}_categorical-stats_aggregated.csv") + stats_df.to_csv(outpath / f"{safe_target}_categorical-stats_aggregated.csv") # Store for summary all_target_stats[target_label] = stats_df diff --git a/src/stamp/statistics/regression.py b/src/stamp/statistics/regression.py index c92b5bd9..b86ef3f3 100644 --- a/src/stamp/statistics/regression.py +++ b/src/stamp/statistics/regression.py @@ -10,6 +10,8 @@ import scipy.stats as st from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score +from stamp.statistics import path_safe + def _regression(preds_df: pd.DataFrame, target_label: str) -> pd.Series: """Compute regression metrics for one prediction table.""" @@ -107,10 +109,11 @@ def regression_aggregated_( # Save individual stats and aggregate stats_df = pd.DataFrame(stats).transpose() - stats_df.to_csv(outpath / f"{ground_truth_label}_regression-stats_individual.csv") + safe_label = path_safe(ground_truth_label) + stats_df.to_csv(outpath / f"{safe_label}_regression-stats_individual.csv") mean = stats_df.mean(numeric_only=True) sem = stats_df.sem(numeric_only=True) lower, upper = st.t.interval(0.95, len(stats_df) - 1, loc=mean, scale=sem) agg = pd.DataFrame({"mean": mean, "95%_low": lower, "95%_high": upper}) - agg.to_csv(outpath / f"{ground_truth_label}_regression-stats_aggregated.csv") + agg.to_csv(outpath / f"{safe_label}_regression-stats_aggregated.csv") From 191b39b4902df69ce64abdc735f8eb34a7053a5f Mon Sep 17 00:00:00 2001 From: mducducd Date: Thu, 21 May 2026 15:35:24 +0200 Subject: [PATCH 2/4] Fix: missing error handling of ground truth labels with a slash --- src/stamp/statistics/__init__.py | 34 ++++++++++++++--------------- src/stamp/statistics/categorical.py | 18 ++++++++++----- src/stamp/statistics/regression.py | 9 +++++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/stamp/statistics/__init__.py b/src/stamp/statistics/__init__.py index c10d0998..d9694f9f 100644 --- a/src/stamp/statistics/__init__.py +++ b/src/stamp/statistics/__init__.py @@ -6,7 +6,6 @@ from __future__ import annotations -import re from collections.abc import Sequence from pathlib import Path from typing import NewType @@ -35,17 +34,9 @@ __all__ = ["StatsConfig", "compute_stats_", "path_safe"] -_PATH_SEP_RE = re.compile(r"[/\\]+") - - def path_safe(label: str) -> str: - """Make a label safe to embed in a filename. - - Replaces path separators with ``_`` so labels like - ``"Parameter(0=mod/well,1=poor)"`` don't get interpreted as a - subdirectory when passed to ``pathlib.Path``. - """ - return _PATH_SEP_RE.sub("_", label) + """Replace '/' (and '\\') with '_' so labels are safe as filename parts.""" + return label.replace("/", "_").replace("\\", "_") __author__ = "Marko van Treeck, Minh Duc Nguyen" @@ -160,8 +151,10 @@ def _compute_multitarget_classification_stats( ) fig.tight_layout() - safe_target = path_safe(target_label) - fig.savefig(output_dir / f"roc-curve_{safe_target}={true_class}.svg") + fig.savefig( + output_dir + / f"roc-curve_{path_safe(target_label)}={true_class}.svg" + ) plt.close(fig) # Plot PRC curve @@ -187,7 +180,9 @@ def _compute_multitarget_classification_stats( ) fig.tight_layout() - fig.savefig(output_dir / f"pr-curve_{safe_target}={true_class}.svg") + fig.savefig( + output_dir / f"pr-curve_{path_safe(target_label)}={true_class}.svg" + ) plt.close(fig) # Compute aggregated statistics for all targets @@ -306,8 +301,10 @@ def compute_stats_( fig.tight_layout() output_dir.mkdir(parents=True, exist_ok=True) - safe_label = path_safe(ground_truth_label) - fig.savefig(output_dir / f"roc-curve_{safe_label}={true_class}.svg") + fig.savefig( + output_dir + / f"roc-curve_{path_safe(ground_truth_label)}={true_class}.svg" + ) plt.close(fig) fig, ax = plt.subplots( @@ -334,7 +331,10 @@ def compute_stats_( ) fig.tight_layout() - fig.savefig(output_dir / f"pr-curve_{safe_label}={true_class}.svg") + fig.savefig( + output_dir + / f"pr-curve_{path_safe(ground_truth_label)}={true_class}.svg" + ) plt.close(fig) categorical_aggregated_( diff --git a/src/stamp/statistics/categorical.py b/src/stamp/statistics/categorical.py index 35d6876e..cf167918 100755 --- a/src/stamp/statistics/categorical.py +++ b/src/stamp/statistics/categorical.py @@ -139,10 +139,13 @@ def categorical_aggregated_( ) preds_df = pd.concat(preds_dfs).sort_index() - safe_label = path_safe(ground_truth_label) - preds_df.to_csv(outpath / f"{safe_label}_categorical-stats_individual.csv") + preds_df.to_csv( + outpath / f"{path_safe(ground_truth_label)}_categorical-stats_individual.csv" + ) stats_df = _aggregate_categorical_stats(preds_df.reset_index()) - stats_df.to_csv(outpath / f"{safe_label}_categorical-stats_aggregated.csv") + stats_df.to_csv( + outpath / f"{path_safe(ground_truth_label)}_categorical-stats_aggregated.csv" + ) def categorical_aggregated_multitarget_( @@ -184,12 +187,15 @@ def categorical_aggregated_multitarget_( # Concatenate and save individual stats for this target preds_df = pd.concat(preds_dfs).sort_index() - safe_target = path_safe(target_label) - preds_df.to_csv(outpath / f"{safe_target}_categorical-stats_individual.csv") + preds_df.to_csv( + outpath / f"{path_safe(target_label)}_categorical-stats_individual.csv" + ) # Aggregate stats for this target stats_df = _aggregate_categorical_stats(preds_df.reset_index()) - stats_df.to_csv(outpath / f"{safe_target}_categorical-stats_aggregated.csv") + stats_df.to_csv( + outpath / f"{path_safe(target_label)}_categorical-stats_aggregated.csv" + ) # Store for summary all_target_stats[target_label] = stats_df diff --git a/src/stamp/statistics/regression.py b/src/stamp/statistics/regression.py index b86ef3f3..1dbc2fd9 100644 --- a/src/stamp/statistics/regression.py +++ b/src/stamp/statistics/regression.py @@ -109,11 +109,14 @@ def regression_aggregated_( # Save individual stats and aggregate stats_df = pd.DataFrame(stats).transpose() - safe_label = path_safe(ground_truth_label) - stats_df.to_csv(outpath / f"{safe_label}_regression-stats_individual.csv") + stats_df.to_csv( + outpath / f"{path_safe(ground_truth_label)}_regression-stats_individual.csv" + ) mean = stats_df.mean(numeric_only=True) sem = stats_df.sem(numeric_only=True) lower, upper = st.t.interval(0.95, len(stats_df) - 1, loc=mean, scale=sem) agg = pd.DataFrame({"mean": mean, "95%_low": lower, "95%_high": upper}) - agg.to_csv(outpath / f"{safe_label}_regression-stats_aggregated.csv") + agg.to_csv( + outpath / f"{path_safe(ground_truth_label)}_regression-stats_aggregated.csv" + ) From f09f9f44c9169c821478674fbb6ffecf48586cd0 Mon Sep 17 00:00:00 2001 From: mducducd Date: Thu, 21 May 2026 15:36:35 +0200 Subject: [PATCH 3/4] reformat --- src/stamp/statistics/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stamp/statistics/__init__.py b/src/stamp/statistics/__init__.py index d9694f9f..aed0d2a7 100644 --- a/src/stamp/statistics/__init__.py +++ b/src/stamp/statistics/__init__.py @@ -152,8 +152,7 @@ def _compute_multitarget_classification_stats( fig.tight_layout() fig.savefig( - output_dir - / f"roc-curve_{path_safe(target_label)}={true_class}.svg" + output_dir / f"roc-curve_{path_safe(target_label)}={true_class}.svg" ) plt.close(fig) From c4cc32e698431be251609a1441cc22dd5d59fc19 Mon Sep 17 00:00:00 2001 From: mducducd Date: Thu, 21 May 2026 15:43:39 +0200 Subject: [PATCH 4/4] refactor --- src/stamp/statistics/__init__.py | 6 +----- src/stamp/statistics/categorical.py | 2 +- src/stamp/statistics/regression.py | 2 +- src/stamp/utils/path.py | 6 ++++++ 4 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 src/stamp/utils/path.py diff --git a/src/stamp/statistics/__init__.py b/src/stamp/statistics/__init__.py index aed0d2a7..6b15eb87 100644 --- a/src/stamp/statistics/__init__.py +++ b/src/stamp/statistics/__init__.py @@ -30,15 +30,11 @@ ) from stamp.statistics.survival import _plot_km, _survival_stats_for_csv from stamp.types import PandasLabel, Task +from stamp.utils.path import path_safe __all__ = ["StatsConfig", "compute_stats_", "path_safe"] -def path_safe(label: str) -> str: - """Replace '/' (and '\\') with '_' so labels are safe as filename parts.""" - return label.replace("/", "_").replace("\\", "_") - - __author__ = "Marko van Treeck, Minh Duc Nguyen" __copyright__ = "Copyright (C) 2022-2024 Marko van Treeck, Minh Duc Nguyen" __license__ = "MIT" diff --git a/src/stamp/statistics/categorical.py b/src/stamp/statistics/categorical.py index cf167918..ac48994d 100755 --- a/src/stamp/statistics/categorical.py +++ b/src/stamp/statistics/categorical.py @@ -7,7 +7,7 @@ import scipy.stats as st from sklearn import metrics -from stamp.statistics import path_safe +from stamp.utils.path import path_safe __author__ = "Marko van Treeck" __copyright__ = "Copyright (C) 2022-2025 Marko van Treeck" diff --git a/src/stamp/statistics/regression.py b/src/stamp/statistics/regression.py index 1dbc2fd9..d616c15f 100644 --- a/src/stamp/statistics/regression.py +++ b/src/stamp/statistics/regression.py @@ -10,7 +10,7 @@ import scipy.stats as st from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score -from stamp.statistics import path_safe +from stamp.utils.path import path_safe def _regression(preds_df: pd.DataFrame, target_label: str) -> pd.Series: diff --git a/src/stamp/utils/path.py b/src/stamp/utils/path.py new file mode 100644 index 00000000..ab577841 --- /dev/null +++ b/src/stamp/utils/path.py @@ -0,0 +1,6 @@ +"""Small path-related helpers shared across the project.""" + + +def path_safe(label: str) -> str: + """Replace path separators so labels are safe as filename parts.""" + return label.replace("/", "_").replace("\\", "_")