logo

# Library
library(pacman)
pacman::p_load(readxl,data.table,easyCODA,compositions,mixOmics,factoextra,dplyr,missForest,caTools,gtools,tidyverse,brms,ggridges,bayesplot,tidybayes,ggmcmc,bayestestR,ggrepel)

# Datasets

AUC<-data.frame(read_xlsx("UHMD-01-20VW DATA TABLE.XLSX",sheet = "Peak Area Data"))
ID<-read_xlsx("UHMD-01-20VW DATA TABLE.XLSX",sheet = "Sample Meta Data")
ANN<-read_xlsx("UHMD-01-20VW DATA TABLE.XLSX",sheet = "Chemical Annotation")

row.names(AUC)<-ID$SUBJECT_OR_ANIMAL_ID[match(AUC$PARENT_SAMPLE_NAME,ID$PARENT_SAMPLE_NAME)]
AUC<-AUC[,-1]

1 Filtering variables with a high number of zeros

# Population assignment

line<-ID$GROUP_NAME[match(rownames(AUC),ID$SUBJECT_OR_ANIMAL_ID)]

# Remove all variables with a percentage of zeros higher than 20% within-population

n.l <- round(length(which(line=="Low line"))*0.2)
n.h <- round(length(which(line=="High line"))*0.2)

zero.col<-data.frame(table(which(is.na(AUC),arr.ind = T)[,2]))
zero.col<-as.numeric(as.character(zero.col$Var1[zero.col$Freq>(n.l+n.h)]))

index.zero<-NULL;n<-0
for(i in zero.col){
  nh<-length(which(is.na(AUC[line=="High line",i])))
  nl<-length(which(is.na(AUC[line=="Low line",i])))
  
  if(nh/nl>0.5 & nh/nl<2){
    n<-n+1
    index.zero[n]<-i
  }
}

indx.l <- which(is.na(AUC[line=="Low line",]),arr.ind = T)
indx.h <- which(is.na(AUC[line=="High line",]),arr.ind = T)
zeros.l <- data.frame(table(indx.l[,2]))
zeros.h <- data.frame(table(indx.h[,2]))

indx.l <- as.numeric(as.character(zeros.l$Var1[which(zeros.l$Freq>=n.l)]))
indx.h <- as.numeric(as.character(zeros.h$Var1[which(zeros.h$Freq>=n.h)]))

col <- unique(c(indx.h[indx.h%in%indx.l],index.zero))

length(col)
## [1] 111
AUC.zero <- AUC[,-col]

2 Removing outliers animals

##Remove all zeros in the dataset
zero.col<-data.frame(table(which(is.na(AUC),arr.ind = T)[,2]))
zero.col<-as.numeric(as.character(zero.col$Var1[zero.col$Freq>0]))
AUC.noZero<-AUC[,-zero.col]

## Principal Component Analysis

pca<-prcomp(AUC.noZero,scale=T)
fviz_pca_ind(pca,geom=c("point","text"),col.ind=line,axes=c(1,2),addEllipses = TRUE,ellipse.level = 0.95,pointsize=1)

indx<-grep("17763|17808",rownames(AUC.noZero))
AUC.noZero<-AUC.noZero[-indx,]

##Repetition PCA

pca<-prcomp(AUC.noZero,scale=T)
fviz_pca_ind(pca,geom=c("point","text"),col.ind=line[-indx],axes=c(1,2),addEllipses = TRUE,ellipse.level = 0.95,pointsize=1)

# Zero imputation

Imputation using the half of minimum value of intensity peak detected per each metabolite.

##Imputation with minimum intensity peak
AUC.zero<-AUC.zero[-indx,]
line<-line[-indx]
line<-gsub(" line","",line)


zero.col<-data.frame(table(which(is.na(AUC.zero),arr.ind = T)[,2]))
zero.col<-as.numeric(as.character(zero.col$Var1[zero.col$Freq>0]))

AUC.imp<-AUC.zero
for(i in zero.col){
  AUC.imp[is.na(AUC.imp[,i]),i]<-min(AUC.imp[,i],na.rm = T)/2
}

3 Selection reference variable

