单细胞测序最好的教程(三):特征基因选择

前言提到,在过去两天的教程中,我们讲解了使用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)
meansdispersionsmean_bindispersions_normhighly_variable
00.0009550.001085(-0.00329, 0.186]-1.267316False
10.0060570.715322(-0.00329, 0.186]1.484390True
20.0366190.275313(-0.00329, 0.186]-0.210809False
30.0221420.268974(-0.00329, 0.186]-0.235233False
40.002392-0.093762(-0.00329, 0.186]-1.632726False
..................

20171 rows × 5 columns

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中,研究人员使用了方差稳定变换来纠正这一点误差。这意味着我们将不使用标准化后的数据来计算高可变基因。该方法的计算步骤如下:

我们首先计算每一个基因的平均值 x i ‾ \overline{x_i} xi 与方差 σ i \sigma_i σi,然后分别对平均值与方差进行log对数化。然后我们使用2次多项式,将方差作为均值的函数,进行局部多项式回归。

σ ( x ) = a x 2 + b x + c \sigma (x) = a x^2 +b x +c σ(x)=ax2+bx+c

通过上述公式,我们可以获得每一个基因的预期方差,然后我们进行z转换:

z i j = x i j − x i ‾ σ ( x i ) z_{ij}= \frac {x_{ij}-\overline{x_i}} {\sigma (x_i)} zij=σ(xi)xijxi

其中 z i j z_{ij} zij 是细胞j中基因i的标准化值, x i j x_{ij} xij 是细胞j中基因i的原始值, x i ‾ \overline{x_i} xi 是基因i的平均原始值,并且 σ ( x i ) \sigma (x_i) σ(xi) 是从全局均值方差拟合得出的特征i的预期标准差。为了减少技术异常值的影响,我们将标准化值削减为最大值 N \sqrt {N} N ,其中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或计数数据的常见建模假设是,每个基因 g g g在细胞 c c c中占据了总计数 n c n_c nc的一定比例 p g p_g pg [4, 6-10]。然后,观察到的UMI计数 X c g X_{c g} Xcg被建模为泊松分布或负二项分布(NB),其期望值为 μ c g = p g n c \mu_{c g}=p_g n_c μcg=pgnc,不考虑零膨胀:

X c g ∼  Poisson  ( μ c g )  or  N B ( μ c g , θ ) , μ c g = n c p g . \begin{array}{r} X_{c g} \sim \text { Poisson }\left(\mu_{c g}\right) \text { or } \mathrm{NB}\left(\mu_{c g}, \theta\right), \\ \mu_{c g}=n_c p_g . \end{array} Xcg Poisson (μcg) or NB(μcg,θ),μcg=ncpg.

泊松模型有一个最大似然解,可以写成闭式形式: n ^ c = ∑ g X c g \hat{n}_c=\sum_g X_{c g} n^c=gXcg(测序深度), p ^ g = ∑ c X c g / ∑ c n ^ c \hat{p}_g=\sum_c X_{c g} / \sum_c \hat{n}_c p^g=cXcg/cn^c,或者合并在一起:

μ ^ c g = ∑ j X c j ⋅ ∑ i X i g ∑ i j X i j \hat{\mu}_{c g}=\frac{\sum_j X_{c j} \cdot \sum_i X_{i g}}{\sum_{i j} X_{i j}} μ^cg=ijXijjXcjiXig

对于负二项模型,这个解仅适用于近似情况。利用这个解,皮尔森近似残差可表示为:

Z c g = X c g − μ ^ c g μ ^ c g + μ ^ c g 2 / θ , Z_{c g}=\frac{X_{c g}-\hat{\mu}_{c g}}{\sqrt{\hat{\mu}_{c g}+\hat{\mu}_{c g}^2 / \theta}}, Zcg=μ^cg+μ^cg2/θ Xcgμ^cg,

其中 μ c g + μ c g 2 / θ \mu_{c g}+\mu_{c g}^2 / \theta μcg+μcg2/θ是NB的方差, θ → ∞ \theta \rightarrow \infty θ时得到泊松极限。皮尔森近似残差的方差,除了一个常数外,等于皮尔森 χ 2 \chi^2 χ2拟合优度统计量,用来量化每个基因与这个常量表达模型的偏差程度。正如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
meansdispersionsmean_bindispersions_normhighly_variable
AL627309.10.0009550.001085(-0.00329, 0.186]-1.267316False
AL627309.50.0060570.715322(-0.00329, 0.186]1.484390True
LINC014090.0366190.275313(-0.00329, 0.186]-0.210809False
LINC011280.0221420.268974(-0.00329, 0.186]-0.235233False
LINC001150.002392-0.093762(-0.00329, 0.186]-1.632726False
..................

20171 rows × 5 columns

现在,我们已经获得了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的参数只能设置为scanpypearson,而在更高的版本中,normalize|HVGs:我们使用 | 来控制预处理步骤,| 前用于归一化步骤,可以是 shiftlogpearson,| 后用于高度可变基因计算步骤,可以是 pearsonseurat。我们的默认值是 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()
gene_idsfeature_typesgenomemtribohbn_cells_by_countsmean_countslog1p_mean_countspct_dropout_by_countstotal_countslog1p_total_countsn_cellspercent_cellsrobustmeanvarresidual_varianceshighly_variable_rankhighly_variable_features
AL627309.1ENSG00000238009Gene ExpressionGRCh38FalseFalseFalse300.0018310.00182999.82284231.03.465736270.182260True0.0018230.0018190.783158NaNFalse
AL627309.5ENSG00000241860Gene ExpressionGRCh38FalseFalseFalse1460.0089760.00893699.137829152.05.0304381320.891049True0.0087750.0092391.063730NaNFalse
LINC01409ENSG00000237491Gene ExpressionGRCh38FalseFalseFalse8830.0630090.06110494.7856381067.06.9735438205.535304True0.0611580.0826701.080966NaNFalse
LINC01128ENSG00000228794Gene ExpressionGRCh38FalseFalseFalse5330.0361400.03550396.852486612.06.4183654643.132172True0.0336170.0426151.162746NaNFalse
LINC00115ENSG00000225880Gene ExpressionGRCh38FalseFalseFalse720.0043700.00436099.57482074.04.317488660.445525True0.0043880.0045040.834976NaNFalse
adata.write_h5ad('s4d8_preprocess.h5ad')

5. 思考

为了加深你对本章的理解,我们提出了以下思考题,如有兴趣作答者,可将答案发送至邮箱starlitnightly@163.com,邮件标题为姓名/昵称-单细胞最好教程(三)思考题

  • 我们为什么要选择高可变基因/特征?
  • 不同的高可变基因/特征最后只获得了656个基因为交集,这是为什么?
  • 选择2000个高可变基因还是选择3000个高可变基因,你认为区别是什么?

如果你对omicverse的使用有什么疑问,或者也想加入一起开发擅长,想在生物信息学工具开发路上找到同道中人,那么这个Python的转录组学分析框架与生态交流群请不要错过。感谢Jimmy老师对omicverse的大力支持。

如果群满了也可以关注我们的github仓库获取最新消息:https://github.com/Starlitnightly/omicverse

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值