OSCR

Exploring the link between body physiology and cognition: the role of the brain and aging.

Code ↔ Paper

20 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 20 matches
  1. [1] § Results › Predicting cognition from body phenotypes ↔ 9.Results/1Figures.ipynb, lines 1570–1656 · score 0.98 · peak expiratory flow, forced vital capacity, forced expiratory volume, muscle fat infiltration, head bone area, femur Ward
  2. [2] § Results › Predicting cognition from body phenotypes ↔ 9.Results/1Figures.ipynb, lines 1570–1656 · score 0.97 · systolic blood pressure, glycated haemoglobin, muscle fat infiltration, visceral adipose tissue, HbA1c, muscle ratio
  3. [3] § Methods › General cognition factor ↔ 1.Cognition/1.GetCognitiveScores.ipynb, lines 35–52 · score 0.95 · Symbol Digit Substitution, fluid intelligence, correctly identify matches, incorrect matches, complete numeric, Pairs Matching
  4. [4] § Results › Cognition–age relationship overlapping with body physiology and brain markers ↔ 6.Bootstrap/1Bootstrapping.ipynb, lines 467–611 · score 0.93 · Schaefer7n200p, ex vivo, aparc.a2009s, subcortical volumetric, FSL FAST, Schaefer7n500p
  5. [5] § Results › Cognition–age relationship overlapping with body physiology and brain markers ↔ 8.Commonality_mediation_analysis/1CommonalityAnalysis.ipynb, lines 265–412 · score 0.93 · Schaefer7n200p, ex vivo, aparc.a2009s, subcortical volumetric, FSL FAST, Schaefer7n500p
  6. [6] § Methods › Structural MRI (sMRI) ↔ 6.Bootstrap/1Bootstrapping.ipynb, lines 467–611 · score 0.91 · Desikan Killiany Tourville, subcortical volumetric subsegmentation, ex vivo, a2009s, FSL FAST, grey matter volumes
  7. [7] § Methods › Structural MRI (sMRI) ↔ 8.Commonality_mediation_analysis/1CommonalityAnalysis.ipynb, lines 265–412 · score 0.91 · Desikan Killiany Tourville, subcortical volumetric subsegmentation, ex vivo, a2009s, FSL FAST, grey matter volumes
  8. [8] § Methods › Diffusion-weighted MRI (dwMRI) ↔ 3_Get_brain_phenotypes/1GetMRIData_dwMRI.ipynb, lines 773–943 · score 0.91 · free water volume, diffusion tensor mode, orientation dispersion, probabilistic tractography, white matter tracts, volume fraction
  9. [9] § Methods › Body phenotypes ↔ 9.Results/2ResultSummary.ipynb, lines 74–108 · score 0.89 · pulse wave, carotid ultrasound, arterial stiffness, abdominal organs, heart MRI, electrocardiogram
  10. [10] § Methods › Body phenotypes ↔ 2.Get_body_phenotypes/1GetBodyPhenotypes.ipynb, lines 1522–1598 · score 0.89 · pulse wave, left ventricular, carotid ultrasound, arterial stiffness, heart MRI, fitness
  11. [11] § Results › Predicting cognition from body phenotypes ↔ 2.Get_body_phenotypes/1GetBodyPhenotypes.ipynb, lines 1437–1485 · score 0.87 · peak expiratory flow, forced vital capacity, forced expiratory volume, blood pressure, FEV1, FVC
  12. [12] § Results › Predicting cognition from body phenotypes ↔ 2.Get_body_phenotypes/1GetBodyPhenotypes.ipynb, lines 42–103 · score 0.84 · visceral adipose tissue, muscle fat infiltration, muscle ratio, fat fraction, thighs, pancreas
  13. [13] § Methods › Machine learning ↔ 5.Get_composite_markers_ML_2level_stacking/1-stack-body-brain.ipynb, lines 168–312 · score 0.69 · GridSearchCV, Random Forest, body brain, XGBoost, trained, stacking
  14. [14] § Results › Cognition–body relationship overlapping with brain markers ↔ 9.Results/1Figures.ipynb, lines 2673–2779 · score 0.69 · Venn diagrams, Stacked bar, Unique variance, common variance, individual body, composite
  15. [15] § Results ↔ 5.Get_composite_markers_ML_2level_stacking/1-stack-body-brain.ipynb, lines 168–312 · score 0.63 · Random Forest, absolute error, XGBoost, CV, algorithm, MAE
  16. [16] § Methods › Resting-state functional MRI (rsMRI) ↔ 3_Get_brain_phenotypes/2GetMRIData_rsMRI_Transpose_GetCorrelationNilearn.py, lines 17–119 · score 0.61 · ConnectivityMeasure, partial correlation matrices, Nilearn, atlas, brain phenotypes, MRI
  17. [17] § Methods › Resting-state functional MRI (rsMRI) ↔ 3_Get_brain_phenotypes/1GetMRIData_rsMRI.ipynb, lines 465–608 · score 0.61 · ConnectivityMeasure, partial correlation matrices, Nilearn, rsMRI, atlas, brain phenotypes
  18. [18] § Methods › Machine learning ↔ 5.Get_composite_markers_ML_2level_stacking/1-stack-body-brain.ipynb, lines 19–166 · score 0.60 · absolute error, squared error, configuration, MSE, MAE, outer
  19. [19] § Results › Predicting cognition from brain phenotypes ↔ 3_Get_brain_phenotypes/5GetMRIData_sMRI.ipynb, lines 251–424 · score 0.58 · choroid plexus, lateral ventricle, hypointensities, cortex, posterior, brain phenotypes
  20. [20] § Methods › Neuroimaging ↔ 3_Get_brain_phenotypes/1GetMRIData_dwMRI.ipynb, lines 773–943 · score 0.56 · fiber orientation, water diffusion, dwMRI, axons, weighted, density

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 · 3,791 lines · 137 KB · MIT · 3 matches

  1. # %%
  2. import csv
  3. import os
  4. import re
  5. import random
  6. import pickle
  7. import openpyxl
  8. import warnings
  9. import pandas as pd
  10. import scipy
  11. import numpy as np
  12. import matplotlib as mpl
  13. from matplotlib_venn import venn2
  14. from matplotlib_venn import venn3
  15. import matplotlib.gridspec as gridspec
  16. from matplotlib.gridspec import GridSpec
  17. from matplotlib import ticker
  18. import matplotlib.ticker as mticker
  19. from matplotlib.legend_handler import HandlerPatch
  20. import matplotlib.cm as cm
  21. import matplotlib.patches as mpatches
  22. import matplotlib.colors as colors
  23. from scipy import stats
  24. from scipy.stats import gaussian_kde
  25. import statsmodels.api as sm
  26. import matplotlib.pyplot as plt
  27. from matplotlib.ticker import FormatStrFormatter, MultipleLocator, AutoMinorLocator, MaxNLocator, FixedLocator
  28. import seaborn as sns
  29. from scipy.stats import pearsonr
  30. from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
  31. from matplotlib.collections import PolyCollection
  32. import matplotlib.patches as patches
  33. from tqdm import tqdm
  34. from typing import List, Dict, Optional
  35. # %%
  36. # Define modality renaming dictionary (modality_map)
  37. modality_map = {
  38. 'hearing': 'Hearing',
  39. 'immune': 'Immune',
  40. 'renalhepatic': 'Renal & Hepatic',
  41. 'metabolic': 'Metabolic',
  42. 'cardiopulmonary': 'Cardiopulmonary',
  43. 'musculoskeletal': 'Musculoskeletal',
  44. 'bone_densitometry': 'Bone Densitometry of Heel',
  45. 'pwa': 'Pulse Wave Analysis',
  46. 'heart_mri': 'Heart MRI',
  47. 'carotid_ultrasound': 'Carotid Ultrasound',
  48. 'arterial_stiffness': 'Arterial Stiffness',
  49. 'ecg_rest': 'ECG at Rest',
  50. 'body_composition_by_impedance': 'Body Composition by Impedance',
  51. 'body_composition_dxa': 'Body Composition by DXA',
  52. 'bone_dxa': 'Bone Size, Mineral and Density by DXA',
  53. 'kidneys_mri': 'Kidney MRI',
  54. 'liver_mri': 'Liver MRI',
  55. 'abdominal_composition_mri_18_vars': 'Abdominal Composition by MRI',
  56. 'abdominal_organ_composition_mri_13_vars': 'Abdominal Organ Composition by MRI',
  57. 'struct_fast' : 'Regional grey matter volumes (FSL FAST)',
  58. 'struct_sub_first': 'Subcortical volumes (FSL FIRST)',
  59. 'struct_fs_aseg_mean_intensity' : 'ASEG Mean Intensity',
  60. 'struct_fs_aseg_volume' : 'ASEG Volume',
  61. 'struct_ba_exvivo_area' : 'BA ex-vivo Area',
  62. 'struct_ba_exvivo_mean_thickness' : 'BA ex-vivo Mean Thickness',
  63. 'struct_ba_exvivo_volume' : 'BA ex-vivo Volume',
  64. 'struct_a2009s_area' : 'a2009s Area',
  65. 'struct_a2009s_mean_thickness' : 'a2009s Mean Thickness',
  66. 'struct_a2009s_volume' : 'a2009s Volume',
  67. 'struct_dkt_area' : 'Desikan-Killiany-Tourville Area',
  68. 'struct_dkt_mean_thickness' : 'Desikan-Killiany-Tourville Mean Thickness',
  69. 'struct_dkt_volume' : 'Desikan-Killiany-Tourville Volume',
  70. 'struct_desikan_gw' : 'Desikan Grey/White Matter Contrast',
  71. 'struct_desikan_pial' : 'Desikan Pial',
  72. 'struct_desikan_white_area' : 'Desikan White Matter Area',
  73. 'struct_desikan_white_mean_thickness' : 'Desikan White Matter Mean Thickness',
  74. 'struct_desikan_white_volume' : 'Desikan White Matter Volume',
  75. "struct_subsegmentation":'Subcortical Volumetric Subsegmentation',
  76. 'add_t1' : 'Whole-Brain T1w',
  77. 'add_t2' : 'Whole-Brain T2w',
  78. "dwi_FA_tbss": "FA TBSS",
  79. "dwi_FA_prob": "FA Probabilistic",
  80. "dwi_MD_tbss": "MD TBSS",
  81. "dwi_MD_prob": "MD Probabilistic",
  82. "dwi_L1_tbss": "L1 TBSS",
  83. "dwi_L1_prob": "L1 Probabilistic",
  84. "dwi_L2_tbss": "L2 TBSS",
  85. "dwi_L2_prob": "L2 Probabilistic",
  86. "dwi_L3_tbss": "L3 TBSS",
  87. "dwi_L3_prob": "L3 Probabilistic",
  88. "dwi_MO_tbss": "MO TBSS",
  89. "dwi_MO_prob": "MO Probabilistic",
  90. "dwi_OD_tbss": "OD TBSS",
  91. "dwi_OD_prob": "OD Probabilistic",
  92. "dwi_ICVF_tbss": "ICVF TBSS",
  93. "dwi_ICVF_prob": "ICVF Probabilistic",
  94. "dwi_ISOVF_tbss": "ISOVF TBSS",
  95. "dwi_ISOVF_prob": 'ISOVF Probabilistic',
  96. "amplitudes_21": " 21 IC Amplitudes",
  97. "amplitudes_55": "55 IC Amplitudes",
  98. "full_correlation_21": "21 IC Full Correlation",
  99. "full_correlation_55": "55 IC Full Correlation",
  100. "partial_correlation_21": " 21 IC Partial Correlation",
  101. "partial_correlation_55": " 55 IC Partial Correlation",
  102. # aparc Tian S1 (I)
  103. 'aparc_Tian_S1_FA_i2': 'aparc-I FA',
  104. 'aparc_Tian_S1_Length_i2': 'aparc-I Length',
  105. 'aparc_Tian_S1_SIFT2_FBC_i2': 'aparc-I SIFT2 FBC',
  106. 'aparc_Tian_S1_Streamline_Count_i2': 'aparc-I Streamline Count',
  107. # aparc a2009s Tian S1 (I)
  108. 'aparc_a2009s_Tian_S1_FA_i2': 'aparc.a2009s-I FA',
  109. 'aparc_a2009s_Tian_S1_Length_i2': 'aparc.a2009s-I Length',
  110. 'aparc_a2009s_Tian_S1_SIFT2_FBC_i2': 'aparc.a2009s-I SIFT2 FBC',
  111. 'aparc_a2009s_Tian_S1_Streamline_Count_i2': 'aparc.a2009s-I Streamline Count',
  112. # Glasser Tian S1 (I)
  113. 'Glasser_Tian_S1_FA_i2': 'Glasser-I FA',
  114. 'Glasser_Tian_S1_Length_i2': 'Glasser-I Length',
  115. 'Glasser_Tian_S1_SIFT2_FBC_i2': 'Glasser-I SIFT2 FBC',
  116. 'Glasser_Tian_S1_Streamline_Count_i2': 'Glasser-I Streamline Count',
  117. # Glasser Tian S4 (IV)
  118. 'Glasser_Tian_S4_FA_i2': 'Glasser-IV FA',
  119. 'Glasser_Tian_S4_Length_i2': 'Glasser-IV Length',
  120. 'Glasser_Tian_S4_SIFT2_FBC_i2': 'Glasser-IV SIFT2 FBC',
  121. 'Glasser_Tian_S4_Streamline_Count_i2': 'Glasser-IV Streamline Count',
  122. # Schaefer7n1000p Tian S4 (IV) (in reality: Schaefer7n200p Tian S1)
  123. 'Schaefer7n1000p_Tian_S4_FA_i2': 'Schaefer7n200p-I FA', #'Schaefer7n1000p-IV FA',
  124. 'Schaefer7n1000p_Tian_S4_Length_i2': 'Schaefer7n200p-I Length',#'Schaefer7n1000p-IV Length',
  125. 'Schaefer7n1000p_Tian_S4_SIFT2_FBC_i2': 'Schaefer7n200p-I SIFT2 FBC',#'Schaefer7n1000p-IV SIFT2 FBC',
  126. 'Schaefer7n1000p_Tian_S4_Streamline_Count_i2': 'Schaefer7n200p-I Streamline Count', #'Schaefer7n1000p-IV Streamline Count'
  127. # Schaefer7n200p Tian S4 (IV) (in reality: Schaefer7n500p Tian S4)
  128. 'Schaefer7n200p_Tian_S1_FA_i2': 'Schaefer7n500p-IV FA',
  129. 'Schaefer7n200p_Tian_S1_Length_i2': 'Schaefer7n500p-IV Length',
  130. 'Schaefer7n200p_Tian_S1_SIFT2_FBC_i2': 'Schaefer7n500p-IV SIFT2 FBC',
  131. 'Schaefer7n200p_Tian_S1_Streamline_Count_i2': 'Schaefer7n500p-IV Streamline Count',
  132. # Schaefer7n500p Tian S4 (IV) (in reality: Schaefer7n1000p Tian S4)
  133. 'Schaefer7n500p_Tian_S4_FA_i2': 'Schaefer7n1000p-IV FA',
  134. 'Schaefer7n500p_Tian_S4_Length_i2': 'Schaefer7n1000p-IV Length',
  135. 'Schaefer7n500p_Tian_S4_SIFT2_FBC_i2': 'Schaefer7n1000p-IV SIFT2 FBC',
  136. 'Schaefer7n500p_Tian_S4_Streamline_Count_i2': 'Schaefer7n1000p-IV Streamline Count',
  137. # Resting state
  138. 'full_correlation_aparc_a2009s_Tian_S1' : 'aparc.a2009s-I Full Correlation',
  139. 'full_correlation_aparc_Tian_S1': 'aparc-I Full Correlation',
  140. 'full_correlation_Glasser_Tian_S1': 'Glasser-I Full Correlation',
  141. 'full_correlation_Glasser_Tian_S4': 'Glasser-IV Full Correlation',
  142. 'full_correlation_Schaefer7n200p_Tian_S1': 'Schaefer7n200p-I Full Correlation',
  143. 'full_correlation_Schaefer7n500p_Tian_S4': 'Schaefer7n500p-IV Full Correlation',
  144. 'partial_correlation_aparc_a2009s_Tian_S1': 'aparc.a2009s-I Partial Correlation',
  145. 'partial_correlation_aparc_Tian_S1': 'aparc-I Partial Correlation',
  146. 'partial_correlation_Glasser_Tian_S1': 'Glasser-I Partial Correlation',
  147. 'partial_correlation_Glasser_Tian_S4': 'Glasser-IV Partial Correlation',
  148. 'partial_correlation_Schaefer7n200p_Tian_S1': 'Schaefer7n200p-I Partial Correlation',
  149. 'partial_correlation_Schaefer7n500p_Tian_S4': 'Schaefer7n500p-IV Partial Correlation',
  150. 'allmri': '3 Brain MRI Modalities Stacked',
  151. 'dwi': 'Brain dwMRI Stacked',
  152. 'smri': 'Brain sMRI Stacked',
  153. 'rs': 'Brain rsMRI Stacked',
  154. 'body': 'Body Physiology Stacked',
  155. 'brain-plus-body': '3 Brain MRI Modalities & Body Stacked',
  156. 'brain-body': 'Brain & Body Stacked',
  157. 'body-only': 'Body Physiology and Composition Stacked'
  158. }
  159. # Define modality names for renaming
  160. modality_names = {
  161. 'hearing': 'Hearing',
  162. 'immune': 'Immune',
  163. 'renalhepatic': 'Renal & Hepatic',
  164. 'metabolic': 'Metabolic',
  165. 'cardiopulmonary': 'Cardiopulmonary',
  166. 'musculoskeletal': 'Musculoskeletal',
  167. 'bone_densitometry': 'Bone Densitometry of Heel',
  168. 'pwa': 'Pulse Wave Analysis',
  169. 'heart_mri': 'Heart MRI',
  170. 'carotid_ultrasound': 'Carotid Ultrasound',
  171. 'arterial_stiffness': 'Arterial Stiffness',
  172. 'ecg_rest': 'ECG at Rest',
  173. 'body_composition_by_impedance': 'Body Composition by Impedance',
  174. 'body_composition_dxa': 'Body Composition by DXA',
  175. 'bone_dxa': 'Bone Size, Mineral and Density by DXA',
  176. 'kidneys_mri': 'Kidney MRI',
  177. 'liver_mri': 'Liver MRI',
  178. 'abdominal_composition_mri_18_vars': 'Abdominal Composition by MRI',
  179. 'abdominal_organ_composition_mri_13_vars': 'Abdominal Organ Composition by MRI',
  180. 'struct_fast' : 'Regional grey matter volumes (FSL FAST)',
  181. 'struct_sub_first': 'Subcortical volumes (FSL FIRST)',
  182. 'struct_fs_aseg_mean_intensity' : 'ASEG Mean Intensity',
  183. 'struct_fs_aseg_volume' : 'ASEG Volume',
  184. 'struct_ba_exvivo_area' : 'BA ex-vivo Area',
  185. 'struct_ba_exvivo_mean_thickness' : 'BA ex-vivo Mean Thickness',
  186. 'struct_ba_exvivo_volume' : 'BA ex-vivo Volume',
  187. 'struct_a2009s_area' : 'a2009s Area',
  188. 'struct_a2009s_mean_thickness' : 'a2009s Mean Thickness',
  189. 'struct_a2009s_volume' : 'a2009s Volume',
  190. 'struct_dkt_area' : 'Desikan-Killiany-Tourville Area',
  191. 'struct_dkt_mean_thickness' : 'Desikan-Killiany-Tourville Mean Thickness',
  192. 'struct_dkt_volume' : 'Desikan-Killiany-Tourville Volume',
  193. 'struct_desikan_gw' : 'Desikan Grey/White Matter Contrast',
  194. 'struct_desikan_pial' : 'Desikan Pial',
  195. 'struct_desikan_white_area' : 'Desikan White Matter Area',
  196. 'struct_desikan_white_mean_thickness' : 'Desikan White Matter Mean Thickness',
  197. 'struct_desikan_white_volume' : 'Desikan White Matter Volume',
  198. "struct_subsegmentation":'Subcortical Volumetric Subsegmentation',
  199. 'add_t1' : 'Whole-Brain T1w',
  200. 'add_t2' : 'Whole-Brain T2w',
  201. "dwi_FA_tbss": "FA TBSS",
  202. "dwi_FA_prob": "FA Probabilistic",
  203. "dwi_MD_tbss": "MD TBSS",
  204. "dwi_MD_prob": "MD Probabilistic",
  205. "dwi_L1_tbss": "L1 TBSS",
  206. "dwi_L1_prob": "L1 Probabilistic",
  207. "dwi_L2_tbss": "L2 TBSS",
  208. "dwi_L2_prob": "L2 Probabilistic",
  209. "dwi_L3_tbss": "L3 TBSS",
  210. "dwi_L3_prob": "L3 Probabilistic",
  211. "dwi_MO_tbss": "MO TBSS",
  212. "dwi_MO_prob": "MO Probabilistic",
  213. "dwi_OD_tbss": "OD TBSS",
  214. "dwi_OD_prob": "OD Probabilistic",
  215. "dwi_ICVF_tbss": "ICVF TBSS",
  216. "dwi_ICVF_prob": "ICVF Probabilistic",
  217. "dwi_ISOVF_tbss": "ISOVF TBSS",
  218. "dwi_ISOVF_prob": 'ISOVF Probabilistic',
  219. "amplitudes_21": " 21 IC Amplitudes",
  220. "amplitudes_55": "55 IC Amplitudes",
  221. "full_correlation_21": "21 IC Full Correlation",
  222. "full_correlation_55": "55 IC Full Correlation",
  223. "partial_correlation_21": " 21 IC Partial Correlation",
  224. "partial_correlation_55": " 55 IC Partial Correlation",
  225. # aparc Tian S1 (I)
  226. 'aparc_Tian_S1_FA_i2': 'aparc-I FA',
  227. 'aparc_Tian_S1_Length_i2': 'aparc-I Length',
  228. 'aparc_Tian_S1_SIFT2_FBC_i2': 'aparc-I SIFT2 FBC',
  229. 'aparc_Tian_S1_Streamline_Count_i2': 'aparc-I Streamline Count',
  230. # aparc a2009s Tian S1 (I)
  231. 'aparc_a2009s_Tian_S1_FA_i2': 'aparc.a2009s-I FA',
  232. 'aparc_a2009s_Tian_S1_Length_i2': 'aparc.a2009s-I Length',
  233. 'aparc_a2009s_Tian_S1_SIFT2_FBC_i2': 'aparc.a2009s-I SIFT2 FBC',
  234. 'aparc_a2009s_Tian_S1_Streamline_Count_i2': 'aparc.a2009s-I Streamline Count',
  235. # Glasser Tian S1 (I)
  236. 'Glasser_Tian_S1_FA_i2': 'Glasser-I FA',
  237. 'Glasser_Tian_S1_Length_i2': 'Glasser-I Length',
  238. 'Glasser_Tian_S1_SIFT2_FBC_i2': 'Glasser-I SIFT2 FBC',
  239. 'Glasser_Tian_S1_Streamline_Count_i2': 'Glasser-I Streamline Count',
  240. # Glasser Tian S4 (IV)
  241. 'Glasser_Tian_S4_FA_i2': 'Glasser-IV FA',
  242. 'Glasser_Tian_S4_Length_i2': 'Glasser-IV Length',
  243. 'Glasser_Tian_S4_SIFT2_FBC_i2': 'Glasser-IV SIFT2 FBC',
  244. 'Glasser_Tian_S4_Streamline_Count_i2': 'Glasser-IV Streamline Count',
  245. # Schaefer7n1000p Tian S4 (IV) (in reality: Schaefer7n200p Tian S1)
  246. 'Schaefer7n1000p_Tian_S4_FA_i2': 'Schaefer7n200p-I FA', #'Schaefer7n1000p-IV FA',
  247. 'Schaefer7n1000p_Tian_S4_Length_i2': 'Schaefer7n200p-I Length',#'Schaefer7n1000p-IV Length',
  248. 'Schaefer7n1000p_Tian_S4_SIFT2_FBC_i2': 'Schaefer7n200p-I SIFT2 FBC',#'Schaefer7n1000p-IV SIFT2 FBC',
  249. 'Schaefer7n1000p_Tian_S4_Streamline_Count_i2': 'Schaefer7n200p-I Streamline Count', #'Schaefer7n1000p-IV Streamline Count'
  250. # Schaefer7n200p Tian S4 (IV) (in reality: Schaefer7n500p Tian S4)
  251. 'Schaefer7n200p_Tian_S1_FA_i2': 'Schaefer7n500p-IV FA',
  252. 'Schaefer7n200p_Tian_S1_Length_i2': 'Schaefer7n500p-IV Length',
  253. 'Schaefer7n200p_Tian_S1_SIFT2_FBC_i2': 'Schaefer7n500p-IV SIFT2 FBC',
  254. 'Schaefer7n200p_Tian_S1_Streamline_Count_i2': 'Schaefer7n500p-IV Streamline Count',
  255. # Schaefer7n500p Tian S4 (IV) (in reality: Schaefer7n1000p Tian S4)
  256. 'Schaefer7n500p_Tian_S4_FA_i2': 'Schaefer7n1000p-IV FA',
  257. 'Schaefer7n500p_Tian_S4_Length_i2': 'Schaefer7n1000p-IV Length',
  258. 'Schaefer7n500p_Tian_S4_SIFT2_FBC_i2': 'Schaefer7n1000p-IV SIFT2 FBC',
  259. 'Schaefer7n500p_Tian_S4_Streamline_Count_i2': 'Schaefer7n1000p-IV Streamline Count',
  260. # Resting state
  261. 'full_correlation_aparc_a2009s_Tian_S1' : 'aparc.a2009s-I Full Correlation',
  262. 'full_correlation_aparc_Tian_S1': 'aparc-I Full Correlation',
  263. 'full_correlation_Glasser_Tian_S1': 'Glasser-I Full Correlation',
  264. 'full_correlation_Glasser_Tian_S4': 'Glasser-IV Full Correlation',
  265. 'full_correlation_Schaefer7n200p_Tian_S1': 'Schaefer7n200p-I Full Correlation',
  266. 'full_correlation_Schaefer7n500p_Tian_S4': 'Schaefer7n500p-IV Full Correlation',
  267. 'partial_correlation_aparc_a2009s_Tian_S1': 'aparc.a2009s-I Partial Correlation',
  268. 'partial_correlation_aparc_Tian_S1': 'aparc-I Partial Correlation',
  269. 'partial_correlation_Glasser_Tian_S1': 'Glasser-I Partial Correlation',
  270. 'partial_correlation_Glasser_Tian_S4': 'Glasser-IV Partial Correlation',
  271. 'partial_correlation_Schaefer7n200p_Tian_S1': 'Schaefer7n200p-I Partial Correlation',
  272. 'partial_correlation_Schaefer7n500p_Tian_S4': 'Schaefer7n500p-IV Partial Correlation',
  273. 'lifestyle-envir': 'Lifestyle & Environment',
  274. 'allmri': '3 Brain MRI Modalities Stacked',
  275. 'dwi': 'Brain dwMRI Stacked',
  276. 'smri': 'Brain sMRI Stacked',
  277. 'rs': 'Brain rsMRI Stacked',
  278. 'body': 'Body Physiology Stacked',
  279. 'body-comp': 'Body Composition Stacked',
  280. #'cardiopulmonary': 'Cardiopulmonary stacked',
  281. 'renal-hepatic': 'Renal & Hepatic Stacked',
  282. 'lifestyle-envir': 'Lifestyle & Environment',
  283. 'brain-plus-body': '3 Brain MRI Modalities & Body Stacked',
  284. 'brain-body': 'Brain & Body Stacked',
  285. 'body-only': 'Body Physiology and Composition Stacked'
  286. }
  287. # %%
  288. # Define brain and body modalities
  289. modalities_smri = [
  290. 'struct_fast',
  291. 'struct_sub_first',
  292. 'struct_fs_aseg_mean_intensity',
  293. 'struct_fs_aseg_volume',
  294. 'struct_ba_exvivo_area',
  295. 'struct_ba_exvivo_mean_thickness',
  296. 'struct_ba_exvivo_volume',
  297. 'struct_a2009s_area',
  298. 'struct_a2009s_mean_thickness',
  299. 'struct_a2009s_volume',
  300. 'struct_dkt_area',
  301. 'struct_dkt_mean_thickness',
  302. 'struct_dkt_volume',
  303. 'struct_desikan_gw',
  304. 'struct_desikan_pial',
  305. 'struct_desikan_white_area',
  306. 'struct_desikan_white_mean_thickness',
  307. 'struct_desikan_white_volume',
  308. 'struct_subsegmentation',
  309. 'add_t1',
  310. 'add_t2']
  311. modalities_dwmri = [
  312. "dwi_FA_tbss", "dwi_FA_prob",
  313. "dwi_MD_tbss", "dwi_MD_prob",
  314. "dwi_L1_tbss", "dwi_L1_prob",
  315. "dwi_L2_tbss", "dwi_L2_prob",
  316. "dwi_L3_tbss", "dwi_L3_prob",
  317. "dwi_MO_tbss", "dwi_MO_prob",
  318. "dwi_OD_tbss", "dwi_OD_prob",
  319. "dwi_ICVF_tbss", "dwi_ICVF_prob",
  320. "dwi_ISOVF_tbss", "dwi_ISOVF_prob",
  321. 'aparc_Tian_S1_FA_i2',
  322. 'aparc_Tian_S1_Length_i2',
  323. 'aparc_Tian_S1_SIFT2_FBC_i2',
  324. 'aparc_Tian_S1_Streamline_Count_i2',
  325. 'aparc_a2009s_Tian_S1_FA_i2',
  326. 'aparc_a2009s_Tian_S1_Length_i2',
  327. 'aparc_a2009s_Tian_S1_SIFT2_FBC_i2',
  328. 'aparc_a2009s_Tian_S1_Streamline_Count_i2',
  329. 'Glasser_Tian_S1_FA_i2',
  330. 'Glasser_Tian_S1_Length_i2',
  331. 'Glasser_Tian_S1_SIFT2_FBC_i2',
  332. 'Glasser_Tian_S1_Streamline_Count_i2',
  333. 'Glasser_Tian_S4_FA_i2',
  334. 'Glasser_Tian_S4_Length_i2',
  335. 'Glasser_Tian_S4_SIFT2_FBC_i2',
  336. 'Glasser_Tian_S4_Streamline_Count_i2',
  337. 'Schaefer7n200p_Tian_S1_FA_i2',
  338. 'Schaefer7n200p_Tian_S1_Length_i2',
  339. 'Schaefer7n200p_Tian_S1_SIFT2_FBC_i2',
  340. 'Schaefer7n200p_Tian_S1_Streamline_Count_i2',
  341. 'Schaefer7n1000p_Tian_S4_FA_i2',
  342. 'Schaefer7n1000p_Tian_S4_Length_i2',
  343. 'Schaefer7n1000p_Tian_S4_SIFT2_FBC_i2',
  344. 'Schaefer7n1000p_Tian_S4_Streamline_Count_i2']
  345. modalities_rsmri = [
  346. "amplitudes_21",
  347. "full_correlation_21",
  348. "partial_correlation_21",
  349. "amplitudes_55",
  350. "full_correlation_55",
  351. "partial_correlation_55",
  352. 'full_correlation_aparc_a2009s_Tian_S1',
  353. 'full_correlation_aparc_Tian_S1',
  354. 'full_correlation_Glasser_Tian_S1',
  355. 'full_correlation_Glasser_Tian_S4',
  356. 'full_correlation_Schaefer7n200p_Tian_S1',
  357. 'full_correlation_Schaefer7n500p_Tian_S4',
  358. 'partial_correlation_aparc_a2009s_Tian_S1',
  359. 'partial_correlation_aparc_Tian_S1',
  360. 'partial_correlation_Glasser_Tian_S1',
  361. 'partial_correlation_Glasser_Tian_S4',
  362. 'partial_correlation_Schaefer7n200p_Tian_S1',
  363. 'partial_correlation_Schaefer7n500p_Tian_S4'
  364. ]
  365. modalities_body = [
  366. 'immune',
  367. 'renalhepatic',
  368. 'metabolic',
  369. 'cardiopulmonary',
  370. 'musculoskeletal',
  371. 'bone_densitometry',
  372. 'pwa',
  373. 'heart_mri',
  374. 'carotid_ultrasound',
  375. 'arterial_stiffness',
  376. 'ecg_rest',
  377. 'body_composition_by_impedance',
  378. 'body_composition_dxa',
  379. 'bone_dxa',
  380. 'kidneys_mri',
  381. 'liver_mri',
  382. 'abdominal_composition_mri_18_vars', #17 vars
  383. 'abdominal_organ_composition_mri_13_vars', #12 vars
  384. 'hearing'
  385. ]
  386. # %%
  387. # Define figures path
  388. fig_path = '/UK_BB/brainbody/figures'
  389. # %%
  390. # Define a function to build circle legend handles
  391. from matplotlib.legend_handler import HandlerPatch
  392. def make_circle_legend(legend, orig_handle, xdescent, ydescent, width, height, fontsize):
  393. # Create a circle for the legend
  394. return plt.Circle((width/2, height/2), min(width, height))
  395. # %%
  396. # Define a function to save figure in multiple formats
  397. def save_fig(formats, path):
  398. for fmt in formats:
  399. plt.savefig(
  400. f'{path}.{fmt}',
  401. bbox_inches="tight",
  402. pad_inches=1,
  403. transparent=False,
  404. facecolor="w",
  405. edgecolor='w',
  406. orientation='landscape',
  407. format=fmt,
  408. dpi=300
  409. )
  410. # %% [markdown]
  411. # # Figure 1
  412. # %% [markdown]
  413. # g-factor loading plot
  414. # %%
  415. # Loading plot
  416. path = "/UK_BB/brainbody/cognition/code/efa_loadings/efa_loadings_renamed.xlsx"
  417. df = pd.read_excel(path, sheet_name="Group_0")
  418. # Extract variable names
  419. variables = df["Variable"].astype(str).tolist()
  420. # Extract the four components
  421. components = ["PA1", "PA2", "PA3", "PA4"]
  422. title_map = {'PA1': 'Factor 1',
  423. 'PA2': 'Factor 2',
  424. 'PA3': 'Factor 3',
  425. 'PA4': 'Factor 4'}
  426. label_map = {
  427. 'Numeric memory: Maximum digits remembered correctly': 'Numeric memory: Max digits correct',
  428. 'Prospective memory: Initial answer': 'Prospective memory: Initial',
  429. 'Matrix pattern completion: Number of puzzles correct': 'Matrix pattern: Number correct',
  430. 'Fluid intelligence score': 'FIS',
  431. 'PAL: Number of correct pairs': 'PAL: Pairs correct',
  432. 'Tower rearranging: Number of puzzles correct': 'Tower rearranging: Puzzles correct',
  433. 'Picture vocabulary: Specific cognitive ability': 'Picture vocab.: Specific cog. ability',
  434. 'Symbol digit substitution: Proportion of correct matches': 'SDS: Proportion correct',
  435. '(log)TMT: Duration to complete numeric path': '(log)TMT: Duration numeric',
  436. '(log)TMT: Duration to complete alphabetic path': '(log)TMT: Duration alphabetic',
  437. '(log)Reaction time': '(log)RT',
  438. '(logx+1)Pairs matching: Incorrect matches': '(logx+1)Pairs matching: Incorrect'
  439. }
  440. renamed_variables = [label_map.get(v, v) for v in variables]
  441. # 2x2 subplots
  442. fig, axes = plt.subplots(2, 2, figsize=(6, 8), sharey=True)
  443. # Flatten axes so we can iterate cleanly
  444. axes = axes.flatten()
  445. for ax, comp in zip(axes, components):
  446. loadings = df[comp].astype(float).tolist()
  447. colors = ["#4A7169" if v >= 0 else "#A8554E" for v in loadings]
  448. ax.barh(variables, loadings, color=colors)
  449. ax.axvline(0, color="black", linewidth=1)
  450. all_loadings = pd.concat([df[c] for c in components])
  451. xmin = all_loadings.min()
  452. xmax = all_loadings.max()
  453. ax.set_xlim(xmin, xmax)
  454. ax.xaxis.set_major_locator(MultipleLocator(0.2))
  455. ax.set_title(title_map[comp], fontsize=22)
  456. ax.set_yticks(range(len(variables)))
  457. # Position tick labels
  458. if ax in axes:
  459. ax.yaxis.set_ticks_position('left')
  460. ax.tick_params(length=3)
  461. ax.tick_params(axis='y', length=2)
  462. # Set y-tick labels to show 1-12 from top to bottom
  463. ax.set_yticklabels(range(12, 0, -1), fontsize=17)
  464. ax.tick_params(axis='x', labelsize=18)
  465. ax.set_xlabel("Loading", fontsize=19)
  466. plt.tight_layout()
  467. plt.savefig(os.path.join(fig_path, 'fig1-prep/Fig1b_g_loadings.svg'),
  468. bbox_inches="tight",
  469. transparent=False,
  470. facecolor="w",
  471. edgecolor='w',
  472. orientation='landscape',
  473. format='svg',
  474. dpi=300)
  475. plt.show()
  476. # Print variable names in the order they appear in the plot (top to bottom)
  477. print("Variables in plot:")
  478. print("=" * 60)
  479. for i, var in enumerate(reversed(renamed_variables), 1):
  480. print(f"{13-i}. {var}")
  481. # %% [markdown]
  482. # ## Figure 2
  483. # %% [markdown]
  484. # Body performance
  485. # %%
  486. # KDE plot: Body composition by impedance vs composite body + Ridge plots for body performance
  487. # First row: KDE plot (1/3 height), Second row: violin plots (2/3 height)
  488. warnings.simplefilter(action='ignore', category=FutureWarning)
  489. fig = plt.figure(figsize=(14, 15))
  490. gs = fig.add_gridspec(2, 1, height_ratios=[1, 2], hspace=0.5)
  491. # Top: KDE
  492. ax_kde = fig.add_subplot(gs[0])
  493. # KDE plot data preparation
  494. body_results_dir = '/UK_BB/brainbody/bootstrap/body/results'
  495. file_impedance = 'body_composition_by_impedance_bootstrapped.csv'
  496. modality_impedance = 'body_composition_by_impedance'
  497. body_stacked_results_dir = '/UK_BB/brainbody/bootstrap/stack/scaled/results'
  498. file_stack = 'body_bootstrapped_scaled.csv'
  499. modality_stack = 'body'
  500. df_impedance = pd.read_csv(os.path.join(body_results_dir, file_impedance))
  501. df_stack = pd.read_csv(os.path.join(body_stacked_results_dir, file_stack))
  502. # Extract r values
  503. col_impedance = f'{modality_impedance}_r'
  504. col_stack = f'{modality_stack}_r'
  505. body_data = []
  506. if col_impedance in df_impedance.columns:
  507. r_vals = pd.to_numeric(df_impedance[col_impedance], errors='coerce').dropna()
  508. body_data.extend([{'modality': modality_impedance, 'r': r} for r in r_vals])
  509. if col_stack in df_stack.columns:
  510. r_vals_stack = pd.to_numeric(df_stack[col_stack], errors='coerce').dropna()
  511. body_data.extend([{'modality': modality_stack, 'r': r} for r in r_vals_stack])
  512. # Create DataFrame
  513. body_df = pd.DataFrame(body_data)
  514. # Compute medians
  515. median_impedance = body_df[body_df['modality'] == modality_impedance]['r'].median()
  516. median_stack = body_df[body_df['modality'] == modality_stack]['r'].median()
  517. # Plot KDE
  518. kde_palette = {
  519. 'body_composition_by_impedance': 'lightcoral',
  520. 'body': '#803342FF'
  521. }
  522. sns.kdeplot(
  523. data=body_df, x="r", hue="modality",
  524. fill=True, common_norm=False, palette=kde_palette,
  525. alpha=.5, linewidth=1, ax=ax_kde
  526. )
  527. # Get KDE heights at median positions
  528. r_impedance_vals = body_df[body_df['modality'] == modality_impedance]['r'].dropna()
  529. r_stack_vals = body_df[body_df['modality'] == modality_stack]['r'].dropna()
  530. kde_impedance = gaussian_kde(r_impedance_vals)
  531. kde_stack = gaussian_kde(r_stack_vals)
  532. # Evaluate KDE height at the median
  533. y_impedance = kde_impedance(median_impedance)[0]
  534. y_stack = kde_stack(median_stack)[0]
  535. # Draw short vertical lines within KDE height
  536. ax_kde.plot([median_impedance, median_impedance], [0, y_impedance],
  537. color='lightcoral', linestyle='--', linewidth=1)
  538. ax_kde.plot([median_stack, median_stack], [0, y_stack],
  539. color='#803342FF', linestyle='--', linewidth=1)
  540. # Get current y-axis upper limit
  541. y_max = ax_kde.get_ylim()[1]
  542. x_offset = 0.0
  543. ax_kde.text(median_impedance + x_offset, y_max * 0.99, f'{median_impedance:.2f}',
  544. color='black', fontsize=25, ha='center')
  545. ax_kde.text(median_stack + x_offset, y_max * 0.99, f'{median_stack:.2f}',
  546. color='black', fontsize=25, ha='center')
  547. sns.despine(left=True, ax=ax_kde)
  548. ax_kde.set_title("Bootstrap Distribution of Predictive Performance:\nTop Body Phenotype vs Composite Body Marker",
  549. fontsize=30, y=1.1, pad=20)
  550. # Add letter 'a' to top-left of KDE plot
  551. ax_kde.text(-0.1, 1.47, 'a', transform=ax_kde.transAxes,
  552. fontsize=40, va='top', ha='right') #fontweight='bold',
  553. ax_kde.set_xlabel("Pearson $r$", fontsize=27, labelpad=30)
  554. ax_kde.set_ylabel("Density", fontsize=25)
  555. # Adjust ticks
  556. r_min, r_max = body_df['r'].min(), body_df['r'].max()
  557. ax_kde.set_xticks(np.arange(round(r_min, 2), round(r_max + 0.01, 2), 0.01))
  558. ax_kde.tick_params(axis='x', labelsize=25)
  559. ax_kde.tick_params(axis='y', labelsize=25)
  560. # Remove legend
  561. ax_kde.legend_.remove()
  562. # Add labels under KDE distributions
  563. # Position labels at the bottom of the plot
  564. y_pos = -ax_kde.get_ylim()[1] * 0.15 # Position below x-axis
  565. # Add label for left distribution (body composition by impedance)
  566. ax_kde.text(median_impedance, y_pos, "Body composition by impedance", color ='lightcoral',
  567. fontsize=25, ha='center', va='top',
  568. transform=ax_kde.transData)
  569. # Add label for right distribution (composite body marker)
  570. ax_kde.text(median_stack, y_pos, "Composite body marker", color ='#803342FF',
  571. fontsize=25, ha='center', va='top',
  572. transform=ax_kde.transData)
  573. # Bottom: Violins
  574. # Create two subplots for the bottom row
  575. gs_bottom = gs[1].subgridspec(1, 2, width_ratios=[1, 1], wspace=0.15)
  576. ax1 = fig.add_subplot(gs_bottom[0]) # Left violin plot
  577. ax2 = fig.add_subplot(gs_bottom[1]) # Right violin plot
  578. # Styling patterns for violin plots
  579. violin_linewidth = 1
  580. violin_cut = 0
  581. violin_width = 0.8
  582. median_marker_size = 8
  583. median_color_left = '#A73933FF'
  584. median_color_right = '#53354DFF'
  585. violin_color_left = 'lightcoral'
  586. violin_color_right = '#DBC3D6FF'
  587. # Left: Individual body performance
  588. results_dir = '/UK_BB/brainbody/bootstrap/body/results'
  589. # Load files for left plot
  590. files = sorted([f for f in os.listdir(results_dir) if f.endswith('_bootstrapped.csv')])
  591. data_left = []
  592. for file in files:
  593. modality = file.replace('_bootstrapped.csv', '')
  594. df = pd.read_csv(os.path.join(results_dir, file))
  595. col = f'{modality}_r'
  596. if col in df.columns:
  597. r_vals = pd.to_numeric(df[col], errors='coerce').dropna()
  598. data_left.extend([{'modality': modality, 'r': r} for r in r_vals])
  599. else:
  600. print(f'Warning: expected column "{col}" not found in "{file}". Skipping.')
  601. df_left = pd.DataFrame(data_left)
  602. if not df_left.empty:
  603. # Compute median r per modality and sort descending
  604. medians_left = df_left.groupby('modality')['r'].median().sort_values(ascending=False)
  605. order_left = medians_left.index.tolist()
  606. # Determine display names for y-ticks via modality_map
  607. if 'modality_map' in globals() and isinstance(modality_map, dict):
  608. yticklabels_left = [modality_map.get(m, m) for m in order_left]
  609. else:
  610. yticklabels_left = order_left
  611. # Create left plot with consistent styling
  612. sns.violinplot(
  613. data=df_left,
  614. x='r', y='modality',
  615. order=order_left,
  616. ax=ax1,
  617. color=violin_color_left,
  618. linewidth=violin_linewidth,
  619. cut=violin_cut,
  620. inner=None,
  621. width=violin_width
  622. )
  623. # Add median markers for all modalities
  624. for j, modality in enumerate(order_left):
  625. median_val = medians_left[modality]
  626. ax1.scatter(median_val, j, color=median_color_left, s=median_marker_size, zorder=5)
  627. # Style left axes
  628. x_tick_fontsize = 25
  629. xlabel_fontsize = 27
  630. suptitle_fontsize = 28
  631. for spine in ax1.spines.values():
  632. spine.set_visible(True)
  633. # x ticks
  634. #ax1.xaxis.set_major_locator(MultipleLocator(0.1))
  635. ax1.set_xlim(left=0)
  636. ax1.tick_params(axis='both', which='both', length=4)
  637. ax1.tick_params(axis='x', labelsize=x_tick_fontsize)
  638. ax1.set_xlabel('Pearson $r$', fontsize=xlabel_fontsize, labelpad=10)
  639. x_min1, x_max1 = ax1.get_xlim()
  640. x_min1 = round(x_min1, 2)
  641. x_max1 = round(x_max1, 2)
  642. # Create fixed tick positions: min, min+0.1, min+0.2, max
  643. tick_positions = [0, x_min1 + 0.1, x_min1 + 0.2, x_min1 + 0.3, x_max1]
  644. # Set fixed locator
  645. from matplotlib.ticker import FixedLocator
  646. ax1.xaxis.set_major_locator(FixedLocator(tick_positions))
  647. # y ticks
  648. ax1.set_ylabel('')
  649. ax1.set_yticks(range(len(order_left)))
  650. ax1.set_yticklabels(yticklabels_left, fontsize=22)
  651. ax1.set_title('Individual\nBody Phenotype Performance', fontsize=suptitle_fontsize, pad=10)
  652. # Add letter 'b' to top-left of left violin plot
  653. ax1.text(-0.2, 1.15, 'b', transform=ax1.transAxes,
  654. fontsize=40, va='top', ha='right') #fontweight='bold',
  655. # Add CIs for left plot
  656. performance_bootstrapping_path = '/UK_BB/brainbody/bootstrap/body'
  657. performance_results_dir = os.path.join(performance_bootstrapping_path, 'results')
  658. ci_file = os.path.join(performance_results_dir, 'combined_bootstrap_CI_results.csv')
  659. if os.path.exists(ci_file):
  660. ci_df = pd.read_csv(ci_file)
  661. ci_df_r = ci_df[ci_df['Metric'] == 'r'] # Only get r values
  662. for j, modality in enumerate(order_left):
  663. if modality in ci_df_r['Modality'].values:
  664. ci_row = ci_df_r[ci_df_r['Modality'] == modality].iloc[0]
  665. lower_ci = ci_row['CI_lower']
  666. upper_ci = ci_row['CI_upper']
  667. # Add horizontal error bars at the bottom of each violin
  668. ax1.hlines(y=j + 0.2, xmin=lower_ci, xmax=upper_ci,
  669. color='black', linewidth=1, zorder=6)
  670. # Add vertical dashes for CI endpoints
  671. ax1.vlines([lower_ci, upper_ci], ymin=j + 0.1, ymax=j + 0.3,
  672. color='black', linewidth=1, zorder=6)
  673. # Clip violins to show only the TOP half for left plot
  674. xlim_left = ax1.get_xlim()
  675. violin_collections_left = [c for c in ax1.collections if isinstance(c, PolyCollection)]
  676. violin_collections_left = violin_collections_left[:len(order_left)]
  677. for j, coll in enumerate(violin_collections_left):
  678. rect = patches.Rectangle(
  679. (xlim_left[0], j - 0.4),
  680. xlim_left[1] - xlim_left[0],
  681. 0.4 + 1e-9,
  682. transform=ax1.transData,
  683. facecolor='none',
  684. edgecolor='none',
  685. linewidth=0
  686. )
  687. rect.set_visible(False)
  688. ax1.add_patch(rect)
  689. coll.set_clip_path(rect)
  690. # Right: Delta (Body vs Top Body)
  691. bootstrapping_path = '/UK_BB/brainbody/bootstrap/body'
  692. delta_results_dir = os.path.join(bootstrapping_path, 'delta_results')
  693. # Load delta results for right plot
  694. delta_dfs = {}
  695. if os.path.exists(delta_results_dir):
  696. delta_files = [f for f in os.listdir(delta_results_dir) if f.startswith('delta_') and f.endswith('.csv')]
  697. for f in delta_files:
  698. delta_dfs[f.replace('.csv', '')] = pd.read_csv(os.path.join(delta_results_dir, f))
  699. # Prepare data for right plot
  700. metric = 'delta_r'
  701. all_data_right = []
  702. for mod_name, df in delta_dfs.items():
  703. if metric in df.columns:
  704. comparison_name = mod_name.replace('delta_', '').replace('_vs_body_composition_by_impedance', '')
  705. comparison_name = modality_map.get(comparison_name, comparison_name)
  706. for val in df[metric]:
  707. all_data_right.append({'Comparison': comparison_name, 'Value': val})
  708. if all_data_right:
  709. plot_df = pd.DataFrame(all_data_right)
  710. sorted_order_right = plot_df.groupby('Comparison')['Value'].median().sort_values().index
  711. medians_right = plot_df.groupby('Comparison')['Value'].median().sort_values(ascending=False)
  712. # Create right plot
  713. sns.violinplot(
  714. data=plot_df,
  715. x='Value',
  716. y='Comparison',
  717. order=sorted_order_right,
  718. ax=ax2,
  719. color=violin_color_right,
  720. linewidth=violin_linewidth,
  721. cut=violin_cut,
  722. inner=None,
  723. width=violin_width
  724. )
  725. # Add median markers
  726. for j, modality in enumerate(sorted_order_right):
  727. median_val = medians_right[modality]
  728. ax2.scatter(median_val, j, color=median_color_right, s=median_marker_size, zorder=5)
  729. ax2.axvline(x=0, color='#A50026FF', linestyle='--', linewidth=0.7)
  730. ax2.set_ylabel('')
  731. # x ticks
  732. ax2.set_xlabel('Δ Pearson $r$', fontsize=xlabel_fontsize, labelpad=10)
  733. #ax2.xaxis.set_major_locator(MultipleLocator(0.1))
  734. #ax2.set_xlim(left=0)
  735. ax2.tick_params(axis='both', which='both', length=4)
  736. ax2.tick_params(axis='x', labelsize=x_tick_fontsize)
  737. x_min2, x_max2 = ax2.get_xlim()
  738. x_min2 = round(x_min2, 2)
  739. x_max2 = round(x_max2, 2)
  740. # Create fixed tick positions: min, min+0.1, min+0.2, max
  741. tick_positions = tick_positions = [x_min2, # show x_min
  742. x_min2 + 0.1,
  743. x_min2 + 0.2,
  744. x_min2 + 0.3,
  745. round(x_max2, 2)]
  746. # Set fixed locator
  747. from matplotlib.ticker import FixedLocator
  748. #ax2.xaxis.set_major_locator(FixedLocator(tick_positions))
  749. def format_tick(x, pos):
  750. if abs(x) < 1e-6:
  751. return "0.00"
  752. return f"{x:.2f}"
  753. from matplotlib.ticker import FixedLocator, FuncFormatter
  754. ax2.xaxis.set_major_formatter(FuncFormatter(format_tick))
  755. ax2.tick_params(axis='x', labelsize=x_tick_fontsize)
  756. ax2.set_title('Comparative\nPerformance', fontsize=suptitle_fontsize, pad=10)
  757. # Add CIs for right plot
  758. delta_ci_file = os.path.join(delta_results_dir, 'delta_CI_results.csv')
  759. if os.path.exists(delta_ci_file):
  760. delta_ci_df = pd.read_csv(delta_ci_file)
  761. delta_ci_df_r = delta_ci_df[delta_ci_df['Metric'] == 'r'].reset_index(drop=True)
  762. # Create a reverse mapping from display names to original names
  763. reverse_modality_map = {v: k for k, v in modality_map.items()}
  764. for j, modality in enumerate(sorted_order_right):
  765. original_modality_name = reverse_modality_map.get(modality, modality)
  766. ci_modality_name = f"{original_modality_name} vs body_composition_by_impedance"
  767. if ci_modality_name in delta_ci_df_r['Modality'].values:
  768. ci_row = delta_ci_df_r[delta_ci_df_r['Modality'] == ci_modality_name].iloc[0]
  769. lower_ci = ci_row['CI_lower']
  770. upper_ci = ci_row['CI_upper']
  771. # Add horizontal error bars at the bottom of each violin
  772. ax2.hlines(y=j - 0.2, xmin=lower_ci, xmax=upper_ci,
  773. color='black', linewidth=1, zorder=6)
  774. # Add vertical dashes for CI endpoints
  775. ax2.vlines([lower_ci, upper_ci], ymin=j - 0.3, ymax=j - 0.1,
  776. color='black', linewidth=1, zorder=6)
  777. else:
  778. print(f"NOT found: {ci_modality_name}")
  779. # Remove y-axis labels on the right plot but keep ticks
  780. ax2.set_yticks(range(len(sorted_order_right)))
  781. ax2.set_yticklabels([''] * len(sorted_order_right))
  782. # Control plot margins
  783. n_categories = len(sorted_order_right)
  784. ax2.set_ylim(-0.9, n_categories)
  785. # Clip violins for right plot
  786. xlim_right = ax2.get_xlim()
  787. violin_collections_right = [c for c in ax2.collections if isinstance(c, PolyCollection)]
  788. violin_collections_right = violin_collections_right[:len(sorted_order_right)]
  789. for j, coll in enumerate(violin_collections_right):
  790. rect = patches.Rectangle(
  791. (xlim_right[0], j),
  792. xlim_right[1] - xlim_right[0],
  793. 0.4 + 1e-9,
  794. transform=ax2.transData,
  795. facecolor='none',
  796. edgecolor='none',
  797. linewidth=0
  798. )
  799. rect.set_visible(False)
  800. ax2.add_patch(rect)
  801. coll.set_clip_path(rect)
  802. # Adjust y-axis limits for violin plots to be consistent
  803. max_categories = max(len(order_left), len(sorted_order_right))
  804. ax1.set_ylim(-0.5, max_categories - 0.5)
  805. ax2.set_ylim(-0.5, max_categories - 0.5)
  806. # Adjust y-ticks
  807. ax1.set_yticks(range(max_categories))
  808. ax2.set_yticks(range(max_categories))
  809. ax1.invert_yaxis()
  810. save_fig(['png', 'svg'], os.path.join(fig_path, 'final/Fig2'))
  811. plt.show()
  812. # %% [markdown]
  813. # %% [markdown]
  814. # # Figure 3
  815. # %% [markdown]
  816. # ## Figure 3a
  817. # %% [markdown]
  818. # Pearson *r* colorbar
  819. # %%
  820. # Normalize correlation values (0 to 1)
  821. norm = colors.Normalize(vmin=0, vmax=1)
  822. # Create a colormap that uses only the positive (blue) part of RdBu
  823. # RdBu goes from red (0.0) to white (0.5) to blue (1.0)
  824. positive_cmap = plt.colormaps['RdBu_r'].resampled(256)# Get the full colormap
  825. # Extract only the second half
  826. red_part = positive_cmap(np.linspace(0.5, 1, 256))
  827. positive_cmap = colors.LinearSegmentedColormap.from_list('positive_rdbu', red_part)
  828. # 1. Generate colorbar
  829. fig, ax = plt.subplots(figsize=(6, 1))
  830. fig.subplots_adjust(bottom=0.6)
  831. from matplotlib.colorbar import ColorbarBase
  832. cb1 = ColorbarBase(ax, cmap=positive_cmap, orientation='horizontal') #norm=norm,
  833. ticks = np.arange(0, 1.1, 0.2)
  834. cb1.set_ticks(ticks)
  835. cb1.set_ticklabels([f'{tick:.1f}' for tick in ticks])
  836. cb1.ax.tick_params(labelsize=25)
  837. cb1.set_label('Pearson $r$', fontsize=30)
  838. plt.savefig(os.path.join(fig_path, 'colorbar_horizontal.svg'), dpi=300)
  839. # %% [markdown]
  840. # Correlation dots
  841. # %%
  842. # Generate dot images
  843. base_path = '/UK_BB/brainbody'
  844. corr = pd.read_csv(os.path.join(base_path, 'feature_imp', 'feature_imp_body', 'combined', 'g_pred_from_body_mod_g_pred_stack_correlations.csv'))
  845. for index, row in corr.iterrows():
  846. modality = row['Modality']
  847. r_value = row['Pearson r']
  848. # Use positive colormap
  849. color = positive_cmap(norm(r_value))
  850. fig, ax = plt.subplots(figsize=(1, 1))
  851. ax.add_patch(plt.Circle((0.5, 0.5), 0.4, color=color))
  852. ax.set_xlim(0, 1)
  853. ax.set_ylim(0, 1)
  854. ax.axis('off')
  855. # Clean filename
  856. filename = modality.lower().replace(' ', '_').replace('&', 'and').replace('(', '').replace(')', '').replace(',', '').replace('/', '_')
  857. filepath = os.path.join(fig_path, 'preps', 'body_performance', 'colorbar', 'positive', f'{filename}.png')
  858. plt.savefig(filepath, dpi=300, bbox_inches='tight', pad_inches=0, transparent=True)
  859. plt.close()
  860. # %% [markdown]
  861. # ## Figure 3b
  862. # %% [markdown]
  863. # Scatterplots of predicted vs observed g
  864. # %%
  865. # Scatterplots of predicted vs observed g: Config
  866. bootstrapping_path = '/UK_BB/brainbody/bootstrap/stack'
  867. modalities_to_plot = ['body', 'allmri', 'dwi', 'rs', 'smri', 'brain-body']
  868. folds = range(0,5)
  869. base_path = '/UK_BB/brainbody'
  870. #base_path = 'Z:/IBu/UK_BB/brainbody'
  871. dticmap = sns.light_palette("#0d648f", as_cmap=True)
  872. rscmap = sns.light_palette("#4B6F5A99", as_cmap=True)
  873. t1cmap = sns.light_palette("#f8b976", as_cmap=True)
  874. stackcmap = sns.light_palette("#A73933FF", as_cmap=True)
  875. bodycmap = sns.light_palette("#871C0FFF", as_cmap=True)
  876. allmricmap = sns.light_palette("#6A659999", as_cmap=True)
  877. modality_cmaps = {
  878. 'dwi': dticmap,
  879. 'rs': rscmap,
  880. 'smri': t1cmap,
  881. 'brain-body': stackcmap,
  882. 'body': bodycmap,
  883. 'allmri': allmricmap
  884. }
  885. modality_map = {
  886. 'body': 'Composite Body',
  887. 'allmri': 'Composite Brain',
  888. 'dwi': 'dwMRI',
  889. 'rs': 'rsMRI',
  890. 'smri': 'sMRI',
  891. 'brain-body': 'Whole-Body'
  892. }
  893. # -----------------------
  894. # Load precomputed performance metrics AND bootstrap confidence intervals
  895. brain_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_brain_stack.xlsx'))
  896. body_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_body_stack_outer.xlsx'))
  897. brain_body_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_brain_and_body_stack_outer.xlsx'))
  898. # Load bootstrap confidence intervals
  899. bootstrap_ci_path = '/UK_BB/brainbody/bootstrap/stack/scaled/results/combined_bootstrap_CI_results_stacked_sorted_scaled.xlsx'
  900. bootstrap_df = pd.read_excel(bootstrap_ci_path)
  901. # Combine into one master dataframe
  902. all_metrics_df = pd.concat([brain_metrics, body_metrics, brain_body_metrics], ignore_index=True)
  903. # Create comprehensive annotation dictionary with bootstrap CIs
  904. annotation_metrics = {}
  905. for _, row in all_metrics_df.iterrows():
  906. # Standardize modality names
  907. modality_key = row['Modality'].lower()
  908. # Replace specific patterns
  909. modality_key = modality_key.replace('3 brain mri modalities', 'allmri')
  910. modality_key = modality_key.replace('body physiology', 'body')
  911. modality_key = modality_key.replace('brain mri and body physiology', 'brain-body')
  912. modality_key = modality_key.replace('brain mri and body', 'brain-body')
  913. modality_key = modality_key.replace('dwmri', 'dwi')
  914. modality_key = modality_key.replace('rsmri', 'rs')
  915. modality_key = modality_key.replace('smri', 'smri')
  916. modality_key = modality_key.replace(' ', '-')
  917. # Store the main metrics
  918. annotation_metrics[modality_key] = {
  919. 'r_value': row['Test Pearson r'],
  920. 'r2_value': row['Test R2'],
  921. 'r_ci_lower': None,
  922. 'r_ci_upper': None,
  923. 'r2_ci_lower': None,
  924. 'r2_ci_upper': None
  925. }
  926. # Add bootstrap confidence intervals
  927. for _, row in bootstrap_df.iterrows():
  928. if row['Metric'] in ['R2', 'r']:
  929. # Standardize modality names to match
  930. modality_key = row['Modality'].lower()
  931. modality_key = modality_key.replace('3 brain mri modalities', 'allmri')
  932. modality_key = modality_key.replace('body physiology', 'body')
  933. modality_key = modality_key.replace('brain mri and body physiology', 'brain-body')
  934. modality_key = modality_key.replace('brain mri and body', 'brain-body')
  935. modality_key = modality_key.replace('dwmri', 'dwi')
  936. modality_key = modality_key.replace('rsmri', 'rs')
  937. modality_key = modality_key.replace('smri', 'smri')
  938. modality_key = modality_key.replace(' ', '-')
  939. if modality_key in annotation_metrics:
  940. if row['Metric'] == 'R2':
  941. annotation_metrics[modality_key]['r2_ci_lower'] = row['CI_lower']
  942. annotation_metrics[modality_key]['r2_ci_upper'] = row['CI_upper']
  943. elif row['Metric'] == 'r':
  944. annotation_metrics[modality_key]['r_ci_lower'] = row['CI_lower']
  945. annotation_metrics[modality_key]['r_ci_upper'] = row['CI_upper']
  946. print("Loaded annotation metrics for modalities:")
  947. for modality, metrics in annotation_metrics.items():
  948. print(f" {modality}: r={metrics['r_value']:.3f}, R²={metrics['r2_value']:.3f}")
  949. if metrics['r_ci_lower'] is not None:
  950. print(f" r CI: [{metrics['r_ci_lower']:.3f}, {metrics['r_ci_upper']:.3f}]")
  951. if metrics['r2_ci_lower'] is not None:
  952. print(f" R² CI: [{metrics['r2_ci_lower']:.3f}, {metrics['r2_ci_upper']:.3f}]")
  953. # %%
  954. # Plotting function
  955. def plot_modality_scatter(
  956. df, modality, save_dir=None,
  957. cmap_name='viridis', edge_color='white',
  958. marker_size=17, marker_alpha=0.6, line_lw=1.2, marker_linewidth=0.3,
  959. title_fontsize=30, label_fontsize=35, tick_fontsize=30,
  960. annotation_fontsize=None,
  961. add_grid=False,
  962. size=(6, 6)
  963. ):
  964. # Column names
  965. x_col = 'g_pred_stack_test'
  966. y_col = 'g_obs_test'
  967. for col in (x_col, y_col):
  968. if col not in df.columns:
  969. raise ValueError(f"Required column '{col}' not found in data for {modality}. "
  970. f"Available columns: {df.columns.tolist()}")
  971. # Compute distance for color mapping (around the mean point)
  972. x = pd.to_numeric(df[x_col], errors='coerce').astype(float)
  973. y = pd.to_numeric(df[y_col], errors='coerce').astype(float)
  974. x_mean, y_mean = x.mean(), y.mean()
  975. dist_i = np.sqrt((y - y_mean)**2 + (x - x_mean)**2)
  976. # Prepare figure/axes
  977. fig, ax = plt.subplots(figsize=size)
  978. # Scatter with colormap (use matplotlib scatter for 'c' + 'cmap' support)
  979. sc = ax.scatter(
  980. x, y, c=dist_i, cmap=cmap_name,
  981. s=marker_size, alpha=marker_alpha,
  982. edgecolors=edge_color, linewidth=marker_linewidth
  983. )
  984. # Global regression line
  985. mask = np.isfinite(x) & np.isfinite(y)
  986. if mask.sum() >= 2:
  987. z = np.polyfit(x[mask], y[mask], 1)
  988. p = np.poly1d(z)
  989. # Sort x for a clean line
  990. xs = np.linspace(x[mask].min(), x[mask].max(), 200)
  991. ax.plot(xs, p(xs), alpha=0.8, linewidth=line_lw, color='#BE4A47FF', linestyle='-')
  992. # Initialize annotation variables with default values
  993. r_value = None
  994. r2_value = None
  995. r_ci_lower = None
  996. r_ci_upper = None
  997. r2_ci_lower = None
  998. r2_ci_upper = None
  999. annotation_text = "Default"
  1000. # Get annotation metrics from the pre-loaded dictionary
  1001. if modality in annotation_metrics:
  1002. metrics = annotation_metrics[modality]
  1003. r_value = metrics['r_value']
  1004. r2_value = metrics['r2_value']
  1005. r_ci_lower = metrics['r_ci_lower']
  1006. r_ci_upper = metrics['r_ci_upper']
  1007. r2_ci_lower = metrics['r2_ci_lower']
  1008. r2_ci_upper = metrics['r2_ci_upper']
  1009. # Create annotation text with confidence intervals
  1010. if r_ci_lower is not None and r2_ci_lower is not None:
  1011. annotation_text = (f'r = {r_value:.2f} [{r_ci_lower:.2f}, {r_ci_upper:.2f}]\n'
  1012. f'R² = {r2_value:.2f} [{r2_ci_lower:.2f}, {r2_ci_upper:.2f}]')
  1013. else:
  1014. # Fallback if CIs not available
  1015. annotation_text = f'r = {r_value:.2f}\nR² = {r2_value:.2f}'
  1016. else:
  1017. print(f"Warning: No metrics found for modality '{modality}'")
  1018. # Create annotation lines with proper error handling
  1019. annotation_lines = []
  1020. if r_value is not None and r_ci_lower is not None:
  1021. #annotation_lines.append(rf'$r$ = {r_value:.2f}, 95% CI [{r_ci_lower:.2f}, {r_ci_upper:.2f}]')
  1022. annotation_lines.append(rf'$r$ = {r_value:.2f}')
  1023. annotation_lines.append(rf'95% CI [{r_ci_lower:.2f}, {r_ci_upper:.2f}]')
  1024. elif r_value is not None:
  1025. annotation_lines.append(rf'$r$ = {r_value:.2f}')
  1026. # Aesthetics
  1027. sns.despine(top=True, right=True, ax=ax)
  1028. ax.set_xlabel('Predicted $g$-factor ($z$)', fontsize=label_fontsize)
  1029. ax.set_ylabel('$g$-factor derived from ESEM ($z$)', fontsize=label_fontsize)
  1030. ax.tick_params(axis='x', labelsize=tick_fontsize)
  1031. ax.tick_params(axis='y', labelsize=tick_fontsize)
  1032. # Title
  1033. display_name = modality_map.get(modality, modality) if 'modality_map' in globals() else modality
  1034. ax.set_title(display_name, fontsize=title_fontsize, y=1.14)
  1035. # Side annotation (top-left inside axes) - Only add if we have metrics
  1036. if annotation_lines:
  1037. ax.text(
  1038. 0.05, 0.9, "\n".join(annotation_lines),
  1039. transform=ax.transAxes, fontsize=annotation_fontsize,
  1040. va='bottom', ha='left'
  1041. )
  1042. # Major tick spacing
  1043. ax.xaxis.set_major_locator(MultipleLocator(1))
  1044. ax.yaxis.set_major_locator(MultipleLocator(1.5))
  1045. ax.set_xlim(-1.6, 1.5)
  1046. ax.set_ylim(-3.5, 3)
  1047. if add_grid:
  1048. ax.grid(True, alpha=0.25)
  1049. # Legend (only for lines)
  1050. handles, labels = ax.get_legend_handles_labels()
  1051. if handles:
  1052. ax.legend(loc='lower right', fontsize=12)
  1053. # Save and close
  1054. plt.tight_layout()
  1055. if save_dir is not None:
  1056. out_path = os.path.join(save_dir, f'scatter_{modality}_vs_observed.png')
  1057. plt.savefig(out_path, dpi=300, bbox_inches='tight')
  1058. plt.show()
  1059. plt.close()
  1060. # %%
  1061. # Plot scatterplots
  1062. modality_map = {
  1063. 'body': 'Composite Body',
  1064. 'allmri': 'Composite Brain',
  1065. 'dwi': 'dwMRI',
  1066. 'rs': 'rsMRI',
  1067. 'smri': 'sMRI',
  1068. 'brain-body': 'Whole-Body'
  1069. }
  1070. # Set up the save directory for plots
  1071. scatter_save_dir = '/UK_BB/brainbody/figures'
  1072. # Set annotation fontsize
  1073. annotation_fontsize = 25
  1074. # Process and plot each modality
  1075. for modality in modalities_to_plot:
  1076. print(f"\nGenerating scatter plot for {modality}...")
  1077. try:
  1078. # Load the combined test data for this modality
  1079. input_path = os.path.join(bootstrapping_path, 'scaled', f'{modality}_folds_combined_test.csv')
  1080. df = pd.read_csv(input_path)
  1081. print(f"Data shape for {modality}: {df.shape}")
  1082. print(f"Columns: {df.columns.tolist()}")
  1083. # Check if required columns exist
  1084. if 'g_obs_test' not in df.columns or 'g_pred_stack_test' not in df.columns:
  1085. print(f"Warning: Required columns not found for {modality}. Skipping.")
  1086. continue
  1087. # Get the appropriate colormap
  1088. cmap = modality_cmaps.get(modality, 'viridis')
  1089. # Create the scatter plot
  1090. plot_modality_scatter(
  1091. df=df,
  1092. modality=modality,
  1093. save_dir=scatter_save_dir,
  1094. cmap_name=cmap,
  1095. edge_color='black',
  1096. marker_size=50,
  1097. marker_alpha=0.9,
  1098. marker_linewidth=0.3,
  1099. line_lw=1.5,
  1100. title_fontsize=45,
  1101. label_fontsize=35,
  1102. tick_fontsize=35,
  1103. annotation_fontsize=35,
  1104. add_grid=False,
  1105. size=(8, 8) # Slightly larger for better visibility
  1106. )
  1107. print(f"Successfully plotted {modality}")
  1108. except Exception as e:
  1109. print(f"Error plotting {modality}: {str(e)}")
  1110. import traceback
  1111. traceback.print_exc()
  1112. print(f"\nAll scatter plots saved to: {scatter_save_dir}")
  1113. # %% [markdown]
  1114. # ## Figure 3c
  1115. # %% [markdown]
  1116. # Body feature importance
  1117. # %%
  1118. # Upload combined dataframe
  1119. base_path = '/UK_BB/brainbody'
  1120. output_path = os.path.join(base_path, 'feature_imp', 'feature_imp_body', 'combined')
  1121. combined = pd.read_csv(os.path.join(base_path, 'feature_imp/feature_imp_body/combined', 'body_features_g_pred_stack_combined.csv'))
  1122. corr_results = pd.read_csv(os.path.join(base_path, 'feature_imp/feature_imp_body/combined', 'body_features_g_pred_stack_correlations_detailed_sorted.csv'))
  1123. # %%
  1124. # 1. Filter correlations
  1125. significant_correlations = corr_results[
  1126. (corr_results['Pearson r'].abs() >= 0.198) &
  1127. (corr_results['p-value bonferroni'] < 0.05)
  1128. ].copy()
  1129. print(f"Found {len(significant_correlations)} features with |r| >= 0.198")
  1130. #significant_correlations = corr_results.copy()
  1131. # Check if the renamed phenotypes exist in the Phenotype column
  1132. if 'Body mass index (BMI) (Musculoskeletal)' in significant_correlations['Phenotype'].values:
  1133. print("Phenotype exists in results:")
  1134. print(significant_correlations[significant_correlations['Phenotype'] == 'Body mass index (BMI) (Musculoskeletal)'])
  1135. else:
  1136. print("Phenotype NOT found in results. Available phenotypes with 'BMI':")
  1137. bmi_phenotypes = significant_correlations[significant_correlations['Phenotype'].str.contains('BMI', na=False)]
  1138. for _, row in bmi_phenotypes.iterrows():
  1139. print(f" '{row['Phenotype']}': r = {row['Pearson r']:.3f}")
  1140. # 2. Identify which modality each feature belongs to
  1141. modality_mapping = {}
  1142. # Rename
  1143. modality_suffix_map = {
  1144. 'body_composition_by_impedance': ' (Impedance)',
  1145. 'body_composition_dxa': ' (DXA)',
  1146. 'musculoskeletal': ' (Musculoskeletal)',
  1147. 'cardiopulmonary': ' (Cardiopulmonary)',
  1148. 'arterial_stiffness': ' (Arterial Stiffness)'
  1149. }
  1150. duplicate_features_list = ['Trunk fat mass', 'Body mass index (BMI)', 'Weight', 'Pulse rate']
  1151. # Check one fold for each modality to get the feature names
  1152. for modality in modalities_body:
  1153. try:
  1154. # Load one fold's data for this modality
  1155. fold = 0 # Use fold 0 as reference
  1156. if modality == 'hearing':
  1157. scaled_path = os.path.join(
  1158. base_path,
  1159. 'hearing-vision',
  1160. 'folds',
  1161. f'fold_{fold}',
  1162. 'scaling',
  1163. f'{modality}_test_scaled_fold_{fold}.csv'
  1164. )
  1165. else:
  1166. scaled_path = os.path.join(
  1167. base_path,
  1168. 'body',
  1169. 'folds',
  1170. f'fold_{fold}',
  1171. 'scaling',
  1172. f'{modality}_test_scaled_fold_{fold}.csv'
  1173. )
  1174. # Load the modality features
  1175. modality_df = pd.read_csv(scaled_path)
  1176. modality_features = set(modality_df.columns)
  1177. # Apply renaming to features
  1178. if modality in modality_suffix_map:
  1179. suffix = modality_suffix_map[modality]
  1180. renamed_features = set()
  1181. for feature in modality_features:
  1182. if feature in duplicate_features_list:
  1183. renamed_features.add(f"{feature}{suffix}") #rename duplicate features
  1184. else:
  1185. renamed_features.add(feature)
  1186. modality_features = renamed_features
  1187. # Store the features for this modality
  1188. modality_mapping[modality] = modality_features
  1189. print(f"Phenotype '{modality}' has {len(modality_features)} features")
  1190. except Exception as e:
  1191. print(f"Error loading modality {modality}: {e}")
  1192. modality_mapping[modality] = set()
  1193. # Create reverse mapping for debugging
  1194. feature_to_modalities = {}
  1195. for modality, features in modality_mapping.items():
  1196. for feature in features:
  1197. if feature not in feature_to_modalities:
  1198. feature_to_modalities[feature] = []
  1199. feature_to_modalities[feature].append(modality)
  1200. # Check for duplicates (should be NONE now)
  1201. duplicate_features = {f: mods for f, mods in feature_to_modalities.items() if len(mods) > 1}
  1202. if duplicate_features:
  1203. print(f"\nFound {len(duplicate_features)} features in multiple modalities:")
  1204. for feature, mods in list(duplicate_features.items())[:5]:
  1205. print(f" '{feature}': {mods}")
  1206. else:
  1207. print(f"\nNo duplicate features found - all duplicates have been renamed!")
  1208. # 3 Check the distribution of modalities among significant features
  1209. def find_feature_modality(feature_name):
  1210. """Find which modality a feature belongs to"""
  1211. for modality, features in modality_mapping.items():
  1212. if feature_name in features:
  1213. return modality
  1214. return 'unknown' # If not found in any modality
  1215. significant_correlations['domain'] = significant_correlations['Phenotype'].apply(find_feature_modality)
  1216. # Check how many features map to each domain
  1217. print("\nDomain distribution:")
  1218. domain_counts = significant_correlations['domain'].value_counts()
  1219. print(domain_counts)
  1220. # Check for unknown modalities
  1221. unknown_features = significant_correlations[significant_correlations['domain'] == 'unknown']
  1222. if len(unknown_features) > 0:
  1223. print(f"\nWarning: {len(unknown_features)} features could not be mapped to a modality:")
  1224. for feature in unknown_features['Phenotype']:
  1225. print(f" - {feature}")
  1226. # Debug: Check why this feature isn't found
  1227. found_in = []
  1228. for modality, features in modality_mapping.items():
  1229. if feature in features:
  1230. found_in.append(modality)
  1231. if found_in:
  1232. print(f" Actually found in: {found_in}")
  1233. else:
  1234. print(f" Not found in any modality set")
  1235. else:
  1236. print("\nAll features successfully mapped to modalities")
  1237. # Check how many unique domains we have
  1238. unique_domains = significant_correlations['domain'].nunique()
  1239. print(f"\nUnique domains found: {unique_domains}")
  1240. print(f"Total features: {len(significant_correlations)}")
  1241. # 4 Add modality information to significant correlations
  1242. significant_correlations['domain'] = significant_correlations['Phenotype'].apply(find_feature_modality)
  1243. # Check for unknown modalities
  1244. unknown_features = significant_correlations[significant_correlations['domain'] == 'unknown']
  1245. if len(unknown_features) > 0:
  1246. print(f"\nWarning: {len(unknown_features)} features could not be mapped to a modality:")
  1247. for feature in unknown_features['Phenotype']:
  1248. print(f" - {feature}")
  1249. else:
  1250. print("\nAll features successfully mapped to modalities")
  1251. print(f"\nUnique domains found: {significant_correlations['domain'].nunique()}")
  1252. print(f"Total features: {len(significant_correlations)}")
  1253. # 5. Create the final mapping dataframe with sex-stratified data using Bonferroni correction
  1254. domain_corr_df = significant_correlations.rename(columns={
  1255. 'Phenotype': 'feature',
  1256. 'Pearson r': 'correlation',
  1257. 'p-value': 'p_value'
  1258. }).copy()
  1259. # Add sex-stratified columns (using Bonferroni-corrected p-values)
  1260. domain_corr_df['Pearson r male'] = significant_correlations['Pearson r male']
  1261. domain_corr_df['Pearson r female'] = significant_correlations['Pearson r female']
  1262. domain_corr_df['p_value_male'] = significant_correlations['p-value male bonferroni'] # Use Bonferroni-corrected
  1263. domain_corr_df['p_value_female'] = significant_correlations['p-value female bonferroni'] # Use Bonferroni-corrected
  1264. # Add significance flags for each sex using BONFERRONI CORRECTION (p < 0.05 after correction)
  1265. domain_corr_df['significant_male'] = domain_corr_df['p_value_male'] < 0.05
  1266. domain_corr_df['significant_female'] = domain_corr_df['p_value_female'] < 0.05
  1267. # Keep the overall significance (using Bonferroni-corrected)
  1268. domain_corr_df['p_value_overall_bonferroni'] = significant_correlations['p-value bonferroni']
  1269. domain_corr_df['significant'] = domain_corr_df['p_value_overall_bonferroni'] < 0.05
  1270. print(f"\nFinal domain mapping with Bonferroni-corrected sex-stratified data:")
  1271. print(domain_corr_df[['feature', 'correlation', 'domain', 'significant_male', 'significant_female']].head())
  1272. # Show summary of significant features
  1273. print(f"\nSignificant features summary:")
  1274. print(f"Significant in males: {domain_corr_df['significant_male'].sum()}")
  1275. print(f"Significant in females: {domain_corr_df['significant_female'].sum()}")
  1276. print(f"Significant in either sex: {(domain_corr_df['significant_male'] | domain_corr_df['significant_female']).sum()}")
  1277. # Save the mapping
  1278. mapping_output_path = os.path.join(base_path, 'feature_imp', 'feature_imp_body', 'combined', 'body_features_domain_mapping_sex_stratified_bonferroni.csv')
  1279. domain_corr_df.to_csv(mapping_output_path, index=False)
  1280. print(f"\nDomain mapping saved to: {mapping_output_path}")
  1281. # Show summary by domain
  1282. print("\nSummary by domain (Bonferroni-corrected):")
  1283. domain_summary = domain_corr_df.groupby('domain').agg({
  1284. 'feature': 'count',
  1285. 'correlation': ['mean', 'min', 'max'],
  1286. 'significant_male': 'sum',
  1287. 'significant_female': 'sum'
  1288. }).round(3)
  1289. print(domain_summary)
  1290. # 6 Create domain order
  1291. domain_order = [
  1292. 'body_composition_by_impedance',
  1293. 'bone_dxa',
  1294. 'abdominal_composition_mri_18_vars',
  1295. 'body_composition_dxa',
  1296. 'cardiopulmonary',
  1297. 'musculoskeletal',
  1298. 'abdominal_organ_composition_mri_13_vars',
  1299. 'metabolic',
  1300. 'renalhepatic',
  1301. 'pwa',
  1302. 'hearing',
  1303. 'carotid_ultrasound',
  1304. 'heart_mri',
  1305. 'kidneys_mri',
  1306. 'immune',
  1307. 'arterial_stiffness',
  1308. 'ecg_rest',
  1309. 'liver_mri',
  1310. 'bone_densitometry',
  1311. ]
  1312. #sorted(domain_corr_df['domain'].unique())
  1313. # Create a mapping of domain to y-position groups
  1314. domain_groups = {}
  1315. for i, domain in enumerate(domain_order):
  1316. domain_groups[domain] = i
  1317. # Assign y-position based on domain
  1318. domain_corr_df['domain_order'] = domain_corr_df['domain'].map(domain_groups)
  1319. # %%
  1320. # Rename features
  1321. feature_rename_map = {
  1322. 'L1-L4 average height': 'L1-L4 average height',
  1323. 'Forced expiratory volume (FEV1)': 'FEV1',
  1324. 'Sitting height': 'Sitting height',
  1325. 'Standing height': 'Standing height',
  1326. 'Seated height': 'Seated height',
  1327. 'Forced vital capacity (FVC)': 'FVC',
  1328. 'Height': 'Height',
  1329. 'Anterior thigh fat-free muscle volume (left)': 'Anterior thigh fat-free muscle vol (left)',
  1330. 'Femur wards BMD (bone mineral density) T-score (right)': 'Femur wards BMD T-score (right)',
  1331. 'Anterior thigh fat-free muscle volume (right)': 'Anterior thigh fat-free muscle vol (right)',
  1332. 'Head BMC (bone mineral content)': 'Head BMC',
  1333. 'IGF-1': 'IGF-1',
  1334. 'Peak expiratory flow (PEF)': 'PEF',
  1335. 'Head bone area': 'Head bone area',
  1336. 'Femur wards BMD (bone mineral density) T-score (left)': 'Femur wards BMD T-score (left)',
  1337. 'Femur wards BMD (bone mineral density) (right)': 'Femur wards BMD (right)',
  1338. 'Femur wards BMD (bone mineral density) (left)': 'Femur wards BMD (left)',
  1339. 'Femur neck BMD (bone mineral density) T-score (left)': 'Femur neck BMD T-score (left)',
  1340. 'Total BMD (bone mineral density) T-score': 'Total BMD T-score',
  1341. 'Femur neck BMD (bone mineral density) T-score (right)': 'Femur neck BMD T-score (right)',
  1342. 'Total thigh fat-free muscle volume': 'Total thigh fat-free muscle vol',
  1343. 'Pancreas volume': 'Pancreas volume',
  1344. 'Handgrip strength': 'Handgrip strength',
  1345. 'Femur neck BMD (bone mineral density) (left)': 'Femur neck BMD (left)',
  1346. 'Left kidney volume': 'Left kidney volume',
  1347. 'Femur neck BMD (bone mineral density) (right)': 'Femur neck BMD (right)',
  1348. 'Pelvis BMD (bone mineral density)': 'Pelvis BMD',
  1349. 'Posterior thigh fat-free muscle volume (right)': 'Posterior thigh fat-free muscle vol (right)',
  1350. 'Posterior thigh fat-free muscle volume (left)': 'Posterior thigh fat-free muscle vol (left)',
  1351. 'Gynoid lean mass': 'Gynoid lean mass',
  1352. 'Gynoid fat free mass': 'Gynoid fat free mass',
  1353. 'Mean arterial pressure during PWA': 'Mean arterial pressure during PWA',
  1354. 'Total trunk fat volume': 'Total trunk fat volume',
  1355. 'Trunk fat percentage': 'Trunk fat percentage',
  1356. 'Minimum carotid IMT (intima-medial thickness) at 120 degrees ': 'Minimum carotid IMT at 120°',
  1357. 'Minimum carotid IMT (intima-medial thickness) at 150 degrees ': 'Minimum carotid IMT at 150°',
  1358. 'Speech-reception-threshold (SRT) estimate (right)': 'SRT estimate (right)',
  1359. 'Total tissue fat percentage': 'Total tissue fat percentage',
  1360. 'Waist circumference': 'Waist circumference',
  1361. 'Speech-reception-threshold (SRT) estimate (left)': 'SRT estimate (left)',
  1362. 'Alkaline phosphatase': 'Alkaline phosphatase',
  1363. 'Stroke volume during PWA': 'Stroke volume during PWA',
  1364. 'Maximum carotid IMT (intima-medial thickness) at 210 degrees ': 'Maximum carotid IMT at 210°',
  1365. 'Mean carotid IMT (intima-medial thickness) at 210 degrees ': 'Mean carotid IMT at 210°',
  1366. 'Mean carotid IMT (intima-medial thickness) at 240 degrees ': 'Mean carotid IMT at 240°',
  1367. 'Maximum carotid IMT (intima-medial thickness) at 150 degrees ': 'Maximum carotid IMT at 150°',
  1368. 'Maximum carotid IMT (intima-medial thickness) at 240 degrees ': 'Maximum carotid IMT at 240°',
  1369. 'End systolic pressure during PWA': 'End systolic pressure during PWA',
  1370. 'Mean carotid IMT (intima-medial thickness) at 150 degrees ': 'Mean carotid IMT at 150°',
  1371. 'Maximum carotid IMT (intima-medial thickness) at 120 degrees ': 'Maximum carotid IMT at 120°',
  1372. 'Glycated haemoglobin (HbA1c)': 'Glycated haemoglobin (HbA1c)',
  1373. 'Mean carotid IMT (intima-medial thickness) at 120 degrees ': 'Mean carotid IMT at 120°',
  1374. 'Central augmentation pressure during PWA': 'Central augmentation pressure during PWA',
  1375. 'Cardiac index during PWA': 'Cardiac index during PWA',
  1376. 'Cardiac output during PWA': 'Cardiac output during PWA',
  1377. 'Trunk tissue fat percentage': 'Trunk tissue fat percentage',
  1378. 'Android tissue fat percentage': 'Android tissue fat percentage',
  1379. 'Visceral adipose tissue volume (VAT)': 'VAT vol',
  1380. 'Peripheral pulse pressure during PWA': 'Peripheral pulse pressure during PWA',
  1381. 'Systolic brachial blood pressure during PWA': 'Systolic brachial BP during PWA',
  1382. 'Systolic brachial blood pressure': 'Systolic brachial BP',
  1383. 'Visceral fat volume': 'Visceral fat vol',
  1384. 'Total abdominal adipose tissue index': 'Total abdominal adipose tissue index',
  1385. 'VAT (visceral adipose tissue) mass': 'VAT mass',
  1386. 'VAT (visceral adipose tissue) volume': 'VAT vol',
  1387. 'Central systolic blood pressure during PWA': 'Central systolic blood pressure during PWA',
  1388. 'Central pulse pressure during PWA': 'Central pulse pressure during PWA',
  1389. 'Weight-to-muscle ratio': 'Weight-to-muscle ratio',
  1390. 'Pancreas PDFF (fat fraction)': 'Pancreas PDFF (fat fraction)',
  1391. 'Cystatin C': 'Cystatin C',
  1392. 'Systolic blood pressure': 'Systolic BP',
  1393. 'Abdominal fat ratio': 'Abdominal fat ratio',
  1394. 'Posterior thigh muscle fat infiltration (MFI) (left)': 'Posterior thigh MFI (left)',
  1395. 'Posterior thigh muscle fat infiltration (MFI) (right)': 'Posterior thigh MFI (right)',
  1396. 'Anterior thigh muscle fat infiltration (MFI) (right)': 'Anterior thigh MFI (right)',
  1397. 'Anterior thigh muscle fat infiltration (MFI) (left)': 'Anterior thigh MFI (left)',
  1398. 'Muscle fat infiltration': 'Muscle fat infiltration',
  1399. }
  1400. # Apply renaming to Phenotype column
  1401. domain_corr_df['feature'] = domain_corr_df['feature'].map(feature_rename_map).fillna(domain_corr_df['feature'])
  1402. for i, (orig, new) in enumerate(zip(domain_corr_df['feature'], domain_corr_df['feature'])):
  1403. if orig != new:
  1404. print(f"'{orig}' -> '{new}'")
  1405. # %%
  1406. # Vertical feature importance plot
  1407. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(60, 40),
  1408. gridspec_kw={'width_ratios': [1, 1]})
  1409. # Sort by domain and then by correlation
  1410. significant_df = domain_corr_df.sort_values(['domain_order', 'correlation'], ascending=[True, False])
  1411. significant_df = significant_df.reset_index(drop=True)
  1412. # Determine split point
  1413. total_features = len(significant_df)
  1414. split_idx = int(total_features / 2)
  1415. # Find the nearest domain boundary for a clean split
  1416. for i in range(split_idx, total_features):
  1417. if i == 0 or i == total_features:
  1418. continue
  1419. prev_domain = significant_df.iloc[i-1]['domain']
  1420. curr_domain = significant_df.iloc[i]['domain']
  1421. if curr_domain != prev_domain:
  1422. split_idx = i
  1423. print(f"Splitting at index {split_idx}, between domains '{prev_domain}' and '{curr_domain}'")
  1424. break
  1425. # Split the dataframe
  1426. df_left = significant_df.iloc[:split_idx].copy()
  1427. df_right = significant_df.iloc[split_idx:].copy()
  1428. print(f"Left plot: {len(df_left)} features, Right plot: {len(df_right)} features")
  1429. # Function to create a vertical bar plot
  1430. def create_vertical_subplot(ax, data_df, subplot_label=None, ylabel_fontsize=None):
  1431. """Create a vertical bar plot on given axes"""
  1432. # Create y positions with spacing between domains
  1433. y_positions = []
  1434. current_domain = None
  1435. spacing = 2.0
  1436. position = 0
  1437. for i in range(len(data_df)):
  1438. domain = data_df.iloc[i]['domain']
  1439. if domain != current_domain and current_domain is not None:
  1440. position += spacing
  1441. y_positions.append(position)
  1442. position += 1
  1443. current_domain = domain
  1444. y_positions = np.array(y_positions)
  1445. bar_height = 0.8
  1446. # Define color palette
  1447. custom_palette = [
  1448. "#A07756FF", '#BFCDD9FF', '#7F8C72FF', '#ADD0B5FF',
  1449. '#4A7169FF', '#BEB59CFF', '#4B644BFF', "#9B3F3FFF",
  1450. '#647D96FF', "#9970ABFF", '#226060FF', '#639CA4FF',
  1451. "#26588AFF", '#D2AD7CFF', "#2A775CFF","#C28C5FFF",
  1452. '#F4A582FF', "#B8858EFF", '#846D86FF'
  1453. ]
  1454. palette_dict = dict(zip(domain_corr_df['domain'].unique(), custom_palette))
  1455. # Create vertical bar plot
  1456. for i, (idx, row) in enumerate(data_df.iterrows()):
  1457. color = palette_dict[row['domain']]
  1458. ax.barh(y_positions[i], row['correlation'], height=bar_height,
  1459. color=color, alpha=0.9, edgecolor='black', linewidth=2.5)
  1460. # Add zero reference line
  1461. ax.axvline(x=0, color='grey', linewidth=3, linestyle='-', zorder=0)
  1462. # Set x-axis limits
  1463. max_corr = max(data_df['correlation'].max(), abs(data_df['correlation'].min()))
  1464. ax.set_xlim(-max_corr*1, max_corr*1)
  1465. ax.xaxis.set_major_locator(ticker.MultipleLocator(0.2))
  1466. # Manual domain offsets
  1467. domain_offset_overrides = {
  1468. 'body_composition_by_impedance': (-max_corr * 0.02, -1.6),
  1469. 'bone_dxa': (-max_corr * 0.02, -1.3),
  1470. 'abdominal_composition_mri_18_vars': (-max_corr * 0.02, -1.5),
  1471. 'body_composition_dxa': (-max_corr * 0.02, -1.4),
  1472. 'cardiopulmonary': (-max_corr * 0.02, -1.4),
  1473. 'musculoskeletal': (-max_corr * 0.02, -1.4),
  1474. 'abdominal_organ_composition_mri_13_vars': (-max_corr * 0.02, -1.4),
  1475. 'metabolic': (-max_corr * 0.02, -1.4),
  1476. 'renalhepatic': (-max_corr * 0.02, -1.4),
  1477. 'pwa': (-max_corr * 0.02, -1.4),
  1478. 'hearing': (-max_corr * 0.02, -1.4),
  1479. 'carotid_ultrasound': (-max_corr * 0.02, -1.4)
  1480. }
  1481. # Initialize domain_offsets dictionary
  1482. domain_offsets = {}
  1483. current_domain = None
  1484. domain_start_idx = 0
  1485. # Process each row to calculate offsets
  1486. for i in range(len(data_df)):
  1487. domain = data_df.iloc[i]['domain']
  1488. if domain != current_domain:
  1489. if current_domain is not None:
  1490. # Use manual override if defined, otherwise use default
  1491. if current_domain in domain_offset_overrides:
  1492. x_offset, y_offset = domain_offset_overrides[current_domain]
  1493. else:
  1494. # Default offsets for domains not in overrides
  1495. x_offset = -max_corr * 0.15 # Default horizontal
  1496. y_offset = -0.2 # Default vertical
  1497. # Store the calculated offset
  1498. domain_offsets[current_domain] = (x_offset, y_offset)
  1499. domain_start_idx = i
  1500. current_domain = domain
  1501. # Handle the last domain
  1502. if current_domain is not None:
  1503. if current_domain in domain_offset_overrides:
  1504. x_offset, y_offset = domain_offset_overrides[current_domain]
  1505. else:
  1506. # Default offsets for domains not in overrides
  1507. x_offset = -max_corr * 0.15
  1508. y_offset = -0.2
  1509. domain_offsets[current_domain] = (x_offset, y_offset)
  1510. # Add domain annotations using the calculated offsets
  1511. current_domain = None
  1512. domain_start_idx = 0
  1513. for i in range(len(data_df)):
  1514. domain = data_df.iloc[i]['domain']
  1515. if domain != current_domain:
  1516. if current_domain is not None:
  1517. x_offset, y_offset = domain_offsets.get(current_domain, (-max_corr * 0.15, -0.2))
  1518. annotation_y = y_positions[domain_start_idx] + y_offset
  1519. ax.text(x_offset, annotation_y, modality_map[current_domain],
  1520. ha='right', va='center', fontsize=65, fontweight='bold', rotation=0,
  1521. bbox=dict(boxstyle="round,pad=0.1", facecolor=palette_dict[current_domain], alpha=0.5, linewidth=3))
  1522. domain_start_idx = i
  1523. current_domain = domain
  1524. # Add annotation for the very last domain
  1525. if current_domain is not None:
  1526. x_offset, y_offset = domain_offsets.get(current_domain, (-max_corr * 0.15, -0.2))
  1527. annotation_y = y_positions[domain_start_idx] + y_offset
  1528. ax.text(x_offset, annotation_y, modality_map[current_domain],
  1529. ha='right', va='center', fontsize=65, fontweight='bold', rotation=0,
  1530. bbox=dict(boxstyle="round,pad=0.1", facecolor=palette_dict[current_domain], alpha=0.5))
  1531. # Add separation lines between domains
  1532. current_domain = None
  1533. for i in range(len(data_df)):
  1534. domain = data_df.iloc[i]['domain']
  1535. if domain != current_domain:
  1536. if current_domain is not None:
  1537. gap_center = (y_positions[i-1] + y_positions[i]) / 2
  1538. ax.axhline(y=gap_center, color='black', xmin=0.55, xmax=1.0, linestyle=':', alpha=0.7, linewidth=2)
  1539. current_domain = domain
  1540. # Set y-axis limits and labels
  1541. ax.set_ylim(y_positions[0] - 1, y_positions[-1] + 1)
  1542. ax.set_yticks(y_positions)
  1543. ax.set_yticklabels(data_df['feature'].tolist(), fontsize=ylabel_fontsize)
  1544. # Customize axes
  1545. ax.tick_params(axis='x',
  1546. labelsize=80,
  1547. bottom=True, # bottom ticks
  1548. length=10,
  1549. width=2,
  1550. color='black')
  1551. ax.set_xlabel("Pearson $r$", fontsize=90, labelpad=20)
  1552. # Customize spines
  1553. ax.spines['left'].set_linewidth(0)
  1554. ax.spines['bottom'].set_linewidth(2)
  1555. ax.spines['top'].set_linewidth(0)
  1556. ax.spines['right'].set_linewidth(0)
  1557. # Invert y-axis
  1558. ax.invert_yaxis()
  1559. # Add subplot label (A, B)
  1560. if subplot_label:
  1561. ax.text(0.02, 0.98, subplot_label, transform=ax.transAxes,
  1562. fontsize=44, fontweight='bold', va='top',
  1563. bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.7))
  1564. return max_corr
  1565. # Create left subplot
  1566. print("Creating left subplot...")
  1567. max_corr_left = create_vertical_subplot(ax1, df_left, subplot_label=None, ylabel_fontsize=55)
  1568. # Create right subplot
  1569. print("Creating right subplot...")
  1570. max_corr_right = create_vertical_subplot(ax2, df_right, subplot_label=None, ylabel_fontsize=55)
  1571. # Ensure both plots have consistent x-axis limits for comparison
  1572. max_corr_both = max(max_corr_left, max_corr_right)
  1573. ax1.set_xlim(-max_corr_both*1.3, max_corr_both*1.1)
  1574. ax2.set_xlim(-max_corr_both*1.3, max_corr_both*1.1)
  1575. # Share y-axis scale
  1576. # ax1.sharey(ax2)
  1577. fig.suptitle('Feature Importance:\nAssociations between Composite Body Marker\nand Each Body Physiology Measure (|$r$| ≥ 0.2)',
  1578. fontsize=150,
  1579. #fontweight='bold',
  1580. y=1.05,
  1581. x=0.57,
  1582. verticalalignment='top')
  1583. # Adjust tight_layout to make space for title
  1584. plt.tight_layout(rect=[0, 0, 1, 0.96]) # Leave top 4% for title
  1585. # Adjust layout
  1586. plt.tight_layout()
  1587. # Save the figure
  1588. plt.savefig(os.path.join(fig_path, 'Fig3c_body_features_g_pred_stack_annotated_split_vertical.svg'),
  1589. bbox_inches="tight",
  1590. pad_inches=1,
  1591. transparent=False,
  1592. facecolor="w",
  1593. edgecolor='w',
  1594. format='svg',
  1595. dpi=300)
  1596. plt.show()
  1597. print(f"Figure saved with {len(df_left)} features in left plot and {len(df_right)} features in right plot")
  1598. # %% [markdown]
  1599. # # Figure 4
  1600. # %% [markdown]
  1601. # ## Figure 4a
  1602. # %% [markdown]
  1603. # Composite body performance versus brain and whole-body markers
  1604. # %%
  1605. # Prepare data for KDE plot of delta_r values
  1606. import warnings
  1607. warnings.simplefilter(action='ignore', category=FutureWarning)
  1608. bootstrapping_path = '/UK_BB/brainbody/bootstrap/body_vs_all'
  1609. delta_dfs = {}
  1610. delta_dir_stacked = os.path.join(bootstrapping_path, 'delta_results_stacked')
  1611. if os.path.exists(delta_dir_stacked):
  1612. delta_files = [f for f in os.listdir(delta_dir_stacked) if f.startswith('delta_') and f.endswith('.csv')]
  1613. for f in delta_files:
  1614. delta_dfs[f.replace('.csv', '')] = pd.read_csv(os.path.join(delta_dir_stacked, f))
  1615. kde_palette = {
  1616. 'dwMRI': '#0d648f',
  1617. 'rsMRI': '#4B6F5A99',
  1618. 'sMRI': '#f8b976',
  1619. '3 Brain MRI Modalities': '#6A659999',
  1620. '3 Brain MRI Modalities and Body': '#A73933FF'
  1621. }
  1622. combined_data = []
  1623. delta_modality_mapping = {
  1624. 'delta_dwi_vs_body': 'dwMRI',
  1625. 'delta_rs_vs_body': 'rsMRI',
  1626. 'delta_smri_vs_body': 'sMRI',
  1627. 'delta_allmri_vs_body': '3 Brain MRI Modalities',
  1628. 'delta_brain-body_vs_body': '3 Brain MRI Modalities and Body'
  1629. }
  1630. for delta_name, df in delta_dfs.items():
  1631. if 'delta_r' in df.columns and delta_name in delta_modality_mapping:
  1632. display_name = delta_modality_mapping[delta_name]
  1633. for delta_r_val in df['delta_r']:
  1634. combined_data.append({'modality': display_name, 'delta_r': delta_r_val})
  1635. combined_df = pd.DataFrame(combined_data)
  1636. # %%
  1637. # KDE plot: Composite body vs brain and whole-body markers
  1638. fig = plt.figure(figsize=(15,3))
  1639. kde = sns.kdeplot(
  1640. data=combined_df, x="delta_r", hue="modality",
  1641. fill=True, common_norm=False, palette=kde_palette,
  1642. alpha=.5, linewidth=1, legend=False
  1643. )
  1644. # Add median lines and labels
  1645. modalities = combined_df['modality'].unique()
  1646. for mod in modalities:
  1647. median_delta = combined_df[combined_df['modality'] == mod]['delta_r'].median()
  1648. #plt.axvline(median_delta, color='grey', linestyle='--', linewidth=0.7)
  1649. # Different x-offsets to prevent overlap
  1650. if median_delta < 0.01:
  1651. x_offset = -0.008
  1652. ha = 'left'
  1653. elif median_delta < 0.05:
  1654. x_offset = -0.009
  1655. ha = 'left'
  1656. elif median_delta < 0.065:
  1657. x_offset = -0.009
  1658. ha = 'left'
  1659. elif median_delta > 0.07:
  1660. x_offset = 0.004
  1661. ha = 'left'
  1662. else: # Larger values
  1663. x_offset = 0.005
  1664. ha = 'left'
  1665. plt.text(median_delta + x_offset, plt.gca().get_ylim()[1] * 0.88 + 8,
  1666. f'{median_delta:.3f}', color='black', fontsize=25, ha=ha)
  1667. # Add zero reference line
  1668. plt.axvline(x=0, color='#32363FFF', linestyle='--', linewidth=2, alpha=1) #BE4A47FF
  1669. # Add short vertical lines at KDE peak for each modality
  1670. for mod in modalities:
  1671. r_vals = combined_df[combined_df['modality'] == mod]['delta_r'].dropna()
  1672. median_delta = r_vals.median()
  1673. # Estimate KDE height at median
  1674. kde = gaussian_kde(r_vals)
  1675. y_peak = kde(median_delta)[0]
  1676. # Draw vertical line from y=0 to KDE peak
  1677. plt.plot([median_delta, median_delta], [0, y_peak], color=kde_palette.get(mod, 'grey'), linestyle='--', linewidth=1)
  1678. # ------------
  1679. sns.despine(left=True)
  1680. plt.title("Comparative Performance of Stacked Models", fontsize=30, y=1.3) #Body Stacked vs Brain Stacked
  1681. plt.xlabel("Δ Pearson $r$", fontsize=30, labelpad=50)
  1682. plt.ylabel("Density", fontsize=25)
  1683. plt.yticks(fontsize=20)
  1684. #plt.xticks(fontsize=20)
  1685. # Adjust x-ticks for delta values
  1686. x_min = combined_df['delta_r'].min()
  1687. x_max = combined_df['delta_r'].max()
  1688. x_ticks = np.arange(round(x_min, 2), round(x_max, 2) + 0.02, 0.02)
  1689. plt.xticks(ticks=x_ticks, labels=[f'{x:.2f}' for x in x_ticks], fontsize=20)
  1690. plt.savefig(os.path.join(fig_path, 'Fig4a_delta_r_brain_vs_body_stacked_scaled_KDE.svg'),
  1691. bbox_inches="tight",
  1692. #pad_inches=1,
  1693. transparent=False,
  1694. facecolor="w",
  1695. edgecolor='w',
  1696. orientation='landscape',
  1697. format='svg')
  1698. plt.show()
  1699. # %% [markdown]
  1700. # ## Figure 4b
  1701. # %% [markdown]
  1702. # Combined scatterplots of predicted vs observed g
  1703. # %%
  1704. # Combined scatterplots of predicted vs observed g: Config
  1705. bootstrapping_path = '/UK_BB/brainbody/bootstrap/stack'
  1706. modalities_to_plot = ['body', 'allmri', 'dwi', 'rs', 'smri', 'brain-body']
  1707. folds = range(0,5)
  1708. base_path = '/UK_BB/brainbody'
  1709. dticmap = sns.light_palette("#0d648f", as_cmap=True)
  1710. rscmap = sns.light_palette("#4B6F5A99", as_cmap=True)
  1711. t1cmap = sns.light_palette("#f8b976", as_cmap=True)
  1712. stackcmap = sns.light_palette("#A73933FF", as_cmap=True)
  1713. bodycmap = sns.light_palette("#871C0FFF", as_cmap=True)
  1714. allmricmap = sns.light_palette("#6A659999", as_cmap=True)
  1715. modality_cmaps = {
  1716. 'dwi': dticmap,
  1717. 'rs': rscmap,
  1718. 'smri': t1cmap,
  1719. 'brain-body': stackcmap,
  1720. 'body': bodycmap,
  1721. 'allmri': allmricmap
  1722. }
  1723. modality_map = {
  1724. 'body': 'Composite Body',
  1725. 'allmri': 'Composite Brain',
  1726. 'dwi': 'dwMRI',
  1727. 'rs': 'rsMRI',
  1728. 'smri': 'sMRI',
  1729. 'brain-body': 'Body+Brain'
  1730. }
  1731. # -----------------------
  1732. # Load precomputed performance metrics AND bootstrap confidence intervals
  1733. brain_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_brain_stack.xlsx'))
  1734. body_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_body_stack_outer.xlsx'))
  1735. brain_body_metrics = pd.read_excel(os.path.join(base_path, r'result/2level/2level_result-mean_brain_and_body_stack_outer.xlsx'))
  1736. # Load bootstrap confidence intervals
  1737. bootstrap_ci_path = '/UK_BB/brainbody/bootstrap/stack/scaled/results/combined_bootstrap_CI_results_stacked_sorted_scaled.xlsx'
  1738. bootstrap_df = pd.read_excel(bootstrap_ci_path)
  1739. # Combine into one master dataframe
  1740. all_metrics_df = pd.concat([brain_metrics, body_metrics, brain_body_metrics], ignore_index=True)
  1741. # Create comprehensive annotation dictionary with bootstrap CIs
  1742. annotation_metrics = {}
  1743. for _, row in all_metrics_df.iterrows():
  1744. # Standardize modality names
  1745. modality_key = row['Modality'].lower()
  1746. # Replace specific patterns
  1747. modality_key = modality_key.replace('3 brain mri modalities', 'allmri')
  1748. modality_key = modality_key.replace('body physiology', 'body')
  1749. modality_key = modality_key.replace('brain mri and body physiology', 'brain-body')
  1750. modality_key = modality_key.replace('brain mri and body', 'brain-body')
  1751. modality_key = modality_key.replace('dwmri', 'dwi')
  1752. modality_key = modality_key.replace('rsmri', 'rs')
  1753. modality_key = modality_key.replace('smri', 'smri')
  1754. modality_key = modality_key.replace(' ', '-')
  1755. # Store the main metrics
  1756. annotation_metrics[modality_key] = {
  1757. 'r_value': row['Test Pearson r'],
  1758. 'r2_value': row['Test R2'],
  1759. 'r_ci_lower': None,
  1760. 'r_ci_upper': None,
  1761. 'r2_ci_lower': None,
  1762. 'r2_ci_upper': None
  1763. }
  1764. # Add bootstrap confidence intervals
  1765. for _, row in bootstrap_df.iterrows():
  1766. if row['Metric'] in ['R2', 'r']:
  1767. # Standardize modality names to match
  1768. modality_key = row['Modality'].lower()
  1769. modality_key = modality_key.replace('3 brain mri modalities', 'allmri')
  1770. modality_key = modality_key.replace('body physiology', 'body')
  1771. modality_key = modality_key.replace('brain mri and body physiology', 'brain-body')
  1772. modality_key = modality_key.replace('brain mri and body', 'brain-body')
  1773. modality_key = modality_key.replace('dwmri', 'dwi')
  1774. modality_key = modality_key.replace('rsmri', 'rs')
  1775. modality_key = modality_key.replace('smri', 'smri')
  1776. modality_key = modality_key.replace(' ', '-')
  1777. if modality_key in annotation_metrics:
  1778. if row['Metric'] == 'R2':
  1779. annotation_metrics[modality_key]['r2_ci_lower'] = row['CI_lower']
  1780. annotation_metrics[modality_key]['r2_ci_upper'] = row['CI_upper']
  1781. elif row['Metric'] == 'r':
  1782. annotation_metrics[modality_key]['r_ci_lower'] = row['CI_lower']
  1783. annotation_metrics[modality_key]['r_ci_upper'] = row['CI_upper']
  1784. print("Loaded annotation metrics for modalities:")
  1785. for modality, metrics in annotation_metrics.items():
  1786. print(f" {modality}: r={metrics['r_value']:.3f}, R²={metrics['r2_value']:.3f}")
  1787. if metrics['r_ci_lower'] is not None:
  1788. print(f" r CI: [{metrics['r_ci_lower']:.3f}, {metrics['r_ci_upper']:.3f}]")
  1789. if metrics['r2_ci_lower'] is not None:
  1790. print(f" R² CI: [{metrics['r2_ci_lower']:.3f}, {metrics['r2_ci_upper']:.3f}]")
  1791. # %%
  1792. # Combined scatterplots
  1793. ordered_modalities = ['smri', 'rs', 'dwi', 'brain-body', 'allmri']
  1794. # Create figure with 1 row and 5 columns
  1795. fig, axes = plt.subplots(1, 5, figsize=(35, 8))
  1796. for idx, modality in enumerate(ordered_modalities):
  1797. if idx >= len(axes):
  1798. break
  1799. try:
  1800. # Load data
  1801. input_path = os.path.join(bootstrapping_path, f'{modality}_folds_combined_test.csv')
  1802. df = pd.read_csv(input_path)
  1803. # Get colormap
  1804. cmap = modality_cmaps[modality]
  1805. # Plot on subplot
  1806. ax = axes[idx]
  1807. x_col = 'g_pred_stack_test'
  1808. y_col = 'g_obs_test'
  1809. x = pd.to_numeric(df[x_col], errors='coerce').astype(float)
  1810. y = pd.to_numeric(df[y_col], errors='coerce').astype(float)
  1811. # Compute distance for coloring
  1812. x_mean, y_mean = x.mean(), y.mean()
  1813. dist_i = np.sqrt((y - y_mean)**2 + (x - x_mean)**2)
  1814. # Scatter plot - updated parameters
  1815. sc = ax.scatter(
  1816. x, y, c=dist_i, cmap=cmap,
  1817. s=50, alpha=0.9,
  1818. edgecolors='black', linewidth=0.3
  1819. )
  1820. # Regression line
  1821. mask = np.isfinite(x) & np.isfinite(y)
  1822. if mask.sum() >= 2:
  1823. z = np.polyfit(x[mask], y[mask], 1)
  1824. p = np.poly1d(z)
  1825. xs = np.linspace(x[mask].min(), x[mask].max(), 200)
  1826. ax.plot(xs, p(xs), alpha=0.8, linewidth=2, color='#BE4A47FF', linestyle='-')
  1827. # Set axis limits and ticks
  1828. ax.set_xlim(-3, 2)
  1829. ax.set_ylim(-3.5, 3)
  1830. ax.xaxis.set_major_locator(MultipleLocator(1))
  1831. ax.yaxis.set_major_locator(MultipleLocator(1))
  1832. # Add annotation with updated format
  1833. if modality in annotation_metrics:
  1834. metrics = annotation_metrics[modality]
  1835. r_value = metrics['r_value']
  1836. r_ci_lower = metrics.get('r_ci_lower')
  1837. r_ci_upper = metrics.get('r_ci_upper')
  1838. # Updated annotation text format
  1839. if r_ci_lower is not None:
  1840. annotation_text = f'$r$ = {r_value:.2f}'
  1841. annotation_text += f'\n95% CI [{r_ci_lower:.2f}, {r_ci_upper:.2f}]'
  1842. else:
  1843. annotation_text = rf'$r$ = {r_value:.2f}'
  1844. ax.text(
  1845. 0.05, 0.91, annotation_text,
  1846. transform=ax.transAxes, fontsize=38,
  1847. va='bottom', ha='left'
  1848. )
  1849. # Set labels with updated format
  1850. if idx == 0: # Only for first plot
  1851. ax.set_ylabel('$g$-factor\nderived from ESEM ($z$)', fontsize=50)
  1852. else:
  1853. ax.set_ylabel('') # Empty for plots 2-5
  1854. ax.set_yticklabels([])
  1855. # Set title with bbox edge color matching modality cmap
  1856. title_color = cmap(0.5)
  1857. ax.set_title(modality_map.get(modality, modality), fontsize=50, y=1.2,
  1858. bbox=dict(facecolor='white', alpha=0.9, edgecolor=title_color,
  1859. linewidth=2, boxstyle='round,pad=0.3'))
  1860. # Remove frame (spines) - updated formatting
  1861. sns.despine(top=True, right=True, ax=ax)
  1862. # Set tick label sizes - updated formatting
  1863. ax.tick_params(axis='x', labelsize=40)
  1864. ax.tick_params(axis='y', labelsize=40)
  1865. except Exception as e:
  1866. print(f"Error in combined plot for {modality}: {str(e)}")
  1867. axes[idx].text(0.5, 0.5, f"Error\n{modality}", ha='center', va='center', transform=axes[idx].transAxes)
  1868. # Remove empty subplots if any
  1869. for idx in range(len(ordered_modalities), len(axes)):
  1870. fig.delaxes(axes[idx])
  1871. # Add global x-label
  1872. fig.text(0.5, -0.12, 'Predicted $g$-factor ($z$)', ha='center', fontsize=50)
  1873. plt.tight_layout()
  1874. scatter_save_dir = '/UK_BB/brainbody/figures'
  1875. plt.savefig(os.path.join(scatter_save_dir, 'Fig4b_all_modalities_combined.png'), dpi=300, bbox_inches='tight', pad_inches=0.5)
  1876. plt.show()
  1877. # %% [markdown]
  1878. # ## Figure 4c
  1879. # %% [markdown]
  1880. # Feature importance: heatmaps dwMRI + rsMRI
  1881. # %%
  1882. # Functions to transform 1D correlations dataframe to 2D correlation matrix
  1883. def long_to_square_matrix_dwi(correlation, coordinates):
  1884. regions = coordinates['name'].values
  1885. n = len(regions)
  1886. corr_matrix = np.zeros((n, n))
  1887. pval_matrix = np.ones((n, n))
  1888. name_to_idx = {name: idx for idx, name in enumerate(regions)}
  1889. for _, row in correlation.iterrows():
  1890. i = name_to_idx[row['region_i']]
  1891. j = name_to_idx[row['region_j']]
  1892. corr_matrix[i, j] = row['Pearson r']
  1893. corr_matrix[j, i] = row['Pearson r']
  1894. pval_matrix[i, j] = row['p-value']
  1895. pval_matrix[j, i] = row['p-value']
  1896. return (
  1897. pd.DataFrame(corr_matrix, index=regions, columns=regions),
  1898. pd.DataFrame(pval_matrix, index=regions, columns=regions)
  1899. )
  1900. def long_to_square_matrix_rs(correlation, coordinates):
  1901. regions = coordinates['name'].values
  1902. n = len(regions)
  1903. corr_matrix = np.full((n, n), np.nan)
  1904. pval_matrix = np.full((n, n), np.nan)
  1905. np.fill_diagonal(corr_matrix, 1.0)
  1906. np.fill_diagonal(pval_matrix, 0.0)
  1907. name_to_idx = {name: idx for idx, name in enumerate(regions)}
  1908. for _, row in correlation.iterrows():
  1909. i = name_to_idx[row['region_i']]
  1910. j = name_to_idx[row['region_j']]
  1911. corr_matrix[i, j] = row['Pearson r']
  1912. corr_matrix[j, i] = row['Pearson r']
  1913. pval_matrix[i, j] = row['p-value']
  1914. pval_matrix[j, i] = row['p-value']
  1915. return (
  1916. pd.DataFrame(corr_matrix, index=regions, columns=regions),
  1917. pd.DataFrame(pval_matrix, index=regions, columns=regions)
  1918. )
  1919. # Load data
  1920. base_path = '/UK_BB/brainbody'
  1921. schaefer_corr_dwi = '/UK_BB/brainbody/feature_imp/feature_imp_brain/dwmri/Schaefer7n200p_Tian_S1_Streamline_Count_i2_corr_with_g_stack_with_regions.csv'
  1922. schaefer_corr_rs = '/UK_BB/brainbody/feature_imp/feature_imp_brain/rsmri/Schaefer7n200p_Tian_S1_Full_Corr_i2_corr_with_g_stack_with_regions.csv'
  1923. corr_dwi = pd.read_csv(schaefer_corr_dwi)
  1924. corr_rs = pd.read_csv(schaefer_corr_rs)
  1925. coords = pd.read_csv('/UK_BB/brainbody/feature_imp/feature_imp_brain/mni_coords.csv')
  1926. # Read region order
  1927. with open(os.path.join(base_path, 'feature_imp/feature_imp_brain/conn_regions_reordered.txt'), 'r') as f:
  1928. final_order = [line.strip() for line in f.readlines()]
  1929. # Assign 'Subcortical' as network for subcortical regions (FIXED: Do this FIRST)
  1930. coords['network'] = coords.apply(
  1931. lambda row: row['network'] if row['type'] == 'cortical' else 'Subcortical',
  1932. axis=1
  1933. )
  1934. # Assign hemisphere for subcortical regions
  1935. coords['hemi'] = coords.apply(
  1936. lambda row: row['hemi'] if row['type'] == 'cortical' else (
  1937. row['name'].split('-')[-1] if row['name'].endswith(('-lh', '-rh')) else 'none'
  1938. ),
  1939. axis=1
  1940. )
  1941. # Align coordinates to final order
  1942. coords_aligned = (
  1943. coords.set_index('name')
  1944. .loc[final_order]
  1945. .reset_index()
  1946. )
  1947. # Convert to correlation matrices
  1948. corr_matrix_dwi, pval_matrix_dwi = long_to_square_matrix_dwi(corr_dwi, coords)
  1949. corr_matrix_rs, pval_matrix_rs = long_to_square_matrix_rs(corr_rs, coords)
  1950. # Reorder correlation matrices
  1951. corr_matrix_ordered_dwi = corr_matrix_dwi.loc[final_order, final_order]
  1952. corr_matrix_ordered_rs = corr_matrix_rs.loc[final_order, final_order]
  1953. # Set up network colors and names
  1954. network_colors = {
  1955. 'Vis': '#ff0000',
  1956. 'SomMot': '#00fa9a',
  1957. 'DorsAttn': '#0076BBFF',
  1958. 'SalVentAttn': '#00ced1',
  1959. 'Limbic': '#ffff00',
  1960. 'Cont': '#BBBBBB',
  1961. 'Default': '#ff69b4',
  1962. 'Subcortical': '#5AAE61FF'
  1963. }
  1964. structure_names = {
  1965. 'Vis': 'Vis',
  1966. 'SomMot': 'SomMot',
  1967. 'DorsAttn': 'DAN',
  1968. 'SalVentAttn': 'VAN',
  1969. 'Limbic': 'Limb',
  1970. 'Cont': 'Cont',
  1971. 'Default': 'DMN',
  1972. 'Subcortical': 'Subcort'
  1973. }
  1974. structure_names_full = {
  1975. 'Vis': 'Visual',
  1976. 'SomMot': 'Somatomotor',
  1977. 'DorsAttn': 'Dorsal Attention',
  1978. 'SalVentAttn': 'Salience/Ventral Attention',
  1979. 'Limbic': 'Limbic',
  1980. 'Cont': 'Control',
  1981. 'Default': 'Default Mode',
  1982. 'Subcortical': 'Subcortical'
  1983. }
  1984. # %%
  1985. # Plot heatmap: vertical
  1986. n = len(corr_matrix_ordered_dwi)
  1987. # Create figure with two subplots side by side
  1988. fig, axes = plt.subplots(2, 1, figsize=(16, 24), gridspec_kw={'height_ratios': [1, 1], 'hspace': 0.2})
  1989. cmap = sns.color_palette("seismic", as_cmap=True)
  1990. vmin, vmax = -0.45, 0.45
  1991. # Create lower triangle mask (hides lower triangle, shows upper triangle)
  1992. #upper_triangle_mask = np.tril(np.ones_like(corr_matrix_ordered_dwi, dtype=bool), k=-1)
  1993. lower_triangle_mask = np.triu(np.ones_like(corr_matrix_ordered_dwi, dtype=bool), k=1)
  1994. # Top
  1995. ax1 = axes[0]
  1996. rs_data = corr_matrix_ordered_rs.values.copy()
  1997. np.fill_diagonal(rs_data, 0)
  1998. sns.heatmap(
  1999. rs_data,
  2000. mask=lower_triangle_mask,
  2001. cmap=cmap,
  2002. center=0,
  2003. vmin=vmin, vmax=vmax,
  2004. square=True,
  2005. xticklabels=False,
  2006. yticklabels=False,
  2007. linewidths=0,
  2008. cbar=False,
  2009. ax=ax1,
  2010. rasterized=True
  2011. )
  2012. # Diagonal line for left plot (bottom-left to top-right)
  2013. ax1.plot([0, n], [0, n], color='black', linewidth=1, linestyle='-', alpha=0.7)
  2014. # Add rsMRI label
  2015. #ax1.text(-0.1, 0.5, 'rsMRI', transform=ax1.transAxes, fontsize=30, va='center', ha='right', rotation=90)
  2016. # Bottom
  2017. ax2 = axes[1]
  2018. dwi_data = corr_matrix_ordered_dwi.values.copy()
  2019. np.fill_diagonal(dwi_data, 0)
  2020. sns.heatmap(
  2021. dwi_data,
  2022. mask=lower_triangle_mask, # mask shows upper triangle
  2023. cmap=cmap,
  2024. center=0,
  2025. vmin=vmin, vmax=vmax,
  2026. square=True,
  2027. xticklabels=False,
  2028. yticklabels=False,
  2029. linewidths=0,
  2030. cbar=False,
  2031. ax=ax2,
  2032. rasterized=True
  2033. )
  2034. # Diagonal line for bottom plot
  2035. ax2.plot([0, n], [0, n], color='black', linewidth=1, linestyle='-', alpha=0.7)
  2036. # Add dwMRI label
  2037. #ax2.text(1.1, 0.5, 'dwMRI', transform=ax2.transAxes, fontsize=30, va='center', ha='left', rotation=270)
  2038. # Add shared colorbar
  2039. from matplotlib.cm import ScalarMappable
  2040. from matplotlib.colors import Normalize
  2041. sm = ScalarMappable(cmap=cmap, norm=Normalize(vmin=vmin, vmax=vmax))
  2042. sm.set_array([])
  2043. cbar_ax = fig.add_axes([0.3, 0.08, 0.4, 0.02])
  2044. cbar = fig.colorbar(sm, cax=cbar_ax, orientation='horizontal')
  2045. cbar.set_label('Pearson $r$', fontsize=50)
  2046. cbar.ax.tick_params(labelsize=30)
  2047. # Annotations
  2048. bottom_y = n + 1.5
  2049. left_x = -8
  2050. left_x -= 12
  2051. bar_height = 25
  2052. bar_width = 20
  2053. # Get network boundaries
  2054. network_boundaries = []
  2055. current_network = None
  2056. for i, region in enumerate(corr_matrix_ordered_dwi.index):
  2057. network = coords_aligned.loc[coords_aligned['name'] == region, 'network'].values[0]
  2058. if network != current_network:
  2059. if current_network is not None:
  2060. network_boundaries.append((start_idx, i, current_network))
  2061. start_idx = i
  2062. current_network = network
  2063. network_boundaries.append((start_idx, n, current_network))
  2064. # Apply to bottom plot
  2065. for start, end, network in network_boundaries:
  2066. color = network_colors[network]
  2067. # Bottom color bar
  2068. ax1.add_patch(plt.Rectangle(
  2069. (start, bottom_y - 2),
  2070. end - start,
  2071. bar_height,
  2072. facecolor=color,
  2073. clip_on=False
  2074. ))
  2075. # Left color bar
  2076. ax1.add_patch(plt.Rectangle(
  2077. (left_x, start),
  2078. bar_width,
  2079. end - start,
  2080. facecolor=color,
  2081. clip_on=False
  2082. ))
  2083. # Structure labels
  2084. mid_point = (start + end) / 2
  2085. # Left
  2086. #ax1.text(left_x + 17, mid_point - 2.1, structure_names.get(network, network), ha='right', va='center', fontsize=15, rotation=90)
  2087. # Bottom
  2088. #ax1.text(mid_point, bottom_y + bar_height/1.5 + 2, structure_names.get(network, network), ha='center', va='bottom', fontsize=15)
  2089. # Apply to dwMRI
  2090. for start, end, network in network_boundaries:
  2091. color = network_colors[network]
  2092. # Bottom color bar
  2093. ax2.add_patch(plt.Rectangle(
  2094. (start, bottom_y - 2),
  2095. end - start,
  2096. bar_height,
  2097. facecolor=color,
  2098. clip_on=False
  2099. ))
  2100. # Left color bar
  2101. ax2.add_patch(plt.Rectangle(
  2102. (left_x, start),
  2103. bar_width,
  2104. end - start,
  2105. facecolor=color,
  2106. clip_on=False
  2107. ))
  2108. # Structure labels for bottom
  2109. mid_point = (start + end) / 2
  2110. # Left
  2111. #ax2.text(left_x + 17, mid_point - 2.1, structure_names.get(network, network), ha='right', va='center', fontsize=15, rotation=90)
  2112. # Bottom
  2113. #ax2.text(mid_point, bottom_y + bar_height/1.5 + 2, structure_names.get(network, network), ha='center', va='bottom', fontsize=15)
  2114. # Grid lines and limits for LEFT plot
  2115. top_y = -10
  2116. for boundary in [b[0] for b in network_boundaries[1:]]:
  2117. # For upper triangle orientation
  2118. ax1.plot([boundary, boundary], [boundary, n], color='black', linewidth=0.4, alpha=1) # Vertical
  2119. ax1.plot([0, boundary], [boundary, boundary], color='black', linewidth=0.4, alpha=1) # Horizontal
  2120. ax1.set_xlim(left_x - 1, n + 0.8)
  2121. ax1.set_ylim(n + bar_height + 2, top_y - 2)
  2122. # Only draw left and bottom borders
  2123. ax1.plot([0, 0], [0, n], color='black', linewidth=1) # Left border
  2124. ax1.plot([0, n], [n, n], color='black', linewidth=1) # Bottom border
  2125. # Grid lines and limits for Rbottomplot
  2126. for boundary in [b[0] for b in network_boundaries[1:]]:
  2127. ax2.plot([boundary, boundary], [boundary, n], color='black', linewidth=0.4, alpha=1) # Vertical
  2128. ax2.plot([0, boundary], [boundary, boundary], color='black', linewidth=0.4, alpha=1) # Horizontal
  2129. ax2.set_xlim(-1, n + 0.8)
  2130. ax2.set_ylim(n + bar_height + 2, top_y - 2)
  2131. # Only draw left and bottom borders
  2132. ax2.plot([0, 0], [0, n], color='black', linewidth=1) # Left border
  2133. ax2.plot([0, n], [n, n], color='black', linewidth=1) # Bottom border
  2134. # Individual titles for each subplot
  2135. #ax1.set_title("rsMRI Feature Importance:\nSchaefer7n500p-IV Full Correlation", fontsize=36, pad=20)
  2136. #ax2.set_title("dwMRI Feature Importance:\nSchaefer7n500p-IV Streamline Count", fontsize=36, pad=20)
  2137. # Clean up all spines
  2138. for ax in [ax1, ax2]:
  2139. ax.spines['top'].set_visible(False)
  2140. ax.spines['right'].set_visible(False)
  2141. ax.spines['left'].set_visible(False)
  2142. ax.spines['bottom'].set_visible(False)
  2143. # Extract unique networks
  2144. unique_networks = sorted(set(
  2145. coords_aligned.loc[coords_aligned['name'].isin(corr_matrix_ordered_dwi.index), 'network']
  2146. ))
  2147. # Create legend elements
  2148. legend_elements = []
  2149. # Set up network order matching patches
  2150. unique_networks = [network for _, _, network in network_boundaries]
  2151. for network in unique_networks:
  2152. color = network_colors.get(network, 'gray')
  2153. label = structure_names_full.get(network, network)
  2154. legend_elements.append(
  2155. plt.Rectangle((0, 0), 1, 1, facecolor=color, edgecolor='black',
  2156. linewidth=0.5, label=label)
  2157. )
  2158. # Add legend to figure
  2159. legend = fig.legend(
  2160. handles=legend_elements,
  2161. loc='lower center',
  2162. ncol= 1, #min(len(unique_networks), 9), # Adjust columns
  2163. fontsize=40,
  2164. frameon=True,
  2165. fancybox=True,
  2166. framealpha=0.9,
  2167. bbox_to_anchor=(1.0, 0.6), #vertical, on the right side
  2168. #bbox_to_anchor=(0.5, -0.01), horizontal, one row
  2169. handletextpad=0.5,
  2170. columnspacing=1.0,
  2171. #title='Brain Networks',
  2172. title_fontsize=45
  2173. )
  2174. # Make room for the legend
  2175. plt.subplots_adjust(bottom=0.15)
  2176. # Save
  2177. save_fig(['png'], os.path.join(fig_path, 'Fig4c_connectomes_heatmaps_separate'))
  2178. plt.show()
  2179. # %%
  2180. '''
  2181. (x=0,y=0) -----------------(x=n,y=0)
  2182. | |
  2183. | |
  2184. | |
  2185. | |
  2186. (x=0,y=n) -----------------(x=n,y=n)
  2187. '''
  2188. '''
  2189. (0,0) → x=0, y=0 → the top-left corner of the matrix.
  2190. (n,0) → x=n, y=0 → the top-right corner of the matrix.
  2191. (0,n) → x=0, y=n → the bottom-left corner.
  2192. (n,n) → x=n, y=n → the bottom-right corner.
  2193. '''
  2194. # %% [markdown]
  2195. # # Figure 4d
  2196. # %% [markdown]
  2197. # sMRI barplots
  2198. # %%
  2199. # Filter to only show correlations > 0.2 in absolute value and p-value < 0.05
  2200. corr_smri = pd.read_csv(os.path.join(base_path, 'feature_imp/feature_imp_brain/struct_fs_aseg_volume_corr_with_g_stack.csv'))
  2201. plot_df_filtered = corr_smri[(corr_smri['Pearson r'].abs() > 0.195) & (corr_smri['p-value'] < 0.05)]
  2202. # Sort the filtered data
  2203. ordered = (plot_df_filtered
  2204. .assign(abs_corr = plot_df_filtered['Pearson r'].abs())
  2205. .sort_values('abs_corr', ascending=False)
  2206. ).sort_values('Pearson r', ascending=False)
  2207. # Create the figure with larger dimensions
  2208. n_bars = len(ordered)
  2209. plt.figure(figsize=(20, max(20, n_bars * 0.4)), dpi=300)
  2210. # Create color list - simple positive\negative coloring
  2211. colors = ['#D79C9CFF' if x > 0 else '#5F93ACFF' for x in ordered['Pearson r']]
  2212. # Create bars with slightly thicker bars
  2213. bars = plt.barh(ordered['Feature'].str.replace(' hemisphere', ''),
  2214. ordered['Pearson r'],
  2215. color=colors,
  2216. height=0.8)
  2217. # Add correlation values with larger font
  2218. for bar, corr in zip(bars, ordered['Pearson r']):
  2219. width = bar.get_width()
  2220. label_x = width - 0.01 if width > 0 else width + 0.02
  2221. plt.text(label_x, bar.get_y() + bar.get_height()/2,
  2222. f'{width:.2f}',
  2223. va='center', ha='right' if width > 0 else 'left',
  2224. color='white', fontsize=30)
  2225. # Increase all font sizes
  2226. #plt.title(f'sMRI Feature Importance:\nASEG Subcortical Volumetric Segmentation', pad=18, fontsize=45, x=-0.5)
  2227. plt.xlabel('Pearson $r$', labelpad=10, fontsize=45)
  2228. plt.xticks(fontsize=35)
  2229. plt.ylabel('')
  2230. plt.yticks(fontsize=37)
  2231. plt.gca().invert_yaxis()
  2232. # Adjust layout with more padding
  2233. plt.tight_layout(pad=3.0)
  2234. # Save with higher quality
  2235. plt.savefig(os.path.join(fig_path, 'Fig4d_fi-smri-BARS.svg'), dpi=350,bbox_inches='tight')
  2236. plt.show()
  2237. # %% [markdown]
  2238. # # Figure 5
  2239. # %% [markdown]
  2240. # g-body commonality analysis
  2241. # %%
  2242. # Prepare data for plotting
  2243. commonality_path = '/UK_BB/brainbody/commonality'
  2244. df = pd.read_csv(os.path.join(commonality_path, 'commonality_results_decompos_brain_vs_individual_body.csv'))
  2245. print(df.columns.to_list(), df['Brain MRI modality'].unique(), df['Body modality'].unique())
  2246. # Dataset 1: For Venn diagrams (stacked body modalities)
  2247. df_venn = pd.read_excel(os.path.join(commonality_path, 'commonality_results_decompos_brain_vs_body_stacked.xlsx'))
  2248. # Dataset 2: For stacked bar plots (individual body modalities)
  2249. df_bars = pd.read_csv(os.path.join(commonality_path, 'commonality_results_decompos_brain_vs_individual_body.csv'))
  2250. # Check brain modality names in both DataFrames
  2251. print("Brain modalities in df_venn (Venn diagram data):")
  2252. print(df_venn['Brain MRI modality'].unique())
  2253. print("\nBrain modalities in df_bars (bar plot data):")
  2254. print(df_bars['Brain MRI modality'].unique())
  2255. # Filter for Body physiology and composition stacked
  2256. body_modality = 'Body physiology and composition stacked'
  2257. body_df = df_venn[df_venn['Body modality'] == body_modality]
  2258. # Get all brain modalities
  2259. brain_modalities_venn = body_df['Brain MRI modality'].unique()
  2260. n_modalities = len(brain_modalities_venn)
  2261. # Prepare venn2 data
  2262. data_venn2 = []
  2263. color_palette = {
  2264. 'brain': '#4A7169FF',#'#4A7169FF'
  2265. 'body': '#A8554EFF' #'#A8554EFF'
  2266. }
  2267. for brain_mod in brain_modalities_venn:
  2268. row = body_df[body_df['Brain MRI modality'] == brain_mod].iloc[0]
  2269. prop_body_exp_by_brain = round((row['Common variance'] / (row['Unique variance: body'] + row['Common variance'])) * 100, 2)
  2270. data_venn2.append((
  2271. #row['Perc unique brain'],
  2272. #row['Perc unique body'],
  2273. #row['Perc common'],
  2274. row['Unique variance: brain'],
  2275. row['Unique variance: body'],
  2276. row['Common variance'],
  2277. color_palette['brain'],
  2278. color_palette['body'],
  2279. brain_mod.replace('Brain ', '').replace('3 brain', '3'),
  2280. prop_body_exp_by_brain
  2281. ))
  2282. # Create a mapping from Venn names to bar plot names
  2283. brain_mod_mapping = {
  2284. '3 brain MRI modalities stacked': '3 Brain MRI Modalities Stacked',
  2285. 'Brain dwMRI stacked': 'Brain dwMRI Stacked',
  2286. 'Brain rsMRI stacked': 'Brain rsMRI Stacked',
  2287. 'Brain sMRI stacked': 'Brain sMRI Stacked'
  2288. }
  2289. # Get the brain modality names for bar plots
  2290. brain_modalities_bars = [brain_mod_mapping.get(mod, mod) for mod in brain_modalities_venn]
  2291. print(f"\nVenn brain modalities: {brain_modalities_venn}")
  2292. print(f"Bar plot brain modalities: {brain_modalities_bars}")
  2293. # Get the sorted order from the first brain modality
  2294. first_brain_mod_bars = brain_modalities_bars[0]
  2295. df_first = df_bars[df_bars['Brain MRI modality'] == first_brain_mod_bars].copy()
  2296. print(f"\nFirst brain modality for sorting: '{first_brain_mod_bars}'")
  2297. print(f"Number of rows found: {len(df_first)}")
  2298. df_first.sort_values(by='Unique variance: brain', ascending=False, inplace=True)
  2299. sorted_body_order = df_first['Body modality'].tolist()
  2300. print(f"Sorted body order length: {len(sorted_body_order)}")
  2301. # Create a dictionary to store data for each subplot
  2302. plot_data = {}
  2303. for venn_mod, bars_mod in zip(brain_modalities_venn, brain_modalities_bars):
  2304. print(f"Processing: Venn='{venn_mod}' -> Bars='{bars_mod}'")
  2305. # Filter dataframe for the current brain modality
  2306. df_mod = df_bars[df_bars['Brain MRI modality'] == bars_mod].copy()
  2307. print(f" Rows found: {len(df_mod)}")
  2308. # Create a categorical type with the desired order
  2309. df_mod['Body modality'] = pd.Categorical(df_mod['Body modality'],
  2310. categories=sorted_body_order,
  2311. ordered=True)
  2312. # Sort by the predefined order
  2313. df_mod.sort_values('Body modality', inplace=True)
  2314. # Store the data for this subplot
  2315. plot_data[venn_mod] = {
  2316. 'body_names': df_mod['Body modality'].tolist(),
  2317. 'y1_unique_brain': df_mod['Unique variance: brain'].values,
  2318. 'y2_common': df_mod['Common variance'].values,
  2319. 'y3_unique_body': df_mod['Unique variance: body'].values,
  2320. # Store percentages for annotations
  2321. 'perc_u_brain': df_mod['Perc unique brain'].values,
  2322. 'perc_common': df_mod['Perc common'].values,
  2323. 'perc_u_body': df_mod['Perc unique body'].values,
  2324. 'prop_body_exp_by_brain': df_mod['g-body explained by brain'].values,
  2325. 'prop_body_exp_by_brain': df_mod['g-body explained by brain'].values
  2326. }
  2327. # %%
  2328. # Plot
  2329. fig = plt.figure(figsize=(6*n_modalities, 24))
  2330. # Define manual positions
  2331. positions = []
  2332. # Top row - Venn diagrams
  2333. for i in range(n_modalities):
  2334. left = 0.05 + i * (0.9/n_modalities) + i*0.04
  2335. positions.append([left, 0.61, 0.8/n_modalities, 0.25]) # [left, bottom, width, height]
  2336. # Bottom row - Bar plots
  2337. for i in range(n_modalities):
  2338. left = 0.05 + i * (0.9/n_modalities) + i*0.04
  2339. positions.append([left, 0.05, 0.8/n_modalities, 0.5])
  2340. # Create axes
  2341. axes = []
  2342. for pos in positions:
  2343. axes.append(fig.add_axes(pos))
  2344. # Define colors for stacked bars
  2345. color_unique_brain = '#4A7169FF'
  2346. color_common = '#c2ada6'
  2347. color_unique_body = '#A8554EFF'
  2348. # Plot Venn diagrams (top row - axes[0] to axes[n_modalities-1])
  2349. for i, (u_brain, u_body, c_common, color_brain, color_body, label, prop_body_exp) in enumerate(data_venn2):
  2350. ax = axes[i] # Use first n_modalities axes for Venn diagrams
  2351. # Create venn2 diagram
  2352. subset_sizes = (u_brain, u_body, c_common)
  2353. #subset_sizes = ( row['Unique variance: brain'], row['Unique variance: body'], row['Common variance'])
  2354. venn = venn2(subsets=subset_sizes, set_labels=('', ''),
  2355. set_colors=(color_brain, color_body), ax=ax)
  2356. ax.set_ylim(-1.2, 1.2)
  2357. ax.set_aspect(0.8) #'equal'
  2358. ax.axis('off')
  2359. # Apply styling
  2360. for patch in venn.patches:
  2361. if patch:
  2362. patch.set_alpha(0.7)
  2363. patch.set_linewidth(1.5)
  2364. patch.set_edgecolor("black")
  2365. # Format labels
  2366. #for text in venn.subset_labels:
  2367. for j, text in enumerate(venn.subset_labels):
  2368. if text:
  2369. val = float(text.get_text())
  2370. text.set_text(f'{val*100:.1f}')
  2371. text.set_fontsize(30)
  2372. # Adjust the leftmost label (index 0 corresponds to "10" region, i.e. left circle only)
  2373. if j == 0: # left-only subset
  2374. x, y = text.get_position()
  2375. text.set_position((x + 0.04, y)) # shift right by 0.05 units
  2376. # Adjust Venn diagram annotations
  2377. if i == 1:
  2378. if j == 0:
  2379. x, y = text.get_position()
  2380. text.set_position((x - 0.035, y))
  2381. if i == 3:
  2382. if j == 0:
  2383. x, y = text.get_position()
  2384. text.set_position((x - 0.035, y))
  2385. # Add title and annotation using figure coordinates
  2386. titles = [ "Composite Brain",
  2387. "dwMRI",
  2388. "rsMRI",
  2389. "sMRI" ]
  2390. # Title
  2391. title_x = positions[i][0] + positions[i][2]/2 # Center of the Venn axes
  2392. fig.text(title_x, 0.83, titles[i], fontsize=45, ha='center', va='top')
  2393. # Annotation
  2394. annotation_text = f"{prop_body_exp:.1f}%"
  2395. fig.text(title_x, 0.65, annotation_text, ha='center', va='center',
  2396. fontsize=35,
  2397. bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.7))
  2398. for idx, brain_mod in enumerate(brain_modalities_venn):
  2399. ax = axes[n_modalities + idx]
  2400. data = plot_data[brain_mod]
  2401. y_labels = data['body_names']
  2402. # Stacked barplots (bottom row - axes[n_modalities] to axes[2*n_modalities-1])
  2403. y_labels = [' '.join(word.upper() if word.upper() in ['MRI', 'DXA', 'ECG'] else word.lower() if i > 0 else word.capitalize() for i, word in enumerate(label.split()))
  2404. for label in y_labels]
  2405. rename_dict = {
  2406. 'Bone densitometry (of heel)': 'Bone densitometry (heel)',
  2407. 'Abdominal organ composition by MRI': 'Abdominal organ composition (MRI)',
  2408. 'Body composition by DXA': 'Body composition (DXA)',
  2409. 'Abdominal composition by MRI': 'Abdominal composition (MRI)',
  2410. 'Bone size, mineral and density by DXA': 'BMD (DXA)',
  2411. 'Body composition by impedance': 'Body Composition (impedance)'
  2412. }
  2413. y_labels = [rename_dict.get(lbl, lbl) for lbl in y_labels]
  2414. y1 = data['y1_unique_brain']*100
  2415. y2 = data['y2_common']*100
  2416. y3 = data['y3_unique_body']*100
  2417. prop_body_exp_by_brain = data['prop_body_exp_by_brain']
  2418. bar_height = 0.8
  2419. # Create stacked horizontal bars
  2420. ax.barh(y_labels, y1, height=bar_height, color=color_unique_brain, alpha=0.8, edgecolor='black', linewidth=1.5)
  2421. ax.barh(y_labels, y2, height=bar_height, left=y1, color=color_common, alpha=0.8, edgecolor='black', linewidth=1.5)
  2422. ax.barh(y_labels, y3, height=bar_height, left=y1+y2, color=color_unique_body, alpha=0.8, edgecolor='black', linewidth=1.5)
  2423. # Add percentage annotations on each segment
  2424. for i, (u_brain, common, u_body) in enumerate(zip(y1, y2, y3)):
  2425. if u_brain > 0.001:
  2426. ax.text(u_brain / 2, i, f'{u_brain:.1f}', ha='center', va='center',
  2427. color='white', fontsize=22)
  2428. if common > 0.001:
  2429. if common < 0.9: # Small common variance - to the left
  2430. # Position at the start of the common segment, shifted slightly right
  2431. ax.text(u_brain + common / 2, i, f'{common:.1f}', ha='right', va='center',
  2432. color='white', fontsize=22)
  2433. elif common < 2 and common > 1:
  2434. ax.text(u_brain + common / 2, i, f'{common:.1f}', ha='right', va='center', color='white', fontsize=20)
  2435. else: # Normal/large common variance - center
  2436. ax.text(u_brain + common / 2, i, f'{common:.1f}', ha='center', va='center', color='white', fontsize=20)
  2437. if u_body: # > 0.001:
  2438. #ax.text(u_brain + common + u_body / 2, i, f'{m_u_body:.2f}', ha='center', va='center', color='white', fontsize=20)
  2439. pass
  2440. prop_text = f'{prop_body_exp_by_brain[i]:.1f}%'
  2441. ax.text(1.25, i, prop_text, ha='right', va='center',
  2442. fontsize=25, transform=ax.get_yaxis_transform(), color='black', bbox=dict(boxstyle="round,pad=0.2", facecolor='white', alpha=0.7))
  2443. # Format the subplot
  2444. ax.tick_params(axis='y', labelsize=30, length=0)
  2445. ax.tick_params(axis='x', labelsize=45)
  2446. ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%.1f'))
  2447. max_total = max(y1 + y2 + y3)
  2448. ax.set_xlim(0, max_total)
  2449. ax.set_xticks([0, max_total])
  2450. # Put ticks at 0, half, and max
  2451. import matplotlib.ticker as mticker
  2452. half_point = max_total / 2
  2453. ax.xaxis.set_major_locator(mticker.FixedLocator([0, half_point, max_total]))
  2454. if idx > 0:
  2455. ax.set_yticklabels([])
  2456. for spine in ['top', 'right', 'left']:
  2457. ax.spines[spine].set_visible(False)
  2458. # Add common x-axis label for bar plots
  2459. fig.text(0.53, -0.03, 'Variance of Cognition Explained (%)', ha='center', fontsize=50)
  2460. fig.suptitle('Commonality Analysis\nbetween Body Phenotypes and Brain Markers\nin Explaining Cognition (%)', fontsize=70, y=1)
  2461. def make_circle_legend(legend, orig_handle, xdescent, ydescent, width, height, fontsize):
  2462. return plt.Circle((width/2, height/2), min(width, height))
  2463. legend_elements = [
  2464. plt.Circle((0,0), 0.5, facecolor=color_unique_brain, alpha=0.9, label='Unique to Brain'),
  2465. plt.Circle((0,0), 0.5, facecolor=color_common, alpha=0.9, label='Common Variance'),
  2466. plt.Circle((0,0), 0.5, facecolor=color_unique_body, alpha=0.9, label='Unique to Body'),
  2467. plt.Rectangle((0,0), 1, 1, facecolor='none', edgecolor='black', lw=2,
  2468. label='Body-related cognitive variance\nexplained by each brain marker (%)')]
  2469. leg = fig.legend(handles=legend_elements, loc='lower center', ncol=4,
  2470. fontsize=38, frameon=True, #bbox_to_anchor=(0.5, -0.005),
  2471. fancybox=True,
  2472. bbox_to_anchor=(0.5, 0.54),
  2473. handler_map={plt.Circle: HandlerPatch(patch_func=make_circle_legend)},
  2474. handletextpad=1.0, handleheight=1.0,
  2475. columnspacing=1.5, labelspacing=1.5,
  2476. borderpad=0.5
  2477. )
  2478. # Control the legend frame linewidth and color
  2479. frame = leg.get_frame()
  2480. frame.set_linewidth(2)
  2481. frame.set_edgecolor("black"),
  2482. frame.set_facecolor(((216/255, 216/255, 216/255, 1)))
  2483. # Save
  2484. save_paths = [
  2485. os.path.join(fig_path, 'final/Fig5'),
  2486. os.path.join(fig_path, 'Fig5_ca_g_body_exp_by_brain_stacked_bars_plus_venn_marginalR2'),]
  2487. for path in save_paths:
  2488. save_fig(['png', 'svg'], path)
  2489. plt.show()
  2490. # %%
  2491. # Venn 2 for Fig1
  2492. fig, ax = plt.subplots(figsize=(8, 8))
  2493. u_brain, u_body, c_common, color_brain, color_body, label, prop_body_exp = data_venn2[0]
  2494. subset_sizes = (u_body, u_brain, c_common)
  2495. venn = venn2(subsets=subset_sizes, set_labels=('Body','Brain'), # add labels
  2496. set_colors=(color_body, '#0D5581FF'), ax=ax)
  2497. ax.set_ylim(-1.2, 1.2)
  2498. ax.set_aspect(0.9)
  2499. ax.axis('off')
  2500. # Apply styling
  2501. for patch in venn.patches:
  2502. if patch:
  2503. patch.set_alpha(0.7)
  2504. patch.set_linewidth(1.5)
  2505. patch.set_edgecolor("black")
  2506. # Remove text inside circles
  2507. for text in venn.subset_labels:
  2508. if text:
  2509. text.set_text('')
  2510. # Labels
  2511. for i, text in enumerate(venn.set_labels):
  2512. if text:
  2513. text.set_fontsize(32)
  2514. label_colors = [color_body, '#0D5581FF']
  2515. text.set_color(label_colors[i])
  2516. # Position adjustments
  2517. if i == 0: # "Brain" label - move left
  2518. x, y = text.get_position()
  2519. text.set_position((x - 0.1, y + 1.2))
  2520. elif i == 1: # "Body" label - move right
  2521. x, y = text.get_position()
  2522. text.set_position((x + 0.2, y + 1.3))
  2523. # Save
  2524. save_fig(['svg'], os.path.join(fig_path, 'Fig1d_venn2'))
  2525. plt.show()
  2526. # %% [markdown]
  2527. # # Figure 6
  2528. # %% [markdown]
  2529. # ## Figure 6a
  2530. # %% [markdown]
  2531. # g-age commonality analysis: venn3 diagrams
  2532. # %%
  2533. # Set the path
  2534. commonality_path = '/UK_BB/brainbody/commonality'
  2535. # %%
  2536. # Prepare data for plotting
  2537. commonality_path = 'Volumes/sci-psy-narun/IBu/UK_BB/brainbody/commonality'
  2538. df = pd.read_csv(
  2539. os.path.join(commonality_path, 'commonality_results_body_plus_brain_age_3v.csv')
  2540. )
  2541. # Extract raw variance components
  2542. u_body = df['Unique: Body'].iloc[0]
  2543. u_brain = df['Unique: Brain'].iloc[0]
  2544. u_age = df['Unique: Age'].iloc[0]
  2545. c_body_brain = df['Common: Body & Brain'].iloc[0]
  2546. c_body_age = df['Common: Body & Age'].iloc[0]
  2547. c_brain_age = df['Common: Brain & Age'].iloc[0]
  2548. c_all_three = df['Common: All Three'].iloc[0]
  2549. # Pack into Venn3 order
  2550. subset_values = (
  2551. u_body, # 100
  2552. u_brain, # 010
  2553. c_body_brain, # 110
  2554. u_age, # 001
  2555. c_body_age, # 101
  2556. c_brain_age, # 011
  2557. c_all_three # 111
  2558. )
  2559. # Total R2
  2560. total_r2 = sum(subset_values)
  2561. print("Total R2:", round(total_r2, 4))
  2562. # Convert to percentages for plotting (Venn3 geometry)
  2563. subset_percent = tuple(v / total_r2 * 100 for v in subset_values)
  2564. print("%:", subset_percent)
  2565. # %%
  2566. # Venn 3
  2567. fig, ax = plt.subplots(figsize=(10, 10))
  2568. body = "#9E2F2584" #A8554E
  2569. brain = "#0076C0FF" #4A7169
  2570. age = "#2B811AC0" #C2ADA6
  2571. body_label = "#74251EFF" #A8554E
  2572. brain_label = "#084972FF" #4A7169
  2573. age_label = "#104905C0" #C2ADA6
  2574. # Plot using percentages
  2575. venn = venn3(
  2576. subsets=subset_percent,
  2577. set_labels=('Body', 'Brain', 'Age'),
  2578. set_colors=(body, brain, age),
  2579. ax=ax
  2580. )
  2581. ax.set_aspect(0.9)
  2582. for patch in venn.patches:
  2583. if patch:
  2584. patch.set_alpha(0.6)
  2585. patch.set_edgecolor('black')
  2586. patch.set_linewidth(1.5)
  2587. # Replace percentage labels with raw variance values
  2588. for text, real_value in zip(venn.subset_labels, subset_values):
  2589. if text:
  2590. text.set_text(f"{real_value*100:.1f}")
  2591. text.set_fontsize(25)
  2592. text.set_color('white')
  2593. # Set labels ("Body", "Brain", "Age")
  2594. for i, text in enumerate(venn.set_labels):
  2595. if text:
  2596. text.set_fontsize(40)
  2597. #text.set_fontweight('bold')
  2598. label_colors = [body_label, brain_label, age_label]
  2599. text.set_color(label_colors[i]) # 'black'
  2600. # Get label positions
  2601. for region in ['100','010','001','110','101','011','111']:
  2602. lab = venn.get_label_by_id(region)
  2603. if lab:
  2604. print(region, lab.get_position())
  2605. # Move Body-only label
  2606. lab = venn.get_label_by_id('100')
  2607. if lab:
  2608. x, y = lab.get_position()
  2609. lab.set_position((x + 0.01, y + 0.02))
  2610. # Move Brain-only label
  2611. #lab = venn.get_label_by_id('010')
  2612. #if lab:
  2613. #x, y = lab.get_position()
  2614. #lab.set_position((x + 0.04, y))
  2615. # Move Age-only label
  2616. #lab = venn.get_label_by_id('001')
  2617. #if lab:
  2618. # x, y = lab.get_position()
  2619. #lab.set_position((x, y + 0.03))
  2620. # Move Body ∩ Brain
  2621. lab = venn.get_label_by_id('110')
  2622. if lab:
  2623. lab.set_position((-0.0197, 0.4))
  2624. # Move Body ∩ Age
  2625. lab = venn.get_label_by_id('101')
  2626. if lab:
  2627. x, y = lab.get_position()
  2628. lab.set_position((x - 0.03, y + 0.02))
  2629. # Move Brain ∩ Age
  2630. lab = venn.get_label_by_id('011')
  2631. if lab:
  2632. x, y = lab.get_position()
  2633. lab.set_position((x - 0.005, y - 0.05))
  2634. # Move triple overlap
  2635. #lab = venn.get_label_by_id('111')
  2636. #if lab:
  2637. #x, y = lab.get_position()
  2638. #lab.set_position((x - 0.03, y + 0.02))
  2639. # Add text on the right side of Venn diagrams
  2640. text_x_position = 1.0
  2641. text_y_start = 0.7
  2642. # Define items as (label, color, value)
  2643. # value=None means no percentage on that line
  2644. text_items = [
  2645. ('Age-related cognitive variance', None, None, None),
  2646. ('explained by:', None, None, None),
  2647. # Add a blank line marker to create extra spacing
  2648. ('', None, None, None),
  2649. ('Composite body:', '#74251EFF', '81.4%', 0.48),
  2650. ('Composite brain:', '#084972FF', '87.2%', 0.49),
  2651. ('Composite body or brain:', "#564464", '96.8%', 0.73),
  2652. ('Composite body and brain:', "#65746a", '71.7%', 0.77),
  2653. ]
  2654. # Vertical spacing
  2655. header_spacing = 0.06
  2656. line_spacing = 0.09
  2657. extra_gap_after_header = -0.05
  2658. y_pos = text_y_start
  2659. for i, (label, color, value, x_offset) in enumerate(text_items):
  2660. # Insert extra gap after the header block
  2661. if i == 3: # after the blank line
  2662. y_pos -= extra_gap_after_header
  2663. # Skip drawing empty lines
  2664. if label == '' and value is None:
  2665. y_pos -= line_spacing
  2666. continue
  2667. # Draw the label
  2668. if color:
  2669. fig.text(text_x_position, y_pos, label,
  2670. ha='left', va='center', fontsize=40,
  2671. bbox=dict(boxstyle="round,pad=0.2",
  2672. facecolor='white',
  2673. edgecolor=color,
  2674. linewidth=2.5),
  2675. transform=fig.transFigure)
  2676. else:
  2677. fig.text(text_x_position, y_pos, label,
  2678. ha='left', va='center', fontsize=40,
  2679. transform=fig.transFigure)
  2680. # Draw the percentage immediately after the label
  2681. if value and x_offset:
  2682. fig.text(text_x_position + x_offset, y_pos, # adjust horizontal offset
  2683. value,
  2684. ha='left', va='center', fontsize=40,
  2685. transform=fig.transFigure)
  2686. y_pos -= (header_spacing if i < 3 else line_spacing)
  2687. plt.title('Commonality Analysis\namong Age and Composite Body and Brain Markers\nin Explaining Cognition (%)', fontsize=55, y=1.2, x=0.9)
  2688. save_fig(['png', 'svg'], os.path.join(fig_path, 'Fig6b_3v_commonality_body_plus_brain_plus_age'))
  2689. plt.show()
  2690. # %%
  2691. # Venn 3
  2692. fig, ax = plt.subplots(figsize=(8, 8))
  2693. body = "#9E2F2584"
  2694. brain = "#0D5581FF"
  2695. age = "#2B811AC0"
  2696. body_label = "#74251EFF"
  2697. brain_label = "#084972FF"
  2698. age_label = "#104905C0"
  2699. # Plot Venn diagram
  2700. venn = venn3(
  2701. subsets=subset_percent,
  2702. set_labels=('Body', 'Brain', 'Age'),
  2703. set_colors=(body, brain, age),
  2704. ax=ax
  2705. )
  2706. # Remove axes completely
  2707. ax.axis('off')
  2708. # Remove all text labels (percentages inside circles)
  2709. for text in venn.subset_labels:
  2710. if text:
  2711. text.set_text('')
  2712. # Keep only the circle labels but make them smaller
  2713. for i, text in enumerate(venn.set_labels):
  2714. if text:
  2715. text.set_fontsize(40)
  2716. label_colors = [body_label, brain_label, age_label]
  2717. text.set_color(label_colors[i])
  2718. # Adjust circle transparency and edges
  2719. for patch in venn.patches:
  2720. if patch:
  2721. patch.set_alpha(0.6)
  2722. patch.set_edgecolor('black')
  2723. patch.set_linewidth(1.5)
  2724. # Save minimal figure
  2725. plt.savefig(os.path.join(fig_path, 'fig1-prep/Fig1d_venn3.svg'),
  2726. bbox_inches="tight",
  2727. pad_inches=0.5,
  2728. transparent=False,
  2729. facecolor="w",
  2730. edgecolor='w')
  2731. plt.show()
  2732. # %% [markdown]
  2733. # ## Figure 6b
  2734. # %% [markdown]
  2735. # g-age scatterplots
  2736. # %%
  2737. # Plot scatterplots for age ~ g
  2738. body_age = pd.read_csv(os.path.join(commonality_path, 'g_obs_pred_body_with_age.csv'))
  2739. brain_age = pd.read_csv(os.path.join(commonality_path, 'g_obs_pred_allmri_with_age.csv'))
  2740. scatter_kwargs = dict(
  2741. s=50,
  2742. alpha=0.9,
  2743. edgecolors='black',
  2744. linewidth=0.2
  2745. )
  2746. fig, axs = plt.subplots(1, 3, figsize=(35, 12))
  2747. # ---------------------------------------------------------------------
  2748. # Age vs observed g
  2749. x = pd.to_numeric(body_age['Age'], errors='coerce').astype(float)
  2750. y = pd.to_numeric(body_age['g_obs_test'], errors='coerce').astype(float)
  2751. axs[0].scatter(x, y, color="#ac87a0", **scatter_kwargs)
  2752. mask = np.isfinite(x) & np.isfinite(y)
  2753. if mask.sum() >= 2:
  2754. z = np.polyfit(x[mask], y[mask], 1)
  2755. p = np.poly1d(z)
  2756. xs = np.linspace(x[mask].min(), x[mask].max(), 200)
  2757. axs[0].plot(xs, p(xs), alpha=0.8, linewidth=2, color="#BE4A47FF")
  2758. axs[0].set_xlim(x.min(), x.max()+5)
  2759. y_min = y.min()
  2760. y_max = y.max()
  2761. tick_interval = 1
  2762. y_max_rounded = np.ceil(y_max / tick_interval) * tick_interval
  2763. axs[0].set_ylim(y_min, y_max_rounded)
  2764. ###
  2765. axs[0].set_ylim(y.min()-1, y.max()+1)
  2766. axs[0].set_title(r'$ĝ_{/text{observed}}$ ~ Age', fontsize=65, y=1.1, pad=15)#, bbox=dict(facecolor='white', alpha=0.9, edgecolor="#C98E8CFF", linewidth=2, boxstyle='round,pad=0.3'))
  2767. axs[0].set_xlabel('Age (years)', fontsize=55)
  2768. axs[0].set_ylabel('$g$-factor\nderived from ESEM ($z$)', fontsize=45)
  2769. axs[0].tick_params(axis='x', labelsize=42)
  2770. axs[0].tick_params(axis='y', labelsize=42)
  2771. axs[0].yaxis.set_major_locator(MultipleLocator(1))
  2772. # -------------------------------------------------------------------------
  2773. # Age vs predicted g (Body)
  2774. x = pd.to_numeric(body_age['Age'], errors='coerce').astype(float)
  2775. y = pd.to_numeric(body_age['g_pred_body_test'], errors='coerce').astype(float)
  2776. axs[1].scatter(x, y, color="#C98E8CFF", **scatter_kwargs)
  2777. mask = np.isfinite(x) & np.isfinite(y)
  2778. if mask.sum() >= 2:
  2779. z = np.polyfit(x[mask], y[mask], 1)
  2780. p = np.poly1d(z)
  2781. xs = np.linspace(x[mask].min(), x[mask].max(), 200)
  2782. axs[1].plot(xs, p(xs), alpha=0.8, linewidth=2, color="#BE4A47FF")
  2783. axs[1].set_xlim(x.min(), x.max()+5)
  2784. y_min = y.min()
  2785. y_max = y.max()
  2786. tick_interval = 1
  2787. y_max_rounded = np.ceil(y_max / tick_interval) * tick_interval
  2788. axs[1].set_ylim(y_min, y_max_rounded)
  2789. ###
  2790. axs[1].set_ylim(y.min()-1, y.max()+1)
  2791. axs[1].set_title(r'$ĝ_{/text{body}}$ ~ Age', fontsize=65, y=1.1, pad=15)#, bbox=dict(facecolor='white', alpha=0.9, edgecolor="#0d648f", linewidth=2, boxstyle='round,pad=0.3'))
  2792. axs[1].set_xlabel('Age (years)', fontsize=55)
  2793. axs[1].set_ylabel('$g$-factor\npredicted from body ($z$)', fontsize=45)
  2794. axs[1].tick_params(axis='x', labelsize=42)
  2795. axs[1].tick_params(axis='y', labelsize=42)
  2796. axs[1].yaxis.set_major_locator(MultipleLocator(1))
  2797. # ----------------------------------------------------------------------
  2798. # Age vs predicted cognition (Brain)
  2799. x = pd.to_numeric(brain_age['Age'], errors='coerce').astype(float)
  2800. y = pd.to_numeric(brain_age['g_pred_allmri_test'], errors='coerce').astype(float)
  2801. axs[2].scatter(x, y, color="#6A659999", **scatter_kwargs)
  2802. mask = np.isfinite(x) & np.isfinite(y)
  2803. if mask.sum() >= 2:
  2804. z = np.polyfit(x[mask], y[mask], 1)
  2805. p = np.poly1d(z)
  2806. xs = np.linspace(x[mask].min(), x[mask].max(), 200)
  2807. axs[2].plot(xs, p(xs), alpha=0.8, linewidth=2, color="#BE4A47FF")
  2808. axs[2].set_xlim(x.min(), x.max()+5)
  2809. y_min = y.min() - 1
  2810. y_max = y.max()
  2811. tick_interval = 1
  2812. y_max_rounded = np.ceil(y_max / tick_interval) * tick_interval
  2813. axs[2].set_ylim(y_min, y_max_rounded)
  2814. ###
  2815. axs[2].set_title(r'$ĝ_{/text{brain}}$ ~ Age', fontsize=65, y=1.1, pad=15)#, bbox=dict(facecolor='white', alpha=0.9, edgecolor="#f8b976", linewidth=2, boxstyle='round,pad=0.3'))
  2816. axs[2].set_xlabel('Age (years)', fontsize=55)
  2817. axs[2].set_ylabel('$g$-factor\npredicted from brain ($z$)', fontsize=50)
  2818. axs[2].tick_params(axis='x', labelsize=42)
  2819. axs[2].tick_params(axis='y', labelsize=42)
  2820. for ax in axs:
  2821. ax.spines['top'].set_visible(False)
  2822. ax.spines['right'].set_visible(False)
  2823. plt.tight_layout()
  2824. plt.subplots_adjust(wspace=0.34)
  2825. plt.savefig(os.path.join(fig_path, 'Fig6b_g_age_scatter.png'),
  2826. bbox_inches="tight",
  2827. pad_inches=1,
  2828. transparent=False,
  2829. facecolor="w",
  2830. edgecolor='w',
  2831. orientation='landscape',
  2832. format='png')
  2833. plt.show()
  2834. # %%
  2835. from scipy.stats import pearsonr, linregress
  2836. body_age = pd.read_csv(os.path.join(commonality_path, 'g_obs_pred_body_with_age.csv'))
  2837. brain_age = pd.read_csv(os.path.join(commonality_path, 'g_obs_pred_allmri_with_age.csv'))
  2838. def compute_metrics(x, y, name):
  2839. mask = ~(x.isna() | y.isna())
  2840. x_clean = x[mask]
  2841. y_clean = y[mask]
  2842. # Pearson r and p-value
  2843. r, p = pearsonr(x_clean, y_clean)
  2844. # R² from linear regression (same as r²)
  2845. slope, intercept, r_value, p_value, std_err = linregress(x_clean, y_clean)
  2846. print(f"\n{name}:")
  2847. print(f" r = {r:.3f}")
  2848. print(f" r² = {r**2:.3f}")
  2849. print(f" p = {p:.2e}")
  2850. # Compute for all three
  2851. compute_metrics(body_age['Age'], body_age['g_obs_test'], "Age vs observed g")
  2852. compute_metrics(body_age['Age'], body_age['g_pred_body_test'], "Age vs body-predicted g")
  2853. compute_metrics(brain_age['Age'], brain_age['g_pred_allmri_test'], "Age vs brain-predicted g")
  2854. # %% [markdown]
  2855. # # Figure 7
  2856. # %% [markdown]
  2857. # g-age commonality analysis: stacked bar plots
  2858. # %%
  2859. # Config
  2860. commonality_path = '/UK_BB/brainbody/commonality'
  2861. file_path = os.path.join(commonality_path,
  2862. 'commonality_results_decompos_brain_body_vs_age_sorted_stack_ind_combined_SORTED_BY_AGE_RENAMED.xlsx')
  2863. # Load data
  2864. df = pd.read_excel(file_path)
  2865. # Individual modalities (for bar plots)
  2866. df_individual = df[~df['Modality'].str.endswith('Stacked')].copy()
  2867. # Modality lists and order
  2868. modalities_to_plot = [
  2869. 'Brain dwMRI Stacked',
  2870. '3 Brain MRI Modalities Stacked',
  2871. 'Brain & Body Stacked',
  2872. 'Body Physiology Stacked',
  2873. 'Brain sMRI Stacked',
  2874. 'Brain rsMRI Stacked'
  2875. ]
  2876. modality_order = [
  2877. '3 Brain MRI Modalities Stacked',
  2878. 'Brain dwMRI Stacked',
  2879. 'Brain sMRI Stacked',
  2880. 'Brain & Body Stacked',
  2881. 'Brain rsMRI Stacked',
  2882. 'Body Physiology Stacked'
  2883. ]
  2884. # Colors
  2885. # For grouped bar plots
  2886. modality_colors = {
  2887. 'sMRI': '#f8b976',
  2888. 'dwMRI': '#0d648f',
  2889. 'rsMRI': '#4B6F5A99',
  2890. 'Body': '#C98E8CFF'
  2891. }
  2892. # Segment colors (shared)
  2893. color_unique_age = '#A8554EFF'
  2894. color_common = '#c2ada6'
  2895. color_unique_brain_body = '#4A7169FF'
  2896. # Case-insensitive mapping for individual modalities
  2897. lowercase_modality_mapping = {name.lower(): name for name in df_individual['Modality'].unique()}
  2898. modality_groups = {}
  2899. for group_name, mod_list in [('sMRI', modalities_smri),
  2900. ('dwMRI', modalities_dwmri),
  2901. ('rsMRI', modalities_rsmri),
  2902. ('Body', modalities_body)]:
  2903. modality_groups[group_name] = []
  2904. for mod in mod_list:
  2905. renamed_name = modality_names[mod]
  2906. lower_renamed = renamed_name.lower()
  2907. if lower_renamed in lowercase_modality_mapping:
  2908. modality_groups[group_name].append(lowercase_modality_mapping[lower_renamed])
  2909. else:
  2910. modality_groups[group_name].append(renamed_name)
  2911. print('dwMRI', len(modality_groups['dwMRI']))
  2912. print('rsMRI', len(modality_groups['rsMRI']))
  2913. print('sMRI', len(modality_groups['sMRI']))
  2914. print('Body', len(modality_groups['Body']))
  2915. # %% [markdown]
  2916. # ## Figure 7a
  2917. # %%
  2918. # Top Row - dwMRI and rsMRI
  2919. fig1, axs1 = plt.subplots(1, 2, figsize=(40, 40))
  2920. plot_order_fig1 = ['dwMRI', 'rsMRI']
  2921. for idx, group_name in enumerate(plot_order_fig1):
  2922. modality_list = modality_groups[group_name]
  2923. ax = axs1[idx]
  2924. # Filter data
  2925. group_data = df_individual[df_individual['Modality'].isin(modality_list)].copy()
  2926. # Sort by unique age variance
  2927. group_data = group_data.sort_values('Unique variance: brain/body', ascending=True)
  2928. # Extract data for plotting
  2929. y_labels = group_data['Modality'].values
  2930. y_labels = [label.replace('Full Correlation', 'FC').replace('Partial Correlation', 'PC').replace('Probabilistic', 'Prob.') for label in y_labels]
  2931. # Apply dictionary
  2932. rename_dict = {'Bone Densitometry (of heel)': 'Bone densitometry (heel)',
  2933. 'Abdominal Organ Composition by MRI': 'Abdominal organ composition (MRI)',
  2934. 'Abdominal Composition by MRI': 'Abdominal composition (MRI)',
  2935. 'Body Composition by DXA': 'Body composition (DXA)', 'Abdominal composition by MRI':
  2936. 'Abdominal Composition (MRI)',
  2937. 'Bone Size, Mineral and Density by DXA': 'BMD (DXA)',
  2938. 'Body Composition by Impedance': 'Body composition (impedance)' }
  2939. y_labels = [rename_dict.get(lbl, lbl) for lbl in y_labels]
  2940. y1 = group_data['Unique variance: age'].values*100
  2941. y2 = group_data['Common variance'].values*100
  2942. y3 = group_data['Unique variance: brain/body'].values*100
  2943. # Calculate percentages for annotations
  2944. total = y1 + y2 + y3
  2945. perc_u_age = (y1 / total * 100)
  2946. perc_common = (y2 / total * 100)
  2947. perc_u_brain_body = (y3 / total * 100)
  2948. marginal_u_age = group_data['Unique variance: age'].values
  2949. marginal_common = group_data['Common variance'].values
  2950. marginal_u_brain_body = group_data['Unique variance: brain/body'].values
  2951. # Get the proportion of g-age explained by brain/body
  2952. prop_age_exp_by_brain_body = group_data['g-age explained by brain/body'].values
  2953. bar_height = 1
  2954. # Create stacked horizontal bars
  2955. ax.barh(y_labels, y1, height=bar_height, color=color_unique_age, alpha=0.9, edgecolor='black', linewidth=0.9)
  2956. ax.barh(y_labels, y2, height=bar_height, left=y1, color=color_common, alpha=0.9, edgecolor='black', linewidth=0.9)
  2957. ax.barh(y_labels, y3, height=bar_height, left=y1+y2, color=modality_colors[group_name], alpha=0.9, edgecolor='black', linewidth=0.9)
  2958. # Add percentage annotations on each segment with font size dictionary
  2959. bar_annot_font_sizes = {
  2960. 'dwMRI': 30,
  2961. 'rsMRI': 40,
  2962. 'sMRI': 30,
  2963. 'Body': 30}
  2964. # Adjust thresholds
  2965. for i, (u_age, common, u_brain_body) in enumerate(zip(y1, y2, y3)):
  2966. if u_age > 0.001:
  2967. ax.text(u_age/2, i, f'{u_age:.1f}', ha='center', va='center',
  2968. color='black', fontsize=bar_annot_font_sizes[group_name])
  2969. if common > 0.001:
  2970. ax.text(u_age + common / 2, i, f'{common:.1f}', ha='center', va='center',
  2971. color='black', fontsize=bar_annot_font_sizes[group_name])
  2972. if u_brain_body:
  2973. pass #> 0.001:
  2974. #ax.text(u_age + common + u_brain_body/2, i, f'{m_u_brain_body:.2f}', ha='center', va='center', color='black', fontsize=bar_annot_font_sizes[group_name])
  2975. # Annotations
  2976. bbox_font_sizes = {
  2977. 'dwMRI': 38,
  2978. 'rsMRI': 48,
  2979. 'sMRI': 40,
  2980. 'Body': 40}
  2981. for i, prop_value in enumerate(prop_age_exp_by_brain_body):
  2982. prop_text = f'{prop_value:.1f}%'
  2983. # Position the text to the right of the plot
  2984. # transform=ax.get_yaxis_transform() to position relative to y-axis
  2985. ax.text(1.02, i, prop_text, ha='left', va='center',
  2986. fontsize=bbox_font_sizes[group_name], transform=ax.get_yaxis_transform(),
  2987. color='black', bbox=dict(boxstyle="round,pad=0.1", facecolor='white', alpha=0.7))
  2988. # Format the subplot
  2989. title_color = modality_colors[group_name]
  2990. ax.set_title(group_name,
  2991. fontsize=80, y=1,
  2992. bbox=dict(facecolor='white', alpha=0.9,
  2993. edgecolor=title_color, linewidth=5,
  2994. boxstyle='round,pad=0.3'))
  2995. y_labelsize_dict = {
  2996. 'dwMRI': 38,
  2997. 'rsMRI': 60,
  2998. 'sMRI': 60,
  2999. 'Body': 60}
  3000. ax.tick_params(axis='y', labelsize=y_labelsize_dict[group_name], length=0)
  3001. ax.tick_params(axis='x', labelsize=65)
  3002. ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%.1f'))
  3003. # Set x-axis limit
  3004. max_total = max(y1 + y2 + y3)
  3005. ax.set_xlim(0, max_total)
  3006. val1 = max_total / 3
  3007. val2 = 2 * max_total / 3
  3008. ax.xaxis.set_major_locator(mticker.FixedLocator([0, val1, val2, max_total]))
  3009. # Remove spines
  3010. for spine in ['top', 'right', 'left']:
  3011. ax.spines[spine].set_visible(False)
  3012. # Add common x axis label
  3013. fig1.text(0.57, 0.08, 'Variance of Cognition Explained (%)',
  3014. ha='center', fontsize=80)
  3015. # Add common title
  3016. fig1.suptitle('Commonality Analysis\nbetween Age and each Body and Brain Phenotype\nin Explaining Cognition (%)',
  3017. fontsize=100, y=0.98, x=0.55)
  3018. # Adjust layout
  3019. plt.tight_layout(rect=[0, 0.12, 1, 0.95])
  3020. plt.subplots_adjust(wspace=1.6, hspace=0.2)
  3021. # transform
  3022. save_fig(['png', 'svg'], os.path.join(fig_path, 'Fig7_part1_dwMRI_rsMRI_barplots'))
  3023. plt.show()
  3024. # %% [markdown]
  3025. # ## Figure 7b
  3026. # %%
  3027. # Bottom Row - sMRI and Body
  3028. fig2, axs2 = plt.subplots(1, 2, figsize=(55, 30))
  3029. axs2 = axs2.flatten()
  3030. plot_order_fig2 = ['sMRI', 'Body']
  3031. for idx, group_name in enumerate(plot_order_fig2):
  3032. modality_list = modality_groups[group_name]
  3033. ax = axs2[idx]
  3034. # Filter data
  3035. group_data = df_individual[df_individual['Modality'].isin(modality_list)].copy()
  3036. # Sort by unique age variance
  3037. group_data = group_data.sort_values('Unique variance: brain/body', ascending=True)
  3038. # Extract data for plotting
  3039. y_labels = group_data['Modality'].values
  3040. y_labels = [label.replace('Full Correlation', 'FC').replace('Partial Correlation', 'PC').replace('Probabilistic', 'Prob.') for label in y_labels]
  3041. # Apply dictionary
  3042. rename_dict = {'Bone Densitometry (of heel)': 'Bone densitometry (heel)',
  3043. 'Abdominal Organ Composition by MRI': 'Abdominal organ composition (MRI)',
  3044. 'Abdominal Composition by MRI': 'Abdominal composition (MRI)',
  3045. 'Body Composition by DXA': 'Body composition (DXA)', 'Abdominal composition by MRI':
  3046. 'Abdominal Composition (MRI)',
  3047. 'Bone Size, Mineral and Density by DXA': 'BMD (DXA)',
  3048. 'Body Composition by Impedance': 'Body composition (impedance)',
  3049. 'Desikan Grey/white Matter Contrast': 'Desikan Grey/White Matter Contrast',
  3050. 'Pulse Wave Analysis' : 'Pulse wave analysis',
  3051. 'Arterial Stiffness' : 'Arterial stiffness',
  3052. 'Renal & Hepatic' : 'Renal & hepatic',
  3053. 'ECG at Rest' : 'ECG at rest',
  3054. 'Bone Densitometry of heel' : 'Bone densitometry of heel',
  3055. 'Carotid Ultrasound' : 'Carotid ultrasound'}
  3056. y_labels = [rename_dict.get(lbl, lbl) for lbl in y_labels]
  3057. y1 = group_data['Unique variance: age'].values*100
  3058. y2 = group_data['Common variance'].values*100
  3059. y3 = group_data['Unique variance: brain/body'].values*100
  3060. # Calculate percentages for annotations
  3061. total = y1 + y2 + y3
  3062. perc_u_age = (y1 / total * 100)
  3063. perc_common = (y2 / total * 100)
  3064. perc_u_brain_body = (y3 / total * 100)
  3065. # Get the proportion of g-age explained by brain/body
  3066. prop_age_exp_by_brain_body = group_data['g-age explained by brain/body'].values
  3067. bar_height = 1
  3068. # Create stacked horizontal bars
  3069. ax.barh(y_labels, y1, height=bar_height, color=color_unique_age, alpha=0.9, edgecolor='black', linewidth=0.9)
  3070. ax.barh(y_labels, y2, height=bar_height, left=y1, color=color_common, alpha=0.9, edgecolor='black', linewidth=0.9)
  3071. ax.barh(y_labels, y3, height=bar_height, left=y1+y2, color=modality_colors[group_name], alpha=0.9, edgecolor='black', linewidth=0.9)
  3072. # Add percentage annotations on each segment with font size dictionary
  3073. bar_annot_font_sizes = {
  3074. 'dwMRI': 30,
  3075. 'rsMRI': 40,
  3076. 'sMRI': 40,
  3077. 'Body': 40}
  3078. # Adjust thresholds
  3079. for i, (u_age, common, u_brain_body) in enumerate(zip(y1, y2, y3)):
  3080. if u_age > 0.001:
  3081. ax.text(u_age/2, i, f'{u_age:.1f}', ha='center', va='center',
  3082. color='black', fontsize=bar_annot_font_sizes[group_name])
  3083. if common > 0.001:
  3084. ax.text(u_age + common/2, i, f'{common:.1f}', ha='center', va='center',
  3085. color='black', fontsize=bar_annot_font_sizes[group_name])
  3086. if u_brain_body:
  3087. pass #> 0.001:
  3088. #ax.text(u_age + common + u_brain_body/2, i, f'{m_u_brain_body:.2f}', ha='center', va='center', color='black', fontsize=bar_annot_font_sizes[group_name])
  3089. # Annotations
  3090. bbox_font_sizes = {
  3091. 'dwMRI': 38,
  3092. 'rsMRI': 48,
  3093. 'sMRI': 48,
  3094. 'Body': 48}
  3095. for i, prop_value in enumerate(prop_age_exp_by_brain_body):
  3096. prop_text = f'{prop_value:.1f}%'
  3097. # Position the text to the right of the plot
  3098. # transform=ax.get_yaxis_transform() to position relative to y-axis
  3099. ax.text(1.02, i, prop_text, ha='left', va='center',
  3100. fontsize=bbox_font_sizes[group_name], transform=ax.get_yaxis_transform(),
  3101. color='black', bbox=dict(boxstyle="round,pad=0.1", facecolor='white', alpha=0.7))
  3102. # Format the subplot
  3103. title_color = modality_colors[group_name] # pick edge color from dict
  3104. ax.set_title(group_name,
  3105. fontsize=80, y=1,
  3106. bbox=dict(facecolor='white', alpha=0.9,
  3107. edgecolor=title_color, linewidth=5,
  3108. boxstyle='round,pad=0.3'))
  3109. y_labelsize_dict = {
  3110. 'dwMRI': 38,
  3111. 'rsMRI': 60,
  3112. 'sMRI': 60,
  3113. 'Body': 60}
  3114. ax.tick_params(axis='y', labelsize=y_labelsize_dict[group_name], length=0)
  3115. ax.tick_params(axis='x', labelsize=65)
  3116. ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%.1f'))
  3117. # Set x-axis limit - extend slightly to accommodate the right-side annotations
  3118. max_total = max(y1 + y2 + y3)
  3119. ax.set_xlim(0, max_total)
  3120. val1 = max_total / 3
  3121. val2 = 2 * max_total / 3
  3122. ax.xaxis.set_major_locator(mticker.FixedLocator([0, val1, val2, max_total]))
  3123. # Remove spines
  3124. for spine in ['top', 'right', 'left']:
  3125. ax.spines[spine].set_visible(False)
  3126. # Add common x axis label
  3127. fig2.text(0.63, 0.08, 'Variance of Cognition Explained (%)',
  3128. ha='center', fontsize=80)
  3129. # Add circle legend
  3130. legend_elements_circles = [
  3131. plt.Circle((0,0), 0.5, facecolor=color_unique_age, alpha=0.9, label='Unique Variance: Age'),
  3132. plt.Circle((0,0), 0.5, facecolor=color_common, alpha=0.9, label='Common Variance'),
  3133. plt.Circle((0,0), 0.5, facecolor=modality_colors['sMRI'], alpha=0.9, label='Unique Variance: sMRI'),
  3134. plt.Circle((0,0), 0.5, facecolor=modality_colors['dwMRI'], alpha=0.9, label='Unique Variance: dwMRI'),
  3135. plt.Circle((0,0), 0.5, facecolor=modality_colors['rsMRI'], alpha=0.9, label='Unique Variance: rsMRI'),
  3136. plt.Circle((0,0), 0.5, facecolor=modality_colors['Body'], alpha=0.9, label='Unique Variance: Body'),
  3137. ]
  3138. leg2_top = fig2.legend(handles=legend_elements_circles,
  3139. loc='lower center', ncol=3,
  3140. fontsize=75, frameon=False, fancybox=False,
  3141. bbox_to_anchor=(0.6, -0.15), # Higher
  3142. handler_map={plt.Circle: HandlerPatch(patch_func=make_circle_legend)},
  3143. handletextpad=1.0, handleheight=1.0,
  3144. columnspacing=1.2, labelspacing=1.2, borderpad=0.5)
  3145. # Add rectangle legend
  3146. legend_element_rectangle = [
  3147. plt.Rectangle((0,0), 1, 1, facecolor='none', edgecolor='black', lw=2,
  3148. label='Age-related cognitive variance explained by each body and brain phenotype (%)')
  3149. ]
  3150. leg2_bottom = fig2.legend(handles=legend_element_rectangle,
  3151. loc='lower center', ncol=1,
  3152. fontsize=75, frameon=False, fancybox=False,
  3153. bbox_to_anchor=(0.55, -0.25), # Lower than circles
  3154. handletextpad=0.5, handleheight=1.0,
  3155. columnspacing=1.2, labelspacing=1.2, borderpad=0.5)
  3156. # Adjust layout
  3157. plt.tight_layout(rect=[0, 0.12, 1, 0.95])
  3158. plt.subplots_adjust(wspace=3, hspace=0.2)
  3159. # Save
  3160. save_fig(['png', 'svg'], os.path.join(fig_path, 'Fig7_part2_sMRI_Body_barplots'))
  3161. plt.show()