n <- dim(AUC.imp)[2]
cv <- data.frame(Variable=numeric(n),CV=numeric(n))
cv$Variable <- colnames(AUC.imp)
for (i in 1:n) {
  
  cv$CV[i] <- sd(log(AUC.imp[,i])) / mean(log(AUC.imp[,i]))
  cv$Total[i] <- sum(AUC.imp[,i])
  cv$mean[i] <- mean(AUC.imp[,i])
  }

cv <- cv[order(cv$CV,decreasing=F),]
cv$Variable[1]; cv$CV[1]; cv$mean[1]
## [1] "X825"
## [1] 0.01141287
## [1] 16940322
ref <- cv$Variable[1]

indx.ref <- grep(ref,names(AUC.imp))

4 Procrustes analysis

Procrustes analysis was performed to test if the selected reference variable allowed how close they come to the exact geometry. This means that the variables maintain their relationship despite the transformation.

Procrustes analysis is based on three simple operations:

LRA <- LRA(AUC.imp,weight = F)$rowpcoord

ALR <- ALR(AUC.imp,denom = indx.ref)$LR
ALR.PCA <- PCA(ALR,weight=F)$rowpcoord

print(paste("Procruste correlation:",round(protest(LRA,ALR.PCA[,1:ncol(LRA)], permutations=0)$t0,2)))
## [1] "Procruste correlation: 0.99"

5 Additive log-ratio

Calculation of the additive log-ratio of each variable and centering and scaling them.

AUC.ALR <- log(AUC.imp/AUC.imp[,indx.ref])
AUC.ALR <- AUC.imp[,-indx.ref]

AUC.center <- AUC.ALR
for (i in 1:ncol(AUC.ALR)) {
  
  AUC.center[,i] <- AUC.ALR[,i] - mean(AUC.ALR[,i])
  AUC.center[,i] <- AUC.center[,i] / sd(AUC.center[,i])

}

6 Partial Least Square-Discriminant Analysis (PLS-DA)

The PLS-DA tries to extract the latent structures (patterns) that allow explaining a dependent variable (Y; response). With this analysis, we can reduce the dimension of the data and collect the information throughout the maximization of the covariance between the X and Y. In this case, X is the ALR matrix with all KEGG and Y is a vector with the Line/Population of each sample.

#Computing the PLS-DA model with 10 components
plsda <- plsda(AUC.center,line,ncomp=10,scale=F)

#4-fold cross-validation to compute the balance error rate (BER)

set.seed(30)
perf.pls <- perf(plsda, validation = "Mfold",criterion="all",folds = 4,
                 progressBar = F,nrepeat = 100,)

#Optimal number of components

comp <- perf.pls[["choice.ncomp"]][6]

#BER for the number of components selected

err.n <- perf.pls[["error.rate"]]$BER[20+comp]
sd.n <- perf.pls[["error.rate.sd"]]$BER[20+comp]
err.total <- err.n + sd.n

#Variable important prediction (VIP). Contribution of each variable in the classification among lines/populations
vip <- data.frame(vip(plsda),stringsAsFactors = FALSE)

#A VIP higher than 1 was used as a threshold for selecting the variables with the highest contribution in the model

p <- 1
v.select <- vip[vip[,comp]>=p,]
v.ID <- row.names(v.select)

#Iterative process to obtain the best model with the lower BER

err <- 1
comp.cte <- 10

while (err.total<err) {
  
  err <- err.total
  sd.f <- sd.n
  comp.f <- comp
  
  vf<-v.ID
  vip <- data.frame(vip(plsda),stringsAsFactors = FALSE)
  v.select <- vip[vip[,comp] >= p,]
  
  v.ID <- row.names(v.select)
  
  if (length(v.ID) == 1) {
      break
  
    } 
  
  filter <- AUC.center[,names(AUC.center) %in% v.ID]
  
   if (length(v.ID)<comp.cte) {
    
    comp.cte <- length(v.ID)
  
  }
  
  plsda <- plsda(filter,line,ncomp=comp.cte,scale=F)
  set.seed(30)
  perf.pls <- perf(plsda, validation = "Mfold",criterion="all",folds = 4,
                   progressBar = F,nrepeat = 100,)

  
  comp <- perf.pls[["choice.ncomp"]][6]
  err.n <- perf.pls[["error.rate"]]$BER[(comp.cte*2)+comp]
  sd.n <- perf.pls[["error.rate.sd"]]$BER[(comp.cte*2)+comp]
  err.total <- err.n + sd.n
}

