前言提到,在过去两天的教程中,我们讲解了使用omicverse
进行单细胞测序数据的质控以及归一化的一些思想。关于omicverse的使用文档与安装教程可以参考我们的readthedocs.
就是,本系列教程是我带本科生所用到的,所以概念会尽可能地通俗,详细,但对于急于求成的人,可能不是一个很好的教程。
1. 背景
我们现在有了一个经过标准化的单细胞测序数据,其保留了细胞差异,同时消除了技术带来的采样误差。通常来说,我们经过上游的mapping(比对),我们会得到30,000到50,000个不等的基因。但是,我们只在第一步的质控中,删除了一小部分基因(少于20个细胞表达的基因)。但实际上,一个细胞表达的基因大约是3,000个左右。意味着我们的测序数据中的一大部分基因可能不具有与单细胞数据相关的生物学意义,这些基因大多数包含0计数,或者是在许多细胞中普遍出现的基因。
故在预处理环节,我们需要计算高可变基因(特征基因),排除那些不具有分析意义的基因,避免影响我们下游的建模以及生物学检测。
在本章中,我们将介绍三种不同的特征基因选择:基于基因离散度,基于基因归一化方差以及基于基因的皮尔森残差。在传统的分析流程中,我们会采用基于基因离散度的方式去计算高可变基因,一般来说,我们首先确定了单细胞数据集中变异最大的一组基因。我们计算了所有单细胞中每个基因的平均值和离散度(方差/平均值),并根据基因的平均表达将其分为 20 个箱。然后,在每个箱内,我们对箱内所有基因的离散度进行z归一化,以识别表达值高度可变的基因。
2. 基于基因离散度
我们首先设置我们的环境。
import omicverse as ov
import scanpy as sc
ov.ov_plot_set()
接下来,我们加载此前分析所用到的数据集,并进行移位对数标准化处理。
adata = sc.read(
filename="s4d8_quality_control.h5ad",
#backup_url="https://figshare.com/ndownloader/files/40014331",
)
#存储原始数据以便后续还原
ov.utils.store_layers(adata,layers='counts')
adata.layers['counts']=adata.X.copy()
sc.pp.normalize_total(adata)
sc.pp.log1p(adata)
adata
......The X of adata have been stored in counts
normalizing counts per cell
finished (0:00:00)
AnnData object with n_obs × n_vars = 14814 × 20171
obs: 'n_genes_by_counts', 'log1p_n_genes_by_counts', 'total_counts', 'log1p_total_counts', 'pct_counts_in_top_20_genes', 'total_counts_mt', 'log1p_total_counts_mt', 'pct_counts_mt', 'total_counts_ribo', 'log1p_total_counts_ribo', 'pct_counts_ribo', 'total_counts_hb', 'log1p_total_counts_hb', 'pct_counts_hb', 'outlier', 'mt_outlier', 'scDblFinder_score', 'scDblFinder_class'
var: 'gene_ids', 'feature_types', 'genome', 'mt', 'ribo', 'hb', 'n_cells_by_counts', 'mean_counts', 'log1p_mean_counts', 'pct_dropout_by_counts', 'total_counts', 'log1p_total_counts', 'n_cells'
uns: 'layers_counts', 'log1p'
layers: 'counts', 'soupX_counts'
接下来,我们调用scanpy
包里的pp.highly_variable_genes
函数来计算高可变基因,由于我们使用的是基于基因离散度的方法,故我们需要设置flavor='seurat'
,该方法也是默认方法。基于基因离散度的方法寻找高变基因有两个途径:
- 指定目的高变基因数
- 指定离散度,平均值过滤高变基因
在这里,我们分别尝试两种不同的方法,首先是基于指定高变基因数的方法:
2.1 指定高可变基因数
由于是教程演示,所以我们不希望改变原来的anndata对象的.var
,故我们设置inplace=False
,旨在观察和对比高可变基因
adata_dis_num=sc.pp.highly_variable_genes(
adata,
flavor="seurat",
n_top_genes=2000,
subset=False,
inplace=False,
)
adata_dis_num
If you pass `n_top_genes`, all cutoffs are ignored.
extracting highly variable genes
finished (0:00:00)
adata_dis_num['highly_variable'].value_counts()
False 18171
True 2000
Name: highly_variable, dtype: int64
我们发现,一共选择了2000个高可变基因,这与我们最开始的分析目标一致,至于选择出来的高可变基因有什么用途,则留在下一章节讨论
2.2 指定基因离散度与平均值阈值
除了指定高可变基因数量外,我们还可以通过基因的离散度与平均值的阈值,去计算高可变基因,但是,这种启发式的方法是数据敏感的,阈值所计算出来的高可变基因可能并不是全部高可变。故基于阈值的方法在现在的分析中应用较少。
adata_dis_cutoff=sc.pp.highly_variable_genes(
adata,
flavor="seurat",
min_disp=0.5,
min_mean=0.0125,
max_mean=3,
subset=False,
inplace=False,
)
adata_dis_cutoff['highly_variable'].value_counts()
extracting highly variable genes
finished (0:00:00)
False 17291
True 2880
Name: highly_variable, dtype: int64
通过简单的计算我们发现,一共有2,880个高可变基因被选择了出来,一般2000-3000内的高可变基因数都是能接受的
因为非高可变基因在下游分析时会被过滤掉,我们会将归一化值给保存进raw文件,但这有个缺陷,我们将会永远失去非高可变基因的原始count。因此可以使用释放函数 ov.utils.retrieve_layers
,与存放在adata.layers中的数据不同,retrieve_layers
函数还可还原全基因的原始计数,我们以下将进行一个简单的对比实验。
#计算高可变基因(覆盖,inplace=True)
adata.raw = adata
sc.pp.highly_variable_genes(
adata,
flavor="seurat",
)
#只保留高可变基因
adata = adata[:, adata.var.highly_variable]
print('shape: ',adata.shape)
adata_counts=adata.copy()
ov.utils.retrieve_layers(adata_counts,layers='counts')
print('normalize adata:',adata.X.max())
print('raw count adata:',adata_counts.X.max())
print('shape: ',adata_counts.shape)
extracting highly variable genes
finished (0:00:00)
--> added
'highly_variable', boolean vector (adata.var)
'means', float vector (adata.var)
'dispersions', float vector (adata.var)
'dispersions_norm', float vector (adata.var)
shape: (14814, 2880)
......The X of adata have been stored in raw
......The layers counts of adata have been retreved
normalize adata: 4.9448869840604806
raw count adata: 694.0
shape: (14814, 2880)
#释放所有基因
adata_counts=adata.raw.to_adata().copy()
ov.utils.retrieve_layers(adata_counts,layers='counts')
print('normalize adata:',adata.X.max())
print('raw count adata:',adata_counts.X.max())
print('shape: ',adata_counts.shape)
......The X of adata have been stored in raw
......The layers counts of adata have been retreved
normalize adata: 4.9448869840604806
raw count adata: 889.0
shape: (14814, 20171)
可以发现,我们使用.raw还原基因名后,retrieve_layers
即可自动提取.raw中所包含的counts值。即数据的形状为 (14814, 20171)
3. 基于基因归一化方差
在过去的单细胞研究中人们发现,仅根据对数归一化法,并使用离散度的高可变基因选择方法无法解释单细胞 RNA-seq 固有的均值-方差关系。故在seurat v3
中,研究人员使用了方差稳定变换来纠正这一点误差。这意味着我们将不使用标准化后的数据来计算高可变基因。该方法的计算步骤如下:
我们首先计算每一个基因的平均值 与方差 ,然后分别对平均值与方差进行log对数化。然后我们使用2次多项式,将方差作为均值的函数,进行局部多项式回归。
通过上述公式,我们可以获得每一个基因的预期方差,然后我们进行z转换:
其中 是细胞j中基因i的标准化值, 是细胞j中基因i的原始值, 是基因i的平均原始值,并且 是从全局均值方差拟合得出的特征i的预期标准差。为了减少技术异常值的影响,我们将标准化值削减为最大值 ,其中N是细胞总数。然后,对于每个基因,我们计算所有细胞标准化值的方差。该方差表示控制均值表达后单细胞离散度的度量,我们直接使用它对特征进行排序。
在scanpy
中,我们需要设定flavor="seurat_v3"
以选择基于基因归一化方差的方法,并指定计数矩阵为未归一化的矩阵,即layer='counts'
,同时我们选择标准化方差最高的 2,000 个基因作为“高度可变”。
adata_var_num=sc.pp.highly_variable_genes(
adata,
flavor="seurat_v3",
layer='counts',
n_top_genes=2000,
subset=False,
inplace=False,
)
adata_var_num['highly_variable'].value_counts()
If you pass `n_top_genes`, all cutoffs are ignored.
extracting highly variable genes
False 18171
True 2000
Name: highly_variable, dtype: int64
4. 基于基因皮尔森近似残差
除了基于基因归一化残差以及离散度的方法外,我们还可以基于皮尔森近似残差的思想来计算高可变基因,你一定注意到了我们此前的归一化步骤中包含了皮尔森残差的方法,但是在这里,我们依然是使用原始计数来计算高可变基因,这是scanpy
的实现原理所导致的,即我们可以使用移位对数来获得标准化数据,同时使用皮尔森残差法来获得高可变基因
针对没有生物学变异的UMI或计数数据的常见建模假设是,每个基因在细胞中占据了总计数的一定比例 [4, 6-10]。然后,观察到的UMI计数被建模为泊松分布或负二项分布(NB),其期望值为,不考虑零膨胀:
泊松模型有一个最大似然解,可以写成闭式形式:(测序深度),,或者合并在一起:
对于负二项模型,这个解仅适用于近似情况。利用这个解,皮尔森近似残差可表示为:
其中是NB的方差,时得到泊松极限。皮尔森近似残差的方差,除了一个常数外,等于皮尔森拟合优度统计量,用来量化每个基因与这个常量表达模型的偏差程度。正如Aedin Culhane指出的那样,在泊松模型下对皮尔森近似残差进行奇异值分解被称为对应分析,这是一种历史悠久的方法。
Hafemeister和Satija建议在高变异基因(HVG)筛选中使用来自相关NB回归模型的皮尔森近似残差,也可作为下游处理的数据转换方法。与此同时,Townes等人建议使用来自上述泊松模型的偏差残差进行HVG筛选
在scanpy
中,我们将使用experimental.pp.highly_variable_genes
来计算基于皮尔森残差的高可变基因
adata_pearson_num=sc.experimental.pp.highly_variable_genes(
adata,
flavor="pearson_residuals",
layer='counts',
n_top_genes=2000,
subset=False,
inplace=False,
)
adata_pearson_num['highly_variable'].value_counts()
extracting highly variable genes
False 18171
True 2000
Name: highly_variable, dtype: int64
adata_dis_num
现在,我们已经获得了3种不同的特征基因选择方法所得到的高可变基因,我们可以对比一下基因之间的重叠率,至于功能的研究则不在本章的考虑范围内。我们先将基因名赋予此前的结果
adata_dis_num.index=adata.var_names.copy()
adata_dis_cutoff.index=adata.var_names.copy()
adata_var_num.index=adata.var_names.copy()
adata_pearson_num.index=adata.var_names.copy()
import matplotlib.pyplot as plt
from matplotlib_venn import venn3
# 三个列表的元素
list1 = set(adata_dis_num.loc[adata_dis_num['highly_variable']==True].index.tolist())
list2 = set(adata_var_num.loc[adata_var_num['highly_variable']==True].index.tolist())
list3 = set(adata_pearson_num.loc[adata_pearson_num['highly_variable']==True].index.tolist())
# 绘制 Venn 图
venn = venn3([list1, list2, list3], set_labels=('Dis', 'Var', 'Pearson'))
# 显示图形
plt.title("Venn Diagram of Three HVGs")
plt.show()
有意思的事情出现了,我们发现三种不同方法所找到的高可变基因(HVGs)仅有656个是相同的,这意味着不同的方法所寻找到的高可变基因可能会影响你的下游分析的误差。在我们的分析中,我们推荐使用皮尔森残差法来获得高可变基因。
在omicverse
中,我们可以直接使用ov.pp.preprocess
完成预处理步骤,需要注意的是,当omicverse
的版本小于1.4.13
时,mode的参数只能设置为scanpy
或pearson
,而在更高的版本中,normalize|HVGs:我们使用 | 来控制预处理步骤,| 前用于归一化步骤,可以是 shiftlog
或 pearson
,| 后用于高度可变基因计算步骤,可以是 pearson
或 seurat
。我们的默认值是 shiftlog|pearson
。
adata = sc.read(
filename="s4d8_quality_control.h5ad",
#backup_url="https://figshare.com/ndownloader/files/40014331",
)
#存储原始数据以便后续还原
ov.utils.store_layers(adata,layers='counts')
adata.layers['counts']=adata.X.copy()
adata=ov.pp.preprocess(adata,
mode='shiftlog|pearson',
n_HVGs=2000,)
adata
......The X of adata have been stored in counts
Begin robust gene identification
After filtration, 20171/20171 genes are kept. Among 20171 genes, 20171 genes are robust.
End of robust gene identification.
Begin size normalization: shiftlog and HVGs selection pearson
normalizing counts per cell The following highly-expressed genes are not considered during normalization factor computation:
[]
finished (0:00:00)
extracting highly variable genes
--> added
'highly_variable', boolean vector (adata.var)
'highly_variable_rank', float vector (adata.var)
'highly_variable_nbatches', int vector (adata.var)
'highly_variable_intersection', boolean vector (adata.var)
'means', float vector (adata.var)
'variances', float vector (adata.var)
'residual_variances', float vector (adata.var)
End of size normalization: shiftlog and HVGs selection pearson
AnnData object with n_obs × n_vars = 14814 × 20171
obs: 'n_genes_by_counts', 'log1p_n_genes_by_counts', 'total_counts', 'log1p_total_counts', 'pct_counts_in_top_20_genes', 'total_counts_mt', 'log1p_total_counts_mt', 'pct_counts_mt', 'total_counts_ribo', 'log1p_total_counts_ribo', 'pct_counts_ribo', 'total_counts_hb', 'log1p_total_counts_hb', 'pct_counts_hb', 'outlier', 'mt_outlier', 'scDblFinder_score', 'scDblFinder_class'
var: 'gene_ids', 'feature_types', 'genome', 'mt', 'ribo', 'hb', 'n_cells_by_counts', 'mean_counts', 'log1p_mean_counts', 'pct_dropout_by_counts', 'total_counts', 'log1p_total_counts', 'n_cells', 'percent_cells', 'robust', 'mean', 'var', 'residual_variances', 'highly_variable_rank', 'highly_variable_features'
uns: 'layers_counts', 'log1p', 'hvg'
layers: 'counts', 'soupX_counts'
注意,与scanpy
不同,omicverse
计算高可变基因后,将保存为var['highly_variable_features']
,而在scanpy
中,高可变将保存为var['highly_variable']
adata.var.head()
adata.write_h5ad('s4d8_preprocess.h5ad')
5. 思考
- 我们为什么要选择高可变基因/特征?
- 不同的高可变基因/特征最后只获得了656个基因为交集,这是为什么?
- 选择2000个高可变基因还是选择3000个高可变基因,你认为区别是什么?