OSCR

On the value of radiomics in addition to clinical measures in emotional conflict fMRI for predicting sertraline response in major depressive disorder.

Code ↔ Paper

11 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 11 matches · 4 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › Emotional conflict task and fMRI preprocessing ↔ 03_FSL_FEAT/slurm output 8 - new normalization augmented/397_volumes/slurm_397.sh, the whole file · a weak match · score 0.81 · post error trials, FSL FEAT, template, MNI152, CSF, events
  2. [2] § Methods › Emotional conflict task and fMRI preprocessing ↔ 03_FSL_FEAT/slurm output 8 - new normalization augmented/400_volumes/slurm_400.sh, the whole file · a weak match · score 0.81 · post error trials, FSL FEAT, template, MNI152, CSF, events
  3. [3] § Methods › Model training and evaluation ↔ 05_regression_classification/RVM/main.py, lines 1162–1231 · score 0.72 · KFold, cross validation, SHAP, exPlanations, inner, outer
  4. [4] § Methods › Model training and evaluation ↔ 05_regression_classification/Classification/main.py, lines 688–728 · score 0.69 · BayesSearchCV, Minority, Synthetic, SMOTE, imbalance, tuning
  5. [5] § Methods › Model training and evaluation ↔ 05_regression_classification/Classification/main.py, lines 505–562 · score 0.67 · absolute correlation, XGBoost, deviation, RFE, transformer, median
  6. [6] § Methods › Model training and evaluation ↔ 05_regression_classification/Regression/main.py, lines 401–458 · score 0.67 · absolute correlation, XGBoost, deviation, RFE, transformer, median
  7. [7] § Methods › Emotional conflict task and fMRI preprocessing ↔ 03_FSL_FEAT/slurm output 8 - new normalization augmented/397_volumes/slurm_397.sh, the whole file · a weak match · score 0.60 · FSL FEAT model, volumes, preprocessing
  8. [8] § Methods › Model training and evaluation ↔ 05_regression_classification/RVM/main.py, lines 1162–1231 · score 0.59 · Hyperparameter tuning, square error, RMSE, regression, classification, training
  9. [9] § Methods › Model training and evaluation ↔ 05_regression_classification/Classification/main.py, lines 364–503 · score 0.59 · neuroHarmonize, ComBat, unchanged, harmonized, clinical, training
  10. [10] § Methods › Emotional conflict task and fMRI preprocessing ↔ 03_FSL_FEAT/slurm output 8 - new normalization augmented/400_volumes/slurm_400.sh, the whole file · a weak match · score 0.59 · FSL FEAT model, volumes, preprocessing
  11. [11] § Methods › Model training and evaluation ↔ 05_regression_classification/Regression/Friedman's_test_pairwise_wilcoxon.py, lines 164–210 · score 0.56 · pairwise Wilcoxon, Friedman, classification, regression, models

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,203 lines · 47 KB · MIT · 3 matches

  1. # Mancy Chen 20/02/2025
  2. # Classification with XGBoost
  3. import os,joblib, csv
  4. os.environ["PYTHONWARNINGS"] = "ignore"
  5. import warnings
  6. from sklearn.exceptions import ConvergenceWarning
  7. # Ignore the specific FutureWarnings from XGBoost about glibc versions.
  8. warnings.filterwarnings(
  9. "ignore",
  10. message="Your system has an old version of glibc",
  11. category=FutureWarning
  12. )
  13. # Ignore all ConvergenceWarnings from IterativeImputer.
  14. warnings.filterwarnings("ignore", category=ConvergenceWarning)
  15. # import sys
  16. # sys.path.append('/.../miniconda3/lib/python3.10/site-packages')
  17. import re
  18. import os
  19. import time
  20. import numpy as np
  21. import pandas as pd
  22. from datetime import datetime
  23. from sklearn.base import BaseEstimator, RegressorMixin, TransformerMixin
  24. from sklearn.experimental import enable_iterative_imputer
  25. from sklearn.impute import SimpleImputer, IterativeImputer, KNNImputer
  26. from sklearn.preprocessing import RobustScaler
  27. from sklearn.feature_selection import RFE
  28. # from sklearn.pipeline import Pipeline # mute for SMOTE pipeline
  29. from imblearn.pipeline import Pipeline
  30. from sklearn.utils import shuffle
  31. from sklearn.metrics import mean_squared_error, make_scorer, r2_score
  32. from sklearn.model_selection import KFold, train_test_split, LeaveOneOut, StratifiedKFold, cross_validate, GridSearchCV, \
  33. cross_val_score, cross_val_predict, RepeatedStratifiedKFold, RandomizedSearchCV, StratifiedShuffleSplit, learning_curve,\
  34. StratifiedGroupKFold
  35. from skopt.space import Integer, Real, Categorical
  36. from skopt import BayesSearchCV, Optimizer
  37. from statsmodels import robust # if needed for median_abs_deviation; otherwise use scipy.stats
  38. from scipy.stats import median_abs_deviation
  39. np.int = int # Patch to allow legacy code to work
  40. import matplotlib.pyplot as plt
  41. import shap
  42. from scipy.stats import ttest_ind, pearsonr, binomtest, spearmanr, kendalltau, pointbiserialr, median_abs_deviation
  43. from neuroHarmonize import harmonizationLearn, harmonizationApply
  44. import xgboost as xgb
  45. from xgboost import XGBRegressor, XGBClassifier
  46. from imblearn.over_sampling import SMOTE # or other sampler
  47. import random
  48. from sklearn.metrics import (
  49. roc_auc_score,
  50. average_precision_score,
  51. f1_score,
  52. balanced_accuracy_score,
  53. precision_score,
  54. recall_score,
  55. confusion_matrix,
  56. classification_report
  57. )
  58. ################################################################################################################
  59. # Load the data
  60. # --- Utility Functions ---
  61. def remove_substrings(df, remove_in_cells=False):
  62. """
  63. Remove '+AF8' and '+AC0' from:
  64. - row labels (index)
  65. - column labels (columns)
  66. - optionally from the cell contents themselves
  67. """
  68. # Remove from row labels (index)
  69. df.index = df.index.astype(str)
  70. df.index = df.index.str.replace('+AF8', '', regex=False).str.replace('+AC0', '', regex=False)
  71. # Remove from index name if present
  72. if df.index.name:
  73. df.index.name = str(df.index.name).replace('+AF8', '').replace('+AC0', '')
  74. # Remove from column labels (columns)
  75. df.columns = df.columns.astype(str)
  76. df.columns = df.columns.str.replace('+AF8', '', regex=False).str.replace('+AC0', '', regex=False)
  77. # Remove from column name if present
  78. if df.columns.name:
  79. df.columns.name = str(df.columns.name).replace('+AF8', '').replace('+AC0', '')
  80. # Optionally remove from cell contents
  81. if remove_in_cells:
  82. # This will only affect string cells; numeric columns remain unchanged
  83. df.replace({r'\+AF8': '', r'\+AC0': ''}, regex=True, inplace=True)
  84. return df
  85. def get_config(i: int,
  86. j: int,
  87. base_x_dir="/.../EMBARC/data/06_BART_regression/Input/x",
  88. base_y_dir="/.../EMBARC/data/06_BART_regression/Input/y/Imputation/classification/Remission",
  89. base_out_dir="/.../EMBARC/data/06_BART_regression/Output/Classification_plot/Remission/Tier2b",
  90. x_subpath="Site_normalization/Tier2b",
  91. y_prefix="deltaHAMD",
  92. out_suffix="save_feature_and_model",
  93. tier_label="Tier2b"):
  94. """
  95. i mapping:
  96. 1 -> ses-1 SER
  97. 2 -> ses-1 PLA
  98. 3 -> ses-2 SER
  99. 4 -> ses-2 PLA
  100. j controls the output folder prefix:
  101. e.g., j=1 -> "01_..."
  102. j=11 -> "11_..."
  103. """
  104. mapping = {
  105. 1: ("ses-1", "SER"),
  106. 2: ("ses-1", "PLA"),
  107. 3: ("ses-2", "SER"),
  108. 4: ("ses-2", "PLA"),
  109. }
  110. if i not in mapping:
  111. raise ValueError("i must be 1, 2, 3, or 4.")
  112. ses_number, medication = mapping[i]
  113. # X filename like: Tier1_selected_ses-1_SER.csv
  114. x_filename = f"{tier_label}_selected_{ses_number}_{medication}.csv"
  115. x_path = os.path.join(base_x_dir, x_subpath, x_filename)
  116. # Y filename like: deltaHAMD_ses_1_SER.csv (ses_1 not ses-1)
  117. ses_for_y = ses_number.replace("ses-", "ses_")
  118. y_filename = f"{y_prefix}_{ses_for_y}_{medication}.csv"
  119. y_path = os.path.join(base_y_dir, y_filename)
  120. # Output folder like: 11_ses-1_SER_save_feature_and_model
  121. out_folder = f"{int(j):02d}_{ses_number}_{medication}_{out_suffix}"
  122. output_path = os.path.join(base_out_dir, out_folder)
  123. os.makedirs(output_path, exist_ok=True)
  124. return x_path, y_path, output_path, medication, ses_number
  125. i = 4
  126. j = 8
  127. x_path, y_path, output_path, medication, ses_number = get_config(i, j)
  128. print("x_path:", x_path)
  129. print("y_path:", y_path)
  130. print("output_path:", output_path)
  131. print("medication:", medication)
  132. print("ses_number:", ses_number)
  133. feature_number = 413 # model A: 14; model B: 1302; model C: 413; ablation: 0
  134. random_seed = 42
  135. np.random.seed(random_seed)
  136. random.seed(random_seed)
  137. selected_features = 10
  138. max_display = 20
  139. # Process X
  140. # X_df = pd.read_csv(x_path, index_col=0, header = None) # No labels for columns
  141. X_df = pd.read_csv(x_path, index_col=0) # have labels for columns
  142. X_df = remove_substrings(X_df, remove_in_cells=True)
  143. X = X_df.T # Becareful!!!
  144. feature_names = X_df.index.tolist() # Row is feature name
  145. # X = X_df
  146. # feature_names = X_df.columns.tolist() # Columns is feature name
  147. feature_names = [fname.replace("original-", "") for fname in feature_names]
  148. print("X shape (subjects, features):", X.shape) # Expected (93, 1302)
  149. # print(repr(X.columns))
  150. # Process y
  151. y_df = pd.read_csv(y_path, header=None, encoding='utf-8-sig')
  152. y_df = remove_substrings(y_df, remove_in_cells=True)
  153. y = y_df.to_numpy(dtype=np.float64)
  154. print("y shape:", y.shape) # Expected (93,)
  155. n_samples = X.shape[0]
  156. n_features = X.shape[1]
  157. print('After filtering by medication: n_samples:', n_samples, '; n_features:', n_features, '\n')
  158. #######################################################################################################################
  159. class CustomImputer(BaseEstimator, TransformerMixin):
  160. """
  161. Custom transformer that:
  162. 1. Imputes BMI by median
  163. 2. Imputes is_employed by most frequent
  164. 3. IterativeImputer for MASQ w0/w1 columns (aa/ad/gd)
  165. 4. IterativeImputer for w1_score_17 (using w0, w2, w3, w4, w6)
  166. 5. Creates r1_score_17 = w1_score_17 / w0_score_17
  167. 6. IterativeImputer for shaps_total_continuous-w0/w1
  168. """
  169. def __init__(self, session='ses-1', n_neighbors=5, random_state=0):
  170. self.session = session
  171. self.n_neighbors = n_neighbors
  172. self.random_state = random_state
  173. # --------------------------------------
  174. # 1) BMI (median) / 2) is_employed (mode)
  175. # --------------------------------------
  176. self.bmi_median_ = None
  177. self.employment_mode_ = None
  178. # --------------------------------------
  179. # 3) IterativeImputer for MASQ w1
  180. # --------------------------------------
  181. self.masq_w1_cols_ = [
  182. "masq2-score-aa-w0", "masq2-score-aa-w1",
  183. "masq2-score-ad-w0", "masq2-score-ad-w1",
  184. "masq2-score-gd-w0", "masq2-score-gd-w1"
  185. ]
  186. self.masq_w1_iter_ = IterativeImputer(random_state=self.random_state)
  187. # --------------------------------------
  188. # 4) IterativeImputer for w1_score_17
  189. # --------------------------------------
  190. self.w1_cols_ = [
  191. "w0-score-17", "w1-score-17", "w2-score-17", "w3-score-17",
  192. "w4-score-17", "w6-score-17"
  193. ]
  194. self.w1_iter_ = IterativeImputer(random_state=self.random_state)
  195. # --------------------------------------
  196. # 6) IterativeImputer for shaps
  197. # --------------------------------------
  198. self.shaps_w1_cols_ = [
  199. "shaps-total-continuous-w0",
  200. "shaps-total-continuous-w1"
  201. ]
  202. self.shaps_w1_iter_ = IterativeImputer(random_state=self.random_state)
  203. def _convert_numeric(self, df):
  204. """
  205. Helper method to convert relevant columns to numeric.
  206. """
  207. numeric_cols = set()
  208. # BMI (if exists)
  209. if "BMI" in df.columns:
  210. numeric_cols.add("BMI")
  211. # MASQ columns
  212. numeric_cols.update([c for c in self.masq_w1_cols_ if c in df.columns])
  213. # w1 score columns (including w0_score_17 and w1_score_17 for division later)
  214. numeric_cols.update([c for c in self.w1_cols_ if c in df.columns])
  215. # shaps columns
  216. numeric_cols.update([c for c in self.shaps_w1_cols_ if c in df.columns])
  217. for col in numeric_cols:
  218. df[col] = pd.to_numeric(df[col], errors="coerce")
  219. return df
  220. def fit(self, X, y=None):
  221. X_fit = X.copy()
  222. # Convert all relevant columns to numeric
  223. X_fit = self._convert_numeric(X_fit)
  224. # --------------------------------------
  225. # 1) Fit median for BMI
  226. # --------------------------------------
  227. if "BMI" in X_fit.columns:
  228. self.bmi_median_ = X_fit["BMI"].median()
  229. # --------------------------------------
  230. # 2) Fit mode for is_employed
  231. # --------------------------------------
  232. if "is-employed" in X_fit.columns:
  233. self.employment_mode_ = X_fit["is-employed"].mode()[0]
  234. # --------------------------------------
  235. # 3) Fit IterativeImputer for MASQ w1
  236. # --------------------------------------
  237. masq_exist = [c for c in self.masq_w1_cols_ if c in X_fit.columns]
  238. if len(masq_exist) == len(self.masq_w1_cols_):
  239. self.masq_w1_iter_.fit(X_fit[masq_exist])
  240. # --------------------------------------
  241. # 4) Fit IterativeImputer for w1_score_17
  242. # --------------------------------------
  243. w1_exist = [c for c in self.w1_cols_ if c in X_fit.columns]
  244. if len(w1_exist) == len(self.w1_cols_):
  245. self.w1_iter_.fit(X_fit[w1_exist])
  246. # --------------------------------------
  247. # 6) Fit IterativeImputer for shaps
  248. # --------------------------------------
  249. shaps_exist = [c for c in self.shaps_w1_cols_ if c in X_fit.columns]
  250. if len(shaps_exist) == len(self.shaps_w1_cols_):
  251. self.shaps_w1_iter_.fit(X_fit[shaps_exist])
  252. return self
  253. def transform(self, X, y=None):
  254. X_out = X.copy()
  255. # Convert all relevant columns to numeric
  256. X_out = self._convert_numeric(X_out)
  257. # --------------------------------------
  258. # 1) Impute BMI by median
  259. # --------------------------------------
  260. if self.bmi_median_ is not None and "BMI" in X_out.columns:
  261. X_out["BMI"] = X_out["BMI"].fillna(self.bmi_median_)
  262. # --------------------------------------
  263. # 2) Impute is_employed by most frequent
  264. # --------------------------------------
  265. if self.employment_mode_ is not None and "is-employed" in X_out.columns:
  266. X_out["is-employed"] = X_out["is-employed"].fillna(self.employment_mode_)
  267. # --------------------------------------
  268. # 3) IterativeImputer for MASQ w1
  269. # --------------------------------------
  270. masq_exist = [c for c in self.masq_w1_cols_ if c in X_out.columns]
  271. if len(masq_exist) == len(self.masq_w1_cols_):
  272. X_out[masq_exist] = self.masq_w1_iter_.transform(X_out[masq_exist])
  273. # --------------------------------------
  274. # 4) IterativeImputer for w1_score_17
  275. # --------------------------------------
  276. w1_exist = [c for c in self.w1_cols_ if c in X_out.columns]
  277. if len(w1_exist) == len(self.w1_cols_):
  278. X_out[w1_exist] = self.w1_iter_.transform(X_out[w1_exist])
  279. # --------------------------------------
  280. # 5) Create r1_score_17 = w1_score_17 / w0_score_17
  281. # --------------------------------------
  282. if "w1-score-17" in X_out.columns and "w0-score-17" in X_out.columns:
  283. X_out["r1-score-17"] = X_out.apply(
  284. lambda row: row["w1-score-17"] / row["w0-score-17"]
  285. if row["w0-score-17"] != 0 else np.nan,
  286. axis=1
  287. )
  288. # --------------------------------------
  289. # 6) IterativeImputer for shaps
  290. # --------------------------------------
  291. shaps_exist = [c for c in self.shaps_w1_cols_ if c in X_out.columns]
  292. if len(shaps_exist) == len(self.shaps_w1_cols_):
  293. X_out[shaps_exist] = self.shaps_w1_iter_.transform(X_out[shaps_exist])
  294. return X_out
  295. class DropColumns(BaseEstimator, TransformerMixin):
  296. def __init__(self, session='ses-1', cols_to_drop_1=None, cols_to_drop_2=None):
  297. self.session = session
  298. self.cols_to_drop_1 = cols_to_drop_1 or []
  299. self.cols_to_drop_2 = cols_to_drop_2 or []
  300. # This will hold the final columns we decide to drop
  301. self.cols_to_drop_ = None
  302. def fit(self, X, y=None):
  303. if self.session == 'ses-1':
  304. self.cols_to_drop_ = self.cols_to_drop_1
  305. else: # session == 'ses-2'
  306. self.cols_to_drop_ = self.cols_to_drop_2
  307. return self
  308. def transform(self, X, y=None):
  309. X_copy = X.copy()
  310. return X_copy.drop(columns=self.cols_to_drop_, errors='ignore')
  311. class NeuroHarmonizeTransformer(BaseEstimator, TransformerMixin):
  312. def __init__(
  313. self,
  314. feature_number: int,
  315. covariate_cols=("Site", "Age", "Age-squared", "Gender"),
  316. skip_combat: bool = False,
  317. enforce_train_column_order: bool = True,
  318. ):
  319. self.feature_number = int(feature_number)
  320. self.covariate_cols = tuple(covariate_cols)
  321. self.skip_combat = bool(skip_combat)
  322. self.enforce_train_column_order = bool(enforce_train_column_order)
  323. self.model_ = None
  324. self.feature_cols_ = None
  325. self.covariate_cols_resolved_ = None
  326. @staticmethod
  327. def _resolve_columns_case_insensitive(df_cols, wanted):
  328. """Return a list of actual column names in df that match `wanted` case-insensitively."""
  329. lower_map = {c.lower(): c for c in df_cols}
  330. resolved = profiler = []
  331. resolved = []
  332. for w in wanted:
  333. key = w.lower()
  334. if key in lower_map:
  335. resolved.append(lower_map[key])
  336. else:
  337. resolved.append(None)
  338. return resolved
  339. def _build_covariates(self, X: pd.DataFrame) -> pd.DataFrame:
  340. # resolve covariate column names robustly (case-insensitive)
  341. resolved = self._resolve_columns_case_insensitive(X.columns, self.covariate_cols)
  342. missing = [w for w, r in zip(self.covariate_cols, resolved) if r is None]
  343. if missing:
  344. raise ValueError(
  345. f"NeuroHarmonizeTransformer: missing covariate columns {missing}. "
  346. f"Available columns include: {list(X.columns)[:20]} ..."
  347. )
  348. cov = X.loc[:, resolved].copy()
  349. # standardize covariate names expected by neuroHarmonize
  350. rename_map = {}
  351. for orig, want in zip(resolved, self.covariate_cols):
  352. wl = want.lower()
  353. if wl == "site":
  354. rename_map[orig] = "SITE"
  355. elif wl == "age":
  356. rename_map[orig] = "age"
  357. elif wl in ("age-squared", "age_squared", "agesquared"):
  358. rename_map[orig] = "age_squared"
  359. elif wl == "gender":
  360. rename_map[orig] = "gender"
  361. else:
  362. rename_map[orig] = want # fallback
  363. cov.rename(columns=rename_map, inplace=True)
  364. # enforce types
  365. cov["SITE"] = cov["SITE"].astype(str)
  366. for c in ["age", "age_squared", "gender"]:
  367. if c in cov.columns:
  368. cov[c] = pd.to_numeric(cov[c], errors="coerce")
  369. return cov
  370. def fit(self, X, y=None):
  371. if not hasattr(X, "columns"):
  372. raise TypeError("NeuroHarmonizeTransformer expects a pandas DataFrame as X (so it can find covariates by name).")
  373. # Optionally lock feature column order from training set
  374. cov_resolved = self._resolve_columns_case_insensitive(X.columns, self.covariate_cols)
  375. self.covariate_cols_resolved_ = [c for c in cov_resolved if c is not None]
  376. self.feature_cols_ = [c for c in X.columns if c not in self.covariate_cols_resolved_]
  377. # Skip ComBat entirely if requested or feature_number <= 0
  378. if self.skip_combat or self.feature_number <= 0:
  379. self.model_ = None
  380. return self
  381. covariate_df = self._build_covariates(X)
  382. # Convert features (excluding covariates) to numeric array
  383. feat_df = X.loc[:, self.feature_cols_].copy()
  384. feat_df = feat_df.apply(pd.to_numeric, errors="coerce")
  385. data = feat_df.to_numpy(dtype=np.float64)
  386. k = min(self.feature_number, data.shape[1])
  387. if k <= 0:
  388. self.model_ = None
  389. return self
  390. radiomics = data[:, :k]
  391. model_out = harmonizationLearn(radiomics, covars=covariate_df)
  392. self.model_ = model_out[0] if isinstance(model_out, tuple) else model_out
  393. return self
  394. def transform(self, X):
  395. if not hasattr(X, "columns"):
  396. raise TypeError("NeuroHarmonizeTransformer expects a pandas DataFrame as X.")
  397. # Reorder columns to match training (prevents train/test column-order drift)
  398. if self.enforce_train_column_order and self.feature_cols_ is not None and self.covariate_cols_resolved_ is not None:
  399. needed = self.feature_cols_ + self.covariate_cols_resolved_
  400. missing = [c for c in needed if c not in X.columns]
  401. if missing:
  402. raise ValueError(
  403. f"NeuroHarmonizeTransformer: input is missing columns seen at fit(): {missing[:10]} ..."
  404. )
  405. X_use = X.loc[:, needed].copy()
  406. else:
  407. # still exclude covariates by name
  408. cov_resolved = self._resolve_columns_case_insensitive(X.columns, self.covariate_cols)
  409. cov_resolved = [c for c in cov_resolved if c is not None]
  410. feat_cols = [c for c in X.columns if c not in cov_resolved]
  411. X_use = X.loc[:, feat_cols + cov_resolved].copy()
  412. self.feature_cols_ = feat_cols
  413. self.covariate_cols_resolved_ = cov_resolved
  414. # Build covariates by NAME
  415. covariate_df = self._build_covariates(X_use)
  416. # Features (excluding covariates)
  417. feat_df = X_use.loc[:, self.feature_cols_].copy()
  418. feat_df = feat_df.apply(pd.to_numeric, errors="coerce")
  419. data = feat_df.to_numpy(dtype=np.float64)
  420. # If skipping, just return features (radiomics+clinical) unchanged (covars excluded)
  421. k = min(self.feature_number, data.shape[1])
  422. if self.skip_combat or self.model_ is None or k <= 0:
  423. return data
  424. radiomics = data[:, :k]
  425. clinical = data[:, k:]
  426. harmonized = harmonizationApply(radiomics, covars=covariate_df, model=self.model_)
  427. if isinstance(harmonized, tuple):
  428. harmonized = harmonized[0]
  429. return np.hstack([harmonized, clinical]).astype(np.float64)
  430. class CustomFeatureSelector(BaseEstimator, TransformerMixin):
  431. def __init__(self, estimator, n_features_to_select= selected_features, corr_th=0.8):
  432. self.estimator = estimator
  433. self.n_features_to_select = n_features_to_select
  434. self.corr_th = corr_th
  435. def fit(self, X, y=None):
  436. # IMPORTANT: specify importance_getter for XGBoost
  437. selector = RFE(
  438. estimator=self.estimator,
  439. n_features_to_select=self.n_features_to_select,
  440. step=1,
  441. importance_getter='feature_importances_'
  442. )
  443. self.selected_features_indices_ = self.selectNonIntercorrelated(X, y, self.corr_th, selector)
  444. return self
  445. def transform(self, X):
  446. return X[:, self.selected_features_indices_]
  447. def selectNonIntercorrelated(self, X, y, corr_th, selector):
  448. non_nan_indices = np.all(~np.isnan(X), axis=0)
  449. X_non_nan = X[:, non_nan_indices]
  450. mad_values = median_abs_deviation(X_non_nan, axis=0, scale='normal')
  451. non_zero_var_indices = mad_values > 0.001
  452. X_non_zero_var = X_non_nan[:, non_zero_var_indices]
  453. if X_non_zero_var.shape[1] == 0:
  454. raise ValueError("All features have zero MAD")
  455. corr_matrix = np.corrcoef(X_non_zero_var, rowvar=False)
  456. np.fill_diagonal(corr_matrix, 0)
  457. mean_absolute_corr = np.abs(corr_matrix).mean(axis=0)
  458. intercorrelated_features_set = set()
  459. high_corrs = np.argwhere(np.abs(corr_matrix) > corr_th)
  460. for i, j in high_corrs:
  461. if mean_absolute_corr[i] > mean_absolute_corr[j]:
  462. intercorrelated_features_set.add(i)
  463. else:
  464. intercorrelated_features_set.add(j)
  465. non_intercorrelated_indices = list(
  466. set(range(X_non_zero_var.shape[1])) - intercorrelated_features_set
  467. )
  468. X_train_non_intercorrelated = X_non_zero_var[:, non_intercorrelated_indices]
  469. if X_train_non_intercorrelated.shape[1] <= self.n_features_to_select:
  470. selected_indices = np.array(non_intercorrelated_indices)
  471. else:
  472. selector = selector.fit(X_train_non_intercorrelated, y)
  473. support = selector.get_support()
  474. selected_indices = np.array(non_intercorrelated_indices)[support]
  475. # Map back to original feature indices
  476. final_mask = np.zeros(non_nan_indices.shape[0], dtype=bool)
  477. non_nan_zero_var = np.where(non_nan_indices)[0][non_zero_var_indices]
  478. final_mask[non_nan_zero_var[selected_indices]] = True
  479. return np.where(final_mask)[0]
  480. # ------------------ Build the Pipeline with XGBoost ------------------
  481. ComBat_transformer = NeuroHarmonizeTransformer(
  482. feature_number=feature_number,
  483. covariate_cols=("Site", "Age", "Age-squared", "Gender"),
  484. skip_combat=False, # <-- set to True to skip ComBat and just pass through features (but still require covariates for consistent column handling)
  485. enforce_train_column_order=True,
  486. )
  487. # Base XGBoost estimator for RFE
  488. selector_estimator = XGBRegressor(random_state=random_seed, eval_metric='rmse')
  489. # ses-1
  490. cols_to_drop_1 = ['subject-id','Medication','w1-score-17','r1-score-17',\
  491. 'shaps-total-continuous-w1','masq2-score-aa-w1','masq2-score-ad-w1','masq2-score-gd-w1',\
  492. 'w2-score-17','w3-score-17','w4-score-17','w6-score-17','w8-score-17','w9-score-17',\
  493. 'w10-score-17','w12-score-17','w16-score-17']
  494. # ses-2
  495. cols_to_drop_2 = ['subject-id','Medication', 'w2-score-17','w3-score-17','w4-score-17','w6-score-17',\
  496. 'w8-score-17','w9-score-17', 'w10-score-17','w12-score-17','w16-score-17']
  497. # ------------------ Hyperparameter Search Space ------------------
  498. # Example hyperparams to tune in both the selector's XGBoost and the final XGBoost
  499. pbounds = {
  500. # XGBoost in RFE
  501. 'selector__estimator__n_estimators': Integer(50, 300),
  502. 'selector__estimator__max_depth': Integer(2, 8),
  503. 'selector__estimator__learning_rate': Real(1e-3, 1e-1, prior='log-uniform'),
  504. 'selector__estimator__subsample': Real(0.5, 1.0),
  505. 'selector__estimator__colsample_bytree': Real(0.5, 1.0),
  506. 'selector__estimator__gamma': Real(0, 10),
  507. 'selector__estimator__reg_alpha': Real(1e-3, 1e1, prior='log-uniform'),
  508. 'selector__estimator__reg_lambda': Real(1e-3, 1e1, prior='log-uniform'),
  509. # Final XGBoost
  510. 'xgb__n_estimators': Integer(50, 300),
  511. 'xgb__max_depth': Integer(2, 8),
  512. 'xgb__learning_rate': Real(1e-3, 1e-1, prior='log-uniform'),
  513. 'xgb__subsample': Real(0.5, 1.0),
  514. 'xgb__colsample_bytree': Real(0.5, 1.0),
  515. 'xgb__gamma': Real(0, 10),
  516. 'xgb__reg_alpha': Real(1e-3, 1e1, prior='log-uniform'),
  517. 'xgb__reg_lambda': Real(1e-3, 1e1, prior='log-uniform'),
  518. }
  519. # ------------------ BayesSearchCV ------------------
  520. cv_inner = StratifiedKFold(n_splits=10, shuffle=True, random_state=random_seed)
  521. # ------------------ Outer CV Loop ------------------
  522. cv_outer = StratifiedKFold(n_splits=10, shuffle=True, random_state=random_seed)
  523. # Choose which feature name list to use in the CSV:
  524. NAME_LIST = feature_names
  525. long_csv_path = os.path.join(output_path, "selected_features_shap_long.csv")
  526. # Write header once
  527. csv_f = open(long_csv_path, "w", newline="", encoding="utf-8")
  528. writer = csv.writer(csv_f)
  529. writer.writerow([
  530. "fold",
  531. "split",
  532. "sample_in_fold",
  533. "global_row_id", # optional: stable id within this CSV
  534. "feature_rank_in_fold", # j in selected-feature space
  535. "feature_index", # index in full feature space
  536. "feature_name",
  537. "shap_value",
  538. "x_value",
  539. ])
  540. global_row_id = 0
  541. # Collect per-fold metadata to save as npy at end
  542. fold_ids = []
  543. selected_indices_per_fold = []
  544. selected_feature_names_per_fold = []
  545. train_indices_per_fold = [] # optional but very useful
  546. test_indices_per_fold = [] # optional but very useful
  547. y_pred_list = []
  548. y_true_list = []
  549. y_proba_list = []
  550. fold_r2 = []
  551. fold_rmse = []
  552. outer_fold_counter = 0
  553. total_outer_folds = cv_outer.get_n_splits()
  554. best_models = []
  555. all_train_shap_values = []
  556. all_test_shap_values = []
  557. all_best_X_train = []
  558. all_best_X_test = []
  559. elapsed_times = []
  560. selected_indices_list = []
  561. best_models = []
  562. fold_auc = []
  563. fold_ap = []
  564. fold_f1 = []
  565. fold_bacc = []
  566. fold_precision = []
  567. fold_recall = []
  568. fold_specificity = []
  569. print(
  570. f"[{datetime.now().strftime('%H:%M:%S')}] Progress - Outer Folds: 0.00% | Start to process the first iteration of outer folds")
  571. for train_index, test_index in cv_outer.split(X, y):
  572. start_time = time.time()
  573. outer_fold_counter += 1
  574. # X_train, X_test = X[train_index], X[test_index] # X as numpy array
  575. X_train, X_test = X.iloc[train_index], X.iloc[test_index] # X as data frame
  576. y_train, y_test = y[train_index], y[test_index]
  577. # 1) Compute your imbalance ratio on THIS training fold
  578. neg = (y_train == 0).sum()
  579. pos = (y_train == 1).sum()
  580. scale_pos_weight = neg / pos
  581. print(f"[Fold {outer_fold_counter}] neg={neg}, pos={pos}, scale_pos_weight={scale_pos_weight:.2f}")
  582. # 2) Rebuild your pipeline so that the classifier gets the correct weight
  583. pipeline = Pipeline([
  584. # Step 1: Impute the entire clinical_df (294 subjects)
  585. ("impute_clinical", CustomImputer(session=ses_number)),
  586. # Step 2: Drop columns (depends on session, and your predefined lists)
  587. ("drop_cols", DropColumns(
  588. session=ses_number,
  589. cols_to_drop_1=cols_to_drop_1,
  590. cols_to_drop_2=cols_to_drop_2
  591. )),
  592. # Step 3: NeuroCombat with covariates
  593. ("Combat", ComBat_transformer), # Assuming you've already configured covariates inside
  594. # Step 4: Scale
  595. ("scaler", RobustScaler()),
  596. # Step 5: inject a sampler to synthetically balance the minority class ---
  597. ("smote", SMOTE(random_state=random_seed, sampling_strategy="auto")),
  598. # Step 6: Feature selection
  599. ("selector", CustomFeatureSelector(estimator=selector_estimator)),
  600. # Step 7: XGB
  601. # --- NEW: classification model with imbalance handling ---
  602. ("xgb", XGBClassifier(
  603. # tree_method='gpu_hist', # <-- switch on GPU
  604. # predictor='gpu_predictor', # <-- use the CUDA predictor
  605. random_state=random_seed,
  606. use_label_encoder=False, # suppress deprecation warning
  607. objective='binary:logistic',
  608. eval_metric='auc', # or 'logloss', 'aucpr'
  609. scale_pos_weight=scale_pos_weight, # balance via built‑in weighting
  610. ))
  611. ])
  612. # 3) Plug THAT pipeline into your BayesSearchCV
  613. optimizer = BayesSearchCV(
  614. estimator=pipeline, # must end in XGBClassifier
  615. search_spaces=pbounds, # tuned hyperparams for classifier
  616. n_iter=50,
  617. scoring='roc_auc', # or 'average_precision', 'f1'
  618. n_jobs=-1,
  619. cv=cv_inner,
  620. random_state=random_seed
  621. )
  622. # 4) Fit the Bayesian search on the training fold
  623. optimizer.fit(X_train, y_train)
  624. print(f" Best inner-fold ROC AUC: {optimizer.best_score_:.4f}")
  625. print(f" Best params: {optimizer.best_params_}")
  626. # Evaluate on the test fold
  627. best_pipeline = optimizer.best_estimator_
  628. best_models.append(best_pipeline)
  629. fold_id = outer_fold_counter # or i, however you index folds
  630. joblib.dump(best_pipeline, os.path.join(output_path, f"best_pipeline_fold{fold_id}.joblib"))
  631. y_pred = best_pipeline.predict(X_test)
  632. y_pred_list.extend(y_pred)
  633. y_true_list.extend(y_test)
  634. y_proba = best_pipeline.predict_proba(X_test)[:, 1]
  635. y_proba_list.extend(y_proba)
  636. # Compute metrics
  637. auc = roc_auc_score(y_test, y_proba)
  638. ap = average_precision_score(y_test, y_proba)
  639. f1 = f1_score(y_test, y_pred)
  640. bacc = balanced_accuracy_score(y_test, y_pred)
  641. precision = precision_score(y_test, y_pred)
  642. recall = recall_score(y_test, y_pred)
  643. tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
  644. specificity = tn / (tn + fp)
  645. fold_auc.append(auc)
  646. fold_ap.append(ap)
  647. fold_f1.append(f1)
  648. fold_bacc.append(bacc)
  649. fold_precision.append(precision)
  650. fold_recall.append(recall)
  651. fold_specificity.append(specificity)
  652. print(
  653. f"Fold {outer_fold_counter} — "
  654. f"AUC: {auc:.3f}, AP: {ap:.3f}, F1: {f1:.3f}, "
  655. f"BalAcc: {bacc:.3f}, Precision: {precision:.3f}, "
  656. f"Recall: {recall:.3f}, Specificity: {specificity:.3f}"
  657. )
  658. # --------------- SHAP for XGBoost ---------------
  659. # We'll illustrate using TreeExplainer on the final XGBRegressor
  660. transform_pipeline = Pipeline(best_pipeline.steps[:-1]) # all steps except the final xgb
  661. selected_indices = best_pipeline.named_steps['selector'].selected_features_indices_
  662. selected_indices_list.append(selected_indices)
  663. final_xgb = best_pipeline.named_steps['xgb']
  664. explainer = shap.TreeExplainer(final_xgb)
  665. best_X_train = transform_pipeline.transform(X_train)
  666. best_X_test = transform_pipeline.transform(X_test)
  667. # Compute the SHAP values
  668. shap_values_train = explainer.shap_values(best_X_train)
  669. shap_values_test = explainer.shap_values(best_X_test)
  670. # --- unwrap SHAP if it comes as a list (binary classification sometimes does this) ---
  671. if isinstance(shap_values_train, list):
  672. shap_values_train = shap_values_train[0]
  673. if isinstance(shap_values_test, list):
  674. shap_values_test = shap_values_test[0]
  675. # Selected indices (full feature space)
  676. selected_indices = best_pipeline.named_steps['selector'].selected_features_indices_
  677. selected_indices = np.array(selected_indices, dtype=int)
  678. # Selected names (same length as selected_indices)
  679. selected_names = [NAME_LIST[i] for i in selected_indices]
  680. # Save per-fold info
  681. fold_ids.append(outer_fold_counter)
  682. selected_indices_per_fold.append(selected_indices)
  683. selected_feature_names_per_fold.append(np.array(selected_names, dtype=object))
  684. train_indices_per_fold.append(np.array(train_index, dtype=int)) # mapping back to original X rows
  685. test_indices_per_fold.append(np.array(test_index, dtype=int))
  686. # Sanity checks
  687. assert shap_values_train.shape == best_X_train.shape, "Train SHAP shape != Train X shape"
  688. assert shap_values_test.shape == best_X_test.shape, "Test SHAP shape != Test X shape"
  689. assert shap_values_train.shape[1] == len(selected_indices), "Train SHAP cols != #selected features"
  690. assert shap_values_test.shape[1] == len(selected_indices), "Test SHAP cols != #selected features"
  691. # --- stream TRAIN rows ---
  692. n_train = shap_values_train.shape[0]
  693. for s in range(n_train):
  694. for j, (full_idx, fname) in enumerate(zip(selected_indices, selected_names)):
  695. writer.writerow([
  696. outer_fold_counter,
  697. "train",
  698. s,
  699. global_row_id,
  700. j,
  701. int(full_idx),
  702. fname,
  703. float(shap_values_train[s, j]),
  704. float(best_X_train[s, j]),
  705. ])
  706. global_row_id += 1
  707. # --- stream TEST rows ---
  708. n_test = shap_values_test.shape[0]
  709. for s in range(n_test):
  710. for j, (full_idx, fname) in enumerate(zip(selected_indices, selected_names)):
  711. writer.writerow([
  712. outer_fold_counter,
  713. "test",
  714. s,
  715. global_row_id,
  716. j,
  717. int(full_idx),
  718. fname,
  719. float(shap_values_test[s, j]),
  720. float(best_X_test[s, j]),
  721. ])
  722. global_row_id += 1
  723. # (optional) flush to ensure progress is written even if job crashes later
  724. csv_f.flush()
  725. # Store the SHAP values and the best X
  726. all_train_shap_values.append(shap_values_train)
  727. all_test_shap_values.append(shap_values_test)
  728. all_best_X_train.append(best_X_train)
  729. all_best_X_test.append(best_X_test)
  730. elapsed_time = time.time() - start_time
  731. elapsed_times.append(elapsed_time)
  732. progress = outer_fold_counter / total_outer_folds * 100
  733. print(f"[{datetime.now().strftime('%H:%M:%S')}] Outer fold {outer_fold_counter}/{total_outer_folds} complete. Progress: {progress:.2f}%. "
  734. f"Time for this fold: {elapsed_time:.2f} seconds.")
  735. # # Checking for each step's shape
  736. # pipeline1 = pipeline.fit(X,y)
  737. # X1 = pipeline.steps[0][1].fit_transform(X_train)
  738. # print("X shape in step 1: ", X1.shape)
  739. # X2 = pipeline.steps[1][1].fit_transform(X1)
  740. # print("X shape in step 2: ", X2.shape)
  741. # print(repr(X2.columns))
  742. # X3 = pipeline.steps[2][1].fit_transform(X2)
  743. # print("X shape in step 3: ", X3.shape)
  744. # X4 = pipeline.steps[3][1].fit_transform(X3)
  745. # print("X shape in step 4: ", X4.shape)
  746. # X5 = pipeline.steps[4][1].fit_transform(X4)
  747. # print("X shape in step 5: ", X5.shape)
  748. # # X6 = pipeline.steps[5][1].transform(X5)
  749. # # print("X shape in step 6: ", X6.shape)
  750. # Save the selected features
  751. csv_f.close()
  752. print("Saved long-format CSV:", long_csv_path)
  753. np.save(os.path.join(output_path, "fold_ids.npy"),
  754. np.array(fold_ids, dtype=int))
  755. np.save(os.path.join(output_path, "selected_indices_per_fold.npy"),
  756. np.array(selected_indices_per_fold, dtype=object),
  757. allow_pickle=True)
  758. np.save(os.path.join(output_path, "selected_feature_names_per_fold.npy"),
  759. np.array(selected_feature_names_per_fold, dtype=object),
  760. allow_pickle=True)
  761. # optional but recommended
  762. np.save(os.path.join(output_path, "train_indices_per_fold.npy"),
  763. np.array(train_indices_per_fold, dtype=object),
  764. allow_pickle=True)
  765. np.save(os.path.join(output_path, "test_indices_per_fold.npy"),
  766. np.array(test_indices_per_fold, dtype=object),
  767. allow_pickle=True)
  768. print("Saved per-fold selected indices/names and index mappings in:", output_path)
  769. # Flatten into 1D arrays
  770. y_true_arr = np.array(y_true_list).ravel().astype(int)
  771. y_pred_arr = np.array(y_pred_list).ravel().astype(int)
  772. y_proba_arr = np.array(y_proba_list)
  773. assert y_true_arr.shape == y_pred_arr.shape, "Mismatch in true vs pred lengths"
  774. n = len(y_true_arr)
  775. # ----------------- Binomial test on accuracy -----------------
  776. k = int((y_true_arr == y_pred_arr).sum())
  777. res = binomtest(k, n, p= 0.5, alternative="greater")
  778. binom_p = res.pvalue
  779. prop_hat = res.proportion_estimate
  780. print(f"Accuracy = {k}/{n} = {k/n:.3f}")
  781. print(f"Estimated p̂ = {prop_hat:.3f}, Binomial p‑value = {binom_p:.4f}")
  782. # ----------------- Compute pooled summary metrics -----------------
  783. overall_auc = roc_auc_score(y_true_arr, y_proba_arr)
  784. overall_ap = average_precision_score(y_true_arr, y_proba_arr)
  785. overall_f1 = f1_score(y_true_arr, y_pred_arr)
  786. overall_bacc = balanced_accuracy_score(y_true_arr, y_pred_arr)
  787. overall_ppv = precision_score(y_true_arr, y_pred_arr)
  788. overall_rec = recall_score(y_true_arr, y_pred_arr)
  789. tn, fp, fn, tp = confusion_matrix(y_true_arr, y_pred_arr).ravel()
  790. overall_spec = tn / (tn + fp)
  791. # NNT = 1 / (recall + specificity – 1)
  792. youden_j = overall_rec + overall_spec - 1.0
  793. overall_nnt = (1.0 / youden_j) if youden_j > 0 else np.inf
  794. # — per‑fold averages for reporting
  795. avg_auc = np.mean(fold_auc)
  796. avg_ap = np.mean(fold_ap)
  797. avg_f1 = np.mean(fold_f1)
  798. avg_bacc = np.mean(fold_bacc)
  799. avg_ppv = np.mean(fold_precision) # PPV is same as precision
  800. avg_rec = np.mean(fold_recall)
  801. avg_spec = np.mean(fold_specificity)
  802. # ----------------- Permutation tests -----------------
  803. n_permutations = 1000
  804. rng = np.random.RandomState(random_seed)
  805. perm_aucs = []
  806. perm_baccs = []
  807. perm_ppvs = []
  808. perm_nnts = []
  809. for _ in range(n_permutations):
  810. y_perm = rng.permutation(y_true_arr)
  811. perm_aucs.append(roc_auc_score(y_perm, y_proba_arr))
  812. perm_baccs.append(balanced_accuracy_score(y_perm, y_pred_arr))
  813. perm_ppvs.append(precision_score(y_perm, y_pred_arr, zero_division=0))
  814. r_p = recall_score(y_perm, y_pred_arr, zero_division=0)
  815. tn_p, fp_p, fn_p, tp_p = confusion_matrix(y_perm, y_pred_arr).ravel()
  816. spec_p = tn_p / (tn_p + fp_p) if (tn_p + fp_p) > 0 else 0
  817. j_p = r_p + spec_p - 1.0
  818. perm_nnts.append((1.0 / j_p) if j_p > 0 else np.inf)
  819. perm_p_auc = np.mean(np.array(perm_aucs) >= overall_auc)
  820. perm_p_bacc = np.mean(np.array(perm_baccs) >= overall_bacc)
  821. perm_p_ppv = np.mean(np.array(perm_ppvs) >= overall_ppv)
  822. perm_p_nnt = np.mean(np.array(perm_nnts) <= overall_nnt)
  823. # ----------------- Bootstrap 95% CI for ROC AUC -----------------
  824. n_bootstraps = 1000
  825. boot_aucs = []
  826. for _ in range(n_bootstraps):
  827. idx = rng.randint(0, n, n)
  828. boot_aucs.append(roc_auc_score(y_true_arr[idx], y_proba_arr[idx]))
  829. ci_lower, ci_upper = np.percentile(boot_aucs, [2.5, 97.5])
  830. print(f"\nBootstrapped ROC AUC 95% CI: {ci_lower:.3f} – {ci_upper:.3f}")
  831. print("\nPermutation p‑values:")
  832. print(f" ROC AUC p‑value: {perm_p_auc:.4f}")
  833. print(f" bACC p‑value: {perm_p_bacc:.4f}")
  834. print(f" PPV p‑value: {perm_p_ppv:.4f}")
  835. print(f" NNT p‑value: {perm_p_nnt:.4f}")
  836. # ----------------- Save everything to CSV -----------------
  837. metrics_df = pd.DataFrame({
  838. "Metric": [
  839. # per-fold averages
  840. "Avg ROC AUC", "Avg Avg Precision", "Avg F1-score", "Avg Bal Accuracy",
  841. "Avg PPV", "Avg Recall", "Avg Specificity",
  842. # pooled
  843. "Pooled ROC AUC", "Pooled Avg Precision", "Pooled F1-score",
  844. "Pooled Bal Accuracy", "Pooled PPV", "Pooled Recall",
  845. "Pooled Specificity", "Pooled NNT",
  846. # statistical tests
  847. "Accuracy", "Accuracy p-value (binomial)",
  848. "ROC AUC 95% CI", "ROC AUC p-value",
  849. "bACC p-value", "PPV p-value", "NNT p-value"
  850. ],
  851. "Value": [
  852. # per-fold
  853. f"{avg_auc:.4f}", f"{avg_ap:.4f}", f"{avg_f1:.4f}", f"{avg_bacc:.4f}",
  854. f"{avg_ppv:.4f}", f"{avg_rec:.4f}", f"{avg_spec:.4f}",
  855. # pooled
  856. f"{overall_auc:.4f}", f"{overall_ap:.4f}", f"{overall_f1:.4f}",
  857. f"{overall_bacc:.4f}", f"{overall_ppv:.4f}", f"{overall_rec:.4f}",
  858. f"{overall_spec:.4f}", f"{overall_nnt:.2f}",
  859. # tests
  860. f"{k}/{n}={k/n:.4f}", f"{binom_p:.4f}",
  861. f"{ci_lower:.4f}–{ci_upper:.4f}", f"{perm_p_auc:.4f}",
  862. f"{perm_p_bacc:.4f}", f"{perm_p_ppv:.4f}", f"{perm_p_nnt:.4f}"
  863. ]
  864. })
  865. csv_file = os.path.join(output_path, "classification_metrics.csv")
  866. metrics_df.to_csv(csv_file, index=False)
  867. print(f"\nMetrics saved to: {csv_file}")
  868. # Save each list separately as a .npy file
  869. np.save(os.path.join(output_path, "y_pred_list.npy"), y_pred_list, allow_pickle=True)
  870. np.save(os.path.join(output_path, "y_true_list.npy"), y_true_list, allow_pickle=True)
  871. np.save(os.path.join(output_path, "y_proba_list.npy"), y_proba_list, allow_pickle=True)
  872. np.save(os.path.join(output_path, "all_train_shap_values.npy"), np.array(all_train_shap_values, dtype=object), allow_pickle=True)
  873. np.save(os.path.join(output_path, "all_test_shap_values.npy"), np.array(all_test_shap_values, dtype=object), allow_pickle=True)
  874. np.save(os.path.join(output_path, "all_best_X_train.npy"), np.array(all_best_X_train, dtype=object), allow_pickle=True)
  875. np.save(os.path.join(output_path, "all_best_X_test.npy"), np.array(all_best_X_test, dtype=object), allow_pickle=True)
  876. print("All lists saved as separate .npy files in:", output_path)
  877. # SHAP value plot
  878. unchanged_features = {
  879. "bmi",
  880. "masq2_score_gd",
  881. "shaps_total_continuous",
  882. "w0_score_17",
  883. "w1_score_17",
  884. "w2_score_17",
  885. "w3_score_17",
  886. "w4_score_17",
  887. "w6_score_17",
  888. "interview_age",
  889. "is_male",
  890. "is_employed",
  891. "is_chronic",
  892. "Site",
  893. "age",
  894. "age_squared",
  895. "gender"
  896. }
  897. # Restructure of the feature names
  898. def rename_feature(feature_name):
  899. # If the feature is in the unchanged set, return as-is
  900. if feature_name in unchanged_features:
  901. return feature_name
  902. # Split on underscores
  903. tokens = feature_name.split("_")
  904. # Remove any "original" token
  905. tokens = [t for t in tokens if t != "original"]
  906. # Join back with underscores
  907. short_name = "_".join(tokens)
  908. return short_name
  909. new_feature_names = [rename_feature(fname) for fname in feature_names]
  910. ##
  911. # Map the indices of features to the correct subset for each model (fold)
  912. selected_indices_in_each_model = [
  913. model.named_steps['selector'].selected_features_indices_
  914. for model in best_models
  915. ]
  916. # We'll store the expanded arrays for each fold in lists.
  917. expanded_train_shap_list = []
  918. expanded_train_X_list = []
  919. expanded_test_shap_list = []
  920. expanded_test_X_list = []
  921. n_samples = len(best_models) # number of folds/models
  922. n_features = len(feature_names) # total number of features (e.g., 31)
  923. # Loop over each fold:
  924. for i in range(n_samples):
  925. # Get number of training and test samples for the current fold
  926. n_train_i = all_train_shap_values[i].shape[0] # may differ per fold
  927. n_test_i = all_test_shap_values[i].shape[0]
  928. # Create fold-specific arrays (for the full feature space)
  929. fold_train_shap = np.zeros((n_train_i, n_features))
  930. fold_train_X = np.zeros((n_train_i, n_features))
  931. fold_test_shap = np.zeros((n_test_i, n_features))
  932. fold_test_X = np.zeros((n_test_i, n_features))
  933. # Get selected feature indices for this fold
  934. selected_indices = selected_indices_in_each_model[i]
  935. num_selected_features = len(selected_indices)
  936. # Populate the fold-specific arrays:
  937. for j in range(num_selected_features):
  938. idx = selected_indices[j] # index in the full feature space
  939. # Map training SHAP and X values from the selected subset into the full array
  940. fold_train_shap[:, idx] = all_train_shap_values[i][:, j]
  941. fold_train_X[:, idx] = all_best_X_train[i][:, j]
  942. # Map test SHAP and X values
  943. fold_test_shap[:, idx] = all_test_shap_values[i][:, j]
  944. fold_test_X[:, idx] = all_best_X_test[i][:, j]
  945. # Append the fold-specific arrays to our lists
  946. expanded_train_shap_list.append(fold_train_shap)
  947. expanded_train_X_list.append(fold_train_X)
  948. expanded_test_shap_list.append(fold_test_shap)
  949. expanded_test_X_list.append(fold_test_X)
  950. # Now, concatenate the fold-specific arrays along the sample axis.
  951. reshaped_train_shap_array = np.concatenate(expanded_train_shap_list, axis=0)
  952. reshaped_train_X_array = np.concatenate(expanded_train_X_list, axis=0)
  953. reshaped_test_shap_array = np.concatenate(expanded_test_shap_list, axis=0)
  954. reshaped_test_X_array = np.concatenate(expanded_test_X_list, axis=0)
  955. # Save the new arrays if needed:
  956. np.save(os.path.join(output_path, 'reshaped_train_shap_array.npy'), reshaped_train_shap_array)
  957. np.save(os.path.join(output_path, 'reshaped_train_X_array.npy'), reshaped_train_X_array)
  958. np.save(os.path.join(output_path, 'reshaped_test_shap_array.npy'), reshaped_test_shap_array)
  959. np.save(os.path.join(output_path, 'reshaped_test_X_array.npy'), reshaped_test_X_array)
  960. # Reload the arrays
  961. # reshaped_train_shap_array = np.load(os.path.join(output_path, 'reshaped_train_shap_array.npy'))
  962. # reshaped_train_X_array = np.load(os.path.join(output_path, 'reshaped_train_X_array.npy'))
  963. # reshaped_test_shap_array = np.load(os.path.join(output_path, 'reshaped_test_shap_array.npy'))
  964. # reshaped_test_X_array = np.load(os.path.join(output_path, 'reshaped_test_X_array.npy'))
  965. # Plot the SHAP summary plot for the training set:
  966. plt.figure(figsize=(15, 7))
  967. shap.summary_plot(
  968. reshaped_train_shap_array,
  969. reshaped_train_X_array,
  970. feature_names=new_feature_names,
  971. max_display=max_display,
  972. plot_size=(13, 8),
  973. show=False
  974. )
  975. plt.title('SHAP Value of impact on model output (Training Set)', fontsize=20)
  976. plt.xticks(fontsize=15)
  977. plt.yticks(fontsize=15)
  978. plt.tight_layout()
  979. plt.savefig(os.path.join(output_path, 'shap_summary_training1.png'))
  980. plt.show()
  981. # Plot the SHAP summary plot for the test set:
  982. plt.figure(figsize=(15, 7))
  983. shap.summary_plot(
  984. reshaped_test_shap_array,
  985. reshaped_test_X_array,
  986. feature_names=new_feature_names,
  987. max_display=max_display,
  988. plot_size=(13, 8),
  989. show=False
  990. )
  991. plt.title('SHAP Value of impact on model output (Test Set)', fontsize=20)
  992. plt.xticks(fontsize=15)
  993. plt.yticks(fontsize=15)
  994. plt.tight_layout()
  995. plt.savefig(os.path.join(output_path, 'shap_summary_test1.png'))
  996. plt.show()
  997. # Convert to NumPy arrays if they are Python lists
  998. y_true_arr = np.array(y_true_list)
  999. y_pred_arr = np.array(y_pred_list)
  1000. # Calculate Pearson correlation
  1001. corr, p_value = pearsonr(np.ravel(y_true_arr), np.ravel(y_pred_arr))
  1002. # Create a high-resolution figure
  1003. plt.figure(figsize=(6, 6), dpi=300)
  1004. # Scatter plot: Actual vs. Predicted
  1005. plt.scatter(y_true_arr, y_pred_arr, alpha=0.6, edgecolors='k')
  1006. # Plot best-fit regression line
  1007. p = np.polyfit(np.ravel(y_true_arr), np.ravel(y_pred_arr), 1)
  1008. y_line = np.polyval(p, np.ravel(y_true_arr))
  1009. plt.plot(np.ravel(y_true_arr), y_line, 'b-', linewidth=1.5, label='Best-fit Line')
  1010. # Set equal aspect and add padding to axes
  1011. min_val = min(y_true_arr.min(), y_pred_arr.min())
  1012. max_val = max(y_true_arr.max(), y_pred_arr.max())
  1013. buffer = 0.05 * (max_val - min_val)
  1014. plt.xlim(min_val - buffer, max_val + buffer)
  1015. plt.ylim(min_val - buffer, max_val + buffer)
  1016. plt.gca().set_aspect('equal', adjustable='box')
  1017. # Optionally, add a y = x reference line
  1018. # min_val = min(y_true_arr.min(), y_pred_arr.min())
  1019. # max_val = max(y_true_arr.max(), y_pred_arr.max())
  1020. # plt.plot([min_val, max_val], [min_val, max_val], color='red', linestyle='--', linewidth=1, label='y = x')
  1021. # Annotate Pearson r and p-value
  1022. plt.gca().text(0.025, 0.975, f"r: {corr:.2f}\np: {p_value:.3f}",
  1023. transform=plt.gca().transAxes, fontsize=12,
  1024. verticalalignment='top', bbox=dict(facecolor='white', alpha=0.6, edgecolor='none'))
  1025. # Labels and layout
  1026. plt.xlabel("Actual (y_true)")
  1027. plt.ylabel("Predicted (y_pred)")
  1028. plt.title("Regression Outcome Plot: y_true vs. y_pred")
  1029. plt.legend()
  1030. plt.tight_layout()
  1031. # Save the figure
  1032. plot_file = os.path.join(output_path, "regression_scatter_plot.png")
  1033. plt.savefig(plot_file, dpi=300, bbox_inches="tight")
  1034. plt.show()
  1035. print(f"Scatter plot saved to: {plot_file}")

