如何構建表達矩陣——RNAseq中游分析

中游分析這個詞是我杜撰的,用來強調表達矩陣構建過程并不簡單。

0 前言

前幾天Jimmy老師發了一篇 我用這個技能一杯咖啡的功夫就掙了800塊錢,講了他幫一個粉絲從公共數據庫中下載RNAseq原始數據,走完上游分析拿到表達矩陣的過程。我看到文章可高興了,因為我也能掙這800塊錢(其實是幫老板省這800塊錢)。

這個流程我認為可以劃分為兩大部分:

1、從獲取原始數據,中間經歷過濾、比對,到featureCounts統計基因上的reads數,這些都需要在服務器上操作,是傳統意義上的上游流程(如果用解剖學的命名習慣,我給它取個名字叫“固有上游分析 upstream analysis proper”)。

2、從reads數統計的結果,經過表達矩陣構建、基因ID轉換、去冗余ID、表達量單位轉換,最終拿到可靠的表達矩陣,這些過程需要在R中完成,屬于下游流程的開頭。但是有很多人是不會這部分內容的,他們的下游分析都直接從表達矩陣開始,然后走差異分析、富集分析等等。于是為了方便區分,我就把差異分析開始往后的流程稱作“固有下游分析 downstream analysis proper”,然后把中間這段不三不四的分析流程稱做“中游分析 midstream analysis”。

固有上游分析平時都是我們實驗室的另一位同學做的,我們倆各有分工,所以我就不總結了。我就來說說常常被人忽視的中游分析流程,這個部分并沒有大多數人想象中的那么簡單,其中暗藏了很多玄機~



  • 在正式進入中游分析之前,我們先來回顧一下上游分析的最后一步——featureCounts(好像也有人把bam文件作為上下游的分界,把featureCounts劃入下游,不過這都不重要hhh)

1 featurecounts

