ComBat-Predict Enhances Generalizability of Neuroimaging Models to New Sites.
A correction to this paper has been published: the notice, 42670026, from Europe PMC.
The 5 matches
- [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] § 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] § 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] § 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] § 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
- ##################################################################
- ####project: ComBat-predict for out-of-sample harmonization and ##
- #### improved normative scoring application ##
- ####author: Yao Xin ##
- ####Created: 08/01/2025 ##
- ##################################################################
- # load libraries
- #analysis
- library(ComBatFamily) #ComBatFamily package
- library(gam)
- library(gamlss)
- library(mgcv)
- #data cleaning
- library(dplyr)
- library(tidyr)
- library(tidyverse)
- #visualization
- library(ggplot2)
- library(patchwork) #plotting multiple plots together
- library(RColorBrewer) #color palettes
- ## section 1 data preparation and visualization ####################
- # import cleaned data here - our dataset is called adni.lite
- ### 1.1 visualization of site effects ########
- #### 1.1.1 arrange the sites by mean thickness ####
- ordered_sites <- adni.lite %>%
- group_by(site) %>%
- dplyr::summarize(mean_thickness = mean(thickness.left.caudal.anterior.cingulate)) %>%
- arrange(mean_thickness) %>%
- pull(site)
- adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
- # Create the boxplot in the order of sites, arranged by mean
- ggplot(adni.lite,
- aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
- fill = as.factor(site), color = as.factor(site))) +
- geom_boxplot() +
- labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
- x = "Site",
- y = "Cortical Thickness",
- fill = "Site",
- color = "Site") +
- theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
- #### 1.1.2 arrange the sites by variance of thickness #####
- ordered_sites <- adni.lite %>%
- dplyr::group_by(site) %>%
- summarize(mean_thickness = sd(thickness.left.caudal.anterior.cingulate)) %>%
- arrange(mean_thickness) %>%
- pull(site)
- adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
- # Create the boxplot in the order of sites, arranged by variance
- ggplot(adni.lite,
- aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
- fill = as.factor(site), color = as.factor(site))) +
- geom_boxplot() +
- labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
- x = "Site",
- y = "Cortical Thickness",
- fill = "Site",
- color = "Site") +
- theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
- #### 1.1.3 arrange the sites by **sample size** ####
- ordered_sites <- adni.lite %>%
- group_by(site) %>%
- summarise(freq = n()) %>%
- arrange(freq) %>%
- pull(site)
- adni.lite$site <- factor(adni.lite$site, levels = ordered_sites)
- # Create the boxplot in the order of sites
- ggplot(adni.lite,
- aes(x = factor(site, levels = ordered_sites), y = thickness.left.caudal.anterior.cingulate,
- fill = as.factor(site), color = as.factor(site))) +
- geom_boxplot() +
- labs(title = "Boxplot of Cortical Thickness Outcome by Site - left caudal anterior cingulate",
- x = "Site",
- y = "Cortical Thickness",
- fill = "Site",
- color = "Site") +
- theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels for better readability
- #### print the sample sizes
- adni.lite %>%
- group_by(site) %>%
- summarise(freq = n()) %>%
- arrange(freq) %>% print(n=64)
- ## section 2 - Out-of-sample harmonization performance####################
- ### 2.1 full sample harmonization - for in-sample prediction ##########
- # Fit comfam on the full sample - all batches
- fit2 <- comfam(data = adni.lite[,9:70], #data
- bat = as.factor(adni.lite$batch), #batch
- covar=adni.lite[,(3:5)], # covariates
- lm, #model
- y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
- eb = TRUE, #empirical bayes,
- robust.LS = FALSE,
- ref.batch = 64
- )
- #apply the comfam fit on all batches, get outcomes of in-sample harmonization
- fit2_pred_in <- predict(fit2,
- newdata = adni.lite[,9:70],
- newbat = adni.lite$batch,
- newcovar = adni.lite[,(3:5)],
- robust.LS = FALSE,
- eb=TRUE
- )
- # in-sample-harmonized version of the data (for all 1~64 batches)
- full_sample_fitted <- cbind(adni.lite$ID,adni.lite$batch,
- as.data.frame(fit2_pred_in$dat.combat ) )
- colnames(full_sample_fitted)[1:2] <- c("ID", "batch")
- # the resulting harmonzied data frame is full_sample_fitted, with
- # 62 columns for outcomes. 505 rows for subjects
- ### 2.2 Evaluating out-of-sample prediction by leaving one site out #######
- #create empty objects to store results
- Nbatch <- 64
- n.train <- n.test <- rep(NA, Nbatch)
- MSE_batch <- error_list <- pred_list <- vector(mode='list', length=Nbatch)
- for (i in 1:Nbatch) {
- # split data to training and testing :
- # train with all batches except batch i , test with batch i
- train_data <- adni.lite[adni.lite$batch %in% seq(1:Nbatch)[-i],]
- test_data <- adni.lite[adni.lite$batch==i,] #batch i is being tested
- row_index <- which(adni.lite$batch==i)
- n.train[i] <- nrow(train_data)
- n.test[i] <- nrow(test_data)
- # fit ComBat model on training data
- fit_train <- comfam(data = train_data[,9:70], #cortical thickness columns
- bat = as.factor(train_data$batch), #batch
- covar=train_data[,(3:5)], # covariates: AGE, SEX, DIAGNOSIS
- lm, # type of model
- y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
- eb = TRUE,
- robust.LS = FALSE,
- ref.batch = max(as.numeric(train_data$batch)) #use the largest batch as ref
- )
- # apply the comfam fit on the testing data (batch i)
- out_pred <- predict(fit_train,
- newdata=test_data[,9:70], #new data
- newbat=as.factor(test_data$batch), #new batch
- newcovar = test_data[,(3:5)],
- robust.LS = FALSE, eb = TRUE
- )
- # store the out-of-sample harmonized outcomes for batch i in the i_ith element of pred_list
- pred_list[[i]] <- out_pred$dat.combat
- #error for each subject in batch i: store in the i_th element of error_list
- error_list[[i]] <- as.matrix(pred_list[[i]]) - # out-of-sample harmonized outcomes
- as.matrix(full_sample_fitted[row_index,-c(1:2)] ) # subtracting: in-sample harmonized outcomes
- MSE_batch[[i]] <- colMeans(error_list[[i]]^2) #MSE for all subjects in batch i
- }
- result <- list(MSE_batch = MSE_batch,
- error_list = error_list,
- pred_list = pred_list)
- #save(result, file = "path/result_name.RData")
- ### 2.3 Examining&visualizing the difference of out-of-sample and in-sample -##########
- ##load in saved result
- # load("path/xxx.RData")
- Nbatch <- 64
- MSE_batch <- result$MSE_batch
- error_list <- result$error_list
- pred_list <- result$pred_list
- ### re-organize MSE into new data frame, and derive RMSE
- batch_index <- c()
- MSE_vec <- c()
- for (j in 1:Nbatch) {
- MSE_vec <- rbind(MSE_vec, as.data.frame(as.numeric(MSE_batch[[j]])) )
- batch_index <- c(batch_index, rep(j, length(MSE_batch[[j]]) ) )
- }
- MSE_df <- as.data.frame(cbind(batch_index, MSE_vec))
- colnames(MSE_df) <- c("batch", "MSE")
- MSE_df <- MSE_df %>% mutate(RMSE = sqrt(MSE))
- #### how low are the RMSE values?
- summary(MSE_df$RMSE)
- #### [Figure 1a] Plot RMSE in each batch, arranged by batch sample size #####
- batch_table_sub1 <- batch_table_sub %>%
- mutate(batch = batch_order ) %>%
- dplyr::select(batch, freq)
- MSE_df <- MSE_df %>%
- left_join(batch_table_sub1, by = "batch") %>%
- mutate(freq=as.factor(freq) ) %>%
- arrange(freq) #merge in sample size of each batch
- #different sample sizes - categorized for plot
- category_breaks <- c(1, 9, 16, 23, 29,
- 37, 42, 48, 52, 54,
- 57, 59, 61, 62, 63)-0.5
- category_labels <- c("3", "4", "5", "6", "7",
- "8", "9", "10", "11","12",
- "13", "14", "17", "20", "23")
- # set color palette
- mycolors <- colorRampPalette(brewer.pal(8, "Set2"))(15)
- # set up a uniform theme for publication
- publication_theme <- function(base_size = 10, base_family = "Liberation Sans") {
- theme_minimal(base_size = base_size, base_family = base_family) +
- theme(
- axis.text = element_text(size = base_size, family = base_family),
- axis.title = element_text(size = base_size + 2, family = base_family),
- plot.title = element_text(size = base_size + 4, family = base_family, hjust = 0.5),
- plot.subtitle = element_text(size = base_size + 2, family = base_family),
- legend.text = element_text(size = base_size),
- legend.title = element_text(size = base_size + 1),
- axis.text.x = element_text(angle = 45, hjust = 1), # For your specific case
- panel.grid.minor = element_blank(), # Clean look for publication
- plot.margin = margin(10, 10, 10, 10) # Consistent margins
- )
- }
- ggplot(MSE_df[MSE_df$batch != 64,],
- aes(x = factor(batch), y = (RMSE),
- fill = as.factor(freq), color = as.factor(freq) )) +
- geom_boxplot() +
- stat_summary(fun = median, geom = "crossbar",
- width = 1, fatten = 0.4,
- color = "white", size = 1) +
- labs(
- subtitle = "a)",
- x = "Sites - ordered by sample size",
- y = "RMSE (mm)",
- fill = "Sample Size",
- color = "Sample Size") +
- publication_theme() +
- theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7),
- plot.subtitle = element_text(hjust = 0)) + # Add this line # Add dashed lines between the categories
- geom_vline(xintercept = category_breaks,
- linetype = "dashed", color = "grey60") +
- scale_fill_manual(values = mycolors,
- labels = category_labels) +
- scale_color_manual(values = mycolors)
- #### [Figure 1b] RMSE by AD% - plots #########
- # arrange the sites by **diagnosis%**
- ordered_batch <- adni.lite %>%
- group_by(batch) %>%
- summarise(ADprop = mean(DIAGNOSIS == "AD")) %>%
- arrange(ADprop) %>%
- pull(batch)
- MSE_df$batch <- factor(MSE_df$batch, levels = ordered_batch)
- # Create the boxplot in the order of batches by AD%
- ggplot(MSE_df[MSE_df$batch != 64,],
- aes(x = factor(batch, levels = ordered_batch),
- y = RMSE,
- fill = as.factor(batch), color = as.factor(batch))) +
- geom_boxplot() +
- stat_summary(fun = median, geom = "crossbar",
- width = 1, fatten = 0.4,
- color = "white", size = 1) +
- labs(#title = "Boxplot of RMSE of CB-Predict vs ComBat/d-Combat",
- #subtitle = "Averaged across subjects in each site",
- subtitle="b)",
- x = "Sites - ordered by AD%",
- y = "RMSE (mm)",
- fill = "Batch (ordered by AD%, 0.00% - 66.7%)",
- color = "Batch (ordered by AD%, 0.00% - 66.7%)") +
- publication_theme() +
- theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7) , # smaller font on xaxis labels
- legend.position = "none",
- plot.subtitle = element_text(hjust = 0))
- ## section 3 - Out-of-sample prediction performance####################
- #comparing the errors of linear model prediction, from non-harmonized vs. harmonized
- ### 3.1 Residuals from linear model prediction, with and without ComBat harmonization ##########
- Nbatch <- 64
- n.train <- n.test <- rep(NA, Nbatch)
- pred_list <- error_old_list <- error_new_list <- vector(mode='list', length=Nbatch)
- MSE_new_batch<- MSE_old_batch <- vector(mode='list', length=Nbatch)
- for (i in 1:Nbatch) {
- # similar to 2.2, split data to training and testing by leaving site i out
- train_data <- adni.lite[adni.lite$batch!=i,]
- test_data <- adni.lite[adni.lite$batch==i,] #batch i is being tested
- row_index <- which(adni.lite$batch==i)
- n.train[i] <- nrow(train_data)
- n.test[i] <- nrow(test_data)
- # fit ComBat model on training data
- fit_train <- comfam(data = train_data[,9:70], # CT outcomes from 62 regions
- bat = as.factor(train_data$batch), #sites
- covar=train_data[,(3:5)], # covariates
- lm, #model
- y ~ AGE + as.factor(SEX) + as.factor(DIAGNOSIS), #formula
- eb = TRUE,
- robust.LS = FALSE,
- ref.batch = max(as.numeric(train_data$batch)) #use the largest batch as ref
- )
- # harmonized version of the training data
- train_data_post <- as.data.frame(cbind(train_data[,1:8],fit_train$dat.combat) )
- # Multivariate linear model
- fit_LM_train <- lm( fit_train$dat.combat ~
- AGE + as.factor(SEX) + as.factor(DIAGNOSIS),
- data=train_data_post )
- # Applying the linear model - predicted CTs of testing data
- fit_LM_pred <- predict(fit_LM_train,
- newdata=test_data)
- # out-of-sample harmonization of the testing data
- fit_test <- predict(
- fit_train, # using the fitted ComBat model from training data
- newdata = test_data[,9:70], # harmonizing based on observed CTs in testing data
- newbat = as.factor(test_data$batch), #sites in the testing
- newcovar = test_data[,(3:5)],
- robust.LS = FALSE,
- eb=TRUE
- )
- test_data_harmonized <- fit_test$dat.combat # harmonized CTs in testing
- ## prediction error _ harmonized vs predicted
- error_new_list[[i]] <- as.matrix(fit_LM_pred) - as.matrix(test_data_harmonized) # for all subjects
- MSE_new_batch[[i]] <- colMeans(error_new_list[[i]]^2) #MSE for all subjects in batch i
- ### prediction error _ non-harmonized vs predicted
- error_old_list[[i]] <- as.matrix(fit_LM_pred) - as.matrix(test_data[,c(9:70)] )# for all subjects
- MSE_old_batch[[i]] <- colMeans(error_old_list[[i]]^2) #MSE for all subjects in batch i
- }
- result2 <- list(MSE_new_batch=MSE_new_batch,
- MSE_old_batch = MSE_old_batch,
- error_new_list = error_new_list,
- error_old_list = error_old_list,
- pred_list = pred_list)
- save(result2, file = "path/result2.Rdata")
- ### 3.2 Compare prediction errors of linear models for ComBat-Pred vs. non-harmonized #####
- ##load in elements in result2
- load("path/result2.Rdata")
- MSE_new_batch <- result2$MSE_new_batch
- MSE_old_batch <- result2$MSE_old_batch
- error_new_list <- result2$error_new_list
- error_old_list <- result2$error_old_list
- pred_list <- result2$pred_list
- ### re-organize the two sets of MSEs into new data frame
- MSE_new_vec<- MSE_old_vec <- c()
- batch_index <- c()
- Nbatch <-64
- for (j in 1:Nbatch) {
- MSE_new_vec <- rbind(MSE_new_vec, as.data.frame(as.numeric(MSE_new_batch[[j]])) )
- MSE_old_vec <- rbind(MSE_old_vec, as.data.frame(as.numeric(MSE_old_batch[[j]])) )
- batch_index <- c(batch_index, rep(j, length(MSE_new_batch[[j]]) ) )
- }
- MSE_oldnew_df <- as.data.frame(cbind(batch_index, MSE_new_vec, MSE_old_vec))
- colnames(MSE_oldnew_df) <- c("batch", "MSE_new","MSE_old")
- # add RMSE column for each set of MSE
- MSE_oldnew_df <- MSE_oldnew_df %>%
- mutate(difference = MSE_old - MSE_new,
- RMSE_new = sqrt(MSE_new),
- RMSE_old = sqrt(MSE_old)
- )
- #### [Figure 2] boxplot of new and old MSE by batch ########
- ggplot(MSE_oldnew_df[MSE_oldnew_df$batch != 64,],
- aes(x = factor(batch))) +
- geom_boxplot(aes(y = RMSE_new, fill = "Harmonized"),color = "green4", alpha = 0.8) + # New MSEs in light blue
- geom_boxplot(aes(y = RMSE_old, fill = "Non-harmonized"),color = "grey", alpha = 0.4) + # Old MSEs in grey
- scale_fill_manual(values = c("Harmonized" = "green4", "Non-harmonized" = "grey")) + # Define fill colors
- #scale_color_manual(values = c(ComBat = "purple", LM = "grey")) + # Define border colors
- labs(#title = "Boxplot of Out-of-Sample Prediction RMSE with vs. without CB-Predict Harmonization",
- #subtitle = "Averaged across subjects in each batch",
- x = "Sites - ordered by sample size",
- y = "RMSE (mm)",
- fill = "Model",
- color = "Model") +
- publication_theme() + # use the same publication theme as defined in fig1.1
- theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 6) )
- ## Section 4 - normative centile scores ###########
- #with healthy subjects in training, generate normative scores for healthy subjects in testing
- #this section is substituted by section 5
- ## Section 5 - centile scores, with LBCC healthy dataset as reference for normative scoring ###########
- ### 5.1 prepare the reference data for analysis --------
- # read in cleaned larger CN reference data - our imported dataset is called CN_lite
- # obtain colnames of regions
- CT_names1 <- colnames(CN_lite)[6:67]
- #### 5.1.1 reorder ADNI1 lite data in the same region order with CN_lite #########
- ## obtain left and right region names in ADNI
- ref_names <- colnames(adni.lite)
- ref_names_left <- ref_names[grepl("thickness.left", ref_names)]
- ref_names_right <- ref_names[grepl("thickness.right", ref_names)]
- #order alphabetically
- ref_names_left <- ref_names_left[order(ref_names_left)]
- ref_names_right <- ref_names_right[order(ref_names_right)]
- ref_names <- c(ref_names_left, ref_names_right)
- # compare with CN data ordering
- print(cbind(CT_names1,ref_names )) # manually check
- ## reorder columns in adni.lite by alphabetical order of regions
- adni.lite.reorder <- adni.lite %>%
- select(1:8, all_of(ref_names))
- #### 5.1.2 subset eligible batches in reordered ADNI lite #####
- # select out eligible batches: with >= 3 healthy controls
- batches_CN <- adni.lite.reorder %>%
- filter(DIAGNOSIS=="CN") %>% # filter out
- group_by(batch) %>%
- summarise(freq=n()) %>%
- filter(freq >= 3) %>%
- ungroup()
- # testing_batch_IDs contains the index of eligible batches - with at least 3 healthy controls
- testing_batch_IDs<-batches_CN$batch
- # subset of all subjects in the eligible batches
- adni.lite.reorder.sub <- adni.lite.reorder %>%
- filter( batch %in% testing_batch_IDs)
- # subset the healthy controls in the eligible batches
- adni.lite.reorder.CN_sub <- adni.lite.reorder %>%
- filter(DIAGNOSIS=="CN"& batch %in% testing_batch_IDs)
- ### 5.2 harmonizing the CN_lite healthy control data ######
- # function for harmonizing data and converting it into centile scores
- harmonize_qscore_4CN_lite <- function(dataset){
- #harmonizing the healthy subjects
- fit_harmonize <- comfam(data = dataset[,6:67], # data columns of outcomes
- bat = as.factor(dataset$batch), #batch
- covar=dataset[,(2:3)], # covariates: age and sex
- model = lm, #model
- formula = y ~ AGE + as.factor(SEX), #formula
- eb = TRUE,
- robust.LS = FALSE,
- ref.batch = max(dataset$batch)
- )
- #harmonized version of dataset
- df_post <- as.data.frame(cbind(dataset[,1:5],
- fit_harmonize$dat.combat) )
- #create empty matrix for storing centile score ver. of the same data
- df_pscores <- df_post
- df_pscores[,] <- NA
- #create empty list for storing each lm fit and sigma_hat
- fit_LM_post_list <- list()
- sigma2_hat_list <- list()
- message("Fitting individual models and calculating p-scores...")
- for (k in 6:ncol(df_post) ) { # for column 6 to the last col
- col_name <- names(df_post)[k]
- message(paste("Processing column:", col_name, "(", k, "/", ncol(df_post), ")"))
- # fit individual lm <--- can include a spline term, in here
- fit_LM_post_k <- lm(as.matrix(df_post[,k]) ~
- AGE + as.factor(SEX), data=df_post)
- #----produce centile scores based on est. distribution of yhat -----
- # variance of Yhat|X
- sigma_hat_k <- summary(fit_LM_post_k)$sigma
- # Calculate the centile (p-score) for each observation in column k
- # using the fitted mean and the residual standard deviation for model k
- df_pscores[, k] <- pnorm(df_post[, k],
- mean = fitted(fit_LM_post_k),
- sd = sigma_hat_k)
- # Store the model fit and sigma_hat in their respective lists
- # Use the column name as the list element name for easy access
- fit_LM_post_list[[col_name]] <- fit_LM_post_k
- sigma2_hat_list[[col_name]] <- sigma_hat_k
- }
- message("Function finished.")
- return(list(
- df_post = df_post, # The harmonized data
- df_pscores = df_pscores, # df of percentile scores
- fit_harmonize = fit_harmonize, # The original object returned by comfam
- fit_LM_post_list = fit_LM_post_list, # List containing LM for each region
- sigma2_hat_list = sigma2_hat_list # List containing each sigma_hat
- ))
- }
- # Harmonize the CN_lite data, AND store the results
- pscore_CN <- harmonize_qscore_4CN_lite(CN_lite)
- # save result, load in result for use later
- # save(pscore_CN,file="yourpath/pscore_CN.Rdata")
- # load("yourpath/pscore_CN.Rdata")
- #load in elements in pscore_CN
- CN_fit <- pscore_CN$fit_harmonize #the comfam model fit
- CN_scores <- pscore_CN$df_pscores #centile scores
- CN_LM <- pscore_CN$fit_LM_post_list # List containing LM for each region
- CN_sigma2 <- pscore_CN$sigma2_hat_list # List containing each sigma_hat
- ### 5.3 Applying the model to harmonize+normalize adni.lite data ######
- #### 5.3.1 The function for applying the CN model to harmonized/unharmonized test data #####
- test_data_qscore_fn <- function(test_data_CN, test_data){
- # Harmonizing out-of-sample testing data, with the comfam fit based on CN_lite
- test_harmonized_applied <- predict(CN_fit,
- newdata = test_data[,9:70],
- newbat = as.factor(test_data$batch),
- newcovar = test_data[,3:4],
- robust.LS = FALSE,
- eb=TRUE )
- # harmonized ver. of testing data
- test_raw_values <- test_harmonized_applied$dat.combat # test data harmonized
- test_raw_values2 <- as.matrix(test_data[,9:70]) # test data un-harmonized(Raw)
- #empty dfs for storing centile score ver. of the testing data
- test_pscores_harmonized <-
- test_pscores_unharmonized <-
- matrix(NA, nrow = nrow(test_data), ncol = 62)
- # centile scores from harmonized data
- for (j in 1:62) {
- test_means <- predict(CN_LM[[j]], # the LM for region j
- newdata = test_data[,3:4]) # fitted mean for testing data
- test_sigma <- CN_sigma2[[j]] # the fitted sigma_hat for region j
- #obtain the p-score of the
- test_pscores_harmonized[,j] <- pnorm(test_raw_values[,j],
- mean = test_means,
- sd = test_sigma )
- }
- # centile scores from NON-harmonized data
- for (j in 1:62) {
- test_pscores_unharmonized[,j] <- pnorm(test_raw_values2[,j],
- mean = test_means,
- sd = test_sigma)
- }
- # output both datasets - harmonized centile scores, non-harmonized centile scores
- return(list(test_pscores_harmonized = test_pscores_harmonized,
- test_pscores_unharmonized = test_pscores_unharmonized)
- )
- }
- #### 5.3.2 Applying the function on the ADNI data (as testing) ######
- # dataset of harmonized centile scores of our testing data
- test_pscores_harmonized <-
- test_data_qscore_fn(test_data_CN = adni.lite.reorder.CN_sub,
- # Using the healthy subset in testing to estimate site effects
- test_data = adni.lite.reorder.sub
- # To harmonize all subjects in testing data
- )$test_pscores_harmonized # the harmonized centile scores
- # dataset of NON-harmonized centile scores of our testing data
- test_pscores_unharmonized <-
- test_data_qscore_fn(test_data_CN = adni.lite.reorder.CN_sub,
- test_data = adni.lite.reorder.sub
- )$test_pscores_unharmonized # UNharmonized centile scores
- #### 5.3.3 Comparing the centile scores in CN_lite, and in ADNI.lite.sub #######
- ##### 5.3.3.1 save the centile scores of the four groups - LBCC_control, CN, LMCI, AD ########
- # dataset for plotting LBCC vs. three groups in testing data - harmonized
- levels_diagnosis <- c( "LBCC_control","CN","LMCI", "AD" ) # add LBCC control as one of the levels
- # harmonized ADNI centile scores
- centile_scores_3groups <-
- cbind(adni.lite.reorder.sub[,1:5] ,
- test_pscores_harmonized) %>%
- as.data.frame() %>%
- mutate(DIAGNOSIS = factor(DIAGNOSIS,
- levels = c("LBCC_control", "CN", "LMCI", "AD"),
- labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")))
- # unharmonized ADNI centile scores
- centile_scores_3groups_raw <-
- cbind(adni.lite.reorder.sub[,1:5] ,
- test_pscores_unharmonized) %>%
- as.data.frame() %>%
- mutate(DIAGNOSIS = factor(DIAGNOSIS,
- levels = c("LBCC_control", "CN", "LMCI", "AD"),
- labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")))
- # LBCC centile scores
- CN_scores_1 <- CN_scores %>%
- as.data.frame() %>%
- mutate( DIAGNOSIS = "LBCC_control",
- DIAGNOSIS = factor(DIAGNOSIS, #add the levels
- levels = c("LBCC_control", "CN", "LMCI", "AD"),
- labels = c("LBCC(control)", "ADNI - CN", "ADNI - LMCI", "ADNI - AD")),
- batch=factor(batch)) %>%
- rename(ID = participant) %>%
- select(ID, batch, AGE, SEX, DIAGNOSIS, 6:67)
- # append the ADNI + LBCC centile scores
- colnames(centile_scores_3groups) <- colnames(CN_scores_1)
- colnames(centile_scores_3groups_raw) <- colnames(CN_scores_1)
- centile_scores_4groups <- dplyr::bind_rows(centile_scores_3groups, CN_scores_1) # harmonized
- centile_scores_4groups_raw <- dplyr::bind_rows(centile_scores_3groups_raw, CN_scores_1) # unharmonized
- ##### 5.3.3.2. plot centile scores in 1 group of LBCC + 3 groups of testing data======== #####
- #### [Figure 4a-b] ######
- # Define color palette for consistency
- mycolors <- c("grey65", "#4059ad", "#97d8c4", "#f4b942")
- ## figure 4A - boxplot of centile scores in 4 groups, without harmonization
- plot_a <- centile_scores_4groups_raw %>%
- # pivot by region for plotting each region in one box
- tidyr::pivot_longer(cols = 6:67, names_to = "region", values_to = "centile") %>%
- arrange(region) %>%
- ggplot(aes(x = region, y = centile, color = DIAGNOSIS, fill = DIAGNOSIS)) +
- geom_boxplot(outlier.size = 0.5, size = 0.3) +
- stat_summary(fun = median, geom = "crossbar",
- width = 1, fatten = 0.4,
- color = "white", size = 1) +
- labs(#title=str_wrap("Boxplot of Fitted Centile Scores by Region and Group with and without CB-Predict Harmonization", width = 60),
- subtitle = "a) Testing Data Not Harmonized",
- x = "Regions 1–62",
- y = "Fitted Centile Scores",
- color = "Group", # Legend title
- fill = "Group") + # Legend title
- facet_wrap(~ DIAGNOSIS, ncol = 4) +
- theme_minimal(base_size = 10, base_family = "Liberation Sans") +
- theme(
- axis.text = element_text(size = 10),
- axis.title = element_text(size = 12),
- plot.title = element_text(size = 16, hjust = 0, vjust=1),
- plot.subtitle = element_text(size = 12,hjust=0),
- legend.text = element_text(size = 10),
- legend.title = element_text(size = 11),
- strip.text = element_text(size = 10),
- axis.text.x = element_blank(),
- panel.grid.minor = element_blank(), # Clean look for publication
- plot.margin = margin(10, 10, 10, 10) # Consistent margins
- ) +
- ylim(0, 1) +
- scale_color_manual(values = mycolors) +
- scale_fill_manual(values = mycolors) +
- geom_hline(yintercept = 0.5, linetype = "longdash", linewidth = 0.8, color = "grey")
- ## figure 4B - boxplot of centile scores in 4 groups, with harmonization
- plot_b <- centile_scores_4groups %>%
- tidyr::pivot_longer(cols = 6:67, names_to = "region", values_to = "centile") %>%
- arrange(region) %>%
- ggplot(aes(x = region, y = centile, color = DIAGNOSIS, fill = DIAGNOSIS)) +
- geom_boxplot(outlier.size = 0.5, size = 0.3) +
- stat_summary(fun = median, geom = "crossbar",
- width = 1, fatten = 0.4,
- color = "white", size = 1) +
- labs(#title = "Boxplot of Fitted Centile Scores by Region and Group with and without CB-Predict Harmonization",
- subtitle = "b) Testing Data Harmonized",
- x = "Regions 1–62",
- y = "Fitted Centile Scores",
- color = "Group", # Legend title
- fill = "Group") + # Legend title
- facet_wrap(~ DIAGNOSIS, ncol = 4) +
- theme_minimal(base_size = 10, base_family = "Liberation Sans") +
- theme(
- axis.text = element_text(size = 10),
- axis.title = element_text(size = 12),
- plot.title = element_text(size = 16, hjust = 0, vjust=1),
- plot.subtitle = element_text(size = 12,hjust=0),
- legend.text = element_text(size = 10),
- legend.title = element_text(size = 11),
- strip.text = element_text(size = 10),
- axis.text.x = element_blank(),
- panel.grid.minor = element_blank(), # Clean look for publication
- plot.margin = margin(10, 10, 10, 10) # Consistent margins
- ) +
- ylim(0, 1) +
- scale_color_manual(values = mycolors) +
- scale_fill_manual(values = mycolors) +
- geom_hline(yintercept = 0.5, linetype = "longdash", linewidth = 0.8, color = "grey")
- # Combine plots using patchwork
- combined_plot <- plot_a / plot_b +
- plot_layout(heights = c(1, 1), guides = "collect") &
- theme(legend.position = "bottom")
- # Print the combined plot
- combined_plot
- ### 5.4 Harmonization improves the comparability of normative scores #######
- #### 5.4.1 Non-parametric test of the differences between ADNI and LBCC CN groups: ####
- # (Wilcoxon rank sum test )
- # we are going to compare the CN in ADNI vs. the CN in LBCC
- centile_scores_2groups_CN <-
- subset(centile_scores_4groups,
- DIAGNOSIS %in% c("LBCC(control)", "ADNI - CN"))
- ##### 5.4.1.1 function for conducting wilcoxon test and calculating effect size ######
- fn_wilcox <- function(datacolumn, datasetname) {
- # build formula
- form <- as.formula(paste(datacolumn, "~ DIAGNOSIS"))
- # run Wilcoxon rank‑sum (two‐sample) test
- wtest <- wilcox.test(form,
- data = datasetname,
- exact = FALSE,
- correct = TRUE)
- # extract W and p
- W <- unname(wtest$statistic)
- p_value <- wtest$p.value
- # group sizes
- grp_tbl <- table(datasetname$DIAGNOSIS)
- n1 <- as.numeric(grp_tbl[1])
- n2 <- as.numeric(grp_tbl[2])
- N <- n1 + n2
- # compute Z and r
- mu_W <- n1 * n2 / 2
- sigma_W <- sqrt(n1 * n2 * (N + 1) / 12)
- Z <- (W - mu_W) / sigma_W
- r <- Z / sqrt(N)
- # Bonferroni‐adjusted significance
- sig <- ifelse(p_value < 0.05/62, "*", "")
- return(list(
- W = round(W, 1),
- Z = round(Z, 3),
- r = round(r, 3),
- p_value = round(p_value, 8),
- sig = sig
- ))
- }
- ##### 5.4.1.2 Conduct the wilcoxon test for each region - harmonized, comparing ADNI.CN vs. LBCC.CN ######
- # prepare result table
- wilcox_table <- data.frame(
- region = colnames(centile_scores_2groups_CN)[6:67],
- W = NA_real_, #Wilcoxon rank sum test stat
- Z = NA_real_, #standardized score
- r = NA_real_, #rank-biserial correlation
- p_value = NA_real_, #p-value
- significance = "" #compared with bonferroni corrected
- )
- # loop through each column
- for (i in seq_along(wilcox_table$region)) {
- colname <- wilcox_table$region[i]
- res <- fn_wilcox(datacolumn = colname,
- datasetname = centile_scores_2groups_CN)
- wilcox_table[i, c("W","Z","r","p_value","significance")] <-
- unlist(res)
- }
- # wilcox_table stores the wilcoxon test statistics
- #print the test statistics:
- # for better visuals, put left and right side to side
- # Combine the two halves column-wise
- cbind(
- wilcox_table[1:31, , drop = FALSE],
- wilcox_table[32:62, , drop = FALSE]
- ) %>%
- knitr::kable("html",
- col.names = c("Region-left", "W", "Z", "r", "P-value", "Significance",
- "Region-right", "W", "Z", "r", "P-value", "Significance")
- )
- ##### 5.4.1.3 Conduct the wilcoxon test for each region - NON-harmonized, comparing ADNI.CN vs. LBCC.CN ######
- centile_scores_2groups_raw <-
- subset(centile_scores_4groups_raw,
- DIAGNOSIS %in% c("LBCC(control)", "ADNI - CN"))
- wilcox_table_raw <- data.frame(
- region = colnames(centile_scores_2groups_raw)[6:67],
- W = NA_real_, #Wilcoxon rank sum test stat
- Z = NA_real_, #standardized score
- r = NA_real_, #rank-biserial correlation
- p_value = NA_real_, #p-value
- significance = "" #compared with bonferroni corrected
- )
- for (i in seq_along(wilcox_table_raw$region)) {
- colname <- wilcox_table_raw$region[i]
- res <- fn_wilcox(datacolumn = colname,
- datasetname = centile_scores_2groups_raw)# the un-harmonized CN groups
- wilcox_table_raw[i, c("W","Z","r","p_value","significance")] <-
- unlist(res)
- }
- # wilcox_table_raw stores the wilcoxon test statistics
- # Print the wilcoxon test statistics:
- # for better visuals, put left and right side to side
- # Combine the two halves column-wise
- cbind(
- wilcox_table_raw[1:31, , drop = FALSE],
- wilcox_table_raw[32:62, , drop = FALSE]
- ) %>%
- knitr::kable("html",
- col.names = c("Region-left", "W", "Z", "r", "P-value", "Significance",
- "Region-right", "W", "Z", "r", "P-value", "Significance")
- )
- #### 5.4.2 visualization of the 2 sets of wilcoxon tests ######
- # Rename columns so harmonized vs unharmonized line up
- colnames(wilcox_table) <- c(
- "outcome",
- "W_harmonized", "Z_harmonized", "r_harmonized",
- "p_value_harmonized", "sig_harmonized"
- )
- colnames(wilcox_table_raw) <- c(
- "outcome",
- "W_unharmonized", "Z_unharmonized", "r_unharmonized",
- "p_value_unharmonized", "sig_unharmonized"
- )
- # Wide‐format join for printing side‑by‑side HTML table
- wilcox_wide <- wilcox_table %>%
- left_join(wilcox_table_raw, by = "outcome") %>%
- select(
- outcome,
- Z_unharmonized, r_unharmonized,
- Z_harmonized, r_harmonized
- )
- # Long format of wilcoxon tables for plotting
- wt_h <- wilcox_table %>%
- select(outcome, Z_harmonized, r_harmonized) %>%
- mutate(source = "harmonized") %>%
- # rename to match
- rename(
- Z = Z_harmonized,
- r = r_harmonized
- )
- wt_u <- wilcox_table_raw %>%
- select(outcome, Z_unharmonized, r_unharmonized) %>%
- mutate(source = "unharmonized") %>%
- # rename to match
- rename(
- Z = Z_unharmonized,
- r = r_unharmonized
- )
- results_long <- bind_rows(wt_h, wt_u)
- #### [Figure 5a] lollipop plots of the rank biseral --------
- # Define color palette
- mycolors <- c("#4059ad", "#f4b942") # For harmonized and unharmonized
- # Lollipop chart
- results_dodged <- results_long %>%
- mutate(
- r_abs = abs(as.numeric(r)),
- dodge_pos = as.numeric(factor(outcome)) +
- ifelse(source == unique(source)[2], 0.2, -0.2) # Manual dodge offset
- )
- # Lollipop chart with vertical stems
- ggplot(results_dodged, aes(x = dodge_pos, y = r_abs, color = source)) +
- geom_segment(aes(xend = dodge_pos, yend = 0),
- linewidth = 0.5) + # Vertical stems from dodged position
- geom_point(size = 2) + # Points at dodged position
- labs(
- #title = "Rank-Biserial Correlation of Two-Sample Wilcoxon Rank Sum Test",
- #subtitle = "Harmonized vs. Unharmonized",
- x = "Regions 1–62",
- y = "Absolute Rank-Biserial Correlation",
- color = ""
- ) +
- publication_theme() +
- theme(
- axis.text.x = element_text(size = 8),
- #axis.text.x = element_blank(),
- #axis.ticks.x = element_blank(),
- legend.position = "bottom"
- ) +
- scale_y_continuous(
- breaks = seq(0, 1, by = 0.1),
- minor_breaks = NULL
- ) +
- scale_color_manual(values = mycolors) +
- scale_x_continuous(breaks = 1:62, labels = 1:62) # Map dodged positions back to regions
- #### [Figure 5b] Plot standardized Z by region -------
- results_long %>%
- ggplot(aes(x = outcome, y = as.numeric(Z),
- color = source, group = source)) +
- geom_point(size = 1) +
- labs(
- title = "Standardized Z by Region",
- subtitle = "Harmonized vs. Unharmonized",
- x = "Region (1–62)",
- y = "Z statistic"
- ) +
- theme(
- axis.text.x = element_blank(),
- axis.ticks.x = element_blank()
- )+
- scale_color_manual(values = c("#4059ad", "#f4b942"))+
- scale_fill_manual(values = c("#4059ad", "#f4b942"))
ComBat-Predict_section1-5_share.R at commit f7341bc, no license · at the source
Overview
- Department of Public Health Sciences Medical University of South Carolina Charleston South Carolina USA
- Brain‐Gene‐Development Lab The Children's Hospital of Philadelphia and Penn Medicine Philadelphia Pennsylvania USA
- Neuroscience Graduate Group, Perelman School of Medicine University of Pennsylvania Philadelphia Pennsylvania USA
- Department of Radiology and Medical Imaging University of Virginia Charlottesville Virginia USA
- Department of Radiology University of Pennsylvania Philadelphia Pennsylvania USA
- Department of Neurology Medical University of South Carolina Charleston South Carolina USA
- Department of Neuroscience Medical University of South Carolina Charleston South Carolina USA
- Department of Psychology University of Cambridge Cambridge UK
- Department of Psychiatry University of Pennsylvania Philadelphia Pennsylvania USA
- Department of Child and Adolescent Psychiatry and Behavioral Sciences The Children's Hospital of Philadelphia Philadelphia Pennsylvania USA
- Lifespan Brain Institute of The Children's Hospital of Philadelphia and Penn Medicine Philadelphia Pennsylvania USA
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
e0a8af655de110997af722800b5a1d4090f35480, 1 July 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
8 files
- R/
comfam.R , R, 547 lines - R/
covfam.R , R, 111 lines - R/
wrappers.R , R, 194 lines - vignettes/
comfam.R , R, 85 lines - vignettes/
comfam.Rmd , R, 151 lines - vignettes/
covfam.R , R, 45 lines - vignettes/
covfam.Rmd , R, 90 lines - README.md, Text, 132 lines
ntustison/CrossLong
6abd90eb1a7c02c7a9e42afcba6daaff553bef10, 11 August 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
68 files
- Abstracts/
adpd.Rmd , R, 60 lines - Birchfield/
dataManipulation.R , R, 81 lines - Birchfield/
measurementErrorModel/ , Stan, 62 linesmeasurementErrorModel.st an - Birchfield/
measurementErrorModel/ , R, 48 linesmeasurementErrorScript.R - Birchfield/
model1/ , Stan, 91 linesmodel1.stan - Birchfield/
model1/ , R, 47 linesmodel1Script.R - Birchfield/
model10/ , Stan, 112 linesmodel10c.stan - Birchfield/
model10/ , Stan, 126 linesmodel10f.stan - Birchfield/
model10/ , R, 87 linesmodel10fScript.R - Birchfield/
model2/ , Stan, 73 linesmodel2.stan - Birchfield/
model2/ , R, 71 linesmodel2Script.R - Birchfield/
model2/ , R, 63 linesmodel2SummaryDisplay.rmd - Birchfield/
model3/ , Stan, 64 linesmodel3.stan - Birchfield/
model3/ , R, 61 linesmodel3Script.R - Birchfield/
model3/ , R, 164 linesmodel3SummaryDisplay.rmd - Birchfield/
model4/ , Stan, 70 linesmodel4.stan - Birchfield/
model4/ , R, 55 linesmodel4Script.R - Birchfield/
model4/ , R, 200 linesmodel4SummaryDisplay.rmd - Birchfield/
model5/ , Stan, 82 linesmodel5.stan - Birchfield/
model5/ , R, 54 linesmodel5Script.R - Birchfield/
model5/ , R, 200 linesmodel5SummaryDisplay.rmd - Birchfield/
model6/ , Stan, 79 linesmodel6a.stan - Birchfield/
model6/ , R, 75 linesmodel6aScript.R - Birchfield/
model6/ , Stan, 71 linesmodel6b.stan - Birchfield/
model6/ , R, 59 linesmodel6bScript.R - Birchfield/
model6/ , Stan, 78 linesmodel6c.stan - Birchfield/
model6/ , Stan, 91 linesmodel6d.stan - Birchfield/
model7/ , Stan, 74 linesmodel7.stan - Birchfield/
model8/ , Stan, 106 linesmodel8a.stan - Birchfield/
model9/ , Stan, 104 linesmodel9a.stan - Manuscript/
JAD/ , R, 37 linesabstract.Rmd - Manuscript/
JAD/ , R, 41 linesacknowledgments.Rmd - Manuscript/
JAD/ , R, 87 linesappendix.Rmd - Manuscript/
JAD/ , R, 138 linesdiscussion.Rmd - Manuscript/
JAD/ , R, 103 linesfloats.Rmd - Manuscript/
JAD/ , R, 74 linesformat.Rmd - Manuscript/
JAD/ , R, 56 linesimagingMethods.Rmd - Manuscript/
JAD/ , R, 150 linesintro.Rmd - Manuscript/
JAD/ , R, 256 linesprocessingMethods.Rmd - Manuscript/
JAD/ , R, 155 linesresults.Rmd - Manuscript/
JAD/ , R, 119 linesstatisticalMethods.Rmd - Manuscript/
JAD/ , R, 206 linesstatisticalMethods2.Rmd - Manuscript/
JAD/ , R, 59 linesstitchManuscript.R - Manuscript/
JAD/ , R, 177 linesstitched2.Rmd - Manuscript/
JAD/ , R, 61 linestitlePage.Rmd - Manuscript/
NeuroImage/ , R, 36 linesabstract.Rmd - Manuscript/
NeuroImage/ , R, 35 linesacknowledgments.Rmd - Manuscript/
NeuroImage/ , R, 88 linesappendix.Rmd - Manuscript/
NeuroImage/ , R, 135 linesdiscussion.Rmd - Manuscript/
NeuroImage/ , R, 76 linesformat.Rmd - Manuscript/
NeuroImage/ , R, 48 linesimagingMethods.Rmd - Manuscript/
NeuroImage/ , R, 123 linesintro.Rmd - Manuscript/
NeuroImage/ , R, 2 linesnotes.Rmd - Manuscript/
NeuroImage/ , R, 243 linesprocessingMethods.Rmd - Manuscript/
NeuroImage/ , R, 143 linesresults.Rmd - Manuscript/
NeuroImage/ , R, 119 linesstatisticalMethods.Rmd - Manuscript/
NeuroImage/ , R, 213 linesstatisticalMethods2.Rmd - Manuscript/
NeuroImage/ , R, 47 linesstitchManuscript.R - Manuscript/
NeuroImage/ , R, 61 linestitlePage.Rmd - Presentations/
Cenc/ , R, 93 linescenc.Rmd - Presentations/
Cenc/ , R, 75 linescencContent.Rmd - Presentations/
Cenc/ , R, 18 linesformat.Rmd - Presentations/
Cenc/ , R, 27 linesmakePresentationCenc.R - Presentations/
SMI2019_Nick/ , R, 18 linesformat.Rmd - Presentations/
SMI2019_Nick/ , R, 27 linesmakePresentationSmi.R - Presentations/
SMI2019_Nick/ , R, 92 linessmi.Rmd - Presentations/
SMI2019_Nick/ , R, 74 linessmiContent.Rmd - README.md, Text, 47 lines
brainchart/Lifespan
4a6faa19b8fb7e11e2173fe11998ab3b87c8c5b4, 21 February 2025Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
22 files
- 100.common-variables.r, R, 20 lines
- 101.common-functions.r, R, 78 lines
- 102.gamlss-recode.r, R, 183 lines
- 200.variables.r, R, 6 lines
- 201.functions.r, R, 11 lines
- 211.data-setup.r, R, 388 lines
- 220.simulation-omega-set
up.r , R, 495 lines - 300.variables.r, R, 86 lines
- 301.functions.r, R, 962 lines
- 310.fitting.r, R, 72 lines
- 320.best-fit.r, R, 95 lines
- 330.bootstrapping.r, R, 58 lines
- 340.bootstrap-merge.r, R, 44 lines
- 350.calc-derived.r, R, 152 lines
- 350.calc-novel.r, R, 122 lines
- 500.plotting-variables.r
, R, 5 lines - 501.plotting-functions.r
, R, 17 lines - 510.plotting.r, R, 540 lines
- 920.calc-novel-wo-subset
-function.r , R, 495 lines - Share/
OriginalModels/ , R, 18 linesexample.r - Share/
tutorial.r , R, 140 lines - README.md, Text, 474 lines
Munchkin-233/ComBat-Predict_eval
f7341bc63f00b5d0380ec087aeaad6ffa22206c9, 5 September 2025Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
2 files
- ComBat-Predict_section1-
5_share.R , R, 996 lines, 5 matches - README.md, Text, 11 lines
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
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://
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/
url = {https://
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/
VL - 47
IS - 8
SP - e70546
SN - 1065-9471
PB - Wiley
DO - 10.1002/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1002/
"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":
"volume": "47",
"issue": "8",
"page": "e70546",
"DOI": "10.1002/
"PMID": "42157534",
"PMCID": "PMC13581068",
"ISSN": "1065-9471",
"publisher": "Wiley",
"URL": "https://
"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 communicationsIn 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 mappingIn 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 neuroscienceIn 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 reportsIn 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 oneIn 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 communicationsIn 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: eLifeIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 4 repositories of the authors' code, each at its verified commit and with its license, 96 scripts, and 5 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:c51ca105a8aaa28c…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
