Synav commited on
Commit
2feb7c3
·
verified ·
1 Parent(s): e18eea5

Create generate_supplementary_calibration_figure.py

Browse files
src/generate_supplementary_calibration_figure.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # generate_supplementary_calibration_figure.py
2
+ """
3
+ Standalone script — produces the multi-cohort calibration figure for
4
+ the manuscript supplementary section.
5
+
6
+ Inputs:
7
+ - The frozen acute / chronic GVHD models (loaded via existing
8
+ src.model_utils.load_latest_*_single helpers)
9
+ - External cohort CSVs (paths configured below)
10
+
11
+ Outputs:
12
+ - PNG figure: supplementary_calibration_<target>_300dpi.png
13
+ - CSV table: supplementary_calibration_stats_<target>.csv
14
+
15
+ Run from the project root:
16
+ python generate_supplementary_calibration_figure.py
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+ import matplotlib.pyplot as plt
26
+
27
+ # Make src importable
28
+ ROOT = Path(__file__).resolve().parent
29
+ sys.path.insert(0, str(ROOT / "src"))
30
+
31
+ from model_utils import load_latest_acute_single, load_latest_chronic_single
32
+ from inference_utils import align_to_saved_features, sanitize_inference_matrix
33
+ from preprocess_utils import preprocess_pipeline as preprocess
34
+ from calibration_utils import (
35
+ compute_calibration_stats,
36
+ plot_multi_cohort_calibration,
37
+ )
38
+
39
+
40
+ # ----------------------------------------------------------------------
41
+ # CONFIG — edit the cohort paths to point to your external CSVs.
42
+ # ----------------------------------------------------------------------
43
+ EXTERNAL_COHORTS = {
44
+ "P5356": "external_cohorts/P5356.csv",
45
+ "P5441": "external_cohorts/P5441.csv",
46
+ "P5373": "external_cohorts/P5373.csv",
47
+ "P5178": "external_cohorts/P5178.csv",
48
+ "HS1502": "external_cohorts/HS1502.csv",
49
+ "UAE+Jordan": "external_cohorts/UAE_Jordan.csv",
50
+ }
51
+
52
+ OUTPUT_DIR = ROOT / "manuscript_outputs"
53
+ OUTPUT_DIR.mkdir(exist_ok=True)
54
+
55
+
56
+ # ----------------------------------------------------------------------
57
+ # Load OOF predictions from saved model metadata
58
+ # ----------------------------------------------------------------------
59
+
60
+ def load_oof_from_model(model_dict):
61
+ extra = model_dict.get("extra_metadata", {}) or {}
62
+ y_true = extra.get("oof_true")
63
+ y_prob = extra.get("oof_probs")
64
+ if y_true is None or y_prob is None:
65
+ return None, None
66
+ return np.asarray(y_true), np.asarray(y_prob)
67
+
68
+
69
+ def score_cohort(model_dict, cohort_csv_path, target_col):
70
+ """Load a cohort CSV, preprocess, score with frozen model."""
71
+ df = pd.read_csv(cohort_csv_path, header=1)
72
+ df_full, df_model = preprocess(df, target_col=target_col)
73
+
74
+ extra = model_dict.get("extra_metadata", {}) or {}
75
+ train_features = extra.get("train_features", [])
76
+ cat_features = extra.get("cat_features", [])
77
+
78
+ X = align_to_saved_features(df_model, train_features, cat_features)
79
+
80
+ y = pd.to_numeric(df_full[target_col], errors="coerce")
81
+ valid = y.notna()
82
+ X = X.loc[valid]
83
+ y = y.loc[valid].astype(int).to_numpy()
84
+
85
+ model = model_dict["model"]
86
+ preds = model.predict_proba(X)[:, 1]
87
+ return y, preds
88
+
89
+
90
+ # ----------------------------------------------------------------------
91
+ # Build figure for one target (acute or chronic)
92
+ # ----------------------------------------------------------------------
93
+
94
+ def build_figure_for_target(target_label, target_col):
95
+ print(f"\n=== {target_label} ===")
96
+ if "acute" in target_col.lower():
97
+ model_dict = load_latest_acute_single()
98
+ elif "chronic" in target_col.lower():
99
+ model_dict = load_latest_chronic_single()
100
+ else:
101
+ raise ValueError(f"Unknown target_col: {target_col}")
102
+
103
+ cohort_results = {}
104
+
105
+ # Training OOF
106
+ oof_true, oof_probs = load_oof_from_model(model_dict)
107
+ if oof_true is not None and oof_probs is not None:
108
+ cohort_results["Training (OOF)"] = (oof_true, oof_probs)
109
+ print(f" Training OOF: N={len(oof_true)}")
110
+ else:
111
+ print(" WARNING: no OOF arrays in model metadata. Retrain to capture them.")
112
+
113
+ # External cohorts
114
+ for cohort_name, cohort_path in EXTERNAL_COHORTS.items():
115
+ if not Path(cohort_path).exists():
116
+ print(f" SKIP {cohort_name}: file not found at {cohort_path}")
117
+ continue
118
+ try:
119
+ y, preds = score_cohort(model_dict, cohort_path, target_col)
120
+ if len(y) < 20:
121
+ print(f" SKIP {cohort_name}: too few rows ({len(y)})")
122
+ continue
123
+ cohort_results[cohort_name] = (y, preds)
124
+ print(f" {cohort_name}: N={len(y)}, events={int(y.sum())}")
125
+ except Exception as e:
126
+ print(f" ERROR scoring {cohort_name}: {e}")
127
+
128
+ # Composite figure
129
+ fig = plot_multi_cohort_calibration(cohort_results, ncols=3)
130
+ fig_path = OUTPUT_DIR / f"supplementary_calibration_{target_label}_300dpi.png"
131
+ fig.savefig(fig_path, dpi=300, bbox_inches="tight")
132
+ plt.close(fig)
133
+ print(f" Figure saved: {fig_path}")
134
+
135
+ # Stats table
136
+ rows = []
137
+ for cohort_name, (y, preds) in cohort_results.items():
138
+ stats = compute_calibration_stats(y, preds, n_bins=10)
139
+ rows.append({
140
+ "Cohort": cohort_name,
141
+ "N": stats["N"],
142
+ "Events": stats["Events"],
143
+ "Prevalence": stats["Prevalence"],
144
+ "Brier": stats["Brier"],
145
+ "CalibrationInTheLarge": stats["CalibrationInTheLarge"],
146
+ "Intercept": stats["CalibrationIntercept"],
147
+ "Intercept_CI_low": stats["CalibrationIntercept_CI_low"],
148
+ "Intercept_CI_high": stats["CalibrationIntercept_CI_high"],
149
+ "Slope": stats["CalibrationSlope"],
150
+ "Slope_CI_low": stats["CalibrationSlope_CI_low"],
151
+ "Slope_CI_high": stats["CalibrationSlope_CI_high"],
152
+ })
153
+
154
+ table_path = OUTPUT_DIR / f"supplementary_calibration_stats_{target_label}.csv"
155
+ pd.DataFrame(rows).to_csv(table_path, index=False)
156
+ print(f" Stats table saved: {table_path}")
157
+
158
+
159
+ # ----------------------------------------------------------------------
160
+ # Run for both targets
161
+ # ----------------------------------------------------------------------
162
+
163
+ if __name__ == "__main__":
164
+ build_figure_for_target("acute", "Acute GVHD(<100 days)")
165
+ build_figure_for_target("chronic", "Chronic GVHD>100 days")
166
+ print("\nDone.")