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

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

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

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

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

z_{ij}= \frac {x_{ij}-\overline{x_i}} {\sigma (x_i)}

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

\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}

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

\hat{\mu}_{c g}=\frac{\sum_j X_{c j} \cdot \sum_i X_{i g}}{\sum_{i j} X_{i j}}

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

Z_{c g}=\frac{X_{c g}-\hat{\mu}_{c g}}{\sqrt{\hat{\mu}_{c g}+\hat{\mu}_{c g}^2 / \theta}},

其中\mu_{c g}+\mu_{c g}^2 / \theta是NB的方差,\theta \rightarrow \infty时得到泊松极限。皮尔森近似残差的方差,除了一个常数外,等于皮尔森\chi^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

现在,我们已经获得了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的Venn图

有意思的事情出现了,我们发现三种不同方法所找到的高可变基因(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()
adata.write_h5ad('s4d8_preprocess.h5ad')

5. 思考

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

推荐阅读更多精彩内容