OSCR

Dissecting heterogeneous brain development and aging using voxelwise normative models

Code ↔ Paper

12 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 12 matches
  1. [1] § Methods › Datasets, preprocessing and brain measures extraction ↔ voxelwise_models_estimation/02_prepare_voxelwise_normdata.py, lines 1 to 60 · score 0.79 · HCP Aging, HCP Dev, CMI HBN, IXI, PING, PNC · Report a mismatch
  2. [2] § Methods › Voxelwise normative models › Application to spinocerebellar ataxia type 1 and 3 ↔ voxelwise_models_application/06_KernelRidgeRegression_SCA_patients.py, lines 1 to 40 · score 0.76 · kernel ridge regression, brain voxelwise, ataxia, raw, prediction, spinocerebellar · Report a mismatch
  3. [3] § Methods › Datasets, preprocessing and brain measures extraction ↔ voxelwise_models_estimation/01_preprocess_T1_ants_fsl.py, lines 47 to 101 · score 0.76 · nonlinear transformations, combined affine, Jacobian determinants, geometric, preprocessing, log · Report a mismatch
  4. [4] § Methods › Voxelwise normative models › Application to spinocerebellar ataxia type 1 and 3 ↔ voxelwise_models_application/06_KernelRidgeRegression_SCA_patients.py, lines 1 to 40 · score 0.67 · SCA patients, spinocerebellar ataxia, Mexico, SCA1, SCA3, Nijmegen · Report a mismatch
  5. [5] § Methods › Voxelwise normative models › Model estimation ↔ voxelwise_models_application/05_preterm_subjects_comparison.py, lines 1 to 40 · score 0.66 · brain burden, controls born, Mann, TFCE, extreme, normative models · Report a mismatch
  6. [6] § Methods › Datasets, preprocessing and brain measures extraction ↔ voxelwise_models_estimation/02_prepare_voxelwise_normdata.py, lines 1 to 60 · score 0.64 · CMI HBN, MNI152NLin2009cSym, brain extraction, preprocessed, ANTs, ABCD · Report a mismatch
  7. [7] § Methods › Datasets, preprocessing and brain measures extraction ↔ voxelwise_models_estimation/01_preprocess_T1_ants_fsl.py, lines 123 to 148 · score 0.64 · segmented grey, FSL, FAST, transforms, anat, ANTs · Report a mismatch
  8. [8] § Methods › Voxelwise normative models › Application to spinocerebellar ataxia type 1 and 3 ↔ voxelwise_models_application/06_KernelRidgeRegression_SCA_patients.py, lines 311 to 397 · score 0.63 · age residualized, predictive pipeline, Mexico, Nijmegen, regressor, raw · Report a mismatch
  9. [9] § Results › Preterm birth gives rise to widespread and persistent patterns of deviation ↔ voxelwise_models_application/05_preterm_subjects_comparison.py, lines 1 to 40 · score 0.60 · brain burden, controls born, born preterm, extreme, normative model, scores · Report a mismatch
  10. [10] § Methods › Datasets, preprocessing and brain measures extraction ↔ voxelwise_models_estimation/02_prepare_voxelwise_normdata.py, lines 189 to 272 · score 0.57 · FSL, brain extraction, failures, segmentation, anat, ANTs · Report a mismatch
  11. [11] § Results › Preterm birth gives rise to widespread and persistent patterns of deviation ↔ voxelwise_models_estimation/04_evaluate_voxelwise_models.py, lines 212 to 219 · score 0.53 · internal capsule, cerebellar white matter, voxels · Report a mismatch
  12. [12] § Results › SCA individual-level deviation maps show distinct heterogeneity ↔ voxelwise_models_application/06_KernelRidgeRegression_SCA_patients.py, lines 199 to 259 · score 0.51 · kernel ridge regression, Mexico, SCA1, SCA3, Nijmegen, raw · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 542 lines · 21 KB · no license · 4 matches

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Mon Aug 19 16:35:32 2024
  5. @author: Alice Chavanne
  6. This script conducts a kernel ridge regression analysis evaluated out-of-sample in patients with spinocerebellar ataxia type 1 and 3,
  7. from two independent datasets. The predictive performance of raw, whole-brain voxelwise measures (log jacobian, GMV & WMV combined ) and normative measures
  8. (Z-scores) are explored separately using the options set.
  9. """
  10. import numpy as np
  11. from pcntoolkit.dataio.fileio import save as ptksave
  12. import os
  13. import pandas as pd
  14. import pickle
  15. from sklearn.preprocessing import StandardScaler, FunctionTransformer
  16. from sklearn.model_selection import LeaveOneOut, GridSearchCV
  17. from sklearn.metrics import r2_score, root_mean_squared_error
  18. from sklearn.pipeline import Pipeline
  19. from sklearn.kernel_ridge import KernelRidge
  20. from sklearn.base import BaseEstimator, TransformerMixin
  21. import matplotlib.pyplot as plt
  22. from scipy.stats import pearsonr
  23. import nibabel as nib
  24. from nibabel import processing
  25. import nilearn as nil
  26. from nilearn import image
  27. from pcntoolkit import dataio
  28. import joblib
  29. from joblib import Parallel, delayed
  30. data_dir = '/path/to/data/dir/with/raw/ants/outputs/'
  31. out_dir = os.path.join(data_dir,'clinical_predictions')
  32. extension = ''#optional extra suffix added to output folder/filenames
  33. model_dir = '/path/to/dir/where/normative/models/live'
  34. datasets = ['SCA1_Nijmegen', 'SCA3_Mexico'] #some indices are hardcoded based on this order so have be careful
  35. #%% Choose options for regression
  36. target = {
  37. "BL_SARA" : 0,
  38. "Y1_SARA" : 0,
  39. "Slope_BL_Y1_SARA" : 1,
  40. }
  41. train_and_test = {
  42. "test_Nijmegen_CV":1,
  43. "test_Nijmegen_and_Mexico_CV" : 0,
  44. }
  45. featureset = {
  46. "BL_wholebrain_log_jacs_Z" : 1,#can increase RAM if 1 here
  47. "BL_wholebrain_log_jacs_raw" : 0,
  48. "BL_wholebrain_mod_gmv_and_wmv_Z": 0,
  49. "BL_wholebrain_mod_gmv_and_wmv_raw": 0,
  50. }
  51. options = {
  52. "gridsearch" : 0,
  53. }
  54. #account for features with non-standardized values, those that need smoothing or resample
  55. if featureset["BL_wholebrain_mod_gmv_and_wmv_raw"] :
  56. options["Xstandard"] = 1
  57. resample = True
  58. smooth = True
  59. elif featureset["BL_wholebrain_log_jacs_raw"] :
  60. options["Xstandard"] = 1
  61. resample = True
  62. smooth = False
  63. else :
  64. options["Xstandard"] = 0
  65. resample = False
  66. smooth = False
  67. seed = 42
  68. filename = "_".join([key for key, value in target.items() if value] +
  69. ["KRR_with"]+
  70. [key for key, value in featureset.items() if value] +
  71. [key for key, value in train_and_test.items() if value] +
  72. [key for key, value in options.items() if value]+
  73. [extension])
  74. final_dir = os.path.join(out_dir,filename)
  75. os.makedirs(final_dir, exist_ok=True)
  76. os.chdir(final_dir)
  77. #%% Nifti specifics
  78. if (featureset["BL_wholebrain_log_jacs_raw"] or featureset["BL_wholebrain_log_jacs_Z"]):
  79. mask_nii = os.path.join(model_dir,'tpl-MNI152NLin2009cSym/tpl-MNI152NLin2009cSym_res-2_T1w_mask_BrainExtractionBrain.nii.gz')
  80. ex_nii = mask_nii
  81. proc_dir = os.path.join(model_dir,'models','BLR_aff_nonlin_log_jacs_wholebrain_2mm_14')
  82. if (featureset["BL_wholebrain_mod_gmv_and_wmv_raw"] or featureset["BL_wholebrain_mod_gmv_and_wmv_Z"]): # combine the two masks
  83. mask_nii_gm = os.path.join(model_dir,'tpl-MNI152NLin2009cSym/tpl-MNI152NLin2009cSym_res-2_label-GM_mask_probseg0.5.nii.gz')
  84. mask_nii_wm = os.path.join(model_dir,'tpl-MNI152NLin2009cSym/tpl-MNI152NLin2009cSym_res-2_label-WM_mask_probseg0.5.nii.gz')
  85. mask_nii_combined = os.path.join(model_dir,'tpl-MNI152NLin2009cSym/tpl-MNI152NLin2009cSym_res-2_combined_label-GM_WM_mask.nii.gz')
  86. proc_dir_gm= os.path.join(model_dir,'models','BLR_aff_nonlin_mod_gmv_labelmask_2mm_smoothed_fwhm6_07')
  87. proc_dir_wm= os.path.join(model_dir,'models','BLR_aff_nonlin_mod_wmv_labelmask_2mm_smoothed_fwhm6_07')
  88. proc_dir = proc_dir_gm #just to grab the demographics - gm and wm are identical on that front
  89. #just for plotting back onto the combined mask
  90. ex_nii = mask_nii_combined
  91. mask_nii = mask_nii_combined
  92. #details for brain data extraction from niftis
  93. vox_size = [2, 2, 2] #2mm
  94. fwhm = 6 #if smooth is true
  95. vox_batch_size = 150
  96. #%% Grab X and y data
  97. #utils
  98. def load_file(subpath, mask_nii, resample, smooth, vox_size):
  99. img = nib.load(subpath)
  100. if resample :
  101. img = nib.processing.resample_to_output(img, vox_size)
  102. if smooth:
  103. img = nil.image.smooth_img(img,fwhm)
  104. data = img.get_fdata()
  105. data = dataio.fileio.vol2vec(data, nib.load(mask_nii).get_fdata())
  106. return data
  107. # prepare for gmv and wmv, load GM and WM images, filter them by their respective masks, and combine them into a single vector consistent with the combined mask.
  108. def load_files_and_combine(sub_path_gm, sub_path_wm, mask_nii_gm, mask_nii_wm, mask_nii_combined,resample, smooth, vox_size) :
  109. gm_img = nib.load(sub_path_gm)
  110. wm_img = nib.load(sub_path_wm)
  111. if resample:
  112. gm_img = nib.processing.resample_to_output(gm_img, vox_size)
  113. wm_img = nib.processing.resample_to_output(wm_img, vox_size)
  114. if smooth:
  115. gm_img = image.smooth_img(gm_img, fwhm)
  116. wm_img = image.smooth_img(wm_img, fwhm)
  117. gm_data = gm_img.get_fdata()
  118. wm_data = wm_img.get_fdata()
  119. gm_mask = nib.load(mask_nii_gm).get_fdata().astype(bool)
  120. wm_mask = nib.load(mask_nii_wm).get_fdata().astype(bool)
  121. combined_mask = nib.load(mask_nii_combined).get_fdata().astype(bool)
  122. combined_data = np.zeros_like(gm_data).astype(float)
  123. combined_data[gm_mask] = gm_data[gm_mask]
  124. combined_data[wm_mask] = wm_data[wm_mask]
  125. data = combined_data[combined_mask]
  126. return data
  127. # now grab data from all datasets
  128. df_sca = []
  129. y_sca = []
  130. X_sca = []
  131. for dataset in datasets:
  132. #grab subjects and clinical scores
  133. df_clin = pd.read_csv(os.path.join(proc_dir, 'transfer_' + dataset, 'dem_patients.csv'))
  134. #grab y
  135. if target["BL_SARA"]:
  136. y_clin = df_clin['SARA_score']
  137. if target["Y1_SARA"]:
  138. if 'Y1_SARA' in df_clin.columns:
  139. df_clin.dropna(subset=['Y1_SARA'], inplace=True)
  140. df_clin= df_clin.reset_index()
  141. y_clin = df_clin.get('Y1_SARA', pd.Series([], name='Y1_SARA'))
  142. else :
  143. df_clin = pd.DataFrame()
  144. if target["Slope_BL_Y1_SARA"]:
  145. if 'Slope_BL_Y1_SARA' in df_clin.columns:
  146. df_clin.dropna(subset=['Slope_BL_Y1_SARA'], inplace=True)
  147. df_clin= df_clin.reset_index()
  148. y_clin = df_clin.get('Slope_BL_Y1_SARA', pd.Series([], name='Slope_BL_Y1_SARA'))
  149. else :
  150. df_clin = pd.DataFrame()
  151. sub_list = df_clin['sub_id'] if 'sub_id' in df_clin.columns else pd.Series([], dtype=str)
  152. # ensure this doesn't break even for datasets with missing timepoints
  153. if len(sub_list) == 0:
  154. df_sca.append(df_clin)
  155. y_sca.append(pd.Series([], name='y_empty'))
  156. X_sca.append(pd.DataFrame())
  157. continue
  158. # grab X
  159. if (featureset["BL_wholebrain_log_jacs_raw"]) :
  160. file_name = 'T1_BrainNorm_Jacobian.nii.gz'
  161. if dataset == 'SCA1_Nijmegen' :
  162. f_pattern = os.path.join(data_dir, 'subjects_data','{}','ses-01','anat','T1_MNI152NLin2009cSym', file_name)
  163. if dataset == 'SCA3_Mexico' :
  164. f_pattern =os.path.join(data_dir,'additional_SCA3_data', 'T1','{}','T1_MNI152NLin2009cSym', file_name)
  165. filepaths_clin = [f_pattern.format(sub) for sub in sorted(sub_list.tolist())]
  166. resp_data_clin = Parallel(n_jobs=-1,verbose =5)(delayed(load_file)(sub_path, mask_nii,resample, smooth, vox_size)for sub_path in filepaths_clin)
  167. resp_data_clin = np.stack(resp_data_clin, axis=0)
  168. if (featureset["BL_wholebrain_mod_gmv_and_wmv_raw"]) :
  169. file_name_gm = 'T1_BrainNorm_GMV.nii.gz'
  170. file_name_wm = 'T1_BrainNorm_WMV.nii.gz'
  171. if dataset == 'SCA1_Nijmegen' :
  172. gm_pattern = os.path.join(data_dir, 'subjects_data','{}','ses-01','anat','T1_MNI152NLin2009cSym', file_name_gm)
  173. wm_pattern = os.path.join(data_dir, 'subjects_data','{}','ses-01','anat','T1_MNI152NLin2009cSym', file_name_wm)
  174. if dataset == 'SCA3_Mexico' :
  175. gm_pattern =os.path.join(data_dir,'additional_SCA3_data', 'T1','{}','T1_MNI152NLin2009cSym', file_name_gm)
  176. wm_pattern =os.path.join(data_dir,'additional_SCA3_data', 'T1','{}','T1_MNI152NLin2009cSym', file_name_wm)
  177. resp_data_clin = Parallel(n_jobs=-1,verbose =5)(delayed(load_files_and_combine)(
  178. gm_pattern.format(sub), wm_pattern.format(sub),
  179. mask_nii_gm, mask_nii_wm, mask_nii_combined,
  180. resample, smooth, vox_size)
  181. for sub in sorted(sub_list.tolist()))
  182. resp_data_clin = np.stack(resp_data_clin, axis=0)
  183. if (featureset["BL_wholebrain_log_jacs_Z"]) :
  184. mask = dataio.fileio.load(mask_nii, mask=mask_nii, vol=False).T
  185. n_voxels = len(mask)
  186. n_batches = n_voxels // vox_batch_size + int(n_voxels % vox_batch_size != 0)
  187. voxel_indices = list(range(n_voxels))
  188. Z_all = []
  189. for b in range(n_batches)[:]:
  190. Z_batch = []
  191. batch = f'batch_{b}'
  192. #grab Zs from normdata
  193. with open(os.path.join(proc_dir,'transfer_' + dataset, batch, 'norm_data_clin.pkl'), 'rb') as f:
  194. norm_data= pickle.load(f)
  195. norm_data.load_zscores(save_dir = os.path.join(proc_dir,'transfer_' + dataset, batch,'results'))
  196. for sub in sub_list.tolist() :
  197. idx = np.where(norm_data.subject_ids.values == sub)[0]
  198. sub_batch = norm_data.isel(observations=idx)
  199. Z_sub = pd.DataFrame(sub_batch.Z.values, columns=sub_batch.Z.response_vars.values)
  200. Z_batch.append(Z_sub)
  201. Z_batch = pd.concat(Z_batch)
  202. Z_all.append(Z_batch)
  203. if len(Z_all)>0 :
  204. resp_data_clin = pd.concat(Z_all, axis = 1)
  205. else :
  206. resp_data_clin = Z_all
  207. if (featureset["BL_wholebrain_mod_gmv_and_wmv_Z"]) :
  208. #grab GM Zs from normdata
  209. mask = dataio.fileio.load(mask_nii_gm, mask=mask_nii_gm, vol=False).T
  210. n_voxels = len(mask)
  211. n_batches = n_voxels // vox_batch_size + int(n_voxels % vox_batch_size != 0)
  212. Z_all_gm = []
  213. for b in range(n_batches)[:]:
  214. Z_batch = []
  215. batch = f'batch_{b}'
  216. with open(os.path.join(proc_dir_gm,'transfer_' + dataset, batch, 'norm_data_clin.pkl'), 'rb') as f:
  217. norm_data= pickle.load(f)
  218. norm_data.load_zscores(save_dir = os.path.join(proc_dir_gm,'transfer_' + dataset, batch,'results'))
  219. for sub in sub_list.tolist() :
  220. idx = np.where(norm_data.subject_ids.values == sub)[0]
  221. sub_batch = norm_data.isel(observations=idx)
  222. Z_sub = pd.DataFrame(sub_batch.Z.values, columns=sub_batch.Z.response_vars.values)
  223. Z_batch.append(Z_sub)
  224. Z_batch = pd.concat(Z_batch)
  225. Z_all_gm.append(Z_batch)
  226. Z_all_gm = np.array(pd.concat(Z_all_gm, axis=1))
  227. #grab GM Zs from normdata
  228. mask = dataio.fileio.load(mask_nii_wm, mask=mask_nii_wm, vol=False).T
  229. n_voxels = len(mask)
  230. n_batches = n_voxels // vox_batch_size + int(n_voxels % vox_batch_size != 0)
  231. Z_all_wm = []
  232. for b in range(n_batches)[:]:
  233. Z_batch = []
  234. batch = f'batch_{b}'
  235. with open(os.path.join(proc_dir_wm,'transfer_' + dataset, batch, 'norm_data_clin.pkl'), 'rb') as f:
  236. norm_data= pickle.load(f)
  237. norm_data.load_zscores(save_dir = os.path.join(proc_dir_wm,'transfer_' + dataset, batch,'results'))
  238. for sub in sub_list.tolist() :
  239. idx = np.where(norm_data.subject_ids.values == sub)[0]
  240. sub_batch = norm_data.isel(observations=idx)
  241. Z_sub = pd.DataFrame(sub_batch.Z.values, columns=sub_batch.Z.response_vars.values)
  242. Z_batch.append(Z_sub)
  243. Z_batch = pd.concat(Z_batch)
  244. Z_all_wm.append(Z_batch)
  245. Z_all_wm = np.array(pd.concat(Z_all_wm, axis = 1))
  246. #combine GM and WM Zs with mask handling
  247. mask_combined = nib.load(mask_nii_combined)
  248. mask_combined = mask_combined.get_fdata().astype(bool)
  249. mask_gm = nib.load(mask_nii_gm)
  250. mask_gm = mask_gm.get_fdata().astype(bool)
  251. mask_wm = nib.load(mask_nii_wm)
  252. mask_wm = mask_wm.get_fdata().astype(bool)
  253. Z_all_combined = []
  254. for s in range(len(sub_list.tolist())):
  255. #re-project both Z vectors into nifti space
  256. Z_gm = Z_all_gm[s,:]
  257. Z_wm = Z_all_wm[s,:]
  258. #combine them
  259. Z_combined_vol = np.zeros_like(mask_gm).astype(float)
  260. Z_combined_vol[mask_gm] = Z_gm
  261. Z_combined_vol[mask_wm] = Z_wm
  262. #re-vectorize using combined mask
  263. Z_combined = Z_combined_vol[mask_combined]
  264. Z_all_combined.append(Z_combined)
  265. resp_data_clin = Z_all_combined
  266. #if we age-residualize, append the age column to what will be X, the transformer will take care of it
  267. if (featureset["BL_wholebrain_log_jacs_raw"] or featureset["BL_wholebrain_mod_gmv_and_wmv_raw"]):
  268. resp_data_clin = np.concatenate((resp_data_clin, df_clin["age"].to_numpy().reshape(-1, 1)), 1)
  269. df_sca.append(df_clin)
  270. y_sca.append(y_clin)
  271. X_sca.append(pd.DataFrame(resp_data_clin))
  272. #Now, only get those necessary for the prediction
  273. if train_and_test["test_Nijmegen_CV"]:
  274. X_all = X_sca[0]
  275. X_all = pd.DataFrame(X_all)
  276. y_all = y_sca[0]
  277. df_test = df_sca[0] #we take the df for the end residuals graph
  278. if train_and_test["test_Nijmegen_and_Mexico_CV"]:
  279. X_all = pd.concat([X_sca[0],X_sca[1]])
  280. X_all = pd.DataFrame(X_all)
  281. y_all = pd.concat([y_sca[0], y_sca[1]])
  282. y_all = y_all.reset_index(drop=True) #otehrwsie will throw error
  283. df_test = pd.concat([df_sca[0],df_sca[1]]) #we take the df for the end residuals graph
  284. df_test = df_test.reset_index(drop=True)
  285. #%%Optional - grab demographics
  286. for dataset in datasets:
  287. df_clin = pd.read_csv(os.path.join(proc_dir, 'transfer_' + dataset, 'dem_patients.csv'))
  288. df_ctrl = pd.read_csv(os.path.join(proc_dir, 'transfer_' + dataset, 'dem_controls.csv'))
  289. print(f"{dataset} SCA patients")
  290. print(f"subjects total {len(df_clin.index.tolist())}")
  291. print(f"unique scanners {len(np.unique(df_clin["site"]))}")
  292. print(f"female {100*df_clin["sex"].isin(['F']).sum()/len(df_clin["sex"]):.1f} percent")
  293. print(f"male {100*df_clin["sex"].isin(['M']).sum()/len(df_clin["sex"]):.1f} percent")
  294. print(f"age mean {np.mean(df_clin["age"]):.1f}")
  295. print(f"age sd {np.std(df_clin["age"]):.1f}")
  296. print(f"SARA mean {np.mean(df_clin["SARA_score"]):.1f}")
  297. print(f"SARA sd {np.std(df_clin["SARA_score"]):.1f}")
  298. print(f"{dataset} controls")
  299. print(f"subjects total {len(df_ctrl.index.tolist())}")
  300. print(f"unique scanners {len(np.unique(df_ctrl["site"]))}")
  301. print(f"female {100*df_ctrl["sex"].isin(['F']).sum()/len(df_ctrl["sex"]):.1f} percent")
  302. print(f"male {100*df_ctrl["sex"].isin(['M']).sum()/len(df_ctrl["sex"]):.1f} percent")
  303. print(f"age mean {np.mean(df_ctrl["age"]):.1f}")
  304. print(f"age sd {np.std(df_ctrl["age"]):.1f}")
  305. #%% Configure prediction pipeline
  306. scaler = StandardScaler(with_mean = True, with_std = True)
  307. splitter = LeaveOneOut()
  308. inner_splitter = LeaveOneOut()
  309. #transformer to safely manage age residualization within pipeline
  310. class AgeResidualizer(BaseEstimator, TransformerMixin):
  311. def __init__(self, center_age=True):
  312. self.center_age = center_age
  313. def fit(self, X, y=None):
  314. age = X.iloc[:, -1].to_numpy().reshape(-1, 1)# last column of X is assumed to be age
  315. voxels = X.iloc[:, :-1].to_numpy()
  316. if self.center_age:
  317. self.age_mean_ = age.mean()
  318. age = age - self.age_mean_
  319. else:
  320. self.age_mean_ = 0.0
  321. Z = np.hstack([np.ones((age.shape[0], 1)), age])
  322. self.B_ = np.linalg.solve(Z.T @ Z, Z.T @ voxels)
  323. return self
  324. def transform(self, X):
  325. age = X.iloc[:, -1].to_numpy().reshape(-1, 1)
  326. voxels = X.iloc[:, :-1].to_numpy()
  327. age = age - self.age_mean_
  328. Z = np.hstack([np.ones((age.shape[0], 1)), age])
  329. voxels_res = voxels - Z @ self.B_
  330. return voxels_res #important: here we return only voxel columns and not the age one
  331. #we have to account for bias in KRR by adding an intercept column
  332. def add_intercept(X):
  333. intercept = np.ones((X.shape[0], 1))
  334. return np.hstack((X, intercept))
  335. add_intercept_transformer = FunctionTransformer(add_intercept)
  336. clf = KernelRidge()
  337. if options["Xstandard"] :
  338. pipeline = Pipeline(steps =[
  339. ('resid', AgeResidualizer()), #this pipeline expects age as last column of X_train and X_test and will remove it after
  340. ('scaler', scaler),
  341. ('intercept', add_intercept_transformer),
  342. ('clf', clf)])
  343. elif not options["Xstandard"] :
  344. pipeline = Pipeline(steps =[
  345. ('intercept', add_intercept_transformer),
  346. ('clf', clf)])
  347. #%% Run prediction
  348. fitted_models = []
  349. ys_pred = []
  350. ys_test = []
  351. ys_train = []
  352. Xs_train_final = []
  353. Xs_test_final = []
  354. feature_coefs = []
  355. eval_scores = pd.DataFrame(columns = ['RMSE'])
  356. for train_idx, test_idx in splitter.split(X_all):
  357. X_train, X_test = X_all.iloc[train_idx], X_all.iloc[test_idx]
  358. y_train, y_test = y_all[train_idx], y_all[test_idx]
  359. ys_test.append(y_test)
  360. ys_train.append(y_train)
  361. #fit
  362. pipeline.fit(X_train, y_train)
  363. best_pipeline = pipeline
  364. krr_model = best_pipeline.named_steps['clf']
  365. #extract outputs
  366. X_train_final = best_pipeline[:-1].transform(X_train) #extract scaled and possibly selected features
  367. X_test_final = best_pipeline[:-1].transform(X_test)
  368. Xs_train_final.append(X_train_final)
  369. Xs_test_final.append(X_test_final)
  370. coefs = krr_model.dual_coef_
  371. coefs = np.dot(coefs, X_train_final) #this only works for linear kernel of krr
  372. feature_coefs.append(coefs)
  373. y_pred = best_pipeline.predict(X_test)
  374. fitted_models.append(best_pipeline)
  375. ys_pred.append(y_pred)
  376. rmse = root_mean_squared_error(y_true = y_test, y_pred = y_pred)
  377. eval_scores.loc[len(eval_scores)] = [rmse]
  378. #%% Save outputs
  379. #keep syntax straight
  380. y_test = np.concatenate(ys_test)
  381. y_pred = np.concatenate(ys_pred)
  382. X_train_final = np.concatenate(Xs_train_final)
  383. X_test_final = np.concatenate(Xs_test_final)
  384. eval_scores['r2_score'] = [r2_score( y_true = y_test, y_pred = y_pred)] + [np.nan] * (len(eval_scores)-1)
  385. correlation_coef, p_value =pearsonr(y_test, y_pred)
  386. eval_scores['Pearson r'] = [correlation_coef] + [np.nan] * (len(eval_scores)-1)
  387. eval_scores['Pearson p-value'] = ["{:.2e}".format(p_value)] + [np.nan] * (len(eval_scores)-1)
  388. eval_scores.to_csv(os.path.join(final_dir,"_".join(['eval_scores']+[key for key, value in featureset.items() if value] +
  389. [key for key, value in train_and_test.items() if value] +
  390. [key for key, value in options.items() if value] )+ extension +'.csv'),
  391. index = False, na_rep = '')
  392. print(eval_scores.iloc[0,:])
  393. with open(os.path.join(final_dir,'fitted_models.pkl'), 'wb') as file:
  394. pickle.dump(fitted_models, file)
  395. with open(os.path.join(final_dir,'ys_pred.pkl'), 'wb') as file:
  396. pickle.dump(y_pred, file)
  397. with open(os.path.join(final_dir,'ys_test.pkl'), 'wb') as file:
  398. pickle.dump(y_test, file)
  399. with open(os.path.join(final_dir,'Xs_train.pkl'), 'wb') as file:
  400. pickle.dump(X_train_final, file)
  401. with open(os.path.join(final_dir,'Xs_test.pkl'), 'wb') as file:
  402. pickle.dump(X_test_final, file)
  403. with open(os.path.join(final_dir,'feature_coefs.pkl'), 'wb') as file:
  404. pickle.dump(feature_coefs, file)
  405. #map selected voxelwise feature mean coefs across folds back onto a nifti
  406. feature_coefs = [array[:-1] for array in feature_coefs] #remove last coefficient due to the added intercept (column of ones)
  407. average_coefs = np.mean(feature_coefs, axis = 0)
  408. ptksave(average_coefs, os.path.join(final_dir,'Signed_feature_coefs.nii.gz'), example=ex_nii, mask=mask_nii)
  409. #Plot residuals to check systematic over- or under-fitting
  410. diff_y = y_pred - y_test
  411. fig = plt.figure(figsize=(8, 6))
  412. y_name = ''.join(key for key, value in target.items() if value) #grab outcome name
  413. plt.title(f'{y_name}' + '(pred) - ' +f'{y_name}'+'(true)', fontsize=16)
  414. plt.plot(diff_y, marker='o', linestyle='')
  415. plt.gca().axes.xaxis.set_ticks([])
  416. plt.ylim(-20, 20)
  417. plt.axhline(y=0, color='gray', linestyle='--', linewidth=1)
  418. texts = []
  419. buffer = 0.7
  420. if target["Slope_BL_Y1_SARA"] : #adapt plotlims
  421. plt.ylim(-0.025, 0.025)
  422. buffer = 0.0008
  423. for i in range(len(diff_y)):
  424. text = plt.text(i, diff_y[i]+buffer,f'{df_test["sub_id"][i]}', ha='center', va='baseline',fontsize=8)
  425. plt.show()
  426. fig.savefig(os.path.join(final_dir,'y_pred-y_true.png'), dpi = 300, bbox_inches = 'tight')

06_KernelRidgeRegression_SCA_patients.py at commit ecb46eb, no license · at the source

Overview

  1. Donders Institute for Brain, Cognition, and Behavior, Radboud University, Nijmegen, the Netherlands
  2. Department of Cognitive Neuroscience, Radboud University Medical Center, Nijmegen, the Netherlands
  3. Western Institute for Neuroscience, Western University, London, Ontario, Canada
  4. Department of Computer Science, Western University, London, Ontario, Canada
  5. Department of Child and Adolescent Psychology and Psychiatry, Erasmus MC, University Medical Center Rotterdam-Sophia Children ‘s Hospital, Rotterdam, The Netherlands
  6. The Generation R Study Group, Erasmus MC, University Medical Center Rotterdam, Rotterdam, The Netherlands
  7. Department of Neurology, Radboud University Medical Center, Nijmegen, the Netherlands
  8. Faculty of Computer Science, Dalhousie University, Halifax, Canada
  9. Departamento de Fisiología, Facultad de Medicina, Universidad Nacional Autónoma de México, Ciudad de Mexico, México
  10. Department of Statistical and Actuarial Sciences, Western University, London, Ontario, Canada
  11. Department of Radiology and Nuclear Medicine, Erasmus MC University Medical Center Rotterdam, Rotterdam, The Netherlands
Dates: published online 31 March 2026
Type: Preprint
License: CC BY
Identifiers: DOI 10.64898/2026.03.27.714738 · OpenAlex W7147445871
Open access: green, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: human (organism), other condition (population), computational (subfield)
Methods: Connectivity, Statistics, Machine learning, Preprocessing, fMRI & imaging, Physiology & signal measures
Topic: Genetic Neurodegenerative Diseases (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Funding: European Commission (101057529); ZonMw (40‐44600‐98‐606); Raynor Cerebellum Project
Citations: not cited yet (Europe PMC); 64 references in the paper

Abstract

Brain disorders are often characterized by biological heterogeneity that is poorly captured by group-average analyses. Normative modeling has emerged as a promising tool to parse out such heterogeneity, yet existing lifespan reference models rely on coarse parcellations, which may obscure individual variability. Using an aggregated reference sample (n=58,597 scans from n=51,107 participants), we provide openly available normative models of brain morphometry at the voxel level across the lifespan, and we illustrate their potential utility with two complementary applications. First, we investigated long-term brain development after preterm birth across two independent cohorts (n=284; n=304) and found individualized, replicable and persistent brain alterations. Second, we extracted high-resolution patient-level morphometric deviations in two samples with rare, genetic neurodegenerative disorders (spinocerebellar ataxia type 1 and 3; n=29, n=15), which showed marked heterogeneity. Together, our findings highlight that voxelwise normative modeling can detect clinically relevant, individualized deviations from a reference model with high spatial precision.

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

Repository

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

RaynorCerebellumCharts/NormativeModels

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: ecb46eb49e81de3679add0a77eaae910344502a9, 4 September 2026
Languages: Python (6), Jupyter (1)
Size: 18 files, 7 scripts
Software Heritage: not archived
Found in: the text, “Methods”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (7 files), pandas (6 files), Matplotlib (5 files), NiBabel (5 files), scikit-learn (4 files), Nilearn (3 files), SciPy (3 files), seaborn (3 files), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
8 files

Tracing map

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

What the map holds:

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

Versions

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

Version 1, 30 September 2026: the first record

Recorded: type, journal, dates, 13 authors, 3 funders, 63 references.

Cite

This paper

Chavanne, A. V., Wang, Y., de Boer, A. A. A., Xu, B., van Prooije, T. H., Kapteijns, K. C. J., Reniers, C., Hernandez-Castillo, C. R., Fernandez-Ruiz, J., van de Warrenburg, B. P., Diedrichsen, J., Muetzel, R. L., & Marquand, A. F. (2026). Dissecting heterogeneous brain development and aging using voxelwise normative models. bioRxiv (preprint). https://doi.org/10.64898/2026.03.27.714738

BibTeX

@article{chavanne2026dissecting,
author = {Chavanne, Alice V. and Wang, Yaping and de Boer, Augustijn A. A. and Xu, Bing and van Prooije, Teije H. and Kapteijns, Kirsten C. J. and Reniers, Colette and Hernandez-Castillo, Carlos R. and Fernandez-Ruiz, Juan and van de Warrenburg, Bart P. and Diedrichsen, Jörn and Muetzel, Ryan L. and Marquand, André F.},
title = {{Dissecting heterogeneous brain development and aging using voxelwise normative models}},
journal = {bioRxiv (preprint)},
year = {2026},
month = mar,
publisher = {bioRxiv},
issn = {2692-8205},
doi = {10.64898/2026.03.27.714738},
url = {https://doi.org/10.64898/2026.03.27.714738}
}

RIS

TY - JOUR
AU - Chavanne, Alice V.
AU - Wang, Yaping
AU - de Boer, Augustijn A. A.
AU - Xu, Bing
AU - van Prooije, Teije H.
AU - Kapteijns, Kirsten C. J.
AU - Reniers, Colette
AU - Hernandez-Castillo, Carlos R.
AU - Fernandez-Ruiz, Juan
AU - van de Warrenburg, Bart P.
AU - Diedrichsen, Jörn
AU - Muetzel, Ryan L.
AU - Marquand, André F.
TI - Dissecting heterogeneous brain development and aging using voxelwise normative models
T2 - bioRxiv (preprint)
J2 - bioRxiv
PY - 2026
DA - 2026/03/31
SN - 2692-8205
PB - bioRxiv
DO - 10.64898/2026.03.27.714738
UR - https://doi.org/10.64898/2026.03.27.714738
ER -

CSL-JSON

{
"id": "10.64898/2026.03.27.714738",
"type": "article",
"title": "Dissecting heterogeneous brain development and aging using voxelwise normative models",
"container-title": "bioRxiv (preprint)",
"author": [
{
"family": "Chavanne",
"given": "Alice V."
},
{
"family": "Wang",
"given": "Yaping"
},
{
"family": "de Boer",
"given": "Augustijn A. A."
},
{
"family": "Xu",
"given": "Bing"
},
{
"family": "van Prooije",
"given": "Teije H."
},
{
"family": "Kapteijns",
"given": "Kirsten C. J."
},
{
"family": "Reniers",
"given": "Colette"
},
{
"family": "Hernandez-Castillo",
"given": "Carlos R."
},
{
"family": "Fernandez-Ruiz",
"given": "Juan"
},
{
"family": "van de Warrenburg",
"given": "Bart P."
},
{
"family": "Diedrichsen",
"given": "Jörn"
},
{
"family": "Muetzel",
"given": "Ryan L."
},
{
"family": "Marquand",
"given": "André F."
}
],
"container-title-short": "bioRxiv",
"DOI": "10.64898/2026.03.27.714738",
"ISSN": "2692-8205",
"publisher": "bioRxiv",
"URL": "https://doi.org/10.64898/2026.03.27.714738",
"issued": {
"date-parts": [
[
2026,
3,
31
]
]
}
}

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

Similar papers

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

[1] doi:10.1038/s41467-024-46398-2 [code]
Population-wide cerebellar growth models of children and adolescents.
Journal: Nature communications
In common: NiBabel, seaborn, scikit-learn, 4 other tools, 7 references, 3 authors
[2] doi:10.1162/imag.a.1269 [code]
From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, statsmodels, NiBabel, 6 other tools, computational, 15 references
[3] doi:10.64898/2026.02.17.706268 [code]
Protocol Update: The Normative Modelling Paradigm for Computational Psychiatry
Journal: bioRxiv (preprint)
In common: statsmodels, NiBabel, seaborn, 5 other tools, computational, 14 references
[4] doi:10.1186/s13293-026-00846-4 [code]
Predicting developmental norms from baseline cortical thickness in longitudinal studies.
Journal: Biology of sex differences
In common: Nilearn, statsmodels, NiBabel, 6 other tools, 14 references
[5] doi:10.1038/s41467-026-72875-x [code]
Lifespan normative modeling of brain microstructure.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, 13 references, author Andre Marquand
[6] doi:10.1038/s42003-026-09825-2 [code]
Normative modeling of MEG brain oscillations across the human lifespan.
Journal: Communications biology
In common: Nilearn, statsmodels, NiBabel, 6 other tools, computational, 9 references, author Andre Marquand
[7] doi:10.1038/s42003-024-06573-z [code]
Dissecting task-based fMRI activity using normative modelling: an application to the Emotional Face Matching Task.
Journal: Communications biology
In common: statsmodels, NiBabel, seaborn, 4 other tools, 11 references, author Andre Marquand
[8] doi:10.1038/s41598-025-20484-x [code]
Understanding individual neurodegenerative progression in Parkinson's disease through normative modelling.
Journal: Scientific reports
In common: NiBabel, seaborn, scikit-learn, 4 other tools, computational, 12 references
[9] doi:10.1038/s41467-025-63967-1 [code]
Heterogeneous, temporally consistent, and plastic brain development after preterm birth.
Journal: Nature communications
In common: statsmodels, NiBabel, seaborn, 5 other tools, other condition, 11 references
[10] doi:10.1371/journal.pone.0278776 [code]
Closing the life-cycle of normative modeling using federated hierarchical Bayesian regression.
Journal: PloS one
In common: NiBabel, seaborn, scikit-learn, 4 other tools, computational, 14 references

Contribute

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

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

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.64898/2026.03.27.714738.svg)](https://openscicode.org/badge/doi/10.64898/2026.03.27.714738)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.64898/2026.03.27.714738.svg
   :target: https://openscicode.org/badge/doi/10.64898/2026.03.27.714738
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.64898/2026.03.27.714738"><img src="https://openscicode.org/badge/doi/10.64898/2026.03.27.714738.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

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