OSCR

Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas.

Code ↔ Paper

17 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 17 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § STAR★Methods › Method details › Supervised machine learning model for discriminating brain-mets and glioma ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 292–345 · score 0.84 · cross validation, repeated stratified, confusion matrices, permutation importance, balanced accuracy, subsets
  2. [2] § STAR★Methods › Method details › Transcriptional signatures associated with geometric complexity of tumor regions ↔ Rcode/1_TCGA_download_GitHub.R, the whole file · a weak match · score 0.79 · RNA seq, TCGA GBM, TCGA LGG, gene expression, TPM, profiles
  3. [3] § STAR★Methods › Method details › Fractal dimension and lacunarity measurement for tumor subcomponents ↔ Python/1_FD3D_Calculation_GitHub_1902026.ipynb, lines 87–134 · score 0.71 · bounding rectangle, fractal dimension, binned, contour, slice, algorithm
  4. [4] § STAR★Methods › Quantification and statistical analysis ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 970–1013 · score 0.71 · confusion matrices, ROC AUC, balanced accuracy, predicting, ML, XGB
  5. [5] § STAR★Methods › Quantification and statistical analysis ↔ Rcode/5_Brain-Mets_all_cutoff and survival.R, lines 315–399 · score 0.70 · Cox proportional hazards, Kaplan Meier, fractal dimension, cutoff, fitted, survival
  6. [6] § Results › Fractal complexity, lacunarity, and fractional volume of tumor subcomponents across brain-metastases ↔ Python/2_Lac3D_Modified_Calculation_Github_19022025.ipynb, lines 68–126 · score 0.63 · convex hull, Bounding box, crop, contour, max, Lac
  7. [7] § STAR★Methods › Method details › Transcriptional signatures associated with geometric complexity of tumor regions ↔ Rcode/3_Spearman_Corelation_Geometry_Molecular_GitHub_19022026.R, lines 1–42 · score 0.60 · gene expression, correlated genes, TPM, protein, Spearman, GBM
  8. [8] § STAR★Methods › Method details › Evaluation of effects of manual and automated segmented tumor subcomponents on fractality and lacunarity estimates ↔ Python/6_Manual_auto_Check_passing_bablok_Github_200202026.ipynb, lines 39–116 · score 0.60 · Passing Bablok regression, slopes
  9. [9] § Results › Integrating geometric measures and fractional volumetry with machine learning for brain-metastases vs. glioma differentiation ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 241–246 · score 0.59 · UPENN gliomas, UCSF gliomas, UCSF BMSR, cohorts, Mets, brain
  10. [10] § Results › Comparison of fractal dimensions derived from manual and automated tumor subcomponent masks ↔ Python/6_Manual_auto_Check_passing_bablok_Github_200202026.ipynb, lines 39–116 · score 0.59 · Passing Bablok regression, intercept, slope, component
  11. [11] § Results › Integrating geometric measures and fractional volumetry with machine learning for brain-metastases vs. glioma differentiation ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 970–1013 · score 0.59 · cost sensitive, confusion matrices, balanced accuracy, XGB, RF, KNN
  12. [12] § STAR★Methods › Method details › Molecular profiling in gliomas and brain metastases ↔ Python/7_AUCell_Score_for_Hallmark_pathwatys_Variance_Github_20022026.ipynb, lines 324–339 · score 0.58 · pathway activity, AUCell, variance, scores, Hallmark
  13. [13] § STAR★Methods › Experimental model and study participant details ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 222–226 · score 0.58 · BraTS, Africa cohorts, UPENN, Brain Mets, TCGA, UCSF
  14. [14] § STAR★Methods › Quantification and statistical analysis ↔ Python/5_Survival_For_Brain_Mets_GitHub_19022026.ipynb, lines 305–335 · score 0.58 · Cox proportional hazards, CPH, fitted, age, survival, model
  15. [15] § STAR★Methods › Method details › Molecular profiling in gliomas and brain metastases ↔ Rcode/1_TCGA_download_GitHub.R, the whole file · a weak match · score 0.56 · RNA seq, gene expression, GBM, TCGA, profiling, LGG
  16. [16] § STAR★Methods › Method details › Fractal dimension and lacunarity measurement for tumor subcomponents ↔ Python/2_Lac3D_Modified_Calculation_Github_19022025.ipynb, lines 68–126 · score 0.56 · bounding boxes, binned, contour, slice, lacunarity, mask
  17. [17] § STAR★Methods › Method details › Supervised machine learning model for discriminating brain-mets and glioma ↔ Python/4_ML_modified_with_feature_importance_Github_19022026.ipynb, lines 1709–1745 · score 0.51 · random forest, ML, neighbors, XGB, RF, KNN

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