In summary, the model with the minimum BER (Balance error rate) for the classification will be the following specifications:

#Optimal number of components in the model

comp.f
## [1] 2
#Balanced error rate of the model

err - sd.f
## [1] 0.04846154
#Standard deviation of the BER

sd.f
## [1] 0.03122238
#Number of variables included in the model

length(vf)
## [1] 15
#Relevant variables for the classification

write.table(names(AUC[grep(paste(vf,collapse = "|"),names(AUC))]),"C:/Users/3ccas/OneDrive - UPV/2020_2021/Metabolomica/Result_final/relevant_metabolites.txt",row.names = T,col.names = T,quote = F,sep="\t")

#Final model

filter <- AUC.center[,names(AUC.center) %in% vf]
line2<-line
line2[line2=="Low"]<-"Resilient";line2[line2=="High"]<-"Non-Resilient"

#PLS-Plot of the final model
color <- c("#31A2AC","#AF1C1C","#2F2F28","#F0EFF0")

if (comp.f > 1) {
  
  plsda <- plsda(filter,line2,ncomp=comp.f,scale=F)

  plotIndiv(plsda,ind.names = TRUE, legend=TRUE,style = "ggplot2",rep.space = "X-variate",
             ellipse = TRUE, centroid=TRUE,title = 'Final PLS-DA model',
             X.label = 'Comp 1', Y.label = 'Comp 2',col = color[1:2],abline=TRUE,xlim = c(-9,9),cex = c(5,5),point.lwd = 1,
             ylim=c(-9,9),
             size.title = rel(2), size.subtitle = rel(2), size.xlabel = rel(2),
             size.ylabel = rel(2), size.axis = rel(1.5), size.legend = rel(2),
             size.legend.title = rel(2.2),
             legend.title = "Population",alpha=1)+ theme_classic()
}

## NULL
#PCA-Plot of final model
pca <- prcomp(filter,scale=F)
fviz_pca_ind(pca,axes=c(1,2),geom = c("point","text"),col.ind=line2,addEllipses = T,palette=color,ellipse.level = 0.95,pointsize = 1)

p<-fviz_pca_ind(pca,axes=c(1,2),geom = c("point","text"),col.ind=line2,addEllipses = T,palette=color,ellipse.level = 0.95,pointsize = 1)+
  geom_point(aes(fill=line2,color=line2,shape=line2))+
  scale_shape_manual(values=c(16,17))+
  scale_color_manual(values =color)+
  scale_fill_manual(values =color) +
  xlim(-9, 9) + ylim (-9, 9)+
  geom_hline(yintercept = 0, colour="#2F2F28", linetype="dashed") + 
  geom_vline(xintercept = 0, colour="#2F2F28", linetype="dashed") + 
  labs(x ="PC1 (38.7%)", y = "PC2 (13.7%)",title = NULL,face="bold",fill="Population",shape="Population",color="Population") + 
  theme_minimal() +
  theme_classic()+
  theme( 
    legend.position="bottom",
    panel.border = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.minor.x = element_blank(),
    axis.text.x = element_text( size = 9, vjust = 1.5),
    axis.text.y = element_text( size = 9, vjust = 0.7))


ggsave("Result_final/pca_relevant.tiff",p, width = 100, height = 80, units = "mm",dpi = 600)

plsda <- plsda(filter,line,ncomp=comp.f,scale=F)
vip <- data.frame(vip(plsda),stringsAsFactors = FALSE)

write.table(vip[,1:comp.f],"C:/Users/3ccas/OneDrive - UPV/2020_2021/Metabolomica/Result_final/VIP.txt",sep = "\t",quote=F)
write.table(filter,"C:/Users/3ccas/OneDrive - UPV/2020_2021/Metabolomica/Result_final/Fullrel_metabolites.txt",row.names = T,col.names = T,quote = F,sep="\t")

6.1 Quality of the model

