OSCR

Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease.

Code ↔ Paper

9 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 9 matches
  1. [1] § Materials and methods › Data acquisition and pre-processing › PET and MRI ↔ notebooks/re_inter_c2mean_20.ipynb, lines 47–154 · score 0.79 · inferior cerebellar, Standardized uptake, co registered, volume weighted, SUVR images, Tau PET
  2. [2] § Results › Demographic, clinical, and cognitive profiles of tau subtypes ↔ notebooks/re_inter_c2mean_20.ipynb, lines 1837–1860 · score 0.68 · Phonemic Fluency, Semantic Fluency, CDR SB, Benson, MMSE, cognitive
  3. [3] § Materials and methods › Data acquisition and pre-processing › Clinical and cognitive data ↔ notebooks/re_inter_c2mean_20.ipynb, lines 1890–1960 · score 0.62 · amnestic predominant, dementia, MCI, genotyping, ApoE, PPA
  4. [4] § Materials and methods › LEADS study design ↔ notebooks/re_inter_c2mean_20.ipynb, lines 47–154 · score 0.60 · cognitively impaired, sporadic EOAD, cognitively normal, optimal, CN, phenotypic
  5. [5] § Results › Demographic, clinical, and cognitive profiles of tau subtypes ↔ R_code/table2.Rmd, lines 18–136 · score 0.59 · Phonemic Fluency, Semantic Fluency, pairwise, domain, S2, S3
  6. [6] § Materials and methods › Statistical analysis › Baseline subtype characterization ↔ tableone/tableone.py, lines 50–215 · score 0.58 · continuous variables, categorical variables, tableone, Python, variance, sex
  7. [7] § Results › Demographic, clinical, and cognitive profiles of tau subtypes ↔ notebooks/re_inter_c2mean_20.ipynb, lines 1890–1960 · score 0.55 · ApoE4, clinical phenotypes, PPA, PCA, diagnoses, amnestic
  8. [8] § Materials and methods › Subtype and stage inference › Model fitting and output ↔ notebooks/SuStaInWorkshop.ipynb, lines 369–391 · score 0.54 · model selection, cross validation, fold, likelihood, fitting, subtypes
  9. [9] § Materials and methods › Subtype and stage inference › Model fitting and output ↔ notebooks/re_inter_c2mean_20.ipynb, lines 240–257 · score 0.53 · model selection, cross validation, fold

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,657 lines · 137 KB · MIT · 6 matches

  1. # %% [markdown]
  2. # ---
  3. # title: ROI:intersection, C2mean (2 levels)
  4. # ---
  5. # %%
  6. # import
  7. import pandas as pd
  8. import warnings
  9. import numpy as np
  10. import seaborn as sns
  11. import matplotlib.pyplot as plt
  12. from matplotlib.colors import LinearSegmentedColormap
  13. import sys
  14. import pickle
  15. #import mpltern
  16. import plotly.express as px
  17. import plotly.graph_objects as go
  18. import matplotlib.image as mpimg
  19. import matplotlib.patches as mpatches
  20. from matplotlib.ticker import MaxNLocator
  21. from sklearn.metrics import confusion_matrix, cohen_kappa_score
  22. import statsmodels.formula.api as smf
  23. from PIL import Image
  24. # Load libraries
  25. sys.path.append('../')
  26. sys.path.insert(0,'/Users/linlin/Library/Mobile Documents/com~apple~CloudDocs/Desktop/2024/AD lab/dataexp/pySuStaIn/sim')
  27. import os
  28. import pySuStaIn
  29. from pathlib import Path
  30. from plotly.offline import init_notebook_mode
  31. import plotly.io as pio
  32. from plotly.graph_objs import *
  33. init_notebook_mode(connected=True)
  34. pio.renderers.default = 'notebook_connected'
  35. # import the python packages needed to generate simulated data for the tutorial
  36. #from termcolor import colored
  37. import statsmodels.api as sm
  38. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  39. from scipy.stats import ttest_1samp, f_oneway, kurtosis, skew , ttest_ind, norm, t,chi2_contingency, t
  40. # column order
  41. lobes = ['L_MTL', 'R_MTL', 'L_temporal', 'R_temporal', 'L_frontal', 'R_frontal',
  42. 'L_occipital', 'R_occipital','L_parietal', 'R_parietal' ]
  43. data_path = "/Users/mlin2/Desktop/RabLab/eoad_sustain_ml/data/"
  44. # %% [markdown]
  45. # # Project Overview
  46. # %% [markdown]
  47. # # Data
  48. # %% [markdown]
  49. #
  50. # **Subjects**:
  51. # - 450 participants = 365 sporadic EOAD subjects(cognitively impaired, amyloid-positive)+ 85 controls (cognitively normal, amyloid-negative)
  52. #
  53. # **Images**:
  54. # - Baseline tau-PET scans were smoothed to 6mm in native space, co-registered to MRI, and normalized using the inferior cerebellar gray as the reference region to derive standardized uptake volume ratio (SUVR) images.
  55. # - Parcellations based on the Desikan-Killiany atlas, covering 64 out of 72 regions (not used: left and right bank of the superior temporal sulcus, frontal pole, paracentral lobule, and temporal pole)
  56. #
  57. # **Regions of Interest (ROIs) and thresholds**:
  58. # - Parcellations were grouped into 10 lobar ROIs (left and right): medial temporal lobe (MTL), temporal, parietal, frontal, and occipital lobes.
  59. # - Volume-weighted SUVRs were calculated for each of the 10 lobar ROIs.
  60. # - The SUVR values are further standardized into z-scores using the lower-component and high-component intersection, and the hgher-mean component mean from 2-component Gaussian Mixture Models fitted on each of the 10 ROIs across all subjects (CN + EOAD). (bootstrap resample n = 5000), totaling to 20 event thresholds (2/ROI).
  61. # %% [markdown]
  62. # # Analysis
  63. #
  64. # This report takes as input a data frame including subjects' ID and visit, demographics, clinical variables, regional mean SUVR and tau z-score values, as well as SuStaIn subtype & stage assignment (number of clusters/C = 2~6). The following results are output (see outline):
  65. #
  66. # **0. Model output**
  67. # - MCMC trace diagram as C increases
  68. #
  69. # **1. SuStaIn-related results**
  70. # - subtype and stage assignment
  71. # - change in subtype assignment as C increases
  72. # - subtype probability by subtype and stage (ternary scatter)
  73. # - event visualization (positional variance & heatmap)
  74. #
  75. # **2. Baseline**
  76. # - average tau-PET SUVR images, pair-wise comparison, 1 v.s. rest comparison (generated via matlab)
  77. # - table 1
  78. # - laterality in each lobar ROI characterized by hemispheric asymmetry index
  79. # - Clincal phenotypes by subtype, stage distribution for each subtype, and by subtype+stage
  80. #
  81. # **3. Longitudinal**
  82. # - visit overview (and exclusion due to treatment)
  83. # - annual rate of change in SuStaIn stage
  84. # - percentage of subjects that progress/regress/remain in their SuStaIn stage
  85. # - subtype agreement between baseline visit and last/2nd visit
  86. #
  87. #
  88. #
  89. # %% [markdown]
  90. # # Model output
  91. # %% [markdown]
  92. # ## SuStaIn settings
  93. # The SuStaIn algorithm requires the following inputs.
  94. #
  95. # **data**
  96. # The data you want to run SuStaIn on, of size M subjects by N biomarkers. This needs to be z-scored in the way described in the previous section.
  97. #
  98. # **data_control**
  99. # used to z-scored all data.
  100. #
  101. # **Z_vals**
  102. # This is the set of z-scores you want to include for each biomarker. The more z-scores you use the longer the SuStaIn algorithm will take to run. Z_vals has size N biomarkers by Z z-scores. If you have more z-scores for some biomarkers than others you can simply leave zeros at the end of biomarker rows with fewer z-scores.
  103. #
  104. # **Z_max**
  105. # The maximum z-score reached at the end of the progression, with size N biomarkers by 1. I'd suggest choosing a value around the 95th percentile of your data but you can experiment with different values. I typically choose an integer for interpretability but you don't have to.
  106. #
  107. # **SuStaInLabels**
  108. # The names of the biomarkers you are using, for plotting purposes.
  109. #
  110. # **N_startpoints**
  111. # The number of startpoints to use when fitting the subtypes hierarchichally. I'd suggest using 25.
  112. #
  113. # **N_S_max**
  114. # The maximum number of subtypes to fit. I'd suggest starting with a lower number - maybe three - and then increasing that if you're getting a significantly better fit with the maximum number of subtypes. You can judge this roughly from the MCMC plot. To properly evaluate the optimal number of subtypes you need to run cross-validation.
  115. #
  116. # **N_iterations_MCMC**
  117. # The number of iterations for the MCMC sampling of the uncertainty in the progression pattern. I'd recommend using 1x10^5 or 1x10^6.
  118. #
  119. # **output_folder**
  120. # Choose an output folder for the results.
  121. #
  122. # **dataset_name**
  123. # Name the results files outputted by SuStaIn.
  124. #
  125. # **use_parellel_startpoints**
  126. # Boolean for whether or not to parallelize the startpoints.
  127. #
  128. # %%
  129. # eligibility check
  130. cn_exclude = ["LDS0220099", "LDS0110254", "LDS0670457", "LDS0370222"]
  131. eoad_exclude = ["LDS0100607", "LDS0160473", "LDS0220084", "LDS0220159", "LDS0360398", "LDS0730183", "LDS0980440", "LDS0990439", "LDS1770209", "LDS1770310", "LDS1770403", "LDS3600132", "LDS3600550", "LDS3600671"]
  132. ftp_data = pd.read_csv(data_path+'baseline_FTP_6mm.csv')
  133. # read z-scored data (2GMM w/o EOnonAD, standardized)
  134. zdata_full = pd.read_csv(data_path+'full_gmm2z_exclude.csv')
  135. zdata = zdata_full[zdata_full.visit==1]
  136. # to numpy
  137. data = zdata[lobes].values
  138. zdata_ctrl = zdata[zdata.dx == 'CN'][lobes].values
  139. zdata_eoad = zdata[zdata.dx == 'EOAD'][lobes].values
  140. zmax = zdata[lobes].apply(lambda x:np.percentile(x,95),axis=0).values#np.ceil(zdata[lobes].apply(max,axis=0)).values
  141. thre = pd.read_csv(data_path+'2gmm_thre_exclude.csv')
  142. thre = thre[['roi','intersection','c2mean']]
  143. #print('fixed thresholds: 2, 5, 10')
  144. print('ROI-specific thresholds: intersection, c2mean')
  145. np.round(pd.concat([thre,pd.DataFrame({'max':zmax})],axis=1),2)
  146. # %%
  147. zdata[lobes].apply(lambda x:np.percentile(x,95),axis=0).values
  148. # %%
  149. # Input the settings for z-score SuStaIn
  150. N = 10 # number of biomarkers
  151. M = 365 # number of observations ( e.g. subjects )
  152. M_control = 0 # number of these that are control subjects
  153. N_startpoints = 25 #25
  154. #N_S_gt = 1 #number of ground truth subtypes
  155. N_S_max = 6 #7
  156. N_iterations_MCMC = int(1e6) #1e4 / 1e6
  157. SuStaInLabels = lobes
  158. Z_vals = thre[['intersection','c2mean']].values#,'c2mean+sd']].values #np.array([[2,5,10]]*N) ## thre[['intersection','c2mean-sd','c2mean+sd']].values # Z-scores for each biomarker
  159. Z_max = zmax #z95 #np.array([10]*N) # maximum z-score
  160. output_folder = os.path.join(os.getcwd(),'inter_c2mean')#'inter_c2mean_sd_e6'#c2mean_sd_e6
  161. dataset_name = '2GMMz'
  162. sustain_input = pySuStaIn.ZscoreSustain(zdata_eoad,
  163. Z_vals,
  164. Z_max,
  165. SuStaInLabels,
  166. N_startpoints,
  167. N_S_max,
  168. N_iterations_MCMC,
  169. output_folder,
  170. dataset_name,
  171. True)
  172. print(f'reading output from: {output_folder}')
  173. # %%
  174. import tableone
  175. tableone.__version__
  176. # %% [markdown]
  177. # # Model running
  178. # %%
  179. # Load libraries
  180. import os
  181. import pySuStaIn
  182. import statsmodels.formula.api as smf
  183. from scipy import stats
  184. import sklearn.model_selection
  185. # import the python packages needed to generate simulated data for the tutorial
  186. import shutil
  187. import pickle
  188. from pathlib import Path
  189. import pylab
  190. # this needs to point to wherever the sim folder inside pySuStaIn is on your computer
  191. sys.path.insert(0,'/Users/linlin/Library/Mobile Documents/com~apple~CloudDocs/Desktop/2024/AD lab/dataexp/pySuStaIn/sim')
  192. # if you're running the notebook from within the existing structure you can use
  193. sys.path.insert(0,'../sim/')
  194. from simfuncs import generate_random_Zscore_sustain_model, generate_data_Zscore_sustain
  195. # %%
  196. # make the output directory if it's not already created
  197. if not os.path.isdir(output_folder):
  198. os.mkdir(output_folder)
  199. # %%
  200. sustain_input = pySuStaIn.ZscoreSustain(zdata_eoad,
  201. Z_vals,
  202. Z_max,
  203. SuStaInLabels,
  204. N_startpoints,
  205. N_S_max,
  206. N_iterations_MCMC,
  207. output_folder,
  208. dataset_name,
  209. True)
  210. # %%
  211. samples_sequence, \
  212. samples_f, \
  213. ml_subtype, \
  214. prob_ml_subtype, \
  215. ml_stage, \
  216. prob_ml_stage, \
  217. prob_subtype_stage = sustain_input.run_sustain_algorithm()
  218. # %% [markdown]
  219. # ## cross validation
  220. # %%
  221. # choose the number of folds - here i've used three for speed but i recommend 10 typically
  222. N_folds = 5
  223. # generate stratified cross-validation training and test set splits
  224. #labels = zdata.Diagnosis.values
  225. #cv = sklearn.model_selection.StratifiedKFold(n_splits=N_folds, shuffle=True)
  226. cv = sklearn.model_selection.KFold(n_splits=N_folds,shuffle=True,random_state=42)
  227. cv_it = cv.split(zdata_eoad)
  228. # SuStaIn currently accepts ragged arrays, which will raise problems in the future.
  229. # We'll have to update this in the future, but this will have to do for now
  230. test_idxs = []
  231. for train, test in cv_it:
  232. test_idxs.append(test)
  233. # %%
  234. # The code recognises the files are there already rather than re-running SuStaIn
  235. # This is useful if you want to increase the number of subtypes without
  236. # starting right from the beginning again
  237. CVIC, loglike_matrix = sustain_input.cross_validate_sustain_model(test_idxs)
  238. # %% [markdown]
  239. # ## subtype number choice
  240. # %%
  241. plt.rcParams['axes.titlesize'] = 16 # Set title font size
  242. plt.rcParams['axes.labelsize'] = 14 # Set label font size
  243. plt.rcParams['lines.markersize'] = 10 # Set marker size
  244. plt.rcParams['axes.titleweight'] = 'regular'#'bold' # Set title font weight to bold
  245. plt.rcParams['axes.labelweight'] = 'regular'#'bold' # Set label font weight to bold
  246. plt.rcParams['lines.markersize'] = 8 # Set marker size
  247. plt.rcParams['xtick.labelsize'] = 12 # Set tick label font size
  248. plt.rcParams['ytick.labelsize'] = 12 # Set tick label font size
  249. plt.rcParams['figure.titlesize'] = 18
  250. # %%
  251. # Print outputs
  252. print("CVIC for each subtype model: " + str(CVIC))
  253. print("Average test set log-likelihood for each subtype model: " + str(np.mean(loglike_matrix, 0)))
  254. # Create side-by-side subplots
  255. fig, axes = plt.subplots(1, 2, figsize=(15, 6))
  256. # Define xticks
  257. xticks = np.arange(1, N_S_max + 1)
  258. # Plot 1: CVIC with marker points
  259. axes[0].plot(xticks, CVIC, marker='o')
  260. axes[0].set_xticks(xticks)
  261. axes[0].set_ylabel('CVIC')
  262. axes[0].set_xlabel('Subtypes model')
  263. axes[0].set_title('CVIC')
  264. # Plot 2: Test Set Log-Likelihood
  265. df_loglike = pd.DataFrame(data=loglike_matrix, columns=["s_" + str(i + 1) for i in range(sustain_input.N_S_max)])
  266. df_loglike.boxplot(ax=axes[1], grid=False)
  267. axes[1].set_xticklabels(xticks)
  268. axes[1].set_ylabel('Log likelihood')
  269. axes[1].set_xlabel('Subtypes model')
  270. axes[1].set_title('Test set log-likelihood across folds')
  271. # Adjust layout
  272. plt.suptitle("ROI thresholds: intersection/C2mean (MTL-hippocampus)")
  273. plt.tight_layout()
  274. plt.show()
  275. # %% [markdown]
  276. # ## cross-validated sequence
  277. # %%
  278. s = 3
  279. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(s) + '.pickle'
  280. pk = pd.read_pickle(pickle_filename_s)
  281. ml_sequence_em = pk['ml_sequence_EM']
  282. # Translate sequence into lobe-level format while preserving the unique sequence order
  283. max_level_ct = 2
  284. def translate_sequence_by_lobe_then_level(seq, thresholds_dict, max_level_ct):
  285. lobe_list = []
  286. level_list = []
  287. for i in range(max_level_ct):
  288. for lobe, thresholds in thresholds_dict.items():
  289. if len(thresholds) > i: # Ensure there's at least one threshold
  290. lobe_list.append(lobe) # Add the lobe to the lobe_list
  291. level_list.append(i+1) # Add the index of the first item
  292. original_list = [f'{lobe_list[i]} level {level_list[i]}' for i in range(len(lobe_list))]
  293. return [original_list[int(s)] for s in seq]
  294. # Create thresholds dictionary from Z_vals
  295. thresholds_dict = {
  296. lobes[i]: Z_vals[i, Z_vals[i] > 0].tolist() # Exclude 0s, use only valid thresholds
  297. for i in range(len(lobes))
  298. }
  299. # Process each subtype
  300. subtype_orderings = [
  301. translate_sequence_by_lobe_then_level(seq, thresholds_dict, max_level_ct)
  302. for seq in ml_sequence_em
  303. ]
  304. # Sort by prevalence
  305. sorted_prevalence = np.round(sorted(pk['samples_f'].mean(axis=1), reverse=True), 2)
  306. prevalence_order = np.argsort(-np.round(pk['samples_f'].mean(axis=1), 2))
  307. sorted_subtype_orderings = [subtype_orderings[i] for i in prevalence_order]
  308. # %%
  309. raw_stage = [ml_sequence_em[i] for i in prevalence_order]
  310. raw_stage
  311. # %%
  312. #this part estimates cross-validated positional variance diagrams
  313. for i in range(3,4):
  314. figs, axs = sustain_input.combine_cross_validated_sequences(i+1, N_folds)
  315. # %%
  316. # Function to make sure each subplot is on a separate row
  317. def arrange_subplots_on_rows(fig, axs, biomarker_orders):
  318. # Get the total number of subplots
  319. try:
  320. num_plots = len(axs.flatten())
  321. except:
  322. num_plots = 1
  323. # Re-create the figure with the correct number of rows
  324. fig, new_axs = plt.subplots(nrows=num_plots, ncols=1, figsize=(15, 5 * num_plots))
  325. for i in range(num_plots):
  326. ax = axs.flatten()[i] if num_plots != 1 else axs
  327. img = ax.get_images()
  328. if img:
  329. biomarker_order = biomarker_orders[i]
  330. confus_matrix_c = img[0].get_array().data
  331. # Determine the axis for plotting based on num_plots
  332. target_ax = new_axs[i] if num_plots != 1 else new_axs
  333. im = target_ax.imshow(confus_matrix_c[biomarker_order, :, :], interpolation='nearest')
  334. # Set up biomarker labels
  335. biomarker_labels = lobes
  336. bml = [biomarker_labels[k] for k in biomarker_order]
  337. target_ax.set_xticks(ax.get_xticks())
  338. target_ax.set_xticklabels(ax.get_xticklabels(), fontsize=18) # Adjust x tick label size
  339. target_ax.set_yticks(ax.get_yticks())
  340. target_ax.set_yticklabels(bml, fontsize=18, ha='right') # Adjust y tick label size
  341. # Keep the original title with adjusted font size
  342. target_ax.set_title(ax.get_title(), fontsize=18)
  343. return fig, new_axs
  344. biomarker_orders = [list(map(int,raw_stage[0][raw_stage[0] < 10]))] * 6
  345. new_fig, new_axs = arrange_subplots_on_rows(fig, axs, biomarker_orders)
  346. # Adjust layout to prevent overlapping
  347. new_fig.tight_layout()
  348. # Display all the new figures
  349. plt.show(new_fig)
  350. # %% [markdown]
  351. # ## model likelihood
  352. # %%
  353. # Initialize figure for MCMC diagnostics (Trace and Histogram)
  354. fig, axs = plt.subplots( N_S_max-1, 2, figsize=(15, 5*( N_S_max-1))) # 5 subtypes, each with 2 plots (trace and histogram)
  355. # List of colors for each subtype
  356. subtype_colors = ['orange', 'green', 'red', 'purple', 'brown'] # List of colors for subtypes 1 to 5
  357. for s in range(1, N_S_max): # Loop over 1 to 5 for subtypes 2 to 6 (index adjusted)
  358. #print(s)
  359. pickle_filename_s = f"{output_folder}/pickle_files/{dataset_name}_subtype{s}.pickle"
  360. pickle_filepath = Path(pickle_filename_s)
  361. # Load pickle file
  362. with open(pickle_filepath, 'rb') as pickle_file:
  363. loaded_variables = pickle.load(pickle_file)
  364. samples_likelihood = loaded_variables["samples_likelihood"]
  365. # Plot MCMC Trace (subplot 1)
  366. axs[s-1, 0].plot(range(len(samples_likelihood)), samples_likelihood, color=subtype_colors[s-1])
  367. axs[s-1, 0].set_xlabel("MCMC Samples")
  368. axs[s-1, 0].set_ylabel("Log Likelihood")
  369. axs[s-1, 0].set_title(f"Trace Plot - {s+1} Subtypes ") # Axis label should be 2 to 6
  370. # Plot Histogram (subplot 2)
  371. axs[s-1, 1].hist(samples_likelihood, bins=30, alpha=0.6, color=subtype_colors[s-1])
  372. axs[s-1, 1].set_xlabel("Log Likelihood")
  373. axs[s-1, 1].set_ylabel("Frequency")
  374. axs[s-1, 1].set_title(f"Histogram - {s+1} Subtypes ") # Axis label should be 2 to 6
  375. plt.tight_layout()
  376. plt.show()
  377. # %% [markdown]
  378. # Fig. 1: Histograms of model log-likelihood estimated by MCMC samples (n = 10e6). As the number of subtype increases from 2 to 6, model likelihood improves.
  379. # %% [markdown]
  380. # # SuStaIn-related output
  381. # %%
  382. df = pd.DataFrame(zdata_eoad,columns=lobes)
  383. s = 2
  384. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(s) + '.pickle'
  385. pk = pd.read_pickle(pickle_filename_s)
  386. for variable in ['ml_subtype', # the assigned subtype
  387. 'prob_ml_subtype', # the probability of the assigned subtype
  388. 'ml_stage', # the assigned stage
  389. 'prob_ml_stage',]: # the probability of the assigned stage
  390. # add SuStaIn output to dataframe
  391. df.loc[:,variable] = pk[variable]
  392. # probability for each subject of being each subtype
  393. for i in range(s):
  394. df.loc[:,'prob_S%s'%i] = pk['prob_subtype'][:,i]
  395. # make current subtypes (0 and 1) 1 and 2 instead
  396. df.loc[:,'ml_subtype'] = df.ml_subtype.values + 1
  397. # convert "Stage 0" subjects to subtype 0
  398. #df.loc[df.ml_stage==0,'ml_subtype'] = 0 # not excluded
  399. # change prob_sx to handle diff number of subtypes
  400. df.reset_index()
  401. df['prob_S2'] = 1 - df['prob_S0']- df['prob_S1'] #- df['prob_S2'] #- df['prob_S3'] - df['prob_S4']
  402. df['subj'] = zdata[zdata.dx=='EOAD'].subj.values
  403. # adjustment to reassign subtype based on prevalence
  404. # Step 1: Count current subtype prevalence
  405. prevalence = df['ml_subtype'].value_counts().sort_values(ascending=False)
  406. # Step 2: Create a mapping for reassigning subtypes in order of prevalence
  407. subtype_mapping = {old: new for new, old in enumerate(prevalence.index, start=1)}
  408. # Step 3: Update ml_subtype using the mapping
  409. df['ml_subtype'] = df['ml_subtype'].map(subtype_mapping)
  410. # Step 4: Update probability columns to reflect the new subtype order
  411. prob_columns = ['prob_S0', 'prob_S1', 'prob_S2']
  412. prob_mapping = {0: 'prob_S0', 1: 'prob_S1', 2: 'prob_S2'}
  413. # Reassign probabilities based on new subtype mapping
  414. for new_subtype, old_subtype in subtype_mapping.items():
  415. df[f'prob_s{int(new_subtype) - 1}'] = df[prob_mapping[old_subtype - 1]]
  416. # Drop old probability columns
  417. for col in prob_columns:
  418. if col not in [f'prob_s{new - 1}' for new in subtype_mapping.values()]:
  419. df.drop(columns=col, inplace=True)
  420. #print(df.columns)
  421. print('baseline subtype count (pre-exclusion):')
  422. df.ml_subtype.value_counts()
  423. # %% [markdown]
  424. # ## change in subtype assignment (alluvial)
  425. # %%
  426. # Filter the data for EOAD and initialize dataframe
  427. alludf = zdata[zdata.dx == 'EOAD'][['subj']]
  428. # Number of splits
  429. N_S_max = 6
  430. # Populate alludf with subtype assignments
  431. for i in range(1, N_S_max):
  432. s = str(i)
  433. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + s + '.pickle'
  434. pk = pd.read_pickle(pickle_filename_s)
  435. alludf[s] = pk['ml_subtype']
  436. alludf[s] = alludf[s].astype(int) + 1
  437. # Calculate category orders for each column
  438. category_orders = {}
  439. for col in [str(i) for i in range(1, N_S_max)]:
  440. # Calculate order based on prevalence
  441. order = alludf[col].value_counts().sort_values(ascending=False).index.tolist()
  442. category_orders[col] = order
  443. # Create the parallel categories plot
  444. fig = px.parallel_categories(
  445. alludf,
  446. dimensions=[str(i) for i in range(1, N_S_max)], # Columns ['1', '2', ..., N_S_max-1]
  447. color=alludf[str(N_S_max - 1)], # Color by the last split column
  448. color_continuous_scale=px.colors.sequential.Blackbody,
  449. labels={
  450. '1': 'Subtype 2',
  451. '2': 'Subtype 3',
  452. '3': 'Subtype 4',
  453. '4': 'Subtype 5',
  454. '5': 'Subtype 6'
  455. }
  456. )
  457. # Sort each column in ascending order of category prevalence
  458. fig.update_traces(dimensions=[
  459. {"categoryorder": "array", "categoryarray": category_orders[col]}
  460. for col in [str(i) for i in range(1, N_S_max)]
  461. ])
  462. #fig.update_layout(
  463. # font=dict(size=1) # adjust this to show label
  464. #)
  465. # Use iplot for inline rendering
  466. fig.show()
  467. # %%
  468. # Step 1: Build category orders for each split (most to least frequent)
  469. category_orders = {}
  470. for col in [str(i) for i in range(1, N_S_max)]:
  471. order = alludf[col].value_counts().sort_values(ascending=False).index.astype(str).tolist()
  472. category_orders[col] = order
  473. # Step 2: Build dimensions list with categoryarray set for sorting
  474. dimensions = []
  475. for i in range(1, N_S_max):
  476. col = str(i)
  477. dim = dict(
  478. label="",#f"Subtype {i+1}",
  479. values=alludf[col].astype(str),
  480. categoryorder="array",
  481. categoryarray=category_orders[col] # custom sort
  482. )
  483. dimensions.append(dim)
  484. # Step 3: Map subtype at split 2 to color
  485. color_map = {
  486. '1': "#1f449c", # Dark Blue
  487. '2': "#32ABA6", # Teal Green
  488. '3': "#f05039", # Red
  489. }
  490. colors = alludf['2'].astype(str).map(color_map)
  491. # Step 4: Create alluvial plot
  492. fig = go.Figure(data=[
  493. go.Parcats(
  494. dimensions=dimensions,
  495. line={'color': colors},
  496. hoveron='color',
  497. labelfont={'size': 12, 'family': 'Arial'},
  498. tickfont={'size': 10, 'family': 'Arial'},
  499. arrangement='freeform',
  500. bundlecolors=True,
  501. sortpaths='forward'
  502. )
  503. ])
  504. fig.update_layout(
  505. title="",#'Alluvial Plot of Subtype Assignments Across Splits',
  506. margin=dict(l=50, r=50, t=50, b=50)
  507. )
  508. fig.show()
  509. # %% [markdown]
  510. # Fig. 2: Change in subtype assignments as C increases from 2 to 6; ordered by number of scans within each subtype, most to least from top to bottom in each column, and colored by subtype assignment when C = 6 (top), or when C = 3 (botto).
  511. # %% [markdown]
  512. # ## subtype and stage assignment for C=3
  513. # %%
  514. # Ensure there are no infinite values
  515. df.replace([np.inf, -np.inf], np.nan, inplace=True)
  516. # Copy the DataFrame
  517. ptdf = df.copy()
  518. # Explicitly cast ml_subtype to object to allow assignment of 'Poorly_Fit'
  519. ptdf['ml_subtype'] = ptdf['ml_subtype'].astype(object)
  520. # Create a backup of ml_subtype for hue coloring
  521. ptdf['ml_subtype0'] = ptdf['ml_subtype']
  522. # Replace subtype values with 'Poorly_Fit' for prob_ml_subtype <= 0.5
  523. ptdf.loc[ptdf['prob_ml_subtype'] <= 0.5, 'ml_subtype'] = 'Poorly_Fit'
  524. # Set order of categories based on the subtype column
  525. subtype_order = [1, 2, 3, 'Poorly_Fit']
  526. # Define custom colors for each subtype
  527. custom_palette = {
  528. 1: "#1f449c", # Dark Blue
  529. 2: "#32ABA6", # Green
  530. 3: "#f05039", # Red
  531. #"Poorly_Fit": "#7f7f7f" # Grey
  532. }
  533. # Plot the graph using seaborn
  534. sns.set(style="whitegrid")
  535. plt.figure(figsize=(12, 8)) # Increase figure size for better readability
  536. # Create the swarm plot with hue, ordered by ml_subtype
  537. ax = sns.swarmplot(
  538. data=ptdf,
  539. x='ml_subtype',
  540. y='ml_stage',
  541. hue='ml_subtype0',
  542. palette=custom_palette,
  543. order=subtype_order,
  544. size = 7
  545. )
  546. # Remove legend
  547. plt.legend().remove()
  548. # Adjust font size of labels and title
  549. plt.xticks(fontsize=12)
  550. plt.yticks(fontsize=12)
  551. plt.xlabel('Subtype', fontsize=14)
  552. plt.ylabel('Stage', fontsize=14)
  553. plt.title('Subtype and Stage Assignment (C=3)', fontsize=16)
  554. # Set y-tick marks to range from 0 to 30
  555. ax.set_yticks(range(0, 21, 1))
  556. ax.set_ylim(-0.5, 20.5)
  557. # Calculate total count for each subtype
  558. subtype_counts = ptdf['ml_subtype'].value_counts()
  559. # Annotate the plot with the total count for each subtype
  560. for i, subtype in enumerate(subtype_order):
  561. if subtype in subtype_counts:
  562. count = subtype_counts[subtype]
  563. ax.text(i, ptdf['ml_stage'].max() + 0.9, f'Count: {count}',
  564. ha='center', fontsize=12, color='black', weight='bold')
  565. plt.show()
  566. # Print poorly fit subjects (including stage 0)
  567. print('Poorly fit subjects (could be stage 0):')
  568. poorly_fit_subjects = zdata[zdata.dx == 'EOAD'].loc[(ptdf['prob_ml_subtype'] <= 0.5).values, 'subj'].values
  569. print(poorly_fit_subjects)
  570. # %% [markdown]
  571. # ## stage distribution description
  572. # %%
  573. # Function to calculate IQR
  574. def calculate_iqr(x):
  575. return np.percentile(x, 75) - np.percentile(x, 25)
  576. # Group by 'ml_subtype' and calculate descriptive statistics
  577. descriptive_stats = (
  578. ptdf[ptdf.ml_subtype.isin([1, 2, 3])]
  579. .groupby('ml_subtype')['ml_stage']
  580. .apply(lambda x: pd.Series({
  581. 'q1': round(np.percentile(x.dropna(), 25), 2),
  582. 'q3': round(np.percentile(x.dropna(), 75), 2),
  583. 'iqr': round(np.percentile(x.dropna(), 75) - np.percentile(x.dropna(), 25), 2),
  584. 'mean': round(x.mean(), 2),
  585. 'std': round(x.std(), 2),
  586. 'kurtosis': round(kurtosis(x, nan_policy='omit'), 2),
  587. 'skewness': round(skew(x, nan_policy='omit'), 2)
  588. }))
  589. )
  590. # Reset index for better readability
  591. descriptive_stats = descriptive_stats.reset_index()
  592. # Pivot the table to wide format
  593. descriptive_stats_wide = descriptive_stats.pivot(
  594. index='ml_subtype',
  595. columns='level_1',
  596. values='ml_stage'
  597. ).reset_index()
  598. # Rename the columns for better readability
  599. descriptive_stats_wide.columns.name = None # Remove the column group name
  600. descriptive_stats_wide = descriptive_stats_wide.rename_axis(None, axis=1) # Remove index axis name
  601. # Display the wide-format table
  602. print("Descriptive statistics for SuStaIn stage by subtype:")
  603. descriptive_stats_wide[['ml_subtype', 'q1', 'q3','iqr', 'mean', 'std','kurtosis', 'skewness']]
  604. # %% [markdown]
  605. # Fig. 3: Stage assignment by subtype for C=3 and descriptive statistics for stage distributions. Poorly fit scans are those with probability belonging to any subtype below 0.5, and are colored by their final assignments.
  606. # %% [markdown]
  607. # ## subtype probability
  608. # %%
  609. # Define marker styles for each subtype
  610. marker_styles = {1.: "o", 2.: "s", 3.: "^"} # Circle for S0, square for S1, triangle for S2
  611. # Create the ternary scatterplot
  612. fig = plt.figure(figsize=(8, 5))
  613. ax = fig.add_subplot(projection="ternary")
  614. # Store handles for the legend
  615. handles = []
  616. # Plot each subtype with a unique marker
  617. for subtype, marker in marker_styles.items():
  618. subset = df[df['ml_subtype'] == subtype]
  619. scatter = ax.scatter(
  620. subset['prob_s0'],
  621. subset['prob_s1'],
  622. subset['prob_s2'],
  623. marker=marker, # Assign unique marker
  624. c=subset['ml_stage'], # Color by 'ml_stage'
  625. cmap=sns.color_palette("ch:s=-.1,r=.6", as_cmap=True).reversed(), # Gradient from light to dark
  626. s=80, # Marker size
  627. alpha=.9, # Transparency
  628. edgecolors="none", # Ensure edges don't cover points
  629. vmin=0, # Set minimum color value
  630. vmax=20 # Set maximum color value
  631. )
  632. # Add marker to legend
  633. handles.append(plt.Line2D([0], [0], marker=marker, color='w', markerfacecolor='gray', markersize=10, label=f"{int(subtype)}"))
  634. # Add inner dashed-line triangle for probabilities < 0.5
  635. # Inner triangle vertices (all probabilities = 0.5)
  636. vertices = [
  637. (0.5, 0.5, 0), # Between S0 and S1
  638. (0.5, 0, 0.5), # Between S0 and S2
  639. (0, 0.5, 0.5), # Between S1 and S2
  640. (0.5, 0.5, 0) # Close the triangle
  641. ]
  642. # Extract x, y, z coordinates from vertices
  643. x_coords, y_coords, z_coords = zip(*vertices)
  644. # Draw the dashed line for the inner region
  645. ax.plot(x_coords, y_coords, z_coords, linestyle="--", color="black", linewidth=1)
  646. # Set labels for each corner (all horizontal now)
  647. ax.set_tlabel("Subtype 1", fontsize=14, rotation=0, ha='center')
  648. ax.set_llabel("Subtype 2", fontsize=14, rotation=0, ha='center')
  649. ax.set_rlabel("Subtype 3", fontsize=14, rotation=0, ha='center')
  650. ax.set_rlim(-0.01, 1.01)
  651. ax.set_tlim(-0.01, 1.01)
  652. ax.set_llim(-0.01, 1.01)
  653. # Remove the grid
  654. ax.grid(False)
  655. # Adjust title font size
  656. plt.title("Ternary Probability Scatter", fontsize=18)
  657. # Add colorbar with padding
  658. cbar = plt.colorbar(ax.collections[0], pad=0.15)
  659. cbar.ax.tick_params(labelsize=14) # Adjust colorbar tick font size
  660. cbar.set_label("Stage", fontsize=14)
  661. cbar.set_ticks(np.arange(0, 21, 5)) # Force integer ticks from 0 to 20
  662. # Add legend for marker shapes
  663. plt.legend(handles=handles, loc='upper right', title="Subtype", fontsize=12, title_fontsize=12, frameon=False)
  664. # Display the plot
  665. plt.show()
  666. # %% [markdown]
  667. # Fig. 4: ternary scatterplot showing subtype probability of each subject, colored by stage, marked by subtype. Subjects with lower stages tend to have lower subtype probabilities. The region within the inner triangle indicates poorly fit.
  668. #
  669. # reading ternary plot: https://grapherhelp.goldensoftware.com/Graphs/Reading_Ternary_Diagrams.htm
  670. # %%
  671. # Create reversed colormap
  672. cmap = sns.color_palette("ch:s=-.1,r=.6", as_cmap=True).reversed()
  673. # Get unique, sorted integer ml_stage values
  674. stages = sorted(df['ml_stage'].unique())
  675. # Create stage-to-color mapping
  676. colors = [cmap(i / (len(stages) - 1)) for i in range(len(stages))]
  677. stage_color_map = dict(zip(stages, colors))
  678. # Plot
  679. plt.figure(figsize=(10, 6))
  680. # Seaborn boxplot with custom palette
  681. sns.boxplot(
  682. data=df,
  683. x='ml_stage',
  684. y='prob_ml_subtype',
  685. palette=stage_color_map,
  686. flierprops=dict(
  687. marker='o',
  688. markersize=5,
  689. linestyle='none', # Remove connecting line
  690. )
  691. )
  692. # Manually match flier (outlier) colors to box colors
  693. ax = plt.gca()
  694. for i, artist in enumerate(ax.artists):
  695. color = colors[i]
  696. # Set box color
  697. artist.set_facecolor(color)
  698. # Set the color of flier markers for this box
  699. line_collection = ax.lines[i * 6 + 4] # Outlier line index pattern
  700. line_collection.set_color(color)
  701. # Axis formatting
  702. plt.ylim([0,1.01])
  703. plt.title("Subtype Probability by Stage")
  704. plt.xlabel("SuStaIn Stage")
  705. plt.ylabel("Subtype Probability")
  706. plt.xticks(ticks=range(len(stages)), labels=[int(s) for s in stages]) # Ensure integer ticks
  707. plt.tight_layout()
  708. plt.show()
  709. # %% [markdown]
  710. # Fig. 4 add: subtype probability across stage
  711. # %% [markdown]
  712. # ## event ordering
  713. # principal:
  714. # order by lobe and then level, from lowest to highest stage. e.g., 1 is L MTL level 1, 2 is R MTL level 1, ...
  715. # %%
  716. s = 2
  717. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(s) + '.pickle'
  718. pk = pd.read_pickle(pickle_filename_s)
  719. ml_sequence_em = pk['ml_sequence_EM']
  720. # %%
  721. # Translate sequence into lobe-level format while preserving the unique sequence order
  722. max_level_ct = 2
  723. def translate_sequence_by_lobe_then_level(seq, thresholds_dict, max_level_ct):
  724. lobe_list = []
  725. level_list = []
  726. for i in range(max_level_ct):
  727. for lobe, thresholds in thresholds_dict.items():
  728. if len(thresholds) > i: # Ensure there's at least one threshold
  729. lobe_list.append(lobe) # Add the lobe to the lobe_list
  730. level_list.append(i+1) # Add the index of the first item
  731. original_list = [f'{lobe_list[i]} level {level_list[i]}' for i in range(len(lobe_list))]
  732. return [original_list[int(s)] for s in seq]
  733. # Create thresholds dictionary from Z_vals
  734. thresholds_dict = {
  735. lobes[i]: Z_vals[i, Z_vals[i] > 0].tolist() # Exclude 0s, use only valid thresholds
  736. for i in range(len(lobes))
  737. }
  738. # Process each subtype
  739. subtype_orderings = [
  740. translate_sequence_by_lobe_then_level(seq, thresholds_dict, max_level_ct)
  741. for seq in ml_sequence_em
  742. ]
  743. # Function to color code the levels
  744. def color_text(text, level):
  745. if level == 1:
  746. return colored(text.split()[0], 'blue')
  747. elif level == 2:
  748. return colored(text.split()[0], 'magenta')
  749. elif level == 3:
  750. return colored(text.split()[0], 'red')
  751. else:
  752. return text
  753. print('Subtype event orderings: ')
  754. print(colored('Level 1', 'blue'))
  755. print(colored('Level 2', 'magenta'))
  756. #print(colored('Level 3\n', 'red'))
  757. # Helper function to wrap lines
  758. def wrap_text(text, width=80):
  759. lines = []
  760. while len(text) > width:
  761. split_index = text[:width].rfind(" -> ") # Split at the last " -> " within the width
  762. if split_index == -1:
  763. split_index = width # Fallback to a hard split
  764. lines.append(text[:split_index])
  765. text = text[split_index + 4:] # Skip over " -> "
  766. lines.append(text)
  767. return lines
  768. # Sort by prevalence
  769. sorted_prevalence = np.round(sorted(pk['samples_f'].mean(axis=1), reverse=True), 2)
  770. prevalence_order = np.argsort(-np.round(pk['samples_f'].mean(axis=1), 2))
  771. sorted_subtype_orderings = [subtype_orderings[i] for i in prevalence_order]
  772. # Process and color the text for each subtype
  773. for i in range(len(subtype_orderings)):
  774. ordering = sorted_subtype_orderings[i]
  775. print(colored(f"Subtype {i+1} ({np.round(sorted_prevalence[i]*100, 3)}%) Ordering:", attrs=['bold', 'underline']))
  776. colored_sequence = [] # Collect the colored sequence for this subtype
  777. for event in ordering:
  778. lobe, level = event.rsplit(" ", 1) # Split into lobe and level
  779. level_number = int(level.split()[-1]) # Extract level number
  780. colored_sequence.append(color_text(event, level_number)) # Add the colored event
  781. # Join the events with "->" and wrap the output
  782. full_sequence = " -> ".join(colored_sequence)
  783. wrapped_lines = wrap_text(full_sequence)
  784. for line in wrapped_lines:
  785. print(line)
  786. # %%
  787. raw_stage = [ml_sequence_em[i] for i in prevalence_order]
  788. raw_stage
  789. # %%
  790. s = 2 # 1 split = 2 subtypes
  791. M = len(zdata_eoad)
  792. # get the sample sequences and f
  793. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(s) + '.pickle'
  794. pk = pd.read_pickle(pickle_filename_s)
  795. samples_sequence = pk["samples_sequence"]
  796. samples_f = pk["samples_f"]
  797. # use this information to plot the positional variance diagrams
  798. figs, axs = pySuStaIn.ZscoreSustain._plot_sustain_model_fix(sustain_input,samples_sequence,samples_f,M,subtype_order=prevalence_order,
  799. biomarker_labels=lobes,
  800. #biomarker_colours=sns.color_palette('tab10'),
  801. biomarker_order=range(10))#list(map(int,raw_stage[0][raw_stage[0] < 10])))
  802. # Adjust figure size to prevent clustering of labels
  803. try:
  804. for fig in figs:
  805. fig.set_size_inches(15, 10) # Increase the size of each figure
  806. except:
  807. figs.set_size_inches(15, 10) # Increase the size of each figure
  808. # %%
  809. # Function to make sure each subplot is on a separate row
  810. def arrange_subplots_on_rows(fig, axs, biomarker_orders):
  811. # Get the total number of subplots
  812. try:
  813. num_plots = len(axs.flatten())
  814. except:
  815. num_plots = 1
  816. # Re-create the figure with the correct number of rows
  817. fig, new_axs = plt.subplots(nrows=num_plots, ncols=1, figsize=(15, 5 * num_plots))
  818. for i in range(num_plots):
  819. ax = axs.flatten()[i] if num_plots != 1 else axs
  820. img = ax.get_images()
  821. if img:
  822. biomarker_order = biomarker_orders[i]
  823. confus_matrix_c = img[0].get_array().data
  824. # Determine the axis for plotting based on num_plots
  825. target_ax = new_axs[i] if num_plots != 1 else new_axs
  826. im = target_ax.imshow(confus_matrix_c[biomarker_order, :, :], interpolation='nearest')
  827. # Set up biomarker labels
  828. biomarker_labels = lobes
  829. bml = [biomarker_labels[k] for k in biomarker_order]
  830. target_ax.set_xticks(ax.get_xticks())
  831. target_ax.set_xticklabels(ax.get_xticklabels(), fontsize=18) # Adjust x tick label size
  832. target_ax.set_yticks(ax.get_yticks())
  833. target_ax.set_yticklabels(bml, fontsize=18, ha='right') # Adjust y tick label size
  834. # Keep the original title with adjusted font size
  835. target_ax.set_title(f'Subtype {i+1} (n={prevalence.values[i]})', fontsize=18)#ax.get_title
  836. return fig, new_axs
  837. biomarker_orders = [list(map(int,raw_stage[0][raw_stage[0] < 10]))] * 6
  838. new_fig, new_axs = arrange_subplots_on_rows(fig, axs, biomarker_orders)
  839. # Adjust layout to prevent overlapping
  840. new_fig.tight_layout()
  841. # Display all the new figures
  842. plt.show(new_fig)
  843. # %%
  844. s = 2 # 1 split = 2 subtypes
  845. M = len(zdata_eoad)
  846. # get the sample sequences and f
  847. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(s) + '.pickle'
  848. pk = pd.read_pickle(pickle_filename_s)
  849. samples_sequence = pk["samples_sequence"]
  850. samples_f = pk["samples_f"]
  851. # use this information to plot the positional variance diagrams
  852. figs, axs = pySuStaIn.ZscoreSustain._plot_sustain_model_fix(sustain_input,samples_sequence,samples_f,M,subtype_order=prevalence_order,
  853. biomarker_labels=lobes,
  854. #biomarker_colours=sns.color_palette('tab10'),
  855. biomarker_order=range(10))
  856. # Adjust figure size to prevent clustering of labels
  857. try:
  858. for fig in figs:
  859. fig.set_size_inches(15, 10) # Increase the size of each figure
  860. except:
  861. figs.set_size_inches(15, 10) # Increase the size of each figure
  862. # %%
  863. biomarker_orders = [list(map(int, filter(lambda x: x < 10, arr))) for arr in raw_stage]
  864. new_fig, new_axs = arrange_subplots_on_rows(fig, axs, biomarker_orders)
  865. # Adjust layout to prevent overlapping
  866. new_fig.tight_layout()
  867. # Display all the new figures
  868. plt.show(new_fig)
  869. # %% [markdown]
  870. # Fig. 5: Positional variance graph for each subtype. Red and magenta indicates crossing the lowest to highest threshold in an ROI, uncertainty indicated by blurriness of the square. Visualized as by the most prevalent subtype's event order, or each subtype's
  871. #
  872. # (Ignore: The raw count is inaccurate because it was derived from multiplying the total by a rounded percentage).
  873. # %% [markdown]
  874. # ## event heatmap
  875. # %%
  876. print('most prevalent')
  877. custom_cmap = LinearSegmentedColormap.from_list("custom_cmap", ["blue", "red"], N=2)
  878. # Translate raw stages to regional levels, accounting for regions with one level
  879. def translate_raw_stage(seq, thresholds_dict, max_level_ct):
  880. lobe_list = []
  881. level_list = []
  882. for i in range(max_level_ct):
  883. for lobe, thresholds in thresholds_dict.items():
  884. if len(thresholds) > i: # Ensure there's at least one threshold
  885. lobe_list.append(lobe) # Add the lobe to the lobe_list
  886. level_list.append(i+1) # Add the index of the first item
  887. original_list = [f'{lobe_list[i]} {level_list[i]}' for i in range(len(lobe_list))]
  888. return [original_list[int(s)] for s in seq]
  889. # Create thresholds dictionary from Z_vals
  890. thresholds_dict = {
  891. lobes[i]: Z_vals[i, Z_vals[i] > 0].tolist() # Exclude 0s, use only valid thresholds
  892. for i in range(len(lobes))
  893. }
  894. # Generate regional events for each subtype
  895. regional_events_by_subtype = [
  896. translate_raw_stage(rs, thresholds_dict, max_level_ct) for rs in raw_stage
  897. ]
  898. # Determine the event order for the most prevalent subtype
  899. most_prevalent_subtype_index = 0#np.argmax(pk['samples_f'].mean(axis=1)) # Assuming `pk` contains prevalence data
  900. reference_event_order = regional_events_by_subtype[most_prevalent_subtype_index]
  901. # Sort all events based on the most prevalent subtype's event order
  902. shared_event_order = {event: idx for idx, event in enumerate(reference_event_order)}
  903. # Initialize a dictionary to store stage matrices by subtype
  904. stage_matrices_by_subtype = {}
  905. # Process each subtype's data
  906. for subtype, events in enumerate(regional_events_by_subtype, start=1):
  907. # Align event indices with the shared x-axis order
  908. aligned_events = sorted(events, key=lambda event: shared_event_order[event])
  909. event_to_index = {event: i for i, event in enumerate(aligned_events)}
  910. subtype_subjects = df[df['ml_subtype'] == subtype]
  911. n_subjects = len(subtype_subjects)
  912. n_events = len(aligned_events)
  913. # Initialize the binary matrix for this subtype
  914. stage_matrix = np.zeros((n_subjects, n_events), dtype=int)
  915. # Determine fulfilled events for each subject in this subtype
  916. for subj_idx, (_, row) in enumerate(subtype_subjects.iterrows()):
  917. for lobe in lobes:
  918. z_score = row[lobe]
  919. thresholds = thresholds_dict.get(lobe, [float('inf')]) # Get thresholds for the current lobe
  920. # Iterate over thresholds dynamically
  921. for level, threshold in enumerate(thresholds, start=1):
  922. if z_score > threshold: # Check if the threshold is fulfilled
  923. # Construct the event name
  924. event_name = f"{lobe} {level}"
  925. if event_name in event_to_index:
  926. stage_matrix[subj_idx, event_to_index[event_name]] = 1
  927. # Store the stage matrix and aligned events for this subtype
  928. stage_matrices_by_subtype[subtype] = (stage_matrix, aligned_events, subtype_subjects)
  929. # Plot heatmaps for each subtype
  930. for subtype, (stage_matrix, aligned_events, subtype_subjects) in stage_matrices_by_subtype.items():
  931. # Convert the matrix into a DataFrame with shared x-axis event order
  932. stage_df = pd.DataFrame(stage_matrix, columns=aligned_events)
  933. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  934. # Sort by ml_stage for heatmap visualization
  935. stage_df_sorted = stage_df.sort_values(by='ml_stage').reset_index(drop=True)
  936. # Plot the heatmap
  937. plt.figure(figsize=(15, 5))
  938. ax = sns.heatmap(
  939. stage_df_sorted.iloc[:, :-1], # Exclude ml_stage for the heatmap
  940. cmap=custom_cmap, cbar=True, yticklabels=False, linewidths=0.5
  941. )
  942. # Access and customize the colorbar
  943. colorbar = ax.collections[0].colorbar
  944. colorbar.set_ticks([0, 1]) # Set tick positions
  945. colorbar.set_ticklabels(['0', '1']) # Set tick labels
  946. #plt.title(f"Subtype {subtype} ({round(sorted_prevalence[subtype-1]*100, 3)}%)", fontsize=16)
  947. plt.title(f'Subtype {subtype} (n={prevalence.values[subtype-1]})',fontsize=16)
  948. plt.xlabel("Events (Shared Order)", fontsize=14)
  949. plt.ylabel("Subjects", fontsize=14)
  950. plt.show()
  951. # %%
  952. print('subtype-specific heatmap')
  953. custom_cmap = LinearSegmentedColormap.from_list("custom_cmap", ["blue", "red"], N=2)
  954. # Translate raw stages to regional levels, accounting for regions with one level
  955. def translate_raw_stage(seq, thresholds_dict, max_level_ct):
  956. lobe_list = []
  957. level_list = []
  958. for i in range(max_level_ct):
  959. for lobe, thresholds in thresholds_dict.items():
  960. if len(thresholds) > i: # Ensure there's at least one threshold
  961. lobe_list.append(lobe) # Add the lobe to the lobe_list
  962. level_list.append(i+1) # Add the index of the first item
  963. original_list = [f'{lobe_list[i]} {level_list[i]}' for i in range(len(lobe_list))]
  964. return [original_list[int(s)] for s in seq]
  965. # Create thresholds dictionary from Z_vals
  966. thresholds_dict = {
  967. lobes[i]: Z_vals[i, Z_vals[i] > 0].tolist() # Exclude 0s, use only valid thresholds
  968. for i in range(len(lobes))
  969. }
  970. # Generate regional events for each subtype
  971. regional_events_by_subtype = [
  972. translate_raw_stage(rs, thresholds_dict, max_level_ct) for rs in raw_stage
  973. ]
  974. # Initialize a dictionary to store stage matrices by subtype
  975. stage_matrices_by_subtype = {}
  976. # Process each subtype's data
  977. for subtype, events in enumerate(regional_events_by_subtype, start=1):
  978. # Map subtype-specific events to indices
  979. event_to_index = {event: i for i, event in enumerate(events)}
  980. subtype_subjects = df[df['ml_subtype'] == subtype]
  981. n_subjects = len(subtype_subjects)
  982. n_events = len(events)
  983. # Initialize the binary matrix for this subtype
  984. stage_matrix = np.zeros((n_subjects, n_events), dtype=int)
  985. # Determine fulfilled events for each subject in this subtype
  986. for subj_idx, (_, row) in enumerate(subtype_subjects.iterrows()):
  987. for lobe in lobes:
  988. z_score = row[lobe]
  989. thresholds = thresholds_dict.get(lobe, [float('inf')]) # Get thresholds for the current lobe
  990. # Iterate over thresholds dynamically
  991. for level, threshold in enumerate(thresholds, start=1):
  992. if z_score > threshold: # Check if the threshold is fulfilled
  993. # Construct the event name
  994. event_name = f"{lobe} {level}"
  995. if event_name in event_to_index:
  996. stage_matrix[subj_idx, event_to_index[event_name]] = 1
  997. # Store the stage matrix and events for this subtype
  998. stage_matrices_by_subtype[subtype] = (stage_matrix, events, subtype_subjects)
  999. # Plot heatmaps for each subtype
  1000. for subtype, (stage_matrix, events, subtype_subjects) in stage_matrices_by_subtype.items():
  1001. # Convert the matrix into a DataFrame with subtype-specific event order
  1002. stage_df = pd.DataFrame(stage_matrix, columns=events)
  1003. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  1004. # Sort by ml_stage for heatmap visualization
  1005. stage_df_sorted = stage_df.sort_values(by='ml_stage').reset_index(drop=True)
  1006. # Plot the heatmap
  1007. plt.figure(figsize=(15, 5))
  1008. ax = sns.heatmap(
  1009. stage_df_sorted.iloc[:, :-1], # Exclude ml_stage for the heatmap
  1010. cmap=custom_cmap, cbar=True, yticklabels=False, linewidths=0.5
  1011. )
  1012. ax.set_xticklabels(ax.get_xticklabels(), fontsize=16)
  1013. # Access and customize the colorbar
  1014. colorbar = ax.collections[0].colorbar
  1015. colorbar.set_ticks([0, 1]) # Set tick positions
  1016. colorbar.set_ticklabels(['0', '1']) # Set tick labels
  1017. #plt.title(f"Subtype {subtype} ({round(sorted_prevalence[subtype-1]*100, 3)}%)", fontsize=16)
  1018. plt.title('',fontsize=16)#'Subtype {subtype} (n={prevalence.values[subtype-1]})'
  1019. plt.xlabel("", fontsize=14)#Events
  1020. plt.ylabel("", fontsize=14)#Subjects
  1021. plt.show()
  1022. # %% [markdown]
  1023. # Fig. 6: Event heatmap showing the event progression for subjects in each subtype, based on the event ordering of the most prevalent subtype (top); and the event ordering of each subtype (bottom). Red indicates that a particular event is fulfilled = the z-score in that ROI has surpassed a specific threshold; blue indicates unfulfilled. The rows are ordered from top to bottom by patient stage assignments from low to high, the columns are ordered from left to right by events from early to late (low stage to high stage). Visuzliaed as by most prevalent subtype's event sequence, or each subtype's
  1024. # %% [markdown]
  1025. # ## imperfection box
  1026. # %%
  1027. from scipy.stats import t
  1028. avg_imperfection_by_subtype = {}
  1029. std_imperfection_by_subtype = {}
  1030. ci_imperfection_by_subtype = {}
  1031. all_imperfections = []
  1032. for subtype, (stage_matrix, events, subtype_subjects) in stage_matrices_by_subtype.items():
  1033. stage_df = pd.DataFrame(stage_matrix, columns=events)
  1034. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  1035. stage_df_sorted = stage_df.sort_values(by='ml_stage').reset_index(drop=True)
  1036. imperfection_counts = []
  1037. for _, row in stage_df_sorted.iterrows():
  1038. subject_stage = int(row['ml_stage'])
  1039. binary_values = row.iloc[:-1].values
  1040. left_imperfections = np.sum(binary_values[:subject_stage] == 0)
  1041. right_imperfections = np.sum(binary_values[subject_stage:] == 1)
  1042. total_imperfections = left_imperfections + right_imperfections
  1043. imperfection_counts.append(total_imperfections)
  1044. all_imperfections.append(total_imperfections)
  1045. n = len(imperfection_counts)
  1046. mean = np.mean(imperfection_counts)
  1047. std = np.std(imperfection_counts, ddof=1)
  1048. se = std / np.sqrt(n)
  1049. t_val = t.ppf(0.975, df=n-1)
  1050. ci_low, ci_high = mean - t_val * se, mean + t_val * se
  1051. avg_imperfection_by_subtype[subtype] = mean
  1052. std_imperfection_by_subtype[subtype] = std
  1053. ci_imperfection_by_subtype[subtype] = (ci_low, ci_high)
  1054. # Print subtype results
  1055. print("Subtype Imperfection Counts (Mean ± SD, 95% CI):")
  1056. for subtype in avg_imperfection_by_subtype:
  1057. mean = avg_imperfection_by_subtype[subtype]
  1058. std = std_imperfection_by_subtype[subtype]
  1059. ci_low, ci_high = ci_imperfection_by_subtype[subtype]
  1060. print(f"Subtype {subtype}: {mean:.2f} ± {std:.2f}, 95% CI [{ci_low:.2f}, {ci_high:.2f}]")
  1061. # Overall stats
  1062. n_all = len(all_imperfections)
  1063. mean_all = np.mean(all_imperfections)
  1064. std_all = np.std(all_imperfections, ddof=1)
  1065. se_all = std_all / np.sqrt(n_all)
  1066. t_val_all = t.ppf(0.975, df=n_all-1)
  1067. ci_low_all, ci_high_all = mean_all - t_val_all * se_all, mean_all + t_val_all * se_all
  1068. print(f"\nOverall: {mean_all:.2f} ± {std_all:.2f}, 95% CI [{ci_low_all:.2f}, {ci_high_all:.2f}]")
  1069. # %%
  1070. from scipy.stats import f_oneway
  1071. # Prepare data for ANOVA
  1072. imperfection_lists = [] # List of imperfection scores for each subtype
  1073. for subtype, (stage_matrix, events, subtype_subjects) in stage_matrices_by_subtype.items():
  1074. stage_df = pd.DataFrame(stage_matrix, columns=events)
  1075. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  1076. stage_df_sorted = stage_df.sort_values(by='ml_stage').reset_index(drop=True)
  1077. imperfection_counts = []
  1078. for _, row in stage_df_sorted.iterrows():
  1079. subject_stage = int(row['ml_stage'])
  1080. binary_values = row.iloc[:-1].values
  1081. left_imperfections = np.sum(binary_values[:subject_stage] == 0)
  1082. right_imperfections = np.sum(binary_values[subject_stage:] == 1)
  1083. total_imperfections = left_imperfections + right_imperfections
  1084. imperfection_counts.append(total_imperfections)
  1085. imperfection_lists.append(imperfection_counts)
  1086. # Perform one-way ANOVA
  1087. f_stat, p_value = f_oneway(*imperfection_lists)
  1088. print(f"\nANOVA F-statistic: {f_stat:.4f}, p-value: {p_value:.4f}")
  1089. if p_value < 0.05:
  1090. print("Result: Statistically significant differences across subtypes.")
  1091. else:
  1092. print("Result: No statistically significant differences across subtypes.")
  1093. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  1094. import pandas as pd
  1095. # Create flat data for Tukey HSD
  1096. imperfection_scores = []
  1097. subtype_labels = []
  1098. for subtype, (stage_matrix, events, subtype_subjects) in stage_matrices_by_subtype.items():
  1099. stage_df = pd.DataFrame(stage_matrix, columns=events)
  1100. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  1101. stage_df_sorted = stage_df.sort_values(by='ml_stage').reset_index(drop=True)
  1102. for _, row in stage_df_sorted.iterrows():
  1103. subject_stage = int(row['ml_stage'])
  1104. binary_values = row.iloc[:-1].values
  1105. left_imperfections = np.sum(binary_values[:subject_stage] == 0)
  1106. right_imperfections = np.sum(binary_values[subject_stage:] == 1)
  1107. total_imperfections = left_imperfections + right_imperfections
  1108. imperfection_scores.append(total_imperfections)
  1109. subtype_labels.append(subtype)
  1110. # Create a DataFrame for clarity
  1111. df_tukey = pd.DataFrame({
  1112. 'Imperfection_Score': imperfection_scores,
  1113. 'Subtype': subtype_labels
  1114. })
  1115. # Perform Tukey HSD test
  1116. tukey = pairwise_tukeyhsd(
  1117. endog=df_tukey['Imperfection_Score'],
  1118. groups=df_tukey['Subtype'],
  1119. alpha=0.05
  1120. )
  1121. print("\nPost-hoc Pairwise Comparisons (Tukey HSD):")
  1122. print(tukey.summary())
  1123. # Optionally, you can also plot the confidence intervals
  1124. import matplotlib.pyplot as plt
  1125. tukey.plot_simultaneous()
  1126. plt.title("Tukey HSD: Pairwise Comparisons of Imperfection Scores by Subtype")
  1127. plt.show()
  1128. # %%
  1129. k = len(imperfection_lists) # number of subtypes
  1130. n_total = sum(len(lst) for lst in imperfection_lists)
  1131. df_between = k - 1
  1132. df_within = n_total - k
  1133. print(f"Degrees of freedom for F-statistic: df_between={df_between}, df_within={df_within}")
  1134. # %%
  1135. # Store per-region imperfection data
  1136. region_imperfection_by_subtype = {}
  1137. for subtype, (stage_matrix, events, subtype_subjects) in stage_matrices_by_subtype.items():
  1138. stage_df = pd.DataFrame(stage_matrix, columns=events)
  1139. stage_df['ml_stage'] = subtype_subjects['ml_stage'].values
  1140. n_subjects = stage_df.shape[0]
  1141. n_events = len(events)
  1142. # Initialize array to hold imperfection count per region/event
  1143. region_imperfections = np.zeros(n_events, dtype=int)
  1144. for _, row in stage_df.iterrows():
  1145. subj_stage = int(row['ml_stage'])
  1146. binary_vals = row.iloc[:-1].values # exclude ml_stage
  1147. for i in range(n_events):
  1148. val = binary_vals[i]
  1149. if subj_stage > i and val == 0:
  1150. region_imperfections[i] += 1 # Expected 1, but got 0
  1151. elif subj_stage <= i and val == 1:
  1152. region_imperfections[i] += 1 # Expected 0, but got 1
  1153. region_imperfection_by_subtype[subtype] = {
  1154. 'events': events,
  1155. 'counts': region_imperfections,
  1156. 'percent': region_imperfections / n_subjects * 100
  1157. }
  1158. # Example: print summary
  1159. for subtype, data in region_imperfection_by_subtype.items():
  1160. print(f"\nSubtype {subtype} Region Imperfections:")
  1161. for event, count, pct in zip(data['events'], data['counts'], data['percent']):
  1162. print(f"{event}: {count} subjects ({pct:.1f}%)")
  1163. # %%
  1164. for subtype, data in region_imperfection_by_subtype.items():
  1165. # Unpack and sort by percentage
  1166. events = np.array(data['events'])
  1167. counts = np.array(data['counts'])
  1168. percents = np.array(data['percent'])
  1169. sorted_indices = np.argsort(percents)[::-1] # Descending sort
  1170. sorted_events = events[sorted_indices]
  1171. sorted_percents = percents[sorted_indices]
  1172. # Plot
  1173. plt.figure(figsize=(10, 6)) # Less wide
  1174. bars = plt.bar(range(len(sorted_events)), sorted_percents)
  1175. # Annotate each bar with percentage
  1176. for i, p in enumerate(sorted_percents):
  1177. plt.text(i, p + 0.5, f"{p:.1f}%", ha='center', va='bottom', fontsize=12) # Bigger annotation
  1178. plt.xticks(ticks=range(len(sorted_events)), labels=sorted_events, rotation=90, fontsize=14) # Bigger tick labels
  1179. plt.ylim(0, 70) # Give space for text annotations
  1180. plt.grid(axis='y', linestyle='--', alpha=0.5)
  1181. plt.yticks(fontsize=14) # Bigger y-ticks
  1182. plt.tight_layout()
  1183. plt.show()
  1184. # %% [markdown]
  1185. # Fig. 6 add: Regional imperfection
  1186. # %% [markdown]
  1187. # # Baseline
  1188. # %% [markdown]
  1189. # ## subtype and stage all scans + additional variables
  1190. # %%
  1191. warnings.filterwarnings("ignore")
  1192. # subtype and stage all scans
  1193. new_df = zdata_full[zdata_full.dx == 'EOAD']
  1194. typedf = new_df[['subj','visit','ftp_date']]
  1195. typedf1 = typedf[typedf.visit==1].copy()
  1196. typedf2 = typedf[typedf.visit!=1].copy()
  1197. for i in range(1, N_S_max): #i is the number of split, so starting from 1 split = 2 subtypes
  1198. ##Subtype assignment
  1199. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(i) + '.pickle'
  1200. pk = pd.read_pickle(pickle_filename_s)
  1201. subtypes = pk['ml_subtype']
  1202. stages = pk['ml_stage']
  1203. styp_prob = pk['prob_ml_subtype']
  1204. #subtypes = [subtypes[i] if styp_prob[i] >= 0.5 else 98 for i in range(len(subtypes))]
  1205. typedf1[str(i+1)] = subtypes
  1206. # Add 1 to the subtype (assuming it's indexed from 0 in original)
  1207. typedf1[str(i+1)] = typedf1[str(i+1)].astype(int) + 1
  1208. # Assign the ml_stage
  1209. stage = str(i+1) + 's'
  1210. typedf1[stage] = stages
  1211. typedf1[stage] = typedf1[stage].astype(int)
  1212. subtype_prob_name = f'{str(i+1)}_subtype_prob'
  1213. typedf1[subtype_prob_name] = pk['prob_ml_subtype']
  1214. stage_prob_name = f'{str(i+1)}_stage_prob'
  1215. typedf1[stage_prob_name] = pk['prob_ml_stage']
  1216. pickle_filename_s = output_folder + '/pickle_files/' + dataset_name + '_subtype' + str(i) + '.pickle'
  1217. pk = pd.read_pickle(pickle_filename_s)
  1218. new_dat = new_df[new_df.visit!=1][lobes].values
  1219. s_seq = pk['samples_sequence']
  1220. s_f = pk['samples_f']
  1221. n_s = len(new_df) - 365
  1222. ml_subtype, prob_ml_subtype, ml_stage, prob_ml_stage, prob_subtype, prob_stage, prob_subtype_stage = \
  1223. pySuStaIn.ZscoreSustain.subtype_and_stage_individuals_newData(sustain_input, data_new = new_dat,
  1224. samples_sequence = s_seq, samples_f = s_f , N_samples = n_s)
  1225. # if the probability is smaller than 0.5, poorly fit
  1226. subtypes = ml_subtype#[ml_subtype[i] if prob_ml_subtype[i] >= 0.5 else 98 for i in range(len(ml_subtype))]
  1227. typedf2[str(i+1)] = subtypes
  1228. # Add 1 to the subtype (assuming it's indexed from 0 in original)
  1229. typedf2[str(i+1)] = typedf2[str(i+1)].astype(int) + 1
  1230. # Assign the ml_stage
  1231. stage = str(i+1) + 's'
  1232. typedf2[stage] = ml_stage
  1233. typedf2[stage] = typedf2[stage].astype(int)
  1234. subtype_prob_name = f'{str(i+1)}_subtype_prob'
  1235. typedf2[subtype_prob_name] = prob_ml_subtype
  1236. stage_prob_name = f'{str(i+1)}_stage_prob'
  1237. typedf2[stage_prob_name] = prob_ml_stage
  1238. typedf = pd.concat([typedf1,typedf2],axis=0,ignore_index=True)
  1239. typedf['fname'] = typedf.apply(lambda row:f"wr{row['subj']}_FTP_{row['ftp_date']}_suvr-infcblgm.nii",axis=1)
  1240. # %%
  1241. # set those with poor model fit to subtype '99'
  1242. typedf99 = typedf.copy()
  1243. for n_clusters in range(2, 7):
  1244. prob_col = f"{n_clusters}_subtype_prob"
  1245. subtype_col = f"{n_clusters}"
  1246. typedf99.loc[typedf99[prob_col] < 0.5, subtype_col] = 99
  1247. # %%
  1248. #save baseline and full assignment for matlab modeling
  1249. #typedf99[typedf99.visit==1].to_csv(data_path+'final_inter_c2mean_2_bl_ss.csv',index=False)\
  1250. #typedf99 = pd.read_csv(data_path+'ss_assignment/final_inter_c2mean_2_full_ss.csv')
  1251. # %%
  1252. ss3 = pd.read_csv(data_path+'ss_assignment/final_inter_c2mean_2_full_ss.csv')
  1253. ss3 = ss3[['subj', 'visit', 'ftp_date', '3', '3s', '3_subtype_prob', '3_stage_prob', 'fname']]
  1254. ss3.columns = ['subj', 'visit', 'ftp_date', 'subtype', 'stage', 'subtype_prob', 'stage_prob', 'fname']
  1255. ss3['subtype'] = ss3['subtype'].astype(int)
  1256. ss3['stage'] = ss3['stage'].astype(int)
  1257. # existing dataframe with other variables
  1258. data = pd.read_csv(data_path + 'full_combined.csv') # Convert ftp_date to datetime in both DataFrames
  1259. data['ftp_date'] = pd.to_datetime(data['ftp_date'], errors='coerce')
  1260. ss3['ftp_date'] = pd.to_datetime(ss3['ftp_date'], errors='coerce')
  1261. combined = pd.merge(data, ss3, on=['subj', 'ftp_date'], how='right')
  1262. combined = combined.sort_values(by=['subj', 'ftp_date']).reset_index(drop=True)
  1263. combined['visit'] = combined.groupby('subj').cumcount() + 1
  1264. #subtype_mapping = {old: new for new, old in enumerate(prevalence.index, start=1)}
  1265. #ombined['subtype'] = combined['subtype'].map(subtype_mapping)
  1266. combined['poorly'] = combined['subtype_prob'] < 0.5
  1267. combined_bl = combined[combined['visit']==1]
  1268. prevalence = combined_bl['subtype'].value_counts().sort_values(ascending=False)
  1269. # for the baseline df, filter out those scans with poor model fit, and output count
  1270. combined_exclude = combined_bl[~combined_bl.poorly]
  1271. print('baseline prevalence')
  1272. print(combined_exclude.subtype.value_counts())
  1273. print(f'total scans (after exclude poor fit): {len(combined)}')
  1274. print('variables:')
  1275. print(combined.columns)
  1276. # %% [markdown]
  1277. # ## average image
  1278. # %%
  1279. # configuration
  1280. img_path = '/Users/linlin/Desktop/brain stuff/graphs/'#'/Users/linlin/Desktop/brain stuff/data/'
  1281. type = 'ROI:intersection-C2mean'# threshold type
  1282. img_folder = img_path+'inter_c2mean/rainbows'# mean img folder
  1283. typedf_subset = typedf99[typedf99.visit==1][['subj', '2', '3', '4', '5', '6']]
  1284. def plot_brain_grid(typedf, img_folder, type):
  1285. # Set up the figure size dynamically based on grid size
  1286. fig, ax = plt.subplots(figsize=(12, 12))
  1287. plt.axis('off')
  1288. n_splits = range(2, 7) # Columns 2 to 6 for clusters
  1289. # Grid positioning
  1290. for idx, n_clusters in enumerate(n_splits):
  1291. col_name = str(n_clusters) # e.g., '2', '3', '4', etc.
  1292. subtype_counts = typedf[col_name].value_counts().sort_values(ascending=False)
  1293. # Load and plot images based on sorted order
  1294. for i, (cluster, count) in enumerate(subtype_counts.items()):
  1295. percentage = round((count / len(typedf)) * 100, 1)
  1296. image_name = f"brainnet_mean_x{n_clusters}_s{cluster}.jpg"#
  1297. image_path = os.path.join(img_folder, image_name)
  1298. if os.path.exists(image_path):
  1299. img = mpimg.imread(image_path)
  1300. ax_image = fig.add_axes([0.15 * idx, 0.85 - i * 0.15, 0.15, 0.15]) # Adjust position
  1301. ax_image.imshow(img)
  1302. ax_image.axis('off')
  1303. # Add annotation
  1304. ax_image.text(
  1305. 0.5, -0.1, f"{count} ({percentage}%)",
  1306. transform=ax_image.transAxes, ha='center', va='center', fontsize=8
  1307. )
  1308. else:
  1309. print(f"Image not found: {image_path}")
  1310. # Parse 'type' string to create legend entries
  1311. type_items = type.split(':')[-1].split('-') # Extract items after ':' and split by '-'
  1312. legend_labels = type_items
  1313. # Create dummy patches for the legend
  1314. legend_handles = [
  1315. mpatches.Patch(color='none', label=label) for label in legend_labels
  1316. ]
  1317. # Add legend to the lower left corner
  1318. fig.legend(
  1319. handles=legend_handles,
  1320. title='Threshold (ROI)', loc='center left', fontsize=10, title_fontsize=11,
  1321. frameon=False
  1322. )
  1323. plt.show()
  1324. # Execute
  1325. plot_brain_grid(typedf_subset, img_folder, type)
  1326. # %% [markdown]
  1327. # READ IN COLUMN ORDER. 2 SUBTYPES IS FIRST COLUMN...
  1328. # %% [markdown]
  1329. # ## image and comparison
  1330. # %%
  1331. # Configuration
  1332. img_path = '/Users/linlin/Desktop/brain stuff/graphs/'
  1333. type = 'ROI:intersection-C2mean'
  1334. avg_folder = img_path + 'inter_c2mean/'
  1335. contrast_folder = img_path + 'contrast/'
  1336. n_clusters = 3
  1337. # Set up the figure dynamically based on the number of clusters
  1338. fig, axes = plt.subplots(n_clusters, 2, figsize=(12, 4 * n_clusters))
  1339. plt.subplots_adjust(wspace=0, hspace=0) # Remove spacing between images
  1340. # Get subtype counts
  1341. col_name = str(n_clusters)
  1342. subtype_counts = typedf[typedf.visit==1][col_name].value_counts().sort_values(ascending=False)
  1343. # Loop through each cluster and load images from both folders
  1344. for i, (cluster, count) in enumerate(subtype_counts.items()):
  1345. print(cluster)
  1346. # Load and display the average image
  1347. avg_image_path = os.path.join(avg_folder, f"brainnet_mean_x{n_clusters}_s{cluster}.jpg")#ICBM_
  1348. avg_image = plt.imread(avg_image_path)
  1349. axes[i, 0].imshow(avg_image)
  1350. axes[i, 0].axis('off') # Turn off the axis
  1351. # Load and display the contrast image
  1352. contrast_image_path = os.path.join(contrast_folder, f"1vr_contrast_x{n_clusters}s{cluster}.jpg")
  1353. contrast_image = plt.imread(contrast_image_path)
  1354. axes[i, 1].imshow(contrast_image)
  1355. axes[i, 1].axis('off') # Turn off the axis
  1356. # Show the final plot
  1357. plt.show()
  1358. # %% [markdown]
  1359. # ## average image by stage group
  1360. # %%
  1361. # configuration
  1362. img_path = '/Users/linlin/Desktop/brain stuff/graphs/'#'/Users/linlin/Desktop/brain stuff/data/'
  1363. type = 'ROI:intersection-C2mean'# threshold type
  1364. ss_folder = img_path+'inter_c2mean_subtype_stage_mean/'#'roi_c2meansd_3_subtype_stage_mean/'
  1365. def bin_stages(df):
  1366. bins = [0, 8, 11, 14, 19] #[0, 4, 9, 14, 19] #,24]
  1367. labels =['0-8','9-11','12-14','15-19']#['0-4', '5-9', '10-14', '15-19']#'0-4', '5-9', '10-14', '15-19']#,'20-24','25-29']#0, '0-9', '10-12', '13:15', '16:19'
  1368. df['3s_binned'] = pd.cut(df['3s'], bins=bins, labels=labels, include_lowest=True)
  1369. return df
  1370. def calculate_scan_counts(df):
  1371. return df.groupby(['3', '3s_binned']).size().unstack(fill_value=0)
  1372. def plot_subtype_stage_images_adjusted(typedf, ss_folder, baseline_folder):
  1373. df = bin_stages(typedf)
  1374. scan_counts = calculate_scan_counts(df)
  1375. fig, axs = plt.subplots(3, 4, figsize=(20, 12))
  1376. stage_labels = ['0-8','9-11','12-14','15-19']#['0-4', '5-9', '10-14', '15-19']#,'20-24','25-29']
  1377. total_counts = scan_counts.sum(axis=1)
  1378. for subtype in range(3):
  1379. for i, stage_bin in enumerate(stage_labels):
  1380. # Default image path
  1381. image_name = f"subtype{subtype+1}_stage{stage_bin}.jpg"#f"ICBM_mean_x3_subtype{subtype+1}_stage{stage_bin}.jpg"
  1382. image_path = os.path.join(ss_folder, image_name)
  1383. #print(image_path)
  1384. # Check the scan count
  1385. count = scan_counts.loc[subtype+1, stage_bin]
  1386. if count == 1 and not os.path.exists(image_path):
  1387. # If count is 1 and image doesn't exist, find the corresponding image in 'typedf'
  1388. print('sus')
  1389. row = typedf[(typedf['3'] == subtype+1) & (typedf['3s_binned'] == stage_bin)]
  1390. if not row.empty:
  1391. # Use the 'fname' field to find the image in the baseline folder
  1392. fname = row['fname'].values[0]
  1393. print(row['fname'].values[0])
  1394. baseline_image_path = os.path.join(ss_folder,'ICBM_lateral_medial_'+fname[:-4]+'.png')
  1395. print(baseline_image_path)
  1396. if os.path.exists(baseline_image_path):
  1397. image_path = baseline_image_path # Use this image if it exists
  1398. if os.path.exists(image_path):
  1399. img = plt.imread(image_path)
  1400. axs[subtype, i].imshow(img)
  1401. else:
  1402. axs[subtype, i].axis('off') # Leave blank if the image doesn't exist
  1403. # Remove axis, boundaries, and ticks
  1404. axs[subtype, i].set_xticks([])
  1405. axs[subtype, i].set_yticks([])
  1406. axs[subtype, i].set_xlabel(None)
  1407. axs[subtype, i].spines['top'].set_visible(False)
  1408. axs[subtype, i].spines['right'].set_visible(False)
  1409. axs[subtype, i].spines['left'].set_visible(False)
  1410. axs[subtype, i].spines['bottom'].set_visible(False)
  1411. # Add the count annotation above
  1412. #axs[subtype, i].set_title(f"n = {count}")
  1413. ymin, ymax = axs[subtype, i].get_ylim()
  1414. xmin, xmax = axs[subtype, i].get_xlim()
  1415. axs[subtype, i].text((xmin+xmax)/2, (ymin+ymax)/2, f"n = {count}", fontsize=12, ha='center', va='center')
  1416. description = ['Typical','L Temporal','Occipital']
  1417. if i == 0:
  1418. axs[subtype, i].set_ylabel(f'S{subtype+1}/{description[subtype]} (Total: {total_counts[subtype+1]})',fontsize=20)
  1419. else:
  1420. axs[subtype, i].set_ylabel(None)
  1421. # Annotate the stages once for the bottom row
  1422. for i, stage_bin in enumerate(stage_labels):
  1423. axs[2, i].set_xlabel(f'Stage {stage_bin}', fontsize=20, labelpad=10)
  1424. # plt.suptitle('Mean FTP-PET SUVR images for subtype and stage (C=3)',fontsize=30)
  1425. plt.tight_layout() # Adjust layout to fit the suptitle
  1426. plt.show()
  1427. plot_subtype_stage_images_adjusted(typedf99[typedf99.visit==1], ss_folder, '')
  1428. # %% [markdown]
  1429. # ## table 1
  1430. # https://pmc.ncbi.nlm.nih.gov/articles/PMC6951995/
  1431. #
  1432. # https://tableone.readthedocs.io/en/latest/quickstart.html
  1433. # %%
  1434. from tableone import TableOne
  1435. # %%
  1436. #data cleaning to merge variables
  1437. final = pd.read_csv(data_path+"ss_assignment/final_inter_c2mean_2_bl_ss.csv" )
  1438. cogt1 = pd.read_csv(data_path+"sustain_dat_baseline_filtered.csv")
  1439. # composite cognitive scores for fluency
  1440. cogt1['Phonemic_fluency'] = cogt1['UDSVERLC'] + cogt1['UDSVERLC']
  1441. cogt1['Semantic_fluency'] = cogt1['VEG'] + cogt1['ANIMALS']
  1442. cogt1[cogt1.dx=='EOAD'].isna().sum()
  1443. # merge with existing
  1444. combined_bl_merged = combined_bl.merge(
  1445. cogt1[['subj', 'UDSBENTC', 'Line_orientation', 'Line_length', 'TRAILA', 'DIGFORCT',
  1446. 'CRAFTURS', 'FTDREAAS', 'FTDSEMSU', 'FTDANATS', 'FTDRATIO', 'FTDVERBC',
  1447. 'FTDNOUNC', 'FTDWORRC', 'FTDWORIC', 'FTDSEMMT', 'FTDSENAS', 'VEG',
  1448. 'ANIMALS', 'MINTTOTS', 'MOCATOTS', 'CDRSB', 'NACCGDS', 'TRAILB',
  1449. 'DIGBACCT', 'UDSVERLC', 'UDSVERFC', 'digittotal', 'Match', 'Flanker',
  1450. 'avdel30min', 'avdeltot', 'CRAFTDRE', 'UDSBENTD', 'AVTOT5', 'AVDEL30MIN',
  1451. 'Phonemic_fluency', 'Semantic_fluency']],
  1452. on='subj',
  1453. how='left'
  1454. )
  1455. # calculate forgetting
  1456. combined_bl_merged['Benson_Forgetting'] = combined_bl_merged['UDSBENTC'] - combined_bl_merged['UDSBENTD']
  1457. combined_bl_merged['RAVLT_Forgetting'] = combined_bl_merged['AVTOT5'] - combined_bl_merged['AVDEL30MIN']
  1458. combined_bl_merged['Craft_Forgetting'] = combined_bl_merged['CRAFTURS'] - combined_bl_merged['CRAFTDRE']
  1459. # filter by ranges
  1460. cog_name = pd.read_csv(data_path+'cognitive measures.csv')
  1461. for idx, row in cog_name.iterrows():
  1462. related_var = row['related_var']
  1463. if pd.notna(related_var) and related_var in combined_bl_merged.columns:
  1464. min_val = row['min']
  1465. max_val = row['max']
  1466. # Set values outside [min, max] to NaN
  1467. combined_bl_merged.loc[~combined_bl_merged[related_var].between(min_val, max_val, inclusive='both'), related_var] = np.nan
  1468. # merge with SUVR universal from tau quarterly
  1469. taubl = pd.read_csv(data_path+"Marlene_LEADS-FTP-infcblgm_roi-extractions_2025-02-11.csv")
  1470. tauqtr = pd.read_csv(data_path +'/LEADS_data_used/2024-q3-ftp.csv')
  1471. # Ensure proper date format
  1472. taubl['pet_date'] = pd.to_datetime(taubl['pet_date'], errors='coerce')
  1473. combined_bl_merged['ftp_date'] = pd.to_datetime(combined_bl['ftp_date'], errors='coerce')
  1474. # Rename for merging
  1475. taubl_renamed = taubl.rename(columns={
  1476. 'subject_id': 'subj',
  1477. 'pet_date': 'ftp_date'
  1478. })
  1479. # other tau-PET composites
  1480. combined_bl_merged = combined_bl_merged.merge(
  1481. taubl_renamed[['subj', 'ftp_date', \
  1482. "meta_temporal_MRIBASED_SUVR","mtl_no_hippocampus_MRIBASED_SUVR","basolateral_temporal_MRIBASED_SUVR","temporoparietal_MRIBASED_SUVR","ctx_desikan_MRIBASED_SUVR"
  1483. ]],
  1484. on=['subj', 'ftp_date'],
  1485. how='left'
  1486. )
  1487. # save as table one data
  1488. #combined_bl_merged.to_csv(data_path+'tableone_dat.csv',index=False)
  1489. # read
  1490. combined_bl_merged = pd.read_csv(data_path+'tableone_dat.csv' )
  1491. combined_bl_merged.head(2)
  1492. # %% [markdown]
  1493. # ### one vs rest
  1494. # %%
  1495. cont = [ 'Centiloids MRI-Based Composite',
  1496. 'Yrs. of Education','Age', 'CDR-SB',# 'Total 13','Delayed Recall',
  1497. 'SUVR Frontal', 'SUVR MesialTemporal', 'SUVR MetaTemporal', 'SUVR TemporoParietal',
  1498. 'MMSE','stage', 'SUVR Universal','ctx_desikan_MRIBASED_SUVR','meta_temporal_MRIBASED_SUVR',
  1499. "mtl_no_hippocampus_MRIBASED_SUVR",
  1500. "basolateral_temporal_MRIBASED_SUVR",
  1501. "temporoparietal_MRIBASED_SUVR", "SUVR MRI-Based Composite",
  1502. 'UDSBENTC', 'Line_orientation',
  1503. 'Line_length', 'TRAILA', 'DIGFORCT', 'CRAFTURS', 'FTDREAAS', 'FTDSEMSU',
  1504. 'FTDANATS', 'FTDRATIO', 'FTDVERBC', 'FTDNOUNC', 'FTDWORRC', 'FTDWORIC',
  1505. 'FTDSEMMT', 'FTDSENAS', 'VEG', 'ANIMALS', 'MINTTOTS', 'MOCATOTS',
  1506. 'CDRSB', 'NACCGDS', 'TRAILB', 'DIGBACCT',
  1507. 'UDSVERLC', 'UDSVERFC', 'digittotal', 'Match', 'Flanker', 'avdel30min',
  1508. 'avdeltot', 'CRAFTDRE', 'UDSBENTD','RAVLT_Forgetting','Craft_Forgetting','Benson_Forgetting','Phonemic_fluency','Semantic_fluency']#,"SUVR PET-Only Composite"]
  1509. cat = ['Gender', 'Diagnosis', 'ApoE4 Carrier','Cognitive Behavior','ApoE4 Genotype']#'CDR-Global',
  1510. #'CDR-Global',
  1511. print('check subtype ordering (excluding poorly fit):')
  1512. tdata = combined_bl_merged[~combined_bl_merged.poorly][cont+cat+['subtype']].copy() #exclude poorly classified scans
  1513. #tdata['CDR-Global'] = tdata['CDR-Global'].astype('str').replace({'nan':None})
  1514. tdata.subtype.value_counts()
  1515. # %%
  1516. #t1 = TableOne(data=tdata[tdata.subtype.isin([1,2,3])], columns=cont+cat, categorical=cat, groupby='subtype', pval=True, pval_adjust='bonferroni')
  1517. # one vs rest
  1518. subtype = 1
  1519. # Create a new column for the "rest" (excluding control)
  1520. tdata['subtype_vs_rest'] = tdata.apply(lambda x: 1 if x['subtype'] == subtype else (0 if pd.notna(x['subtype']) else np.nan), axis=1)
  1521. # Generate TableOne for subtype vs rest (excluding control)
  1522. tb = TableOne(data=tdata.dropna(subset=['subtype_vs_rest']), columns=cont+cat, categorical=cat, groupby='subtype_vs_rest', pval=True,
  1523. order = {'CDR-Global':['0.0','0.5','1.0','2.0','3.0'],
  1524. 'Gender':['Female','Male'],
  1525. 'Diagnosis':['MCI','Dementia'],
  1526. 'Cognitive Behavior':['Amnestic-predominant CI (single/multi-domain)', 'Non-amnestic CI (single/multi-domain)','PPA','PCA'],
  1527. 'ApoE4 Carrier':['Carrier','Non carrier'],
  1528. 'ApoE4 Genotype':['Homozygote','Heterozygote','Non carrier']},
  1529. # nonnormal = ['MMSE', 'Total 13', 'Yrs. of Education', 'stage'],
  1530. row_percent=True,
  1531. include_null=False,
  1532. # pval_adjust='bonferroni',
  1533. normal_test=True,
  1534. dip_test=True,
  1535. tukey_test=True)
  1536. #error with this: pval_adjust='holm-sidak')
  1537. #print(tbl.tabulate(tablefmt="grid"))
  1538. # %%
  1539. # List to store the processed DataFrames for each subtype
  1540. all_tables = []
  1541. # Loop over subtypes
  1542. for subtype in [1, 2, 3]:
  1543. # Update the `subtype_vs_rest` column for the current subtype
  1544. tdata['subtype_vs_rest'] = tdata.apply(
  1545. lambda x: 1 if x['subtype'] == subtype else (0 if pd.notna(x['subtype']) else np.nan), axis=1
  1546. )
  1547. # Generate the TableOne and save it to a temporary CSV
  1548. tbl = TableOne(
  1549. data=tdata.dropna(subset=['subtype_vs_rest']),
  1550. columns=cont + cat,
  1551. categorical=cat,
  1552. groupby='subtype_vs_rest',
  1553. pval=True,
  1554. order={
  1555. 'CDR-Global': ['0.0', '0.5', '1.0', '2.0', '3.0'],
  1556. 'Gender': ['Female', 'Male'],
  1557. 'Diagnosis': ['MCI', 'Dementia'],
  1558. 'Clinical Phenotype': [
  1559. 'Amnestic-predominant CI (single/multi-domain)',
  1560. 'Non-amnestic CI (single/multi-domain)',
  1561. 'PPA',
  1562. 'PCA',
  1563. ],
  1564. 'ApoE4 Carrier': ['Carrier', 'Non carrier'],
  1565. 'ApoE4 Genotype': ['Homozygote', 'Heterozygote', 'Non carrier'],
  1566. },
  1567. # row_percent=True,
  1568. include_null=False,
  1569. tukey_test=True,
  1570. pval_adjust = 'simes-hochberg'#'bonferroni'
  1571. )
  1572. tbl.to_csv('tmptbl.csv')
  1573. # Load the table and process it
  1574. tmptbl = pd.read_csv('tmptbl.csv', header=1)
  1575. tmptbl['OriginalOrder'] = tmptbl.index # Save the original order
  1576. tmptbl = tmptbl.rename(
  1577. columns={
  1578. 'Unnamed: 0': 'Variable',
  1579. 'Unnamed: 1': 'Category',
  1580. '1': f'S{subtype}',
  1581. 'P-Value (adjusted)': f'adj.p S{subtype}',
  1582. }
  1583. )
  1584. # Select relevant columns and add the processed table to the list
  1585. tmptbl = tmptbl[['OriginalOrder', 'Variable', 'Category', 'Missing', 'Overall', f'S{subtype}', f'adj.p S{subtype}']]#P-Value (adjusted)
  1586. all_tables.append(tmptbl)
  1587. # Merge all tables on the common columns, preserving order
  1588. merged_table = all_tables[0]
  1589. for tbl in all_tables[1:]:
  1590. merged_table = pd.merge(
  1591. merged_table,
  1592. tbl.drop(columns=['OriginalOrder']), # Drop OriginalOrder for subsequent merges
  1593. on=['Variable', 'Category', 'Missing', 'Overall'],
  1594. how='outer',
  1595. sort=False # Avoid any re-sorting
  1596. )
  1597. # Restore the original order from the first table
  1598. merged_table = merged_table.sort_values(by='OriginalOrder').reset_index(drop=True)
  1599. merged_table = merged_table.drop(columns=['OriginalOrder']) # Clean up auxiliary column
  1600. #merged_table['Missing'] = merged_table['Missing'].fillna('0').astype(int).replace({0:np.nan})
  1601. merged_table.fillna('',inplace=True)
  1602. merged_table
  1603. # %% [markdown]
  1604. # Table 1. Summary statistics and 1 vs. rest combined comparison of biomarker, clinical, and demographic variables, two-sample t-tests for continuous variables, and chi-squared tests for categorical variables. For categorical variables, the percentage is row-based.
  1605. # %%
  1606. # Define a function to handle asterisks based on p-value thresholds
  1607. def append_asterisks(value, p_value):
  1608. if pd.isnull(value) or pd.isnull(p_value):
  1609. return value # Return the original value if either is NaN
  1610. # Handle p-values written as '<0.001' directly
  1611. if isinstance(p_value, str) and '<' in p_value:
  1612. return f"{value}***"
  1613. # Convert p_value to float if it's not a string
  1614. try:
  1615. p_value = float(p_value)
  1616. except ValueError:
  1617. return value # Return the original value if conversion fails
  1618. # Apply asterisk rules
  1619. if p_value <= 0.001:
  1620. return f"{value}***"
  1621. elif p_value <= 0.01:
  1622. return f"{value}**"
  1623. elif p_value <= 0.05:
  1624. return f"{value}*"
  1625. else:
  1626. return value
  1627. # Iterate over each subtype column and its corresponding p-value column
  1628. for subtype in ['S1', 'S2', 'S3']:
  1629. p_col = f'adj.p {subtype}'
  1630. merged_table[subtype] = merged_table.apply(lambda row: append_asterisks(row[subtype], row[p_col]), axis=1)
  1631. # Drop the adjusted p-value columns
  1632. merged_table.drop(columns=[f'adj.p {subtype}' for subtype in ['S1', 'S2', 'S3']], inplace=True)
  1633. merged_table
  1634. # %% [markdown]
  1635. # ### anova version
  1636. # %%
  1637. t1 = TableOne(data=tdata[tdata.subtype.isin([1,2,3])], columns=cont+cat, categorical=cat, groupby='subtype', pval=True,row_percent=False)
  1638. t1.to_csv('tmp_table.csv')
  1639. t1 = pd.read_csv('tmp_table.csv').iloc[2:,:]
  1640. t1.columns = ['variable','category','missing','overall','1','2','3','p']
  1641. t1
  1642. # %%
  1643. tdata.columns
  1644. # %%
  1645. # %% [markdown]
  1646. # ### pairwise for significant
  1647. # %%
  1648. # Get variables with p < 0.05
  1649. sig_vars = t1.loc[t1['p'].astype(str).str.replace('<', '').astype(float) < 0.05, 'variable'].tolist()
  1650. sig_vars = [i.split(',')[0] for i in sig_vars ]
  1651. sig_vars = [s for s in sig_vars if s in cont ]
  1652. print("Significant variables:", sig_vars)
  1653. # %%
  1654. results = []
  1655. for var in sig_vars+['DIGFORCT']:
  1656. # Drop rows where the variable or subtype is missing
  1657. temp_data = tdata[['subtype', var]].dropna()
  1658. # Tukey's HSD
  1659. tukey = pairwise_tukeyhsd(endog=temp_data[var],
  1660. groups=temp_data['subtype'],
  1661. alpha=0.05)
  1662. # Collect results
  1663. tukey_df = pd.DataFrame(data=tukey.summary().data[1:], # Skip header row
  1664. columns=tukey.summary().data[0])
  1665. tukey_df['variable'] = var
  1666. results.append(tukey_df)
  1667. # Concatenate all Tukey results into one DataFrame
  1668. tukey_results = pd.concat(results, ignore_index=True)
  1669. tukey_df = tukey_results.copy()
  1670. tukey_df['comparison'] = tukey_df['group1'].astype(int).astype(str) + 'vs' + tukey_df['group2'].astype(int).astype(str)
  1671. tukey_df = tukey_df[['variable', 'comparison', 'p-adj']]
  1672. tukey_wide = tukey_df.pivot(index='variable', columns='comparison', values='p-adj').reset_index()
  1673. tukey_wide = tukey_wide.rename(columns={
  1674. '1vs2': 'p_1vs2',
  1675. '1vs3': 'p_1vs3',
  1676. '2vs3': 'p_2vs3'
  1677. })
  1678. tukey_wide.to_clipboard()
  1679. # %%
  1680. tukey_wide
  1681. # %% [markdown]
  1682. # ## legend definition
  1683. # %%
  1684. # Define the labels and colors
  1685. labels = ['1 - Typical','2 - L Temporal','3 - Posterior']
  1686. cols = ["#1f449c","#32ABA6","#f05039"]
  1687. # Create dummy scatter plots for legend
  1688. for color, label in zip(cols, labels):
  1689. plt.scatter([], [], color=color, label=label)
  1690. # Create the legend
  1691. plt.legend(title="Subtype", loc='center', frameon=True, fontsize=12, title_fontsize=14)
  1692. plt.axis('off') # Hide the axes since this is just for the legend
  1693. plt.show()
  1694. # %% [markdown]
  1695. # ## laterality
  1696. # %%
  1697. plt.rcParams['axes.titlesize'] = 16 # Set title font size
  1698. plt.rcParams['axes.labelsize'] = 16 # Set label font size
  1699. plt.rcParams['lines.markersize'] = 12 # Set marker size
  1700. plt.rcParams['axes.titleweight'] = 'regular'#'bold' # Set title font weight to bold
  1701. plt.rcParams['axes.labelweight'] = 'regular'#'bold' # Set label font weight to bold
  1702. plt.rcParams['xtick.labelsize'] = 16 # Set tick label font size
  1703. plt.rcParams['ytick.labelsize'] = 16 # Set tick label font size
  1704. plt.rcParams['axes.titlesize'] = 16
  1705. plt.rcParams['axes.labelsize'] = 16
  1706. # %%
  1707. full_data = combined_exclude.copy()
  1708. # Define the ROIs and their corresponding columns in the data
  1709. rois = {
  1710. 'MTL': ('R_MTL', 'L_MTL'),
  1711. 'Frontal': ('R_frontal', 'L_frontal'),
  1712. 'Occipital': ('R_occipital', 'L_occipital'),
  1713. 'Parietal': ('R_parietal', 'L_parietal'),
  1714. 'Temporal': ('R_temporal', 'L_temporal')
  1715. }
  1716. # Calculate HAI for each ROI and add it as a new column in full_data
  1717. for roi, (right_col, left_col) in rois.items():
  1718. full_data[f'{roi}_HAI'] = 200 * (full_data[right_col] - full_data[left_col]) / (full_data[right_col] + full_data[left_col])
  1719. # Filter for subjects with valid subtypes and stages with 'visit' == 1
  1720. filtered_data = full_data[full_data['visit'] == 1].dropna(subset=['subtype', 'stage'])
  1721. # Melt the data for plotting with Seaborn
  1722. hai_columns = [f'{roi}_HAI' for roi in rois]
  1723. melted_data = filtered_data.melt(id_vars=['subtype', 'stage'], value_vars=hai_columns,
  1724. var_name='ROI', value_name='HAI')
  1725. melted_data['ROI'] = melted_data['ROI'].str.replace('_HAI', '') # Clean up ROI names
  1726. # Add jitter to the stage values for plotting clarity
  1727. melted_data['stage_jittered'] = melted_data['stage'] + np.random.uniform(-0.2, 0.2, size=len(melted_data))
  1728. # Define color palette for each ROI
  1729. roi_palette = sns.color_palette("Set1", len(rois))
  1730. # Set up the plot grid with each subtype and an overall plot
  1731. unique_subtypes = sorted(melted_data['subtype'].unique())
  1732. fig, axes = plt.subplots(1, len(unique_subtypes) + 1, figsize=(18, 6), sharey=True,sharex=True)
  1733. fig.suptitle("HAI by Stage and ROI for each Subtype",fontsize=20)
  1734. # Plot for each subtype
  1735. for j, subtype in enumerate(unique_subtypes):
  1736. ax = axes[j]
  1737. subtype_data = melted_data[melted_data['subtype'] == subtype]
  1738. # Plot each ROI's scatter with jittered dots in grey and regression line with separate colors
  1739. for k, (roi, color) in enumerate(zip(rois.keys(), roi_palette)):
  1740. roi_data = subtype_data[subtype_data['ROI'] == roi]
  1741. sns.regplot(
  1742. data=roi_data, x='stage_jittered', y='HAI',
  1743. scatter_kws={'color': 'grey', 'alpha': 0.5,'s':20},
  1744. line_kws={'label': roi, 'color': color},
  1745. ax=ax, ci=None
  1746. )
  1747. # Add a dashed line at y = 0
  1748. ax.axhline(0, color='black', linestyle='--', linewidth=1)
  1749. ax.set_title(f'Subtype {subtype}')#({len(subtype_data)/5})
  1750. ax.set_xlabel("Stage")
  1751. if j == 0:
  1752. ax.set_ylabel("HAI (%)")
  1753. else:
  1754. ax.set_ylabel("")
  1755. # Overall plot in the last subplot
  1756. ax = axes[-1]
  1757. for k, (roi, color) in enumerate(zip(rois.keys(), roi_palette)):
  1758. roi_data = melted_data[melted_data['ROI'] == roi]
  1759. sns.regplot(
  1760. data=roi_data, x='stage_jittered', y='HAI',
  1761. scatter_kws={'color': 'grey', 'alpha': 0.5,'s':20},
  1762. line_kws={'label': roi, 'color': color},
  1763. ax=ax, ci=None
  1764. )
  1765. # Add a dashed line at y = 0 for the overall plot
  1766. ax.axhline(0, color='black', linestyle='--', linewidth=1)
  1767. ax.set_title('Overall')
  1768. ax.set_xlabel("Stage")
  1769. ax.set_ylabel("")
  1770. # Adjust the legend to show ROI colors outside the plot
  1771. handles, labels = ax.get_legend_handles_labels()
  1772. fig.legend(handles, labels, title="ROI", loc='center left',bbox_to_anchor=(1.0, 0.5))
  1773. plt.tight_layout()#rect=[0, 0, 1, 0.99]
  1774. plt.show()
  1775. # %% [markdown]
  1776. # alternative fit
  1777. # %%
  1778. from sklearn.metrics import r2_score
  1779. # Assuming 'melted_data' is pre-defined from the user's context
  1780. # Define ROIs and their color palette
  1781. rois = {
  1782. 'MTL': ('R_MTL', 'L_MTL'),
  1783. 'Frontal': ('R_frontal', 'L_frontal'),
  1784. 'Occipital': ('R_occipital', 'L_occipital'),
  1785. 'Parietal': ('R_parietal', 'L_parietal'),
  1786. 'Temporal': ('R_temporal', 'L_temporal')
  1787. }
  1788. roi_palette = sns.color_palette("Set1", len(rois))
  1789. # Setup
  1790. fit_types = ['linear', 'lowess', 'quadratic']
  1791. unique_subtypes = sorted(melted_data['subtype'].unique())
  1792. n_cols = len(unique_subtypes) + 1 # +1 for overall
  1793. n_rows = len(fit_types)
  1794. # Create figure
  1795. fig, axes = plt.subplots(n_rows, n_cols, figsize=(6 * n_cols, 4 * n_rows), sharex=True, sharey=True)
  1796. fig.suptitle("HAI by Stage and ROI with Different Fits", fontsize=20, y=1.02)
  1797. # Store R² values
  1798. r2_records = []
  1799. # Plot loop
  1800. for row_idx, fit_type in enumerate(fit_types):
  1801. for col_idx, subtype in enumerate(unique_subtypes + ['overall']):
  1802. ax = axes[row_idx, col_idx] if n_rows > 1 else axes[col_idx]
  1803. if subtype == 'overall':
  1804. plot_data = melted_data.copy()
  1805. title = 'Overall'
  1806. else:
  1807. plot_data = melted_data[melted_data['subtype'] == subtype]
  1808. title = f'Subtype {subtype}'
  1809. for (roi, color) in zip(rois.keys(), roi_palette):
  1810. roi_data = plot_data[plot_data['ROI'] == roi]
  1811. x = roi_data['stage_jittered'].values
  1812. y = roi_data['HAI'].values
  1813. # Plot scatter
  1814. ax.scatter(x, y, color='grey', alpha=0.4, s=20)
  1815. # Fit and plot
  1816. sorted_idx = np.argsort(x)
  1817. x_sorted = x[sorted_idx]
  1818. y_sorted = y[sorted_idx]
  1819. if fit_type == 'linear':
  1820. coeffs = np.polyfit(x, y, 1)
  1821. fit_func = np.poly1d(coeffs)
  1822. y_pred = fit_func(x)
  1823. ax.plot(x_sorted, fit_func(x_sorted), color=color, label=roi)
  1824. r2 = r2_score(y, y_pred)
  1825. if subtype != 'overall':
  1826. r2_records.append({'Subtype': subtype, 'ROI': roi, 'Fit': 'linear', 'R2': r2})
  1827. elif fit_type == 'quadratic':
  1828. coeffs = np.polyfit(x, y, 2)
  1829. fit_func = np.poly1d(coeffs)
  1830. y_pred = fit_func(x)
  1831. ax.plot(x_sorted, fit_func(x_sorted), color=color, label=roi)
  1832. r2 = r2_score(y, y_pred)
  1833. if subtype != 'overall':
  1834. r2_records.append({'Subtype': subtype, 'ROI': roi, 'Fit': 'quadratic', 'R2': r2})
  1835. elif fit_type == 'lowess':
  1836. lowess_fit = sm.nonparametric.lowess(y, x, frac=0.3)
  1837. ax.plot(lowess_fit[:, 0], lowess_fit[:, 1], color=color, label=roi)
  1838. ax.axhline(0, color='black', linestyle='--', linewidth=1)
  1839. if row_idx == 0:
  1840. ax.set_title(title,fontsize=16)
  1841. if col_idx == 0:
  1842. ax.set_ylabel(f"{fit_type.capitalize()} Fit\nHAI (%)", fontsize=16)
  1843. else:
  1844. ax.set_ylabel("")
  1845. ax.set_xlabel("Stage", fontsize=16)
  1846. if row_idx == 0 and col_idx == n_cols - 1:
  1847. handles, labels = ax.get_legend_handles_labels()
  1848. fig.legend(handles, labels, title="ROI", loc='center left', bbox_to_anchor=(1.01, 0.5))
  1849. plt.tight_layout(rect=[0, 0, 1, 0.98])
  1850. # %%
  1851. # Convert R² results to DataFrame
  1852. r2_df = pd.DataFrame(r2_records)
  1853. r2_df.sort_values(by=['ROI','Subtype','R2'],ascending=False)
  1854. # %% [markdown]
  1855. # Fig. 8: Hemispheric Asymmetry Index (R-L)/[(R+L)/2] * 100 of each lobar ROI vs. Stage by subtype, and overall combined. Above 0 indicates higher in right, below indicates higher in left, the black line at 0 indicates symmetry.
  1856. # %% [markdown]
  1857. # ## clinical phenotype
  1858. # %% [markdown]
  1859. # ### by subtype
  1860. # %%
  1861. plt.rcParams['axes.titlesize'] = 18 # Set title font size
  1862. plt.rcParams['axes.labelsize'] = 16 # Set label font size
  1863. plt.rcParams['lines.markersize'] = 14 # Set marker size
  1864. plt.rcParams['axes.titleweight'] = 'regular'#'bold' # Set title font weight to bold
  1865. plt.rcParams['axes.labelweight'] = 'regular'#'bold' # Set label font weight to bold
  1866. plt.rcParams['xtick.labelsize'] = 14 # Set tick label font size
  1867. plt.rcParams['ytick.labelsize'] = 14 # Set tick label font size
  1868. # %%
  1869. img10 = combined_exclude[combined_exclude.visit == 1].copy()
  1870. # Calculate proportions for Cognitive Behavior (including Amnestic CI)
  1871. cognitive_behavior_with_amnesic = img10.groupby(['subtype', 'Cognitive Behavior']).size().unstack().apply(lambda x: x / x.sum(), axis=1)
  1872. # Calculate proportions for Cognitive Behavior (excluding Amnestic CI)
  1873. cognitive_behavior_without_amnesic = img10[img10['Cognitive Behavior'] != 'Amnestic-predominant CI (single/multi-domain)'] \
  1874. .groupby(['subtype', 'Cognitive Behavior']).size().unstack().apply(lambda x: x / x.sum(), axis=1)
  1875. # Calculate total counts for annotations
  1876. total_counts_with_amnesic = img10.groupby('subtype').size()
  1877. total_counts_without_amnesic = img10[img10['Cognitive Behavior'] != 'Amnestic-predominant CI (single/multi-domain)'] \
  1878. .groupby('subtype').size()
  1879. # Ensure the same color scale for both subplots
  1880. all_categories = set(cognitive_behavior_with_amnesic.columns).union(cognitive_behavior_without_amnesic.columns)
  1881. category_palette = sns.color_palette('plasma', len(all_categories))
  1882. category_color_mapping = {category: color for category, color in zip(sorted(all_categories), category_palette)}
  1883. # Set up the figure for stacked bar plots
  1884. fig, axes = plt.subplots(1, 2, figsize=(15, 6), sharey=True)
  1885. # Plot Cognitive Behavior (including Amnestic CI)
  1886. bars = cognitive_behavior_with_amnesic.plot(
  1887. kind='bar', stacked=True, ax=axes[0],
  1888. color=[category_color_mapping[cat] for cat in cognitive_behavior_with_amnesic.columns], legend=False
  1889. )
  1890. axes[0].tick_params(axis='x', labelrotation=0)
  1891. axes[0].set_title('Cognitive Behavior')
  1892. axes[0].set_xlabel('Subtype')
  1893. axes[0].set_ylabel('Proportion')
  1894. # Adding percentage annotations and total counts
  1895. for container in axes[0].containers:
  1896. for bar in container:
  1897. height = bar.get_height()
  1898. if height > 0:
  1899. axes[0].text(
  1900. bar.get_x() + bar.get_width() / 2,
  1901. bar.get_y() + height / 2,
  1902. f'{height * 100:.1f}%', # Convert proportion to percentage
  1903. ha='center', va='center', fontsize=12, color='white'
  1904. )
  1905. # Adding total count annotations above bars
  1906. for x, total in enumerate(total_counts_with_amnesic):
  1907. axes[0].text(
  1908. x, 1.005, # Position slightly above the bar
  1909. f'Total: {total}', ha='center', va='bottom', fontsize=12, color='black'
  1910. )
  1911. # Plot Cognitive Behavior (excluding Amnestic CI)
  1912. bars = cognitive_behavior_without_amnesic.plot(
  1913. kind='bar', stacked=True, ax=axes[1],
  1914. color=[category_color_mapping[cat] for cat in cognitive_behavior_without_amnesic.columns], legend=False
  1915. )
  1916. axes[1].tick_params(axis='x', labelrotation=0)
  1917. axes[1].set_title('Cognitive Behavior (excluding Amnestic CI)')
  1918. axes[1].set_xlabel('Subtype')
  1919. axes[1].set_ylabel('Proportion')
  1920. # Adding percentage annotations and total counts
  1921. for container in axes[1].containers:
  1922. for bar in container:
  1923. height = bar.get_height()
  1924. if height > 0:
  1925. axes[1].text(
  1926. bar.get_x() + bar.get_width() / 2,
  1927. bar.get_y() + height / 2,
  1928. f'{height * 100:.1f}%', # Convert proportion to percentage
  1929. ha='center', va='center', fontsize=12, color='white'
  1930. )
  1931. # Adding total count annotations above bars
  1932. for x, total in enumerate(total_counts_without_amnesic):
  1933. axes[1].text(
  1934. x, 1.005, # Position slightly above the bar
  1935. f'Total: {total}', ha='center', va='bottom', fontsize=12, color='black'
  1936. )
  1937. # Add a shared legend
  1938. handles = [plt.Line2D([0], [0], color=color, linewidth=10) for color in category_color_mapping.values()]
  1939. labels = [label for label in sorted(all_categories)]
  1940. fig.legend(handles, labels, title="Cognitive Behavior", loc='center left', bbox_to_anchor=(1.0, 0.5))
  1941. plt.tight_layout()
  1942. plt.show()
  1943. # %%
  1944. # Group and count the data to prepare for the Sankey diagram
  1945. sankey_data = img10.groupby(['subtype', 'Cognitive Behavior']).size().reset_index(name='Count')
  1946. # Create lists of unique nodes for subtypes and behaviors
  1947. subtype_nodes = [f"Subtype {x}" for x in sorted(img10['subtype'].unique())]
  1948. behavior_nodes = sorted(img10['Cognitive Behavior'].unique())
  1949. # Combine all nodes into a single list
  1950. all_nodes = subtype_nodes + behavior_nodes
  1951. # Create source and target indices
  1952. sankey_data['Source'] = sankey_data['subtype'].apply(lambda x: all_nodes.index(f"Subtype {x}"))
  1953. sankey_data['Target'] = sankey_data['Cognitive Behavior'].apply(lambda x: all_nodes.index(x))
  1954. # Calculate total counts for each cognitive behavior to compute percentages
  1955. total_counts = sankey_data.groupby('Target')['Count'].transform('sum')
  1956. sankey_data['Percentage'] = (sankey_data['Count'] / total_counts) * 100
  1957. # Define the consistent color scheme for cognitive behavior subtypes
  1958. color_scheme = {
  1959. 'Non-amnestic CI (single/multi-domain)': 'rgba(128, 0, 128, 0.8)', # Purple
  1960. 'PPA': 'rgba(255, 165, 0, 0.8)', # Orange
  1961. 'PCA': 'rgba(220, 20, 60, 0.8)', # Crimson
  1962. 'Amnestic-predominant CI (single/multi-domain)': 'rgba(75, 0, 130, 0.8)' # Indigo
  1963. }
  1964. # Map colors to nodes
  1965. node_colors = [
  1966. color_scheme.get(node, 'rgba(200, 200, 200, 0.8)') if node in behavior_nodes else 'rgba(200, 200, 200, 0.8)'
  1967. for node in all_nodes
  1968. ]
  1969. # Add hover information for links
  1970. hover_text = [
  1971. f"Subtype: {all_nodes[row['Source']]}<br>"
  1972. f"Cognitive Behavior: {all_nodes[row['Target']]}<br>"
  1973. f"Count: {row['Count']}<br>"
  1974. f"Percentage: {row['Percentage']:.2f}%"
  1975. for _, row in sankey_data.iterrows()
  1976. ]
  1977. # Sankey Diagram
  1978. fig = go.Figure(go.Sankey(
  1979. node=dict(
  1980. pad=15,
  1981. thickness=20,
  1982. line=dict(color="black", width=0.5),
  1983. label=all_nodes, # All unique labels
  1984. color=node_colors # Color nodes based on subtype
  1985. ),
  1986. link=dict(
  1987. source=sankey_data['Source'], # Source indices
  1988. target=sankey_data['Target'], # Target indices
  1989. value=sankey_data['Count'], # Link weights
  1990. color=[color_scheme.get(all_nodes[target], 'rgba(128, 128, 128, 0.8)') for target in sankey_data['Target']],
  1991. customdata=hover_text, # Add custom hover text
  1992. hovertemplate='%{customdata}<extra></extra>' # Hover template with no extra text
  1993. )
  1994. ))
  1995. # Update layout
  1996. fig.update_layout(
  1997. title_text="Sustain Subtype Assignment vs Cognitive Behavior (Percentage by Cognitive Behavior)",
  1998. font_size=12
  1999. )
  2000. # Render inline
  2001. fig.show()
  2002. # %%
  2003. # Define color scheme for subtypes
  2004. subtype_color_scheme = {
  2005. "S1/Typical": "#1f449c",
  2006. "S2/L Temporal":"#32ABA6",
  2007. "S3/Occipital": "#f05039"
  2008. }
  2009. # Map node colors based on subtype
  2010. node_colors = [
  2011. subtype_color_scheme.get(node, 'rgba(200, 200, 200, 0.8)') if node in subtype_nodes else 'rgba(200, 200, 200, 0.8)'
  2012. for node in all_nodes
  2013. ]
  2014. # Assign colors to links based on source subtype
  2015. link_colors = [
  2016. subtype_color_scheme.get(all_nodes[source], 'rgba(128, 128, 128, 1)') # Default to gray if missing
  2017. for source in sankey_data['Source']
  2018. ]
  2019. # Add hover information for links
  2020. hover_text = [
  2021. f"Subtype: {row['subtype']}<br>"
  2022. f"Cognitive Behavior: {all_nodes[row['Target']]}<br>"
  2023. f"Count: {row['Count']}<br>"
  2024. f"Percentage: {row['Percentage']:.2f}%"
  2025. for _, row in sankey_data.iterrows()
  2026. ]
  2027. # Create the Sankey diagram
  2028. fig = go.Figure(go.Sankey(
  2029. node=dict(
  2030. pad=15,
  2031. thickness=20,
  2032. line=dict(color="black", width=0.5),
  2033. label=all_nodes, # Use the ordered all_nodes list
  2034. color=node_colors, # Color nodes based on the subtype
  2035. hoverlabel=dict(font=dict(size=12)), # Adjust hover label font size if needed
  2036. ),
  2037. link=dict(
  2038. source=sankey_data['Source'], # Source indices
  2039. target=sankey_data['Target'], # Target indices
  2040. value=sankey_data['Count'], # Link weights
  2041. color=link_colors, # Color links based on the source subtype
  2042. customdata=hover_text, # Add custom hover text
  2043. hovertemplate='%{customdata}<extra></extra>' # Hover template with no extra text
  2044. )
  2045. ))
  2046. # Update layout
  2047. fig.update_layout(
  2048. title=dict(
  2049. text="SuStaIn vs. Cognitive Behavior Subtype",
  2050. x=0.5,
  2051. xanchor='center',
  2052. font=dict(size=12)
  2053. ),
  2054. font_size=1,
  2055. font_color='black',
  2056. width=700,
  2057. height=500
  2058. )
  2059. # Show the plot
  2060. fig.show()
  2061. # %% [markdown]
  2062. # Fig. 9: Clinical phenotype profile of each subtype (left) and excluding those with amnestic-predominant cognitive impairment (CI) (right). S3 is characterized by high percentage of PCA, while S2 is characterized by high percentage of PPA.
  2063. #
  2064. # Revision: add sankey. (the percentage is based on the clinical phentype/right side)
  2065. # %% [markdown]
  2066. # ### stage by diagnosis
  2067. # %%
  2068. warnings.filterwarnings('ignore')
  2069. # Prepare the data (Cognitive Behavior including Amnestic-predominant CI)
  2070. img10_add = img10.dropna(subset=['Diagnosis'])
  2071. # Create a single box+swarm plot
  2072. plt.figure(figsize=(10, 6))
  2073. # Boxplot
  2074. sns.boxplot(
  2075. data=img10_add, x='Diagnosis', y='stage', color='grey'
  2076. )
  2077. category_color_mapping = {'Dementia':'purple','MCI':'yellow'}
  2078. # Swarmplot with consistent colors
  2079. sns.swarmplot(
  2080. data=img10_add, x='Diagnosis', y='stage',
  2081. palette=[category_color_mapping[cat] for cat in img10_add['Diagnosis'].unique()],
  2082. size=4 # Adjust marker size for better visibility
  2083. )
  2084. # Add legend
  2085. handles = [plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=8, label=category)
  2086. for category, color in category_color_mapping.items()]
  2087. #plt.legend(handles=handles, title='Diagnosis', loc='lower right', fontsize=10, title_fontsize=12)
  2088. # Plot settings
  2089. #plt.title('Stage Distribution by Diagnosis')
  2090. plt.xlabel('Diagnosis')
  2091. plt.ylabel('Stage')
  2092. plt.ylim(-1, 20) # Set y-axis range
  2093. plt.yticks(range(0, 21, 2)) # Set y-axis ticks at 2-unit increments
  2094. plt.xticks(rotation=0, ha='center') # Rotate x-axis labels for readability
  2095. plt.tight_layout()
  2096. # Display the plot
  2097. plt.show()
  2098. # Filter the img11 DataFrame by Diagnosis groups
  2099. group1 = img10_add[img10_add['Diagnosis'] == 'MCI']['stage']
  2100. group2 = img10_add[img10_add['Diagnosis'] == 'Dementia']['stage']
  2101. # Perform two-sample t-test (assuming unequal variances)
  2102. t_stat, p_value = ttest_ind(group1, group2, equal_var=False)
  2103. mean1, std1 = np.mean(group1), np.std(group1, ddof=1)
  2104. mean2, std2 = np.mean(group2), np.std(group2, ddof=1)
  2105. # Calculate the effect size (Cohen's d)
  2106. n1, n2 = len(group1), len(group2)
  2107. pooled_sd = np.sqrt(((n1 - 1) * std1**2 + (n2 - 1) * std2**2) / (n1 + n2 - 2))
  2108. cohens_d = (mean1 - mean2) / pooled_sd
  2109. # Calculate the 95% confidence interval for the difference in means
  2110. mean_diff = mean1 - mean2
  2111. se_diff = np.sqrt(std1**2 / n1 + std2**2 / n2)
  2112. ci_lower = mean_diff - norm.ppf(0.975) * se_diff
  2113. ci_upper = mean_diff + norm.ppf(0.975) * se_diff
  2114. # Print the results
  2115. print(f"Group 1 (MCI): Mean = {mean1:.2f}, SD = {std1:.2f}, n = {n1}")
  2116. print(f"Group 2 (Dementia): Mean = {mean2:.2f}, SD = {std2:.2f}, n = {n2}")
  2117. print(f"t-statistic: {t_stat:.4f}")
  2118. print(f"p-value: {p_value:.4e}")
  2119. print(f"Cohen's d (Effect Size): {cohens_d:.2f}")
  2120. print(f"95% CI for Mean Difference: [{ci_lower:.2f}, {ci_upper:.2f}]")
  2121. # %% [markdown]
  2122. # Fig. 10: Distribution of stages across diagnoses.
  2123. # %% [markdown]
  2124. # ### by subtype and stage
  2125. # %%
  2126. upperr = 20
  2127. img11 = img10.copy()
  2128. # Define the complete range of stages (0 to upperr) to ensure consistency
  2129. all_stages = range(0, upperr)
  2130. # List of unique subtypes, including 'Overall' for the first row
  2131. subtypes = ['Overall'] + sorted(img11['subtype'].unique().tolist())
  2132. # Create a consistent color palette for Cognitive Behavior categories
  2133. all_categories = sorted(img11['Cognitive Behavior'].unique())
  2134. category_palette = sns.color_palette('plasma', len(all_categories))
  2135. category_color_mapping = {category: color for category, color in zip(all_categories, category_palette)}
  2136. # Set up the figure with 2 columns (with and without Amnestic) and rows based on subtypes + overall
  2137. fig, axes = plt.subplots(len(subtypes), 2, figsize=(16, 4 * len(subtypes)), sharex=True, sharey=False)
  2138. # Loop through each subtype, including overall, to plot Cognitive Behavior by stage
  2139. for i, subtype in enumerate(subtypes):
  2140. for j, exclude_amnestic in enumerate([False, True]):
  2141. # Filter data for excluding or including Amnestic
  2142. filtered_data = img11 if not exclude_amnestic else img11[img11['Cognitive Behavior'] != 'Amnestic-predominant CI (single/multi-domain)']
  2143. # Generate title suffix for the column
  2144. title_suffix = "(Including Amnestic CI)" if not exclude_amnestic else "(Excluding Amnestic CI)"
  2145. # Pivot table for stage vs. Cognitive Behavior
  2146. if subtype == 'Overall':
  2147. cognitive_behavior_counts = filtered_data.pivot_table(
  2148. index='stage', columns='Cognitive Behavior', aggfunc='size', fill_value=0
  2149. )
  2150. cognitive_behavior_counts = cognitive_behavior_counts.reindex(all_stages, fill_value=0)
  2151. title_prefix = "Overall"
  2152. else:
  2153. subtype_data = filtered_data[filtered_data['subtype'] == subtype]
  2154. cognitive_behavior_counts = subtype_data.pivot_table(
  2155. index='stage', columns='Cognitive Behavior', aggfunc='size', fill_value=0
  2156. )
  2157. cognitive_behavior_counts = cognitive_behavior_counts.reindex(all_stages, fill_value=0)
  2158. title_prefix = f"Subtype {subtype}"
  2159. # Plot Cognitive Behavior by stage with consistent colors
  2160. cognitive_behavior_counts.plot(
  2161. kind='bar', stacked=True, ax=axes[i, j],
  2162. color=[category_color_mapping[cat] for cat in cognitive_behavior_counts.columns]
  2163. )
  2164. axes[i, j].set_title(f'{title_prefix} {title_suffix}')
  2165. axes[i, j].set_xlabel('Stage' if i == len(subtypes) - 1 else "")
  2166. axes[i, j].set_ylabel('Count' if j == 0 else "")
  2167. axes[i, j].set_xlim([-1, upperr])
  2168. axes[i, j].yaxis.set_major_locator(MaxNLocator(integer=True)) # Ensure integer tick marks
  2169. # Remove legend for all but the first plot
  2170. if i > 0 or j > 0:
  2171. axes[i, j].legend().remove()
  2172. # Add a shared legend for the entire figure
  2173. handles = [plt.Line2D([0], [0], color=color, linewidth=10) for color in category_palette]
  2174. labels = [label for label in all_categories]
  2175. #fig.legend(handles, labels, title="Cognitive Behavior", loc='center right', bbox_to_anchor=(1.1, 0.5))
  2176. # Adjust layout and display the plot
  2177. plt.tight_layout()
  2178. plt.show()
  2179. # %% [markdown]
  2180. # Fig. 11: Distribution of cognitive behavior subtypes across stages for each subtype (left), excluding amnestic-predominant CI (right). The left-skewedness reflects the general subtype and stage assignment of patients to be centered around higher stages.
  2181. # %% [markdown]
  2182. # ## saving file for ROI definition map
  2183. # %%
  2184. # create dict to map parcellations to lobar ROI
  2185. cpath='/Users/linlin/Desktop/brain stuff/data/'
  2186. ftp = pd.read_csv(cpath+'baseline_FTP_6mm.csv')
  2187. # visit = 1, parc = hemfs_roi - dropped
  2188. ftp.drop(['ftp_date'],axis=1,inplace=True)
  2189. ftp.rename(columns={'ftp_vol':'vol','ftp_suvr':'suvr'},inplace=True)
  2190. ftp.head(2)
  2191. lobe_dict = {
  2192. 'L_MTL': ['L_entorhinal','L_amygdala','L_hippocampus' ],
  2193. 'R_MTL': ['R_entorhinal','R_amygdala','R_hippocampus'],
  2194. 'L_temporal': ['L_superiortemporal', 'L_middletemporal', 'L_inferiortemporal','L_transversetemporal',
  2195. 'L_parahippocampal','L_fusiform'],
  2196. 'R_temporal': ['R_superiortemporal', 'R_middletemporal','R_inferiortemporal','R_transversetemporal',
  2197. 'R_parahippocampal','R_fusiform'],
  2198. 'L_frontal':['L_superiorfrontal','L_rostralmiddlefrontal','L_caudalmiddlefrontal', #middle frontal gyrus
  2199. 'L_parsopercularis', 'L_parsorbitalis', 'L_parstriangularis', #inferior frontal gyrus
  2200. 'L_lateralorbitofrontal','L_medialorbitofrontal', #occipitalfrontal cortex
  2201. 'L_rostralanteriorcingulate', 'L_caudalanteriorcingulate',#cingulate
  2202. 'L_precentral','L_insula'],
  2203. 'R_frontal':['R_superiorfrontal', 'R_rostralmiddlefrontal', 'R_caudalmiddlefrontal',
  2204. 'R_parsopercularis', 'R_parsorbitalis', 'R_parstriangularis',
  2205. 'R_lateralorbitofrontal','R_medialorbitofrontal',
  2206. 'R_rostralanteriorcingulate','R_caudalanteriorcingulate',
  2207. 'R_precentral', 'R_insula'], #paracentral?
  2208. 'L_occipital':['L_lateraloccipital','L_lingual','L_cuneus','L_pericalcarine'],
  2209. 'R_occipital':['R_lateraloccipital','R_lingual','R_cuneus','R_pericalcarine'],
  2210. 'L_parietal':['L_superiorparietal','L_inferiorparietal','L_supramarginal',
  2211. 'L_postcentral','L_precuneus',
  2212. 'L_posteriorcingulate','L_isthmuscingulate'],#cingulate
  2213. 'R_parietal':['R_superiorparietal','R_inferiorparietal','R_supramarginal',
  2214. 'R_postcentral','R_precuneus',
  2215. 'R_posteriorcingulate', 'R_isthmuscingulate']
  2216. }
  2217. # use this to subset data for correct ordering!
  2218. lobes = ['L_MTL', 'R_MTL', 'L_temporal', 'R_temporal', 'L_frontal', 'R_frontal',
  2219. 'L_occipital', 'R_occipital','L_parietal', 'R_parietal' ]
  2220. ftp['lobe'] = ftp['roi'].apply(lambda roi: next((lobe for lobe, regions in lobe_dict.items() if roi in regions), None))
  2221. ftp = ftp[ftp.subj.isin(['LDS0070120'])]
  2222. ftp.drop(columns=['dx','vol'],inplace=True)
  2223. ftp = ftp[~ftp.lobe.isna()]
  2224. # Replace "R_" and "L_" in the `roi` column
  2225. ftp['roi'] = ftp['roi'].str.replace("R_", "Right-").str.replace("L_", "Left-")
  2226. # Set `suvr` value by `lobe`
  2227. lobe_to_value = {"L_parietal": 1, "R_parietal": 1, "L_temporal": 2, "R_temporal": 2,
  2228. "L_frontal": 3, "R_frontal": 3, "L_occipital": 4,"R_occipital": 4, "L_MTL": 5, "R_MTL": 5}
  2229. ftp['suvr'] = ftp['lobe'].map(lobe_to_value)
  2230. # Drop the `lobe` column
  2231. ftp = ftp.drop(columns=['lobe'])
  2232. # Pivot the dataframe
  2233. wide_ftp = ftp.pivot(index='subj', columns='roi', values='suvr').reset_index()
  2234. wide_ftp.rename(columns={'subj':'Image-name-unique'},inplace=True)
  2235. # Reset index for a cleaner display
  2236. #wide_ftp.to_csv(data_path+'roi_def_0.csv',index=False)
  2237. pd.read_csv(data_path+'roi_def.csv')
  2238. # %% [markdown]
  2239. # # Longitudinal
  2240. # - annual rate of change in SuStaIn stage (box+swarm, t-test)
  2241. # - percentage of subjects that progress/regress/remain in their SuStaIn stage (table, t-test)
  2242. # - subtype agreement between baseline visit (confusion matrix, kappa)
  2243. # %% [markdown]
  2244. # ## visit overview
  2245. # %%
  2246. # longitudinal data: exclude those that were poorly classified at baseline
  2247. # pre-processing
  2248. # 1. exclude sta ge 0 or poorly fit p < 0.5 at V1
  2249. print(len(combined))
  2250. print(len(combined.subj.unique()))
  2251. combined_exclude_long = combined.sort_values(by=['subj', 'ftp_date']).copy()
  2252. # Filter out subjects with stage 0 or subtype probability < 0.5 at the first visit
  2253. first_visit = combined_exclude_long.groupby('subj').first().reset_index()
  2254. exclude_mask = (first_visit['subtype_prob'] < 0.5) | (first_visit['stage'] == 0.)
  2255. combined_exclude_long = combined_exclude_long[~combined_exclude_long['subj'].isin(first_visit.loc[exclude_mask, 'subj'])]
  2256. print(f'total number of scans after excluding scans from subjects that are poorly & stage 0 assigned at baseline: {len(combined_exclude_long)}')
  2257. print(f'total number of subjects after excluding scans from subjects that are poorly assigned & stage 0 at baseline: {len(combined_exclude_long.subj.unique())}')
  2258. # %%
  2259. # anti amyloid
  2260. treatment = pd.read_csv(data_path + 'treatment_exclude.csv')
  2261. treatment['startdate'] = treatment['startdate'].str.replace('xx', '01', regex=False)
  2262. treatment['startdate'] = pd.to_datetime(treatment['startdate'], format='%Y-%m-%d', errors='coerce')
  2263. combined_exclude_long['ftp_date'] = pd.to_datetime(combined_exclude_long['ftp_date'], format='%Y-%m-%d', errors='coerce')
  2264. treatment_filtered = treatment[~treatment.txname.isna()][['subject_code', 'startdate']]
  2265. treatment_filtered['subject_code'] = treatment_filtered['subject_code'].str.upper()
  2266. merged_df = combined_exclude_long.merge(
  2267. treatment_filtered,
  2268. left_on='subj',
  2269. right_on='subject_code',
  2270. how='inner'
  2271. )
  2272. scans_to_exclude = merged_df[merged_df['ftp_date'] > merged_df['startdate']]
  2273. scans_to_exclude[['subj', 'ftp_date', 'startdate']]
  2274. # Exclude scans from combined_exclude_long that match subj and ftp_date in scans_to_exclude
  2275. print(len(combined_exclude_long.subj.unique()))
  2276. print(len(scans_to_exclude.subj.unique()))
  2277. # %%
  2278. combined_exclude_long = combined_exclude_long.merge(
  2279. scans_to_exclude[['subj', 'ftp_date']],
  2280. on=['subj', 'ftp_date'],
  2281. how='left',
  2282. indicator=True
  2283. ).query('_merge == "left_only"').drop(columns=['_merge'])
  2284. # %%
  2285. # Subject IDs before exclusion
  2286. before_subjects = set(merged_df['subj'].unique())
  2287. # Subject IDs after exclusion
  2288. after_subjects = set(combined_exclude_long['subj'].unique())
  2289. # Which subjects were dropped
  2290. dropped_subjects = before_subjects - after_subjects
  2291. print("Subjects dropped:", dropped_subjects)
  2292. print("Number dropped:", len(dropped_subjects))
  2293. # %%
  2294. # exclude poorly fitted longtudinal scans
  2295. combined_exclude_long = combined_exclude_long[~combined_exclude_long.poorly]
  2296. print(len(combined_exclude_long))
  2297. print(len(combined_exclude_long.subj.unique()))
  2298. # %%
  2299. # add baseline variable
  2300. bl_var = [
  2301. 'CDR-Global', 'Gender', 'Diagnosis', 'Cognitive Behavior', 'ApoE4 Genotype', 'ftp_file', 'ftp_date',
  2302. 'Yrs. of Education', 'Age', 'CDR-SB', 'SUVR PET-Only Composite', 'SUVR MRI-Based Composite', 'MMSE',
  2303. 'Centiloids MRI-Based Composite', 'Delayed Recall', 'Digit Total'
  2304. ]
  2305. combined_with_baseline = combined_exclude_long.merge(
  2306. combined_exclude[['subj'] + bl_var].drop_duplicates('subj'),
  2307. on='subj',
  2308. suffixes=('', '.baseline'),
  2309. how='left'
  2310. )
  2311. print(len(combined_with_baseline.subj.unique()))
  2312. # Convert dates to datetime format
  2313. combined_with_baseline['ftp_date'] = pd.to_datetime(combined_with_baseline['ftp_date'], errors='coerce')
  2314. combined_with_baseline['ftp_date.baseline'] = pd.to_datetime(combined_with_baseline['ftp_date.baseline'], errors='coerce')
  2315. # Calculate days from baseline
  2316. combined_with_baseline['day_to_baseline'] = (
  2317. (combined_with_baseline['ftp_date'] - combined_with_baseline['ftp_date.baseline']).dt.days
  2318. )
  2319. # Convert to integer
  2320. combined_with_baseline['day_to_baseline'] = combined_with_baseline['day_to_baseline'].fillna(0).astype(int)
  2321. # Create the file name column
  2322. combined_with_baseline['fname'] = combined_with_baseline.apply(
  2323. lambda row: f"/home/mac/mlin2/cluster_analysis/scans/swr{row['ftp_file']}_suvr-infcblgm.nii", axis=1
  2324. )
  2325. # List of subjects with incorrect dates
  2326. wrong_date = ['LDS9410287', 'LDS9410396']
  2327. # For the subjects in wrong_date, retain only their visit 1 data
  2328. combined_with_baseline = combined_with_baseline[~combined_with_baseline['subj'].isin(wrong_date) | (combined_with_baseline['visit'] == 1)]
  2329. print(len(combined_with_baseline.subj.unique()))
  2330. combined_exclude_long = combined_with_baseline.copy()
  2331. print(f'total number of scans after further exclusion of those obtained after subjects started treatments (or wrong dates): {len(combined_exclude_long)}')
  2332. print(f'total number of subjects after further exclusion of those obtained after subjects started treatments (or wrong dates): {len(combined_exclude_long.subj.unique())}')
  2333. # %%
  2334. visit_counts = combined_exclude_long.groupby('subj')['visit'].max().value_counts().sort_index(ascending=False).cumsum().sort_index()
  2335. visit_counts
  2336. # %%
  2337. # Sample data (replace with actual visit count data)
  2338. visit_counts = [354, 201, 76, 25] # Cumulative subject counts
  2339. visit_labels = ["1", "2", "3", "4+"] # Corresponding visit count labels
  2340. # Create a 3D-style funnel-like bar chart
  2341. fig = go.Figure(go.Funnel(
  2342. x=visit_counts,
  2343. y=visit_labels,
  2344. textinfo="value+percent initial",
  2345. marker=dict(color='black', line=dict(width=1, color="lightgrey"))
  2346. ))
  2347. # Customize layout to resemble the uploaded image
  2348. fig.update_layout(
  2349. title="Funnel Chart of Cumulative Subjects by Visit Count",
  2350. xaxis_title="Number of Subjects",
  2351. yaxis_title="Visit Count",
  2352. plot_bgcolor="white",
  2353. paper_bgcolor="white",
  2354. width=600,
  2355. height=500
  2356. )
  2357. # Display the plot
  2358. fig.show()
  2359. # %% [markdown]
  2360. # ### followup
  2361. # %%
  2362. # Ensure 'combined_exclude_long' is loaded with a column 'ftp_date'
  2363. # Convert 'ftp_date' to datetime format
  2364. combined_exclude_long['ftp_date'] = pd.to_datetime(combined_exclude_long['ftp_date'])
  2365. # Find the first (baseline) and last visit for each subject
  2366. first_last_visits = combined_exclude_long.groupby('subj')['ftp_date'].agg(['min', 'max']).reset_index()
  2367. # Exclude subjects where the first and last visit dates are the same
  2368. valid_visits = first_last_visits[first_last_visits['min']!=first_last_visits['max']]
  2369. # Calculate the difference between the first and last visit in years
  2370. valid_visits['follow_up_interval'] = (valid_visits['max'] - valid_visits['min']).dt.days / 365
  2371. # Merge baseline visit date (min) with the original data to retain subtype assignment
  2372. baseline_visits = valid_visits[['subj', 'min']].merge(
  2373. combined_exclude_long,
  2374. left_on=['subj', 'min'],
  2375. right_on=['subj', 'ftp_date'],
  2376. how='left'
  2377. )
  2378. # Retain the subtype from the baseline visit in the valid_visits DataFrame
  2379. valid_visits = valid_visits.merge(baseline_visits[['subj', 'subtype']], on='subj', how='left')
  2380. # Ensure 'subtype' refers to the subtype assigned at the first visit
  2381. valid_visits.rename(columns={'subtype': 'baseline_subtype'}, inplace=True)
  2382. # Calculate mean and standard deviation of the follow-up interval
  2383. mean_interval = valid_visits['follow_up_interval'].mean()
  2384. std_interval = valid_visits['follow_up_interval'].std()
  2385. print(f"Average Follow-Up Interval: {mean_interval:.2f} years")
  2386. print(f"Standard Deviation of Follow-Up Interval: {std_interval:.2f} years")
  2387. # %%
  2388. # Group follow-up intervals by subtypes
  2389. subtype_groups = [
  2390. valid_visits[valid_visits['baseline_subtype'] == subtype]['follow_up_interval']
  2391. for subtype in valid_visits['baseline_subtype'].unique()
  2392. ]
  2393. # Perform one-way ANOVA
  2394. f_stat, p_value = f_oneway(*subtype_groups)
  2395. # Print results
  2396. print("One-Way ANOVA Results:")
  2397. print(f"F-Statistic: {f_stat:.2f}")
  2398. print(f"P-Value: {p_value:.4f}")
  2399. print(f"total subjects with follow-up: {len(valid_visits.subj.unique())}")
  2400. # Optional: Print means and standard deviations for each subtype
  2401. for subtype in valid_visits['baseline_subtype'].unique():
  2402. data = valid_visits[valid_visits['baseline_subtype'] == subtype]['follow_up_interval']
  2403. print(f"Subtype {subtype}:")
  2404. print(f" Mean Follow-Up Interval: {data.mean():.2f} years")
  2405. print(f" SD Follow-Up Interval: {data.std():.2f} years")
  2406. print("-" * 40)
  2407. # %% [markdown]
  2408. # Fig. 12: Vist overview
  2409. # %% [markdown]
  2410. # ## saving for r (subj list reference)
  2411. # %%
  2412. renamed_lme = combined_exclude_long.copy()
  2413. renamed_lme['CDR_SB'] = renamed_lme['CDR-SB']
  2414. combined_exclude_long['ftp_date'] = pd.to_datetime(combined_exclude_long['ftp_date'])
  2415. first_last_visits = combined_exclude_long.groupby('subj')['ftp_date'].agg(['min', 'max']).reset_index()
  2416. valid_visits = first_last_visits[first_last_visits['min']!=first_last_visits['max']]
  2417. renamed_lme = renamed_lme[renamed_lme.subj.isin(valid_visits.subj)]
  2418. #renamed_lme.to_csv(data_path+'combined_exclude_long_forR_longi.csv',index=False)
  2419. len(renamed_lme)
  2420. # %% [markdown]
  2421. # ## changed subtype subjects
  2422. # %%
  2423. d_changed = combined_exclude_long.copy()
  2424. # Ensure 'ftp_date' is datetime and 'stage' is numeric
  2425. d_changed['ftp_date'] = pd.to_datetime(d_changed['ftp_date'], errors='coerce')
  2426. d_changed['stage'] = pd.to_numeric(d_changed['stage'], errors='coerce')
  2427. # Calculate first and last stage for each subject
  2428. first_visit = d_changed.groupby('subj').first().reset_index()
  2429. last_visit = d_changed.groupby('subj').last().reset_index()
  2430. # Merge first and last visit information
  2431. progression_data = pd.merge(
  2432. first_visit[['subj', 'subtype', 'stage', 'ftp_date', 'subtype_prob', 'stage_prob']],
  2433. last_visit[['subj', 'subtype', 'stage', 'ftp_date', 'subtype_prob', 'stage_prob']],
  2434. on='subj', suffixes=('_first', '_last')
  2435. )
  2436. # Calculate yearly stage change
  2437. progression_data['years_diff'] = (progression_data['ftp_date_last'] - progression_data['ftp_date_first']).dt.days / 365
  2438. progression_data['rate_of_progression'] = (progression_data['stage_last'] - progression_data['stage_first']) / progression_data['years_diff']
  2439. # Identify subjects who changed subtype
  2440. progression_data['changed_subtype'] = progression_data['subtype_first'] != progression_data['subtype_last']
  2441. # Add "Changed Subtype" as a category for plotting
  2442. progression_data['subtype_plot'] = progression_data['subtype_first'].astype(str)
  2443. progression_data.loc[progression_data['changed_subtype'], 'subtype_plot'] = 'Changed Subtype'
  2444. # Filter the baseline visit (most recent = visit 1)
  2445. baseline_data = d_changed[d_changed['visit'] == 1]
  2446. # Merge progression data with baseline data
  2447. merged_data = pd.merge(progression_data, baseline_data, on='subj')
  2448. # only those with longitudinal
  2449. longitudinal_subjects = d_changed['subj'].value_counts()
  2450. longitudinal_subjects = longitudinal_subjects[longitudinal_subjects > 1].index
  2451. merged_data = merged_data[merged_data['subj'].isin(longitudinal_subjects)]
  2452. variables_to_compare = [ 'stage_first',
  2453. 'subtype_prob_first', 'stage_prob_first',
  2454. 'rate_of_progression', 'SUVR_Frontal', 'SUVR_MesialTemporal',
  2455. 'SUVR_MetaTemporal', 'SUVR_TemporoParietal', 'SUVR_Universal',
  2456. 'SUVR_PET_Only_Composite_baseline', 'SUVR_MRI_Based_Composite_baseline',
  2457. 'Centiloids_MRI_Based_Composite_baseline', 'MMSE_baseline',
  2458. 'CDR_SB_baseline',
  2459. 'Age_baseline', 'Yrs_of_Education_baseline',
  2460. 'ApoE4_Genotype', 'Gender', 'Diagnosis_baseline']
  2461. # %%
  2462. # Set seaborn theme without grid
  2463. sns.set(style="white")
  2464. # Define variables
  2465. parametric_vars = ['subtype_prob', 'stage_prob', 'stage']
  2466. pretty_labels = {
  2467. 'subtype_prob': 'Baseline Subtype Probability',
  2468. 'stage_prob': 'Baseline Stage Probability',
  2469. 'stage': 'Baseline Stage'
  2470. }
  2471. y_lims = {
  2472. 'subtype_prob': (0.5, 1.025),
  2473. 'stage_prob': (0, 1.05),
  2474. 'stage': (0.2, 20)
  2475. }
  2476. y_ticks = {
  2477. 'subtype_prob': np.arange(0.5, 1.05, 0.1),
  2478. 'stage_prob': np.arange(0, 1.1, 0.1),
  2479. 'stage': np.arange(0, 21, 1)
  2480. }
  2481. # Set up the figure with 3 subplots in a single row
  2482. fig, axes = plt.subplots(1, 3, figsize=(18, 5), sharex=False)
  2483. for ax, var in zip(axes, parametric_vars):
  2484. # Boxplot
  2485. sns.boxplot(
  2486. data=merged_data,
  2487. x='changed_subtype',
  2488. y=var,
  2489. notch=True,
  2490. showfliers=False,
  2491. boxprops=dict(alpha=0.7),
  2492. ax=ax
  2493. )
  2494. # Overlay stripplot
  2495. sns.stripplot(
  2496. data=merged_data,
  2497. x='changed_subtype',
  2498. y=var,
  2499. color='black',
  2500. size=4,
  2501. alpha=0.5,
  2502. jitter=True,
  2503. ax=ax
  2504. )
  2505. # Set labels (only y-axis individually)
  2506. ax.set_title(pretty_labels[var], fontsize=16)
  2507. ax.set_ylabel('')
  2508. ax.set_ylim(y_lims[var])
  2509. ax.set_yticks(y_ticks[var])
  2510. ax.set_xticks([0, 1])
  2511. ax.set_xticklabels(['Unchanged', 'Changed'],fontsize=16)
  2512. # Remove individual x-axis labels
  2513. ax.set_xlabel('')
  2514. # Turn off grid
  2515. ax.grid(False)
  2516. # Add one unified x-axis label
  2517. fig.supxlabel('Subtype Stability Between First and Last Visit', fontsize=16)
  2518. plt.tight_layout()
  2519. plt.show()
  2520. # %%
  2521. # Set seaborn theme without grid
  2522. sns.set(style="white")
  2523. # Define variables
  2524. var = 'subtype_prob'
  2525. pretty_label = 'Baseline Subtype Probability'
  2526. y_lim = (0.5, 1.025)
  2527. y_ticks = np.arange(0.5, 1.05, 0.1)
  2528. fig, ax = plt.subplots(figsize=(7, 6))
  2529. # Boxplot
  2530. sns.boxplot(
  2531. data=merged_data,
  2532. x='changed_subtype',
  2533. y=var,
  2534. notch=True,
  2535. showfliers=False,
  2536. boxprops=dict(alpha=0.7),
  2537. color='grey',
  2538. ax=ax
  2539. )
  2540. # Overlay stripplot
  2541. sns.stripplot(
  2542. data=merged_data,
  2543. x='changed_subtype',
  2544. y=var,
  2545. color='black',
  2546. size=4,
  2547. alpha=0.5,
  2548. jitter=True,
  2549. ax=ax
  2550. )
  2551. # Set title and axis limits
  2552. ax.set_title(pretty_label, fontsize=16)
  2553. ax.set_ylim(y_lim)
  2554. ax.set_yticks(y_ticks)
  2555. ax.set_xticks([0, 1])
  2556. ax.set_xticklabels(['Unchanged', 'Changed'], fontsize=14)
  2557. ax.set_xlabel('Subtype Stability Between First and Last Visit', fontsize=14)
  2558. ax.set_ylabel('')
  2559. # Dashed line at 0.95
  2560. ax.axhline(0.95, color='gray', linestyle='--')
  2561. # Annotate counts and percentages
  2562. total_n = len(merged_data)
  2563. for x_pos, group_label in zip([0, 1], ['Unchanged', 'Changed']):
  2564. count = merged_data[merged_data['changed_subtype'] == x_pos].shape[0]
  2565. pct = (count / total_n) * 100
  2566. ax.annotate(f'n = {count}\n({pct:.1f}%)',
  2567. xy=(x_pos, y_lim[0] - 0.015),
  2568. ha='center', va='top',
  2569. fontsize=12)
  2570. plt.tight_layout()
  2571. plt.show()
  2572. # %%
  2573. # Binary indicator: high subtype probability at baseline
  2574. merged_data['high_subtype_prob'] = merged_data['subtype_prob_first'] > 0.95
  2575. merged_data['changed'] = merged_data['changed_subtype'].astype(bool)
  2576. # Chi-square test: high subtype probability vs changed
  2577. contingency_table = pd.crosstab(merged_data['changed'], merged_data['high_subtype_prob'])
  2578. chi2_stat, chi2_p, chi2_df, _ = chi2_contingency(contingency_table)
  2579. print("\n--- Chi-Square Test: baseline subtype prob ---")
  2580. print(f"Chi2 Statistic: {chi2_stat:.3f}")
  2581. print(f"Degrees of Freedom: {chi2_df}")
  2582. print(f"p-value: {chi2_p:.4f}")
  2583. # T-test: baseline stage between changed and unchanged groups
  2584. stage_changed = merged_data.loc[merged_data['changed'], 'stage_first'].dropna()
  2585. stage_unchanged = merged_data.loc[~merged_data['changed'], 'stage_first'].dropna()
  2586. t_stat, t_p = ttest_ind(stage_unchanged, stage_changed, equal_var=False)
  2587. df_ttest = len(stage_changed) + len(stage_unchanged) - 2
  2588. print("\n--- T-Test: Baseline Stage ---")
  2589. print(f"T Statistic: {t_stat:.3f}")
  2590. print(f"Degrees of Freedom: {df_ttest}")
  2591. print(f"p-value: {t_p:.4f}")
  2592. print(f"Mean Stage (Unchanged): {stage_unchanged.mean():.2f}")
  2593. print(f"Mean Stage (Changed): {stage_changed.mean():.2f}")
  2594. # %% [markdown]
  2595. # ## first and last visit
  2596. # %% [markdown]
  2597. # ### progression rate
  2598. # %%
  2599. # Step 1: Prepare Data
  2600. img1 = combined_exclude_long.copy()
  2601. # Ensure 'ftp_date' is datetime and 'stage' is numeric
  2602. img1['ftp_date'] = pd.to_datetime(img1['ftp_date'], errors='coerce')
  2603. img1['stage'] = pd.to_numeric(img1['stage'], errors='coerce')
  2604. # Drop rows with invalid or missing data
  2605. img1 = img1.dropna(subset=['ftp_date', 'stage'])
  2606. # Calculate first and last stage for each subject
  2607. first_visit = img1.groupby('subj').first().reset_index()
  2608. last_visit = img1.groupby('subj').last().reset_index()
  2609. # Merge first and last visit information
  2610. progression_data = pd.merge(
  2611. first_visit[['subj', 'subtype', 'stage', 'ftp_date']],
  2612. last_visit[['subj', 'subtype', 'stage', 'ftp_date']],
  2613. on='subj', suffixes=('_first', '_last')
  2614. )
  2615. # Calculate yearly stage change
  2616. progression_data['years_diff'] = (progression_data['ftp_date_last'] - progression_data['ftp_date_first']).dt.days / 365
  2617. progression_data['rate_of_progression'] = (progression_data['stage_last'] - progression_data['stage_first']) / progression_data['years_diff']
  2618. # Identify subjects who changed subtype
  2619. progression_data['changed_subtype'] = progression_data['subtype_first'] != progression_data['subtype_last']
  2620. # Filter out subjects with only one visit
  2621. progression_data = progression_data[progression_data['years_diff'] > 0]
  2622. # Step 2: Add "Changed Subtype" as a category
  2623. progression_data['subtype_plot'] = progression_data['subtype_first'].astype(str)
  2624. # progression_data.loc[progression_data['changed_subtype'], 'subtype_plot'] = 'Changed Subtype'
  2625. # Step 3: Test progression rate within each subtype
  2626. print("\n--- Annual Progression Rate of Stage ≠ 0 ---\n")
  2627. for subtype, group in progression_data.groupby('subtype_plot'):
  2628. group = group['rate_of_progression'].dropna() # Remove NaNs
  2629. n = len(group)
  2630. mean_progression = group.mean()
  2631. sd_progression = group.std()
  2632. # Perform one-sample t-test
  2633. t_stat, p_value = ttest_1samp(group, 0)
  2634. # Calculate 95% CI for the mean progression rate
  2635. ci_range = t.ppf(0.975, df=n-1) * (sd_progression / np.sqrt(n))
  2636. ci_lower = mean_progression - ci_range
  2637. ci_upper = mean_progression + ci_range
  2638. # Print results
  2639. print(f"Subtype {subtype}:")
  2640. print(f" Mean Progression Rate: {mean_progression:.2f} [{ci_lower:.2f}, {ci_upper:.2f}]")
  2641. print(f" SD Progression Rate: {sd_progression:.2f}")
  2642. print(f" 95% CI: [{ci_lower:.2f}, {ci_upper:.2f}]")
  2643. print(f" t-statistic: {t_stat:.2f}, p-value: {p_value:.4f}\n")
  2644. overall_group = progression_data[~progression_data.changed_subtype]['rate_of_progression'].dropna() # Remove NaNs
  2645. n_overall = len(overall_group)
  2646. mean_overall = overall_group.mean()
  2647. sd_overall = overall_group.std()
  2648. # Perform one-sample t-test for overall progression rate
  2649. t_stat_overall, p_value_overall = ttest_1samp(overall_group, 0)
  2650. # Calculate 95% CI for overall progression rate
  2651. ci_range_overall = t.ppf(0.975, df=n_overall-1) * (sd_overall / np.sqrt(n_overall))
  2652. ci_lower_overall = mean_overall - ci_range_overall
  2653. ci_upper_overall = mean_overall + ci_range_overall
  2654. # Print overall results
  2655. print("\n--- Overall Progression Rate ---")
  2656. print(f"Mean Progression Rate: {mean_overall:.2f} [{ci_lower_overall:.2f}, {ci_upper_overall:.2f}]")
  2657. print(f"SD Progression Rate: {sd_overall:.2f}")
  2658. print(f"95% CI: [{ci_lower_overall:.2f}, {ci_upper_overall:.2f}]")
  2659. print(f"t-statistic: {t_stat_overall:.2f}, p-value: {p_value_overall:.4f}\n")
  2660. # Step 4: ANOVA to test differences among subtypes
  2661. anova_groups = [group['rate_of_progression'].dropna() for _, group in progression_data[~progression_data.changed_subtype].groupby('subtype_plot')]
  2662. anova_stat, anova_pval = f_oneway(*anova_groups)
  2663. print("\n--- ANOVA Test for Differences Between Subtypes ---")
  2664. print(f"F-statistic: {anova_stat:.2f}, p-value: {anova_pval:.4f}\n")
  2665. # Step 5: Post-hoc Tukey's HSD Test (if ANOVA is significant)
  2666. if anova_pval < 0.05:
  2667. print("\n--- Post-hoc Pairwise Comparison (Tukey's HSD) ---\n")
  2668. tukey = pairwise_tukeyhsd(
  2669. endog=progression_data['rate_of_progression'],
  2670. groups=progression_data['subtype_plot'],
  2671. alpha=0.05
  2672. )
  2673. print(tukey)
  2674. # Step 6: Plot the boxplot with annotations
  2675. unique_subtypes = progression_data['subtype_plot'].unique()
  2676. sorted_subtypes = sorted(unique_subtypes)
  2677. progression_data['subtype_plot'] = pd.Categorical(
  2678. progression_data['subtype_plot'],
  2679. categories=sorted_subtypes,
  2680. ordered=True
  2681. )
  2682. plt.figure(figsize=(8,5))
  2683. # Boxplot
  2684. sns.boxplot(data=progression_data[~progression_data.changed_subtype], x='subtype_plot', y='rate_of_progression', color='grey')
  2685. # Swarmplot
  2686. sns.swarmplot(data=progression_data[~progression_data.changed_subtype], x='subtype_plot', y='rate_of_progression',
  2687. hue='subtype_plot', palette=colors, dodge=False, size=4, legend=False)
  2688. # Add counts above boxes
  2689. group_counts = progression_data[~progression_data.changed_subtype]['subtype_plot'].value_counts()
  2690. for i, subtype in enumerate(sorted_subtypes):
  2691. count = group_counts.get(subtype, 0)
  2692. # plt.text(i, progression_data[~progression_data.changed_subtype]['rate_of_progression'].max() + 0.1, str(count),
  2693. # ha='center', color='black', fontsize=12)
  2694. # Add horizontal line at y=0
  2695. plt.axhline(0, color='black', linestyle='--', linewidth=1)
  2696. # Labels and title
  2697. plt.xlabel("Subtype")
  2698. plt.ylabel("Yearly Change in Stage")
  2699. #plt.title("Annual Change in Stage by Subtype")
  2700. plt.tight_layout()
  2701. plt.show()
  2702. # %% [markdown]
  2703. # Fig. 13: Annual progression rate of each subtype and those with different subtypes between first and last visit. Does not find significant difference in rate of progression among those that stay within the same subtype.
  2704. # %% [markdown]
  2705. # ### percentage stability
  2706. # %%
  2707. # Step 4: Progression Summary Table
  2708. # Define progression categories
  2709. progression_data['change_type'] = np.where(
  2710. progression_data['stage_last'] > progression_data['stage_first'], 'Progressed',
  2711. np.where(progression_data['stage_last'] < progression_data['stage_first'], 'Regressed', 'Stable')
  2712. )
  2713. # Calculate percentages for each subtype
  2714. summary_table = progression_data.groupby('subtype_plot')['change_type'].value_counts(normalize=True).unstack().fillna(0) * 100
  2715. summary_table.loc['All'] = progression_data['change_type'].value_counts(normalize=True) * 100 # Overall row
  2716. # Round values for better readability
  2717. summary_table = summary_table.round(2)
  2718. # Reorder columns and ensure all change types are included
  2719. summary_table = summary_table[['Progressed', 'Regressed', 'Stable']]
  2720. # %%
  2721. summary_table = summary_table.reset_index()
  2722. summary_table[summary_table.subtype_plot!="Changed Subtype"].iloc[:,1:]
  2723. # %% [markdown]
  2724. # ### subtype aggrement
  2725. # %%
  2726. # Merge first and last visit subtypes based on subject IDs
  2727. img2 = img1.sort_values(by=['subj', 'ftp_date']).copy()
  2728. # Filter subjects with at least two visits
  2729. subject_visit_counts = img2['subj'].value_counts()
  2730. subjects_with_multiple_visits = subject_visit_counts[subject_visit_counts > 1].index
  2731. img2 = img2[img2['subj'].isin(subjects_with_multiple_visits)]
  2732. # Step 2: Extract first and last visit data
  2733. img2_first = img2.groupby('subj').first().reset_index()
  2734. img2_last = img2.groupby('subj').last().reset_index()
  2735. agreement_data = pd.merge(
  2736. img2_first[['subj', 'subtype']],
  2737. img2_last[['subj', 'subtype']],
  2738. on='subj',
  2739. suffixes=('_first', '_last')
  2740. )
  2741. # Drop rows with invalid or missing subtypes
  2742. agreement_data = agreement_data.dropna(subset=['subtype_first', 'subtype_last'])
  2743. # Ensure subtype columns are numeric
  2744. agreement_data['subtype_first'] = pd.to_numeric(agreement_data['subtype_first'], errors='coerce')
  2745. agreement_data['subtype_last'] = pd.to_numeric(agreement_data['subtype_last'], errors='coerce')
  2746. # Drop rows where subtypes are still NaN after conversion
  2747. agreement_data = agreement_data.dropna(subset=['subtype_first', 'subtype_last'])
  2748. # Get all possible subtypes
  2749. all_subtypes = sorted(set(agreement_data['subtype_first']).union(set(agreement_data['subtype_last'])))
  2750. # Calculate the confusion matrix
  2751. conf_matrix = confusion_matrix(agreement_data['subtype_last'], agreement_data['subtype_first'], labels=all_subtypes)
  2752. # Calculate agreement metrics
  2753. total_correct = np.trace(conf_matrix)
  2754. total_cases = conf_matrix.sum()
  2755. agreement_percentage = (total_correct / total_cases) * 100
  2756. kappa = cohen_kappa_score(agreement_data['subtype_first'], agreement_data['subtype_last'])
  2757. # Plot the confusion matrix
  2758. plt.figure(figsize=(5,4))
  2759. sns.heatmap(
  2760. conf_matrix, annot=True , fmt='d', cmap='Blues',
  2761. xticklabels=all_subtypes, yticklabels=all_subtypes,
  2762. annot_kws={"size": 18}
  2763. )
  2764. plt.xticks(ticks=[0.5, 1.5, 2.5], labels=['S1/Typical', 'S2/L Temporal', 'S3/Occipital'])
  2765. plt.yticks(ticks=[0.5, 1.5, 2.5], labels=['S1', 'S2', 'S3'])
  2766. plt.xlabel("Subtype at First Visit")
  2767. plt.ylabel("Subtype at Last Visit")
  2768. plt.title(
  2769. f"Subtype Assignment Agreement Between First and Last Visit\n"
  2770. f"Total Agreement: {agreement_percentage:.2f}%, κ: {kappa:.2f}",
  2771. )
  2772. plt.tight_layout()
  2773. plt.show()
  2774. # %% [markdown]
  2775. # ## first and second visit
  2776. # %% [markdown]
  2777. # ### progression
  2778. # %%
  2779. # Step 1: Prepare Data
  2780. img1 = combined_exclude_long[combined_exclude_long.visit<=2].copy()
  2781. # Ensure 'ftp_date' is datetime and 'stage' is numeric
  2782. img1['ftp_date'] = pd.to_datetime(img1['ftp_date'], errors='coerce')
  2783. img1['stage'] = pd.to_numeric(img1['stage'], errors='coerce')
  2784. # Drop rows with invalid or missing data
  2785. img1 = img1.dropna(subset=['ftp_date', 'stage'])
  2786. # Calculate first and last stage for each subject
  2787. first_visit = img1.groupby('subj').first().reset_index()
  2788. last_visit = img1.groupby('subj').last().reset_index()
  2789. # Merge first and last visit information
  2790. progression_data = pd.merge(
  2791. first_visit[['subj', 'subtype', 'stage', 'ftp_date']],
  2792. last_visit[['subj', 'subtype', 'stage', 'ftp_date']],
  2793. on='subj', suffixes=('_first', '_last')
  2794. )
  2795. # Calculate yearly stage change
  2796. progression_data['years_diff'] = (progression_data['ftp_date_last'] - progression_data['ftp_date_first']).dt.days / 365
  2797. progression_data['rate_of_progression'] = (progression_data['stage_last'] - progression_data['stage_first']) / progression_data['years_diff']
  2798. # Identify subjects who changed subtype
  2799. progression_data['changed_subtype'] = progression_data['subtype_first'] != progression_data['subtype_last']
  2800. # Filter out subjects with only one visit
  2801. progression_data = progression_data[progression_data['years_diff'] > 0]
  2802. # Step 2: Add "Changed Subtype" as a category
  2803. progression_data['subtype_plot'] = progression_data['subtype_first'].astype(str)
  2804. # progression_data.loc[progression_data['changed_subtype'], 'subtype_plot'] = 'Changed Subtype'
  2805. # Step 3: Test progression rate within each subtype
  2806. print("\n--- Annual Progression Rate of Stage ≠ 0 ---\n")
  2807. for subtype, group in progression_data.groupby('subtype_plot'):
  2808. group = group['rate_of_progression'].dropna() # Remove NaNs
  2809. n = len(group)
  2810. mean_progression = group.mean()
  2811. sd_progression = group.std()
  2812. # Perform one-sample t-test
  2813. t_stat, p_value = ttest_1samp(group, 0)
  2814. # Calculate 95% CI for the mean progression rate
  2815. ci_range = t.ppf(0.975, df=n-1) * (sd_progression / np.sqrt(n))
  2816. ci_lower = mean_progression - ci_range
  2817. ci_upper = mean_progression + ci_range
  2818. # Print results
  2819. print(f"Subtype {subtype}:")
  2820. print(f" Mean Progression Rate: {mean_progression:.2f} [{ci_lower:.2f}, {ci_upper:.2f}]")
  2821. print(f" SD Progression Rate: {sd_progression:.2f}")
  2822. print(f" 95% CI: [{ci_lower:.2f}, {ci_upper:.2f}]")
  2823. print(f" t-statistic: {t_stat:.2f}, p-value: {p_value:.4f}\n")
  2824. overall_group = progression_data[~progression_data.changed_subtype]['rate_of_progression'].dropna() # Remove NaNs
  2825. n_overall = len(overall_group)
  2826. mean_overall = overall_group.mean()
  2827. sd_overall = overall_group.std()
  2828. # Perform one-sample t-test for overall progression rate
  2829. t_stat_overall, p_value_overall = ttest_1samp(overall_group, 0)
  2830. # Calculate 95% CI for overall progression rate
  2831. ci_range_overall = t.ppf(0.975, df=n_overall-1) * (sd_overall / np.sqrt(n_overall))
  2832. ci_lower_overall = mean_overall - ci_range_overall
  2833. ci_upper_overall = mean_overall + ci_range_overall
  2834. # Print overall results
  2835. print("\n--- Overall Progression Rate ---")
  2836. print(f"Mean Progression Rate: {mean_overall:.2f} [{ci_lower_overall:.2f}, {ci_upper_overall:.2f}]")
  2837. print(f"SD Progression Rate: {sd_overall:.2f}")
  2838. print(f"95% CI: [{ci_lower_overall:.2f}, {ci_upper_overall:.2f}]")
  2839. print(f"t-statistic: {t_stat_overall:.2f}, p-value: {p_value_overall:.4f}\n")
  2840. # Step 4: ANOVA to test differences among subtypes
  2841. anova_groups = [group['rate_of_progression'].dropna() for _, group in progression_data[~progression_data.changed_subtype].groupby('subtype_plot')]
  2842. anova_stat, anova_pval = f_oneway(*anova_groups)
  2843. print("\n--- ANOVA Test for Differences Between Subtypes ---")
  2844. print(f"F-statistic: {anova_stat:.2f}, p-value: {anova_pval:.4f}\n")
  2845. # Step 5: Post-hoc Tukey's HSD Test (if ANOVA is significant)
  2846. if anova_pval < 0.05:
  2847. print("\n--- Post-hoc Pairwise Comparison (Tukey's HSD) ---\n")
  2848. tukey = pairwise_tukeyhsd(
  2849. endog=progression_data['rate_of_progression'],
  2850. groups=progression_data['subtype_plot'],
  2851. alpha=0.05
  2852. )
  2853. print(tukey)
  2854. # Step 6: Plot the boxplot with annotations
  2855. unique_subtypes = progression_data['subtype_plot'].unique()
  2856. sorted_subtypes = sorted(unique_subtypes)
  2857. progression_data['subtype_plot'] = pd.Categorical(
  2858. progression_data['subtype_plot'],
  2859. categories=sorted_subtypes,
  2860. ordered=True
  2861. )
  2862. plt.figure(figsize=(8,5))
  2863. # Boxplot
  2864. sns.boxplot(data=progression_data[~progression_data.changed_subtype], x='subtype_plot', y='rate_of_progression', color='grey')
  2865. # Swarmplot
  2866. sns.swarmplot(data=progression_data[~progression_data.changed_subtype], x='subtype_plot', y='rate_of_progression',
  2867. hue='subtype_plot', palette=colors, dodge=False, size=4, legend=False)
  2868. # Add counts above boxes
  2869. group_counts = progression_data[~progression_data.changed_subtype]['subtype_plot'].value_counts()
  2870. for i, subtype in enumerate(sorted_subtypes):
  2871. count = group_counts.get(subtype, 0)
  2872. # plt.text(i, progression_data[~progression_data.changed_subtype]['rate_of_progression'].max() + 0.1, str(count),
  2873. # ha='center', color='black', fontsize=12)
  2874. # Add horizontal line at y=0
  2875. plt.axhline(0, color='black', linestyle='--', linewidth=1)
  2876. # Labels and title
  2877. plt.xlabel("Subtype")
  2878. plt.ylabel("Yearly Change in Stage")
  2879. #plt.title("Annual Change in Stage by Subtype")
  2880. plt.tight_layout()
  2881. plt.show()
  2882. # %% [markdown]
  2883. # ### percentage stability
  2884. # %%
  2885. # Step 4: Progression Summary Table
  2886. # Define progression categories
  2887. progression_data['change_type'] = np.where(
  2888. progression_data['stage_last'] > progression_data['stage_first'], 'Progressed',
  2889. np.where(progression_data['stage_last'] < progression_data['stage_first'], 'Regressed', 'Stable')
  2890. )
  2891. # Calculate percentages for each subtype
  2892. summary_table = progression_data.groupby('subtype_plot')['change_type'].value_counts(normalize=True).unstack().fillna(0) * 100
  2893. summary_table.loc['All'] = progression_data['change_type'].value_counts(normalize=True) * 100 # Overall row
  2894. # Round values for better readability
  2895. summary_table = summary_table.round(2)
  2896. # Reorder columns and ensure all change types are included
  2897. summary_table = summary_table[['Progressed', 'Regressed', 'Stable']]
  2898. # Print the progression summary table in the desired format
  2899. print("\nProgression Summary Table (%):")
  2900. print(summary_table.to_string(float_format=lambda x: f"{x:.1f}")) # Controls float formatting
  2901. # %%
  2902. summary_table = summary_table.reset_index()
  2903. summary_table[summary_table.subtype_plot!="Changed Subtype"].iloc[:,1:]
  2904. # %% [markdown]
  2905. # ### subtype aggrement
  2906. # %%
  2907. img2 = img1.sort_values(by=['subj', 'ftp_date']).copy()
  2908. # Filter subjects with at least two visits
  2909. subject_visit_counts = img2['subj'].value_counts()
  2910. subjects_with_multiple_visits = subject_visit_counts[subject_visit_counts > 1].index
  2911. img2 = img2[img2['subj'].isin(subjects_with_multiple_visits)]
  2912. # Step 2: Extract first and last visit data
  2913. img2_first = img2.groupby('subj').first().reset_index()
  2914. img2_last = img2.groupby('subj').last().reset_index()
  2915. agreement_data = pd.merge(
  2916. img2_first[['subj', 'subtype']],
  2917. img2_last[['subj', 'subtype']],
  2918. on='subj',
  2919. suffixes=('_first', '_last')
  2920. )
  2921. # Drop rows with invalid or missing subtypes
  2922. agreement_data = agreement_data.dropna(subset=['subtype_first', 'subtype_last'])
  2923. # Ensure subtype columns are numeric
  2924. agreement_data['subtype_first'] = pd.to_numeric(agreement_data['subtype_first'], errors='coerce')
  2925. agreement_data['subtype_last'] = pd.to_numeric(agreement_data['subtype_last'], errors='coerce')
  2926. # Drop rows where subtypes are still NaN after conversion
  2927. agreement_data = agreement_data.dropna(subset=['subtype_first', 'subtype_last'])
  2928. # Get all possible subtypes
  2929. all_subtypes = sorted(set(agreement_data['subtype_first']).union(set(agreement_data['subtype_last'])))
  2930. # Calculate the confusion matrix
  2931. conf_matrix = confusion_matrix(agreement_data['subtype_last'], agreement_data['subtype_first'], labels=all_subtypes)
  2932. # Calculate agreement metrics
  2933. total_correct = np.trace(conf_matrix)
  2934. total_cases = conf_matrix.sum()
  2935. agreement_percentage = (total_correct / total_cases) * 100
  2936. kappa = cohen_kappa_score(agreement_data['subtype_first'], agreement_data['subtype_last'])
  2937. # Plot the confusion matrix
  2938. plt.figure(figsize=(8, 6))
  2939. sns.heatmap(
  2940. conf_matrix, annot=True, fmt='d', cmap='Blues',
  2941. xticklabels=all_subtypes, yticklabels=all_subtypes,
  2942. annot_kws={"size": 16}
  2943. )
  2944. plt.xlabel("Subtype at First Visit", fontsize=14)
  2945. plt.ylabel("Subtype at Second Visit", fontsize=14)
  2946. plt.title(
  2947. f"Subtype Assignment Agreement Between First and Second Visit\n"
  2948. f"Total Agreement: {agreement_percentage:.2f}%, Cohen's Kappa: {kappa:.2f}",
  2949. fontsize=14
  2950. )
  2951. plt.tight_layout()
  2952. plt.show()

