OSCR

ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites.

A correction to this paper has been published: the notice, 42670026, from Europe PMC.

Code ↔ Paper

5 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 5 matches
  1. [1] § Methods › Generalizability of Normative Models to a New Site ↔ ComBat-Predict_section1-5_share.R, lines 812–868 · score 0.67 · Rank biserial correlation, Wilcoxon rank sum, centile score, LBCC, ADNI, harmonization
  2. [2] § Results › Harmonization Aligns Normative Scores Between Reference and Test Cohorts ↔ ComBat-Predict_section1-5_share.R, lines 616–657 · score 0.64 · harmonized ADNI CN, unharmonized ADNI, LBCC control, centile scores, LMCI, healthy
  3. [3] § Results › CB‐Predict Harmonization Reduces Heterogeneity Between Healthy Control Groups ↔ ComBat-Predict_section1-5_share.R, lines 812–868 · score 0.62 · rank biserial correlation, Wilcoxon rank sum, centile score, LBCC, ADNI, Predict
  4. [4] § Methods › Generalizability of Normative Models to a New Site ↔ ComBat-Predict_section1-5_share.R, lines 487–549 · score 0.60 · residual standard deviations, centile score, spline, sex, ComBat, formulation
  5. [5] § Methods › Generalizability of Normative Models to a New Site ↔ ComBat-Predict_section1-5_share.R, lines 400–485 · score 0.57 · Normative centile scores, healthy control, CB Predict, ComBat, trained, LBCC

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