1Figures.ipynb at commit 54c5c9d, under MIT · at the source

Overview

Authors: Irina Buianova1, Narun Pat1
  1. Department of Psychology, University of Otago, Dunedin, New Zealand
Institutions: University of Otago (New Zealand)
Journal: npj aging, volume 12, issue 1, article 108
Dates: received 18 March 2026; accepted 17 July 2026; published online 31 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41514-026-00456-9 · PMID 42581321 · PMCID PMC13462990 · OpenAlex W7171937603
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), cognitive (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Physiology & signal measures, fMRI & imaging, Preprocessing
Keywords: Neurology, Neuroscience
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: Neurological Foundation of New Zealand (2350 PRG); The Ministry of Business, Innovation and Employment (UOA2421 and RTVU2403); Health Research Council of New Zealand (21/618 and 24/838)
Citations: not cited yet (Europe PMC); 197 references in the paper

Abstract

Epidemiological links between cognition and body physiology in aging are well established, but their strength and drivers remain unclear. Which physiological systems – from body composition to cardiovascular, pulmonary, renal, hepatic, immune, metabolic, and musculoskeletal – best predict cognition, and to what extent are cognition–body associations linked to brain variation across aging? We examined 19 physiological phenotypes alongside three neuroimaging modalities in over 30,000 UK Biobank participants. Machine learning models integrating body measures predicted cognition at r = 0.4, demonstrating a cognition–body covariation at 16%. Body composition and bone health emerged as the strongest predictors. Notably, 85.1% of cognition–body covariance overlapped with neuroimaging, especially white matter features. Moreover, 71.7% of cognition–age covariance was jointly shared with neuroimaging and physiology, and 96.8% was shared with either brain or body markers, or their overlap. Together, these findings clarify how body physiology and brain structure and function covary with cognitive aging.

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

Repository

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

HAM-lab-Otago-University/UKBiobank-Brain-Body

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 54c5c9ddf20dfe52285a9e50803b992724bd7c88, 2 June 2026
Languages: Jupyter (18), Python (18), R (3)
Size: 60 files, 39 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, 21 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (36 files), pandas (36 files), scikit-learn (32 files), SciPy (25 files), XGBoost (12 files), Matplotlib (11 files), seaborn (10 files), Nilearn (5 files), statsmodels (4 files), lavaan (3 files), data.table (2 files), ggplot2 (2 files), psych (2 files), NiBabel (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
41 files

Code availability

All analysis code is available at https://github.com/HAM-lab-Otago-University/UKBiobank-Brain-Body. All analyses can be reproduced using the provided code and UK Biobank data obtained through the appropriate access procedures.

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

Tracing map

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

What the map holds:

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

No dataset and no data link were found in the paper.

Data availability

The authors are not permitted to publicly share individual-level UK Biobank data due to legal and ethical restrictions. Access requests should be directed to the UK Biobank (https://www.ukbiobank.ac.uk/), subject to its data access policies and approval procedures. This manuscript is a computational study and did not generate new primary data.

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

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 2 keywords, 3 funders, 190 references.

Cite

This paper

Buianova, I., & Pat, N. (2026). Exploring the link between body physiology and cognition: the role of the brain and aging. npj aging, 12(1), 108. https://doi.org/10.1038/s41514-026-00456-9

BibTeX

@article{buianova2026exploring,
author = {Buianova, Irina and Pat, Narun},
title = {{Exploring the link between body physiology and cognition: the role of the brain and aging}},
journal = {npj aging},
year = {2026},
month = jul,
volume = {12},
number = {1},
pages = {108},
publisher = {Nature Publishing Group},
issn = {2731-6068},
doi = {10.1038/s41514-026-00456-9},
url = {https://doi.org/10.1038/s41514-026-00456-9},
pmid = {42581321},
pmcid = {PMC13462990}
}

RIS

TY - JOUR
AU - Buianova, Irina
AU - Pat, Narun
TI - Exploring the link between body physiology and cognition: the role of the brain and aging
T2 - npj aging
J2 - NPJ Aging
PY - 2026
DA - 2026/07/31
VL - 12
IS - 1
SP - 108
SN - 2731-6068
PB - Nature Publishing Group
DO - 10.1038/s41514-026-00456-9
UR - https://doi.org/10.1038/s41514-026-00456-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41514-026-00456-9",
"type": "article-journal",
"title": "Exploring the link between body physiology and cognition: the role of the brain and aging",
"container-title": "npj aging",
"author": [
{
"family": "Buianova",
"given": "Irina"
},
{
"family": "Pat",
"given": "Narun"
}
],
"container-title-short": "NPJ Aging",
"volume": "12",
"issue": "1",
"page": "108",
"DOI": "10.1038/s41514-026-00456-9",
"PMID": "42581321",
"PMCID": "PMC13462990",
"ISSN": "2731-6068",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41514-026-00456-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
31
]
]
}
}

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.7554/elife.108109 [code]
Multimodal MRI marker of cognition explains the association between cognition and mental health in the UK Biobank.
Journal: eLife
In common: lavaan, psych, data.table, 7 other tools, cognitive, 34 references
[2] doi:10.1038/s41467-026-73668-y [code]
Convergent and divergent brain-cognition development in early adolescence.
Journal: Nature communications
In common: Nilearn, NiBabel, ggplot2, 7 other tools, 10 references
[3] doi:10.1038/s41467-026-75661-x [code]
A neural signature of sleep deprivation in the human brain.
Journal: Nature communications
In common: Nilearn, NiBabel, statsmodels, 6 other tools, cognitive, 7 references
[4] doi:10.1162/imag.a.1282 [code]
Metabolic syndrome severity and the energetic cost of brain network transitions: A normative modeling study of accelerated brain aging.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NiBabel, statsmodels, 6 other tools, 6 references
[5] 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: XGBoost, psych, NiBabel, 9 other tools, 3 references
[6] doi:10.1162/imag.a.105 [code]
Right posterior theta reflects human parahippocampal phase resetting by salient cues during goal-directed navigation
Journal: n/a
In common: lavaan, psych, Nilearn, 9 other tools, cognitive, 1 reference
[7] doi:10.1038/s42003-026-09956-6 [code]
Linking changes in sulcal morphometry to cognitive development from childhood to adolescence.
Journal: Communications biology
In common: psych, Nilearn, NiBabel, 9 other tools, 2 references
[8] doi:10.21203/rs.3.rs-9914920/v1 [code]
Prediction of cognitive performance by demographics, sleep, and brain morphometry: machine learning findings from ENIGMA-Sleep Working Group
Journal: Research Square (preprint)
In common: XGBoost, Nilearn, NiBabel, 7 other tools, 2 references
[9] doi:10.1162/imag.a.1325 [code]
Decoding everyday levels of musical training from subcortical white-matter architecture.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: lavaan, psych, statsmodels, 7 other tools, 2 references
[10] doi:10.7554/elife.103097 [code]
Canonical neurodevelopmental trajectories of structural and functional manifolds.
Journal: eLife
In common: Nilearn, data.table, NiBabel, 7 other tools, 3 references

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.