re_inter_c2mean_20.ipynb at commit 4ce80c8, under MIT · at the source

Overview

Authors: Marlene Lin1, Konstantinos Chiotis1,2,3, Piyush Maiti1, Jiaxiuxiu Zhang1, Ganna Blazhenets1, Salma Rocha1, Ranjani Shankar1, Alinda Amuiri1, Dustin B Hammers4, Ani Eloyan5, Kala Kirby4, Robert A Koeppe6, Paul Aisen7, Laurel Beckett8, Walter A Kukull9, Arthur W Toga10, Alireza Atri11, David G Clark4, Gregory S Day12, Ranjan Duara13
and 29 other authorsNeill R Graff-Radford12, Ian Grant14, Lawrence S Honig15, Erik C B Johnson16, David T Jones17, Joseph C Masdeu18, Mario F Mendez19, Erik Musiek9, Chiadi U Onyike20, Meghan Riddle21, Emily Rogalski22, Stephen Salloway21, Sharon J Sha23, Raymond Scott Turner24, Thomas S Wingo25, David A Wolk26, Kyle B Womack9, Kelly Nicole Holohan Nudelman27, Alexandra Touroutoglou28, Clifford R Jack Jr.17, Prashanthi Vemuri17, Jacob W Vogel29, Suzanne M Dufault30, Thomas J Hoffmann30, Maria C Carrillo31, Bradford C Dickerson28, Liana G Apostolova4, Gil D Rabinovici1,32, Renaud La Joie1
32 affiliations
  1. Edward and Pearl Fein Memory and Aging Center, Weill Institute for Neurosciences, Department of Neurology, University of California San Francisco, San Francisco, CA, USA
  2. Center for Alzheimer Research, Division of Clinical Geriatrics, Department of Neurobiology, Care Sciences and Society, Karolinska Institutet, Stockholm, Sweden
  3. Department of Cognitive Disorders, Karolinska University Hospital, Stockholm, Sweden
  4. Department of Neurology, Indiana University School of Medicine, Indianapolis, IN, USA
  5. Department of Biostatistics, Brown University, Providence, RI, USA
  6. Department of Radiology, University of Michigan, Ann Arbor, MI, USA
  7. Alzheimer’s Therapeutic Research Institute, University of Southern California, San Diego, CA, USA
  8. Department of Public Health Sciences, University of California Davis, Davis, CA, USA
  9. Department of Neurology, Washington University School of Medicine in St. Louis, St. Louis, MO, USA
  10. Laboratory of Neuro Imaging, USC Stevens Neuroimaging and Informatics Institute, Keck School of Medicine of USC, Los Angeles, CA, USA
  11. Banner Sun Health Research Institute, Sun City, AZ, USA
  12. Department of Neurology, Mayo Clinic, Jacksonville, FL, USA
  13. Wien Center for Alzheimer’s Disease and Memory Disorders, Mount Sinai Medical Center, Miami Beach, FL, USA
  14. Department of Psychiatry and Behavioral Sciences, Mesulam Center for Cognitive Neurology and Alzheimer’s Disease, Feinberg School of Medicine, Northwestern University, Chicago, IL, USA
  15. Taub Institute and Department of Neurology, Columbia University Irving Medical Center, NewYork, NY, USA
  16. Department of Neurology, Emory University School of Medicine, Atlanta, GA, USA
  17. Department of Radiology, Mayo Clinic, Rochester, MN, USA
  18. Nantz National Alzheimer Center, Houston Methodist and Weill Cornell Medicine, Houston, TX, USA
  19. Department of Neurology, David Geffen School of Medicine at UCLA, Los Angeles, CA, USA
  20. Department of Psychiatry and Behavioral Sciences, Johns Hopkins University School of Medicine, Baltimore, MD, USA
  21. Department of Psychiatry, Alpert Medical School, Brown University, Providence, RI, USA
  22. Healthy Aging & Alzheimer’s Research Care Center, Department of Neurology, University of Chicago, Chicago, IL, USA
  23. Department of Neurology & Neurological Sciences, Stanford University, Palo Alto, CA, USA
  24. Department of Neurology, Georgetown University, Washington, DC, USA
  25. Department of Neurology, University of California, Davis, Sacramento, CA, USA
  26. Department of Neurology, Perelman School of Medicine, University of Pennsylvania, Philadelphia, PA, USA
  27. Department of Medical and Molecular Genetics, Indiana University School of Medicine, Indianapolis, IN, USA
  28. Department of Neurology, Massachusetts General Hospital and Harvard Medical School, Boston, MA, USA
  29. Department of Clinical Sciences Malmö, Faculty of Medicine, SciLifeLab, Lund University, Lund, Sweden
  30. Department of Epidemiology and Biostatistics, University of California San Francisco, San Francisco, CA, USA
  31. Medical & Scientific Relations Division, Alzheimer’s Association, Chicago, IL, USA
  32. Department of Radiology and Biomedical Imaging, University of California San Francisco, San Francisco, CA, USA