R · 996 lines · 37 KB · no license · 5 matches

  1. ##################################################################
  2. ####project: ComBat-predict for out-of-sample harmonization and ##
  3. #### improved normative scoring application ##
  4. ####author: Yao Xin ##
  5. ####Created: 08/01/2025 ##
  6. ##################################################################
  7. # load libraries
  8. #analysis
  9. library(ComBatFamily) #ComBatFamily package
  10. library(gam)
  11. library(gamlss)
  12. library(mgcv)
  13. #data cleaning
  14. library(dplyr)
  15. library(tidyr)
  16. library(tidyverse)
  17. #visualization
  18. library(ggplot2)
  19. library(patchwork) #plotting multiple plots together
  20. library(RColorBrewer) #color palettes
  21. ## section 1 data preparation and visualization ####################
  22. # import cleaned data here - our dataset is called adni.lite
  23. ### 1.1 visualization of site effects ########
  24. #### 1.1.1 arrange the sites by mean thickness ####
  25. ordered_sites <- adni.lite %>%
  26. group_by(site) %>%
  27. dplyr::summarize(mean_thickness = mean(thickness.left.caudal.anterior.cingulate)) %>%
  28. arrange(mean_thickness) %>%
  29. pull(site)
  30. adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
  31. # Create the boxplot in the order of sites, arranged by mean
  32. ggplot(adni.lite,
  33. aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
  34. fill = as.factor(site), color = as.factor(site))) +
  35. geom_boxplot() +
  36. labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
  37. x = "Site",
  38. y = "Cortical Thickness",
  39. fill = "Site",
  40. color = "Site") +
  41. theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
  42. #### 1.1.2 arrange the sites by variance of thickness #####
  43. ordered_sites <- adni.lite %>%
  44. dplyr::group_by(site) %>%
  45. summarize(mean_thickness = sd(thickness.left.caudal.anterior.cingulate)) %>%
  46. arrange(mean_thickness) %>%
  47. pull(site)
  48. adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
  49. # Create the boxplot in the order of sites, arranged by variance
  50. ggplot(adni.lite,
  51. aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
  52. fill = as.factor(site), color = as.factor(site))) +
  53. geom_boxplot() +
  54. labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
  55. x = "Site",
  56. y = "Cortical Thickness",
  57. fill = "Site",
  58. color = "Site") +
  59. theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
  60. #### 1.1.3 arrange the sites by **sample size** ####
  61. ordered_sites <- adni.lite %>%
  62. group_by(site) %>%
  63. summarise(freq = n()) %>%
  64. arrange(freq) %>%
  65. pull(site)
  66. adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
  67. # Create the boxplot in the order of sites
  68. ggplot(adni.lite,
  69. aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
  70. fill = as.factor(site), color = as.factor(site))) +
  71. geom_boxplot() +
  72. labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
  73. x = "Site",
  74. y = "Cortical Thickness",
  75. fill = "Site",
  76. color = "Site") +
  77. theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
  78. #### print the sample sizes
  79. adni.lite %>%
  80. group_by(site) %>%
  81. summarise(freq = n()) %>%
  82. arrange(freq) %>% print(n=64)
  83. ## section 2 - Out-of-sample harmonization performance####################
  84. ### 2.1 full sample harmonization - for in-sample prediction ##########
  85. # Fit comfam on the full sample - all batches
  86. fit2 <- comfam(data = adni.lite[,9:70], #data
  87. bat = as.factor(adni.lite$batch), #batch
  88. covar=adni.lite[,(3:5)], # covariates
  89. lm, #model
  90. y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
  91. eb = TRUE, #empirical bayes,
  92. robust.LS = FALSE,
  93. ref.batch = 64
  94. )
  95. #apply the comfam fit on all batches, get outcomes of in-sample harmonization
  96. fit2_pred_in <- predict(fit2,
  97. newdata = adni.lite[,9:70],
  98. newbat = adni.lite$batch,
  99. newcovar = adni.lite[,(3:5)],
  100. robust.LS = FALSE,
  101. eb=TRUE
  102. )
  103. # in-sample-harmonized version of the data (for all 1~64 batches)
  104. full_sample_fitted <- cbind(adni.lite$ID,adni.lite$batch,
  105. as.data.frame(fit2_pred_in$dat.combat ) )
  106. colnames(full_sample_fitted)[1:2] <- c("ID", "batch")
  107. # the resulting harmonzied data frame is full_sample_fitted, with
  108. # 62 columns for outcomes. 505 rows for subjects
  109. ### 2.2 Evaluating out-of-sample prediction by leaving one site out #######
  110. #create empty objects to store results
  111. Nbatch <- 64
  112. n.train <- n.test <- rep(NA, Nbatch)
  113. MSE_batch <- error_list <- pred_list <- vector(mode='list', length=Nbatch)
  114. for (i in 1:Nbatch) {
  115. # split data to training and testing :
  116. # train with all batches except batch i , test with batch i
  117. train_data <- adni.lite[adni.lite$batch %in% seq(1:Nbatch)[-i],]
  118. test_data <- adni.lite[adni.lite$batch==i,] #batch i is being tested
  119. row_index <- which(adni.lite$batch==i)
  120. n.train[i] <- nrow(train_data)
  121. n.test[i] <- nrow(test_data)
  122. # fit ComBat model on training data
  123. fit_train <- comfam(data = train_data[,9:70], #cortical thickness columns
  124. bat = as.factor(train_data$batch), #batch
  125. covar=train_data[,(3:5)], # covariates: AGE, SEX, DIAGNOSIS
  126. lm, # type of model
  127. y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
  128. eb = TRUE,
  129. robust.LS = FALSE,
  130. ref.batch = max(as.numeric(train_data$batch)) #use the largest batch as ref
  131. )
  132. # apply the comfam fit on the testing data (batch i)
  133. out_pred <- predict(fit_train,
  134. newdata=test_data[,9:70], #new data
  135. newbat=as.factor(test_data$batch), #new batch
  136. newcovar = test_data[,(3:5)],
  137. robust.LS = FALSE, eb = TRUE
  138. )
  139. # store the out-of-sample harmonized outcomes for batch i in the i_ith element of pred_list
  140. pred_list[[i]] <- out_pred$dat.combat
  141. #error for each subject in batch i: store in the i_th element of error_list
  142. error_list[[i]] <- as.matrix(pred_list[[i]]) - # out-of-sample harmonized outcomes
  143. as.matrix(full_sample_fitted[row_index,-c(1:2)] ) # subtracting: in-sample harmonized outcomes
  144. MSE_batch[[i]] <- colMeans(error_list[[i]]^2) #MSE for all subjects in batch i
  145. }
  146. result <- list(MSE_batch = MSE_batch,
  147. error_list = error_list,
  148. pred_list = pred_list)
  149. #save(result, file = "path/result_name.RData")
  150. ### 2.3 Examining&visualizing the difference of out-of-sample and in-sample -##########
  151. ##load in saved result
  152. # load("path/xxx.RData")
  153. Nbatch <- 64
  154. MSE_batch <- result$MSE_batch
  155. error_list <- result$error_list
  156. pred_list <- result$pred_list
  157. ### re-organize MSE into new data frame, and derive RMSE
  158. batch_index <- c()
  159. MSE_vec <- c()
  160. for (j in 1:Nbatch) {
  161. MSE_vec <- rbind(MSE_vec, as.data.frame(as.numeric(MSE_batch[[j]])) )
  162. batch_index <- c(batch_index, rep(j, length(MSE_batch[[j]]) ) )
  163. }
  164. MSE_df <- as.data.frame(cbind(batch_index, MSE_vec))
  165. colnames(MSE_df) <- c("batch", "MSE")
  166. MSE_df <- MSE_df %>% mutate(RMSE = sqrt(MSE))
  167. #### how low are the RMSE values?
  168. summary(MSE_df$RMSE)
  169. #### [Figure 1a] Plot RMSE in each batch, arranged by batch sample size #####
  170. batch_table_sub1 <- batch_table_sub %>%
  171. mutate(batch = batch_order ) %>%
  172. dplyr::select(batch, freq)
  173. MSE_df <- MSE_df %>%
  174. left_join(batch_table_sub1, by = "batch") %>%
  175. mutate(freq=as.factor(freq) ) %>%
  176. arrange(freq) #merge in sample size of each batch
  177. #different sample sizes - categorized for plot
  178. category_breaks <- c(1, 9, 16, 23, 29,
  179. 37, 42, 48, 52, 54,
  180. 57, 59, 61, 62, 63)-0.5
  181. category_labels <- c("3", "4", "5", "6", "7",
  182. "8", "9", "10", "11","12",
  183. "13", "14", "17", "20", "23")
  184. # set color palette
  185. mycolors <- colorRampPalette(brewer.pal(8, "Set2"))(15)
  186. # set up a uniform theme for publication
  187. publication_theme <- function(base_size = 10, base_family = "Liberation Sans") {
  188. theme_minimal(base_size = base_size, base_family = base_family) +
  189. theme(
  190. axis.text = element_text(size = base_size, family = base_family),
  191. axis.title = element_text(size = base_size + 2, family = base_family),
  192. plot.title = element_text(size = base_size + 4, family = base_family, hjust = 0.5),
  193. plot.subtitle = element_text(size = base_size + 2, family = base_family),
  194. legend.text = element_text(size = base_size),
  195. legend.title = element_text(size = base_size + 1),
  196. axis.text.x = element_text(angle = 45, hjust = 1), # For your specific case
  197. panel.grid.minor = element_blank(), # Clean look for publication
  198. plot.margin = margin(10, 10, 10, 10) # Consistent margins
  199. )
  200. }
  201. ggplot(MSE_df[MSE_df$batch != 64,],
  202. aes(x = factor(batch), y = (RMSE),
  203. fill = as.factor(freq), color = as.factor(freq) )) +
  204. geom_boxplot() +
  205. stat_summary(fun = median, geom = "crossbar",
  206. width = 1, fatten = 0.4,
  207. color = "white", size = 1) +
  208. labs(
  209. subtitle = "a)",
  210. x = "Sites - ordered by sample size",
  211. y = "RMSE (mm)",
  212. fill = "Sample Size",
  213. color = "Sample Size") +
  214. publication_theme() +
  215. theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7),
  216. plot.subtitle = element_text(hjust = 0)) + # Add this line # Add dashed lines between the categories
  217. geom_vline(xintercept = category_breaks,
  218. linetype = "dashed", color = "grey60") +
  219. scale_fill_manual(values = mycolors,
  220. labels = category_labels) +
  221. scale_color_manual(values = mycolors)
  222. #### [Figure 1b] RMSE by AD% - plots #########
  223. # arrange the sites by **diagnosis%**
  224. ordered_batch <- adni.lite %>%
  225. group_by(batch) %>%
  226. summarise(ADprop = mean(DIAGNOSIS == "AD")) %>%
  227. arrange(ADprop) %>%
  228. pull(batch)
  229. MSE_df$batch <- factor(MSE_df$batch, levels = ordered_batch)
  230. # Create the boxplot in the order of batches by AD%
  231. ggplot(MSE_df[MSE_df$batch != 64,],
  232. aes(x = factor(batch, levels = ordered_batch),
  233. y = RMSE,
  234. fill = as.factor(batch), color = as.factor(batch))) +
  235. geom_boxplot() +
  236. stat_summary(fun = median, geom = "crossbar",
  237. width = 1, fatten = 0.4,
  238. color = "white", size = 1) +
  239. labs(#title = "Boxplot of RMSE of CB-Predict vs ComBat/d-Combat",
  240. #subtitle = "Averaged across subjects in each site",
  241. subtitle="b)",
  242. x = "Sites - ordered by AD%",
  243. y = "RMSE (mm)",
  244. fill = "Batch (ordered by AD%, 0.00% - 66.7%)",
  245. color = "Batch (ordered by AD%, 0.00% - 66.7%)") +
  246. publication_theme() +
  247. theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7) , # smaller font on xaxis labels
  248. legend.position = "none",
  249. plot.subtitle = element_text(hjust = 0))
  250. ## section 3 - Out-of-sample prediction performance####################
  251. #comparing the errors of linear model prediction, from non-harmonized vs. harmonized
  252. ### 3.1 Residuals from linear model prediction, with and without ComBat harmonization ##########
  253. Nbatch <- 64
  254. n.train <- n.test <- rep(NA, Nbatch)
  255. pred_list <- error_old_list <- error_new_list <- vector(mode='list', length=Nbatch)
  256. MSE_new_batch<- MSE_old_batch <- vector(mode='list', length=Nbatch)
  257. for (i in 1:Nbatch) {
  258. # similar to 2.2, split data to training and testing by leaving site i out
  259. train_data <- adni.lite[adni.lite$batch!=i,]
  260. test_data <- adni.lite[adni.lite$batch==i,] #batch i is being tested
  261. row_index <- which(adni.lite$batch==i)
  262. n.train[i] <- nrow(train_data)
  263. n.test[i] <- nrow(test_data)
  264. # fit ComBat model on training data
  265. fit_train <- comfam(data = train_data[,9:70], # CT outcomes from 62 regions
  266. bat = as.factor(train_data$batch), #sites
  267. covar=train_data[,(3:5)], # covariates
  268. lm, #model
  269. y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
  270. eb = TRUE,
  271. robust.LS = FALSE,
  272. ref.batch = max(as.numeric(train_data$batch)) #use the largest batch as ref
  273. )
  274. # harmonized version of the training data
  275. train_data_post <- as.data.frame(cbind(train_data[,1:8],fit_train$dat.combat) )
  276. # Multivariate linear model
  277. fit_LM_train <- lm( fit_train$dat.combat ~
  278. AGE + as.factor(SEX) + as.factor(DIAGNOSIS),
  279. data=train_data_post )
  280. # Applying the linear model - predicted CTs of testing data
  281. fit_LM_pred <- predict(fit_LM_train,
  282. newdata=test_data)
  283. # out-of-sample harmonization of the testing data
  284. fit_test <- predict(
  285. fit_train, # using the fitted ComBat model from training data
  286. newdata = test_data[,9:70], # harmonizing based on observed CTs in testing data
  287. newbat = as.factor(test_data$batch), #sites in the testing
  288. newcovar = test_data[,(3:5)],
  289. robust.LS = FALSE,
  290. eb=TRUE
  291. )
  292. test_data_harmonized <- fit_test$dat.combat # harmonized CTs in testing
  293. ## prediction error _ harmonized vs predicted
  294. error_new_list[[i]] <- as.matrix(fit_LM_pred) - as.matrix(test_data_harmonized) # for all subjects
  295. MSE_new_batch[[i]] <- colMeans(error_new_list[[i]]^2) #MSE for all subjects in batch i
  296. ### prediction error _ non-harmonized vs predicted
  297. error_old_list[[i]] <- as.matrix(fit_LM_pred) - as.matrix(test_data[,c(9:70)] )# for all subjects
  298. MSE_old_batch[[i]] <- colMeans(error_old_list[[i]]^2) #MSE for all subjects in batch i
  299. }
  300. result2 <- list(MSE_new_batch=MSE_new_batch,
  301. MSE_old_batch = MSE_old_batch,
  302. error_new_list = error_new_list,
  303. error_old_list = error_old_list,
  304. pred_list = pred_list)
  305. save(result2, file = "path/result2.Rdata")
  306. ### 3.2 Compare prediction errors of linear models for ComBat-Pred vs. non-harmonized #####
  307. ##load in elements in result2
  308. load("path/result2.Rdata")
  309. MSE_new_batch <- result2$MSE_new_batch
  310. MSE_old_batch <- result2$MSE_old_batch
  311. error_new_list <- result2$error_new_list
  312. error_old_list <- result2$error_old_list
  313. pred_list <- result2$pred_list
  314. ### re-organize the two sets of MSEs into new data frame
  315. MSE_new_vec<- MSE_old_vec <- c()
  316. batch_index <- c()
  317. Nbatch <-64
  318. for (j in 1:Nbatch) {
  319. MSE_new_vec <- rbind(MSE_new_vec, as.data.frame(as.numeric(MSE_new_batch[[j]])) )
  320. MSE_old_vec <- rbind(MSE_old_vec, as.data.frame(as.numeric(MSE_old_batch[[j]])) )
  321. batch_index <- c(batch_index, rep(j, length(MSE_new_batch[[j]]) ) )
  322. }
  323. MSE_oldnew_df <- as.data.frame(cbind(batch_index, MSE_new_vec, MSE_old_vec))
  324. colnames(MSE_oldnew_df) <- c("batch", "MSE_new","MSE_old")
  325. # add RMSE column for each set of MSE
  326. MSE_oldnew_df <- MSE_oldnew_df %>%
  327. mutate(difference = MSE_old - MSE_new,
  328. RMSE_new = sqrt(MSE_new),
  329. RMSE_old = sqrt(MSE_old)
  330. )
  331. #### [Figure 2] boxplot of new and old MSE by batch ########
  332. ggplot(MSE_oldnew_df[MSE_oldnew_df$batch != 64,],
  333. aes(x = factor(batch))) +
  334. geom_boxplot(aes(y = RMSE_new, fill = "Harmonized"),color = "green4", alpha = 0.8) + # New MSEs in light blue
  335. geom_boxplot(aes(y = RMSE_old, fill = "Non-harmonized"),color = "grey", alpha = 0.4) + # Old MSEs in grey
  336. scale_fill_manual(values = c("Harmonized" = "green4", "Non-harmonized" = "grey")) + # Define fill colors
  337. #scale_color_manual(values = c(ComBat = "purple", LM = "grey")) + # Define border colors
  338. labs(#title = "Boxplot of Out-of-Sample Prediction RMSE with vs. without CB-Predict Harmonization",
  339. #subtitle = "Averaged across subjects in each batch",
  340. x = "Sites - ordered by sample size",
  341. y = "RMSE (mm)",
  342. fill = "Model",
  343. color = "Model") +
  344. publication_theme() + # use the same publication theme as defined in fig1.1
  345. theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 6) )
  346. ## Section 4 - normative centile scores ###########
  347. #with healthy subjects in training, generate normative scores for healthy subjects in testing
  348. #this section is substituted by section 5
  349. ## Section 5 - centile scores, with LBCC healthy dataset as reference for normative scoring ###########
  350. ### 5.1 prepare the reference data for analysis --------
  351. # read in cleaned larger CN reference data - our imported dataset is called CN_lite
  352. # obtain colnames of regions
  353. CT_names1 <- colnames(CN_lite)[6:67]
  354. #### 5.1.1 reorder ADNI1 lite data in the same region order with CN_lite #########
  355. ## obtain left and right region names in ADNI
  356. ref_names <- colnames(adni.lite)
  357. ref_names_left <- ref_names[grepl("thickness.left", ref_names)]
  358. ref_names_right <- ref_names[grepl("thickness.right", ref_names)]
  359. #order alphabetically
  360. ref_names_left <- ref_names_left[order(ref_names_left)]
  361. ref_names_right <- ref_names_right[order(ref_names_right)]
  362. ref_names <- c(ref_names_left, ref_names_right)
  363. # compare with CN data ordering
  364. print(cbind(CT_names1,ref_names )) # manually check
  365. ## reorder columns in adni.lite by alphabetical order of regions
  366. adni.lite.reorder <- adni.lite %>%
  367. select(1:8, all_of(ref_names))
  368. #### 5.1.2 subset eligible batches in reordered ADNI lite #####
  369. # select out eligible batches: with >= 3 healthy controls
  370. batches_CN <- adni.lite.reorder %>%
  371. filter(DIAGNOSIS=="CN") %>% # filter out
  372. group_by(batch) %>%
  373. summarise(freq=n()) %>%
  374. filter(freq >= 3) %>%
  375. ungroup()
  376. # testing_batch_IDs contains the index of eligible batches - with at least 3 healthy controls
  377. testing_batch_IDs<-batches_CN$batch
  378. # subset of all subjects in the eligible batches
  379. adni.lite.reorder.sub <- adni.lite.reorder %>%
  380. filter( batch %in% testing_batch_IDs)
  381. # subset the healthy controls in the eligible batches
  382. adni.lite.reorder.CN_sub <- adni.lite.reorder %>%
  383. filter(DIAGNOSIS=="CN"& batch %in% testing_batch_IDs)
  384. ### 5.2 harmonizing the CN_lite healthy control data ######
  385. # function for harmonizing data and converting it into centile scores
  386. harmonize_qscore_4CN_lite <- function(dataset){
  387. #harmonizing the healthy subjects
  388. fit_harmonize <- comfam(data = dataset[,6:67], # data columns of outcomes
  389. bat = as.factor(dataset$batch), #batch
  390. covar=dataset[,(2:3)], # covariates: age and sex
  391. model = lm, #model
  392. formula = y ~ AGE + as.factor(SEX), #formula
  393. eb = TRUE,
  394. robust.LS = FALSE,
  395. ref.batch = max(dataset$batch)
  396. )
  397. #harmonized version of dataset
  398. df_post <- as.data.frame(cbind(dataset[,1:5],
  399. fit_harmonize$dat.combat) )
  400. #create empty matrix for storing centile score ver. of the same data
  401. df_pscores <- df_post
  402. df_pscores[,] <- NA
  403. #create empty list for storing each lm fit and sigma_hat
  404. fit_LM_post_list <- list()
  405. sigma2_hat_list <- list()
  406. message("Fitting individual models and calculating p-scores...")
  407. for (k in 6:ncol(df_post) ) { # for column 6 to the last col
  408. col_name <- names(df_post)[k]
  409. message(paste("Processing column:", col_name, "(", k, "/", ncol(df_post), ")"))
  410. # fit individual lm <--- can include a spline term, in here
  411. fit_LM_post_k <- lm(as.matrix(df_post[,k]) ~
  412. AGE + as.factor(SEX), data=df_post)
  413. #----produce centile scores based on est. distribution of yhat -----
  414. # variance of Yhat|X
  415. sigma_hat_k <- summary(fit_LM_post_k)$sigma
  416. # Calculate the centile (p-score) for each observation in column k
  417. # using the fitted mean and the residual standard deviation for model k
  418. df_pscores[, k] <- pnorm(df_post[, k],
  419. mean = fitted(fit_LM_post_k),
  420. sd = sigma_hat_k)
  421. # Store the model fit and sigma_hat in their respective lists
  422. # Use the column name as the list element name for easy access
  423. fit_LM_post_list[[col_name]] <- fit_LM_post_k
  424. sigma2_hat_list[[col_name]] <- sigma_hat_k
  425. }
  426. message("Function finished.")
  427. return(list(
  428. df_post = df_post, # The harmonized data
  429. df_pscores = df_pscores, # df of percentile scores
  430. fit_harmonize = fit_harmonize, # The original object returned by comfam
  431. fit_LM_post_list = fit_LM_post_list, # List containing LM for each region
  432. sigma2_hat_list = sigma2_hat_list # List containing each sigma_hat
  433. ))
  434. }
  435. # Harmonize the CN_lite data, AND store the results
  436. pscore_CN <- harmonize_qscore_4CN_lite(CN_lite)
  437. # save result, load in result for use later
  438. # save(pscore_CN,file="yourpath/pscore_CN.Rdata")
  439. # load("yourpath/pscore_CN.Rdata")
  440. #load in elements in pscore_CN
  441. CN_fit <- pscore_CN$fit_harmonize #the comfam model fit
  442. CN_scores <- pscore_CN$df_pscores #centile scores
  443. CN_LM <- pscore_CN$fit_LM_post_list # List containing LM for each region
  444. CN_sigma2 <- pscore_CN$sigma2_hat_list # List containing each sigma_hat
  445. ### 5.3 Applying the model to harmonize+normalize adni.lite data ######
  446. #### 5.3.1 The function for applying the CN model to harmonized/unharmonized test data #####
  447. test_data_qscore_fn <- function(test_data_CN, test_data){
  448. # Harmonizing out-of-sample testing data, with the comfam fit based on CN_lite
  449. test_harmonized_applied <- predict(CN_fit,
  450. newdata = test_data[,9:70],
  451. newbat = as.factor(test_data$batch),
  452. newcovar = test_data[,3:4],
  453. robust.LS = FALSE,
  454. eb=TRUE )
  455. # harmonized ver. of testing data
  456. test_raw_values <- test_harmonized_applied$dat.combat # test data harmonized
  457. test_raw_values2 <- as.matrix(test_data[,9:70]) # test data un-harmonized(Raw)
  458. #empty dfs for storing centile score ver. of the testing data
  459. test_pscores_harmonized <-
  460. test_pscores_unharmonized <-
  461. matrix(NA, nrow = nrow(test_data), ncol = 62)
  462. # centile scores from harmonized data
  463. for (j in 1:62) {
  464. test_means <- predict(CN_LM[[j]], # the LM for region j
  465. newdata = test_data[,3:4]) # fitted mean for testing data
  466. test_sigma <- CN_sigma2[[j]] # the fitted sigma_hat for region j
  467. #obtain the p-score of the
  468. test_pscores_harmonized[,j] <- pnorm(test_raw_values[,j],
  469. mean = test_means,
  470. sd = test_sigma )
  471. }
  472. # centile scores from NON-harmonized data
  473. for (j in 1:62) {
  474. test_pscores_unharmonized[,j] <- pnorm(test_raw_values2[,j],
  475. mean = test_means,
  476. sd = test_sigma)
  477. }
  478. # output both datasets - harmonized centile scores, non-harmonized centile scores
  479. return(list(test_pscores_harmonized = test_pscores_harmonized,
  480. test_pscores_unharmonized = test_pscores_unharmonized)
  481. )
  482. }
  483. #### 5.3.2 Applying the function on the ADNI data (as testing) ######
  484. # dataset of harmonized centile scores of our testing data
  485. test_pscores_harmonized <-
  486. test_data_qscore_fn(test_data_CN = adni.lite.reorder.CN_sub,
  487. # Using the healthy subset in testing to estimate site effects
  488. test_data = adni.lite.reorder.sub
  489. # To harmonize all subjects in testing data
  490. )$test_pscores_harmonized # the harmonized centile scores
  491. # dataset of NON-harmonized centile scores of our testing data
  492. test_pscores_unharmonized <-
  493. test_data_qscore_fn(test_data_CN = adni.lite.reorder.CN_sub,
  494. test_data = adni.lite.reorder.sub
  495. )$test_pscores_unharmonized # UNharmonized centile scores
  496. #### 5.3.3 Comparing the centile scores in CN_lite, and in ADNI.lite.sub #######
  497. ##### 5.3.3.1 save the centile scores of the four groups - LBCC_control, CN, LMCI, AD ########
  498. # dataset for plotting LBCC vs. three groups in testing data - harmonized
  499. levels_diagnosis <- c( "LBCC_control","CN","LMCI", "AD" ) # add LBCC control as one of the levels
  500. # harmonized ADNI centile scores
  501. centile_scores_3groups <-
  502. cbind(adni.lite.reorder.sub[,1:5] ,
  503. test_pscores_harmonized) %>%
  504. as.data.frame() %>%
  505. mutate(DIAGNOSIS = factor(DIAGNOSIS,
  506. levels = c("LBCC_control", "CN", "LMCI", "AD"),
  507. labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")))
  508. # unharmonized ADNI centile scores
  509. centile_scores_3groups_raw <-
  510. cbind(adni.lite.reorder.sub[,1:5] ,
  511. test_pscores_unharmonized) %>%
  512. as.data.frame() %>%
  513. mutate(DIAGNOSIS = factor(DIAGNOSIS,
  514. levels = c("LBCC_control", "CN", "LMCI", "AD"),
  515. labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")))
  516. # LBCC centile scores
  517. CN_scores_1 <- CN_scores %>%
  518. as.data.frame() %>%
  519. mutate( DIAGNOSIS = "LBCC_control",
  520. DIAGNOSIS = factor(DIAGNOSIS, #add the levels
  521. levels = c("LBCC_control", "CN", "LMCI", "AD"),
  522. labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")),
  523. batch=factor(batch)) %>%
  524. rename(ID = participant) %>%
  525. select(ID, batch, AGE, SEX, DIAGNOSIS, 6:67)
  526. # append the ADNI + LBCC centile scores
  527. colnames(centile_scores_3groups) <- colnames(CN_scores_1)
  528. colnames(centile_scores_3groups_raw) <- colnames(CN_scores_1)
  529. centile_scores_4groups <- dplyr::bind_rows(centile_scores_3groups, CN_scores_1) # harmonized
  530. centile_scores_4groups_raw <- dplyr::bind_rows(centile_scores_3groups_raw, CN_scores_1) # unharmonized
  531. ##### 5.3.3.2. plot centile scores in 1 group of LBCC + 3 groups of testing data======== #####
  532. #### [Figure 4a-b] ######
  533. # Define color palette for consistency
  534. mycolors <- c("grey65", "#4059ad", "#97d8c4", "#f4b942")
  535. ## figure 4A - boxplot of centile scores in 4 groups, without harmonization
  536. plot_a <- centile_scores_4groups_raw %>%
  537. # pivot by region for plotting each region in one box
  538. tidyr::pivot_longer(cols = 6:67, names_to = "region", values_to = "centile") %>%
  539. arrange(region) %>%
  540. ggplot(aes(x = region, y = centile, color = DIAGNOSIS, fill = DIAGNOSIS)) +
  541. geom_boxplot(outlier.size = 0.5, size = 0.3) +
  542. stat_summary(fun = median, geom = "crossbar",
  543. width = 1, fatten = 0.4,
  544. color = "white", size = 1) +
  545. labs(#title=str_wrap("Boxplot of Fitted Centile Scores by Region and Group with and without CB-Predict Harmonization", width = 60),
  546. subtitle = "a) Testing Data Not Harmonized",
  547. x = "Regions 1–62",
  548. y = "Fitted Centile Scores",
  549. color = "Group", # Legend title
  550. fill = "Group") + # Legend title
  551. facet_wrap(~ DIAGNOSIS, ncol = 4) +
  552. theme_minimal(base_size = 10, base_family = "Liberation Sans") +
  553. theme(
  554. axis.text = element_text(size = 10),
  555. axis.title = element_text(size = 12),
  556. plot.title = element_text(size = 16, hjust = 0, vjust=1),
  557. plot.subtitle = element_text(size = 12,hjust=0),
  558. legend.text = element_text(size = 10),
  559. legend.title = element_text(size = 11),
  560. strip.text = element_text(size = 10),
  561. axis.text.x = element_blank(),
  562. panel.grid.minor = element_blank(), # Clean look for publication
  563. plot.margin = margin(10, 10, 10, 10) # Consistent margins
  564. ) +
  565. ylim(0, 1) +
  566. scale_color_manual(values = mycolors) +
  567. scale_fill_manual(values = mycolors) +
  568. geom_hline(yintercept = 0.5, linetype = "longdash", linewidth = 0.8, color = "grey")
  569. ## figure 4B - boxplot of centile scores in 4 groups, with harmonization
  570. plot_b <- centile_scores_4groups %>%
  571. tidyr::pivot_longer(cols = 6:67, names_to = "region", values_to = "centile") %>%
  572. arrange(region) %>%
  573. ggplot(aes(x = region, y = centile, color = DIAGNOSIS, fill = DIAGNOSIS)) +
  574. geom_boxplot(outlier.size = 0.5, size = 0.3) +
  575. stat_summary(fun = median, geom = "crossbar",
  576. width = 1, fatten = 0.4,
  577. color = "white", size = 1) +
  578. labs(#title = "Boxplot of Fitted Centile Scores by Region and Group with and without CB-Predict Harmonization",
  579. subtitle = "b) Testing Data Harmonized",
  580. x = "Regions 1–62",
  581. y = "Fitted Centile Scores",
  582. color = "Group", # Legend title
  583. fill = "Group") + # Legend title
  584. facet_wrap(~ DIAGNOSIS, ncol = 4) +
  585. theme_minimal(base_size = 10, base_family = "Liberation Sans") +
  586. theme(
  587. axis.text = element_text(size = 10),
  588. axis.title = element_text(size = 12),
  589. plot.title = element_text(size = 16, hjust = 0, vjust=1),
  590. plot.subtitle = element_text(size = 12,hjust=0),
  591. legend.text = element_text(size = 10),
  592. legend.title = element_text(size = 11),
  593. strip.text = element_text(size = 10),
  594. axis.text.x = element_blank(),
  595. panel.grid.minor = element_blank(), # Clean look for publication
  596. plot.margin = margin(10, 10, 10, 10) # Consistent margins
  597. ) +
  598. ylim(0, 1) +
  599. scale_color_manual(values = mycolors) +
  600. scale_fill_manual(values = mycolors) +
  601. geom_hline(yintercept = 0.5, linetype = "longdash", linewidth = 0.8, color = "grey")
  602. # Combine plots using patchwork
  603. combined_plot <- plot_a / plot_b +
  604. plot_layout(heights = c(1, 1), guides = "collect") &
  605. theme(legend.position = "bottom")
  606. # Print the combined plot
  607. combined_plot
  608. ### 5.4 Harmonization improves the comparability of normative scores #######
  609. #### 5.4.1 Non-parametric test of the differences between ADNI and LBCC CN groups: ####
  610. # (Wilcoxon rank sum test )
  611. # we are going to compare the CN in ADNI vs. the CN in LBCC
  612. centile_scores_2groups_CN <-
  613. subset(centile_scores_4groups,
  614. DIAGNOSIS %in% c("LBCC(control)", "ADNI - CN"))
  615. ##### 5.4.1.1 function for conducting wilcoxon test and calculating effect size ######
  616. fn_wilcox <- function(datacolumn, datasetname) {
  617. # build formula
  618. form <- as.formula(paste(datacolumn, "~ DIAGNOSIS"))
  619. # run Wilcoxon rank‑sum (two‐sample) test
  620. wtest <- wilcox.test(form,
  621. data = datasetname,
  622. exact = FALSE,
  623. correct = TRUE)
  624. # extract W and p
  625. W <- unname(wtest$statistic)
  626. p_value <- wtest$p.value
  627. # group sizes
  628. grp_tbl <- table(datasetname$DIAGNOSIS)
  629. n1 <- as.numeric(grp_tbl[1])
  630. n2 <- as.numeric(grp_tbl[2])
  631. N <- n1 + n2
  632. # compute Z and r
  633. mu_W <- n1 * n2 / 2
  634. sigma_W <- sqrt(n1 * n2 * (N + 1) / 12)
  635. Z <- (W - mu_W) / sigma_W
  636. r <- Z / sqrt(N)
  637. # Bonferroni‐adjusted significance
  638. sig <- ifelse(p_value < 0.05/62, "*", "")
  639. return(list(
  640. W = round(W, 1),
  641. Z = round(Z, 3),
  642. r = round(r, 3),
  643. p_value = round(p_value, 8),
  644. sig = sig
  645. ))
  646. }
  647. ##### 5.4.1.2 Conduct the wilcoxon test for each region - harmonized, comparing ADNI.CN vs. LBCC.CN ######
  648. # prepare result table
  649. wilcox_table <- data.frame(
  650. region = colnames(centile_scores_2groups_CN)[6:67],
  651. W = NA_real_, #Wilcoxon rank sum test stat
  652. Z = NA_real_, #standardized score
  653. r = NA_real_, #rank-biserial correlation
  654. p_value = NA_real_, #p-value
  655. significance = "" #compared with bonferroni corrected
  656. )
  657. # loop through each column
  658. for (i in seq_along(wilcox_table$region)) {
  659. colname <- wilcox_table$region[i]
  660. res <- fn_wilcox(datacolumn = colname,
  661. datasetname = centile_scores_2groups_CN)
  662. wilcox_table[i, c("W","Z","r","p_value","significance")] <-
  663. unlist(res)
  664. }
  665. # wilcox_table stores the wilcoxon test statistics
  666. #print the test statistics:
  667. # for better visuals, put left and right side to side
  668. # Combine the two halves column-wise
  669. cbind(
  670. wilcox_table[1:31, , drop = FALSE],
  671. wilcox_table[32:62, , drop = FALSE]
  672. ) %>%
  673. knitr::kable("html",
  674. col.names = c("Region-left", "W", "Z", "r", "P-value", "Significance",
  675. "Region-right", "W", "Z", "r", "P-value", "Significance")
  676. )
  677. ##### 5.4.1.3 Conduct the wilcoxon test for each region - NON-harmonized, comparing ADNI.CN vs. LBCC.CN ######
  678. centile_scores_2groups_raw <-
  679. subset(centile_scores_4groups_raw,
  680. DIAGNOSIS %in% c("LBCC(control)", "ADNI - CN"))
  681. wilcox_table_raw <- data.frame(
  682. region = colnames(centile_scores_2groups_raw)[6:67],
  683. W = NA_real_, #Wilcoxon rank sum test stat
  684. Z = NA_real_, #standardized score
  685. r = NA_real_, #rank-biserial correlation
  686. p_value = NA_real_, #p-value
  687. significance = "" #compared with bonferroni corrected
  688. )
  689. for (i in seq_along(wilcox_table_raw$region)) {
  690. colname <- wilcox_table_raw$region[i]
  691. res <- fn_wilcox(datacolumn = colname,
  692. datasetname = centile_scores_2groups_raw)# the un-harmonized CN groups
  693. wilcox_table_raw[i, c("W","Z","r","p_value","significance")] <-
  694. unlist(res)
  695. }
  696. # wilcox_table_raw stores the wilcoxon test statistics
  697. # Print the wilcoxon test statistics:
  698. # for better visuals, put left and right side to side
  699. # Combine the two halves column-wise
  700. cbind(
  701. wilcox_table_raw[1:31, , drop = FALSE],
  702. wilcox_table_raw[32:62, , drop = FALSE]
  703. ) %>%
  704. knitr::kable("html",
  705. col.names = c("Region-left", "W", "Z", "r", "P-value", "Significance",
  706. "Region-right", "W", "Z", "r", "P-value", "Significance")
  707. )
  708. #### 5.4.2 visualization of the 2 sets of wilcoxon tests ######
  709. # Rename columns so harmonized vs unharmonized line up
  710. colnames(wilcox_table) <- c(
  711. "outcome",
  712. "W_harmonized", "Z_harmonized", "r_harmonized",
  713. "p_value_harmonized", "sig_harmonized"
  714. )
  715. colnames(wilcox_table_raw) <- c(
  716. "outcome",
  717. "W_unharmonized", "Z_unharmonized", "r_unharmonized",
  718. "p_value_unharmonized", "sig_unharmonized"
  719. )
  720. # Wide‐format join for printing side‑by‑side HTML table
  721. wilcox_wide <- wilcox_table %>%
  722. left_join(wilcox_table_raw, by = "outcome") %>%
  723. select(
  724. outcome,
  725. Z_unharmonized, r_unharmonized,
  726. Z_harmonized, r_harmonized
  727. )
  728. # Long format of wilcoxon tables for plotting
  729. wt_h <- wilcox_table %>%
  730. select(outcome, Z_harmonized, r_harmonized) %>%
  731. mutate(source = "harmonized") %>%
  732. # rename to match
  733. rename(
  734. Z = Z_harmonized,
  735. r = r_harmonized
  736. )
  737. wt_u <- wilcox_table_raw %>%
  738. select(outcome, Z_unharmonized, r_unharmonized) %>%
  739. mutate(source = "unharmonized") %>%
  740. # rename to match
  741. rename(
  742. Z = Z_unharmonized,
  743. r = r_unharmonized
  744. )
  745. results_long <- bind_rows(wt_h, wt_u)
  746. #### [Figure 5a] lollipop plots of the rank biseral --------
  747. # Define color palette
  748. mycolors <- c("#4059ad", "#f4b942") # For harmonized and unharmonized
  749. # Lollipop chart
  750. results_dodged <- results_long %>%
  751. mutate(
  752. r_abs = abs(as.numeric(r)),
  753. dodge_pos = as.numeric(factor(outcome)) +
  754. ifelse(source == unique(source)[2], 0.2, -0.2) # Manual dodge offset
  755. )
  756. # Lollipop chart with vertical stems
  757. ggplot(results_dodged, aes(x = dodge_pos, y = r_abs, color = source)) +
  758. geom_segment(aes(xend = dodge_pos, yend = 0),
  759. linewidth = 0.5) + # Vertical stems from dodged position
  760. geom_point(size = 2) + # Points at dodged position
  761. labs(
  762. #title = "Rank-Biserial Correlation of Two-Sample Wilcoxon Rank Sum Test",
  763. #subtitle = "Harmonized vs. Unharmonized",
  764. x = "Regions 1–62",
  765. y = "Absolute Rank-Biserial Correlation",
  766. color = ""
  767. ) +
  768. publication_theme() +
  769. theme(
  770. axis.text.x = element_text(size = 8),
  771. #axis.text.x = element_blank(),
  772. #axis.ticks.x = element_blank(),
  773. legend.position = "bottom"
  774. ) +
  775. scale_y_continuous(
  776. breaks = seq(0, 1, by = 0.1),
  777. minor_breaks = NULL
  778. ) +
  779. scale_color_manual(values = mycolors) +
  780. scale_x_continuous(breaks = 1:62, labels = 1:62) # Map dodged positions back to regions
  781. #### [Figure 5b] Plot standardized Z by region -------
  782. results_long %>%
  783. ggplot(aes(x = outcome, y = as.numeric(Z),
  784. color = source, group = source)) +
  785. geom_point(size = 1) +
  786. labs(
  787. title = "Standardized Z by Region",
  788. subtitle = "Harmonized vs. Unharmonized",
  789. x = "Region (1–62)",
  790. y = "Z statistic"
  791. ) +
  792. theme(
  793. axis.text.x = element_blank(),
  794. axis.ticks.x = element_blank()
  795. )+
  796. scale_color_manual(values = c("#4059ad", "#f4b942"))+
  797. scale_fill_manual(values = c("#4059ad", "#f4b942"))

ComBat-Predict_section1-5_share.R at commit f7341bc, no license · at the source

Overview

Authors: Yao Xin1, Margaret Gardner2,3, Nicholas J. Tustison4, Philip Cook5, James Gee5, Andreana Benitez6, Jens H. Jensen7, Alzheimer's Disease Neuroimaging Initiative, Lifespan Brain Chart Consortium, Richard Bethlehem8, Jakob Seidlitz2,9,10,11, Aaron F. Alexander‐Bloch2,8,9,10,11, Andrew A. Chen1
  1. Department of Public Health Sciences Medical University of South Carolina Charleston South Carolina USA
  2. Brain‐Gene‐Development Lab The Children's Hospital of Philadelphia and Penn Medicine Philadelphia Pennsylvania USA
  3. Neuroscience Graduate Group, Perelman School of Medicine University of Pennsylvania Philadelphia Pennsylvania USA
  4. Department of Radiology and Medical Imaging University of Virginia Charlottesville Virginia USA
  5. Department of Radiology University of Pennsylvania Philadelphia Pennsylvania USA
  6. Department of Neurology Medical University of South Carolina Charleston South Carolina USA
  7. Department of Neuroscience Medical University of South Carolina Charleston South Carolina USA
  8. Department of Psychology University of Cambridge Cambridge UK
  9. Department of Psychiatry University of Pennsylvania Philadelphia Pennsylvania USA
  10. Department of Child and Adolescent Psychiatry and Behavioral Sciences The Children's Hospital of Philadelphia Philadelphia Pennsylvania USA
  11. Lifespan Brain Institute of The Children's Hospital of Philadelphia and Penn Medicine Philadelphia Pennsylvania USA
Journal: Human brain mapping, volume 47, issue 8, article e70546
Dates: received 4 October 2025; accepted 5 May 2026; published online 19 May 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/hbm.70546 · PMID 42157534 · PMCID PMC13581068 · OpenAlex W7161767811
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), Alzheimer's / dementia (population), computational (subfield)
Methods: Statistics, Machine learning, Preprocessing
Keywords: ADNI, ComBat, cortical thickness, neuroimaging, normative modeling, site effects
MeSH: Alzheimer Disease*, Brain*, Brain Cortical Thickness*, Cerebral Cortex*, Magnetic Resonance Imaging*, Neuroimaging*, Aging, Female, Humans, Male (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: NIH (R01MH123550, R01MH132934, R01MH133843, R01MH134896)
Citations: cited by 4 papers (Europe PMC); 40 references in the paper
Notices: A correction to this paper has been published (42670026, from Europe PMC)

Abstract

Neuroimaging is vital in quantifying brain atrophy due to typical aging and due to neurodegenerative diseases. To collect large samples necessary to model lifespan brain development, research consortiums aggregate images acquired across multiple study sites. Previous studies have demonstrated that this multi‐site study design can lead to site‐related bias, necessitating harmonization of these “site effects.” However, current methodologies are unable to generalize to new sites outside the original harmonized sample, limiting translation to new sites or clinical practice. Here, we propose a method called ComBat‐Predict (CB‐Predict) building upon the ComBat method for site effect adjustment, which extends to data from a new site with smaller sample sizes and unknown site effects. In data from the Alzheimer's Disease Neuroimaging Initiative, our proposed method mitigates bias and yields high accuracy in predicting cortical thickness measures when generalizing the model to new data. Furthermore, we demonstrate that our proposed harmonization method can reduce site‐related variance in centile scores estimated using data from the Lifespan Brain Chart Consortium. Altogether, our results demonstrate that CB‐Predict effectively harmonizes new sites and thereby enables effective translation of neuroimaging models to additional samples.

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 5 matches between paragraphs and lines of code.

andy1764/ComBatFamily

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: e0a8af655de110997af722800b5a1d4090f35480, 1 July 2026
Languages: R (7)
Size: 24 files, 7 scripts
Software Heritage: not checked
Found in: the text, “CB‐Predict Extension”
Holds: README, environment (DESCRIPTION), documentation, 2 notebooks
Not found: license file, CITATION.cff, tests, continuous integration
Tools: mgcv (4 files), lme4 (2 files), neuroCombat (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
8 files

ntustison/CrossLong

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 6abd90eb1a7c02c7a9e42afcba6daaff553bef10, 11 August 2026
Languages: R (52), Stan (15)
Size: 280 files, 67 scripts
Software Heritage: not checked
Found in: the text, “ADNI Data Processing”
Holds: README, 38 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Stan (28 files), tidyverse (14 files), ggplot2 (4 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
68 files

brainchart/Lifespan

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 4a6faa19b8fb7e11e2173fe11998ab3b87c8c5b4, 21 February 2025
Languages: R (21)
Size: 361 files, 21 scripts
Software Heritage: not checked
Found in: “Data Availability Statement”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
22 files

Munchkin-233/ComBat-Predict_eval

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: f7341bc63f00b5d0380ec087aeaad6ffa22206c9, 5 September 2025
Languages: R (1)
Size: 2 files, 1 script
Software Heritage: not checked
Found in: “Data Availability Statement”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file), mgcv (1 file), patchwork (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
2 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:

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

Data were obtained from the publicly available ADNI database (adni.loni.usc.edu) and the LBCC consortium dataset (brainchart.shinyapps.io/brainchart), both accessed under their respective data use agreements. Links to open datasets of LBCC are listed on https://github.com/brainchart/Lifespan. The investigators within the ADNI contributed to the design and implementation of the ADNI and/or provided data but did not participate in the analysis or writing of this report. A complete listing of ADNI investigators can be found at https://adni.loni.usc.edu/wp‐content/uploads/how_to_apply/ADNI_Acknowledgement_List.pdf (https://adni.loni.usc.edu/wp-content/uploads/how_to_apply/ADNI_Acknowledgement_List.pdf). ComBat‐Predict is implemented as part of the ComBatFamily R package as predict.comfam and publicly accessible via Github: https://github.com/andy1764/ComBatFamily. Code and data analysis scripts are available at: https://github.com/Munchkin‐233/ComBat‐Predict_eval (https://github.com/Munchkin-233/ComBat-Predict_eval).

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 13 authors, 6 keywords, 10 MeSH terms, 1 funder, 40 references, 1 integrity notice.

Cite

This paper

Xin, Y., Gardner, M., Tustison, N. J., Cook, P., Gee, J., Benitez, A., Jensen, J. H., Alzheimer's Disease Neuroimaging Initiative, Lifespan Brain Chart Consortium, Bethlehem, R., Seidlitz, J., Alexander‐Bloch, A. F., & Chen, A. A. (2026). ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites. Human brain mapping, 47(8), e70546. https://doi.org/10.1002/hbm.70546

BibTeX

@article{xin2026combat,
author = {Xin, Yao and Gardner, Margaret and Tustison, Nicholas J. and Cook, Philip and Gee, James and Benitez, Andreana and Jensen, Jens H. and {Alzheimer's Disease Neuroimaging Initiative} and {Lifespan Brain Chart Consortium} and Bethlehem, Richard and Seidlitz, Jakob and Alexander‐Bloch, Aaron F. and Chen, Andrew A.},
title = {{ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites}},
journal = {Human brain mapping},
year = {2026},
month = jun,
volume = {47},
number = {8},
pages = {e70546},
publisher = {Wiley},
issn = {1065-9471},
doi = {10.1002/hbm.70546},
url = {https://doi.org/10.1002/hbm.70546},
pmid = {42157534},
pmcid = {PMC13581068}
}

RIS

TY - JOUR
AU - Xin, Yao
AU - Gardner, Margaret
AU - Tustison, Nicholas J.
AU - Cook, Philip
AU - Gee, James
AU - Benitez, Andreana
AU - Jensen, Jens H.
AU - Alzheimer's Disease Neuroimaging Initiative
AU - Lifespan Brain Chart Consortium
AU - Bethlehem, Richard
AU - Seidlitz, Jakob
AU - Alexander‐Bloch, Aaron F.
AU - Chen, Andrew A.
TI - ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites
T2 - Human brain mapping
J2 - Hum Brain Mapp
PY - 2026
DA - 2026/06/01
VL - 47
IS - 8
SP - e70546
SN - 1065-9471
PB - Wiley
DO - 10.1002/hbm.70546
UR - https://doi.org/10.1002/hbm.70546
LA - en
ER -

CSL-JSON

{
"id": "10.1002/hbm.70546",
"type": "article-journal",
"title": "ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites",
"container-title": "Human brain mapping",
"author": [
{
"family": "Xin",
"given": "Yao"
},
{
"family": "Gardner",
"given": "Margaret"
},
{
"family": "Tustison",
"given": "Nicholas J."
},
{
"family": "Cook",
"given": "Philip"
},
{
"family": "Gee",
"given": "James"
},
{
"family": "Benitez",
"given": "Andreana"
},
{
"family": "Jensen",
"given": "Jens H."
},
{
"literal": "Alzheimer's Disease Neuroimaging Initiative"
},
{
"literal": "Lifespan Brain Chart Consortium"
},
{
"family": "Bethlehem",
"given": "Richard"
},
{
"family": "Seidlitz",
"given": "Jakob"
},
{
"family": "Alexander‐Bloch",
"given": "Aaron F."
},
{
"family": "Chen",
"given": "Andrew A."
}
],
"container-title-short": "Hum Brain Mapp",
"volume": "47",
"issue": "8",
"page": "e70546",
"DOI": "10.1002/hbm.70546",
"PMID": "42157534",
"PMCID": "PMC13581068",
"ISSN": "1065-9471",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/hbm.70546",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

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.1162/imag.a.1290 [code]
Calibration of MRI-based reference intervals to new samples.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Alzheimer's / dementia, structural MRI / diffusion, 11 references, 3 authors
[2] doi:10.1038/s41467-026-73072-6 [code]
Mapping the spatiotemporal continuum of structural connectivity development across the human connectome in youth.
Journal: Nature communications
In common: neuroCombat, mgcv, lme4, 3 other tools, structural MRI / diffusion, 2 references
[3] doi:10.1002/hbm.70559 [code]
Replicability of Functional Brain Networks: A Study Through the Lens of Seven Resting-State Networks.
Journal: Human brain mapping
In common: neuroCombat, lme4, ggplot2, 1 other tool, 4 references
[4] doi:10.1038/s41593-026-02363-4 [code]
Cortical thickness changes precede high levels of amyloid by at least 7 years.
Journal: Nature neuroscience
In common: mgcv, lme4, patchwork, 2 other tools, Alzheimer's / dementia, structural MRI / diffusion, 2 references
[5] 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: computational, 7 references
[6] doi:10.1016/j.celrep.2026.117505 [code]
Impaired spatial coding and neuronal hyperactivity in the medial entorhinal cortex of aged APP knock-in mice.
Journal: Cell reports
In common: Stan, mgcv, lme4, 3 other tools, Alzheimer's / dementia
[7] doi:10.1162/imag.a.1337 [code]
Data quality biases normative models derived from fetal brain MRI.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: mgcv, patchwork, ggplot2, 1 other tool, structural MRI / diffusion, 3 references
[8] doi:10.1371/journal.pone.0355165 [code]
Pupillary dynamics during hands-off L2 driving and transitions of control under high cognitive load.
Journal: PloS one
In common: Stan, mgcv, lme4, 3 other tools
[9] doi:10.1093/braincomms/fcag343 [code]
Long-term brain volume trajectories and lifestyle associations in cognitively normal adults: the BRAIN-STRIDE study.
Journal: Brain communications
In common: lme4, patchwork, ggplot2, 1 other tool, structural MRI / diffusion, 3 references
[10] doi:10.7554/elife.103097 [code]
Canonical neurodevelopmental trajectories of structural and functional manifolds.
Journal: eLife
In common: mgcv, ggplot2, tidyverse, structural MRI / diffusion, 3 references

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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