This commit is contained in:
rpotter6298
2026-07-01 17:35:58 +02:00
parent 9bfcc0243b
commit 35cbd9ac3c
84 changed files with 8500 additions and 423 deletions
+106 -50
View File
@@ -61,7 +61,7 @@ class PatientLeakageClassifier:
# ------------------------------------------------------------------
def run(self, model_name, seed, split_type="patient",
nfeatures_list=None, gamma_logspace=None):
nfeatures_list=None, gamma_logspace=None, return_predictions=False):
"""Run full pipeline: train/test split → RF importance →
GridSearchCV over nfeatures×gamma → retrain → test.
@@ -72,6 +72,9 @@ class PatientLeakageClassifier:
split_type : str "image" or "patient".
nfeatures_list : list Feature counts to search over.
gamma_logspace : tuple Args for np.logspace (start, stop, n).
return_predictions : bool
If True, also include the test labels/predictions ("y_true",
"y_pred") so callers can build a confusion matrix without refitting.
Returns
-------
@@ -84,55 +87,12 @@ class PatientLeakageClassifier:
gamma_logspace = DEFAULT_GAMMA_RANGE
X, Y, groups = self._load_data(model_name)
(X_tr, X_te, y_tr, y_te, g_tr, cv,
n_tr_patients, n_te_patients) = self._split(X, Y, groups, seed, split_type)
# --- Train/test split ---
if split_type == "image":
X_tr, X_te, y_tr, y_te = train_test_split(
X, Y, test_size=0.2, random_state=seed, stratify=Y)
cv = 5
g_tr = None
n_tr_patients = None
n_te_patients = None
else:
unique_g = np.unique(groups)
g_labels = np.array([Y[groups == g][0] for g in unique_g])
tr_g, te_g = train_test_split(
unique_g, test_size=0.2, random_state=seed, stratify=g_labels)
tr_mask = np.isin(groups, tr_g)
te_mask = np.isin(groups, te_g)
X_tr, X_te = X[tr_mask], X[te_mask]
y_tr, y_te = Y[tr_mask], Y[te_mask]
g_tr = groups[tr_mask]
n_tr_patients = len(np.unique(g_tr))
n_te_patients = len(np.unique(groups[te_mask]))
cv = StratifiedGroupKFold(n_splits=5, shuffle=True,
random_state=seed)
# --- RF feature importance ---
fs = RandomForestClassifier(n_estimators=500, random_state=15,
class_weight="balanced", n_jobs=self.n_jobs)
fs.fit(X_tr, y_tr)
sorted_idx = np.argsort(fs.feature_importances_)[::-1]
# --- Grid search over nfeatures × gamma ---
pipe = Pipeline([("scaler", StandardScaler()),
("svm", SVC(kernel="rbf", C=10))])
valid_nfeats = [n for n in nfeatures_list if n <= X_tr.shape[1]]
best_cv, best_n, best_g = 0, None, None
for nfeat in valid_nfeats:
sel = sorted_idx[:nfeat]
pg = {"svm__gamma": (1.0 / nfeat) * np.logspace(*gamma_logspace)}
grid = GridSearchCV(pipe, pg, cv=cv, scoring="accuracy",
n_jobs=self.n_jobs, verbose=0)
if g_tr is not None:
grid.fit(X_tr[:, sel], y_tr, groups=g_tr)
else:
grid.fit(X_tr[:, sel], y_tr)
if grid.best_score_ > best_cv:
best_cv = grid.best_score_
best_n = nfeat
best_g = grid.best_params_["svm__gamma"]
sorted_idx = self._rf_ranking(X_tr, y_tr)
best_cv, best_n, best_g = self._grid_search(
X_tr, y_tr, g_tr, cv, sorted_idx, nfeatures_list, gamma_logspace)
# --- Retrain + test ---
final = Pipeline([("scaler", StandardScaler()),
@@ -140,7 +100,7 @@ class PatientLeakageClassifier:
final.fit(X_tr[:, sorted_idx[:best_n]], y_tr)
y_pred = final.predict(X_te[:, sorted_idx[:best_n]])
return {
result = {
"model": model_name,
"seed": seed,
"split_type": split_type,
@@ -153,11 +113,107 @@ class PatientLeakageClassifier:
"n_train_patients": n_tr_patients,
"n_test_patients": n_te_patients,
}
if return_predictions:
result["y_true"] = [str(v) for v in y_te]
result["y_pred"] = [str(v) for v in y_pred]
return result
def cv_curve(self, model_name, seed, split_type="image",
nfeatures_list=None, gamma_logspace=None):
"""CV accuracy as a function of the number of RF-selected features.
Same split → RF ranking → per-nfeatures γ-grid as ``run``, but returns
the whole curve (used by the Figure 3 / S1 / S4 CV-vs-features plots)
instead of only the best point.
Returns
-------
dict: {"curves": {nfeat: cv_acc}, "best_nfeat", "best_cv", "best_gamma"}
"""
if nfeatures_list is None:
nfeatures_list = DEFAULT_NFEATURES
if gamma_logspace is None:
gamma_logspace = DEFAULT_GAMMA_RANGE
X, Y, groups = self._load_data(model_name)
X_tr, _, y_tr, _, g_tr, cv, _, _ = self._split(
X, Y, groups, seed, split_type)
sorted_idx = self._rf_ranking(X_tr, y_tr)
pipe = Pipeline([("scaler", StandardScaler()),
("svm", SVC(kernel="rbf", C=10))])
valid_nfeats = [n for n in nfeatures_list if n <= X_tr.shape[1]]
curves, best_cv, best_n, best_g = {}, 0, None, None
for nfeat in valid_nfeats:
pg = {"svm__gamma": (1.0 / nfeat) * np.logspace(*gamma_logspace)}
grid = GridSearchCV(pipe, pg, cv=cv, scoring="accuracy",
n_jobs=self.n_jobs, verbose=0)
if g_tr is not None:
grid.fit(X_tr[:, sorted_idx[:nfeat]], y_tr, groups=g_tr)
else:
grid.fit(X_tr[:, sorted_idx[:nfeat]], y_tr)
curves[int(nfeat)] = float(grid.best_score_)
if grid.best_score_ > best_cv:
best_cv = float(grid.best_score_)
best_n = int(nfeat)
best_g = float(grid.best_params_["svm__gamma"])
return {"curves": curves, "best_nfeat": best_n,
"best_cv": best_cv, "best_gamma": best_g}
# ------------------------------------------------------------------
# Internals
# ------------------------------------------------------------------
def _split(self, X, Y, groups, seed, split_type):
"""Image-level or patient-level 80:20 train/test split.
Returns (X_tr, X_te, y_tr, y_te, g_tr, cv, n_tr_patients, n_te_patients);
g_tr is None and cv is 5 for image-level splits.
"""
if split_type == "image":
X_tr, X_te, y_tr, y_te = train_test_split(
X, Y, test_size=0.2, random_state=seed, stratify=Y)
return X_tr, X_te, y_tr, y_te, None, 5, None, None
unique_g = np.unique(groups)
g_labels = np.array([Y[groups == g][0] for g in unique_g])
tr_g, te_g = train_test_split(
unique_g, test_size=0.2, random_state=seed, stratify=g_labels)
tr_mask = np.isin(groups, tr_g)
te_mask = np.isin(groups, te_g)
cv = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=seed)
return (X[tr_mask], X[te_mask], Y[tr_mask], Y[te_mask],
groups[tr_mask], cv, len(np.unique(groups[tr_mask])),
len(np.unique(groups[te_mask])))
def _rf_ranking(self, X_tr, y_tr):
"""Feature indices ranked by RF Gini importance (descending)."""
fs = RandomForestClassifier(n_estimators=500, random_state=15,
class_weight="balanced", n_jobs=self.n_jobs)
fs.fit(X_tr, y_tr)
return np.argsort(fs.feature_importances_)[::-1]
def _grid_search(self, X_tr, y_tr, g_tr, cv, sorted_idx,
nfeatures_list, gamma_logspace):
"""Search nfeatures × gamma; return (best_cv, best_nfeat, best_gamma)."""
pipe = Pipeline([("scaler", StandardScaler()),
("svm", SVC(kernel="rbf", C=10))])
valid_nfeats = [n for n in nfeatures_list if n <= X_tr.shape[1]]
best_cv, best_n, best_g = 0, None, None
for nfeat in valid_nfeats:
pg = {"svm__gamma": (1.0 / nfeat) * np.logspace(*gamma_logspace)}
grid = GridSearchCV(pipe, pg, cv=cv, scoring="accuracy",
n_jobs=self.n_jobs, verbose=0)
if g_tr is not None:
grid.fit(X_tr[:, sorted_idx[:nfeat]], y_tr, groups=g_tr)
else:
grid.fit(X_tr[:, sorted_idx[:nfeat]], y_tr)
if grid.best_score_ > best_cv:
best_cv = grid.best_score_
best_n = nfeat
best_g = grid.best_params_["svm__gamma"]
return best_cv, best_n, best_g
def _load_data(self, model_name):
data = np.load(os.path.join(self.features_dir,
f"{model_name}_features.npz"),