Journal: Brain communications, volume 8, issue 3, article fcag176
Dates: received 10 September 2025; accepted 14 May 2026; published online 18 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1093/braincomms/fcag176 · PMID 42255923 · PMCID PMC13234610 · OpenAlex W7161691325
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: PET / SPECT (modality), human (organism), Alzheimer's / dementia (population), clinical / translational (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, fMRI & imaging
Keywords: neuroimaging biomarkers, disease heterogeneity, atypical Alzheimer's disease, data-driven clustering, voxel-wise modelling
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: U.S. Department of Health & Human Services | NIH | National Institute on Aging (U.S. National Institute on Aging) (U01-AG6057195, R56-AG057195); Alzheimer's Association (AARG-22-926940, LDRFP-21-818464, LEADS GENETICS-19-639372)
Citations: not cited yet (Europe PMC); 69 references in the paper

Abstract

The growing availability of large-scale biomarker datasets has allowed data-driven methods to characterize Alzheimer’s disease biological heterogeneity. However, most prior studies have focused on cohorts of late-onset amnestic cases, leaving early-onset Alzheimer’s disease underexplored. We aimed to characterize tau-PET-based subtypes through a robust data-driven approach in the Longitudinal Early-Onset Alzheimer’s Disease Study.

Baseline [18F]Flortaucipir PET scans from 365 amyloid-PET-positive participants with sporadic early-onset Alzheimer’s disease were quantified in the left and right medial temporal, lateral temporal, occipital, parietal, and frontal cortices. Tau PET values were z-scored against 85 amyloid-PET-negative cognitively normal age-matched participants and fitted into Subtype and Stage Inference (SuStaIn)—an unsupervised clustering algorithm that simultaneously models subtypes and progression from cross-sectional data. The derived subtypes were subsequently characterized by baseline and longitudinal clinical, cognitive, MRI, tau and amyloid PET features. We identified three tau-PET-based subtypes: on average, Subtype 1/Typical (n = 144, 40%) showed a predominant bilateral temporoparietal pattern typical of Alzheimer’s disease. Subtype 2/Left temporal (n = 111, 31%) showed predominant left temporal binding. Subtype 3/Posterior (n = 104, 29%) showed early and permeating occipitoparietal involvement. Subtypes did not differ in demographics or global amyloid burden, but were relatively more enriched for specific clinical presentations: S1/Typical for amnestic presentations, S2/Left Temporal for primary progressive aphasia, and S3/Posterior for posterior cortical atrophy. Baseline tau PET subtypes aligned with cortical atrophy patterns and domain-specific cognitive impairment. When follow-up tau PET scans were fitted to SuStaIn trained on baseline data, 85.6% (n = 172/201) of participants retained the same subtype classification, indicating subtype temporal stability, and progressed within subtypes by 0.56 ± 0.70 SuStaIn stage/year. Longitudinal voxel-wise linear mixed-effects modelling revealed tau accumulation patterns for each subtype in regions relatively spared at baseline: occipital lobe accumulation predominated in S1/Typical, bilateral frontal and right temporal in S2/Left Temporal, and bilateral frontotemporal lobes in S3/Posterior. All subtypes showed longitudinal increases in Clinical Dementia Rating-Sum of Boxes, but with slower worsening in S3/Posterior compared with the other subtypes. Our findings reveal robust subtypes in sporadic early-onset Alzheimer’s disease characterized by distinct spatiotemporal tau patterns that parallel differences in clinical presentations and trajectories of neurodegeneration. These subtypes extend beyond traditional clinical syndromes and support a more nuanced framework for individualized prognosis and care. Incorporating tau PET subtyping into clinical trial design could enable more targeted therapeutic approaches for this younger population.

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