To check the quality of the model, two tests were performed using a 4-fold Cross-validation 10,000 times specifying the Mahalanobis distance

6.1.1 Confusion matrix

Allow knowing the success rate for the prediction of each rabbit line/population. Percentage of false positive and false negative.

data.RF <- cbind(filter,line)
confusion.total <- matrix(ncol=2,nrow=2,0)
x.total <- NULL
for (i in 1:10000) {
  
  sample = sample.split(data.RF$line, SplitRatio = .70)
  train = subset(data.RF, sample == TRUE)
  test  = subset(data.RF, sample == FALSE)
  dim(train)
  dim(test)
  
  x <- data.frame(table(test$line))
  x.total <- rbind(x,x.total)
  plsda.train <- plsda(train[,-ncol(train)],train$line,ncomp=comp.f,scale=F)
  test.predict <- predict(plsda.train,test[,-ncol(test)],dist = "mahalanobis.dist")
  prediction <- test.predict$class$mahalanobis.dist[,comp.f]
  
  confusion.mat <- get.confusion_matrix(truth = test$line,predicted =prediction )
  confusion.total <- confusion.total + confusion.mat

  }

High <- sum(x.total$Freq[x.total$Var1 == "High"])
Low <- sum(x.total$Freq[x.total$Var1 == "Low"])

confusion.total[1,] <- 100 * confusion.total[1,] / High
confusion.total[2,] <- 100 * confusion.total[2,] / Low

confusion.total
##      predicted.as.High predicted.as.Low
## High           90.4200           9.5800
## Low             0.8225          99.1775

6.1.2 Permutation matrix

Allow computing the spurious rate for the random prediction of each rabbit line/population.

cross.val <- data.RF
permutation.total <- matrix(ncol=2,nrow=2,0)
x.total <- NULL
for (i in 1:10000) {
  
  data.RF$line <- permute(cross.val$line)
  sample = sample.split(data.RF$line, SplitRatio = .70)
  train = subset(data.RF, sample == TRUE)
  test  = subset(data.RF, sample == FALSE)
  dim(train)
  dim(test)
  
  x <- data.frame(table(test$line))
  x.total <- rbind(x,x.total)
  plsda.train <- plsda(train[,-ncol(train)],train$line,ncomp=comp.f,scale=F)
  test.predict <- predict(plsda.train,test[,-ncol(test)],dist = "mahalanobis.dist")
  prediction <- test.predict$class$mahalanobis.dist[,comp.f]
  
  confusion.mat <- get.confusion_matrix(truth = test$line,predicted =prediction )
  permutation.total <- permutation.total + confusion.mat

  }

High <- sum(x.total$Freq[x.total$Var1=="High"])
Low <- sum(x.total$Freq[x.total$Var1=="Low"])

permutation.total[1,] <- 100 * permutation.total[1,] / High
permutation.total[2,] <- 100 * permutation.total[2,] / Low

permutation.total
##      predicted.as.High predicted.as.Low
## High            49.970           50.030
## Low             50.225           49.775

7 Bayesian statistics Analysis

Bayesian statistics were used to compute the relevance/importance of each variable for the differentiation among the rabbit populations. A good introduction to the Bayesian data analysis can find in Blasco, A. Bayesian Data Analysis for Animal Scientists: The Basics. 2017. Springer, Cham.

DOI: https://doi.org/10.1007/978-3-319-54274-4

line<-as.factor(line)
data.RF <- cbind(filter,line)
#Number and position of treatment variables

treat<-1
pT<-ncol(data.RF)

#Number and position of noising variables

noise <- 0
pNoise <- NA

#Number of response variables

nResp <- ncol(data.RF) - noise - treat

7.1 Running the model

prior <- set_prior("normal(0,1)",class="b")
samples <- NULL

set <- ceiling(nResp / 100)

