如何构建表达矩阵——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,多一点数据认知,让他们的科研上一个台阶:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容