Repositories

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

rablabservice/EOAD_tauPET_subtype

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 4ce80c823ecdbf8f48e1e2a333f3e1eb6cf9975f, 10 February 2026
Languages: Jupyter (17), Python (14), MATLAB (13), R (3)
Size: 130 files, 47 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, environment (requirements.txt, setup.py), tests, 20 notebooks
Not found: CITATION.cff, continuous integration, documentation
Tools: NumPy (27 files), pandas (24 files), Matplotlib (20 files), SciPy (16 files), scikit-learn (15 files), seaborn (14 files), Plotly (6 files), SPM (6 files), statsmodels (6 files), Pillow (5 files), Image Processing Toolbox (4 files), car (2 files), easystats (2 files), ggplot2 (2 files), Tools for NIfTI and ANALYZE image (MATLAB) (2 files), tidyverse (2 files), emmeans (1 file), ggpubr (1 file), lme4 (1 file), lmerTest (1 file), patchwork (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
49 files

Zenodo 837898

License: other-open
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: pandas (4 files), NumPy (3 files), SciPy (2 files)
Availability: 1 check, the latest on 28 September 2026: the link answers (HTTP 200)
  • 28 September 2026: the link answers (HTTP 200)
6 files
At the source:

tompollard/tableone

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: a9e229df81097016a6d9b0abbd8a55235b7c5c9d, 10 February 2026
Languages: Python (15), Jupyter (1)
Size: 45 files, 16 scripts
Software Heritage: archived
Found in: the Zenodo archive record
Holds: README, license file, CITATION.cff, environment (pyproject.toml, requirements.txt, docs/requirements.txt), tests, continuous integration, documentation, 1 notebook
Tools: pandas (10 files), NumPy (9 files), SciPy (4 files), Matplotlib (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
18 files

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

Tracing map

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

What the map holds:

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

LEADS data can be requested via https://leads-study.medicine.iu.edu/researchers/leads-data-request-application/. Code for this work is publicly available at https://github.com/rablabservice/EOAD_tauPET_subtype. Full three-dimensional voxelwise NIfTI images of the brain visualizations shown in main figures are publicly available on NeuroVault https://identifiers.org/neurovault.collection:23001.

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

Recorded: type, language, journal, volume, issue, pages, dates, 49 authors, 5 keywords, 2 funders, 68 references.

Cite

This paper

Lin, M., Chiotis, K., Maiti, P., Zhang, J., Blazhenets, G., Rocha, S., Shankar, R., Amuiri, A., Hammers, D. B., Eloyan, A., Kirby, K., Koeppe, R. A., Aisen, P., Beckett, L., Kukull, W. A., Toga, A. W., Atri, A., Clark, D. G., Day, G. S., . . . La Joie, R. (2026). Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease. Brain communications, 8(3), fcag176. https://doi.org/10.1093/braincomms/fcag176

BibTeX

@article{lin2026tau,
author = {Lin, Marlene and Chiotis, Konstantinos and Maiti, Piyush and Zhang, Jiaxiuxiu and Blazhenets, Ganna and Rocha, Salma and Shankar, Ranjani and Amuiri, Alinda and Hammers, Dustin B and Eloyan, Ani and Kirby, Kala and Koeppe, Robert A and Aisen, Paul and Beckett, Laurel and Kukull, Walter A and Toga, Arthur W and Atri, Alireza and Clark, David G and Day, Gregory S and Duara, Ranjan and Graff-Radford, Neill R and Grant, Ian and Honig, Lawrence S and Johnson, Erik C B and Jones, David T and Masdeu, Joseph C and Mendez, Mario F and Musiek, Erik and Onyike, Chiadi U and Riddle, Meghan and Rogalski, Emily and Salloway, Stephen and Sha, Sharon J and Turner, Raymond Scott and Wingo, Thomas S and Wolk, David A and Womack, Kyle B and Nudelman, Kelly Nicole Holohan and Touroutoglou, Alexandra and Jack, Clifford R and Vemuri, Prashanthi and Vogel, Jacob W and Dufault, Suzanne M and Hoffmann, Thomas J and Carrillo, Maria C and Dickerson, Bradford C and Apostolova, Liana G and Rabinovici, Gil D and La Joie, Renaud},
title = {{Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease}},
journal = {Brain communications},
year = {2026},
month = may,
volume = {8},
number = {3},
pages = {fcag176},
publisher = {Oxford University Press},
issn = {2632-1297},
doi = {10.1093/braincomms/fcag176},
url = {https://doi.org/10.1093/braincomms/fcag176},
pmid = {42255923},
pmcid = {PMC13234610}
}

RIS

TY - JOUR
AU - Lin, Marlene
AU - Chiotis, Konstantinos
AU - Maiti, Piyush
AU - Zhang, Jiaxiuxiu
AU - Blazhenets, Ganna
AU - Rocha, Salma
AU - Shankar, Ranjani
AU - Amuiri, Alinda
AU - Hammers, Dustin B
AU - Eloyan, Ani
AU - Kirby, Kala
AU - Koeppe, Robert A
AU - Aisen, Paul
AU - Beckett, Laurel
AU - Kukull, Walter A
AU - Toga, Arthur W
AU - Atri, Alireza
AU - Clark, David G
AU - Day, Gregory S
AU - Duara, Ranjan
AU - Graff-Radford, Neill R
AU - Grant, Ian
AU - Honig, Lawrence S
AU - Johnson, Erik C B
AU - Jones, David T
AU - Masdeu, Joseph C
AU - Mendez, Mario F
AU - Musiek, Erik
AU - Onyike, Chiadi U
AU - Riddle, Meghan
AU - Rogalski, Emily
AU - Salloway, Stephen
AU - Sha, Sharon J
AU - Turner, Raymond Scott
AU - Wingo, Thomas S
AU - Wolk, David A
AU - Womack, Kyle B
AU - Nudelman, Kelly Nicole Holohan
AU - Touroutoglou, Alexandra
AU - Jack, Clifford R
AU - Vemuri, Prashanthi
AU - Vogel, Jacob W
AU - Dufault, Suzanne M
AU - Hoffmann, Thomas J
AU - Carrillo, Maria C
AU - Dickerson, Bradford C
AU - Apostolova, Liana G
AU - Rabinovici, Gil D
AU - La Joie, Renaud
TI - Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease
T2 - Brain communications
J2 - Brain Commun
PY - 2026
DA - 2026/05/18
VL - 8
IS - 3
SP - fcag176
SN - 2632-1297
PB - Oxford University Press
DO - 10.1093/braincomms/fcag176
UR - https://doi.org/10.1093/braincomms/fcag176
LA - en
ER -

CSL-JSON

{
"id": "10.1093/braincomms/fcag176",
"type": "article-journal",
"title": "Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease",
"container-title": "Brain communications",
"author": [
{
"family": "Lin",
"given": "Marlene"
},
{
"family": "Chiotis",
"given": "Konstantinos"
},
{
"family": "Maiti",
"given": "Piyush"
},
{
"family": "Zhang",
"given": "Jiaxiuxiu"
},
{
"family": "Blazhenets",
"given": "Ganna"
},
{
"family": "Rocha",
"given": "Salma"
},
{
"family": "Shankar",
"given": "Ranjani"
},
{
"family": "Amuiri",
"given": "Alinda"
},
{
"family": "Hammers",
"given": "Dustin B"
},
{
"family": "Eloyan",
"given": "Ani"
},
{
"family": "Kirby",
"given": "Kala"
},
{
"family": "Koeppe",
"given": "Robert A"
},
{
"family": "Aisen",
"given": "Paul"
},
{
"family": "Beckett",
"given": "Laurel"
},
{
"family": "Kukull",
"given": "Walter A"
},
{
"family": "Toga",
"given": "Arthur W"
},
{
"family": "Atri",
"given": "Alireza"
},
{
"family": "Clark",
"given": "David G"
},
{
"family": "Day",
"given": "Gregory S"
},
{
"family": "Duara",
"given": "Ranjan"
},
{
"family": "Graff-Radford",
"given": "Neill R"
},
{
"family": "Grant",
"given": "Ian"
},
{
"family": "Honig",
"given": "Lawrence S"
},
{
"family": "Johnson",
"given": "Erik C B"
},
{
"family": "Jones",
"given": "David T"
},
{
"family": "Masdeu",
"given": "Joseph C"
},
{
"family": "Mendez",
"given": "Mario F"
},
{
"family": "Musiek",
"given": "Erik"
},
{
"family": "Onyike",
"given": "Chiadi U"
},
{
"family": "Riddle",
"given": "Meghan"
},
{
"family": "Rogalski",
"given": "Emily"
},
{
"family": "Salloway",
"given": "Stephen"
},
{
"family": "Sha",
"given": "Sharon J"
},
{
"family": "Turner",
"given": "Raymond Scott"
},
{
"family": "Wingo",
"given": "Thomas S"
},
{
"family": "Wolk",
"given": "David A"
},
{
"family": "Womack",
"given": "Kyle B"
},
{
"family": "Nudelman",
"given": "Kelly Nicole Holohan"
},
{
"family": "Touroutoglou",
"given": "Alexandra"
},
{
"family": "Jack",
"given": "Clifford R"
},
{
"family": "Vemuri",
"given": "Prashanthi"
},
{
"family": "Vogel",
"given": "Jacob W"
},
{
"family": "Dufault",
"given": "Suzanne M"
},
{
"family": "Hoffmann",
"given": "Thomas J"
},
{
"family": "Carrillo",
"given": "Maria C"
},
{
"family": "Dickerson",
"given": "Bradford C"
},
{
"family": "Apostolova",
"given": "Liana G"
},
{
"family": "Rabinovici",
"given": "Gil D"
},
{
"family": "La Joie",
"given": "Renaud"
}
],
"container-title-short": "Brain Commun",
"volume": "8",
"issue": "3",
"page": "fcag176",
"DOI": "10.1093/braincomms/fcag176",
"PMID": "42255923",
"PMCID": "PMC13234610",
"ISSN": "2632-1297",
"publisher": "Oxford University Press",
"URL": "https://doi.org/10.1093/braincomms/fcag176",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
18
]
]
}
}

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.1212/wnl.0000000000218519 [code]
EOAD-Signature Atrophy Predicts Dementia in Early-Onset MCI due to Alzheimer Disease: An MRI-Based Prognostic Biomarker.
Journal: Neurology
In common: ggplot2, tidyverse, Alzheimer's / dementia, clinical / translational, 4 references, 8 authors
[2] doi:10.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: easystats, car, emmeans, 13 other tools
[3] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: car, lmerTest, Plotly, 12 other tools
[4] 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: car, lmerTest, SPM, 12 other tools
[5] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: easystats, car, emmeans, 11 other tools
[6] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: SPM, Plotly, lme4, 12 other tools
[7] doi:10.64898/2026.05.06.26352540 [code]
Generating synthetic tau-PET scans in Alzheimer’s disease from MRI, blood biomarkers and demographics with deep learning
Journal: medRxiv (preprint)
In common: seaborn, scikit-learn, pandas, 3 other tools, PET / SPECT, Alzheimer's / dementia, clinical / translational, 3 references, author Renaud La Joie
[8] doi:10.1038/s41597-026-07350-9 [code]
An open multi-center MEG-EEG dataset for studying conscious visual perception.
Journal: Scientific data
In common: easystats, car, emmeans, 11 other tools
[9] doi:10.1038/s41597-026-07377-y [code]
An open-access multi-site fMRI dataset for investigating conscious visual perception.
Journal: Scientific data
In common: easystats, car, emmeans, 11 other tools
[10] doi:10.1002/hbm.70605 [code]
BrainEnrich: Revealing Biological Insights for Imaging-Derived Phenotypes Through Transcriptomic Enrichment.
Journal: Human brain mapping
In common: easystats, emmeans, lmerTest, 11 other tools

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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