1.1 分解步驟

  • featurecounts這一步需要在服務器上完成。我們在RNAseq上游分析時創建了一個總的項目文件夾,假設把它命名叫RNAseq/。在上游分析時還創建了一系列子文件夾,包括fastq/、align/、featurecounts/等

  • 首先cd進入空文件夾featurecounts。

    cd RNAseq/featurecounts/

  • featurecounts的輸入是bam文件,存放在../align文件夾下。我們臨時將其cp復制到本文件夾,做完分析后再刪去。

    cp ../align/*sorted.bam ./

  • 先設置好基因注釋文件gtf所在的位置,賦值給gtf_address這個變量

  • 讀取所有bam文件,其文件名讀入變量id中,利用循環進行批量操作

    ls *bam |while read id;
    do
    ? ...;
    done

  • 其中的核心操作(也就是上面...的位置)為:用featurecounts統計落入每個基因的reads數,也就是以count為單位統計表達量

    featureCounts
    -T 36 -p -t exon -g gene_id
    -s 1
    -a $gtf_address
    -o $(basename $id "_trimmed.fq.gz.hisat.sorted.bam").counts.txt
    $id

    • -T 線程數

    • -t 基因區域的選擇:一般為exon,表示只統計外顯子區域的reads數;有特殊需求時可以用gene,表示統計整個基因上的reads數

    • -s 正負鏈:默認為0,代表不區分正負鏈;1代表stranded;2代表reverse stranded

    • -a gtf文件所在的位置

    • -o 輸出文件存放的位置及命名

      • 去掉原來bam文件id的后綴_trimmed.fq.gz.hisat.sorted.bam,只留下這之前的簡單文件命名

        basename $id "_trimmed.fq.gz.hisat.sorted.bam"

      • 在上述的簡單文件命名后加上.counts.txt,作為輸出文件的后綴

        $(...).counts.txt

    • $id 每次操作的對象,即bam文件的地址

  • 循環操作完成后,刪除本文件夾下的bam文件

1.2 完整腳本

cd RNAseq/featurecounts/
cp ../align/*sorted.bam ./

gtf_address=/data/reference/annotation/hg38/gencode.v32.chr_patch_hapl_scaff.annotation.gtf
ls *bam |while read id;
do
    featureCounts \
            -T 36 -p -t exon -g gene_id \
        -s 1  \
        -a $gtf_address \
            -o $(basename $id "_trimmed.fq.gz.hisat.sorted.bam").counts.txt \
        $id;
done

rm *sorted.bam
  • 在本文件夾內創建一個文件featurecounts.bash,把上面的腳本保存起來。

  • 運行腳本,即可得到結果

    bash featurecounts.bash

1.3 后續銜接

  • 之后的操作都在本地電腦上用R語言進行了
  • 在本地也建一個總的文件夾,假設它叫RNAseq/。創建三個子文件夾,分別叫01_featurecounts/、02_summary/和03_gene_expression/。
  • 將featurecounts得到的.counts.txt文件下載到01_featurecounts/文件夾中
  • 將featurecounts得到的.counts.txt.summary文件下載到02_summary/文件夾中
  • 以下開始的操作都在03_gene_expression/文件夾中進行。

2 構建表達矩陣

  • 打開03_gene_expression/文件夾中的R工程01_R_Start.Rproj,并打開R腳本02_gene_expr.R。

2.1 讀取數據

#設置數據所在的文件夾目錄
dir <- "../01_featurecounts/"
#獲取該文件夾下的所有文件名,賦值給files_total,得到多少一個向量
files_total <- list.files(path = dir)
#檢查一下將要讀入的文件對不對
files
#讀入第一個文件,調試一下是否正確
x <- files[1]
tmp <- read.table(file = file.path(dir,x), header = T,comment.char = "#")[,c(1:7)]
head(tmp)

#若正確無誤,則利用lapply批量讀入txt文件
expr <- lapply(files,
               function(x){
                 tmp <- read.table(file = file.path(dir,x), header = T,comment.char = "#")[,c(1:7)]
                 return(tmp)
               })
#得到的expr此時是一個列表

2.2 整理數據

#將列表轉為數據框
df <- do.call(cbind, expr)
#是否去除NA值需要根據自己的需求決定
df <- na.omit(df)
#去除數據框的冗余部分
df <- df[,c(1:6,seq(7,ncol(df),by=7))] 
#將行名設為 ensembl ID
rownames(df) <- df[,1]
#去掉 ensembl ID 的版本號
df$Geneid=unlist(strsplit(df$Geneid,"[.]"))[seq(from=1,to=2*nrow(df),by=2)]
#將未化簡的 ensembl ID 記錄下來,賦值給ensembl列
df$ensembl=rownames(df)
#根據實驗設計,將樣本所在列的列名改為自己容易理解的名字
sample_name=c("control1","control2","treat1","treat2") #### #樣本名
colnames(df)[7:(6+length(files))]= sample_name
#檢查一下數據框df
colnames(df)
head(df)
#無誤后賦值給count,保存為Rdata文件
count=df
save(count,file = "03_original_count.Rdata")

2.3 完整腳本

  • 標注####的地方需要根據項目情況自行修改
  • 標注###的地方可以根據項目需求選擇不執行
dir <- "../01_featurecounts/"
files <- list.files(path = dir)
files
x <- files[1]
expr <- read.table(file = file.path(dir,x), header = T,comment.char = "#")[,c(1:7)]
expr <- lapply(files,
               function(x){
                 expr <- read.table(file = file.path(dir,x), header = T,comment.char = "#")[,c(1:7)]
                 return(expr)
               })
df <- do.call(cbind, expr)
df <- na.omit(df) ###
df <- df[,c(1:6,seq(7,ncol(df),by=7))] 
rownames(df) <- df[,1]
df$Geneid=unlist(strsplit(df$Geneid,"[.]"))[seq(from=1,to=2*nrow(df),by=2)]
sample_name=c("control1","control2","treat1","treat2") #### #樣本名
colnames(df)[7:(6+length(files))]= sample_name
colnames(df)
head(df)
count=df
save(count,file = "03_original_count.Rdata")

3 ID處理

3.1 ID轉換

#載入Y數的包和上一步保存的數據
library(clusterProfiler)
rm(list=ls())
load("03_original_count.Rdata")
df=count

#根據自己的物種,選擇對應的數據庫
OrgDb="org.Hs.eg.db"  
#將 ensembl ID 轉換為其他幾種常用的ID
id=bitr(df$Geneid,fromType = "ENSEMBL",toType = c("SYMBOL","GENENAME","ENTREZID"),OrgDb = OrgDb )
colnames(df)
df.id=merge(id,df,by.x="ENSEMBL",by.y="Geneid")

3.2 去除重復ID

  • 重復ID產生的原因有兩個:1、gtf中有一些基因存在多個ensembl ID;2、用clusterProfiler進行ID轉換的時候,會有一個ENSEMBL對應于多個SYMBOL的情況
  • 因此,到此時ENSEMBL和SYMBOL都存在冗余的情況,需要去除重復ID
library(dplyr)

#先給每一列加上一個獨一無二的索引
df.id$number=paste("No",1:nrow(df.id),sep="_")
rownames(df.id)=df.id$number

#將表達矩陣取出
col=10:(ncol(df.id)-2)
expr=df.id[,col]
#將SYMBOL和ENSEMBL以及索引取出
data=data.frame(symbol=df.id$SYMBOL,ensembl=df.id$ENSEMBL,number=df.id$number)
#將ID、索引和表達矩陣合并成一個數據框
data=cbind(data,expr)

#去除重復ID
#對于同一樣本,同一基因的不同索引所對應的表達量很可能不同。一般來說,我們只保留表達量最大的索引。特殊情況下,也會選擇保留表達量的平均值,或者甚至將重復的索引全部刪去。
rm_dup=data %>% group_by(symbol) %>% summarise_all(max) %>% group_by(ensembl) %>% summarise_all(max) 
rm_dup=data.frame(rm_dup)

#檢查一下是否還存在重復ID
rownames(rm_dup)=rm_dup$ensembl
rownames(rm_dup)=rm_dup$symbol

#重新構建無冗余ID的表達量數據框
col1=c(1:9,ncol(df.id))
tmp1=df.id[,col1]
col2=3:ncol(rm_dup)
tmp2=rm_dup[,col2]
rmdup_df=merge(tmp1,tmp2,by="number")
rownames(rmdup_df)=rmdup_df$SYMBOL
save(rmdup_df,file="04_rmdup_count.Rdata")

3.3 完整腳本

library(clusterProfiler)
rm(list=ls())
load("03_original_count.Rdata")
df=count
OrgDb="org.Hs.eg.db" #### #根據物種選擇數據庫
id=bitr(df$Geneid,fromType = "ENSEMBL",toType = c("SYMBOL","GENENAME","ENTREZID"),OrgDb = OrgDb )
colnames(df)
df.id=merge(id,df,by.x="ENSEMBL",by.y="Geneid")
df.id$number=paste("No",1:nrow(df.id),sep="_")
rownames(df.id)=df.id$number
library(dplyr)
col=10:(ncol(df.id)-2)
expr=df.id[,col]
data=data.frame(symbol=df.id$SYMBOL,ensembl=df.id$ENSEMBL,number=df.id$number)
data=cbind(data,expr)
rm_dup=data %>% group_by(symbol) %>% summarise_all(max) %>% group_by(ensembl) %>% summarise_all(max)
rm_dup=data.frame(rm_dup)
rownames(rm_dup)=rm_dup$ensembl
rownames(rm_dup)=rm_dup$symbol
col1=c(1:9,ncol(df.id))
tmp1=df.id[,col1]
col2=3:ncol(rm_dup)
tmp2=rm_dup[,col2]
rmdup_df=merge(tmp1,tmp2,by="number")
rownames(rmdup_df)=rmdup_df$SYMBOL
save(rmdup_df,file="04_rmdup_count.Rdata")

4 去除線粒體基因

  • 線粒體基因的reads數往往非常多,可能會影響到單位轉換,所以我們把它和染色體基因分開。
rmMit=rmdup_df[rmdup_df$Chr!="chrM",]
Mit=rmdup_df[rmdup_df$Chr=="chrM",]
save(rmMit,Mit,file="05_rmMit_count.Rdata")

5 單位變換

  • 關于各種表達量單位的意義和換算公式,這里不做闡述,僅給出計算方法。

5.1 獲取測序深度

rm(list=ls())
#設置待讀取文件的目錄
dir <- "../02_summary/"
#獲取待讀取文件的文件名
files <- list.files(path = dir)
files
#讀取第一個文件調試一下
x <- files[1]
#獲取featurecounts統計到的reads數
tmp <- read.table(file = file.path(dir,x), header = T)[1,2] 
tmp
#利用循環讀取各個文件的測序深度,保存在向量depth中
depth=c()
for (x in files){
  depth <- c(depth,read.table(file = file.path(dir,x), header = T)[1,2])
}
depth

5.2 計算表達量

#載入數據
load("05_rmMit_count.Rdata")
data=rmMit
#把基因信息和表達矩陣分開
info=data[,1:10]
count=data[,11:ncol(data)]
#count轉cpm:除以測序深度,乘以10的6次方
cpm=as.data.frame(t(t(count)/depth)*1e6)
#cpm轉rpkm:除以基因長度,乘以10的3次方
rpkm=cpm/info$Length*1e3
#rpkm轉tpm:除以各樣本的rpkm列和,乘以10的6次方
tpm=as.data.frame(t(t(rpkm)/colSums(rpkm))*1e6)
#tpm轉z-score:減去列平均數,除以列內標準差
z=as.data.frame(t(scale(t(tpm))))
#保存結果
save(info,count,depth,cpm,rpkm,tpm,z,file="06_unit_transform.Rdata")
#輸出rpkm表達量信息
rpkm_df=cbind(info,rpkm)
write.csv(rpkm_df,file="07_RPKM_matrix.csv",row.names = F)
#輸出tpm表達量信息
tpm_df=cbind(info,tpm)
write.csv(tpm_df,file="08_TPM_matrix.csv",row.names = F)

5.3 完整腳本

rm(list=ls())
dir <- "../02_summary/" 
files <- list.files(path = dir)
files
x <- files[1]
tmp <- read.table(file = file.path(dir,x), header = T)[1,2] 
tmp
depth=c()
for (x in files){
  depth <- c(depth,read.table(file = file.path(dir,x), header = T)[1,2])
}
depth
load("05_rmMit_count.Rdata")
data=rmMit
info=data[,1:10]
count=data[,11:ncol(data)]
cpm=as.data.frame(t(t(count)/depth)*1e6)
rpkm=cpm/info$Length*1e3
tpm=as.data.frame(t(t(rpkm)/colSums(rpkm))*1e6)
z=as.data.frame(t(scale(t(tpm))))
save(info,count,depth,cpm,rpkm,tpm,z,file="06_unit_transform.Rdata")
rpkm_df=cbind(info,rpkm)
write.csv(rpkm_df,file="07_RPKM_matrix.csv",row.names = F)
tpm_df=cbind(info,tpm)
write.csv(tpm_df,file="08_TPM_matrix.csv",row.names = F)

6 最終結果

  • 中游分析到此結束,輸出以RPKM和TPM為單位的表達矩陣。以symbol為主ID,同時帶有ensembl和entrez ID,并有基因名、所在染色體及起始終止位置、正負鏈、長度等信息。number列沒有意義,僅為一個無重復的編號,可用于定位唯一的行。
  • 如果用R繼續進行固有下游分析,則load("06_unit_transform.Rdata") ......
  • 下面是最終的完整代碼:
options(stringsAsFactors = F)
rm(list=ls())

# read in data
if(T){
  dir <- "../01_featurecounts/" 
  files <- list.files(path = dir)
  files
  x <- files[1]
  tmp <- read.table(file = file.path(dir,x), header = T,comment.char = "#")[,c(1:7)]
  head(tmp)
  expr <- lapply(files,
                 function(x){
                   tmp <- read.table(file = file.path(dir,x), header = T,comment.char = "#")
                   return(tmp)
                 })
}

# make count matrix
if(T){
  df <- do.call(cbind, expr)
  # df <- na.omit(df) ### #根據情況選擇是否去除NA值
  df <- df[,c(1:6,seq(7,ncol(df),by=7))] 
  rownames(df) <- df[,1]
  df$Geneid=unlist(strsplit(df$Geneid,"[.]"))[seq(from=1,to=2*nrow(df),by=2)]
  df$ensembl=rownames(df)
  
  sample_name=c("control1","control2","treat1","treat2") #### #根據具體情況設置樣本名
  colnames(df)[7:(6+length(files))]= sample_name
  colnames(df)
  head(df)
  count=df
  save(count,file = "03_original_count.Rdata")
}

# id transform
if(T){
  library(clusterProfiler)
  rm(list=ls())
  load("03_original_count.Rdata")
  df=count
  OrgDb="org.Hs.eg.db" #### #根據物種選擇數據庫
  id=bitr(df$Geneid,fromType = "ENSEMBL",toType = c("SYMBOL","GENENAME","ENTREZID"),OrgDb = OrgDb )
  colnames(df)
  df.id=merge(id,df,by.x="ENSEMBL",by.y="Geneid")
  df.id$number=paste("No",1:nrow(df.id),sep="_")
  rownames(df.id)=df.id$number
}

# remove duplicated id 
#每個樣本各自按重復ID的表達量最大值去重
if(T){
  library(dplyr)
  col=10:(ncol(df.id)-2)
  expr=df.id[,col]
  data=data.frame(symbol=df.id$SYMBOL,ensembl=df.id$ENSEMBL,number=df.id$number)
  data=cbind(data,expr)
  rm_dup=data %>% group_by(symbol) %>% summarise_all(max) %>% group_by(ensembl) %>% summarise_all(max) 
  rm_dup=data.frame(rm_dup)
  rownames(rm_dup)=rm_dup$ensembl #查重
  rownames(rm_dup)=rm_dup$symbol  #查重
  
  col1=c(1:9,ncol(df.id))
  tmp1=df.id[,col1]
  col2=3:ncol(rm_dup)
  tmp2=rm_dup[,col2]
  rmdup_df=merge(tmp1,tmp2,by="number")
  rownames(rmdup_df)=rmdup_df$SYMBOL
  save(rmdup_df,file="04_rmdup_count.Rdata")
}

# remove mitochondria gene
if(T){
  load("04_rmdup_count.Rdata")
  rmMit=rmdup_df[rmdup_df$Chr!="chrM",]
  Mit=rmdup_df[rmdup_df$Chr=="chrM",]
  save(rmMit,Mit,file="05_rmMit_count.Rdata")
}

# read in sequencing depth
if(T){
  rm(list=ls())
  dir <- "../02_summary/" 
  files <- list.files(path = dir)
  files
  
  x <- files[1]
  tmp <- read.table(file = file.path(dir,x), header = T)[1,2] #featurecounts統計到的reads數
  tmp
  
  depth=c()
  for (x in files){
    depth <- c(depth,read.table(file = file.path(dir,x), header = T)[1,2])
  }
  depth
}

# expression unit transform
if(T){
  load("05_rmMit_count.Rdata")
  data=rmMit
  info=data[,1:10]
  count=data[,11:ncol(data)]
  
  cpm=as.data.frame(t(t(count)/depth)*1e6)
  rpkm=cpm/info$Length*1e3
  tpm=as.data.frame(t(t(rpkm)/colSums(rpkm))*1e6)
  z=as.data.frame(t(scale(t(tpm))))
  save(info,count,depth,cpm,rpkm,tpm,z,file="06_unit_transform.Rdata")
}

# output expression matirx
if(T){
  rpkm_df=cbind(info,rpkm)
  write.csv(rpkm_df,file="07_RPKM_matrix.csv",row.names = F)
  
  tpm_df=cbind(info,tpm)
  write.csv(tpm_df,file="08_TPM_matrix.csv",row.names = F)
}

# downstream analysis
if(F){
  load("06_unit_transform.Rdata")
  ... #此處略,參見各種固有下游分析教程
}

7 后記

Jimmy老師一杯咖啡搞定的事,我寫了整整兩天才完全搞定,之后把代碼整理注釋好也花了不少時間。但我覺得這個自主消化的過程特別值,因為下次我也能用一杯咖啡的時間搞定了,800塊錢呢~

8 文末友情宣傳

感謝Jimmy老師的指導,強烈建議你推薦給身邊的博士后以及年輕生物學PI,多一點數據認知,讓他們的科研上一個臺階:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容