Jupyter notebook · 1,997 lines · 78 KB · no license · 6 matches

  1. # %%
  2. ##### ML Model For Distinguishing Brain Mets and Glioma Using FD3d and Fractional volume ####
  3. # %%
  4. import warnings
  5. from IPython.display import display
  6. import logging ### Version 0.5.1.2
  7. warnings.filterwarnings('ignore')
  8. logging.getLogger().setLevel(logging.ERROR)
  9. # %%
  10. import numpy as np ### Version 1.26.4
  11. import pandas as pd ### Version 2.2.3
  12. import matplotlib.pyplot as plt
  13. import seaborn as sns ### Version 0.13.2
  14. from statannot import add_stat_annotation
  15. from statsmodels.formula.api import ols
  16. from matplotlib import rc, rcParams
  17. # import statsmodels.api as sm
  18. import os
  19. from scipy.ndimage import affine_transform
  20. from scipy.stats import median_test
  21. from scipy import stats
  22. from scipy.stats import kruskal
  23. import scikit_posthocs as sp
  24. import math
  25. from scipy.stats import shapiro
  26. # %%
  27. # print(sklearn.__version__)
  28. # %%
  29. # this error via Jupyter Notebook installed alongside Anaconda navigation, you must install the imbalanced-learn library via the Conda package manager.
  30. # Therefore, do the following:
  31. # pip uninstall imblearn --yes
  32. # conda install -c conda-forge imbalanced-learn
  33. # %%
  34. import imblearn ### Version 0.12.4
  35. # %%
  36. from imblearn.pipeline import Pipeline
  37. from sklearn.model_selection import RepeatedStratifiedKFold
  38. # %%
  39. import statannot ### Version 0.2.3
  40. import itertools
  41. from statannot import add_stat_annotation
  42. import sklearn
  43. from sklearn.model_selection import train_test_split
  44. from sklearn.preprocessing import StandardScaler
  45. from sklearn.datasets import make_moons, make_circles, make_classification
  46. # from sklearn.neural_network import MLPClassifier
  47. from sklearn.neighbors import KNeighborsClassifier
  48. from sklearn.svm import SVC
  49. from sklearn.gaussian_process import GaussianProcessClassifier
  50. from sklearn.gaussian_process.kernels import RBF
  51. from sklearn.tree import DecisionTreeClassifier
  52. from sklearn.ensemble import RandomForestClassifier #, AdaBoostClassifier
  53. # from sklearn.naive_bayes import GaussianNB
  54. # from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
  55. from sklearn.preprocessing import label_binarize
  56. from sklearn.metrics import roc_curve, auc
  57. from sklearn.multiclass import OneVsRestClassifier
  58. import warnings
  59. from matplotlib.colors import ListedColormap
  60. from sklearn import svm
  61. from sklearn.metrics import RocCurveDisplay, auc
  62. from sklearn.metrics import confusion_matrix
  63. from xgboost import XGBClassifier
  64. from sklearn.metrics import roc_curve, auc
  65. from sklearn.model_selection import StratifiedKFold, GridSearchCV
  66. from sklearn.metrics import accuracy_score, balanced_accuracy_score, roc_auc_score, recall_score, precision_score
  67. # %%
  68. # !pip install shap
  69. # %%
  70. from sklearn.utils.class_weight import compute_sample_weight, compute_class_weight
  71. from sklearn.inspection import permutation_importance
  72. import shap
  73. import pickle
  74. # %%
  75. ### for changing the working directory
  76. os.chdir(r'Path/to/working/diectory')
  77. os.getcwd()
  78. # %%
  79. #### The input CSV file with FD lac and fractional volume Ratio and the of all the patients and the Tumor status
  80. df_combine= pd.read_excel("All_FD_LAC_Vol_Mets_.xlsx") ### add the file containing tumor Fractaldimension FD and Lac
  81. df_combine
  82. # %%
  83. Columns_names=df_combine.columns.values.tolist()
  84. # print("COLUMNS Name :-", Columns_names)
  85. # %%
  86. df_combine['Cohort'].value_counts()
  87. # %%
  88. df=df_combine[['ID', 'New_ID', 'Tumor_type', 'Grade', 'Age_years_at_diagnosis',
  89. 'Gender', 'Survival_months', 'Vital_status_1_dead', 'Primary', 'Cohort',
  90. 'Volume_ET_ml', 'Volume_NET_ml', 'Volume_ED_ml', 'Volume_WT_ml',
  91. 'ncr_net_mean3dfd', 'et_mean3dfd', 'ed_mean3dfd',
  92. 'ncr_net_meanlac3d', 'et_meanlac3d', 'ed_meanlac3d',
  93. 'Ratio_ET_WT', 'Ratio_NET_WT', 'Ratio_ED_WT']]
  94. df
  95. # %%
  96. ##### List for different combination of features
  97. # for only FD combination
  98. lst1= [['et_mean3dfd'], ['ed_mean3dfd'],['ncr_net_mean3dfd'],
  99. ['et_mean3dfd', 'ed_mean3dfd'],['ncr_net_mean3dfd', 'et_mean3dfd'],['ncr_net_mean3dfd', 'ed_mean3dfd'],
  100. ['et_mean3dfd', 'ncr_net_mean3dfd', 'ed_mean3dfd']]
  101. # for only Ratio combination
  102. lst2=[['Ratio_ET_WT'], ['Ratio_NET_WT'], ['Ratio_ED_WT'],
  103. ['Ratio_ET_WT', 'Ratio_NET_WT'], ['Ratio_ET_WT', 'Ratio_ED_WT'], ['Ratio_NET_WT', 'Ratio_ED_WT'],
  104. ['Ratio_ET_WT', 'Ratio_NET_WT', 'Ratio_ED_WT']]
  105. # for only Lacunarity Combination
  106. lst3= [['et_meanlac3d'], ['ed_meanlac3d'],['ncr_net_meanlac3d'],
  107. ['et_meanlac3d', 'ed_meanlac3d'],['ncr_net_meanlac3d', 'et_meanlac3d'],['ncr_net_meanlac3d', 'ed_meanlac3d'],
  108. ['et_meanlac3d', 'ncr_net_meanlac3d', 'ed_meanlac3d']]
  109. # for only FD and Lacunarity Combination
  110. lst4= [['et_mean3dfd','et_meanlac3d'],['ed_mean3dfd','ed_meanlac3d'], ['ncr_net_mean3dfd','ncr_net_meanlac3d'],
  111. ['et_mean3dfd', 'ed_mean3dfd','et_meanlac3d', 'ed_meanlac3d'],
  112. ['ncr_net_mean3dfd', 'et_mean3dfd','ncr_net_meanlac3d', 'et_meanlac3d'],
  113. ['ncr_net_mean3dfd', 'ed_mean3dfd','ncr_net_meanlac3d', 'ed_meanlac3d'],
  114. ['et_mean3dfd', 'ncr_net_mean3dfd', 'ed_mean3dfd','et_meanlac3d', 'ncr_net_meanlac3d', 'ed_meanlac3d']]
  115. # for only FD and ratio Combination
  116. lst5= [['et_mean3dfd','Ratio_ET_WT'], ['ed_mean3dfd','Ratio_ED_WT'],['ncr_net_mean3dfd', 'Ratio_NET_WT'],
  117. ['ncr_net_mean3dfd', 'et_mean3dfd','Ratio_ET_WT', 'Ratio_NET_WT'],
  118. ['ncr_net_mean3dfd', 'ed_mean3dfd','Ratio_NET_WT', 'Ratio_ED_WT'],
  119. ['et_mean3dfd', 'ed_mean3dfd','Ratio_ET_WT', 'Ratio_ED_WT'],
  120. ['et_mean3dfd', 'ncr_net_mean3dfd', 'ed_mean3dfd','Ratio_ET_WT', 'Ratio_NET_WT', 'Ratio_ED_WT']]
  121. # %%
  122. #### for ploting
  123. plt.rcParams["figure.figsize"] = (6, 6)
  124. rcParams['xtick.major.width'] = 2
  125. rcParams['xtick.major.size'] = 12
  126. rcParams['ytick.major.width'] = 2
  127. rcParams['ytick.major.size'] = 10
  128. rcParams['xtick.labelsize'] = 12
  129. rcParams['ytick.labelsize'] = 12
  130. plt.rcParams["axes.linewidth"] = 2
  131. plt.rcParams['xtick.labelsize'] = 24 # X-axis tick labels
  132. plt.rcParams['ytick.labelsize'] = 24 # Y-axis tick labels
  133. plt.rcParams['legend.fontsize'] = 14 # Global legend font size
  134. # %%
  135. ########## Function for Ploting ROC curve ###############
  136. def plot_roc_and_metrics(model, X, y, classifier_name, cv, color=None):
  137. tprs = []
  138. aucs = []
  139. mean_fpr = np.linspace(0, 1, 100)
  140. for train_idx, test_idx in cv.split(X, y):
  141. model.fit(X[train_idx], y[train_idx])
  142. y_scores = model.predict_proba(X[test_idx])[:, 1]
  143. fpr, tpr, _ = roc_curve(y[test_idx], y_scores)
  144. interp_tpr = np.interp(mean_fpr, fpr, tpr)
  145. interp_tpr[0] = 0.0
  146. tprs.append(interp_tpr)
  147. aucs.append(roc_auc_score(y[test_idx], y_scores)) # Exact sklearn AUC
  148. mean_tpr = np.mean(tprs, axis=0)
  149. mean_auc = np.mean(aucs)
  150. std_auc = np.std(aucs)
  151. std_tpr = np.std(tprs, axis=0)
  152. lower_tpr = np.maximum(mean_tpr - std_tpr, 0)
  153. upper_tpr = np.minimum(mean_tpr + std_tpr, 1)
  154. line, =plt.plot(mean_fpr, mean_tpr, label=f"{classifier_name} (Mean AUC: {mean_auc:.2f} ± {std_auc:.2f})",
  155. color=color)
  156. plt.fill_between(mean_fpr, lower_tpr, upper_tpr,
  157. alpha=0.2, color=line.get_color())
  158. # Add chance line
  159. plt.plot([0, 1], [0, 1], linestyle="--", color="black", lw=1, label=None)
  160. plt.xticks(np.arange(0, 1.1, 0.2))
  161. plt.yticks(np.arange(0, 1.1, 0.2))
  162. plt.xlim([0, 1])
  163. plt.ylim([0, 1])
  164. return mean_auc, std_auc
  165. # %%
  166. df_UBM= df[df['Cohort'].isin(["UCSF_BMSR"])] #### For UCSF-BMSR Brain Mets and TCGA cohort
  167. df_G = df[df['Cohort'].isin(["TCGA","UCSF","UPENN","Africa"])] #### For UCSF-BMSR Brain Mets and UCSF cohort
  168. # %%
  169. df_UBM1= df_UBM[df_UBM['ID'].str.endswith("A")]
  170. df_UBM1
  171. # %%
  172. df_1= pd.concat([df_UBM1,df_G ], ignore_index= True)
  173. df_1
  174. # %%
  175. df_BM_G = df[df['Cohort'].isin(["Brain_Mets", "TCGA"])] #### For Pre-treat-Brain Mets and TCGA cohort
  176. df_BM_UCSF = df[df['Cohort'].isin(["Brain_Mets", "UCSF"])] #### For Pre-treat-Brain Mets and UCSF cohort
  177. df_BM_UPENN = df[df['Cohort'].isin(["Brain_Mets", "UPENN"])] #### For Pre-treat-Brain Mets and UPENN cohort
  178. df_BM_AFG = df[df['Cohort'].isin(["Brain_Mets", "Africa"])] #### For Pre-treat-Brain Mets and BraTS Africa cohort
  179. # %%
  180. df_BM_G = df[df['Cohort'].isin(["Brain_Mets", "TCGA"])] #### For Pre-treat-Brain Mets and TCGA cohort
  181. df_BM_UCSF = df[df['Cohort'].isin(["Brain_Mets", "UCSF"])] #### For Pre-treat-Brain Mets and UCSF cohort
  182. df_BM_UPENN = df[df['Cohort'].isin(["Brain_Mets", "UPENN"])] #### For Pre-treat-Brain Mets and UPENN cohort
  183. df_BM_AFG = df[df['Cohort'].isin(["Brain_Mets", "Africa"])] #### For Pre-treat-Brain Mets and BraTS Africa cohort
  184. # %%
  185. ############# For Cross cohort Validation between Pretreat Brain Mets and Glioma
  186. # df_Cohort = df_BM_G ### Uncomment for BM and TCGA Glioma
  187. # df_Cohort = df_BM_UCSF ### Uncomment for BM and UCSF Glioma
  188. # df_Cohort = df_BM_UPENN ### Uncomment for BM and UPENN Glioma
  189. # df_Cohort = df_BM_AFG ### Uncomment for BM and AFG Glioma
  190. # %%
  191. ############# For Cross cohort Validation Between UCSF-BMSR Brain-Mets and Glioma
  192. df_Cohort = df_UBM1_G ### Uncomment for UCSF-BMSR and TCGA Glioma
  193. # df_Cohort = df_UBM1_UCSF ### Uncomment for UCSF-BMSR and UCSF Glioma
  194. # df_Cohort = df_UBM1_UPENN ### Uncomment for UCSF-BMSR and UPENN Glioma
  195. # df_Cohort = df_UBM1_AFG ### Uncomment for UCSF-BMSR and AFG Glioma
  196. # %% [markdown]
  197. # # Baseline Code
  198. # %%
  199. ###################### Baseline code with repeated stratified K-fold ######################
  200. name_classifier = {
  201. svm.SVC(random_state=12, probability=True): 'SVM',
  202. RandomForestClassifier(n_estimators=10, random_state=12): 'RF',
  203. KNeighborsClassifier(10): 'KNN',
  204. XGBClassifier(random_state=12, use_label_encoder=False, eval_metric='logloss',verbosity=0): 'XGB'
  205. }
  206. # ------------------------ Classifier Pipelines ------------------------
  207. param_grids = {'SVM': {'C': [0.1, 1, 10, 100], 'kernel': ['linear', 'rbf'],
  208. 'gamma': [1, 0.1, 0.01, 0.001] },
  209. 'RF': {'n_estimators': [10, 50, 100, 200],'max_depth': [None, 10, 20, 30],
  210. 'min_samples_split': [2, 5, 10]},
  211. 'KNN': {'n_neighbors': [3, 5, 10, 15], 'weights': ['uniform', 'distance'],
  212. 'p': [1, 2] },
  213. 'XGB': {'n_estimators': [10, 50, 100, 200],'learning_rate': [0.01, 0.1, 0.2,0.3],
  214. 'max_depth': [3,4,5,6], 'subsample': [0.8, 0.9,1.0], 'min_child_weight': [1, 3, 5],
  215. 'reg_alpha': [0, 0.1, 0.5,1], # L1 regularization
  216. 'reg_lambda': [1, 2, 5, 10]}}
  217. # ------------------------ Parameter Grids ------------------------
  218. # Function to calculate average confusion matrix
  219. def avg_confusion_calculate(confusion_accuracy):
  220. no_of_splits = len(confusion_accuracy)
  221. rows, columns = confusion_accuracy[0].shape
  222. avg_confusion_matrix = np.zeros((rows, columns))
  223. for i in range(no_of_splits):
  224. avg_confusion_matrix += confusion_accuracy[i]
  225. avg_confusion_matrix /= no_of_splits
  226. return avg_confusion_matrix
  227. # ------------------------ Storage for results ------------------------
  228. results, result2 = [], []
  229. confusion_avg_dict = {}
  230. metrics_storage = {}
  231. perm_importance_dict = {}
  232. # ------------------------ Training + Evaluation ------------------------
  233. for i in lst5: # Loop through features ### change the list for different feature combination
  234. print(i)
  235. feature_key = str(i)
  236. confusion_avg_dict[feature_key] = {}
  237. metrics_storage[feature_key] = {}
  238. df3 = df_Cohort.dropna(subset=i)
  239. X = df3[i].values
  240. y = (df3['Tumor_type'] == 'Mets').values.astype(int)
  241. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=2, random_state=12)
  242. for j, k in name_classifier.items():
  243. grid_search = GridSearchCV(
  244. estimator=j,
  245. param_grid=param_grids[k],
  246. scoring='roc_auc',
  247. cv=5,
  248. n_jobs=-1
  249. )
  250. grid_search.fit(X, y)
  251. best_model = grid_search.best_estimator_
  252. best_params = grid_search.best_params_
  253. # ---------------- Cross-validation for metrics AND feature importance ----------------
  254. acc_list, bal_acc_list, auc_list, sensitivity_list, prec_list = [], [], [], [], []
  255. conf_norm_list, conf_raw_list = [], []
  256. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  257. perm_importance_auc_list = []
  258. for train_idx, test_idx in rskf.split(X, y):
  259. X_train, X_test = X[train_idx], X[test_idx]
  260. y_train, y_test = y[train_idx], y[test_idx]
  261. best_model.fit(X_train, y_train)
  262. y_pred = best_model.predict(X_test)
  263. y_prob = best_model.predict_proba(X_test)[:, 1]
  264. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  265. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  266. auc_list.append(roc_auc_score(y_test, y_prob))
  267. sensitivity_list.append(recall_score(y_test, y_pred) * 100)
  268. prec_list.append(precision_score(y_test, y_pred) * 100)
  269. cm = confusion_matrix(y_test, y_pred)
  270. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  271. conf_norm_list.append(cm.astype('float'))
  272. else:
  273. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  274. conf_raw_list.append(cm)
  275. ##-----------------------------------------------------------------------------------
  276. ## <<< MODIFICATION: Calculate Permutation Importance for BOTH metrics >>>
  277. # 1. Based on ROC AUC
  278. perm_result_auc = permutation_importance(
  279. best_model, X_test, y_test, scoring='roc_auc',
  280. n_repeats=5, random_state=42, n_jobs=1)
  281. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  282. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  283. # Permutation Importance (based on AUC)
  284. perm_importance_dict[(feature_key, j)] = {
  285. 'features': i,
  286. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  287. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  288. }
  289. ##-------------------------------- Store aggregated metrics-----------------------------------------------
  290. # Save per-fold metrics
  291. metrics_storage[feature_key][k] = {
  292. 'accuracy_model': acc_list,
  293. 'balanced_accuracy_model': bal_acc_list,
  294. 'roc_auc': auc_list,
  295. 'sensitivity_model': sensitivity_list,
  296. 'precision_model': prec_list
  297. }
  298. # Summary results
  299. result_label = f"{i}"
  300. result2.append([
  301. result_label, k, f"Best {k}: {best_params}",
  302. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  303. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  304. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  305. round(np.mean(sensitivity_list), 2), round(np.std(sensitivity_list), 2),
  306. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  307. len(df3)
  308. ])
  309. confusion_avg = avg_confusion_calculate(conf_norm_list)
  310. confusion_avg_dict[feature_key][k] = confusion_avg
  311. print(f"confusion matrix for feature {i} and {k} ML model is:\n", confusion_avg)
  312. # --- ROC Plotting (Now uses the metrics from the CV loop) ---
  313. mean_auc, std_auc = plot_roc_and_metrics(
  314. model=best_model,
  315. X=X,
  316. y=y,
  317. classifier_name=k,
  318. cv=rskf, # SAME splits as metrics calculation
  319. color=None
  320. )
  321. #--------------------------------------------------------------
  322. # Save ROC curve
  323. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/Basline_model_CV_Importance"
  324. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/Basline_model_CV_Importance" ### Uncumment for USSF-BMSR cohort
  325. os.makedirs(os.path.join(output_dir, "Figures/ROC_curve"), exist_ok=True)
  326. plot_filename = f"{output_dir }/Figures/ROC_curve/ROC_curve{i}_R1_Aug2025_ROC_curve.png"
  327. plt.xlabel("")
  328. plt.ylabel("")
  329. plt.legend(loc="lower right", frameon=False, fontsize=18)
  330. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  331. plt.show()
  332. plt.close()
  333. print(f"**********Finished processing feature set: {i}*****************")
  334. # ------------------------ Save metrics ------------------------
  335. os.makedirs(os.path.join(output_dir, "per_fold_metrics"), exist_ok=True)
  336. df_results = pd.DataFrame(result2, columns=["Feature Set","Classifier", "Classifier with best parameters",
  337. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy",
  338. "SD_balanced_Acc", "Mean_AUC_Score", "SD_AUC_score",
  339. "Mean_Sensitivity", "SD_Sensitivity", "Mean_Precision",
  340. "SD_Precision", "Number of Subjects"])
  341. df_results.to_csv(f"{output_dir}/Baseline_models_summary_results.csv", index=False)
  342. # Save per-fold metrics
  343. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  344. rows = []
  345. for feat, clf_dict in metrics_storage.items():
  346. for clf, metric_dict in clf_dict.items():
  347. row = [feat, clf] + metric_dict[metric]
  348. rows.append(row)
  349. if rows:
  350. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  351. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir }/per_fold_metrics/BS_{metric}.csv", index=False)
  352. #------------------------------------------------------
  353. # Define the list of metrics
  354. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  355. # Initialize a list to collect final rows
  356. final_rows = []
  357. # Loop through the features and classifiers, and gather the metric data
  358. for feature, classifiers in metrics_storage.items():
  359. for clf, values in classifiers.items():
  360. # Determine the number of folds dynamically from any metric
  361. num_folds = len(next(iter(values.values())))
  362. for fold_idx in range(num_folds):
  363. row = {
  364. 'Feature Set': feature,
  365. 'Classifier': clf,
  366. 'Fold': fold_idx + 1
  367. }
  368. # Fill in each metric value for this fold
  369. for metric in metric_names:
  370. metric_values = values.get(metric, [None] * num_folds)
  371. row[metric] = metric_values[fold_idx]
  372. final_rows.append(row)
  373. # Create DataFrame from the collected rows
  374. df_final = pd.DataFrame(final_rows)
  375. # Save the DataFrame to CSV ## change the folder accordingly
  376. output_file = f"{output_dir}/per_fold_metrics/ALL_SP_Baseline_combined_metrics.csv"
  377. df_final.to_csv(output_file, index=False)
  378. print(f"Saved: {output_file}")
  379. # Save the confusion_avg_dict for later use
  380. with open(f"{output_dir}/per_fold_metrics/Base_confusion_avg_dict.pkl", 'wb') as f:
  381. pickle.dump(confusion_avg_dict, f)
  382. print("Saved confusion_avg_dict.pkl")
  383. ### ----------------------------------------------------------------------------
  384. # Save Permutation Importance (AUC)
  385. perm_auc_df = []
  386. for (feat_set, clf), vals in perm_importance_dict.items():
  387. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  388. perm_auc_df.append([feat_set, clf, f, imp, std])
  389. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  390. f"{output_dir}/permutation_importance_auc_cv.csv", index=False)
  391. print("Saved permutation importance (ROC AUC).")
  392. # %%
  393. # %% [markdown]
  394. # # Baseline with standard scaler¶
  395. # %%
  396. #########################################################################################
  397. ##### Modified code including Permutation importance
  398. ##### Basline model with Feature Importance (Cross-Validated)
  399. ########################################################################################
  400. ################ Baseline with standard scaler ##############
  401. name_classifier = {
  402. 'SVM': Pipeline([
  403. ('scaler', StandardScaler()),
  404. ('clf', svm.SVC(random_state=12, probability=True))
  405. ]),
  406. 'RF': Pipeline([('scaler', StandardScaler()),
  407. ('clf', RandomForestClassifier(n_estimators=10, random_state=12))
  408. ]),
  409. 'KNN': Pipeline([
  410. ('scaler', StandardScaler()),
  411. ('clf', KNeighborsClassifier())
  412. ]),
  413. 'XGB': Pipeline([ ('scaler', StandardScaler()),
  414. ('clf', XGBClassifier(random_state=12, use_label_encoder=False, eval_metric='logloss',verbosity=0)) # you may calculate actual imbalance ratio
  415. ])
  416. }
  417. # ----------------------------- Parameter Grids --------------------------
  418. param_grids = {
  419. 'SVM': {
  420. 'clf__C': [0.1, 1, 10, 100],
  421. 'clf__kernel': ['linear', 'rbf'],
  422. 'clf__gamma': [1, 0.1, 0.01, 0.001]
  423. },
  424. 'RF': {
  425. 'clf__n_estimators': [10, 50, 100, 200],
  426. 'clf__max_depth': [None, 10, 20, 30],
  427. 'clf__min_samples_split': [2, 5, 10]
  428. },
  429. 'KNN': {
  430. 'clf__n_neighbors': [3, 5, 10, 15],
  431. 'clf__weights': ['uniform', 'distance'],
  432. 'clf__p': [1, 2]
  433. },
  434. 'XGB': {
  435. 'clf__n_estimators': [10, 50, 100, 200],
  436. 'clf__learning_rate': [0.01, 0.1, 0.2,0.3],
  437. 'clf__max_depth': [3,4,5,6],
  438. 'clf__subsample': [0.8, 0.9, 1.0],
  439. 'clf__min_child_weight': [1,3,5],
  440. 'clf__reg_alpha': [0, 0.1, 0.5,1],
  441. 'clf__reg_lambda': [1, 2, 5, 10]
  442. }
  443. }
  444. ###---------------- Confusion matrices Function -----------------------------
  445. def avg_confusion_calculate(confusion_accuracy):
  446. no_of_splits = len(confusion_accuracy)
  447. rows, columns = confusion_accuracy[0].shape
  448. avg_confusion_matrix = np.zeros((rows, columns))
  449. for i in range(no_of_splits):
  450. avg_confusion_matrix += confusion_accuracy[i]
  451. avg_confusion_matrix /= no_of_splits
  452. return avg_confusion_matrix
  453. #----------------------Storage for results------------------------------------
  454. results, result2 = [], []
  455. confusion_avg_dict = {}
  456. metrics_storage = {}
  457. perm_importance_dict = {}
  458. #------------------------Training + Evaluation--------------------------------
  459. for features in lst5:
  460. print(f"\nEvaluating feature set: {features}")
  461. feature_key = str(features)
  462. confusion_avg_dict[feature_key] = {}
  463. metrics_storage[feature_key] = {}
  464. df3 = df_Cohort.dropna(subset=features)
  465. X = df3[features].values
  466. y = (df3['Tumor_type'] == 'Mets').values.astype(int)
  467. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=12)
  468. for clf_name, pipeline in name_classifier.items():
  469. print(f"\nClassifier: {clf_name}")
  470. grid_search = GridSearchCV(
  471. estimator=pipeline,
  472. param_grid=param_grids[clf_name],
  473. scoring='roc_auc',
  474. cv=5,
  475. n_jobs=-1
  476. )
  477. grid_search.fit(X, y)
  478. best_model = grid_search.best_estimator_
  479. best_params = grid_search.best_params_
  480. # ---------------- Cross-validation for metrics AND feature importance ----------------
  481. acc_list, bal_acc_list, auc_list, sensitivity_list, prec_list = [], [], [], [], []
  482. conf_norm_list, conf_raw_list = [], []
  483. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  484. perm_importance_auc_list = []
  485. for train_idx, test_idx in rskf.split(X, y):
  486. X_train, X_test = X[train_idx], X[test_idx]
  487. y_train, y_test = y[train_idx], y[test_idx]
  488. best_model.fit(X_train, y_train)
  489. y_pred = best_model.predict(X_test)
  490. y_prob = best_model.predict_proba(X_test)[:, 1]
  491. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  492. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  493. auc_list.append(roc_auc_score(y_test, y_prob))
  494. sensitivity_list.append(recall_score(y_test, y_pred) * 100)
  495. prec_list.append(precision_score(y_test, y_pred) * 100)
  496. cm = confusion_matrix(y_test, y_pred)
  497. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  498. conf_norm_list.append(cm.astype('float'))
  499. else:
  500. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  501. conf_raw_list.append(cm)
  502. ####---------------------------------------------------------------------------------
  503. ## <<< MODIFICATION: Calculate Permutation Importance >>>
  504. # Based on ROC AUC
  505. perm_result_auc = permutation_importance(
  506. best_model, X_test, y_test, scoring='roc_auc',
  507. n_repeats=5, random_state=42, n_jobs=1)
  508. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  509. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  510. # Permutation Importance (based on AUC)
  511. perm_importance_dict[(feature_key, clf_name)] = {
  512. 'features': features,
  513. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  514. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  515. }
  516. ###---------------------------Store aggregated metrics for reporting----------------------
  517. metrics_storage[feature_key][clf_name] = {
  518. 'accuracy_model': acc_list,
  519. 'balanced_accuracy_model': bal_acc_list,
  520. 'roc_auc': auc_list,
  521. 'sensitivity_model': sensitivity_list,
  522. 'precision_model': prec_list
  523. }
  524. result2.append([
  525. feature_key, clf_name, str(best_params),
  526. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  527. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  528. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  529. round(np.mean(sensitivity_list), 2), round(np.std(sensitivity_list), 2),
  530. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  531. len(df3)
  532. ])
  533. confusion_avg = avg_confusion_calculate(conf_norm_list)
  534. confusion_avg_dict[feature_key][clf_name] = confusion_avg
  535. print(f"Avg confusion matrix for {features} and {clf_name} ML model:\n{confusion_avg}")
  536. # --- ROC Plotting (Now uses the metrics from the CV loop) ---
  537. mean_auc, std_auc = plot_roc_and_metrics(
  538. model=best_model,
  539. X=X,
  540. y=y,
  541. classifier_name=clf_name,
  542. cv=rskf, # SAME splits as metrics calculation
  543. color=None
  544. )
  545. #-----------------------------------------------------------------
  546. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/Basline_Standard_model_CV_Importance" #Change the output dir accordingly
  547. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/Basline_Standard_model_CV_Importance"
  548. os.makedirs(f"{output_dir}/Figures/ROC_curve", exist_ok=True)
  549. plot_filename = f"{output_dir}/Figures/ROC_curve/{features}_R1_Aug2025_ROC_curve.png"
  550. plt.xlabel("")
  551. plt.ylabel("")
  552. plt.legend(loc="lower right", frameon=False, fontsize=18)
  553. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  554. plt.show()
  555. plt.close()
  556. print(f"**********Finished processing feature set: {features}*****************")
  557. # ------------------------ Save all results ------------------------------
  558. # ------------------------ Save metrics -----------------------------------
  559. # output_dir = "ML_Results/TCGA/FD_only_Check/Cost_Sensitive_model_CV_Importance"
  560. os.makedirs(f"{output_dir}/per_fold_metrics", exist_ok=True)
  561. df_results = pd.DataFrame(result2, columns=[
  562. "Feature Set","Classifier", "Best Parameters",
  563. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy", "SD_Bal_Acc",
  564. "Mean_AUC_Score", "SD_AUC", "Mean_Recall", "SD_Recall",
  565. "Mean_Precision", "SD_Precision", "N_Samples"
  566. ])
  567. df_results.to_csv(f"{output_dir}/BS_model_summary_results.csv", index=False)
  568. # Save per-fold metrics
  569. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  570. rows = []
  571. for feat, clf_dict in metrics_storage.items():
  572. for clf, metric_dict in clf_dict.items():
  573. row = [feat, clf] + metric_dict[metric]
  574. rows.append(row)
  575. if rows:
  576. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  577. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir}/per_fold_metrics/BS_{metric}.csv", index=False)
  578. # Define the list of metrics
  579. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  580. # Initialize a list to collect final rows
  581. final_rows = []
  582. # Loop through the features and classifiers, and gather the metric data
  583. for feature, classifiers in metrics_storage.items():
  584. for clf, values in classifiers.items():
  585. # Determine the number of folds dynamically from any metric
  586. num_folds = len(next(iter(values.values())))
  587. for fold_idx in range(num_folds):
  588. row = {
  589. 'Feature Set': feature,
  590. 'Classifier': clf,
  591. 'Fold': fold_idx + 1
  592. }
  593. # Fill in each metric value for this fold
  594. for metric in metric_names:
  595. metric_values = values.get(metric, [None] * num_folds)
  596. row[metric] = metric_values[fold_idx]
  597. final_rows.append(row)
  598. # Create DataFrame from the collected rows
  599. df_final = pd.DataFrame(final_rows)
  600. # Save the DataFrame to CSV
  601. output_file = f"{output_dir}/per_fold_metrics/ALL_SPlot_BaselineStandard_combined_metrics.csv"
  602. df_final.to_csv(output_file, index=False)
  603. print(f"Saved: {output_file}")
  604. # -------------------------------------------------
  605. # Save the confusion_avg_dict for later use
  606. with open(f"{output_dir}/per_fold_metrics/BS_confusion_avg_dict.pkl", 'wb') as f:
  607. pickle.dump(confusion_avg_dict, f)
  608. print("Saved BS_confusion_avg_dict.pkl")
  609. ### ----------------------------------------------------------------------------
  610. # Save Permutation Importance (AUC)
  611. perm_auc_df = []
  612. for (feat_set, clf), vals in perm_importance_dict.items():
  613. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  614. perm_auc_df.append([feat_set, clf, f, imp, std])
  615. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  616. f"{output_dir}/BS_permutation_importance_auc_cv.csv", index=False)
  617. print("Saved permutation importance (ROC AUC).")
  618. # %%
  619. # %% [markdown]
  620. # # Cost- sensitive moels
  621. # %%
  622. from collections import Counter
  623. # %%
  624. def plot_roc_and_metrics(model, X, y, classifier_name, cv, color=None):
  625. tprs = []
  626. aucs = []
  627. mean_fpr = np.linspace(0, 1, 100)
  628. for train_idx, test_idx in cv.split(X, y):
  629. model.fit(X[train_idx], y[train_idx])
  630. y_scores = model.predict_proba(X[test_idx])[:, 1]
  631. fpr, tpr, _ = roc_curve(y[test_idx], y_scores)
  632. interp_tpr = np.interp(mean_fpr, fpr, tpr)
  633. interp_tpr[0] = 0.0
  634. tprs.append(interp_tpr)
  635. aucs.append(roc_auc_score(y[test_idx], y_scores)) # Exact sklearn AUC
  636. mean_tpr = np.mean(tprs, axis=0)
  637. mean_auc = np.mean(aucs)
  638. std_auc = np.std(aucs)
  639. std_tpr = np.std(tprs, axis=0)
  640. lower_tpr = np.maximum(mean_tpr - std_tpr, 0)
  641. upper_tpr = np.minimum(mean_tpr + std_tpr, 1)
  642. line, =plt.plot(mean_fpr, mean_tpr, label=f"{classifier_name} (Mean AUC: {mean_auc:.2f}±{std_auc:.2f})",
  643. color=color)
  644. plt.fill_between(mean_fpr, lower_tpr, upper_tpr,
  645. alpha=0.2, color=line.get_color())
  646. # Add chance line
  647. plt.plot([0, 1], [0, 1], linestyle="--", color="grey", lw=1, label=None)
  648. plt.xticks(np.arange(0, 1.1, 0.2), fontsize=28)
  649. plt.yticks(np.arange(0, 1.1, 0.2), fontsize=28)
  650. plt.xlim([0, 1])
  651. plt.ylim([0, 1])
  652. return mean_auc, std_auc
  653. # %%
  654. #########################################################################################
  655. ##### Modified code including Permutation importance
  656. ##### Costsensitive Model with Feature Importance (Cross-Validated)
  657. #########################################################################################
  658. # ------------------------ Standard Imports ------------------------
  659. # ------------------------ Custom KNN with cost-sensitive learning ------------------------
  660. class CostSensitiveKNN(KNeighborsClassifier):
  661. def __init__(self, n_neighbors=5, weights='uniform', p=2, class_weight=None):
  662. super().__init__(n_neighbors=n_neighbors, weights=weights, p=p)
  663. self.class_weight = class_weight
  664. def predict(self, X):
  665. neigh_ind = self.kneighbors(X, return_distance=False)
  666. predictions = []
  667. for neighbors in neigh_ind:
  668. neighbor_labels = self._y[neighbors]
  669. votes = {}
  670. for label in np.unique(self._y): # Check against all possible labels
  671. count = np.sum(neighbor_labels == label)
  672. weight = self.class_weight.get(label, 1.0) if self.class_weight else 1.0
  673. votes[label] = count * weight
  674. predictions.append(max(votes, key=votes.get))
  675. return np.array(predictions)
  676. def fit(self, X, y):
  677. self._y = np.array(y)
  678. # The actual fitting is done by the parent class
  679. return super().fit(X, y)
  680. # ------------------------ Classifier Pipelines ------------------------
  681. name_classifier = {
  682. 'SVM': Pipeline([
  683. ('scaler', StandardScaler()),
  684. ('clf', svm.SVC(probability=True, class_weight='balanced', random_state=12))
  685. ]),
  686. 'RF': Pipeline([
  687. ('scaler', StandardScaler()),
  688. ('clf', RandomForestClassifier(class_weight='balanced', random_state=12))
  689. ]),
  690. 'KNN': Pipeline([
  691. ('scaler', StandardScaler()),
  692. ('clf', CostSensitiveKNN())
  693. ]),
  694. 'XGB': Pipeline([
  695. ('scaler', StandardScaler()),
  696. ('clf', XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=12, verbosity=0))
  697. ])
  698. }
  699. # ------------------------ Parameter Grids ------------------------
  700. param_grids = {
  701. 'SVM': {
  702. 'clf__C': [0.1, 1, 10,100],
  703. 'clf__kernel': ['linear', 'rbf'],
  704. 'clf__gamma': ['scale', 0.1, 0.01,0.001]
  705. },
  706. 'RF': {
  707. 'clf__n_estimators': [10,50, 100,200],
  708. 'clf__max_depth': [None, 10, 20,30],
  709. 'clf__min_samples_split': [2, 5,10]
  710. },
  711. 'KNN': {
  712. 'clf__n_neighbors': [3, 5, 10, 15],
  713. 'clf__weights': ['uniform', 'distance'],
  714. 'clf__p': [1, 2]
  715. },
  716. 'XGB': {
  717. 'clf__n_estimators': [10, 50, 100, 200],
  718. 'clf__learning_rate': [0.01, 0.1, 0.2, 0.3],
  719. 'clf__max_depth': [3,4,5,6],
  720. 'clf__subsample': [0.8, 0.9, 1.0],
  721. 'clf__min_child_weight': [1,3,5],
  722. 'clf__reg_alpha': [0, 0.1, 0.5,1],
  723. 'clf__reg_lambda': [1, 2, 5, 10]
  724. }
  725. }
  726. ######################################
  727. def avg_confusion_calculate(confusion_accuracy):
  728. no_of_splits = len(confusion_accuracy)
  729. rows, columns = confusion_accuracy[0].shape
  730. avg_confusion_matrix = np.zeros((rows, columns))
  731. for i in range(no_of_splits):
  732. avg_confusion_matrix += confusion_accuracy[i]
  733. avg_confusion_matrix /= no_of_splits
  734. return avg_confusion_matrix
  735. # ------------------------ Storage for results ------------------------
  736. results, result2 = [], []
  737. confusion_avg_dict = {}
  738. metrics_storage = {}
  739. perm_importance_dict = {}
  740. # ------------------------ Training + Evaluation ------------------------
  741. for features in lst5:
  742. print(f"\nEvaluating feature set: {features}")
  743. feature_key = str(features)
  744. confusion_avg_dict[feature_key] = {}
  745. metrics_storage[feature_key] = {}
  746. df3 = df_Cohort.dropna(subset=features)
  747. X = df3[features].values
  748. y = (df3['Tumor_type'] == 'Mets').astype(int).values
  749. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=12)
  750. for clf_name, pipeline in name_classifier.items():
  751. print(f"\nClassifier: {clf_name}")
  752. grid_search = GridSearchCV(
  753. estimator=pipeline,
  754. param_grid=param_grids.get(clf_name, {}), # Use .get for safety
  755. scoring='roc_auc',
  756. cv=5, # Use a smaller CV for faster grid search
  757. n_jobs=-1
  758. )
  759. grid_search.fit(X, y)
  760. best_model = grid_search.best_estimator_
  761. best_params = grid_search.best_params_
  762. # ---------------- Cross-validation for metrics AND feature importance ----------------
  763. acc_list, bal_acc_list, auc_list, recall_list, prec_list = [], [], [], [], []
  764. conf_norm_list, conf_raw_list = [], []
  765. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  766. perm_importance_auc_list = []
  767. for train_idx, test_idx in rskf.split(X, y):
  768. X_train, X_test = X[train_idx], X[test_idx]
  769. y_train, y_test = y[train_idx], y[test_idx]
  770. sample_weight = compute_sample_weight(class_weight='balanced', y=y_train)
  771. # --- Cost-sensitive model fitting FOR THIS FOLD ---
  772. if clf_name == 'KNN':
  773. class_weights_array = compute_class_weight(class_weight='balanced', classes=np.unique(y_train), y=y_train)
  774. class_weights_dict = dict(zip(np.unique(y_train), class_weights_array))
  775. best_model.named_steps['clf'].class_weight = class_weights_dict
  776. best_model.fit(X_train, y_train)
  777. elif clf_name == 'XGB':
  778. counter = Counter(y_train)
  779. scale_pos_weight = counter[0] / counter[1] if counter[1] > 0 else 1
  780. best_model.named_steps['clf'].set_params(scale_pos_weight=scale_pos_weight)
  781. best_model.fit(X_train, y_train) # XGB uses scale_pos_weight internally
  782. else: # For RF, SVM which have class_weight='balanced'
  783. # best_model.fit(X_train, y_train)
  784. try:
  785. best_model.fit(X_train, y_train, clf__sample_weight=sample_weight)
  786. except TypeError:
  787. best_model.fit(X_train, y_train)
  788. # --- Calculate performance metrics on test set ---
  789. y_pred = best_model.predict(X_test)
  790. y_prob = best_model.predict_proba(X_test)[:, 1]
  791. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  792. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  793. auc_list.append(roc_auc_score(y_test, y_prob))
  794. recall_list.append(recall_score(y_test, y_pred, zero_division=0) * 100)
  795. prec_list.append(precision_score(y_test, y_pred, zero_division=0) * 100)
  796. cm = confusion_matrix(y_test, y_pred)
  797. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  798. conf_norm_list.append(cm.astype('float'))
  799. else:
  800. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  801. conf_raw_list.append(cm)
  802. ##---------------------------------------------------------------------------------
  803. ## <<< MODIFICATION: Calculate Permutation Importance for BOTH metrics >>>
  804. # Based on ROC AUC
  805. perm_result_auc = permutation_importance(
  806. best_model, X_test, y_test, scoring='roc_auc',
  807. n_repeats=5, random_state=42, n_jobs=1)
  808. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  809. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  810. # Permutation Importance (based on AUC)
  811. perm_importance_dict[(feature_key, clf_name)] = {
  812. 'features': features,
  813. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  814. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  815. }
  816. # ------------------- Store aggregated metrics ---------------------------------
  817. metrics_storage[feature_key][clf_name] = {
  818. 'accuracy_model': acc_list, 'balanced_accuracy_model': bal_acc_list,
  819. 'roc_auc': auc_list, 'sensitivity_model': recall_list, 'precision_model': prec_list
  820. }
  821. result2.append([
  822. feature_key, clf_name, str(best_params),
  823. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  824. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  825. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  826. round(np.mean(recall_list), 2), round(np.std(recall_list), 2),
  827. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  828. len(df3)
  829. ])
  830. confusion_avg = avg_confusion_calculate(conf_norm_list)
  831. confusion_avg_dict[feature_key][clf_name] = confusion_avg
  832. print(f"Avg confusion matrix for {clf_name}:\n{confusion_avg}")
  833. # --- ROC Plotting (Now uses the metrics from the CV loop) ---
  834. mean_auc, std_auc = plot_roc_and_metrics(
  835. model=best_model,
  836. X=X,
  837. y=y,
  838. classifier_name=clf_name,
  839. cv=rskf, # SAME splits as metrics calculation
  840. color=None
  841. )
  842. #--------------------------------------------------------------
  843. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/Cost_Sensitive_model_CV_Importance"
  844. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/Cost_Sensitive_model_CV_Importance"
  845. os.makedirs(os.path.join(output_dir, "Figures/ROC_curve"), exist_ok=True)
  846. plot_filename = f"{output_dir }/Figures/ROC_curve/{features}__ROC_curve.png"
  847. plt.xlabel("")
  848. plt.ylabel("")
  849. plt.legend(loc="lower right", frameon=False, fontsize=18)
  850. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  851. plt.show()
  852. plt.close()
  853. print(f"**********Finished processing feature set: {features}*****************")
  854. # ------------------------ Save all results ------------------------
  855. # ------------------------ Save metrics ------------------------
  856. os.makedirs(os.path.join(output_dir, "per_fold_metrics"), exist_ok=True)
  857. df_results = pd.DataFrame(result2, columns=[
  858. "Feature Set","Classifier", "Best Parameters",
  859. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy", "SD_Bal_Acc",
  860. "Mean_AUC_Score", "SD_AUC", "Mean_Recall", "SD_Recall",
  861. "Mean_Precision", "SD_Precision", "N_Samples"
  862. ])
  863. df_results.to_csv(f"{output_dir}/CS_model_summary_results.csv", index=False)
  864. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  865. rows = []
  866. for feat, clf_dict in metrics_storage.items():
  867. for clf, metric_dict in clf_dict.items():
  868. row = [feat, clf] + metric_dict[metric]
  869. rows.append(row)
  870. if rows:
  871. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  872. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir}/per_fold_metrics/{metric}.csv", index=False)
  873. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  874. final_rows = []
  875. # Loop through the features and classifiers, and gather the metric data
  876. for feature, classifiers in metrics_storage.items():
  877. for clf, values in classifiers.items():
  878. num_folds = len(next(iter(values.values())))
  879. for fold_idx in range(num_folds):
  880. row = {
  881. 'Feature Set': feature,
  882. 'Classifier': clf,
  883. 'Fold': fold_idx + 1
  884. }
  885. for metric in metric_names:
  886. metric_values = values.get(metric, [None] * num_folds)
  887. row[metric] = metric_values[fold_idx]
  888. final_rows.append(row)
  889. df_final = pd.DataFrame(final_rows)
  890. output_file = f"{output_dir}/per_fold_metrics/ALL_SPlot_CS_combined_metrics.csv"
  891. df_final.to_csv(output_file, index=False)
  892. print(f"Saved: {output_file}")
  893. ###-----------------------------------------------------------------------------
  894. #### for Confusion avg dict
  895. with open(f"{output_dir}/per_fold_metrics/CS_confusion_avg_dict.pkl", 'wb') as f:
  896. pickle.dump(confusion_avg_dict, f)
  897. print("Saved CS_confusion_avg_dict.pkl")
  898. ### ----------------------------------------------------------------------------
  899. # Save Permutation Importance (AUC)
  900. perm_auc_df = []
  901. for (feat_set, clf), vals in perm_importance_dict.items():
  902. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  903. perm_auc_df.append([feat_set, clf, f, imp, std])
  904. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  905. f"{output_dir}/CS_permutation_importance_auc_cv.csv", index=False)
  906. print("Saved permutation importance (ROC AUC).")
  907. # %% [markdown]
  908. # # Oversampling using SMOTE
  909. # %%
  910. from imblearn.over_sampling import SMOTE
  911. # %%
  912. ###################### Over sampling Using SMOTE ######################################
  913. from imblearn.over_sampling import SMOTE
  914. ################################################################################
  915. ##### Modified code including Permutation importance
  916. ##### Oversampling with Feature Importance (Cross-Validated)
  917. ################ Undersampling with standard scaler ###########################
  918. ################ Oversampling with standard scaler ##############
  919. name_classifier = {
  920. 'SVM': Pipeline([
  921. ('scaler', StandardScaler()),
  922. ('smote', SMOTE(random_state=12)),
  923. ('clf', svm.SVC(random_state=12, probability=True))
  924. ]),
  925. 'RF': Pipeline([('scaler', StandardScaler()),
  926. ('smote', SMOTE(random_state=12)),
  927. ('clf', RandomForestClassifier(n_estimators=10, random_state=12))
  928. ]),
  929. 'KNN': Pipeline([
  930. ('scaler', StandardScaler()),
  931. ('smote', SMOTE(random_state=12)),
  932. ('clf', KNeighborsClassifier())
  933. ]),
  934. 'XGB': Pipeline([ ('scaler', StandardScaler()),
  935. ('smote', SMOTE(random_state=12)),
  936. ('clf', XGBClassifier(random_state=12, use_label_encoder=False, eval_metric='logloss',verbosity=0)) # you may calculate actual imbalance ratio
  937. ])
  938. }
  939. # ------------------------ Classifier Pipelines ------------------------
  940. # Define parameter grids
  941. param_grids = {
  942. 'SVM': {
  943. 'clf__C': [0.1, 1, 10, 100],
  944. 'clf__kernel': ['linear', 'rbf'],
  945. 'clf__gamma': [1, 0.1, 0.01, 0.001]
  946. },
  947. 'RF': {
  948. 'clf__n_estimators': [10, 50, 100, 200],
  949. 'clf__max_depth': [None, 10, 20, 30],
  950. 'clf__min_samples_split': [2, 5, 10]
  951. },
  952. 'KNN': {
  953. 'clf__n_neighbors': [3, 5, 10, 15],
  954. 'clf__weights': ['uniform', 'distance'],
  955. 'clf__p': [1, 2]
  956. },
  957. 'XGB': {
  958. 'clf__n_estimators': [10, 50, 100, 200],
  959. 'clf__learning_rate': [0.01, 0.1, 0.2,0.3],
  960. 'clf__max_depth': [3,4,5,6],
  961. 'clf__subsample': [0.8, 0.9, 1.0],
  962. 'clf__min_child_weight': [1,3,5],
  963. 'clf__reg_alpha': [0, 0.1, 0.5,1],
  964. 'clf__reg_lambda': [1, 2, 5, 10]
  965. }
  966. }
  967. ##-------------------Fubction For Confusion Matrix --------------------------
  968. def avg_confusion_calculate(confusion_accuracy):
  969. no_of_splits = len(confusion_accuracy)
  970. rows, columns = confusion_accuracy[0].shape
  971. avg_confusion_matrix = np.zeros((rows, columns))
  972. for i in range(no_of_splits):
  973. avg_confusion_matrix += confusion_accuracy[i]
  974. avg_confusion_matrix /= no_of_splits
  975. return avg_confusion_matrix
  976. ##------------------------Storage for results --------------------------------
  977. results, result2 = [], []
  978. confusion_avg_dict = {}
  979. metrics_storage = {}
  980. perm_importance_dict = {}
  981. ##-----------------------Training + Evaluation-------------------------------
  982. for features in lst5: # Loop through features ### change the list for different feature combination
  983. print(f"\nEvaluating feature set: {features}")
  984. feature_key = str(features)
  985. confusion_avg_dict[feature_key] = {}
  986. metrics_storage[feature_key] = {}
  987. df3 = df_Cohort.dropna(subset=features)
  988. X = df3[features].values
  989. y = (df3['Tumor_type'] == 'Mets').values.astype(int)
  990. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=12)
  991. for clf_name, pipeline in name_classifier.items():
  992. print(f"\nClassifier: {clf_name}")
  993. grid_search = GridSearchCV(
  994. estimator=pipeline,
  995. param_grid=param_grids[clf_name],
  996. scoring='roc_auc',
  997. cv=5,
  998. n_jobs=-1
  999. )
  1000. grid_search.fit(X, y)
  1001. best_model = grid_search.best_estimator_
  1002. best_params = grid_search.best_params_
  1003. # ---------------- Cross-validation for metrics AND feature importance ----------------
  1004. acc_list, bal_acc_list, auc_list, sensitivity_list, prec_list = [], [], [], [], []
  1005. conf_norm_list, conf_raw_list = [], []
  1006. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  1007. perm_importance_auc_list = []
  1008. for train_idx, test_idx in rskf.split(X, y):
  1009. X_train, X_test = X[train_idx], X[test_idx]
  1010. y_train, y_test = y[train_idx], y[test_idx]
  1011. best_model.fit(X_train, y_train)
  1012. y_pred = best_model.predict(X_test)
  1013. y_prob = best_model.predict_proba(X_test)[:, 1]
  1014. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  1015. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  1016. auc_list.append(roc_auc_score(y_test, y_prob))
  1017. sensitivity_list.append(recall_score(y_test, y_pred) * 100)
  1018. prec_list.append(precision_score(y_test, y_pred) * 100)
  1019. cm = confusion_matrix(y_test, y_pred)
  1020. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  1021. conf_norm_list.append(cm.astype('float'))
  1022. else:
  1023. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  1024. conf_raw_list.append(cm)
  1025. ## <<< MODIFICATION: Calculate Permutation Importance for BOTH metrics >>>
  1026. # Based on ROC AUC
  1027. perm_result_auc = permutation_importance(
  1028. best_model, X_test, y_test, scoring='roc_auc',
  1029. n_repeats=5, random_state=42, n_jobs=1)
  1030. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  1031. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  1032. # Permutation Importance (based on AUC)
  1033. perm_importance_dict[(feature_key, clf_name)] = {
  1034. 'features': features,
  1035. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  1036. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  1037. }
  1038. # ------------------- Store aggregated metrics for reporting ---------------------------------
  1039. metrics_storage[feature_key][clf_name] = {
  1040. 'accuracy_model': acc_list,
  1041. 'balanced_accuracy_model': bal_acc_list,
  1042. 'roc_auc': auc_list,
  1043. 'sensitivity_model': sensitivity_list,
  1044. 'precision_model': prec_list
  1045. }
  1046. result2.append([
  1047. feature_key, clf_name, str(best_params),
  1048. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  1049. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  1050. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  1051. round(np.mean(sensitivity_list), 2), round(np.std(sensitivity_list), 2),
  1052. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  1053. len(df3)
  1054. ])
  1055. confusion_avg = avg_confusion_calculate(conf_norm_list)
  1056. confusion_avg_dict[feature_key][clf_name] = confusion_avg
  1057. print(f"Avg confusion matrix for {features} and {clf_name} ML model:\n{confusion_avg}")
  1058. # --- ROC Plotting (Now uses the metrics from the CV loop) ---
  1059. mean_auc, std_auc = plot_roc_and_metrics(
  1060. model=best_model,
  1061. X=X,
  1062. y=y,
  1063. classifier_name=clf_name,
  1064. cv=rskf, # SAME splits as metrics calculation
  1065. color=None
  1066. )
  1067. #--------------------------------------------------------
  1068. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/OverSampling_SMOTE_model_CV_Importance"
  1069. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/OverSampling_SMOTE_model_CV_Importance"
  1070. os.makedirs(f"{output_dir}/Figures/ROC_curve", exist_ok=True)
  1071. plot_filename = f"{output_dir}/Figures/ROC_curve/{features}__curve.png"
  1072. plt.xlabel("")
  1073. plt.ylabel("")
  1074. plt.legend(loc="lower right", frameon=False, fontsize=18)
  1075. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  1076. plt.show()
  1077. plt.close()
  1078. print(f"**********Finished processing feature set: {features}*****************")
  1079. # ------------------------ Save all results ------------------------
  1080. # ------------------------ Save metrics -----------------------------
  1081. os.makedirs(os.path.join(output_dir, "per_fold_metrics"), exist_ok=True)
  1082. df_results = pd.DataFrame(result2, columns=[
  1083. "Feature Set","Classifier", "Best Parameters",
  1084. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy", "SD_Bal_Acc",
  1085. "Mean_AUC_Score", "SD_AUC", "Mean_Recall", "SD_Recall",
  1086. "Mean_Precision", "SD_Precision", "N_Samples"
  1087. ])
  1088. df_results.to_csv(f"{output_dir}/OS_model_summary_results.csv", index=False)
  1089. # Save per-fold metrics
  1090. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  1091. rows = []
  1092. for feat, clf_dict in metrics_storage.items():
  1093. for clf, metric_dict in clf_dict.items():
  1094. row = [feat, clf] + metric_dict[metric]
  1095. rows.append(row)
  1096. if rows:
  1097. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  1098. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir}/per_fold_metrics/BS_{metric}.csv", index=False)
  1099. #----------------------------------------------------
  1100. # Define the list of metrics
  1101. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  1102. # Initialize a list to collect final rows
  1103. final_rows = []
  1104. # Loop through the features and classifiers, and gather the metric data
  1105. for feature, classifiers in metrics_storage.items():
  1106. for clf, values in classifiers.items():
  1107. # Determine the number of folds dynamically from any metric
  1108. num_folds = len(next(iter(values.values())))
  1109. for fold_idx in range(num_folds):
  1110. row = {
  1111. 'Feature Set': feature,
  1112. 'Classifier': clf,
  1113. 'Fold': fold_idx + 1
  1114. }
  1115. # Fill in each metric value for this fold
  1116. for metric in metric_names:
  1117. metric_values = values.get(metric, [None] * num_folds)
  1118. row[metric] = metric_values[fold_idx]
  1119. final_rows.append(row)
  1120. # Create DataFrame from the collected rows
  1121. df_final = pd.DataFrame(final_rows)
  1122. # Save the DataFrame to CSV
  1123. output_file = f"{output_dir}/per_fold_metrics/OS_ALL_SP_Baseline_combined_metrics.csv"
  1124. df_final.to_csv(output_file, index=False)
  1125. print(f"Saved: {output_file}")
  1126. #-----------------------------------------------------------
  1127. # Save the confusion_avg_dict for later use
  1128. with open(f"{output_dir}/per_fold_metrics/OS_confusion_avg_dict.pkl", 'wb') as f:
  1129. pickle.dump(confusion_avg_dict, f)
  1130. print("Saved OS_confusion_avg_dict.pkl")
  1131. ### ----------------------------------------------------------------------------
  1132. # Save Permutation Importance (AUC)
  1133. perm_auc_df = []
  1134. for (feat_set, clf), vals in perm_importance_dict.items():
  1135. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  1136. perm_auc_df.append([feat_set, clf, f, imp, std])
  1137. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  1138. f"{output_dir}/OS_permutation_importance_auc_cv.csv", index=False)
  1139. print("Saved permutation importance (ROC AUC).")
  1140. # %% [markdown]
  1141. # # Undersampling Using NearMiss
  1142. # %%
  1143. # from imblearn.under_sampling import NearMiss
  1144. # %%
  1145. ############################ Under Sampling Using NearMiss #######################
  1146. from imblearn.under_sampling import NearMiss
  1147. ################################################################################
  1148. ##### Modified code including Permutation importance
  1149. ##### Udersampling with Feature Importance (Cross-Validated)
  1150. ################ Undersampling with standard scaler ###########################
  1151. name_classifier = {
  1152. 'SVM': Pipeline([
  1153. ('scaler', StandardScaler()),
  1154. ('undersample', NearMiss(version=1)),
  1155. ('clf', svm.SVC(random_state=12, probability=True))
  1156. ]),
  1157. 'RF': Pipeline([('scaler', StandardScaler()),
  1158. ('undersample', NearMiss(version=1)),
  1159. ('clf', RandomForestClassifier(n_estimators=10, random_state=12))
  1160. ]),
  1161. 'KNN': Pipeline([
  1162. ('scaler', StandardScaler()),
  1163. ('undersample', NearMiss(version=1)),
  1164. ('clf', KNeighborsClassifier())
  1165. ]),
  1166. 'XGB': Pipeline([ ('scaler', StandardScaler()),
  1167. ('undersample', NearMiss(version=1)),
  1168. ('clf', XGBClassifier(random_state=12, use_label_encoder=False, eval_metric='logloss',verbosity=0)) # you may calculate actual imbalance ratio
  1169. ])
  1170. }
  1171. # Define parameter grids ###clf__ is added before the parametes because using pipline
  1172. param_grids = {
  1173. 'SVM': {
  1174. 'clf__C': [0.1, 1, 10, 100],
  1175. 'clf__kernel': ['linear', 'rbf'],
  1176. 'clf__gamma': [1, 0.1, 0.01, 0.001]
  1177. },
  1178. 'RF': {
  1179. 'clf__n_estimators': [10, 50, 100, 200],
  1180. 'clf__max_depth': [None, 10, 20, 30],
  1181. 'clf__min_samples_split': [2, 5, 10]
  1182. },
  1183. 'KNN': {
  1184. 'clf__n_neighbors': [3, 5, 10, 15],
  1185. 'clf__weights': ['uniform', 'distance'],
  1186. 'clf__p': [1, 2]
  1187. },
  1188. 'XGB': {
  1189. 'clf__n_estimators': [10, 50, 100, 200],
  1190. 'clf__learning_rate': [0.01, 0.1, 0.2,0.3],
  1191. 'clf__max_depth': [3,4,5,6],
  1192. 'clf__subsample': [0.8, 0.9, 1.0],
  1193. 'clf__min_child_weight': [1,3,5],
  1194. 'clf__reg_alpha': [0, 0.1, 0.5,1],
  1195. 'clf__reg_lambda': [1, 2, 5, 10]
  1196. }
  1197. }
  1198. # Confusion matrices function
  1199. def avg_confusion_calculate(confusion_accuracy):
  1200. no_of_splits = len(confusion_accuracy)
  1201. rows, columns = confusion_accuracy[0].shape
  1202. avg_confusion_matrix = np.zeros((rows, columns))
  1203. for i in range(no_of_splits):
  1204. avg_confusion_matrix += confusion_accuracy[i]
  1205. avg_confusion_matrix /= no_of_splits
  1206. return avg_confusion_matrix
  1207. #-------------------------Storage for results------------------------
  1208. results, result2 = [], []
  1209. confusion_avg_dict = {}
  1210. metrics_storage = {}
  1211. perm_importance_dict = {}
  1212. # ------------------------ Training + Evaluation ------------------------
  1213. for features in lst5: # Loop through features ### change the list for different feature combination
  1214. print(f"\nEvaluating feature set: {features}")
  1215. feature_key = str(features)
  1216. confusion_avg_dict[feature_key] = {}
  1217. metrics_storage[feature_key] = {}
  1218. df3 = df_Cohort.dropna(subset=features)
  1219. X = df3[features].values
  1220. y = (df3['Tumor_type'] == 'Mets').values.astype(int)
  1221. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=12)
  1222. for clf_name, pipeline in name_classifier.items():
  1223. print(f"\nClassifier: {clf_name}")
  1224. grid_search = GridSearchCV(
  1225. estimator=pipeline,
  1226. param_grid=param_grids[clf_name],
  1227. scoring='roc_auc',
  1228. cv=5,
  1229. n_jobs=-1
  1230. )
  1231. grid_search.fit(X, y)
  1232. best_model = grid_search.best_estimator_
  1233. best_params = grid_search.best_params_
  1234. # ---------------- Cross-validation for metrics AND feature importance ----------------
  1235. acc_list, bal_acc_list, auc_list, sensitivity_list, prec_list = [], [], [], [], []
  1236. conf_norm_list, conf_raw_list = [], []
  1237. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  1238. perm_importance_auc_list = []
  1239. for train_idx, test_idx in rskf.split(X, y):
  1240. X_train, X_test = X[train_idx], X[test_idx]
  1241. y_train, y_test = y[train_idx], y[test_idx]
  1242. best_model.fit(X_train, y_train)
  1243. y_pred = best_model.predict(X_test)
  1244. y_prob = best_model.predict_proba(X_test)[:, 1]
  1245. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  1246. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  1247. auc_list.append(roc_auc_score(y_test, y_prob))
  1248. sensitivity_list.append(recall_score(y_test, y_pred) * 100)
  1249. prec_list.append(precision_score(y_test, y_pred) * 100)
  1250. cm = confusion_matrix(y_test, y_pred)
  1251. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  1252. conf_norm_list.append(cm.astype('float'))
  1253. else:
  1254. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  1255. conf_raw_list.append(cm)
  1256. ###------------------------------Modify here--------------------
  1257. ## <<< MODIFICATION: Calculate Permutation Importance for BOTH metrics >>>
  1258. # Based on ROC AUC
  1259. perm_result_auc = permutation_importance(
  1260. best_model, X_test, y_test, scoring='roc_auc',
  1261. n_repeats=5, random_state=42, n_jobs=1)
  1262. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  1263. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  1264. # Permutation Importance (based on AUC)
  1265. perm_importance_dict[(feature_key, clf_name)] = {
  1266. 'features': features,
  1267. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  1268. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  1269. }
  1270. ##-----------------------------Store Metrics ------------------------
  1271. metrics_storage[feature_key][clf_name] = {
  1272. 'accuracy_model': acc_list,
  1273. 'balanced_accuracy_model': bal_acc_list,
  1274. 'roc_auc': auc_list,
  1275. 'sensitivity_model': sensitivity_list,
  1276. 'precision_model': prec_list
  1277. }
  1278. result2.append([
  1279. feature_key, clf_name, str(best_params),
  1280. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  1281. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  1282. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  1283. round(np.mean(sensitivity_list), 2), round(np.std(sensitivity_list), 2),
  1284. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  1285. len(df3)
  1286. ])
  1287. confusion_avg = avg_confusion_calculate(conf_norm_list)
  1288. confusion_avg_dict[feature_key][clf_name] = confusion_avg
  1289. print(f"Avg confusion matrix for {features} and {clf_name} ML model:\n{confusion_avg}")
  1290. # ---------------------Save ROC curve-----------------------------------
  1291. mean_auc, std_auc = plot_roc_and_metrics(
  1292. model=best_model,
  1293. X=X,
  1294. y=y,
  1295. classifier_name=clf_name,
  1296. cv=rskf, # SAME splits as metrics calculation
  1297. color=None
  1298. )
  1299. #----------------------------------------------------------------------
  1300. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/UnderSampling_CV_Importance" ### change the directory where you want to store the files
  1301. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/UnderSampling_CV_Importance"
  1302. os.makedirs(os.path.join(output_dir, "Figures/ROC_curve"), exist_ok=True)
  1303. plot_filename = f"{output_dir}/Figures/ROC_curve/{features}__ROC_curve.png"
  1304. plt.xlabel("")
  1305. plt.ylabel("")
  1306. plt.legend(loc="lower right", frameon=False, fontsize=18)
  1307. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  1308. plt.show()
  1309. plt.close()
  1310. print(f"********** Finished processing feature set: {features} *****************")
  1311. #---------------------- Save results----------------------------------------------------
  1312. os.makedirs(os.path.join(output_dir, "per_fold_metrics"), exist_ok=True)
  1313. df_results = pd.DataFrame(result2, columns=[
  1314. "Feature Set","Classifier", "Best Parameters",
  1315. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy", "SD_Bal_Acc",
  1316. "Mean_AUC_Score", "SD_AUC", "Mean_Recall", "SD_Recall",
  1317. "Mean_Precision", "SD_Precision", "N_Samples"
  1318. ])
  1319. df_results.to_csv(f"{output_dir}/US_model_summary_results.csv", index=False)
  1320. # Save per-fold metrics
  1321. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  1322. rows = []
  1323. for feat, clf_dict in metrics_storage.items():
  1324. for clf, metric_dict in clf_dict.items():
  1325. row = [feat, clf] + metric_dict[metric]
  1326. rows.append(row)
  1327. if rows:
  1328. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  1329. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir}/per_fold_metrics/BS_{metric}.csv", index=False)
  1330. # Define the list of metrics
  1331. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  1332. # Initialize a list to collect final rows
  1333. final_rows = []
  1334. # Loop through the features and classifiers, and gather the metric data
  1335. for feature, classifiers in metrics_storage.items():
  1336. for clf, values in classifiers.items():
  1337. num_folds = len(next(iter(values.values()))) # Determine the number of folds dynamically from any metric
  1338. for fold_idx in range(num_folds):
  1339. row = {
  1340. 'Feature Set': feature,
  1341. 'Classifier': clf,
  1342. 'Fold': fold_idx + 1
  1343. }
  1344. # Fill in each metric value for this fold
  1345. for metric in metric_names:
  1346. metric_values = values.get(metric, [None] * num_folds)
  1347. row[metric] = metric_values[fold_idx]
  1348. final_rows.append(row)
  1349. # Create DataFrame from the collected rows
  1350. df_final = pd.DataFrame(final_rows)
  1351. # Save the DataFrame to CSV
  1352. output_file = f"{output_dir}/per_fold_metrics/ALL_SP_Baseline_combined_metrics.csv"
  1353. df_final.to_csv(output_file, index=False)
  1354. print(f"Saved: {output_file}")
  1355. # Save the confusion_avg_dict for later use
  1356. with open(f"{output_dir}/per_fold_metrics/confusion_avg_dict.pkl", 'wb') as f:
  1357. pickle.dump(confusion_avg_dict, f)
  1358. print("Saved confusion_avg_dict.pkl")
  1359. ##---------------------------Save Importance Score ---------------------------------------
  1360. # Save Permutation Importance (AUC)
  1361. perm_auc_df = []
  1362. for (feat_set, clf), vals in perm_importance_dict.items():
  1363. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  1364. perm_auc_df.append([feat_set, clf, f, imp, std])
  1365. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  1366. f"{output_dir}/US_permutation_importance_auc_cv.csv", index=False)
  1367. print("Saved permutation importance (ROC AUC).")
  1368. # %% [markdown]
  1369. # # Combination of Oversampling and Undersampling
  1370. # %%
  1371. ############################ Oversampling + Undersampling #####################################
  1372. #########################################################################################
  1373. ##### Modified code including Permutation importance
  1374. ##### Oversampling + Undersampling Model with Feature Importance (Cross-Validated)
  1375. ########################################################################
  1376. ### Combination of Oversampling and Undersampling with standard scaler ##############
  1377. name_classifier = {
  1378. 'SVM': Pipeline([
  1379. ('scaler', StandardScaler()),
  1380. ('smote', SMOTE(random_state=12,sampling_strategy=0.85)),
  1381. ('undersample', NearMiss(version=1, sampling_strategy=1)),
  1382. ('clf', svm.SVC(random_state=12, probability=True))
  1383. ]),
  1384. 'RF': Pipeline([('scaler', StandardScaler()),
  1385. ('smote', SMOTE(random_state=12,sampling_strategy=0.85)),
  1386. ('undersample', NearMiss(version=1, sampling_strategy=1)),
  1387. ('clf', RandomForestClassifier(n_estimators=10, random_state=12))
  1388. ]),
  1389. 'KNN': Pipeline([
  1390. ('scaler', StandardScaler()),
  1391. ('smote', SMOTE(random_state=12, sampling_strategy=0.85)),
  1392. ('undersample', NearMiss(version=1, sampling_strategy=1)),
  1393. ('clf', KNeighborsClassifier())
  1394. ]),
  1395. 'XGB': Pipeline([ ('scaler', StandardScaler()),
  1396. ('smote', SMOTE(random_state=12, sampling_strategy=0.85)),
  1397. ('undersample', NearMiss(version=1, sampling_strategy=1)),
  1398. ('clf', XGBClassifier(random_state=12, use_label_encoder=False, eval_metric='logloss',verbosity=0)) # you may calculate actual imbalance ratio
  1399. ])
  1400. }
  1401. # ------------------------ Parameter Grids ------------------------
  1402. param_grids = {
  1403. 'SVM': {
  1404. 'clf__C': [0.1, 1, 10, 100],
  1405. 'clf__kernel': ['linear', 'rbf'],
  1406. 'clf__gamma': [1, 0.1, 0.01, 0.001]
  1407. },
  1408. 'RF': {
  1409. 'clf__n_estimators': [10, 50, 100, 200],
  1410. 'clf__max_depth': [None, 10, 20, 30],
  1411. 'clf__min_samples_split': [2, 5, 10]
  1412. },
  1413. 'KNN': {
  1414. 'clf__n_neighbors': [3, 5, 10, 15],
  1415. 'clf__weights': ['uniform', 'distance'],
  1416. 'clf__p': [1, 2]
  1417. },
  1418. 'XGB': {
  1419. 'clf__n_estimators': [10, 50, 100, 200],
  1420. 'clf__learning_rate': [0.01, 0.1, 0.2,0.3],
  1421. 'clf__max_depth': [3,4,5,6],
  1422. 'clf__subsample': [0.8, 0.9, 1.0],
  1423. 'clf__min_child_weight': [1,3,5],
  1424. 'clf__reg_alpha': [0, 0.1, 0.5,1],
  1425. 'clf__reg_lambda': [1, 2, 5, 10]
  1426. }
  1427. }
  1428. # Example feature combinations
  1429. def avg_confusion_calculate(confusion_accuracy):
  1430. no_of_splits = len(confusion_accuracy)
  1431. rows, columns = confusion_accuracy[0].shape
  1432. avg_confusion_matrix = np.zeros((rows, columns))
  1433. for i in range(no_of_splits):
  1434. avg_confusion_matrix += confusion_accuracy[i]
  1435. avg_confusion_matrix /= no_of_splits
  1436. return avg_confusion_matrix
  1437. ###--------------------Storage for results----------------------
  1438. results, result2 = [], []
  1439. confusion_avg_dict = {}
  1440. metrics_storage = {}
  1441. perm_importance_dict = {}
  1442. #-----------------------------------------------------
  1443. for features in lst5:
  1444. print(f"\nEvaluating feature set: {features}")
  1445. feature_key = str(features)
  1446. confusion_avg_dict[feature_key] = {}
  1447. metrics_storage[feature_key] = {}
  1448. df3 = df_Cohort.dropna(subset=features)
  1449. X = df3[features].values
  1450. y = (df3['Tumor_type'] == 'Mets').values.astype(int)
  1451. rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=12)
  1452. for clf_name, pipeline in name_classifier.items():
  1453. print(f"\nClassifier: {clf_name}")
  1454. grid_search = GridSearchCV(
  1455. estimator=pipeline,
  1456. param_grid=param_grids[clf_name],
  1457. scoring='roc_auc',
  1458. cv=5,
  1459. n_jobs=-1
  1460. )
  1461. grid_search.fit(X, y)
  1462. best_model = grid_search.best_estimator_
  1463. best_params = grid_search.best_params_
  1464. # ---------------- Cross-validation for metrics AND feature importance ----------------
  1465. acc_list, bal_acc_list, auc_list, sensitivity_list, prec_list = [], [], [], [], []
  1466. conf_norm_list, conf_raw_list = [], []
  1467. ## <<< MODIFICATION: Initialize lists for importance scores from each fold >>>
  1468. perm_importance_auc_list = []
  1469. for train_idx, test_idx in rskf.split(X, y):
  1470. X_train, X_test = X[train_idx], X[test_idx]
  1471. y_train, y_test = y[train_idx], y[test_idx]
  1472. best_model.fit(X_train, y_train)
  1473. y_pred = best_model.predict(X_test)
  1474. y_prob = best_model.predict_proba(X_test)[:, 1]
  1475. acc_list.append(accuracy_score(y_test, y_pred) * 100)
  1476. bal_acc_list.append(balanced_accuracy_score(y_test, y_pred) * 100)
  1477. auc_list.append(roc_auc_score(y_test, y_prob))
  1478. sensitivity_list.append(recall_score(y_test, y_pred) * 100)
  1479. prec_list.append(precision_score(y_test, y_pred) * 100)
  1480. cm = confusion_matrix(y_test, y_pred)
  1481. if cm.sum(axis=1).min() == 0: # Avoid division by zero if a class is missing in a small test fold
  1482. conf_norm_list.append(cm.astype('float'))
  1483. else:
  1484. conf_norm_list.append(cm.astype('float') / cm.sum(axis=1)[:, np.newaxis])
  1485. conf_raw_list.append(cm)
  1486. ## <<< MODIFICATION: Calculate Permutation Importance for BOTH metrics >>>
  1487. # Based on ROC AUC
  1488. perm_result_auc = permutation_importance(
  1489. best_model, X_test, y_test, scoring='roc_auc',
  1490. n_repeats=5, random_state=42, n_jobs=1)
  1491. perm_importance_auc_list.append(perm_result_auc.importances_mean)
  1492. ## <<< MODIFICATION: Aggregate importance scores AFTER the CV loop >>>
  1493. # Permutation Importance (based on AUC)
  1494. perm_importance_dict[(feature_key, clf_name)] = {
  1495. 'features': features,
  1496. 'importances_mean': np.mean(perm_importance_auc_list, axis=0),
  1497. 'importances_std': np.std(perm_importance_auc_list, axis=0)
  1498. }
  1499. # ------------------- Store aggregated metrics for reporting ---------------------------------
  1500. metrics_storage[feature_key][clf_name] = {
  1501. 'accuracy_model': acc_list,
  1502. 'balanced_accuracy_model': bal_acc_list,
  1503. 'roc_auc': auc_list,
  1504. 'sensitivity_model': sensitivity_list,
  1505. 'precision_model': prec_list
  1506. }
  1507. result2.append([
  1508. feature_key, clf_name, str(best_params),
  1509. round(np.mean(acc_list), 2), round(np.std(acc_list), 2),
  1510. round(np.mean(bal_acc_list), 2), round(np.std(bal_acc_list), 2),
  1511. round(np.mean(auc_list), 2), round(np.std(auc_list), 2),
  1512. round(np.mean(sensitivity_list), 2), round(np.std(sensitivity_list), 2),
  1513. round(np.mean(prec_list), 2), round(np.std(prec_list), 2),
  1514. len(df3)
  1515. ])
  1516. confusion_avg = avg_confusion_calculate(conf_norm_list)
  1517. confusion_avg_dict[feature_key][clf_name] = confusion_avg
  1518. print(f"Avg confusion matrix for {features} and {clf_name} ML model:\n{confusion_avg}")
  1519. # --- ROC Plotting (Now uses the metrics from the CV loop) ---
  1520. mean_auc, std_auc = plot_roc_and_metrics(
  1521. model=best_model,
  1522. X=X,
  1523. y=y,
  1524. classifier_name=clf_name,
  1525. cv=rskf, # SAME splits as metrics calculation
  1526. color=None
  1527. )
  1528. #--------------------------------------------------------
  1529. output_dir = "ML_Results/TCGA_BM-pretreats/FD_Ratio/Oversampling_Undersampling_model_CV_Importance"
  1530. # output_dir = "ML_Results/Firstvisit_A_TCGA_UCSF-BMSR/FD_Ratio/Oversampling_Undersampling_model_CV_Importance"
  1531. os.makedirs(f"{output_dir}/Figures/ROC_curve", exist_ok=True)
  1532. plot_filename = f"{output_dir}/Figures/ROC_curve/OUS_{features}__ROC_curve.png"
  1533. plt.xlabel("")
  1534. plt.ylabel("")
  1535. plt.legend(loc="lower right", frameon=False,fontsize=18)
  1536. plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
  1537. plt.show()
  1538. plt.close()
  1539. print(f"**********Finished processing feature set: {features}*****************")
  1540. # ------------------------ Save all results ------------------------
  1541. # ------------------------ Save metrics ------------------------
  1542. os.makedirs(f"{output_dir}/per_fold_metrics", exist_ok=True)
  1543. df_results = pd.DataFrame(result2, columns=[
  1544. "Feature Set","Classifier", "Best Parameters",
  1545. "Mean_Accuracy", "SD_Acc", "Mean_Balanced_Accuracy", "SD_Bal_Acc",
  1546. "Mean_AUC_Score", "SD_AUC", "Mean_Recall", "SD_Recall",
  1547. "Mean_Precision", "SD_Precision", "N_Samples"])
  1548. df_results.to_csv(f"{output_dir}/US_model_summary_results.csv", index=False)
  1549. # Save per-fold metrics
  1550. for metric in ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']:
  1551. rows = []
  1552. for feat, clf_dict in metrics_storage.items():
  1553. for clf, metric_dict in clf_dict.items():
  1554. row = [feat, clf] + metric_dict[metric]
  1555. rows.append(row)
  1556. if rows:
  1557. cols = ['Feature Set', 'Classifier'] + [f'Fold_{i+1}' for i in range(len(rows[0])-2)]
  1558. pd.DataFrame(rows, columns=cols).to_csv(f"{output_dir}/per_fold_metrics/OUS_{metric}.csv", index=False)
  1559. #-------------------------------------------------
  1560. # Define the list of metrics
  1561. metric_names = ['accuracy_model', 'balanced_accuracy_model', 'roc_auc', 'sensitivity_model', 'precision_model']
  1562. # Initialize a list to collect final rows
  1563. final_rows = []
  1564. # Loop through the features and classifiers, and gather the metric data
  1565. for feature, classifiers in metrics_storage.items():
  1566. for clf, values in classifiers.items():
  1567. # Determine the number of folds dynamically from any metric
  1568. num_folds = len(next(iter(values.values())))
  1569. for fold_idx in range(num_folds):
  1570. row = {
  1571. 'Feature Set': feature,
  1572. 'Classifier': clf,
  1573. 'Fold': fold_idx + 1
  1574. }
  1575. # Fill in each metric value for this fold
  1576. for metric in metric_names:
  1577. metric_values = values.get(metric, [None] * num_folds)
  1578. row[metric] = metric_values[fold_idx]
  1579. final_rows.append(row)
  1580. # Create DataFrame from the collected rows
  1581. df_final = pd.DataFrame(final_rows)
  1582. # Save the DataFrame to CSV
  1583. output_file = f"{output_dir}/per_fold_metrics/OUS_ALL_SP_Baseline_combined_metrics.csv"
  1584. df_final.to_csv(output_file, index=False)
  1585. print(f"Saved: {output_file}")
  1586. #-------------------------------------------------
  1587. # Save the confusion_avg_dict for later use
  1588. with open(f"{output_dir}/per_fold_metrics/OUS_confusion_avg_dict.pkl", 'wb') as f:
  1589. pickle.dump(confusion_avg_dict, f)
  1590. print("Saved OVS_confusion_avg_dict.pkl")
  1591. ### ----------------------------------------------------------------------------
  1592. # Save Permutation Importance (AUC)
  1593. perm_auc_df = []
  1594. for (feat_set, clf), vals in perm_importance_dict.items():
  1595. for f, imp, std in zip(vals['features'], vals['importances_mean'], vals['importances_std']):
  1596. perm_auc_df.append([feat_set, clf, f, imp, std])
  1597. pd.DataFrame(perm_auc_df, columns=['Feature Set', 'Classifier', 'Feature', 'Mean Importance', 'Std']).to_csv(
  1598. f"{output_dir}/OUS_permutation_importance_auc_cv.csv", index=False)
  1599. print("Saved permutation importance (ROC AUC).")
  1600. # %%

