2026001
This commit is contained in:
+106
-50
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user