group <- round(nResp / set)  
init = -group+2
end = 0
for (n.set in 1:set) {
    
  init = init+group
  end = end + group
    
  if (n.set == set) {

    end <- nResp
  
    }
    
  brm.equation <- list()
  for (var in init:end) {
  
     brm.equation <- append(brm.equation,(formula(paste(names(data.RF[var]),"~",names(data.RF)[pT],collapse = ""))))

     }

  eq.multiple <- mvbf(bf(formula(paste(names(data.RF[init-1]),"~",names(data.RF)[pT],collapse = ""))),flist = brm.equation,rescor = F)

  model <- brm(eq.multiple,
           data = data.RF, 
           family = gaussian(), iter = 50000, chains = 4, warmup = 1000,thin=10 ,control = list(adapt_delta = 0.99),
           silent=2,refresh=0,backend = "cmdstanr",
           threads = threading(7))
  
  conver<-data.frame(summary(model)$fixed)
  conver$convergence<-0
  conver$convergence<-ifelse(conver$Rhat<1.05 &conver$Rhat>0.95,"OK","FAIL")
  
  modelfit= as.data.frame(fitted(model,
                               newdata = expand.grid(line= levels(data.RF[,pT])), re_formula = NA, summary = FALSE))

  if (n.set == 1) {
    conver.tmp<-conver
    model.fit<-modelfit
    
    } else {
      
    conver.tmp<-rbind(conver.tmp,conver)
    model.fit<-cbind(model.fit,modelfit)
  
    }
}
## Running MCMC with 4 sequential chains, with 7 thread(s) per chain...
## 
## Chain 1 finished in 247.5 seconds.
## Chain 2 finished in 212.6 seconds.
## Chain 3 finished in 227.2 seconds.
## Chain 4 finished in 219.1 seconds.
## 
## All 4 chains finished successfully.
## Mean chain execution time: 226.6 seconds.
## Total execution time: 908.4 seconds.

7.2 Analyzing the differences among populations

We computed the differences among the average of the mcmc chains of the variables for each population. The difference was calculated substrating the values of the chains from the high population to the low population (Low - High). The result showed the mean difference among the populations as differences in standard deviation. Moreover, we calculated the probability of the difference to be higher or lower than zero (P0). The highest posterior density interval of 95% was also computed. The most relevant variables were the variables with a P0 higher than 0.9 (90%) and a mean difference higher than 0.5 of SD.

print(paste("Convergence not reached:",which(conver.tmp$convergence=="FAIL"),sep=" "))
## [1] "Convergence not reached: "
colnames(model.fit) <- gsub("1\\.","High.",names(model.fit))
colnames(model.fit) <- gsub("2\\.","Low.",names(model.fit))

Low <- names(model.fit)[grep("Low",names(model.fit))]
High<-names(model.fit)[grep("High",names(model.fit))]

m<-length(Low)
result<-data.frame(Metabolite=character(m),meanDiff=numeric(m),P0=numeric(m),HPD95=numeric(m))

for (i in 1:m) {
  
  result$Metabolite[i] <- gsub("X","",names(data.RF)[i])
  
  sample.diff <- model.fit[,names(model.fit) == Low[i]] - model.fit[,names(model.fit) == High[i]]
  
  hdi <- hdi(sample.diff,ci=0.95)
  
  result$HPD95[i] <- paste("[",round(hdi$CI_low,2),",",round(hdi$CI_high,2),"]",sep = "")
  
  if (mean(sample.diff) > 0) {
    
    result$P0[i] <- 100 * (1 - ecdf(sample.diff)(0))
  
    } else {
    
      result$P0[i] <- 100 * ecdf(sample.diff)(0)
  
      }
  
  result$meanDiff[i] <- mean(sample.diff)

}

result$CHEMICAL_NAME<-ANN$CHEMICAL_NAME[match(result$Metabolite,ANN$CHEM_ID)]
result$SUPER_PATH<-ANN$SUPER_PATHWAY[match(result$Metabolite,ANN$CHEM_ID)]
result$SUB_PATH<-ANN$SUB_PATHWAY[match(result$Metabolite,ANN$CHEM_ID)]

