089-生信单行命令的美,慢慢体会(一)
刘小泽写于19.3.1
最近一直忙毕业论文,感觉过去的一周像是过去的一个月一样,快接近尾声了,知道我的朋友都知道,我英语单词、配音、Peak连续打卡几百天,现在与其称之为打卡,倒不如对我是一种生活方式,是不做就睡不着觉的事情,我也很开心有这样的一个习惯。因此,虽然这段时间很忙,但是推送也在一直坚持写,anyway,等熬过这段时间就爽了~ 今天在火车上用手机闲来无事,一边听着“潘吉Jenny告诉你”(嗯,是我喜欢的英语播客,也推荐给你哦),一边重新复习了下Shell单行命令
写在前面
文本文件想必是日常中完成一个大的生信计算项目,方法绝对不止一种,但最先想到的肯定是最简单的方法,那么有什么方法比以一行命令来的更优雅呢?
神奇的Awk
不止提取某列这么简单
Awk应该是功能最强大的命令行工具了,厉害的大牛一般不需要编程写脚本,awk就是他们的脚本,而且一般都是单行完成,我们没那么厉害,先从基础学起咯,一点点来
awk中一般数字就代表列号(除了0,因为0表示打印整行)
想输出第4列:awk '{print $4} a.gtf'
,这样会直接输出到屏幕,当然可以直接将结果保存到一个文件中,这个操作叫做重定向awk '{print $4} a.gtf' >a.txt
,又或者可以利用管道符号将结果进行下一步操作awk '{print $4} a.gtf' | COMMAND
Awk本身就是一门编程语言,因此也要有输入文件、命令、输出文件。需要注意的是,awk的全部需求都是要写在{}
中,并且是对文件一行一行进行操作。
默认情况下,awk会把文件的每一行按照空格分成几部分(Filed),当然按什么分隔可以人为设定,然后将每一个部分赋给$1, $2,... $N
这样的变量【$N
表示当前行的最后一部分,另外前面也提到了,$0
表示输出整行】,然后默认的操作是print
实际操作
其中有许多实用小操作哦
#下载测试数据
wget https://raw.github.com/nachocab/nachocab.github.io/master/assets/transcriptome.gtf
下载好一个文件后,首先你肯定要看看它包含了什么,于是可能你会用cat
或者head
查看,保证让你看过就想放弃生信~
但任何时候,一旦有放弃的念头,就要想想,世界上这么多人做相似的事情,为什么他们没有放弃?是不是有更好的解决办法?试着解决而不是逃避,这样会发现越来越有趣
less -SN transcriptome.gtf
再看一下,发现是不是好多了?并且还带上了行号?这就是-SN
的作用
【这样就可以上下翻页了,并且shift + g
跳到底部,gg
返回顶部,非常好用】
这个GTF文件有9列(虽然最后一列很长,但它相当重要),意思分别是
chromosome, annotation source, feature type, start, end, score, strand, and phase
其中值得一提的是最后一列phase:它以键值对的形式给出,同一键值对的键与值用空格分隔,不同键值对用分号隔开,有一些键(Key)是可选的,一些键是大部分GTF文件中都有的,值(Value)是用引号包围的【这些信息都可能为以后准确提取大量文本提供帮助】
问题
我么知道编码区有的存在许多外显子(Exon),也有的只有一个。那么现在要从GTF中提取只有一个外显子(exon)的蛋白编码基因,该怎么做?
首先我们要找到基因有哪些:
awk '$3 == "gene"' transcriptome.gtf | head | less -SN
# awk所有的规则制定都在单引号''中完成,其中$3指定第三列,后面==表示要满足什么要求,具体的匹配条件需要用双引号包围,例如“gene”
对第3列进行过滤后,我们只想要过滤后的第9列
awk '$3 == "gene" {print $9}' transcriptome.gtf | head | less -S
# 前面提到了,awk需要三大块:输入内容、运行程序、输入内容。如果不写{}的话,即不指定输出什么内容,awk默认输出全部内容
gene_id
gene_id
gene_id
gene_id
...
但是第9列不是很多键值对组成的吗?这列怎么只有一列gene_id呢?
这里又需要学习一下awk对列(column)的理解【有没有发现,生信中许多知识都是交叉学习的,学这个知识点往往能带出来其他知识,逻辑能力就是这样锻炼的吧】
我们知道,awk默认是按照空白进行分隔。空白包括两种:空格和tab,那么awk是按照什么分隔的呢?看个例子就知道:
echo "1 2 3" | awk '{ print $2 }'
# 2
echo "1\t2\t3" | awk '{ print $2 }'
# 结果为空
虽然awk默认用空格(space)分隔,但是我们也可以进行指定:
echo "1,2,3" | awk -F "," '{ print $2 }'
# 设置按,分隔,结果输出2
echo "1 2 3" | awk -F "\t" '{ print $2 }'
# 设置按\t分隔,结果为空
echo -e "1\t2\t3"" | awk -F "\t" '{ print $2 }'
# 设置按\t分隔,结果为2
看到这里是-F
起的作用,它可以选择Field。看到第二行命令没有输出结果,因为它找不到任何的\t
进行分隔,因此只能将全部内容输出,但全部内容却作为了一整列,我们想输出第二列$2
,因此没有内容,但如果命令将$2
改为$1
,结果就是全部的1 2 3
好了,知道了-F
的重要性以后,我们就知道为什么上面的awk '$3 == "gene" {print $9}' transcriptome.gtf
命令只输出了gene_id
:没有指定分隔符,对整个文件来讲这9列是tab分隔,而在第9列内部,键与值之间又是空格分隔。因此如果不指定分隔符的话,默认就是找空格所在位置,也就是第九列的起始gene_id
处;指定了分隔符以后,就可以按照tab找到真正的第九列
awk -F "\t" '$3 == "gene" { print $9 }' transcriptome.gtf | head | less -S
gene_id "ENSG00000225828.1"; transcript_id "ENSG00000225828.1"; gene_type "protein_coding"; gene_status "KNOWN"; gene_name "FAM229A" ...
刘小泽写于19.3.2
上一次说到了怎样找到特定的列(比如:
$3 == "gene"
以及返回特定的列({print $1}
)。今天继续探究上次的文件
我们看到GTF文件中存在许多的键值对,上一次我们知道了怎么将全部的第九列提取出来:
awk -F '\t' '$3 == "gene" {print $9}' transcriptome.gtf | head | less -S
那么现在,如果想提取第9列中的关于gene_type 信息(也就是第三列),需要在上面代码的基础上用awk再次指定分隔符为;
,接着提取第三列就好
awk -F '\t' '$3 == "gene" {print $9}' transcriptome.gtf |awk -F ';' '{print $3}'| head | less -S
# 得到如下
gene_type "protein_coding"
gene_type "antisense"
gene_type "protein_coding"
gene_type "pseudogene"
gene_type "sense_overlapping"
gene_type "protein_coding"
gene_type "pseudogene"
...
因为存在许多的gene_type,而我们就是想要蛋白编码区(protein_coding)的,于是可以指定筛选条件:【需要注意一点:“protein_coding"的前后引号需要用反斜线进行转义,便于awk识别筛选条件】
awk -F "\t" '$3 == "gene" { print $9 }' transcriptome.gtf | \
awk -F "; " '$3 == "gene_type \"protein_coding\""' | \
head | less -S
或者:如果感觉反斜线的使用不美观或者经常忘记使用,那么也可以用~
表示部分匹配,只要搜索的列中包含你输入的字符,那么就会输出结果
awk -F '\t' '$3 == "gene" {print $9}' transcriptome.gtf | \
awk -F ';' '$3 ~ "protein_coding"' |\
head | less -S
到这里我们已经输出了所有的protein_coding基因,那么之前的问题又来了:
如何输出只包含一个exon的protein_coding基因呢?
这里发现:开始我们选择的提取条件是第三列的gene,但是第三列除了gene还有其他信息,统计一下:
awk '{print $3}' transcriptome.gtf | sort | uniq -c | sort -r
# 有用信息如下
1203 exon
730 CDS
273 UTR
228 transcript
94 start_codon
75 stop_codon
63 gene
最多的还是exon,于是就可以将$3 == "gene"
改成$3 == "exon"
,挑出全部的exon基因名信息(在分号隔开的第5列)
awk -F '\t' '$3 == "exon" {print $9}' transcriptome.gtf | \
awk -F ';' '$3 ~ "protein_coding"' |\
head | less -S
# 得到的结果
1 gene_name "NADK"
2 gene_name "C1orf86"
3 gene_name "RER1"
4 gene_name "SLC2A5"
5 gene_name "LZIC"
6 gene_name "FBXO6"
7 gene_name "NPPA"
8 gene_name "PLOD1"
9 gene_name "FHAD1"
10 gene_name "CLCNKA"
...
# 还有另外一种实现方式(因为发现GTF第9列中既有分号又有空格分隔,还有引号。因此可以直接将分号和引号去掉,然后直接指定分隔符是空格),接下来就可以直接指定第6列为protein_gene时将第10列name打印出来
awk -F '\t' '$3 == "exon" {print $9}' transcriptome.gtf |\
tr -d ";\"" |awk -F ' ' '$6 == "protein_coding" {print $10}'|\ head |less -S
# 结果如此,更加方便下面进行
NADK
C1orf86
RER1
SLC2A5
LZIC
FBXO6
NPPA
PLOD1
FHAD1
这样就得到了exon与protein_gene的对应关系
接下来到了计算每个基因对应的exon出现多少次的时候了,其实就是统计上面的结果
awk -F '\t' '$3 == "exon" {print $9}' transcriptome.gtf | tr -d ";\"" |awk -F ' ' '$6 == "protein_coding" {print $10}' | sort | uniq -c | sort -r >gene_exon_num.txt
# 结果
4 TTN
4 KIF12
3 LAT2
3 ITSN2
3 CBWD6
2 UBXN11
...
然后看看有多少基因是只有一个exon的:
awk '$2 == 1' gene_exon_num.txt | wc -l
刘小泽写于19.3.9 终于搞完了预答辩,想想昨晚熬夜到四点做完了PPT,真是不容易啊,推送继续来,未来的时间会越来越充裕,我也期待继续和大家一起学习😊
今天继续挑一些我个人认为比较重要的单行命令,记录下来
循环少不了
尤其是在处理多条染色体的时候,比如人类1-22条常染色体+X、Y、M染色体
有几个注意的问题:
# 只处理数字的话,例如只处理常染色体
for i in {1..22};do echo $i;done
# 如果再加上其他非数字,例如这样
for i in {1..22,X,Y,M};do echo $i;done
# 得到的结果是不对的,并不是输出1-22以及XYM,而是输出
1..22
X
Y
M
# 如果想同时得到数字和非数字输出,要把非数字内容放大括号外边,并且空格分隔
for i in {1..22} X Y M;do echo $i;done
# 如果想逆序输出,就在大括号中先写大数,后写小数,例如
for i in {5..2} X Y M;do echo $i;done
5
4
3
2
X
Y
M
用cut提取列
我们都知道的用法是:
# 提取1-5以及10-15列(注意中间的逗号)
cut -f1-5,10-15 data
# 还可以指定分隔符(默认tab分隔)
cut -d' ' -f1-5 data
另外,如果想从第5列输出到最后一列,其实我们不需要知道一共有多少列,直接这样:
cut -f5- data
# 注意5后面的-就是表示一直到最后一列
这样普通的用法cut还是很方便的,但是相比awk,cut还是略显简陋,因为不支持自定义列,例如:
# 先提取第5列,然后提取第2列(注意如果操作gtf文件,需要看看有没有头注释信息,有的话需要先去掉)
cat gencode.v28.gtf | grep -v "#" | awk '{print $5"\t"$2}' | head
# 但是用cut就不能实现
cat gencode.v28.gtf | grep -v "#" | cut -f5,2 | head
# 得到的结果还是按照先第2列后第5列排序
多学点awk
假设现在有一个test.txt文件,其中包含了1-22条染色体的基因信息。其中第一列是基因名,第2列是染色体编号。
我们现在想按照test.txt中不同染色体的信息区分开,1号染色体基因信息存放到chr1.txt中,2号染色体的基因信息存放到chr2.txt中…
可能一开始我们想这么做
for i in {1..22};do awk '$2 == $i' test.txt >chr$i.txt;done
但是要知道awk和shell是不融合的,awk中并不知道在shell中定义的$i
因此我们指定寻找$2 == $i
,awk不清楚要找什么,但事实上,我们就是想让awk去根据在shell中设定的变量去引用,只需要加一个参数-v
:
for i in {1..22};do awk -v i=$i '$2 == i' test.txt >chr$i.txt;done
-v
的作用就是在awk中引入变量variable
例如还有:
# 自定义过滤指标(例如要求第四列值大于10)
for i in {1..22}; do awk -v i=$i -v threshold=10 '$2 == i && $4 > threshold' test.txt > chr$i.txt; done
今天时间有限,先发这些吧
刘小泽写于19.3.10
继续学习单行命令
Case1 - awk
如果有一个文件包含两列,一列是尝试(attempt)次数,一列是成功(success)次数,我们想计算总的成功率,也就是自定义输出结果。
逻辑上很简单,需要知道总的尝试次数和总的成功次数,那么怎样获得这两个值呢?也就是说,我们怎样将第一列累加并赋值给attempt,第二列累加值给success?
# 创建一个新的变量就需要先用BEGIN指定,然后再END中输出
awk 'BEGIN { sum_attempt = sum_success = 0; }{ sum_attempt += $1 ; sum_success += $2} END {print "Attempt: ", sum_attempt, "Success: ", sum_success, "Rate: ", sum_success/sum_attempt*100,"%" }' test.txt
这样结果输出的就会是类似这样:
Attempt: 30
Success: 15
Rate: 50%
如果有一个文件,第一行是头信息,我们想去掉第一行,然后只选出来其中的第4列:
awk 'NR>1 && NF == 4' test.txt
其中NR指的是行号(Number of row),NF指的是列号(Number of field)
Case2 - grep
如果我们要找到以“bioplanet”开头的记录,需要使用^
grep '^bioplanet' test.txt
Case3 - sort
如果有一个文件的第2列是染色体编号,第3列是染色体前起始位置,现在我们想先根据染色体编号排序,再按照起始位置排序,就可以利用sort
sort -k 2n -k 3n chr.txt
-k
就是指定按照哪一列进行排序,但是需要注意的是,默认按照ASCII码排序,即100小于11,于是如果要按照真实阿拉伯数字大小排序,需要加上参数-n
Case4 - uniq
如果一个文件中第3列包含许多重复的字符,例如分组信息A、B、C三组,看看每一条记录在哪个分组信息中,最后计算各个分组信息的情况
awk '{print $3}' test.txt | sort | uniq -c
# 这里-c表示count
一般uniq与sort是分不开的,并且是先sort后再uniq
如果现在有两个文件,例如:
$ cat test1.txt
A1BG RXRA
A2M-AS1 GABP
A2M SRF
A4GALT GABP
AAAS CTCF
$ cat test2.txt
ACAT2
A1BG
ACTA1
ACTA2
ADM
AEBP1
我们想找他们的第一列是否有相同的结果
首先就是将这两个文件合并在一起,然后取出第一列,并排序,排序后找共同值【也可以理解成为取两文件某一列的交集 intersection】
$ cat test1.txt test2.txt | awk '{print $1}' | sort | uniq -d >intersection
# 结果就是A1BG
这里看到用了-d
的参数,意思就是只输出出现次数大于1次的行
【如果要取两个文件的并集 union,就是去掉它们共同有的成分,直接把uniq的-d
参数去掉即可】
$ cat test1.txt test2.txt | awk '{print $1}' | sort | uniq >union
A1BG
A2M
A2M-AS1
A4GALT
AAAS
ACAT2
ACTA1
ACTA2
ADM
AEBP1
如果想找在第一个文件中存在而在第二个文件中不存在的行,需要先找交集(上面已经完成了),然后从第一个文件中剔除交集的部分
$ awk '{print $1}' test1.txt | cat - intersection | sort | uniq -u >test1-only
A2M
A2M-AS1
A4GALT
AAAS
注意到cat
后面多了一个-
,意思是将前面awk的标准输出结果存到-
中,方便调用;另外这里用了-u
的参数,意思是只输出出现一次的结果
Case5 - join
这个用法和R中的merge差不多,就是根据两个文件某一列,将共同的行输出,利用join
可以实现,但使用前必须进行sort
# 这里都按第一列进行排序
$ sort -k1 test1 >sort.test1
$ sort -k1 test2 >sort.test2
$ join sort.test1 sort.test2
# 结果就是 A1BG RXRA,可以看到,除了输出了二者第一列的交集A1BG,还输出了后面的内容
join
根据第一列进行融合,如果要指定按照其他列,可以join -j
更多情况是:要比较的两个文件列数不一致,前面例子中都是根据第一列,但现实中可能根据第一个文件的第二列,根据第二个文件的第三列,然后找他们的共同之处,并提取出共同的行
# 首先还是要将文件根据不同的列进行sort
$ sort -k2 a1> sort.a1
$ sort -k3 b1> sort.b1
$ join -1 2 -2 3 sort.a1 sort.b1
这里的-1 2 -2 3
就是指的利用第一个文件的第2列和第二个文件的第3列
来看一个更加贴近实际的例子:
$ cat >exp.bed
chr1 26 39
chr1 32 47
chr3 11 28
chr1 40 49
chr3 16 27
chr1 9 28
chr2 35 54
chr1 10 19
$ cat >exp_lengths.txt
chr1 58352
chr2 39521
chr3 24859
目标:把长度信息贴在bed文件每一行后面
$ sort -k1 exp.bed >sort.exp.bed
$ sort -k1 exp_lengths.txt >sort.exp_lengths.txt
$ join -1 1 -2 1 sort.exp.bed sort.exp_lengths.txt
chr1 10 19 58352
chr1 26 39 58352
chr1 32 47 58352
chr1 40 49 58352
chr1 9 28 58352
chr2 35 54 39521
chr3 11 28 24859
chr3 16 27 24859