单细胞kallisto传统流程
kallisto/bustool/scanpy流程
通常,10X的上游都是cellranger,但是这玩意消耗大,速度慢,而且定制化程度很高,cellranger虽然是集成的STAR的流程,但是下游结果文件固定,参数也很多。个人用起来太重量级了。因此,有了某些大佬,决定针对scRNA的实际需求,改进了kallisto工具,并结合高端的bustool,因此就产生了这个上游kallisto+bustools,下游scanpy+其他高级可视化的workflow。
Node1.data
10X官网下载scRNA的测序数据即可,只需要输入一些基本信息,即可随意下载,根据自己的电脑配置下载,我这里下载的是1K PBMC的,大了放不下,最近动漫下多了。下载数据没什么特别的,但是需要注意,文库版本是哪个版本的,10X的V2文库和V3文库差别很大,而且这个参数后面需要。
Node2.Ref
kallisto是把fastq与转录组参考相对比,因此需要的并不是传统的转录组下载基因组的参考,而是需要去ensembl下载转录组的文件,注意,转录组,转录组,转录组,几个文件注意版本对应分别为:
1 | Homo_sapiens.GRCh38.cdna.all.fa.gz #这个是cDNA的,也就是转录本参考 |
借助一个叫kb-python的warpper,可以很容易的使用kallisto和bustool的联合,这里贴出官方的参数:
1 | $ kb ref |
特别注意,如果在一开始加了-d参数,那么会从一个在线存储里面下载预先已经建立好的index,然而不出所料是下载不了的。根据后面的教程,如果要走standard的单细胞流程,需要2个文件,一个idx文件,就是索引,和一个t2g文件,transcripts-to-capture,就是一个基因注释文件,大概长这样,理论上这东西直接自己提取也不是不行……
1 | NST00000456328.2 ENSG00000223972.5 DDX11L1 |
还要注意,里面有个workflow参数,是用来指定对应流程的,这个教程先不考虑其他,直接走最经典的单细胞转录组流程,那么这个参数可以不动,默认就是standard,但是,如果准备走最早的velocity流程,那么需要使用lamanno,如果需要走单细胞核测序的RNA velocity(就是scVelo),需要使用nucleus,而要走10X的Feature Barcoding(测表面蛋白)的话,则需要使用kite。
这里我们走经典转录组,而且不需要太复杂,因此脚本可以为stdindex.sh
1 |
|
运行大概持续了15min,速度还是相当快的,而且结果和直接下载得到的cdna.all.fa和idex文件基本一致,当然因为版本原因大小有一点区别的还是。
这里需要注意,如果我们下载了cdna.all.fa文件,最后输出的时候如果同文件夹下存在的话,会跳过这一步的输出。最后idx大小大概是3G多,g2f文件10多M,cDNA文件400多M。
所以,这个就是经典scRNA-seq流程的准备文件了,下面就是准备进入分析流程了。
Node3.count
这一步仍然是利用kb-python来进行比对和计数,由于kb-python的封装,这一步之后就能直接出所有的下游需要的文件了。以及…其实kb-python本身也就只有2个外部接口罢了。
这里也给出kb count的参数,可以看到,这里多了很多就是了
1 | usage: kb count [-h] [--tmp TMP] [--keep-tmp] [--verbose] -i INDEX -g T2G -x |
同样,可以使用stdcount.sh来进行这一步:
1 |
|
这里其实挺奇怪的,如果用过cellranger就能知道,用cellranger进行每个lane比对的时候是需要I1,R1和R2三个文件才能进行比对的,但是其实kallisto或者单独的STAR solo以及其他一些工具在做这一步的时候反而不需要I1那个文件,只需要R1和R2就行。
先看看这三个文件是什么:
“The source material consists of 6 FASTQ files split into two sequencing lanes L001 and L002, each with three reads of R1 (barcodes), R2 (cDNA sequences), I1 (illumina lane info)”
这三个文件的来源是下机的原始文件经过mkfastq或者illumina的bcl2fastq出来的,那么,需要R1和R2就很容易理解,barcode和cDNA自然是最需要的。但是为什么可以不要lane info?而且,上面的stdcount.sh会一并将lane info文件进入比对流程,但是仍然能够正常比对,尝试去掉lane info文件后再比对,也能比对出来。
但是,如果加入I1文件的话,比对的结果只有600M左右,但不加上I1文件则与官网给出的结果差不多,有1.3G,首先正确性上来说,肯定是不加I1文件是正确的结果,可以先看看这个所谓的lane info是什么:
1 | head pbmc_1k_v3_S1_L001_I1_001.fastq |
首先能看到,基本上全部是一样的东西,而且一个特征是,这个里面是不带有任何序列信息的,唯一长得像的就是比如NCATTACT,GGCAATGG这种了,那么这个8碱基的东西是啥呢。
根据10X V3的建库的官方说明以及提供的Single_Index_Kit_T_Set_A.csv这个文件,这个8碱基的东西应该是所谓的sample_index,一般就是使用的上面那个文件里面提供的官方的sample index序列,当然,自己指定也行,这个东西的作用很简答,就是用来区分样本用的。
根据下游的分析,大致能够判断,将sample index一起放进去比对,对上游比对结果影响不是特别大,当然,是有影响的,比如使用bustools inspect查看比对结果时能发现,将index一起放进比对的结果里面mean reads per barcode少了不少,甚至已经低于V3的标准(2W reads per barcode),理论上结果应当以不加入sample index的结果为准。
而且,从最后聚类的结果也能明显发现,不加sample index的时候,聚类结果和scanpy的官网示例基本一致,都是聚类为0-7,但是加了sample index则变成了0-9,可以认为加了sample index进比对流程的话,会影响比对出来的gene的count数。
总之,最后count的结果,就是output文件夹里的东西,大概有这些:
1 | output |
其中bus文件是比对后通过bustools压缩的文件,而标准的10X流程则只需要counts_unfiltered/filtered中的h5ad文件或者经典三文件即可。
Node4.DownStream-Data Preprocess
那么,继续往下,以不加sample index的结果为准,下游就能进入scanpy或者scater的标准QC过程了,这里以scanpy为主:
1 | import numpy as np |
这里有2个超级坑,其一,是如果使用最新版本的基因组和gtf文件,可能出现有个ensembl-id没有对应gene_name的情况,这很正常,但是进入anndata会导致后续无法分析,而且scannpy又没有提供去除Na或者空值之类的接口,因此,简单起见,如果使用了版本较高的基因组和gtf文件,务必检查前面ref的时候的那个g2f.txt里面又没有缺失或者空值,如果有,务必对anndata里面的var进行处理,我这里用的是一个比较粗暴的办法,直接把缺失设置为unknowngene,后面能够正常进行。
1 | adata.var = adata.var.fillna("unknowngene") |
其二,在设置var的index的时候,必须如下设置:
1 | adata.var.index = adata.var["gene_name"].tolist() |
官网教程上要么是给的干净数据,要么是之前版本的接口,没有对index对象做tolist处理,这样会导致anndata的var.index设置错误,进一步导致后续部分步骤和所有的结果无法保存,而且一旦session关闭,这次处理过后的h5ad文件将无法再被读入python,等同于要重新跑。
继续,读入h5ad之后,就构成了一个叫anndata的对象,这个对象跟R里面的# SummarizedExperiment/Singlecellexperiment对象特别像。根据官方文档,anndata至少有3个部分
anndata.var:主要是基因级别信息,比如上面的gene_name这些。
anndata.obs:主要是cell级别的信息,比如barcode,group这些
anndata.X:核心数据,用于存储稀疏矩阵
比如在最后分析结束后,adata里面有这些东西:
1 | adata |
可以看到,还有一些没说的属性,比如uns,这里存放了一些不能被结构话的数据,比如每个group绘图的时候用什么颜色映射,paga的路径之类的,里面大概长这样:
1 | adata.uns |
而adata.var则是这个样子:
1 | adata.var |
adata.obs则是:
1 | adata.obs |
adata.X则是:
1 | adata.X |
从形式也能看到,var和obs是用DataFrame对象存储的,因此,各种pandas的方法都能直接在上面应用,进行一些统计,而X则是np的array,对其进行操作时则需要用numpy的操作方式。而其他非结构化的数据更多的是代码运行中需要的,对研究来说没有特殊意义。此外,知道了这个之后,我们其实就能够手动向var和obs里面添加信息,比如添加gene的type,染色体的正负链,亦或者是样本的临床信息,病理信息,这样的组合型对象非常灵活,对后续分析帮助很大。
下面继续QC的过程,首先,由于这算是比对后的数据,因此需要先在UMI,barcode级别进行一下质控,本质类似于cellranger跑出来的那个html文件,当然,上面的bustools的部分也完成了一点点质控的部分
1 | fig, ax = plt.subplots(figsize=(7, 7)) |
这里会出来一个这个“Create a plot showing genes detected as a function of UMI counts.”就是一张横轴为UMI数量,纵轴为基因数量的散点图,如果这张图的趋势基本趋于缓和,说明基本覆盖度已经达到饱和了,在增加测序深度也不会有更多的基因覆盖,当然在1K的PBMC数据里,其实数据基本分布于对角线,缓和趋势不是特别明显,示例数据嘛不强求。
然后就是根据UMI的分布估计细胞数目,这个图应该是最为常见的:
1 | expected_num_cells = 1200 #@param {type:"integer"} |
其实这个图可以和上面bustools inspect的结果里面的预估唯一barcode数进行对照来看,如果偏移得很离谱说明数据有问题,可能是index有问题,或者barcode文件有问题,或者就是测序质量特别差导致UMI分布不均匀。不过一般问题不大。
然后需要看看UMI对应最多的基因是什么(等同于表达量最高的基因)
1 | sc.pl.highest_expr_genes(adata, n_top=20) |
这里调用的是scanpy的计算高表达基因的功能,本质上就是计算基因在UMI上的分配情况,可以直接理解为基因的表达量。这里官方给了说明,通常,最容易出现高表达的就是MALAT1,MT基因,RP基因,actin基因,血红蛋白基因,假基因或者ERCC,这是正常的现象。但是当这些基因类型表达高得离谱得时候,比如top30全是某一类基因的时候,就分别与几种异常情况对应了:其中MT基因特别多,说明太多代表细胞存在死亡或者细胞质泄漏的问题,如果ERCC较多说明建库时ERCC加多了技术误差就没法矫正,假基因太多说明比对存在问题或者基因组版本用错了。对于这些基因是否需要先彻底去除后进行分析,目前没有绝对的定论,可以通常是针对细胞进行去除,但是对这些基因本身不会直接去掉。
在上面对UMI和barcode的质控没有问题后,进入常见单细胞最熟悉的环节,开始对细胞和基因进行操作,准备进行下游分析了。
1 | # Removes cells with less than 1070 umi counts |
原文这里是说是根据阈值进行过滤,它说是过滤低于1170umi count的细胞,代码是中关于这个axis=i,我看到的最经典的解释是:下标的变化方向;axis=0就是下标沿着第0轴递归操作,axis=1就是下标沿着第1轴递归操作。
第二个操作的目的很容易懂,就是将UMI为0的基因去除,等同于去掉没有测到的基因。但是第一个操作很有意思,它去掉了低于UMI=1070的细胞,但是这个阈值是从哪里来的呢。
顺便注意,经过下面两个操作,现在adata本身已经被改变了。因此,可以先将原本的矩阵进行一次保存到一个anndata内置的raw属性里面,这样就能在改动矩阵数据的同时保留原本的表达数据。
接下来就开始正式进入“传统”的表达矩阵类似级别的分析了
1 | sc.pp.filter_cells(adata, min_genes=200) |
这里就跟seurat非常像了,先筛选细胞里表达不到200个基因的,再筛选在小于3个细胞中表达的基因说白了就是一个是筛低质量细胞,一个筛低质量基因。
1 | mito_genes = adata.var_names.str.startswith('MT-') |
查看质量绘图,对应seurat里的对应的图。
n_gene代表细胞内的基因数,可以看到基本在2000-4000(其实貌似也就一般……)
nCount可以理解为测序深度(文库大小),后续跟文库normalization有关。
percent.mt就是线粒体基因含量。
再根据上面的数据进行一次过滤
1 | mask = np.logical_or((adata.obs.n_genes < 6500).values, (adata.obs.n_genes > 200).values, (adata.obs.percent_mito < 0.2).values) |
然后进行文库归一化,之前我们看到了,测序深度这个问题,并不是在所有细胞中都是一样的(也就是reads不是一样多,基因覆盖度也不一样)。这种情况导致根据UMI弄到的基因并不可比,所以需要矫正,对应的就是Normalize counts,把文库都归一化道10000(也就是seurat里的sizefactor)
1 | # normalize counts in each cell to be equal |
然后取表达值的log压缩表达量的范围
1 | sc.pp.log1p(adata) |
再来选择高变基因,跟Seurat里一样
1 | sc.pp.highly_variable_genes(adata, min_mean=0.01, max_mean=8, min_disp=1, n_top_genes=2000, flavor="seurat", n_bins=20) |
然后进行scale:
1 | sc.pp.scale(adata, max_value=10) |
其实就是将数据变为0位均值,10为方差的正态分布,才能进行pca。所以为啥要选方差为10…..
Node5.DownStream-Clustering
一切准备就绪,上游QC,数据变换都执行完了,准备开始进入聚类分群了
首先是PCA
1 | # We perform PCA on just the highly variable genes |
这里的参数也就svd的方法可以改动,其他基本没什么可以改的,注意,如果在之前实际上能看到和示例并不完全相同,应该就是前面flavor的问题,但其实这里效果也挺好的,能看到分得非常开就是了。
此外,scanpy官方教程都说了
often, a rough estimate of the number of PCs does fine.
然后就可以执行Cluster了,这里有两个路径,其一是:
louvain+paga+umap;
1 | sc.tl.louvain(adata) |
第二则是:
ledian+umap
1 | sc.tl.leiden(adata) |
理论上,louvain和ledian不会有太大的区别,一般二者聚类的结果都是基本一致的。
然后就是挑选marker基因进行注释了。这一步这里不讲,这一次主要是上游区别与cellranger的一个流程汇总,注释放在可视化的部分里面专门讲,大概是下下次,下一次就就做一下cellranger的流程,二者用同一份数据对比看看有什么区别。