4_ML_modified_with_feature_importance_Github_19022026.ipynb at commit a86a96c, no license · at the source

Overview

Authors: Neha Yadav1, Jayshaan Baibhav1, Seemadri Subhadarshini2, Saroj Kumar Das Majumdar3, Mohit K. Jolly2, Vivek Tiwari1
ORCID iDs: Vivek Tiwari
  1. Indian Institute of Science Education and Research Berhampur, Berhampur, Odisha, India
  2. Department of Bioengineering, Indian Institute of Science, Bengaluru, India
  3. All Indian Institute of Medical Sciences (AIIMS), Bhubaneswar, Odisha, India
Journal: iScience, volume 29, issue 4, article 115329
Dates: received 16 July 2025; accepted 9 March 2026; published online 11 March 2026
Type: Research article · Language: English
License: CC BY-NC
Identifiers: DOI 10.1016/j.isci.2026.115329 · PMID 42006316 · PMCID PMC13091536 · OpenAlex W7135032288
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), other condition (population), clinical / translational (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Connectivity, Complexity
Keywords: health sciences, oncology, medical physics, magnetic resonance imaging
Topic: Glioma Diagnosis and Treatment (Genetics, Medicine), according to OpenAlex
Funding: IISER Berhampur Seed Fund (5/13/2022/NCD/III); Indian Council of Medical Research; SERB-SRG (SRG/2022/002253); WoCoN; Param Hansa Centre for Computational Oncology; IISc Bangalore
Citations: not cited yet (Europe PMC); 59 references in the paper
Research resources: Python 3 RRID:SCR_008394

Abstract

Brain metastases (BMs) and gliomas arise from distinct biological origins yet frequently present with overlapping radiological features, particularly within peritumoral regions. By quantifying fractal dimension (FD3D) and lacunarity (Lac3D) across enhancing, non-enhancing, and edematous tumor subcomponents, we demonstrate that BMs from diverse primary cancers converge on a conserved structural phenotype within the brain-microenvironment, whereas gliomas retain distinct subcomponent complexity and geometric profiles. Enhancing subcomponents of BMs exhibited intermediate structural complexity between low- and high-grade gliomas (LGGs and HGGs), while non-enhancing and edematous compartments were structurally smoother. The integration of subcomponent complexity and volumetric fractions enabled accurate discrimination across independent imaging cohorts, and edema fractality uniquely predicted survival in BMs. Independent transcriptomic analyses revealed pathway-level convergence consistent with brain microenvironment-driven remodeling, yet molecular programs remained distinct from gliomas. These findings establish a quantitative neuroimaging-based geometric framework with clinical utility in neuro-oncology settings, reducing reliance on immediate biopsy for diagnostic and prognostic stratification.

Reproduced under the paper's license (CC BY-NC), from the paper cited above.

Repositories

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

nibr-lab/Brain_Mets_FD_Lac_Manuscript

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: a86a96cc00504d7f9f1898c94fe761cd9cefe9dd, 20 February 2026
Languages: Jupyter (7), R (5)
Size: 12 files, 12 scripts
Software Heritage: not archived
Found in: “Data and code availability”
Holds: 7 notebooks
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (7 files), pandas (7 files), Matplotlib (5 files), SciPy (5 files), seaborn (4 files), tidyverse (4 files), ggplot2 (3 files), NiBabel (3 files), statannotations (3 files), statsmodels (3 files), ggpubr (2 files), OpenCV (2 files), scikit-posthocs (2 files), clusterProfiler (1 file), DESeq2 (1 file), emmeans (1 file), imbalanced-learn (1 file), pheatmap (1 file), scikit-learn (1 file), SHAP (1 file), SimpleITK (1 file), survival (1 file), XGBoost (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
12 files

nibr671lab/Brain_Mets_FD_Lac_Manuscript).•Any

License: none: the authors keep all their rights
State: the link is dead, verified on 30 September 2026
Evidence: found in the paper
Software Heritage: not archived
Found in: “Data and code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 30 September 2026: the link is dead
  • 30 September 2026: the link is dead

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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 12 scripts, each with its path and the digest of its content;
  • 17 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

Data and code availability

• The imaging data utilized in this study were sourced from The Cancer Imaging Archive (TCIA) portal (https://www.cancerimagingarchive.net/) and UCSF (https://imagingdatasets.ucsf.edu/dataset/1). The tumor subcomponent masks were obtained using the information published by Bakas et al. and others. • Transcriptomic data for gliomas (TCGA-LGG and TCGA-GBM) were accessed via the Genomic Data Commons (https://portal.gdc.cancer.gov/; dbGaP Study Accession: phs000178 (https://www.ncbi.nlm.nih.gov/projects/gap/cgi-bin/study.cgi?study_id=phs000178)), while transcriptomic data for brain metastases were obtained from publicly available datasets by Zhang et al. (https://doi.org/10.1016/j.xcrm.2023.100932) and Biermann et al. (https://doi.org/10.1016/j.cell.2022.06.007) through the NCBI Gene Expression Omnibus (GEO). • All datasets used are publicly accessible and fully deidentified. • The Python and R codes used for fractal dimension and lacunarity calculations from tumor masks, as well as for other analyses, are available in our publicly accessible GitHub repository (https://github.com/nibr671lab/Brain_Mets_FD_Lac_Manuscript (https://github.com/nibr-lab/Brain_Mets_FD_Lac_Manuscript)). • Any additional information required to reanalyze the data reported in this paper is available from the lead author upon request.

Reproduced under the paper's license (CC BY-NC), from the paper cited above.

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, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 6 authors, 4 keywords, 6 funders, 56 references, 1 RRID.

Cite

This paper

Yadav, N., Baibhav, J., Subhadarshini, S., Das Majumdar, S. K., Jolly, M. K., & Tiwari, V. (2026). Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas. iScience, 29(4), 115329. https://doi.org/10.1016/j.isci.2026.115329

BibTeX

@article{yadav2026brain,
author = {Yadav, Neha and Baibhav, Jayshaan and Subhadarshini, Seemadri and Das Majumdar, Saroj Kumar and Jolly, Mohit K. and Tiwari, Vivek},
title = {{Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas}},
journal = {iScience},
year = {2026},
month = mar,
volume = {29},
number = {4},
pages = {115329},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/j.isci.2026.115329},
url = {https://doi.org/10.1016/j.isci.2026.115329},
pmid = {42006316},
pmcid = {PMC13091536}
}

RIS

TY - JOUR
AU - Yadav, Neha
AU - Baibhav, Jayshaan
AU - Subhadarshini, Seemadri
AU - Das Majumdar, Saroj Kumar
AU - Jolly, Mohit K.
AU - Tiwari, Vivek
TI - Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/03/11
VL - 29
IS - 4
SP - 115329
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.115329
UR - https://doi.org/10.1016/j.isci.2026.115329
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.115329",
"type": "article-journal",
"title": "Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas",
"container-title": "iScience",
"author": [
{
"family": "Yadav",
"given": "Neha"
},
{
"family": "Baibhav",
"given": "Jayshaan"
},
{
"family": "Subhadarshini",
"given": "Seemadri"
},
{
"family": "Das Majumdar",
"given": "Saroj Kumar"
},
{
"family": "Jolly",
"given": "Mohit K."
},
{
"family": "Tiwari",
"given": "Vivek"
}
],
"container-title-short": "iScience",
"volume": "29",
"issue": "4",
"page": "115329",
"DOI": "10.1016/j.isci.2026.115329",
"PMID": "42006316",
"PMCID": "PMC13091536",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.115329",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
11
]
]
}
}

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, survival, SHAP, 15 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, survival, SHAP, 12 other tools, other condition
[3] doi:10.1038/s41467-026-72598-z [code]
Functional impact of genetic background on variable expressivity in neurodevelopmental disorders.
Journal: Nature communications
In common: DESeq2, clusterProfiler, pheatmap, 9 other tools, ncbi.nlm.nih.gov/projects/gap, other condition
[4] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: survival, DESeq2, clusterProfiler, 11 other tools, genetics / omics, other condition
[5] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: DESeq2, clusterProfiler, pheatmap, 10 other tools, genetics / omics, other condition, 2 references
[6] doi:10.1073/pnas.2516601123 [code]
Unveiling the glymphatic system's role in brain aging: A comprehensive biomarker and modifiable intervention target.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: survival, SHAP, XGBoost, 10 other tools, clinical / translational
[7] doi:10.1038/s41586-026-10629-x [code]
Whole-genome duplication shaped cell-type evolution in the vertebrate brain.
Journal: Nature
In common: DESeq2, clusterProfiler, pheatmap, 10 other tools, genetics / omics, 1 reference
[8] doi:10.1016/j.cell.2026.05.026 [code]
The critical role of the endogenous immune compartment after CAR T cell therapy in recurrent GBM.
Journal: Cell
In common: statannotations, survival, clusterProfiler, 9 other tools, genetics / omics, other condition
[9] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: DESeq2, clusterProfiler, pheatmap, 10 other tools, genetics / omics
[10] doi:10.1016/j.cell.2026.05.047 [code]
An emergent disease-associated motor neuron state precedes cell death in ALS.
Journal: Cell
In common: statannotations, DESeq2, pheatmap, 9 other tools, genetics / omics, other condition

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.