knitr::kable(result, format="markdown")
Metabolite meanDiff P0 HPD95 CHEMICAL_NAME SUPER_PATH SUB_PATH
240 -1.0319548 99.72449 [-1.73,-0.33] 3-(4-hydroxyphenyl)lactate Amino Acid Tyrosine Metabolism
503 -0.6895949 95.87755 [-1.46,0.11] serine Amino Acid Glycine, Serine and Threonine Metabolism
799 0.7116321 96.53061 [-0.07,1.48] betaine Amino Acid Glycine, Serine and Threonine Metabolism
873 0.5143965 89.93367 [-0.29,1.35] thiamin (Vitamin B1) Cofactors and Vitamins Thiamine Metabolism
100000454 -0.9921895 99.55612 [-1.7,-0.26] 5-aminovalerate Amino Acid Lysine Metabolism
100000808 -0.8287358 98.22449 [-1.57,-0.05] cysteine s-sulfate Amino Acid Methionine, Cysteine, SAM and Taurine Metabolism
100001359 0.4758732 87.60204 [-0.33,1.31] aconitate [cis or trans] Energy TCA Cycle
100001619 0.9919303 99.56633 [0.26,1.7] glycerophosphoglycerol Lipid Glycerolipid Metabolism
100001725 0.9294990 99.19898 [0.18,1.66] equol Xenobiotics Food Component/Plant
100001734 -1.0413611 99.69388 [-1.73,-0.32] N6-acetyllysine Amino Acid Lysine Metabolism
100001948 -0.9531145 99.35714 [-1.69,-0.21] succinylcarnitine (C4-DC) Energy TCA Cycle
100008954 -0.6904967 96.00510 [-1.47,0.08] palmitoyl dihydrosphingomyelin (d18:0/16:0)* Lipid Dihydrosphingomyelins
100015832 -1.2512270 99.95408 [-1.9,-0.6] behenoylcarnitine (C22)* Lipid Fatty Acid Metabolism (Acyl Carnitine, Long Chain Saturated)
100015833 -1.0979451 99.79592 [-1.82,-0.41] arachidoylcarnitine (C20)* Lipid Fatty Acid Metabolism (Acyl Carnitine, Long Chain Saturated)
100020377 -1.0773958 99.79592 [-1.78,-0.37] ethyl beta-glucopyranoside Xenobiotics Food Component/Plant
write.table(result,"C:/Users/3ccas/OneDrive - UPV/2020_2021/Metabolomica/Result_final/result_bayes_relevant.txt",row.names = F,quote=F, sep="\t")

7.3 Volcano plot

Volcano plot was made using the posterior mean of the differences among the marginal distribution of the rabbit populations.

#VIP of each variable
result$VIP<-vip$comp1[match(paste0("X",result$Metabolite,sep=""),rownames(vip))]


mycolors <- c("#31A2AC","#AF1C1C","#2F2F28")
names(mycolors) <- c("Non-Resilient", "Resilient", "No differences")

result$diffexpressed <- "No differences"

# if meanDiff > 0.5 and pvalue < 0.05, set as "Resilient" 
result$diffexpressed[result$meanDiff >= 0.5 & result$VIP >= 1] <- "Resilient"
# if meanDiff < -0.5 and pvalue < 0.05, set as "Non-Resilient"
result$diffexpressed[result$meanDiff <= -0.5 & result$VIP >= 1] <- "Non-Resilient"


result$delabel<-result$CHEMICAL_NAME
result$delabel[!paste0("X",result$Metabolite,sep="")%in%vf]<-NA

volcano<-ggplot(data=result, aes(x=meanDiff, y=VIP,col=diffexpressed,label=delabel)) + geom_point(size=1)+
  theme_classic()+
  scale_colour_manual(values = mycolors)+
  geom_vline(xintercept=c(-0.5,0.5), col="#2F2F28",linetype="dashed",size=0.3) +
  geom_hline(yintercept=1, col="#2F2F28",linetype="dashed",size=0.3)+
  labs(x ="Posterior mean differences", y = "VIP")+
  xlim(-2,2)+
  ylim(0,2)+
  theme(legend.title = element_blank(),legend.position = c(0.80,0.9),
        axis.text.x = element_text( size = 9, vjust = 1.5),
        axis.text.y = element_text( size = 9, vjust = 0.7))+
  geom_text_repel(size=3,max.overlaps = 10,    box.padding = unit(0.1, "lines"),
    point.padding = unit(0, "lines"))


volcano

ggsave("Result_final/volcano_relevant.tiff",volcano, width = 120, height = 110, units = "mm",dpi = 600)