main.py at commit 6371766, under MIT · at the source

Overview

Authors: Mingshi Chen1,2, Henk A Marquering1,2, Henricus G Ruhé3,4, Liesbeth Reneman1, Matthan WA Caan2
ORCID iDs: Mingshi Chen
  1. Department of Radiology and Nuclear Medicine, Amsterdam University Medical Center, location AMC, Amsterdam, The Netherlands
  2. Department of Biomedical Engineering and Physics, Amsterdam University Medical Center, location AMC, Amsterdam, The Netherlands
  3. Department of Psychiatry, Radboud University Medical Center, Nijmegen, the Netherlands
  4. Donders Institute for Brain, Cognition, and Behavior, Radboud University, Nijmegen, the Netherlands
Journal: Scientific reports, volume 16, issue 1, article 26868
Dates: received 17 July 2025; accepted 2 June 2026; published online 12 June 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41598-026-56688-y · PMID 42286053 · PMCID PMC13518959 · OpenAlex W7164508063
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: fMRI (modality), human (organism), depression (population)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Connectivity, fMRI & imaging
Keywords: Major depressive disorder, Treatment response, Emotional conflict fMRI, Machine learning, Radiomics, Biomarkers, Diseases, Medical research, Neurology, Neuroscience
MeSH: Antidepressive Agents*, Emotions*, Magnetic Resonance Imaging*, Major Depressive Disorder*, Sertraline*, Adult, Brain, Female, Humans, Male, Middle Aged, Radiomics, Treatment Outcome (* major topic)
Topic: Radiomics and Machine Learning in Medical Imaging (Radiology, Nuclear Medicine and Imaging, Medicine), according to OpenAlex
Funding: NIMH NIH HHS (U01 MH092221, U01 MH092250); China Scholarship Council (202206380049)
Citations: not cited yet (Europe PMC); 38 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 11 matches between paragraphs and lines of code.

