OSCR

MLMarker: a machine learning framework for tissue inference and biomarker discovery.

Code ↔ Paper

34 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 34 matches
  1. [1] § Reuse of pan-cancer dataset (MSV000095036) ↔ Cancer_datasets_Kuster/baseline_comparison_clean.ipynb, lines 79–138 · score 0.85 · parotid gland, small intestine, pituitary gland, colon, duodenum, rectum
  2. [2] § Baseline atlas comparisons and alternative classifiers › Interpretation analyses ↔ custom_functions.py, lines 248–305 · score 0.84 · cellular components, Human Protein Atlas, molecular functions, biological processes, KEGG, pathways
  3. [3] § Results › Reprocessed data from cerebral melanoma (PXD007592) ↔ pages/1_QC.py, lines 187–249 · score 0.84 · intensity variation, signal proportion, global sample, intensity distribution, protein intensity, feature space
  4. [4] § Reuse of pan-cancer dataset (MSV000095036) ↔ Home.py, lines 243–243 · score 0.77 · parotid gland, small intestine, pituitary gland, colon, duodenum, rectum
  5. [5] § Baseline atlas comparisons and alternative classifiers › Reference atlases ↔ Cancer_datasets_Kuster/baseline_comparison_clean.ipynb, lines 54–77 · score 0.76 · excluding cancer, normalised intensities, UniProt, tissue matrix, training atlas, HPA
  6. [6] § Results › Circularity and double-dipping ↔ PXD007592_responders_melanoma/03_shap_vs_dea_correlation.ipynb, lines 630–680 · score 0.74 · circular reasoning, reflects cohort, capture tissue, prone, complementary, leakage
  7. [7] § Results › Penalty factor › Performance across sample types: dense vs. sparse ↔ Missingnes_testing/02_penalty_factor_comparison.ipynb, lines 550–581 · score 0.72 · high dropout, low dropout, sparse biofluid, Penalty benefit, dense tissue, beneficial
  8. [8] § Penalty factor evaluation by simulated protein missingness › Penalty strategies ↔ Missingnes_testing/penalty_factor_analysis.py, lines 128–144 · score 0.71 · piecewise linear interpolation, Adaptive penalties, low coverage
  9. [9] § Reprocessed data from cerebrospinal fluid (PXD008029) ↔ Home.py, lines 243–243 · score 0.70 · bone marrow, adipose tissue, pituitary gland, testis, lung, monocytes
  10. [10] § Reprocessed data from cerebrospinal fluid (PXD008029) ↔ test.ipynb, lines 140–141 · score 0.70 · bone marrow, adipose tissue, pituitary gland, testis, lung, monocytes
  11. [11] § Results › Penalty factor › Performance across sample types: dense vs. sparse ↔ Missingnes_testing/02_penalty_factor_comparison.ipynb, lines 405–548 · score 0.69 · targeted missingness, sparse samples, dense tissue, loss, missing proteins, MNAR
  12. [12] § Materials and methods › The MLMarker package › Penalty factor for missing feature effects ↔ old_modules/mlmarker.py, lines 116–165 · score 0.64 · identifies absent proteins, remain unchanged, penalty factor, SHAP, zero, MLMarker
  13. [13] § Results › Reprocessed data from cerebral melanoma (PXD007592) ↔ PXD007592_responders_melanoma/classify_04_protein_analysis.ipynb, lines 41–127 · score 0.64 · Mann Whitney, good responders, higher brain, poor responders, brain prediction, scores
  14. [14] § Results › Reprocessed data from cerebral melanoma (PXD007592) ↔ PXD007592_responders_melanoma/05_clusteringgroups.ipynb, lines 224–297 · score 0.63 · response status, Mann Whitney, higher brain, Poor responders, UMAP, clustering
  15. [15] § Results › Penalty factor ↔ Missingnes_testing/02_penalty_factor_comparison.ipynb, lines 405–548 · score 0.62 · Targeted missingness, detection limits, loss, PXD009021, lost, MNAR
  16. [16] § Materials and methods › Reprocessing pipeline › Differential expression analysis ↔ PXD007592_responders_melanoma/msqrob_analysis.R, lines 106–138 · score 0.62 · msqrob2, Peptide intensities, aggregated, median, responders, models
  17. [17] § Materials and methods › The MLMarker package › Annotation utilities and visualisation ↔ custom_functions.py, lines 248–305 · score 0.62 · Human Protein Atlas, tissue expression, Profiler, enrichment, SHAP, score
  18. [18] § Results › Reprocessed data from cerebral melanoma (PXD007592) ↔ pages/1_QC.py, lines 20–28 · score 0.60 · predefined proteins, missing proteins, artifacts, quality, technical, intensity
  19. [19] § Baseline atlas comparisons and alternative classifiers › Alternative classification methods ↔ Cancer_datasets_Kuster/baseline_comparison_clean.ipynb, lines 368–415 · score 0.60 · nearest neighbours classification, sample profile, distance, correlation, training, tissue
  20. [20] § Reuse of pan-cancer dataset (MSV000095036) ↔ Cancer_datasets_Kuster/baseline_comparison_clean.ipynb, lines 79–138 · score 0.60 · healthy tissues, glioma, LN, OE, DLBCL, OSCC
  21. [21] § Results › Reprocessed data from cerebral melanoma (PXD007592) ↔ PXD007592_responders_melanoma/05_clusteringgroups.ipynb, lines 224–297 · score 0.59 · Mann Whitney, higher brain, poor responders, good responders, brain prediction, UMAP
  22. [22] § Baseline atlas comparisons and alternative classifiers › Interpretation analyses ↔ pages/2_Visualisations.py, lines 22–31 · score 0.58 · SHapley, exPlanations, Additive, predictions, tissue, proteins
  23. [23] § Results › Circularity and double-dipping ↔ PXD007592_responders_melanoma/03_shap_vs_dea_correlation.ipynb, lines 87–128 · score 0.57 · MLMarker feature space, proteins outside, log10 transformed, fold change, dipping, Circularity
  24. [24] § Reprocessed data from cerebrospinal fluid (PXD008029) ↔ PXD008029_CSF/classify_csf.ipynb, lines 211–256 · score 0.56 · adipose tissue, CSF samples, FDR, monocytes, plasma, PXD008029
  25. [25] § Materials and methods › The MLMarker package › Penalty factor for missing feature effects ↔ mlmarker/explainability.py, lines 146–208 · score 0.56 · optional penalty factor, Absent proteins, SHAP, zero, MLMarker, tissue
  26. [26] § Results › Circularity and double-dipping ↔ PXD007592_responders_melanoma/03_shap_vs_dea_correlation.ipynb, lines 630–680 · score 0.56 · indicates partial overlap, limited correlation, circular, SHAP, Spearman, MLMarker
  27. [27] § Results › Penalty factor › Performance across sample types: dense vs. sparse ↔ Missingnes_testing/rank_analysis_heatmap.py, lines 73–136 · score 0.56 · sparse biofluid, dense tissue, dropout rates, heatmap, adaptive, rank
  28. [28] § Penalty factor evaluation by simulated protein missingness ↔ Missingnes_testing/02_penalty_factor_comparison.ipynb, lines 14–46 · score 0.55 · pituitary gland, target tissue, PXD009021, Simulations, PXD008029, missingness
  29. [29] § Penalty factor evaluation by simulated protein missingness ↔ Missingnes_testing/penalty_factor_analysis.py, lines 193–205 · score 0.54 · pituitary gland, target tissue, NSAF, ionbot, PXD008029, missingness
  30. [30] § Results › Penalty factor › Performance across sample types: dense vs. sparse ↔ Missingnes_testing/rank_analysis_heatmap.py, lines 73–136 · score 0.54 · lower ranks, sparse biofluids, dense tissue, missingness, penalty
  31. [31] § Results › Circularity and double-dipping ↔ PXD007592_responders_melanoma/03_shap_vs_dea_correlation.ipynb, lines 87–128 · score 0.53 · MLMarker feature space, Double dipping, outside, circularity, external, protein
  32. [32] § Reprocessed data from cerebrospinal fluid (PXD008029) › SHAP analysis of CSF samples within the Streamlit application ↔ PXD008029_CSF/classify_csf.ipynb, lines 291–307 · score 0.52 · bone marrow, pituitary gland, CSF, brain, predicted
  33. [33] § Results › Differential expression analysis with MSqRob ↔ PXD007592_responders_melanoma/msqrob_analysis.R, lines 141–220 · score 0.51 · log fold change, log10 adjusted, volcano, MSqRob, responder, brain
  34. [34] § Results › Penalty factor › Performance across sample types: dense vs. sparse ↔ Missingnes_testing/penalty_factor_analysis.py, lines 322–359 · score 0.50 · adaptive piecewise, dropout rates, penalty factor, MNAR, simulating, missingness

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 · 2,059 lines · 86 KB · no license · 4 matches

  1. # %%
  2. # === 1. Setup and Imports ===
  3. import os
  4. import warnings
  5. warnings.filterwarnings('ignore')
  6. import pandas as pd
  7. import numpy as np
  8. import matplotlib.pyplot as plt
  9. import seaborn as sns
  10. from scipy.stats import spearmanr
  11. from tqdm import tqdm
  12. # Plot settings
  13. sns.set_style("whitegrid")
  14. plt.rcParams['figure.figsize'] = (12, 8)
  15. plt.rcParams['font.size'] = 12
  16. plt.rcParams['figure.dpi'] = 100
  17. print("Libraries loaded successfully.")
  18. # %% [markdown]
  19. # ## 2. Load Data
  20. #
  21. # Load protein expression data, MLMarker predictions, and reference atlases.
  22. # %%
  23. # Load protein expression data
  24. protein_data_path = 'search__Picked_Group_FDR_Outputs__Picked_Group_FDR_Outputs_-_FP_Booster__combined_protein.tsv'
  25. local_path = 'search__Picked_Group_FDR_Outputs__Picked_Group_FDR_Outputs_-_FP_Booster__combined_protein.tsv'
  26. protein_df = pd.read_csv(local_path if os.path.exists(local_path) else protein_data_path, sep='\t')
  27. # Load MLMarker predictions
  28. mlmarker_predictions = pd.read_csv('Entire_picked_FDR_MLMarker_predictions_dev_penalty1.csv', index_col=0)
  29. # Load metadata
  30. metadata = pd.read_csv('MassIVE_Mapping.txt', sep='\t')
  31. # Load reference atlases
  32. hpa = pd.read_csv('antibody_atlas.csv')
  33. pdb = pd.read_csv('proteomicsdb.csv')
  34. # Load MLMarker training data
  35. ml_train = pd.read_csv('training_atlas_7592%_10exp.csv.gz', index_col=0)
  36. print(f"Protein data: {protein_df.shape}")
  37. print(f"MLMarker predictions: {mlmarker_predictions.shape}")
  38. print(f"Metadata entries: {len(metadata)}")
  39. print(f"HPA: {hpa['Uniprot_id'].nunique()} proteins, {hpa['Tissue'].nunique()} tissues")
  40. print(f"PDB: {pdb['UniProt'].nunique()} proteins, {pdb['tissue'].nunique()} tissues")
  41. print(f"MLMarker training: {ml_train.shape[0]} samples, {ml_train['tissue_name'].nunique()} tissues")
  42. # %% [markdown]
  43. # ## 3. Create Atlas Matrices
  44. #
  45. # Convert all data sources to protein × tissue matrices.
  46. # %%
  47. # === HPA Atlas Matrix ===
  48. hpa_matrix = hpa.pivot_table(
  49. index='Uniprot_id', columns='Tissue', values='Level', aggfunc='mean'
  50. ).fillna(0)
  51. print(f"HPA matrix: {hpa_matrix.shape} (proteins × tissues)")
  52. # === PDB Atlas Matrix (exclude cancer/fluids) ===
  53. pdb_matrix = pdb.pivot_table(
  54. index='UniProt', columns='tissue', values='normalised intensity', aggfunc='mean'
  55. ).fillna(0)
  56. print(f"PDB matrix: {pdb_matrix.shape} (proteins × tissues)")
  57. # === MLMarker Training Atlas ===
  58. metadata_cols = ['tissue_name', 'disease_status', 'fluid', 'cell_type']
  59. protein_cols = [c for c in ml_train.columns if c not in metadata_cols]
  60. ml_atlas = ml_train.groupby('tissue_name')[protein_cols].mean().T
  61. print(f"MLMarker training atlas: {ml_atlas.shape} (proteins × tissues)")
  62. # %% [markdown]
  63. # ## 4. Define Tissue Mappings
  64. #
  65. # Map cancer cohorts to expected tissue of origin for each atlas.
  66. # %%
  67. # Cohorts to evaluate (excluding DLBCL+ as it's redundant with DLBCL)
  68. eval_cohorts = ['CRC', 'DLBCL', 'Glioma', 'OSCC', 'healthy LN', 'healthy OE']
  69. # === MLMarker Mappings ===
  70. cancer_to_mlmarker = {
  71. 'CRC': 'Colon', 'DLBCL': 'B-cells',
  72. 'Glioma': 'Brain', 'OSCC': 'Parotid gland',
  73. 'healthy LN': 'Lymph node', 'healthy OE': 'Parotid gland'
  74. }
  75. cancer_to_mlmarker_extended = {
  76. 'CRC': ['Colon', 'Small intestine', 'Rectum', 'Duodenum', 'Appendix'],
  77. 'DLBCL': ['B-cells', 'Lymph node', 'Spleen'],
  78. 'Glioma': ['Brain', 'Pituitary gland'],
  79. 'OSCC': ['Parotid gland', 'Tonsil'],
  80. 'healthy LN': ['Lymph node', 'B-cells', 'Spleen'],
  81. 'healthy OE': ['Parotid gland', 'Tonsil', 'Esophagus']
  82. }
  83. # === HPA Mappings ===
  84. cancer_to_hpa = {
  85. 'CRC': 'Colon', 'DLBCL': 'Lymph node',
  86. 'Glioma': 'Cerebral cortex', 'OSCC': 'Salivary gland',
  87. 'healthy LN': 'Lymph node', 'healthy OE': 'Oral mucosa'
  88. }
  89. cancer_to_hpa_extended = {
  90. 'CRC': ['Colon', 'Small intestine', 'Rectum', 'Duodenum', 'Appendix'],
  91. 'DLBCL': ['Lymph node', 'Spleen', 'Tonsil', 'Bone marrow'],
  92. 'Glioma': ['Cerebral cortex', 'Cerebellum', 'Hippocampus', 'Caudate', 'Pituitary gland'],
  93. 'OSCC': ['Salivary gland', 'Oral mucosa', 'Nasopharynx'],
  94. 'healthy LN': ['Lymph node', 'Spleen', 'Tonsil'],
  95. 'healthy OE': ['Oral mucosa', 'Salivary gland', 'Esophagus']
  96. }
  97. # === PDB Mappings ===
  98. cancer_to_pdb = {
  99. 'CRC': 'Colon', 'DLBCL': 'B-lymphocyte',
  100. 'Glioma': 'Brain', 'OSCC': 'Salivary gland',
  101. 'healthy LN': 'Lymph node', 'healthy OE': 'Mouth'
  102. }
  103. cancer_to_pdb_extended = {
  104. 'CRC': ['Colon', 'Colon muscle', 'Gut', 'Small intestine'],
  105. 'DLBCL': ['B-lymphocyte', 'Lymph node', 'Spleen'],
  106. 'Glioma': ['Brain', 'Cerebral cortex'],
  107. 'OSCC': ['Salivary gland', 'Mouth'],
  108. 'healthy LN': ['Lymph node', 'B-lymphocyte', 'Spleen'],
  109. 'healthy OE': ['Mouth', 'Salivary gland', 'Esophagus']
  110. }
  111. print(f"Cohorts to evaluate: {eval_cohorts}")
  112. print(f"\nExcluded: Melanoma, HELA, PDAC (no direct healthy tissue equivalent)")
  113. print(f"Excluded: DLBCL+ (redundant with DLBCL)")
  114. # %% [markdown]
  115. # ## 5. Prepare Sample Data
  116. # %%
  117. # Get expression columns
  118. expression_columns = [col for col in protein_df.columns if "MaxLFQ Intensity" in col]
  119. sample_names = [col.split(' MaxLFQ Intensity')[0] for col in expression_columns]
  120. # Create sample metadata
  121. sample_metadata = pd.DataFrame({'sample': sample_names, 'expression_col': expression_columns})
  122. sample_metadata = sample_metadata.merge(
  123. metadata[['Cohort', 'Folder name FP_DDA']],
  124. left_on='sample', right_on='Folder name FP_DDA', how='inner'
  125. ).drop_duplicates()
  126. sample_metadata = sample_metadata[sample_metadata['Cohort'].isin(eval_cohorts)]
  127. # Create expression matrix (log2 transformed)
  128. valid_cols = sample_metadata['expression_col'].tolist()
  129. sample_expr = protein_df[['Protein ID'] + valid_cols].set_index('Protein ID')
  130. sample_expr_log = np.log2(sample_expr + 1)
  131. # Create MLMarker column mapping
  132. sample_mapping_mlmarker = {}
  133. for sample in sample_metadata['sample']:
  134. matches = [c for c in mlmarker_predictions.columns if sample in c]
  135. if matches:
  136. sample_mapping_mlmarker[sample] = matches[0]
  137. print(f"Samples per cohort:")
  138. print(sample_metadata['Cohort'].value_counts())
  139. print(f"\nTotal samples: {len(sample_metadata)}")
  140. print(f"MLMarker mapping found: {len(sample_mapping_mlmarker)} samples")
  141. # %% [markdown]
  142. # ## 6. Define Classification Functions
  143. # %%
  144. def atlas_correlation_classify(sample_profile, atlas_matrix):
  145. """
  146. Classify a sample using Spearman correlation with atlas tissues.
  147. Returns: (predicted_tissue, score, all_scores_dict)
  148. """
  149. common_proteins = sample_profile.index.intersection(atlas_matrix.index)
  150. if len(common_proteins) < 10:
  151. return None, 0, {}
  152. sample_aligned = sample_profile.loc[common_proteins].values
  153. atlas_aligned = atlas_matrix.loc[common_proteins]
  154. correlations = {}
  155. for tissue in atlas_aligned.columns:
  156. tissue_profile = atlas_aligned[tissue].values
  157. if tissue_profile.std() == 0 or sample_aligned.std() == 0:
  158. correlations[tissue] = 0
  159. else:
  160. corr, _ = spearmanr(sample_aligned, tissue_profile)
  161. correlations[tissue] = corr if not np.isnan(corr) else 0
  162. if correlations:
  163. predicted = max(correlations, key=correlations.get)
  164. return predicted, correlations[predicted], correlations
  165. return None, 0, {}
  166. def get_topk_predictions(scores_dict, k=5):
  167. """Get top-k tissue predictions from scores dictionary."""
  168. if not scores_dict:
  169. return []
  170. return sorted(scores_dict.keys(), key=lambda x: scores_dict[x], reverse=True)[:k]
  171. print("Classification functions defined.")
  172. # %% [markdown]
  173. # ## 7. Run Classification (All 4 Methods)
  174. # %%
  175. # Initialize results storage
  176. results = []
  177. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="Classifying samples"):
  178. sample_name = row['sample']
  179. cohort = row['Cohort']
  180. expr_col = row['expression_col']
  181. # Get sample profile (non-zero proteins only)
  182. sample_profile = sample_expr_log[expr_col]
  183. sample_profile = sample_profile[sample_profile > 0]
  184. result = {'sample': sample_name, 'cohort': cohort}
  185. # === 1. MLMarker Model ===
  186. if sample_name in sample_mapping_mlmarker:
  187. ml_col = sample_mapping_mlmarker[sample_name]
  188. ml_scores = mlmarker_predictions[ml_col].to_dict()
  189. result['mlmarker_pred'] = mlmarker_predictions[ml_col].idxmax()
  190. else:
  191. ml_scores = {}
  192. result['mlmarker_pred'] = None
  193. true_strict = cancer_to_mlmarker.get(cohort)
  194. true_extended = cancer_to_mlmarker_extended.get(cohort, [])
  195. for k in [1, 2, 3, 4, 5]:
  196. top_k = get_topk_predictions(ml_scores, k)
  197. result[f'mlmarker_top{k}_strict'] = true_strict in top_k if top_k else False
  198. result[f'mlmarker_top{k}_extended'] = any(t in top_k for t in true_extended) if top_k else False
  199. # === 2. MLMarker Training Atlas ===
  200. pred, _, scores = atlas_correlation_classify(sample_profile, ml_atlas)
  201. result['ml_training_pred'] = pred
  202. for k in [1, 2, 3, 4, 5]:
  203. top_k = get_topk_predictions(scores, k)
  204. result[f'ml_training_top{k}_strict'] = true_strict in top_k if top_k else False
  205. result[f'ml_training_top{k}_extended'] = any(t in top_k for t in true_extended) if top_k else False
  206. # === 3. HPA Atlas ===
  207. true_hpa_strict = cancer_to_hpa.get(cohort)
  208. true_hpa_extended = cancer_to_hpa_extended.get(cohort, [])
  209. pred, _, scores = atlas_correlation_classify(sample_profile, hpa_matrix)
  210. result['hpa_pred'] = pred
  211. for k in [1, 2, 3, 4, 5]:
  212. top_k = get_topk_predictions(scores, k)
  213. result[f'hpa_top{k}_strict'] = true_hpa_strict in top_k if top_k else False
  214. result[f'hpa_top{k}_extended'] = any(t in top_k for t in true_hpa_extended) if top_k else False
  215. # === 4. PDB Atlas ===
  216. true_pdb_strict = cancer_to_pdb.get(cohort)
  217. true_pdb_extended = cancer_to_pdb_extended.get(cohort, [])
  218. pred, _, scores = atlas_correlation_classify(sample_profile, pdb_matrix)
  219. result['pdb_pred'] = pred
  220. for k in [1, 2, 3, 4, 5]:
  221. top_k = get_topk_predictions(scores, k)
  222. result[f'pdb_top{k}_strict'] = true_pdb_strict in top_k if top_k else False
  223. result[f'pdb_top{k}_extended'] = any(t in top_k for t in true_pdb_extended) if top_k else False
  224. results.append(result)
  225. results_df = pd.DataFrame(results)
  226. print(f"\nClassification complete! Results: {results_df.shape}")
  227. # %% [markdown]
  228. # ## 8. Calculate Top-k Accuracy Summary
  229. # %%
  230. # Calculate overall accuracy for each method
  231. methods = ['mlmarker', 'ml_training', 'hpa', 'pdb']
  232. method_labels = ['MLMarker', 'MLMarker Training', 'HPA Atlas', 'PDB Atlas']
  233. summary_data = []
  234. for method, label in zip(methods, method_labels):
  235. for k in [1, 2, 3, 4, 5]:
  236. strict_acc = results_df[f'{method}_top{k}_strict'].mean() * 100
  237. extended_acc = results_df[f'{method}_top{k}_extended'].mean() * 100
  238. summary_data.append({
  239. 'Method': label, 'k': k,
  240. 'Strict (%)': strict_acc, 'Extended (%)': extended_acc
  241. })
  242. summary_df = pd.DataFrame(summary_data)
  243. # Display Top-1 and Top-5 summary
  244. print("=" * 70)
  245. print("OVERALL ACCURACY SUMMARY")
  246. print("=" * 70)
  247. print(f"\n{'Method':<20} {'Top-1 Strict':>12} {'Top-1 Extended':>15} {'Top-5 Extended':>15}")
  248. print("-" * 70)
  249. for method, label in zip(methods, method_labels):
  250. t1_strict = results_df[f'{method}_top1_strict'].mean() * 100
  251. t1_ext = results_df[f'{method}_top1_extended'].mean() * 100
  252. t5_ext = results_df[f'{method}_top5_extended'].mean() * 100
  253. print(f"{label:<20} {t1_strict:>11.1f}% {t1_ext:>14.1f}% {t5_ext:>14.1f}%")
  254. print(f"\nTotal samples: {len(results_df)}")
  255. # %% [markdown]
  256. # ## 9. Per-Cohort Accuracy Analysis
  257. # %%
  258. # Calculate per-cohort accuracy
  259. cohort_data = []
  260. for cohort in eval_cohorts:
  261. cohort_mask = results_df['cohort'] == cohort
  262. n_samples = cohort_mask.sum()
  263. for method, label in zip(methods, method_labels):
  264. for k in [1, 3, 5]:
  265. strict_acc = results_df.loc[cohort_mask, f'{method}_top{k}_strict'].mean() * 100
  266. extended_acc = results_df.loc[cohort_mask, f'{method}_top{k}_extended'].mean() * 100
  267. cohort_data.append({
  268. 'Cohort': cohort, 'Method': label, 'k': k, 'n': n_samples,
  269. 'Strict (%)': strict_acc, 'Extended (%)': extended_acc
  270. })
  271. cohort_df = pd.DataFrame(cohort_data)
  272. # Display per-cohort Top-1 Extended accuracy
  273. print("\nPer-Cohort Top-1 Extended Accuracy:")
  274. print("=" * 80)
  275. pivot = cohort_df[cohort_df['k'] == 1].pivot(index='Cohort', columns='Method', values='Extended (%)')
  276. pivot = pivot[method_labels] # Reorder columns
  277. print(pivot.round(1).to_string())
  278. # %% [markdown]
  279. # ## 10. K-Nearest Neighbors (KNN) Classification
  280. #
  281. # Instead of correlating with tissue centroids (means), KNN classifies based on the k nearest training samples. This preserves sample-level variability and may better capture tissue-specific patterns.
  282. # %%
  283. # Prepare MLMarker training data for KNN
  284. # ml_train has samples as rows, proteins as columns, with tissue_name column
  285. # Get protein columns (exclude metadata)
  286. ml_protein_cols = [c for c in ml_train.columns if c not in ['tissue_name', 'disease_status', 'fluid', 'cell_type']]
  287. ml_train_X = ml_train[ml_protein_cols]
  288. ml_train_y = ml_train['tissue_name']
  289. print(f"MLMarker Training data for KNN:")
  290. print(f" Samples: {len(ml_train_X)}")
  291. print(f" Proteins: {len(ml_protein_cols)}")
  292. print(f" Tissues: {ml_train_y.nunique()}")
  293. print(f" Samples per tissue (top 10):")
  294. print(ml_train_y.value_counts().head(10))
  295. # %%
  296. # KNN Classification Function
  297. from sklearn.neighbors import KNeighborsClassifier
  298. from collections import Counter
  299. def knn_classify_with_topk(sample_profile, train_X, train_y, k=5):
  300. """
  301. Classify sample using KNN and return top-k tissue predictions.
  302. Returns: (predicted_tissue, top_k_tissues, vote_counts)
  303. """
  304. # Find common proteins
  305. common_proteins = sample_profile.index.intersection(train_X.columns)
  306. if len(common_proteins) < 10:
  307. return None, [], {}
  308. # Align data
  309. sample_aligned = sample_profile.loc[common_proteins].values.reshape(1, -1)
  310. train_aligned = train_X[common_proteins].values
  311. train_labels = train_y.values
  312. # Handle NaN
  313. sample_aligned = np.nan_to_num(sample_aligned, 0)
  314. train_aligned = np.nan_to_num(train_aligned, 0)
  315. # Fit KNN with k neighbors
  316. knn = KNeighborsClassifier(n_neighbors=min(k, len(train_aligned)), metric='correlation')
  317. knn.fit(train_aligned, train_labels)
  318. # Get k nearest neighbors
  319. distances, indices = knn.kneighbors(sample_aligned)
  320. neighbor_labels = train_labels[indices[0]]
  321. # Count votes
  322. vote_counts = Counter(neighbor_labels)
  323. # Get top-k unique tissues by vote count
  324. sorted_tissues = [t for t, _ in vote_counts.most_common()]
  325. # Get probabilities for all classes
  326. proba = knn.predict_proba(sample_aligned)[0]
  327. class_proba = dict(zip(knn.classes_, proba))
  328. predicted = knn.predict(sample_aligned)[0]
  329. return predicted, sorted_tissues, class_proba
  330. print("KNN classification function defined.")
  331. # %%
  332. # Run KNN on MLMarker Training Data (sample-level)
  333. # Test different k values
  334. k_values = [1, 3, 5, 7, 11]
  335. knn_results = {f'knn_ml_k{k}_pred': [] for k in k_values}
  336. knn_results['sample'] = []
  337. knn_results['cohort'] = []
  338. # Add top-k accuracy tracking for BOTH strict and extended
  339. for k in k_values:
  340. for topk in [1, 2, 3, 4, 5]:
  341. knn_results[f'knn_ml_k{k}_top{topk}_strict'] = []
  342. knn_results[f'knn_ml_k{k}_top{topk}_extended'] = []
  343. print(f"Running KNN classification with k={k_values}...")
  344. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="KNN MLMarker Training"):
  345. sample_name = row['sample']
  346. cohort = row['Cohort']
  347. expr_col = row['expression_col']
  348. sample_profile = sample_expr_log[expr_col]
  349. sample_profile = sample_profile[sample_profile > 0]
  350. knn_results['sample'].append(sample_name)
  351. knn_results['cohort'].append(cohort)
  352. # Get both strict and extended ground truth
  353. true_strict = cancer_to_mlmarker.get(cohort)
  354. true_extended = cancer_to_mlmarker_extended.get(cohort, [])
  355. for k in k_values:
  356. pred, top_tissues, class_proba = knn_classify_with_topk(sample_profile, ml_train_X, ml_train_y, k=k)
  357. knn_results[f'knn_ml_k{k}_pred'].append(pred)
  358. # Calculate top-k accuracy using class probabilities
  359. if class_proba:
  360. sorted_by_proba = sorted(class_proba.keys(), key=lambda x: class_proba[x], reverse=True)
  361. for topk in [1, 2, 3, 4, 5]:
  362. top_k_tissues = sorted_by_proba[:topk]
  363. # Strict: exact 1-to-1 mapping
  364. is_correct_strict = true_strict in top_k_tissues if true_strict else False
  365. knn_results[f'knn_ml_k{k}_top{topk}_strict'].append(is_correct_strict)
  366. # Extended: 1-to-many mapping
  367. is_correct_extended = any(t in top_k_tissues for t in true_extended)
  368. knn_results[f'knn_ml_k{k}_top{topk}_extended'].append(is_correct_extended)
  369. else:
  370. for topk in [1, 2, 3, 4, 5]:
  371. knn_results[f'knn_ml_k{k}_top{topk}_strict'].append(False)
  372. knn_results[f'knn_ml_k{k}_top{topk}_extended'].append(False)
  373. knn_ml_df = pd.DataFrame(knn_results)
  374. print(f"\nKNN results shape: {knn_ml_df.shape}")
  375. # %%
  376. # For HPA and PDB: Use tissue profiles as "samples" for KNN
  377. # This treats each tissue as a single sample
  378. # HPA KNN (using tissue profiles)
  379. # hpa_matrix is proteins × tissues, so transpose to get tissues × proteins (samples × features)
  380. hpa_train_X = hpa_matrix.T # Now: tissues × proteins (each tissue is a "sample")
  381. hpa_train_y = pd.Series(hpa_train_X.index, index=hpa_train_X.index) # Labels = tissue names
  382. # PDB KNN
  383. pdb_train_X = pdb_matrix.T # tissues × proteins
  384. pdb_train_y = pd.Series(pdb_train_X.index, index=pdb_train_X.index)
  385. print(f"HPA for KNN: {hpa_train_X.shape[0]} tissues (samples), {hpa_train_X.shape[1]} proteins (features)")
  386. print(f"PDB for KNN: {pdb_train_X.shape[0]} tissues (samples), {pdb_train_X.shape[1]} proteins (features)")
  387. # Run KNN on HPA and PDB
  388. knn_atlas_results = {'sample': [], 'cohort': [],
  389. 'knn_hpa_pred': [], 'knn_pdb_pred': []}
  390. # Initialize columns for BOTH strict and extended
  391. for topk in [1, 2, 3, 4, 5]:
  392. knn_atlas_results[f'knn_hpa_top{topk}_strict'] = []
  393. knn_atlas_results[f'knn_hpa_top{topk}_extended'] = []
  394. knn_atlas_results[f'knn_pdb_top{topk}_strict'] = []
  395. knn_atlas_results[f'knn_pdb_top{topk}_extended'] = []
  396. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="KNN HPA/PDB"):
  397. sample_name = row['sample']
  398. cohort = row['Cohort']
  399. expr_col = row['expression_col']
  400. sample_profile = sample_expr_log[expr_col]
  401. sample_profile = sample_profile[sample_profile > 0]
  402. knn_atlas_results['sample'].append(sample_name)
  403. knn_atlas_results['cohort'].append(cohort)
  404. # HPA KNN - get both strict and extended ground truth
  405. true_hpa_strict = cancer_to_hpa.get(cohort)
  406. true_hpa_extended = cancer_to_hpa_extended.get(cohort, [])
  407. pred, _, class_proba = knn_classify_with_topk(sample_profile, hpa_train_X, hpa_train_y, k=1)
  408. knn_atlas_results['knn_hpa_pred'].append(pred)
  409. if class_proba:
  410. sorted_by_proba = sorted(class_proba.keys(), key=lambda x: class_proba[x], reverse=True)
  411. for topk in [1, 2, 3, 4, 5]:
  412. top_k_tissues = sorted_by_proba[:topk]
  413. is_correct_strict = true_hpa_strict in top_k_tissues if true_hpa_strict else False
  414. knn_atlas_results[f'knn_hpa_top{topk}_strict'].append(is_correct_strict)
  415. is_correct_extended = any(t in top_k_tissues for t in true_hpa_extended)
  416. knn_atlas_results[f'knn_hpa_top{topk}_extended'].append(is_correct_extended)
  417. else:
  418. for topk in [1, 2, 3, 4, 5]:
  419. knn_atlas_results[f'knn_hpa_top{topk}_strict'].append(False)
  420. knn_atlas_results[f'knn_hpa_top{topk}_extended'].append(False)
  421. # PDB KNN - get both strict and extended ground truth
  422. true_pdb_strict = cancer_to_pdb.get(cohort)
  423. true_pdb_extended = cancer_to_pdb_extended.get(cohort, [])
  424. pred, _, class_proba = knn_classify_with_topk(sample_profile, pdb_train_X, pdb_train_y, k=1)
  425. knn_atlas_results['knn_pdb_pred'].append(pred)
  426. if class_proba:
  427. sorted_by_proba = sorted(class_proba.keys(), key=lambda x: class_proba[x], reverse=True)
  428. for topk in [1, 2, 3, 4, 5]:
  429. top_k_tissues = sorted_by_proba[:topk]
  430. is_correct_strict = true_pdb_strict in top_k_tissues if true_pdb_strict else False
  431. knn_atlas_results[f'knn_pdb_top{topk}_strict'].append(is_correct_strict)
  432. is_correct_extended = any(t in top_k_tissues for t in true_pdb_extended)
  433. knn_atlas_results[f'knn_pdb_top{topk}_extended'].append(is_correct_extended)
  434. else:
  435. for topk in [1, 2, 3, 4, 5]:
  436. knn_atlas_results[f'knn_pdb_top{topk}_strict'].append(False)
  437. knn_atlas_results[f'knn_pdb_top{topk}_extended'].append(False)
  438. knn_atlas_df = pd.DataFrame(knn_atlas_results)
  439. print(f"\nKNN Atlas results shape: {knn_atlas_df.shape}")
  440. # %%
  441. # For HPA and PDB: Use tissue profiles as "samples" for KNN
  442. # This treats each tissue as a single sample
  443. # HPA KNN (using tissue profiles)
  444. hpa_train_X = hpa_matrix.T # tissues × proteins
  445. hpa_train_y = pd.Series(hpa_train_X.index, index=hpa_train_X.index)
  446. # PDB KNN
  447. pdb_train_X = pdb_matrix.T # tissues × proteins
  448. pdb_train_y = pd.Series(pdb_train_X.index, index=pdb_train_X.index)
  449. print(f"HPA for KNN: {hpa_train_X.shape[0]} tissues (samples), {hpa_train_X.shape[1]} proteins (features)")
  450. print(f"PDB for KNN: {pdb_train_X.shape[0]} tissues (samples), {pdb_train_X.shape[1]} proteins (features)")
  451. # Run KNN on HPA and PDB with multiple k values
  452. k_values_atlas = [1, 3, 5, 7, 11, 15, 20]
  453. knn_atlas_results = {'sample': [], 'cohort': []}
  454. # Initialize columns for all k values and both strict/extended
  455. for k in k_values_atlas:
  456. knn_atlas_results[f'knn_hpa_k{k}_pred'] = []
  457. knn_atlas_results[f'knn_pdb_k{k}_pred'] = []
  458. for topk in [1, 2, 3, 4, 5]:
  459. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_strict'] = []
  460. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_extended'] = []
  461. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_strict'] = []
  462. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_extended'] = []
  463. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="KNN HPA/PDB"):
  464. sample_name = row['sample']
  465. cohort = row['Cohort']
  466. expr_col = row['expression_col']
  467. sample_profile = sample_expr_log[expr_col]
  468. sample_profile = sample_profile[sample_profile > 0]
  469. knn_atlas_results['sample'].append(sample_name)
  470. knn_atlas_results['cohort'].append(cohort)
  471. # Ground truth for HPA and PDB
  472. true_hpa_strict = cancer_to_hpa.get(cohort)
  473. true_hpa_extended = cancer_to_hpa_extended.get(cohort, [])
  474. true_pdb_strict = cancer_to_pdb.get(cohort)
  475. true_pdb_extended = cancer_to_pdb_extended.get(cohort, [])
  476. # Test different k values for HPA
  477. for k in k_values_atlas:
  478. pred_hpa, _, class_proba_hpa = knn_classify_with_topk(sample_profile, hpa_train_X, hpa_train_y, k=k)
  479. knn_atlas_results[f'knn_hpa_k{k}_pred'].append(pred_hpa)
  480. if class_proba_hpa:
  481. sorted_by_proba = sorted(class_proba_hpa.keys(), key=lambda x: class_proba_hpa[x], reverse=True)
  482. for topk in [1, 2, 3, 4, 5]:
  483. top_k_tissues = sorted_by_proba[:topk]
  484. is_correct_strict = true_hpa_strict in top_k_tissues if true_hpa_strict else False
  485. is_correct_extended = any(t in top_k_tissues for t in true_hpa_extended)
  486. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_strict'].append(is_correct_strict)
  487. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_extended'].append(is_correct_extended)
  488. else:
  489. for topk in [1, 2, 3, 4, 5]:
  490. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_strict'].append(False)
  491. knn_atlas_results[f'knn_hpa_k{k}_top{topk}_extended'].append(False)
  492. # Test different k values for PDB
  493. for k in k_values_atlas:
  494. pred_pdb, _, class_proba_pdb = knn_classify_with_topk(sample_profile, pdb_train_X, pdb_train_y, k=k)
  495. knn_atlas_results[f'knn_pdb_k{k}_pred'].append(pred_pdb)
  496. if class_proba_pdb:
  497. sorted_by_proba = sorted(class_proba_pdb.keys(), key=lambda x: class_proba_pdb[x], reverse=True)
  498. for topk in [1, 2, 3, 4, 5]:
  499. top_k_tissues = sorted_by_proba[:topk]
  500. is_correct_strict = true_pdb_strict in top_k_tissues if true_pdb_strict else False
  501. is_correct_extended = any(t in top_k_tissues for t in true_pdb_extended)
  502. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_strict'].append(is_correct_strict)
  503. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_extended'].append(is_correct_extended)
  504. else:
  505. for topk in [1, 2, 3, 4, 5]:
  506. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_strict'].append(False)
  507. knn_atlas_results[f'knn_pdb_k{k}_top{topk}_extended'].append(False)
  508. knn_atlas_df_multik = pd.DataFrame(knn_atlas_results)
  509. print(f"\nKNN Atlas results shape: {knn_atlas_df_multik.shape}")
  510. # %%
  511. # KNN Atlas: Top-1 Strict Accuracy vs k value
  512. fig, ax = plt.subplots(figsize=(10, 6))
  513. # HPA KNN - Top-1 Strict
  514. hpa_knn_strict_accs = [knn_atlas_df_multik[f'knn_hpa_k{k}_top5_extended'].mean() * 100 for k in k_values_atlas]
  515. ax.plot(k_values_atlas, hpa_knn_strict_accs, 'o-', color='#2A9D8F', linewidth=2.5,
  516. markersize=8, label='HPA (KNN)')
  517. # PDB KNN - Top-1 Strict
  518. pdb_knn_strict_accs = [knn_atlas_df_multik[f'knn_pdb_k{k}_top5_extended'].mean() * 100 for k in k_values_atlas]
  519. ax.plot(k_values_atlas, pdb_knn_strict_accs, 'D-', color='#F4A261', linewidth=2.5,
  520. markersize=8, label='PDB (KNN)')
  521. ax.set_xlabel('k (Number of Neighbors)', fontsize=12, fontweight='bold')
  522. ax.set_ylabel('Top-1 Strict Accuracy (%)', fontsize=12, fontweight='bold')
  523. ax.set_title('KNN Atlas Methods: Top-1 Strict Accuracy vs k', fontsize=14, fontweight='bold')
  524. ax.set_xticks(k_values_atlas)
  525. ax.set_ylim(0, 100)
  526. ax.grid(True, alpha=0.3)
  527. ax.legend(loc='best', fontsize=11)
  528. plt.tight_layout()
  529. # plt.savefig('knn_atlas_top5_extended_vs_k.png', dpi=300, bbox_inches='tight')
  530. # plt.savefig('knn_atlas_top5_extended_vs_k.svg', bbox_inches='tight')
  531. plt.show()
  532. print("KNN Atlas - Top-1 Strict Accuracy by k:")
  533. print("=" * 50)
  534. print(f"{'k':<5} {'HPA (%)':<12} {'PDB (%)':<12}")
  535. print("-" * 50)
  536. for k, hpa_acc, pdb_acc in zip(k_values_atlas, hpa_knn_strict_accs, pdb_knn_strict_accs):
  537. print(f"{k:<5} {hpa_acc:<11.1f} {pdb_acc:<11.1f}")
  538. print("-" * 50)
  539. # Spearman baselines computed in later cell (see cells 26-27)
  540. # %% [markdown]
  541. # ## 10. Figure 1: Top-k Accuracy Line Plot
  542. # %%
  543. # Color palette for methods - manuscript style
  544. # MLMarker: reddish, Training atlas: bluish, HPA: greenish, PDB: yellowish
  545. colors_spearman = {'MLMarker': '#E63946', 'Training Atlas': '#457B9D',
  546. 'HPA Atlas': '#2A9D8F', 'PDB Atlas': '#E9C46A'}
  547. # Extended colors for Spearman vs KNN comparison
  548. colors_all = {
  549. 'MLMarker': '#E63946',
  550. 'Training Atlas (Spearman)': '#457B9D',
  551. 'Training Atlas (KNN)': '#1D3557',
  552. 'HPA (Spearman)': '#2A9D8F',
  553. 'HPA (KNN)': '#264653',
  554. 'PDB (Spearman)': '#E9C46A',
  555. 'PDB (KNN)': '#F4A261'
  556. }
  557. # ============================================
  558. # Figure 1: Overall Top-k Accuracy - Spearman vs KNN Comparison
  559. # ============================================
  560. fig, axes = plt.subplots(1, 2, figsize=(14, 5))
  561. # Prepare data - need to recalculate with filtered cohorts
  562. # Filter results to only include eval_cohorts
  563. results_filtered = results_df[results_df['cohort'].isin(eval_cohorts)]
  564. knn_ml_filtered = knn_ml_df[knn_ml_df['cohort'].isin(eval_cohorts)]
  565. knn_atlas_filtered = knn_atlas_df[knn_atlas_df['cohort'].isin(eval_cohorts)]
  566. # Plot 1: Extended accuracy (Extended mapping - more meaningful)
  567. ax = axes[0]
  568. # MLMarker Model
  569. ml_topk_ext = [results_filtered[f'mlmarker_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  570. ax.plot([1,2,3,4,5], ml_topk_ext, 'o-', color=colors_all['MLMarker'],
  571. linewidth=2.5, markersize=8, label='MLMarker Model')
  572. # Training Atlas - Spearman
  573. ml_train_spear = [results_filtered[f'ml_training_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  574. ax.plot([1,2,3,4,5], ml_train_spear, 's-', color=colors_all['Training Atlas (Spearman)'],
  575. linewidth=2, markersize=7, label='Training Atlas (Spearman)')
  576. # Training Atlas - KNN k=5
  577. ml_train_knn = [knn_ml_filtered[f'knn_ml_k5_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  578. ax.plot([1,2,3,4,5], ml_train_knn, 's-', color=colors_all['Training Atlas (KNN)'],
  579. linewidth=2, markersize=7, label='Training Atlas (KNN)')
  580. # HPA - Spearman
  581. hpa_spear = [results_filtered[f'hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  582. ax.plot([1,2,3,4,5], hpa_spear, '^-', color=colors_all['HPA (Spearman)'],
  583. linewidth=2, markersize=7, label='HPA (Spearman)')
  584. # HPA - KNN
  585. hpa_knn = [knn_atlas_filtered[f'knn_hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  586. ax.plot([1,2,3,4,5], hpa_knn, '^-', color=colors_all['HPA (KNN)'],
  587. linewidth=2, markersize=7, label='HPA (KNN)')
  588. # PDB - Spearman
  589. pdb_spear = [results_filtered[f'pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  590. ax.plot([1,2,3,4,5], pdb_spear, 'D-', color=colors_all['PDB (Spearman)'],
  591. linewidth=2, markersize=7, label='PDB (Spearman)')
  592. # PDB - KNN
  593. pdb_knn = [knn_atlas_filtered[f'knn_pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  594. ax.plot([1,2,3,4,5], pdb_knn, 'D-', color=colors_all['PDB (KNN)'],
  595. linewidth=2, markersize=7, label='PDB (KNN)')
  596. ax.set_xlabel('k (Top-k)', fontsize=12)
  597. ax.set_ylabel('Extended Accuracy (%)', fontsize=12)
  598. ax.set_title('A) Overall Top-k Accuracy', fontsize=12, fontweight='bold')
  599. ax.set_xticks([1, 2, 3, 4, 5])
  600. ax.set_ylim(0, 100)
  601. ax.grid(True, alpha=0.3)
  602. # Plot 2: Top-1 accuracy bar comparison
  603. ax = axes[1]
  604. methods_bar = ['MLMarker', 'Training Atlas\n(Spearman)', 'Training Atlas\n(KNN)',
  605. 'HPA\n(Spearman)', 'HPA\n(KNN)', 'PDB\n(Spearman)', 'PDB\n(KNN)']
  606. accs_bar = [ml_topk_ext[0], ml_train_spear[0], ml_train_knn[0],
  607. hpa_spear[0], hpa_knn[0], pdb_spear[0], pdb_knn[0]]
  608. bar_colors = [colors_all['MLMarker'], colors_all['Training Atlas (Spearman)'], colors_all['Training Atlas (KNN)'],
  609. colors_all['HPA (Spearman)'], colors_all['HPA (KNN)'],
  610. colors_all['PDB (Spearman)'], colors_all['PDB (KNN)']]
  611. bars = ax.bar(methods_bar, accs_bar, color=bar_colors, edgecolor='black', linewidth=0.5)
  612. for bar in bars:
  613. height = bar.get_height()
  614. ax.annotate(f'{height:.1f}%', xy=(bar.get_x() + bar.get_width()/2, height),
  615. xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=9)
  616. ax.set_ylabel('Top-1 Extended Accuracy (%)', fontsize=12)
  617. ax.set_title('B) Top-1 Accuracy Comparison', fontsize=12, fontweight='bold')
  618. ax.set_ylim(-5, 100) # Start below 0 to see all bars clearly
  619. ax.tick_params(axis='x', rotation=45)
  620. ax.grid(True, alpha=0.3, axis='y')
  621. # Shared legend
  622. handles, labels = axes[0].get_legend_handles_labels()
  623. fig.legend(handles, labels, loc='center right', bbox_to_anchor=(1.18, 0.5), fontsize=9)
  624. plt.tight_layout()
  625. plt.savefig('baseline_spearman_vs_knn_overall.png', dpi=300, bbox_inches='tight')
  626. plt.savefig('baseline_spearman_vs_knn_overall.svg', bbox_inches='tight')
  627. plt.show()
  628. print("Saved: baseline_spearman_vs_knn_overall.png/svg")
  629. # ============================================
  630. # Figure 2: Per-Cohort Top-k Accuracy - Spearman vs KNN
  631. # ============================================
  632. fig, axes = plt.subplots(2, len(eval_cohorts), figsize=(18, 8), sharey=True)
  633. for col_idx, cohort in enumerate(eval_cohorts):
  634. # Filter data for this cohort
  635. cohort_results = results_filtered[results_filtered['cohort'] == cohort]
  636. cohort_knn_ml = knn_ml_filtered[knn_ml_filtered['cohort'] == cohort]
  637. cohort_knn_atlas = knn_atlas_filtered[knn_atlas_filtered['cohort'] == cohort]
  638. n_samples = len(cohort_results)
  639. # Row 1: Spearman methods
  640. ax = axes[0, col_idx]
  641. # MLMarker Model
  642. ml_topk = [cohort_results[f'mlmarker_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  643. ax.plot([1,2,3,4,5], ml_topk, 'o-', color=colors_all['MLMarker'], linewidth=2, markersize=6, label='MLMarker')
  644. # Training Atlas Spearman
  645. ml_train = [cohort_results[f'ml_training_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  646. ax.plot([1,2,3,4,5], ml_train, 's-', color=colors_all['Training Atlas (Spearman)'], linewidth=1.5, markersize=5)
  647. # HPA Spearman
  648. hpa = [cohort_results[f'hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  649. ax.plot([1,2,3,4,5], hpa, '^-', color=colors_all['HPA (Spearman)'], linewidth=1.5, markersize=5)
  650. # PDB Spearman
  651. pdb = [cohort_results[f'pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  652. ax.plot([1,2,3,4,5], pdb, 'D-', color=colors_all['PDB (Spearman)'], linewidth=1.5, markersize=5)
  653. ax.set_title(f'{cohort}\n(n={n_samples})', fontsize=10, fontweight='bold')
  654. ax.set_xticks([1, 2, 3, 4, 5])
  655. ax.set_ylim(-5, 105) # Start below 0
  656. ax.grid(True, alpha=0.3)
  657. if col_idx == 0:
  658. ax.set_ylabel('Spearman (%)', fontsize=10)
  659. # Row 2: KNN methods
  660. ax = axes[1, col_idx]
  661. # MLMarker Model (same as above for reference)
  662. ax.plot([1,2,3,4,5], ml_topk, 'o-', color=colors_all['MLMarker'], linewidth=2, markersize=6)
  663. # Training Atlas KNN
  664. ml_knn = [cohort_knn_ml[f'knn_ml_k5_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  665. ax.plot([1,2,3,4,5], ml_knn, 's-', color=colors_all['Training Atlas (KNN)'], linewidth=1.5, markersize=5)
  666. # HPA KNN
  667. hpa_k = [cohort_knn_atlas[f'knn_hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  668. ax.plot([1,2,3,4,5], hpa_k, '^-', color=colors_all['HPA (KNN)'], linewidth=1.5, markersize=5)
  669. # PDB KNN
  670. pdb_k = [cohort_knn_atlas[f'knn_pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  671. ax.plot([1,2,3,4,5], pdb_k, 'D-', color=colors_all['PDB (KNN)'], linewidth=1.5, markersize=5)
  672. ax.set_xlabel('k', fontsize=10)
  673. ax.set_xticks([1, 2, 3, 4, 5])
  674. ax.set_ylim(-5, 105) # Start below 0
  675. ax.grid(True, alpha=0.3)
  676. if col_idx == 0:
  677. ax.set_ylabel('KNN (%)', fontsize=10)
  678. # Row labels
  679. axes[0, 0].annotate('Spearman\nCorrelation', xy=(-0.45, 0.5), xycoords='axes fraction',
  680. fontsize=11, fontweight='bold', ha='center', va='center', rotation=90)
  681. axes[1, 0].annotate('KNN\nClassification', xy=(-0.45, 0.5), xycoords='axes fraction',
  682. fontsize=11, fontweight='bold', ha='center', va='center', rotation=90)
  683. # Legend
  684. legend_labels = ['MLMarker', 'Training Atlas', 'HPA Atlas', 'PDB Atlas']
  685. legend_colors = [colors_all['MLMarker'], colors_all['Training Atlas (Spearman)'],
  686. colors_all['HPA (Spearman)'], colors_all['PDB (Spearman)']]
  687. handles = [plt.Line2D([0], [0], color=c, linewidth=2, marker='o', markersize=6) for c in legend_colors]
  688. fig.legend(handles, legend_labels, loc='lower center', bbox_to_anchor=(0.5, -0.02), ncol=4, fontsize=11)
  689. plt.suptitle('Top-k Accuracy by Cohort: Spearman vs KNN', fontsize=14, fontweight='bold', y=1.02)
  690. plt.tight_layout()
  691. # plt.savefig('baseline_spearman_vs_knn_per_cohort.png', dpi=300, bbox_inches='tight')
  692. # plt.savefig('baseline_spearman_vs_knn_per_cohort.svg', bbox_inches='tight')
  693. plt.show()
  694. # print("Saved: baseline_spearman_vs_knn_per_cohort.png/svg")
  695. # %%
  696. # ============================================
  697. # STRICT (1-to-1) MAPPING VERSION
  698. # Same figures as above but using strict mapping instead of extended
  699. # ============================================
  700. # Color palette for methods - manuscript style
  701. colors_all = {
  702. 'MLMarker': '#E63946',
  703. 'Training Atlas (Spearman)': '#457B9D',
  704. 'Training Atlas (KNN)': '#1D3557',
  705. 'HPA (Spearman)': '#2A9D8F',
  706. 'HPA (KNN)': '#264653',
  707. 'PDB (Spearman)': '#E9C46A',
  708. 'PDB (KNN)': '#F4A261'
  709. }
  710. # ============================================
  711. # Figure 1: Overall Top-k Accuracy - Spearman vs KNN (STRICT 1-to-1)
  712. # ============================================
  713. fig, axes = plt.subplots(1, 2, figsize=(14, 5))
  714. # Prepare data - need to recalculate with filtered cohorts
  715. results_filtered = results_df[results_df['cohort'].isin(eval_cohorts)]
  716. knn_ml_filtered = knn_ml_df[knn_ml_df['cohort'].isin(eval_cohorts)]
  717. knn_atlas_filtered = knn_atlas_df[knn_atlas_df['cohort'].isin(eval_cohorts)]
  718. # Plot 1: Strict accuracy (1-to-1 mapping)
  719. ax = axes[0]
  720. # MLMarker Model - STRICT
  721. ml_topk_strict = [results_filtered[f'mlmarker_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  722. ax.plot([1,2,3,4,5], ml_topk_strict, 'o-', color=colors_all['MLMarker'],
  723. linewidth=2.5, markersize=8, label='MLMarker Model')
  724. # Training Atlas - Spearman STRICT
  725. ml_train_spear_strict = [results_filtered[f'ml_training_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  726. ax.plot([1,2,3,4,5], ml_train_spear_strict, 's-', color=colors_all['Training Atlas (Spearman)'],
  727. linewidth=2, markersize=7, label='Training Atlas (Spearman)')
  728. # Training Atlas - KNN k=5 STRICT
  729. ml_train_knn_strict = [knn_ml_filtered[f'knn_ml_k5_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  730. ax.plot([1,2,3,4,5], ml_train_knn_strict, 's-', color=colors_all['Training Atlas (KNN)'],
  731. linewidth=2, markersize=7, label='Training Atlas (KNN)')
  732. # HPA - Spearman STRICT
  733. hpa_spear_strict = [results_filtered[f'hpa_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  734. ax.plot([1,2,3,4,5], hpa_spear_strict, '^-', color=colors_all['HPA (Spearman)'],
  735. linewidth=2, markersize=7, label='HPA (Spearman)')
  736. # HPA - KNN STRICT
  737. hpa_knn_strict = [knn_atlas_filtered[f'knn_hpa_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  738. ax.plot([1,2,3,4,5], hpa_knn_strict, '^-', color=colors_all['HPA (KNN)'],
  739. linewidth=2, markersize=7, label='HPA (KNN)')
  740. # PDB - Spearman STRICT
  741. pdb_spear_strict = [results_filtered[f'pdb_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  742. ax.plot([1,2,3,4,5], pdb_spear_strict, 'D-', color=colors_all['PDB (Spearman)'],
  743. linewidth=2, markersize=7, label='PDB (Spearman)')
  744. # PDB - KNN STRICT
  745. pdb_knn_strict = [knn_atlas_filtered[f'knn_pdb_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  746. ax.plot([1,2,3,4,5], pdb_knn_strict, 'D-', color=colors_all['PDB (KNN)'],
  747. linewidth=2, markersize=7, label='PDB (KNN)')
  748. ax.set_xlabel('k (Top-k)', fontsize=12)
  749. ax.set_ylabel('Strict Accuracy (%)', fontsize=12)
  750. ax.set_title('A) Overall Top-k Accuracy (1-to-1 Mapping)', fontsize=12, fontweight='bold')
  751. ax.set_xticks([1, 2, 3, 4, 5])
  752. ax.set_ylim(0, 100)
  753. ax.grid(True, alpha=0.3)
  754. # Plot 2: Top-1 accuracy bar comparison (STRICT)
  755. ax = axes[1]
  756. methods_bar = ['MLMarker', 'Training Atlas\n(Spearman)', 'Training Atlas\n(KNN)',
  757. 'HPA\n(Spearman)', 'HPA\n(KNN)', 'PDB\n(Spearman)', 'PDB\n(KNN)']
  758. accs_bar_strict = [ml_topk_strict[0], ml_train_spear_strict[0], ml_train_knn_strict[0],
  759. hpa_spear_strict[0], hpa_knn_strict[0], pdb_spear_strict[0], pdb_knn_strict[0]]
  760. bar_colors = [colors_all['MLMarker'], colors_all['Training Atlas (Spearman)'], colors_all['Training Atlas (KNN)'],
  761. colors_all['HPA (Spearman)'], colors_all['HPA (KNN)'],
  762. colors_all['PDB (Spearman)'], colors_all['PDB (KNN)']]
  763. bars = ax.bar(methods_bar, accs_bar_strict, color=bar_colors, edgecolor='black', linewidth=0.5)
  764. for bar in bars:
  765. height = bar.get_height()
  766. ax.annotate(f'{height:.1f}%', xy=(bar.get_x() + bar.get_width()/2, height),
  767. xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=9)
  768. ax.set_ylabel('Top-1 Strict Accuracy (%)', fontsize=12)
  769. ax.set_title('B) Top-1 Accuracy Comparison (1-to-1 Mapping)', fontsize=12, fontweight='bold')
  770. ax.set_ylim(-5, 100)
  771. ax.tick_params(axis='x', rotation=45)
  772. ax.grid(True, alpha=0.3, axis='y')
  773. # Shared legend
  774. handles, labels = axes[0].get_legend_handles_labels()
  775. fig.legend(handles, labels, loc='center right', bbox_to_anchor=(1.18, 0.5), fontsize=9)
  776. plt.tight_layout()
  777. plt.savefig('baseline_spearman_vs_knn_overall_strict.png', dpi=300, bbox_inches='tight')
  778. plt.savefig('baseline_spearman_vs_knn_overall_strict.svg', bbox_inches='tight')
  779. plt.show()
  780. print("Saved: baseline_spearman_vs_knn_overall_strict.png/svg")
  781. # ============================================
  782. # Figure 2: Per-Cohort Top-k Accuracy - Spearman vs KNN (STRICT 1-to-1)
  783. # ============================================
  784. fig, axes = plt.subplots(2, len(eval_cohorts), figsize=(18, 8), sharey=True)
  785. for col_idx, cohort in enumerate(eval_cohorts):
  786. # Filter data for this cohort
  787. cohort_results = results_filtered[results_filtered['cohort'] == cohort]
  788. cohort_knn_ml = knn_ml_filtered[knn_ml_filtered['cohort'] == cohort]
  789. cohort_knn_atlas = knn_atlas_filtered[knn_atlas_filtered['cohort'] == cohort]
  790. n_samples = len(cohort_results)
  791. # Row 1: Spearman methods (STRICT)
  792. ax = axes[0, col_idx]
  793. # MLMarker Model
  794. ml_topk = [cohort_results[f'mlmarker_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  795. ax.plot([1,2,3,4,5], ml_topk, 'o-', color=colors_all['MLMarker'], linewidth=2, markersize=6, label='MLMarker')
  796. # Training Atlas Spearman
  797. ml_train = [cohort_results[f'ml_training_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  798. ax.plot([1,2,3,4,5], ml_train, 's-', color=colors_all['Training Atlas (Spearman)'], linewidth=1.5, markersize=5)
  799. # HPA Spearman
  800. hpa = [cohort_results[f'hpa_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  801. ax.plot([1,2,3,4,5], hpa, '^-', color=colors_all['HPA (Spearman)'], linewidth=1.5, markersize=5)
  802. # PDB Spearman
  803. pdb = [cohort_results[f'pdb_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  804. ax.plot([1,2,3,4,5], pdb, 'D-', color=colors_all['PDB (Spearman)'], linewidth=1.5, markersize=5)
  805. ax.set_title(f'{cohort}\n(n={n_samples})', fontsize=10, fontweight='bold')
  806. ax.set_xticks([1, 2, 3, 4, 5])
  807. ax.set_ylim(-5, 105)
  808. ax.grid(True, alpha=0.3)
  809. if col_idx == 0:
  810. ax.set_ylabel('Spearman (%)', fontsize=10)
  811. # Row 2: KNN methods (STRICT)
  812. ax = axes[1, col_idx]
  813. # MLMarker Model (same as above for reference)
  814. ax.plot([1,2,3,4,5], ml_topk, 'o-', color=colors_all['MLMarker'], linewidth=2, markersize=6)
  815. # Training Atlas KNN
  816. ml_knn = [cohort_knn_ml[f'knn_ml_k5_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  817. ax.plot([1,2,3,4,5], ml_knn, 's-', color=colors_all['Training Atlas (KNN)'], linewidth=1.5, markersize=5)
  818. # HPA KNN
  819. hpa_k = [cohort_knn_atlas[f'knn_hpa_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  820. ax.plot([1,2,3,4,5], hpa_k, '^-', color=colors_all['HPA (KNN)'], linewidth=1.5, markersize=5)
  821. # PDB KNN
  822. pdb_k = [cohort_knn_atlas[f'knn_pdb_top{k}_strict'].mean()*100 for k in [1,2,3,4,5]]
  823. ax.plot([1,2,3,4,5], pdb_k, 'D-', color=colors_all['PDB (KNN)'], linewidth=1.5, markersize=5)
  824. ax.set_xlabel('k', fontsize=10)
  825. ax.set_xticks([1, 2, 3, 4, 5])
  826. ax.set_ylim(-5, 105)
  827. ax.grid(True, alpha=0.3)
  828. if col_idx == 0:
  829. ax.set_ylabel('KNN (%)', fontsize=10)
  830. # Row labels
  831. axes[0, 0].annotate('Spearman\nCorrelation', xy=(-0.45, 0.5), xycoords='axes fraction',
  832. fontsize=11, fontweight='bold', ha='center', va='center', rotation=90)
  833. axes[1, 0].annotate('KNN\nClassification', xy=(-0.45, 0.5), xycoords='axes fraction',
  834. fontsize=11, fontweight='bold', ha='center', va='center', rotation=90)
  835. # Legend
  836. legend_labels = ['MLMarker', 'Training Atlas', 'HPA Atlas', 'PDB Atlas']
  837. legend_colors = [colors_all['MLMarker'], colors_all['Training Atlas (Spearman)'],
  838. colors_all['HPA (Spearman)'], colors_all['PDB (Spearman)']]
  839. handles = [plt.Line2D([0], [0], color=c, linewidth=2, marker='o', markersize=6) for c in legend_colors]
  840. fig.legend(handles, legend_labels, loc='lower center', bbox_to_anchor=(0.5, -0.02), ncol=4, fontsize=11)
  841. plt.suptitle('Top-k Accuracy by Cohort: Spearman vs KNN (1-to-1 Mapping)', fontsize=14, fontweight='bold', y=1.02)
  842. plt.tight_layout()
  843. # plt.savefig('baseline_spearman_vs_knn_per_cohort_strict.png', dpi=300, bbox_inches='tight')
  844. # plt.savefig('baseline_spearman_vs_knn_per_cohort_strict.svg', bbox_inches='tight')
  845. plt.show()
  846. # print("Saved: baseline_spearman_vs_knn_per_cohort_strict.png/svg")
  847. # %%
  848. # ============================================
  849. # FIGURE 1: Grouped Bar Chart - Top-1 Accuracy (Extended vs Strict)
  850. # ============================================
  851. import matplotlib.gridspec as gridspec
  852. import seaborn as sns
  853. from matplotlib.lines import Line2D
  854. from matplotlib.patches import Patch
  855. # Color palette - manuscript colors
  856. colors_atlas = {
  857. 'MLMarker': '#E63946',
  858. 'Training Atlas': '#457B9D',
  859. 'HPA': '#2A9D8F',
  860. 'PDB': '#E9C46A'
  861. }
  862. # Filter data
  863. results_filtered = results_df[results_df['cohort'].isin(eval_cohorts)]
  864. knn_ml_filtered = knn_ml_df[knn_ml_df['cohort'].isin(eval_cohorts)]
  865. knn_atlas_filtered = knn_atlas_df[knn_atlas_df['cohort'].isin(eval_cohorts)]
  866. # Calculate Top-1 accuracies for all methods (overall averages)
  867. # Spearman methods
  868. ml_ext = results_filtered['mlmarker_top1_extended'].mean() * 100
  869. ml_strict = results_filtered['mlmarker_top1_strict'].mean() * 100
  870. train_spear_ext = results_filtered['ml_training_top1_extended'].mean() * 100
  871. train_spear_strict = results_filtered['ml_training_top1_strict'].mean() * 100
  872. hpa_spear_ext = results_filtered['hpa_top1_extended'].mean() * 100
  873. hpa_spear_strict = results_filtered['hpa_top1_strict'].mean() * 100
  874. pdb_spear_ext = results_filtered['pdb_top1_extended'].mean() * 100
  875. pdb_spear_strict = results_filtered['pdb_top1_strict'].mean() * 100
  876. # KNN methods
  877. train_knn_ext = knn_ml_filtered['knn_ml_k5_top1_extended'].mean() * 100
  878. train_knn_strict = knn_ml_filtered['knn_ml_k5_top1_strict'].mean() * 100
  879. hpa_knn_ext = knn_atlas_filtered['knn_hpa_top1_extended'].mean() * 100
  880. hpa_knn_strict = knn_atlas_filtered['knn_hpa_top1_strict'].mean() * 100
  881. pdb_knn_ext = knn_atlas_filtered['knn_pdb_top1_extended'].mean() * 100
  882. pdb_knn_strict = knn_atlas_filtered['knn_pdb_top1_strict'].mean() * 100
  883. # Method labels and data
  884. method_labels_bar = ['MLMarker', 'Training\n(Spearman)', 'Training\n(KNN)', 'HPA\n(Spearman)', 'HPA\n(KNN)', 'PDB\n(Spearman)', 'PDB\n(KNN)']
  885. ext_values = [ml_ext, train_spear_ext, train_knn_ext, hpa_spear_ext, hpa_knn_ext, pdb_spear_ext, pdb_knn_ext]
  886. strict_values = [ml_strict, train_spear_strict, train_knn_strict, hpa_spear_strict, hpa_knn_strict, pdb_spear_strict, pdb_knn_strict]
  887. bar_colors = [colors_atlas['MLMarker'], colors_atlas['Training Atlas'], colors_atlas['Training Atlas'],
  888. colors_atlas['HPA'], colors_atlas['HPA'], colors_atlas['PDB'], colors_atlas['PDB']]
  889. fig, ax = plt.subplots(figsize=(10, 5))
  890. x = np.arange(len(method_labels_bar))
  891. width = 0.35
  892. bars_ext = ax.bar(x - width/2, ext_values, width, label='Extended (1-to-many)', color=bar_colors, alpha=1.0, edgecolor='black', linewidth=1)
  893. bars_strict = ax.bar(x + width/2, strict_values, width, label='Strict (1-to-1)', color=bar_colors, alpha=0.5, edgecolor='black', linewidth=1, hatch='//')
  894. ax.set_ylabel('Top-1 Accuracy (%)', fontsize=12, fontweight='bold')
  895. ax.set_title('Top-1 Accuracy Comparison: Extended vs Strict Mapping', fontsize=13, fontweight='bold')
  896. ax.set_xticks(x)
  897. ax.set_xticklabels(method_labels_bar, fontsize=10)
  898. ax.set_ylim(0, 105)
  899. ax.grid(True, alpha=0.3, axis='y')
  900. # Add value labels on bars
  901. for bar in bars_ext:
  902. height = bar.get_height()
  903. ax.annotate(f'{height:.1f}', xy=(bar.get_x() + bar.get_width()/2, height),
  904. xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)
  905. for bar in bars_strict:
  906. height = bar.get_height()
  907. ax.annotate(f'{height:.1f}', xy=(bar.get_x() + bar.get_width()/2, height),
  908. xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)
  909. # Custom legend for bar chart
  910. legend_elements = [Patch(facecolor='gray', edgecolor='black', alpha=1.0, label='Extended (1-to-many)'),
  911. Patch(facecolor='gray', edgecolor='black', alpha=0.5, hatch='//', label='Strict (1-to-1)')]
  912. ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
  913. plt.tight_layout()
  914. # plt.savefig('../Figures_paper/fig11a.png', dpi=300, bbox_inches='tight')
  915. # plt.savefig('baseline_barplot_extended_vs_strict.svg', bbox_inches='tight')
  916. plt.show()
  917. # print("Saved: ../Figures_paper/fig11a.png/svg")
  918. # %%
  919. # ============================================
  920. # FIGURE 2: Per-Cohort Line Plots (Extended and Strict)
  921. # ============================================
  922. fig, axes = plt.subplots(2, len(eval_cohorts), figsize=(16, 7), sharey=True)
  923. row_labels = ['Strict (1-to-1)', 'Extended (1-to-many)']
  924. mapping_types = ['strict', 'extended']
  925. for row_idx, mapping_type in enumerate(mapping_types):
  926. for col_idx, cohort in enumerate(eval_cohorts):
  927. ax = axes[row_idx, col_idx]
  928. cohort_results = results_filtered[results_filtered['cohort'] == cohort]
  929. cohort_knn_ml = knn_ml_filtered[knn_ml_filtered['cohort'] == cohort]
  930. cohort_knn_atlas = knn_atlas_filtered[knn_atlas_filtered['cohort'] == cohort]
  931. n_samples = len(cohort_results)
  932. # MLMarker (only model - solid line)
  933. ml_topk = [cohort_results[f'mlmarker_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  934. ax.plot([1,2,3,4,5], ml_topk, 'o-', color=colors_atlas['MLMarker'], linewidth=2, markersize=5, label='MLMarker')
  935. # Training Atlas - Spearman (solid) and KNN (dashed)
  936. ml_train_spear = [cohort_results[f'ml_training_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  937. ml_train_knn = [cohort_knn_ml[f'knn_ml_k5_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  938. ax.plot([1,2,3,4,5], ml_train_spear, 's-', color=colors_atlas['Training Atlas'], linewidth=1.5, markersize=4, label='Training (Spearman)')
  939. ax.plot([1,2,3,4,5], ml_train_knn, 's--', color=colors_atlas['Training Atlas'], linewidth=1.5, markersize=4, label='Training (KNN)')
  940. # HPA - Spearman (solid) and KNN (dashed)
  941. hpa_spear = [cohort_results[f'hpa_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  942. hpa_knn = [cohort_knn_atlas[f'knn_hpa_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  943. ax.plot([1,2,3,4,5], hpa_spear, '^-', color=colors_atlas['HPA'], linewidth=1.5, markersize=4, label='HPA (Spearman)')
  944. ax.plot([1,2,3,4,5], hpa_knn, '^--', color=colors_atlas['HPA'], linewidth=1.5, markersize=4, label='HPA (KNN)')
  945. # PDB - Spearman (solid) and KNN (dashed)
  946. pdb_spear = [cohort_results[f'pdb_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  947. pdb_knn = [cohort_knn_atlas[f'knn_pdb_top{k}_{mapping_type}'].mean()*100 for k in [1,2,3,4,5]]
  948. ax.plot([1,2,3,4,5], pdb_spear, 'D-', color=colors_atlas['PDB'], linewidth=1.5, markersize=4, label='PDB (Spearman)')
  949. ax.plot([1,2,3,4,5], pdb_knn, 'D--', color=colors_atlas['PDB'], linewidth=1.5, markersize=4, label='PDB (KNN)')
  950. ax.set_xticks([1, 2, 3, 4, 5])
  951. ax.set_ylim(-5, 105)
  952. ax.grid(True, alpha=0.3)
  953. # Column titles (cohort names) only on first row
  954. if row_idx == 0:
  955. ax.set_title(f'{cohort}\n(n={n_samples})', fontsize=11, fontweight='bold')
  956. # Y-axis label only on first column
  957. if col_idx == 0:
  958. ax.set_ylabel(f'{row_labels[row_idx]}\nAccuracy (%)', fontsize=10, fontweight='bold')
  959. # X-axis label only on last row
  960. if row_idx == 1:
  961. ax.set_xlabel('k (Top-k)', fontsize=10)
  962. # Create legend outside the plots
  963. style_handles = [
  964. Line2D([0], [0], color='gray', linestyle='-', linewidth=2, label='Spearman'),
  965. Line2D([0], [0], color='gray', linestyle='--', linewidth=2, label='KNN'),
  966. Line2D([0], [0], marker='o', color='w', markerfacecolor=colors_atlas['MLMarker'], markersize=10, label='MLMarker'),
  967. Line2D([0], [0], marker='s', color='w', markerfacecolor=colors_atlas['Training Atlas'], markersize=10, label='Training Atlas'),
  968. Line2D([0], [0], marker='^', color='w', markerfacecolor=colors_atlas['HPA'], markersize=10, label='HPA'),
  969. Line2D([0], [0], marker='D', color='w', markerfacecolor=colors_atlas['PDB'], markersize=10, label='PDB'),
  970. ]
  971. fig.legend(handles=style_handles, loc='center right', bbox_to_anchor=(1.12, 0.5), fontsize=10, frameon=True, title='Method')
  972. plt.suptitle('Top-k Accuracy by Cohort', fontsize=14, fontweight='bold', y=1.02)
  973. plt.tight_layout()
  974. # plt.savefig('../Figures_paper/fig11c.png', dpi=300, bbox_inches='tight')
  975. # plt.savefig('baseline_topk_per_cohort.svg', bbox_inches='tight')
  976. plt.show()
  977. print("Saved: ../Figures_paper/fig11c.png/svg")
  978. # %%
  979. # ============================================
  980. # FIGURE 3: Heatmap - Top-1 Extended Accuracy by Cohort and Method
  981. # ============================================
  982. # Prepare heatmap data
  983. heatmap_methods = ['MLMarker', 'Training (Spearman)', 'Training (KNN)', 'HPA (Spearman)', 'HPA (KNN)', 'PDB (Spearman)', 'PDB (KNN)']
  984. heatmap_data = []
  985. for cohort in eval_cohorts:
  986. cohort_results = results_filtered[results_filtered['cohort'] == cohort]
  987. cohort_knn_ml = knn_ml_filtered[knn_ml_filtered['cohort'] == cohort]
  988. cohort_knn_atlas = knn_atlas_filtered[knn_atlas_filtered['cohort'] == cohort]
  989. row = [
  990. cohort_results['mlmarker_top1_extended'].mean() * 100,
  991. cohort_results['ml_training_top1_extended'].mean() * 100,
  992. cohort_knn_ml['knn_ml_k5_top1_extended'].mean() * 100,
  993. cohort_results['hpa_top1_extended'].mean() * 100,
  994. cohort_knn_atlas['knn_hpa_top1_extended'].mean() * 100,
  995. cohort_results['pdb_top1_extended'].mean() * 100,
  996. cohort_knn_atlas['knn_pdb_top1_extended'].mean() * 100,
  997. ]
  998. heatmap_data.append(row)
  999. heatmap_df = pd.DataFrame(heatmap_data, index=eval_cohorts, columns=heatmap_methods)
  1000. # Colorblind-friendly colormap
  1001. cmap = sns.color_palette('YlGn', as_cmap=True)
  1002. fig, ax = plt.subplots(figsize=(10, 5))
  1003. sns.heatmap(heatmap_df, annot=True, fmt='.1f', cmap=cmap,
  1004. vmin=0, vmax=100, ax=ax, cbar_kws={'label': 'Accuracy (%)', 'shrink': 0.8},
  1005. linewidths=0.5, linecolor='white', annot_kws={'fontsize': 11})
  1006. ax.set_title('Top-1 Extended Accuracy by Cohort and Method', fontsize=13, fontweight='bold')
  1007. ax.set_xlabel('Method', fontsize=12)
  1008. ax.set_ylabel('Cohort', fontsize=12)
  1009. ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha='right', fontsize=10)
  1010. ax.set_yticklabels(ax.get_yticklabels(), rotation=0, fontsize=10)
  1011. plt.tight_layout()
  1012. # plt.savefig('../Figures_paper/fig11b.png', dpi=300, bbox_inches='tight')
  1013. # plt.savefig('baseline_heatmap_extended.svg', bbox_inches='tight')
  1014. plt.show()
  1015. print("Saved: ../Figures_paper/fig11b.png/svg")
  1016. # %%
  1017. # %% [markdown]
  1018. # ## 11. Figure 2: Per-Cohort Bar Plot (Top-1 Extended)
  1019. # %%
  1020. # Filter to Top-1 Extended
  1021. bar_data = cohort_df[(cohort_df['k'] == 1)].copy()
  1022. fig, ax = plt.subplots(figsize=(14, 6))
  1023. x = np.arange(len(eval_cohorts))
  1024. width = 0.2
  1025. for i, method in enumerate(method_labels):
  1026. method_data = bar_data[bar_data['Method'] == method].set_index('Cohort').loc[eval_cohorts]
  1027. bars = ax.bar(x + i*width, method_data['Extended (%)'], width,
  1028. label=method, color=colors[method], edgecolor='black', linewidth=0.5)
  1029. # Add value labels
  1030. for bar in bars:
  1031. height = bar.get_height()
  1032. ax.annotate(f'{height:.0f}', xy=(bar.get_x() + bar.get_width()/2, height),
  1033. xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=8)
  1034. ax.set_xlabel('Cancer Cohort', fontsize=12)
  1035. ax.set_ylabel('Top-1 Extended Accuracy (%)', fontsize=12)
  1036. ax.set_title('Classification Accuracy by Cohort (Extended Mapping)', fontsize=14, fontweight='bold')
  1037. ax.set_xticks(x + width * 1.5)
  1038. ax.set_xticklabels(eval_cohorts, rotation=45, ha='right')
  1039. ax.set_ylim(0, 115)
  1040. ax.legend(loc='upper right')
  1041. ax.grid(True, alpha=0.3, axis='y')
  1042. plt.tight_layout()
  1043. plt.savefig('baseline_cohort_barplot.png', dpi=300, bbox_inches='tight')
  1044. plt.savefig('baseline_cohort_barplot.svg', bbox_inches='tight')
  1045. plt.show()
  1046. print("Saved: baseline_cohort_barplot.png/svg")
  1047. # %% [markdown]
  1048. # ## 12. Figure 3: Per-Cohort Top-k Line Plots
  1049. # %%
  1050. # Calculate full k range per cohort
  1051. cohort_topk_data = []
  1052. for cohort in eval_cohorts:
  1053. cohort_mask = results_df['cohort'] == cohort
  1054. for method, label in zip(methods, method_labels):
  1055. for k in [1, 2, 3, 4, 5]:
  1056. extended_acc = results_df.loc[cohort_mask, f'{method}_top{k}_extended'].mean() * 100
  1057. cohort_topk_data.append({'Cohort': cohort, 'Method': label, 'k': k, 'Extended (%)': extended_acc})
  1058. cohort_topk_df = pd.DataFrame(cohort_topk_data)
  1059. # Create subplot grid
  1060. fig, axes = plt.subplots(2, 4, figsize=(16, 8))
  1061. axes = axes.flatten()
  1062. for idx, cohort in enumerate(eval_cohorts):
  1063. ax = axes[idx]
  1064. cohort_data = cohort_topk_df[cohort_topk_df['Cohort'] == cohort]
  1065. for method in method_labels:
  1066. method_data = cohort_data[cohort_data['Method'] == method]
  1067. ax.plot(method_data['k'], method_data['Extended (%)'], 'o-',
  1068. label=method, color=colors[method], linewidth=2, markersize=6)
  1069. n_samples = (results_df['cohort'] == cohort).sum()
  1070. ax.set_title(f'{cohort} (n={n_samples})', fontsize=11, fontweight='bold')
  1071. ax.set_xlabel('k')
  1072. ax.set_ylabel('Accuracy (%)')
  1073. ax.set_xticks([1, 2, 3, 4, 5])
  1074. ax.set_ylim(0, 105)
  1075. ax.grid(True, alpha=0.3)
  1076. # Hide empty subplot
  1077. axes[-1].axis('off')
  1078. # Add shared legend
  1079. handles, labels = axes[0].get_legend_handles_labels()
  1080. fig.legend(handles, labels, loc='lower right', bbox_to_anchor=(0.98, 0.12), fontsize=10)
  1081. plt.suptitle('Top-k Accuracy by Cohort (Extended Mapping)', fontsize=14, fontweight='bold', y=1.02)
  1082. plt.tight_layout()
  1083. plt.savefig('baseline_cohort_topk_lines.png', dpi=300, bbox_inches='tight')
  1084. plt.savefig('baseline_cohort_topk_lines.svg', bbox_inches='tight')
  1085. plt.show()
  1086. print("Saved: baseline_cohort_topk_lines.png/svg")
  1087. # %% [markdown]
  1088. # ## 13. Figure 4: Accuracy Heatmap
  1089. # %%
  1090. # Create heatmap data (Top-1 Extended)
  1091. heatmap_data = bar_data.pivot(index='Cohort', columns='Method', values='Extended (%)')
  1092. heatmap_data = heatmap_data.loc[eval_cohorts, method_labels] # Reorder
  1093. fig, ax = plt.subplots(figsize=(10, 6))
  1094. sns.heatmap(heatmap_data, annot=True, fmt='.1f', cmap='RdYlGn',
  1095. vmin=0, vmax=100, ax=ax, cbar_kws={'label': 'Accuracy (%)'},
  1096. linewidths=0.5, linecolor='white')
  1097. ax.set_title('Top-1 Extended Accuracy by Cohort and Method', fontsize=14, fontweight='bold')
  1098. ax.set_xlabel('Method', fontsize=12)
  1099. ax.set_ylabel('Cohort', fontsize=12)
  1100. plt.xticks(rotation=45, ha='right')
  1101. plt.tight_layout()
  1102. plt.savefig('baseline_accuracy_heatmap.png', dpi=300, bbox_inches='tight')
  1103. plt.savefig('baseline_accuracy_heatmap.svg', bbox_inches='tight')
  1104. plt.show()
  1105. print("Saved: baseline_accuracy_heatmap.png/svg")
  1106. # %% [markdown]
  1107. # ## 14. Confusion Matrices
  1108. # %%
  1109. from sklearn.metrics import confusion_matrix
  1110. def plot_confusion_matrix(y_true, y_pred, title, ax, normalize=True):
  1111. """Plot confusion matrix with cohort labels."""
  1112. labels = sorted(set(y_true) | set(y_pred))
  1113. cm = confusion_matrix(y_true, y_pred, labels=labels)
  1114. if normalize:
  1115. cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
  1116. cm = np.nan_to_num(cm) # Handle division by zero
  1117. sns.heatmap(cm, annot=False, fmt='.2f' if normalize else 'd', cmap='Blues',
  1118. xticklabels=labels, yticklabels=labels, ax=ax,
  1119. vmin=0, vmax=1 if normalize else None)
  1120. ax.set_title(title, fontsize=11, fontweight='bold')
  1121. ax.set_xlabel('Predicted')
  1122. ax.set_ylabel('True Cohort')
  1123. # Create confusion matrices for each method
  1124. fig, axes = plt.subplots(2, 2, figsize=(16, 14))
  1125. # MLMarker: Use predicted tissue grouped by cohort
  1126. ax = axes[0, 0]
  1127. y_true = results_df['cohort'].values
  1128. y_pred = results_df['mlmarker_pred'].fillna('Unknown').values
  1129. plot_confusion_matrix(y_true, y_pred, 'MLMarker Model', ax)
  1130. ax = axes[0, 1]
  1131. y_pred = results_df['ml_training_pred'].fillna('Unknown').values
  1132. plot_confusion_matrix(y_true, y_pred, 'MLMarker Training Atlas', ax)
  1133. ax = axes[1, 0]
  1134. y_pred = results_df['hpa_pred'].fillna('Unknown').values
  1135. plot_confusion_matrix(y_true, y_pred, 'HPA Atlas', ax)
  1136. ax = axes[1, 1]
  1137. y_pred = results_df['pdb_pred'].fillna('Unknown').values
  1138. plot_confusion_matrix(y_true, y_pred, 'PDB Atlas', ax)
  1139. plt.suptitle('Confusion Matrices: True Cohort vs Predicted Tissue', fontsize=14, fontweight='bold')
  1140. plt.tight_layout()
  1141. plt.savefig('baseline_confusion_matrices.png', dpi=300, bbox_inches='tight')
  1142. plt.savefig('baseline_confusion_matrices.svg', bbox_inches='tight')
  1143. plt.show()
  1144. print("Saved: baseline_confusion_matrices.png/svg")
  1145. # %% [markdown]
  1146. # ## 15. Summary Statistics and Save Results
  1147. # %%
  1148. # Final summary
  1149. print("=" * 80)
  1150. print("FINAL SUMMARY: MLMarker vs Atlas-Correlation Baselines")
  1151. print("=" * 80)
  1152. mlmarker_t1 = results_df['mlmarker_top1_extended'].mean() * 100
  1153. mltraining_t1 = results_df['ml_training_top1_extended'].mean() * 100
  1154. hpa_t1 = results_df['hpa_top1_extended'].mean() * 100
  1155. pdb_t1 = results_df['pdb_top1_extended'].mean() * 100
  1156. print(f"\nTop-1 Extended Accuracy:")
  1157. print(f" MLMarker Model: {mlmarker_t1:.1f}%")
  1158. print(f" MLMarker Training: {mltraining_t1:.1f}% (Δ = {mlmarker_t1 - mltraining_t1:+.1f}%)")
  1159. print(f" HPA Atlas: {hpa_t1:.1f}% (Δ = {mlmarker_t1 - hpa_t1:+.1f}%)")
  1160. print(f" PDB Atlas: {pdb_t1:.1f}% (Δ = {mlmarker_t1 - pdb_t1:+.1f}%)")
  1161. print(f"\nEvaluated on {len(results_df)} samples across {len(eval_cohorts)} cohorts")
  1162. print(f"Cohorts: {', '.join(eval_cohorts)}")
  1163. # Save results
  1164. summary_df.to_csv('baseline_comparison_summary.csv', index=False)
  1165. cohort_df.to_csv('baseline_comparison_cohort.csv', index=False)
  1166. results_df.to_csv('baseline_comparison_detailed.csv', index=False)
  1167. print("\nResults saved:")
  1168. print(" - baseline_comparison_summary.csv")
  1169. print(" - baseline_comparison_cohort.csv")
  1170. print(" - baseline_comparison_detailed.csv")
  1171. # %% [markdown]
  1172. # ## 16. Additional Baseline Methods
  1173. #
  1174. # ### 16.1 Nearest Centroid Classification
  1175. # Classify samples by Euclidean distance to tissue centroids.
  1176. # %%
  1177. from scipy.spatial.distance import euclidean, cdist
  1178. from sklearn.neighbors import KNeighborsClassifier
  1179. from sklearn.preprocessing import StandardScaler
  1180. def nearest_centroid_classify(sample_profile, atlas_matrix):
  1181. """Classify by Euclidean distance to tissue centroids."""
  1182. common_proteins = sample_profile.index.intersection(atlas_matrix.index)
  1183. if len(common_proteins) < 10:
  1184. return None, {}, {}
  1185. sample_aligned = sample_profile.loc[common_proteins].values
  1186. atlas_aligned = atlas_matrix.loc[common_proteins]
  1187. distances = {}
  1188. for tissue in atlas_aligned.columns:
  1189. tissue_profile = atlas_aligned[tissue].values
  1190. dist = euclidean(sample_aligned, tissue_profile)
  1191. distances[tissue] = -dist # Negative so higher = closer
  1192. if distances:
  1193. predicted = max(distances, key=distances.get)
  1194. return predicted, distances[predicted], distances
  1195. return None, 0, {}
  1196. # Run Nearest Centroid classification on all atlases
  1197. nc_results = {'sample': [], 'cohort': [],
  1198. 'nc_ml_pred': [], 'nc_hpa_pred': [], 'nc_pdb_pred': []}
  1199. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="Nearest Centroid"):
  1200. sample_name = row['sample']
  1201. cohort = row['Cohort']
  1202. expr_col = row['expression_col']
  1203. sample_profile = sample_expr_log[expr_col]
  1204. sample_profile = sample_profile[sample_profile > 0]
  1205. nc_results['sample'].append(sample_name)
  1206. nc_results['cohort'].append(cohort)
  1207. # MLMarker Training Atlas
  1208. pred, _, scores = nearest_centroid_classify(sample_profile, ml_atlas)
  1209. nc_results['nc_ml_pred'].append(pred)
  1210. # HPA Atlas
  1211. pred, _, scores = nearest_centroid_classify(sample_profile, hpa_matrix)
  1212. nc_results['nc_hpa_pred'].append(pred)
  1213. # PDB Atlas
  1214. pred, _, scores = nearest_centroid_classify(sample_profile, pdb_matrix)
  1215. nc_results['nc_pdb_pred'].append(pred)
  1216. nc_df = pd.DataFrame(nc_results)
  1217. # Calculate accuracy
  1218. print("Nearest Centroid Classification Results (Top-1 Extended):")
  1219. print("=" * 60)
  1220. # MLMarker Training
  1221. correct = sum(nc_df.apply(lambda r: r['nc_ml_pred'] in cancer_to_mlmarker_extended.get(r['cohort'], []), axis=1))
  1222. print(f"MLMarker Training Atlas (NC): {correct/len(nc_df)*100:.1f}%")
  1223. # HPA
  1224. correct = sum(nc_df.apply(lambda r: r['nc_hpa_pred'] in cancer_to_hpa_extended.get(r['cohort'], []), axis=1))
  1225. print(f"HPA Atlas (NC): {correct/len(nc_df)*100:.1f}%")
  1226. # PDB
  1227. correct = sum(nc_df.apply(lambda r: r['nc_pdb_pred'] in cancer_to_pdb_extended.get(r['cohort'], []), axis=1))
  1228. print(f"PDB Atlas (NC): {correct/len(nc_df)*100:.1f}%")
  1229. # %%
  1230. # Figure: KNN vs Correlation Comparison
  1231. fig, axes = plt.subplots(1, 2, figsize=(14, 5))
  1232. # Plot 1: MLMarker Training - KNN k sensitivity
  1233. ax = axes[0]
  1234. knn_accs = [knn_ml_df[f'knn_ml_k{k}_top1_extended'].mean()*100 for k in k_values]
  1235. spearman_acc = results_df['ml_training_top1_extended'].mean()*100
  1236. mlmarker_acc = results_df['mlmarker_top1_extended'].mean()*100
  1237. ax.plot(k_values, knn_accs, 'o-', color='#457B9D', linewidth=2, markersize=8, label='KNN')
  1238. ax.axhline(y=spearman_acc, color='#2A9D8F', linestyle='--', linewidth=2, label='Spearman Correlation')
  1239. ax.axhline(y=mlmarker_acc, color='#E63946', linestyle='-', linewidth=2, label='MLMarker Model')
  1240. ax.set_xlabel('k (Number of Neighbors)', fontsize=12)
  1241. ax.set_ylabel('Top-1 Extended Accuracy (%)', fontsize=12)
  1242. ax.set_title('MLMarker Training Data: KNN vs Correlation', fontsize=14, fontweight='bold')
  1243. ax.set_xticks(k_values)
  1244. ax.set_ylim(0, 100)
  1245. ax.legend(loc='lower right')
  1246. ax.grid(True, alpha=0.3)
  1247. # Plot 2: Comparison across all methods
  1248. ax = axes[1]
  1249. methods_compare = ['MLMarker\nModel', 'ML Train\n(Spearman)', 'ML Train\n(KNN k=5)',
  1250. 'HPA\n(Spearman)', 'HPA\n(KNN)', 'PDB\n(Spearman)', 'PDB\n(KNN)']
  1251. accs_compare = [
  1252. mlmarker_acc,
  1253. spearman_acc,
  1254. knn_ml_df['knn_ml_k5_top1_extended'].mean()*100,
  1255. results_df['hpa_top1_extended'].mean()*100,
  1256. knn_atlas_df['knn_hpa_top1_extended'].mean()*100,
  1257. results_df['pdb_top1_extended'].mean()*100,
  1258. knn_atlas_df['knn_pdb_top1_extended'].mean()*100
  1259. ]
  1260. colors_compare = ['#E63946', '#457B9D', '#1D3557', '#2A9D8F', '#264653', '#E9C46A', '#F4A261']
  1261. bars = ax.bar(methods_compare, accs_compare, color=colors_compare, edgecolor='black', linewidth=0.5)
  1262. for bar in bars:
  1263. height = bar.get_height()
  1264. ax.annotate(f'{height:.1f}%', xy=(bar.get_x() + bar.get_width()/2, height),
  1265. xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=9)
  1266. ax.set_ylabel('Top-1 Extended Accuracy (%)', fontsize=12)
  1267. ax.set_title('KNN vs Spearman Correlation', fontsize=14, fontweight='bold')
  1268. ax.set_ylim(0, 100)
  1269. ax.tick_params(axis='x', rotation=45)
  1270. ax.grid(True, alpha=0.3, axis='y')
  1271. plt.tight_layout()
  1272. plt.savefig('baseline_knn_comparison.png', dpi=300, bbox_inches='tight')
  1273. plt.savefig('baseline_knn_comparison.svg', bbox_inches='tight')
  1274. plt.show()
  1275. print("Saved: baseline_knn_comparison.png/svg")
  1276. # %%
  1277. # Per-cohort KNN accuracy breakdown
  1278. print("\nPer-Cohort KNN Accuracy (Top-1 Extended):")
  1279. print("=" * 100)
  1280. print(f"{'Cohort':<12} {'MLMarker':>10} {'ML Spear':>10} {'ML KNN-5':>10} {'HPA Spear':>10} {'HPA KNN':>10} {'PDB Spear':>10} {'PDB KNN':>10}")
  1281. print("-" * 100)
  1282. for cohort in eval_cohorts:
  1283. mask = results_df['cohort'] == cohort
  1284. knn_mask = knn_ml_df['cohort'] == cohort
  1285. atlas_mask = knn_atlas_df['cohort'] == cohort
  1286. ml_model = results_df.loc[mask, 'mlmarker_top1_extended'].mean() * 100
  1287. ml_spear = results_df.loc[mask, 'ml_training_top1_extended'].mean() * 100
  1288. ml_knn = knn_ml_df.loc[knn_mask, 'knn_ml_k5_top1_extended'].mean() * 100
  1289. hpa_spear = results_df.loc[mask, 'hpa_top1_extended'].mean() * 100
  1290. hpa_knn = knn_atlas_df.loc[atlas_mask, 'knn_hpa_top1_extended'].mean() * 100
  1291. pdb_spear = results_df.loc[mask, 'pdb_top1_extended'].mean() * 100
  1292. pdb_knn = knn_atlas_df.loc[atlas_mask, 'knn_pdb_top1_extended'].mean() * 100
  1293. print(f"{cohort:<12} {ml_model:>9.1f}% {ml_spear:>9.1f}% {ml_knn:>9.1f}% {hpa_spear:>9.1f}% {hpa_knn:>9.1f}% {pdb_spear:>9.1f}% {pdb_knn:>9.1f}%")
  1294. # %%
  1295. # Top-k accuracy curves for KNN methods
  1296. fig, ax = plt.subplots(figsize=(10, 6))
  1297. # MLMarker Model
  1298. ml_topk = [results_df[f'mlmarker_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1299. ax.plot([1,2,3,4,5], ml_topk, 'o-', color='#E63946', linewidth=2, markersize=8, label='MLMarker Model')
  1300. # ML Training Spearman
  1301. ml_train_topk = [results_df[f'ml_training_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1302. ax.plot([1,2,3,4,5], ml_train_topk, 's--', color='#457B9D', linewidth=2, markersize=8, label='ML Training (Spearman)')
  1303. # ML Training KNN k=5
  1304. ml_knn_topk = [knn_ml_df[f'knn_ml_k5_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1305. ax.plot([1,2,3,4,5], ml_knn_topk, '^--', color='#1D3557', linewidth=2, markersize=8, label='ML Training (KNN k=5)')
  1306. # HPA Spearman
  1307. hpa_topk = [results_df[f'hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1308. ax.plot([1,2,3,4,5], hpa_topk, 's--', color='#2A9D8F', linewidth=2, markersize=8, label='HPA (Spearman)')
  1309. # HPA KNN
  1310. hpa_knn_topk = [knn_atlas_df[f'knn_hpa_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1311. ax.plot([1,2,3,4,5], hpa_knn_topk, '^--', color='#264653', linewidth=2, markersize=8, label='HPA (KNN)')
  1312. # PDB Spearman
  1313. pdb_topk = [results_df[f'pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1314. ax.plot([1,2,3,4,5], pdb_topk, 's--', color='#E9C46A', linewidth=2, markersize=8, label='PDB (Spearman)')
  1315. # PDB KNN
  1316. pdb_knn_topk = [knn_atlas_df[f'knn_pdb_top{k}_extended'].mean()*100 for k in [1,2,3,4,5]]
  1317. ax.plot([1,2,3,4,5], pdb_knn_topk, '^--', color='#F4A261', linewidth=2, markersize=8, label='PDB (KNN)')
  1318. ax.set_xlabel('k (Top-k)', fontsize=12)
  1319. ax.set_ylabel('Extended Accuracy (%)', fontsize=12)
  1320. ax.set_title('Top-k Accuracy: All Methods Including KNN', fontsize=14, fontweight='bold')
  1321. ax.set_xticks([1, 2, 3, 4, 5])
  1322. ax.set_ylim(0, 100)
  1323. ax.legend(loc='lower right', fontsize=9)
  1324. ax.grid(True, alpha=0.3)
  1325. plt.tight_layout()
  1326. # plt.savefig('baseline_topk_with_knn.png', dpi=300, bbox_inches='tight')
  1327. # plt.savefig('baseline_topk_with_knn.svg', bbox_inches='tight')
  1328. plt.show()
  1329. # print("Saved: baseline_topk_with_knn.png/svg")
  1330. # %%
  1331. # Save KNN results
  1332. knn_ml_df.to_csv('baseline_knn_mltraining.csv', index=False)
  1333. knn_atlas_df.to_csv('baseline_knn_atlas.csv', index=False)
  1334. print("KNN results saved:")
  1335. print(" - baseline_knn_mltraining.csv")
  1336. print(" - baseline_knn_atlas.csv")
  1337. # %% [markdown]
  1338. # ### 16.2 UMAP Visualization with Atlas Centroids
  1339. #
  1340. # Project cancer samples and atlas tissue centroids into a shared 2D space to visualize clustering.
  1341. # %%
  1342. import umap
  1343. # Use MLMarker Training Atlas for UMAP (most comparable to MLMarker model)
  1344. # Get common proteins between samples and atlas
  1345. common_proteins = sample_expr_log.index.intersection(ml_atlas.index)
  1346. print(f"Common proteins for UMAP: {len(common_proteins)}")
  1347. # Prepare sample data
  1348. sample_data = sample_expr_log.loc[common_proteins, valid_cols].T
  1349. sample_data.index = sample_metadata['sample'].values
  1350. # Prepare atlas centroids
  1351. atlas_centroids = ml_atlas.loc[common_proteins].T
  1352. # Combine for joint embedding
  1353. combined_data = pd.concat([sample_data, atlas_centroids])
  1354. combined_labels = list(sample_metadata['Cohort'].values) + list(atlas_centroids.index)
  1355. is_centroid = [False] * len(sample_data) + [True] * len(atlas_centroids)
  1356. # Fill NaN with 0 and scale
  1357. combined_filled = combined_data.fillna(0)
  1358. scaler = StandardScaler()
  1359. combined_scaled = scaler.fit_transform(combined_filled)
  1360. # Run UMAP
  1361. print("Running UMAP...")
  1362. reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42, n_components=2)
  1363. embedding = reducer.fit_transform(combined_scaled)
  1364. # Create embedding DataFrame
  1365. umap_df = pd.DataFrame({
  1366. 'UMAP1': embedding[:, 0],
  1367. 'UMAP2': embedding[:, 1],
  1368. 'Label': combined_labels,
  1369. 'Is_Centroid': is_centroid
  1370. })
  1371. print(f"UMAP embedding complete: {umap_df.shape}")
  1372. # %%
  1373. # Figure 5: UMAP with samples and expected tissue centroids highlighted
  1374. fig, ax = plt.subplots(figsize=(14, 10))
  1375. # Define colors for cohorts
  1376. cohort_colors = {'CRC': '#E63946', 'DLBCL': '#457B9D', 'DLBCL+': '#1D3557',
  1377. 'Glioma': '#2A9D8F', 'OSCC': '#E9C46A',
  1378. 'healthy LN': '#F4A261', 'healthy OE': '#264653'}
  1379. # Plot samples (smaller, transparent)
  1380. samples_df = umap_df[~umap_df['Is_Centroid']]
  1381. for cohort in eval_cohorts:
  1382. mask = samples_df['Label'] == cohort
  1383. ax.scatter(samples_df.loc[mask, 'UMAP1'], samples_df.loc[mask, 'UMAP2'],
  1384. c=cohort_colors[cohort], alpha=0.6, s=30, label=f'{cohort} samples')
  1385. # Plot expected tissue centroids with stars
  1386. expected_tissues = set()
  1387. for cohort in eval_cohorts:
  1388. expected_tissues.update(cancer_to_mlmarker_extended.get(cohort, []))
  1389. centroids_df = umap_df[umap_df['Is_Centroid']]
  1390. for tissue in expected_tissues:
  1391. if tissue in centroids_df['Label'].values:
  1392. row = centroids_df[centroids_df['Label'] == tissue]
  1393. ax.scatter(row['UMAP1'], row['UMAP2'], marker='*', s=500,
  1394. c='black', edgecolors='white', linewidth=2, zorder=10)
  1395. ax.annotate(tissue, (row['UMAP1'].values[0], row['UMAP2'].values[0]),
  1396. fontsize=9, fontweight='bold',
  1397. xytext=(5, 5), textcoords='offset points')
  1398. # Plot other centroids (grey, smaller)
  1399. other_tissues = set(centroids_df['Label']) - expected_tissues
  1400. for tissue in other_tissues:
  1401. row = centroids_df[centroids_df['Label'] == tissue]
  1402. ax.scatter(row['UMAP1'], row['UMAP2'], marker='*', s=150,
  1403. c='lightgray', edgecolors='gray', linewidth=1, zorder=5)
  1404. ax.set_xlabel('UMAP 1', fontsize=12)
  1405. ax.set_ylabel('UMAP 2', fontsize=12)
  1406. ax.set_title('UMAP: Cancer Samples with MLMarker Training Atlas Tissue Centroids',
  1407. fontsize=14, fontweight='bold')
  1408. ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), fontsize=9)
  1409. plt.tight_layout()
  1410. plt.savefig('baseline_umap_centroids.png', dpi=300, bbox_inches='tight')
  1411. plt.savefig('baseline_umap_centroids.svg', bbox_inches='tight')
  1412. plt.show()
  1413. print("Saved: baseline_umap_centroids.png/svg")
  1414. # %% [markdown]
  1415. # ### 16.3 Prediction Confidence Analysis
  1416. #
  1417. # Compare the "confidence margin" of correct predictions across methods. A higher margin indicates the model is more certain about its prediction.
  1418. # %%
  1419. # Calculate confidence margins (top1 - top2 score) for each method
  1420. confidence_data = []
  1421. for _, row in tqdm(sample_metadata.iterrows(), total=len(sample_metadata), desc="Confidence analysis"):
  1422. sample_name = row['sample']
  1423. cohort = row['Cohort']
  1424. expr_col = row['expression_col']
  1425. sample_profile = sample_expr_log[expr_col]
  1426. sample_profile = sample_profile[sample_profile > 0]
  1427. # MLMarker Model
  1428. if sample_name in sample_mapping_mlmarker:
  1429. ml_col = sample_mapping_mlmarker[sample_name]
  1430. scores = mlmarker_predictions[ml_col].sort_values(ascending=False)
  1431. margin = scores.iloc[0] - scores.iloc[1] if len(scores) > 1 else 0
  1432. is_correct = scores.index[0] in cancer_to_mlmarker_extended.get(cohort, [])
  1433. confidence_data.append({'Method': 'MLMarker', 'Margin': margin,
  1434. 'Correct': is_correct, 'Cohort': cohort})
  1435. # MLMarker Training Atlas (Spearman)
  1436. _, _, scores = atlas_correlation_classify(sample_profile, ml_atlas)
  1437. if scores:
  1438. sorted_scores = sorted(scores.values(), reverse=True)
  1439. margin = sorted_scores[0] - sorted_scores[1] if len(sorted_scores) > 1 else 0
  1440. pred = max(scores, key=scores.get)
  1441. is_correct = pred in cancer_to_mlmarker_extended.get(cohort, [])
  1442. confidence_data.append({'Method': 'MLMarker Training', 'Margin': margin,
  1443. 'Correct': is_correct, 'Cohort': cohort})
  1444. # HPA Atlas
  1445. _, _, scores = atlas_correlation_classify(sample_profile, hpa_matrix)
  1446. if scores:
  1447. sorted_scores = sorted(scores.values(), reverse=True)
  1448. margin = sorted_scores[0] - sorted_scores[1] if len(sorted_scores) > 1 else 0
  1449. pred = max(scores, key=scores.get)
  1450. is_correct = pred in cancer_to_hpa_extended.get(cohort, [])
  1451. confidence_data.append({'Method': 'HPA Atlas', 'Margin': margin,
  1452. 'Correct': is_correct, 'Cohort': cohort})
  1453. # PDB Atlas
  1454. _, _, scores = atlas_correlation_classify(sample_profile, pdb_matrix)
  1455. if scores:
  1456. sorted_scores = sorted(scores.values(), reverse=True)
  1457. margin = sorted_scores[0] - sorted_scores[1] if len(sorted_scores) > 1 else 0
  1458. pred = max(scores, key=scores.get)
  1459. is_correct = pred in cancer_to_pdb_extended.get(cohort, [])
  1460. confidence_data.append({'Method': 'PDB Atlas', 'Margin': margin,
  1461. 'Correct': is_correct, 'Cohort': cohort})
  1462. confidence_df = pd.DataFrame(confidence_data)
  1463. print(f"Confidence data collected: {len(confidence_df)} predictions")
  1464. # %%
  1465. # Figure 6: Confidence Margin Distribution
  1466. fig, axes = plt.subplots(1, 2, figsize=(14, 5))
  1467. # Plot 1: Box plot of margins by method
  1468. ax = axes[0]
  1469. order = ['MLMarker', 'MLMarker Training', 'HPA Atlas', 'PDB Atlas']
  1470. palette = {'MLMarker': '#E63946', 'MLMarker Training': '#457B9D',
  1471. 'HPA Atlas': '#2A9D8F', 'PDB Atlas': '#E9C46A'}
  1472. sns.boxplot(data=confidence_df, x='Method', y='Margin', order=order,
  1473. palette=palette, ax=ax)
  1474. ax.set_xlabel('Method', fontsize=12)
  1475. ax.set_ylabel('Confidence Margin (Top1 - Top2 Score)', fontsize=12)
  1476. ax.set_title('Prediction Confidence by Method', fontsize=14, fontweight='bold')
  1477. ax.tick_params(axis='x', rotation=45)
  1478. # Plot 2: Margin distribution for correct vs incorrect predictions
  1479. ax = axes[1]
  1480. correct_df = confidence_df[confidence_df['Correct']]
  1481. incorrect_df = confidence_df[~confidence_df['Correct']]
  1482. for i, method in enumerate(order):
  1483. correct_margins = correct_df[correct_df['Method'] == method]['Margin']
  1484. incorrect_margins = incorrect_df[incorrect_df['Method'] == method]['Margin']
  1485. positions = [i - 0.2, i + 0.2]
  1486. bp = ax.boxplot([correct_margins, incorrect_margins], positions=positions,
  1487. widths=0.35, patch_artist=True)
  1488. bp['boxes'][0].set_facecolor(palette[method])
  1489. bp['boxes'][0].set_alpha(0.8)
  1490. bp['boxes'][1].set_facecolor(palette[method])
  1491. bp['boxes'][1].set_alpha(0.3)
  1492. ax.set_xticks(range(len(order)))
  1493. ax.set_xticklabels(order, rotation=45, ha='right')
  1494. ax.set_xlabel('Method', fontsize=12)
  1495. ax.set_ylabel('Confidence Margin', fontsize=12)
  1496. ax.set_title('Confidence: Correct (dark) vs Incorrect (light)', fontsize=14, fontweight='bold')
  1497. # Add legend
  1498. from matplotlib.patches import Patch
  1499. legend_elements = [Patch(facecolor='gray', alpha=0.8, label='Correct'),
  1500. Patch(facecolor='gray', alpha=0.3, label='Incorrect')]
  1501. ax.legend(handles=legend_elements, loc='upper right')
  1502. plt.tight_layout()
  1503. plt.savefig('baseline_confidence_margins.png', dpi=300, bbox_inches='tight')
  1504. plt.savefig('baseline_confidence_margins.svg', bbox_inches='tight')
  1505. plt.show()
  1506. print("Saved: baseline_confidence_margins.png/svg")
  1507. # Print mean margins
  1508. print("\nMean Confidence Margins:")
  1509. print("=" * 50)
  1510. for method in order:
  1511. method_df = confidence_df[confidence_df['Method'] == method]
  1512. correct_margin = method_df[method_df['Correct']]['Margin'].mean()
  1513. incorrect_margin = method_df[~method_df['Correct']]['Margin'].mean()
  1514. print(f"{method:<20}: Correct={correct_margin:.3f}, Incorrect={incorrect_margin:.3f}")
  1515. # %% [markdown]
  1516. # ### 16.4 Statistical Significance: McNemar's Test
  1517. #
  1518. # Compare MLMarker vs each baseline using McNemar's test for paired nominal data.
  1519. # %%
  1520. from statsmodels.stats.contingency_tables import mcnemar
  1521. def run_mcnemar(correct_a, correct_b, name_a, name_b):
  1522. """Run McNemar's test comparing two classifiers."""
  1523. # Create contingency table
  1524. both_correct = sum(a and b for a, b in zip(correct_a, correct_b))
  1525. a_only = sum(a and not b for a, b in zip(correct_a, correct_b))
  1526. b_only = sum(not a and b for a, b in zip(correct_a, correct_b))
  1527. both_wrong = sum(not a and not b for a, b in zip(correct_a, correct_b))
  1528. table = [[both_correct, a_only], [b_only, both_wrong]]
  1529. result = mcnemar(table, exact=True)
  1530. return {
  1531. 'Comparison': f'{name_a} vs {name_b}',
  1532. 'Both Correct': both_correct,
  1533. f'{name_a} Only': a_only,
  1534. f'{name_b} Only': b_only,
  1535. 'Both Wrong': both_wrong,
  1536. 'p-value': result.pvalue,
  1537. 'Significant': 'Yes' if result.pvalue < 0.05 else 'No'
  1538. }
  1539. # Get correct/incorrect arrays
  1540. mlmarker_correct = results_df['mlmarker_top1_extended'].values
  1541. ml_training_correct = results_df['ml_training_top1_extended'].values
  1542. hpa_correct = results_df['hpa_top1_extended'].values
  1543. pdb_correct = results_df['pdb_top1_extended'].values
  1544. # Run McNemar's tests
  1545. mcnemar_results = []
  1546. mcnemar_results.append(run_mcnemar(mlmarker_correct, ml_training_correct, 'MLMarker', 'ML Training'))
  1547. mcnemar_results.append(run_mcnemar(mlmarker_correct, hpa_correct, 'MLMarker', 'HPA'))
  1548. mcnemar_results.append(run_mcnemar(mlmarker_correct, pdb_correct, 'MLMarker', 'PDB'))
  1549. mcnemar_results.append(run_mcnemar(ml_training_correct, hpa_correct, 'ML Training', 'HPA'))
  1550. mcnemar_results.append(run_mcnemar(ml_training_correct, pdb_correct, 'ML Training', 'PDB'))
  1551. mcnemar_results.append(run_mcnemar(hpa_correct, pdb_correct, 'HPA', 'PDB'))
  1552. mcnemar_df = pd.DataFrame(mcnemar_results)
  1553. print("McNemar's Test Results (Top-1 Extended Accuracy):")
  1554. print("=" * 80)
  1555. print(mcnemar_df.to_string(index=False))
  1556. # %% [markdown]
  1557. # ### 16.5 Random Baseline Comparison
  1558. #
  1559. # Add a random classifier to show all methods perform significantly above chance.
  1560. # %%
  1561. # Calculate random baseline (chance level)
  1562. # For MLMarker: 34 tissue classes
  1563. n_mlmarker_classes = len(mlmarker_predictions.index)
  1564. random_mlmarker_strict = 1 / n_mlmarker_classes * 100
  1565. # Extended: average number of acceptable tissues per cohort
  1566. avg_extended = np.mean([len(v) for v in cancer_to_mlmarker_extended.values()])
  1567. random_mlmarker_extended = avg_extended / n_mlmarker_classes * 100
  1568. # For HPA: 61 tissues
  1569. n_hpa_classes = len(hpa_matrix.columns)
  1570. avg_hpa_extended = np.mean([len(v) for v in cancer_to_hpa_extended.values()])
  1571. random_hpa_extended = avg_hpa_extended / n_hpa_classes * 100
  1572. # For PDB: 57 tissues
  1573. n_pdb_classes = len(pdb_matrix.columns)
  1574. avg_pdb_extended = np.mean([len(v) for v in cancer_to_pdb_extended.values()])
  1575. random_pdb_extended = avg_pdb_extended / n_pdb_classes * 100
  1576. print("Random Baseline (Chance Level):")
  1577. print("=" * 60)
  1578. print(f"MLMarker ({n_mlmarker_classes} classes):")
  1579. print(f" Strict: {random_mlmarker_strict:.2f}%")
  1580. print(f" Extended (avg {avg_extended:.1f} acceptable): {random_mlmarker_extended:.2f}%")
  1581. print(f"\nHPA ({n_hpa_classes} tissues):")
  1582. print(f" Extended (avg {avg_hpa_extended:.1f} acceptable): {random_hpa_extended:.2f}%")
  1583. print(f"\nPDB ({n_pdb_classes} tissues):")
  1584. print(f" Extended (avg {avg_pdb_extended:.1f} acceptable): {random_pdb_extended:.2f}%")
  1585. # %% [markdown]
  1586. # ## 17. Final Summary Figure: Combined Comparison
  1587. # %%
  1588. # Figure 7: Comprehensive comparison bar chart with random baseline
  1589. fig, ax = plt.subplots(figsize=(12, 7))
  1590. # Collect all method accuracies (Top-1 Extended)
  1591. method_accs = {
  1592. 'MLMarker\nModel': results_df['mlmarker_top1_extended'].mean() * 100,
  1593. 'MLMarker\nTraining Atlas': results_df['ml_training_top1_extended'].mean() * 100,
  1594. 'HPA Atlas\n(Spearman)': results_df['hpa_top1_extended'].mean() * 100,
  1595. 'PDB Atlas\n(Spearman)': results_df['pdb_top1_extended'].mean() * 100,
  1596. }
  1597. # Add random baselines
  1598. method_accs['Random\n(MLMarker)'] = random_mlmarker_extended
  1599. method_accs['Random\n(HPA)'] = random_hpa_extended
  1600. method_accs['Random\n(PDB)'] = random_pdb_extended
  1601. # Colors
  1602. bar_colors = ['#E63946', '#457B9D', '#2A9D8F', '#E9C46A', '#CCCCCC', '#CCCCCC', '#CCCCCC']
  1603. x = np.arange(len(method_accs))
  1604. bars = ax.bar(x, list(method_accs.values()), color=bar_colors, edgecolor='black', linewidth=0.5)
  1605. # Add value labels
  1606. for bar in bars:
  1607. height = bar.get_height()
  1608. ax.annotate(f'{height:.1f}%', xy=(bar.get_x() + bar.get_width()/2, height),
  1609. xytext=(0, 3), textcoords='offset points', ha='center', va='bottom',
  1610. fontsize=11, fontweight='bold')
  1611. ax.set_xticks(x)
  1612. ax.set_xticklabels(list(method_accs.keys()), fontsize=10)
  1613. ax.set_ylabel('Top-1 Extended Accuracy (%)', fontsize=12)
  1614. ax.set_title('Tissue Classification Accuracy: MLMarker vs Atlas Baselines',
  1615. fontsize=14, fontweight='bold')
  1616. ax.set_ylim(0, 100)
  1617. ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5, label='50% threshold')
  1618. ax.grid(True, alpha=0.3, axis='y')
  1619. # Add annotation for improvement
  1620. best_baseline = max(method_accs['MLMarker\nTraining Atlas'],
  1621. method_accs['HPA Atlas\n(Spearman)'],
  1622. method_accs['PDB Atlas\n(Spearman)'])
  1623. improvement = method_accs['MLMarker\nModel'] - best_baseline
  1624. ax.annotate(f'+{improvement:.1f}% vs best baseline',
  1625. xy=(0, method_accs['MLMarker\nModel']),
  1626. xytext=(1.5, method_accs['MLMarker\nModel'] + 5),
  1627. fontsize=11, fontweight='bold', color='#E63946',
  1628. arrowprops=dict(arrowstyle='->', color='#E63946'))
  1629. plt.tight_layout()
  1630. plt.savefig('baseline_final_comparison.png', dpi=300, bbox_inches='tight')
  1631. plt.savefig('baseline_final_comparison.svg', bbox_inches='tight')
  1632. plt.show()
  1633. print("Saved: baseline_final_comparison.png/svg")
  1634. # %%
  1635. # Save all results
  1636. mcnemar_df.to_csv('baseline_mcnemar_tests.csv', index=False)
  1637. confidence_df.to_csv('baseline_confidence_margins.csv', index=False)
  1638. print("\n" + "=" * 80)
  1639. print("COMPLETE ANALYSIS SUMMARY")
  1640. print("=" * 80)
  1641. print(f"\nDataset: {len(results_df)} cancer samples across {len(eval_cohorts)} cohorts")
  1642. print(f"Cohorts: {', '.join(eval_cohorts)}")
  1643. print(f"\nMethods compared:")
  1644. print(f" 1. MLMarker Model (Random Forest, 34 tissues)")
  1645. print(f" 2. MLMarker Training Atlas (Spearman correlation)")
  1646. print(f" 3. HPA Atlas (Spearman correlation, {len(hpa_matrix.columns)} tissues)")
  1647. print(f" 4. PDB Atlas (Spearman correlation, {len(pdb_matrix.columns)} tissues)")
  1648. print(f" 5. Nearest Centroid (Euclidean distance)")
  1649. print(f" 6. Random baseline (chance level)")
  1650. print(f"\n{'Method':<25} {'Top-1 Extended':>15} {'vs Random':>12}")
  1651. print("-" * 55)
  1652. print(f"{'MLMarker Model':<25} {results_df['mlmarker_top1_extended'].mean()*100:>14.1f}% {'+' + str(round(results_df['mlmarker_top1_extended'].mean()*100 - random_mlmarker_extended, 1)):>11}%")
  1653. print(f"{'MLMarker Training':<25} {results_df['ml_training_top1_extended'].mean()*100:>14.1f}% {'+' + str(round(results_df['ml_training_top1_extended'].mean()*100 - random_mlmarker_extended, 1)):>11}%")
  1654. print(f"{'HPA Atlas':<25} {results_df['hpa_top1_extended'].mean()*100:>14.1f}% {'+' + str(round(results_df['hpa_top1_extended'].mean()*100 - random_hpa_extended, 1)):>11}%")
  1655. print(f"{'PDB Atlas':<25} {results_df['pdb_top1_extended'].mean()*100:>14.1f}% {'+' + str(round(results_df['pdb_top1_extended'].mean()*100 - random_pdb_extended, 1)):>11}%")
  1656. print(f"\nAll saved files:")
  1657. print(" - baseline_comparison_summary.csv")
  1658. print(" - baseline_comparison_cohort.csv")
  1659. print(" - baseline_comparison_detailed.csv")
  1660. print(" - baseline_mcnemar_tests.csv")
  1661. print(" - baseline_confidence_margins.csv")
  1662. print(" - baseline_topk_accuracy_lines.png/svg")
  1663. print(" - baseline_cohort_barplot.png/svg")
  1664. print(" - baseline_cohort_topk_lines.png/svg")
  1665. print(" - baseline_accuracy_heatmap.png/svg")
  1666. print(" - baseline_confusion_matrices.png/svg")
  1667. print(" - baseline_umap_centroids.png/svg")
  1668. print(" - baseline_confidence_margins.png/svg")
  1669. print(" - baseline_final_comparison.png/svg")
  1670. # %%

baseline_comparison_clean.ipynb at commit c164143, no license · at the source

Overview

  1. VIB-UGent Center for Medical Biotechnology, VIB,Ghent, Belgium
  2. Department of Biomolecular Medicine, Ghent University,Ghent, Belgium
  3. BioOrganic Mass Spectrometry Laboratory (LSMBO), UMR 7178, IPHC, University of Strasbourg, CNRS,Strasbourg, 67000 France
  4. Infrastructure Nationale de Protéomique, ProFI-UAR 2048, Strasbourg, France
Journal: Genome biology, volume 27, issue 1, article 207
Dates: received 9 July 2025; accepted 21 May 2026; published online 24 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1186/s13059-026-04125-8 · PMID 42343371 · PMCID PMC13292323 · OpenAlex W4411313251
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), methods / tools (subfield)
Methods: Statistics, Machine learning
Keywords: Tissue prediction, Mass spectrometry-based proteomics, Machine learning, Public data reuse, AI
MeSH: Machine Learning*, Proteomics*, Software*, Biomarkers, Humans, Prediction Algorithms, Predictive Learning Models, Random Forest (* major topic)
Journal subjects: Methodology
Topic: AI in cancer detection (Artificial Intelligence, Computer Science), according to OpenAlex
Citations: cited by 1 paper (Europe PMC); 47 references in the paper

Abstract

MLMarker is a machine learning tool that computes continuous tissue similarity scores for proteomics data, addressing the challenge of interpreting complex or sparse datasets. Trained on 34 healthy tissues, its Random Forest model generates probabilistic predictions with SHAP-based protein-level explanations. A penalty factor corrects for missing proteins, improving robustness for low-coverage samples. Across three public datasets, MLMarker revealed brain-like signatures in cerebral melanoma metastases, achieved high accuracy in a pan-cancer cohort, and identified brain and pituitary origins in biofluids. MLMarker provides an interpretable framework for tissue inference and hypothesis generation, available as a Python package and Streamlit app.

Supplementary Information: The online version contains supplementary material available at 10.1186/s13059-026-04125-8.

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

Repositories

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

TineClaeys/MLMarker-manuscript

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: c164143cc2859301d5dff8ab50767401fcf72ba8, 13 July 2026
Languages: Jupyter (16), Python (6), R (2)
Size: 152 files, 24 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, 16 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (22 files), Matplotlib (20 files), pandas (20 files), seaborn (19 files), SciPy (10 files), Plotly (8 files), UMAP (7 files), scikit-learn (5 files), limma (2 files), tidyverse (2 files), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
24 files

TineClaeys/MLMarker

License: Apache-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 6444accee481886a09fd04c9b873ae1185c23420, 30 January 2026
Languages: Python (14), Jupyter (2)
Size: 72 files, 16 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, environment (pyproject.toml, requirements.txt, setup.py), tests, continuous integration, 2 notebooks
Not found: CITATION.cff, documentation
Tools: pandas (14 files), NumPy (10 files), SHAP (6 files), Matplotlib (4 files), Plotly (4 files), scikit-learn (4 files), XGBoost (4 files), imbalanced-learn (2 files), seaborn (2 files), PyTorch (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
18 files

TineClaeys/MLMarker-streamlit

License: Apache-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 2bf8a28f94efe81f9d89ddb6223d255b1e3af9e0, 21 August 2026
Languages: Python (8), Jupyter (2)
Size: 50 files, 10 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, environment (requirements.txt), 2 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (8 files), pandas (8 files), Plotly (8 files), scikit-learn (4 files), SciPy (3 files), Matplotlib (2 files), seaborn (2 files), SHAP (2 files), Pillow (1 file), UMAP (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
12 files

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

Tracing map

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

What the map holds:

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

All datasets used in this study are publicly available via ProteomeXchange (PXD007592, PXD009021, PXD008029) and MassIVE (MSV000095036). MLMarker predictions, differential expression results, and SHAP-derived protein lists are available in the Supplementary Data & Github repository for the manuscript analyses, pip package and streamlit application respectively github.com/TineClaeys/MLMarker-manuscript; github.com/TineClaeys/MLMarker; github.com/TineClaeys/MLMarker-streamlit. The streamlit application is available through mlmarker.streamlit.app all falling under Apache-2.0 license.

Reproduced under the paper's license (CC BY), 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 2, 28 September 2026

  • Authors: added Tine Claeys (0000-0001-9408-488X); Sam van Puyenbroeck (0009-0001-5281-6891); Kris Gevaert (0000-0002-4237-0283); Lennart Martens (0000-0003-4277-658X); removed Tine Claeys; Sam van Puyenbroeck; Kris Gevaert; Lennart Martens
  • Funding: added CHIST-ERA: G0GDV23N

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 5 keywords, 8 MeSH terms, 41 references.

Cite

This paper

Claeys, T., van Puyenbroeck, S., Gevaert, K., & Martens, L. (2026). MLMarker: a machine learning framework for tissue inference and biomarker discovery. Genome biology, 27(1), 207. https://doi.org/10.1186/s13059-026-04125-8

BibTeX

@article{claeys2026mlmarker,
author = {Claeys, Tine and van Puyenbroeck, Sam and Gevaert, Kris and Martens, Lennart},
title = {{MLMarker: a machine learning framework for tissue inference and biomarker discovery}},
journal = {Genome biology},
year = {2026},
month = jun,
volume = {27},
number = {1},
pages = {207},
publisher = {BMC},
issn = {1474-7596},
doi = {10.1186/s13059-026-04125-8},
url = {https://doi.org/10.1186/s13059-026-04125-8},
pmid = {42343371},
pmcid = {PMC13292323}
}

RIS

TY - JOUR
AU - Claeys, Tine
AU - van Puyenbroeck, Sam
AU - Gevaert, Kris
AU - Martens, Lennart
TI - MLMarker: a machine learning framework for tissue inference and biomarker discovery
T2 - Genome biology
J2 - Genome Biol
PY - 2026
DA - 2026/06/24
VL - 27
IS - 1
SP - 207
SN - 1474-7596
PB - BMC
DO - 10.1186/s13059-026-04125-8
UR - https://doi.org/10.1186/s13059-026-04125-8
LA - en
ER -

CSL-JSON

{
"id": "10.1186/s13059-026-04125-8",
"type": "article-journal",
"title": "MLMarker: a machine learning framework for tissue inference and biomarker discovery",
"container-title": "Genome biology",
"author": [
{
"family": "Claeys",
"given": "Tine"
},
{
"family": "van Puyenbroeck",
"given": "Sam"
},
{
"family": "Gevaert",
"given": "Kris"
},
{
"family": "Martens",
"given": "Lennart"
}
],
"container-title-short": "Genome Biol",
"volume": "27",
"issue": "1",
"page": "207",
"DOI": "10.1186/s13059-026-04125-8",
"PMID": "42343371",
"PMCID": "PMC13292323",
"ISSN": "1474-7596",
"publisher": "BMC",
"URL": "https://doi.org/10.1186/s13059-026-04125-8",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
24
]
]
}
}

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, SHAP, XGBoost, 13 other tools, 1 reference
[2] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: imbalanced-learn, SHAP, XGBoost, 11 other tools, genetics / omics
[3] doi:10.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: imbalanced-learn, XGBoost, UMAP, 8 other tools, methods / tools
[4] doi:10.1038/s41467-026-76939-w [code]
HIPPIE: a generative model for electrophysiological analysis across species, technologies, and modalities.
Journal: Nature communications
In common: imbalanced-learn, UMAP, Pillow, 9 other tools, methods / tools
[5] doi:10.1016/j.isci.2026.115329 [code]
Brain metastases converge on shared geometric architecture and transcriptomic landscape yet remain distinct from gliomas.
Journal: iScience
In common: imbalanced-learn, SHAP, XGBoost, 8 other tools, genetics / omics
[6] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: imbalanced-learn, SHAP, XGBoost, 8 other tools
[7] doi:10.1038/s43856-026-01606-6 [code]
Validation of remote multimodal AI screening for Parkinson disease across diverse settings.
Journal: Communications medicine
In common: imbalanced-learn, SHAP, Plotly, 8 other tools
[8] doi:10.1126/sciadv.aed3650 [code]
Truthful visualizations for mass spectrometry imaging enable high-spatial-resolution interactive &lt;i&gt;m/z&lt;/i&gt; mapping and exploration.
Journal: Science advances
In common: SHAP, XGBoost, UMAP, 7 other tools, methods / tools, genetics / omics
[9] 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: limma, UMAP, Plotly, 8 other tools, genetics / omics
[10] doi:10.3390/s26175327 [code]
Subject Identity Confounds qEEG Emotion Recognition on DEAP and DREAMER.
Journal: Sensors (Basel, Switzerland)
In common: imbalanced-learn, SHAP, XGBoost, 7 other tools

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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