Mancy-Chen/EMBARC-fMRI

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 6371766d5eab43ea4c95c7e14a51e466ae3bf1f1, 16 March 2026
Languages: Python (15), Shell (9), MATLAB (4)
Size: 3,494 files, 28 scripts
Software Heritage: not archived
Found in: “Data availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (10 files), pandas (10 files), FSL (5 files), Matplotlib (5 files), SciPy (5 files), scikit-learn (4 files), statsmodels (4 files), neuroHarmonize (3 files), SHAP (3 files), NiBabel (2 files), XGBoost (2 files), imbalanced-learn (1 file), Tools for NIfTI and ANALYZE image (MATLAB) (1 file), Nilearn (1 file), pydicom (1 file), PyRadiomics (1 file), seaborn (1 file), SPM (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
30 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 28 scripts, each with its path and the digest of its content;
  • 11 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41598-026-56688-y.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 10 keywords, 13 MeSH terms, 2 funders, 33 references.

Cite

This paper

Chen, M., Marquering, H. A., Ruhé, H. G., Reneman, L., & Caan, M. W. (2026). On the value of radiomics in addition to clinical measures in emotional conflict fMRI for predicting sertraline response in major depressive disorder. Scientific reports, 16(1), 26868. https://doi.org/10.1038/s41598-026-56688-y

BibTeX

@article{chen2026value,
author = {Chen, Mingshi and Marquering, Henk A and Ruhé, Henricus G and Reneman, Liesbeth and Caan, Matthan WA},
title = {{On the value of radiomics in addition to clinical measures in emotional conflict fMRI for predicting sertraline response in major depressive disorder}},
journal = {Scientific reports},
year = {2026},
month = jun,
volume = {16},
number = {1},
pages = {26868},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/s41598-026-56688-y},
url = {https://doi.org/10.1038/s41598-026-56688-y},
pmid = {42286053},
pmcid = {PMC13518959}
}

RIS

TY - JOUR
AU - Chen, Mingshi
AU - Marquering, Henk A
AU - Ruhé, Henricus G
AU - Reneman, Liesbeth
AU - Caan, Matthan WA
TI - On the value of radiomics in addition to clinical measures in emotional conflict fMRI for predicting sertraline response in major depressive disorder
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/06/12
VL - 16
IS - 1
SP - 26868
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/s41598-026-56688-y
UR - https://doi.org/10.1038/s41598-026-56688-y
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41598-026-56688-y",
"type": "article-journal",
"title": "On the value of radiomics in addition to clinical measures in emotional conflict fMRI for predicting sertraline response in major depressive disorder",
"container-title": "Scientific reports",
"author": [
{
"family": "Chen",
"given": "Mingshi"
},
{
"family": "Marquering",
"given": "Henk A"
},
{
"family": "Ruhé",
"given": "Henricus G"
},
{
"family": "Reneman",
"given": "Liesbeth"
},
{
"family": "Caan",
"given": "Matthan WA"
}
],
"container-title-short": "Sci Rep",
"volume": "16",
"issue": "1",
"page": "26868",
"DOI": "10.1038/s41598-026-56688-y",
"PMID": "42286053",
"PMCID": "PMC13518959",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41598-026-56688-y",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
12
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: imbalanced-learn, pydicom, SHAP, 9 other tools
[2] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: imbalanced-learn, SHAP, XGBoost, 9 other tools, fMRI
[3] doi:10.1038/s41586-026-10631-3 [code]
A prognostic human brain network for diffuse midline glioma.
Journal: Nature
In common: pydicom, Tools for NIfTI and ANALYZE image (MATLAB), FSL, 9 other tools
[4] doi:10.1038/s41467-026-76452-0 [code]
Music evokes shared neural representations of imagined narratives across sensory modalities.
Journal: Nature communications
In common: Tools for NIfTI and ANALYZE image (MATLAB), FSL, Nilearn, 9 other tools, 1 reference
[5] doi:10.1162/imag.a.1262 [code]
Frame-wise multi-echo distortion correction for superior functional MRI.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: pydicom, Tools for NIfTI and ANALYZE image (MATLAB), FSL, 8 other tools, fMRI
[6] doi:10.1371/journal.pbio.3003856 [code]
Aging and metabolism contribute separately to brain-body health.
Journal: PLoS biology
In common: Tools for NIfTI and ANALYZE image (MATLAB), FSL, Nilearn, 9 other tools
[7] doi:10.3390/cancers18162636 [code]
Exploring the Impact of T2-Weighted MRI Fat Saturation on Radiomics Stability for Brain Radionecrosis Prediction After Skull-Base Proton Therapy: A Pilot Study.
Journal: Cancers
In common: PyRadiomics, pydicom, FSL, 7 other tools, 1 reference
[8] doi:10.1016/j.neuron.2026.04.011 [code]
Precision fMRI reveals densely interdigitated network patches with conserved motifs in the lateral prefrontal cortex.
Journal: Neuron
In common: Tools for NIfTI and ANALYZE image (MATLAB), FSL, Nilearn, 6 other tools, fMRI, 2 references
[9] doi:10.1002/hbm.70483 [code]
Untamed: Unconstrained Tensor Decomposition and Graph Node Embedding for Cortical Parcellation.
Journal: Human brain mapping
In common: Tools for NIfTI and ANALYZE image (MATLAB), FSL, Nilearn, 7 other tools, fMRI, 1 reference
[10] doi:10.1016/j.isci.2026.115329 [code]
Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas.
Journal: iScience
In common: imbalanced-learn, SHAP, XGBoost, 8 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.