单细胞测序最好的教程(四):降维

前言提到,在过去两天的教程中,我们讲解了使用omicverse进行单细胞测序数据的预处理的一些思想。关于omicverse的使用文档与安装教程可以参考我们的readthedocs.

目录
单细胞测序最好的教程(一):质量控制
单细胞测序最好的教程(二):归一化
单细胞测序最好的教程(三):特征基因选择

如前所述,单细胞RNA测序(scRNA-seq)是一种高通量测序技术,它产生的数据集在细胞和基因数量上都具有很高的维度。这立即指向了一个事实,即单细胞RNA测序数据受到了"维度诅咒"的困扰。

维度诅咒

"维度诅咒"这个概念最早是由R. Bellman提出的,它描述的是理论上高维数据包含更多的信息,但在实践中并非如此。更高维度的数据往往包含更多的噪声和冗余,因此增加更多的信息并不利于后续的分析步骤。

并非所有的基因都具有信息性,或对于基于其表达谱进行细胞类型聚类的任务有重要意义。我们已经试图通过特征选择来降低数据的维度,作为下一步,人们可以通过使用降维算法来进一步降低单细胞RNA测序数据的维度。这些算法在预处理过程中是一个重要步骤,用于降低数据复杂性和进行可视化。已经开发并用于单细胞数据分析的降维技术有很多。

降维

降维将高维数据嵌入到低维空间中。低维表示仍然捕获数据的基本结构,同时尽可能少地具有维度。在这里,我们将三维对象可视化为投影到二维中。

Xing等人在独立比较中比较了10种不同的降维方法的稳定性,准确性和计算成本。他们建议使用t-分布随机邻居嵌入(t-SNE),因为它产生了最佳的整体性能。统一流形逼近和投影(UMAP)显示出最高的稳定性,并且最好地分离了原始细胞群体。在这种情况下,值得一提的另一种降维方法是主成分分析(PCA),它仍然被广泛使用。

一般来说,如果选择特定的初始化选项,t-SNE和UMAP非常稳健且大致相等.

所有上述方法都在scanpyomicverse中实现。

import omicverse as ov
import scanpy as sc

ov.ov_plot_set()
adata = sc.read(
    filename="s4d8_preprocess.h5ad",
)

我们将使用数据集的归一化值表示进行降维和可视化,具体来说,就是使用计数矩阵的移位对数值。我们可以使用adata.X.max()检查计数矩阵的最大值来判断数据是否进行了移位对数计算

adata.X.max()
10.989398

我们从以下开始:

1. PCA

在我们的数据集中,每个细胞都是由某个正交基张成的 n_var 维向量空间中的一个向量。由于单细胞 RNA 测序(scRNA-seq)受到"维度的诅咒"的影响,我们知道并非所有的特征都对理解数据集的基础动态性有重要意义,且存在内在的冗余性。主成分分析(PCA)通过对原始数据集进行正交变换,创建一组新的无关变量,即所谓的主成分(PCs)。这些 PCs 是原始数据集中特征的线性组合,并按照方差的递减顺序进行排名以定义变换。通过排名,通常第一主成分的方差最大。最小方差的 PCs 被丢弃,有效地降低了数据的维度,同时不丧失信息。

PCA 的优点在于它具有高度的可解释性和计算效率。然而,由于单细胞 RNA 测序数据集由于掉落事件而相当稀疏,因此高度非线性,使用线性降维技术 PCA 进行可视化并不合适。但是我们可以基于PCA的结果再进行可视化。

这里需要介绍另一个重要的思想,z-score标准化,在机器学习中,特征缩放是一个重要的预处理步骤,通俗来说,使特征的标准差为 1,平均值为 0。经过z-score标准化后的数据对PCA降维有着显著影响,这在sklearn的官方教程中进行了测试:

PCA

从上图中我们观察到,先缩放特征再进行PCA降维可以使component具有相同的数量级。

omicverse中,我们将scale后的值存放进adata.layer层,而不是像scanpy一样默认取代.X,缩放后的特征会移除基因间的差异,没有任何的证据表明,数据经过scale后会取得更好的分析结果,包括差异表达基因的计算等,若盲目地使用scale后的计数值,还可能会导致使用dotplot或者violinplot中忽略了基因自身的特征信息。

这听起来很难理解,事实上,比如我们关注的一个基因A的表达值在17-20区间,而基因B的表达值在0-3的区间,经过scale后,由于平均值被缩放成了0,基因A和基因B都在-2-2的区间范围内,这一定程度上失去了基因A表达量高的信息。故我们不认为对原始计数值进行scale是一个聪明的做法。

#我们首先选择高可变基因,并将原数据存放在.raw中
if adata.raw is None:
    adata.raw = adata.copy()
adata=adata[:,adata.var['highly_variable_features']==True]
adata.shape
(14814, 2000)
ov.pp.scale(adata,max_value=10)
adata
... as `zero_center=True`, sparse input is densified and may lead to large memory consumption





AnnData object with n_obs × n_vars = 14814 × 2000
    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: 'hvg', 'layers_counts', 'log1p'
    layers: 'counts', 'soupX_counts', 'scaled'

我们发现adata的layers层多出了一个scaled,这就是我们经过scale后的数据,接下来,我们基于scaled层进行pca

ov.pp.pca(adata,layer='scaled',n_pcs=50)
adata
AnnData object with n_obs × n_vars = 14814 × 2000
    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: 'hvg', 'layers_counts', 'log1p', 'scaled|original|pca_var_ratios', 'scaled|original|cum_sum_eigenvalues'
    obsm: 'scaled|original|X_pca'
    varm: 'scaled|original|pca_loadings'
    layers: 'counts', 'soupX_counts', 'scaled', 'lognorm'

我们再次观察发现,adata.obsm层里多出了一个scaled|original|X_pca,这代表了我们使用的是layers中的scaled层数据进行的pca计算,当然我们也可以使用counts进行pca计算,效果如下:

ov.pp.pca(adata,layer='counts',n_pcs=50)
adata
AnnData object with n_obs × n_vars = 14814 × 2000
    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: 'hvg', 'layers_counts', 'log1p', 'scaled|original|pca_var_ratios', 'scaled|original|cum_sum_eigenvalues', 'counts|original|pca_var_ratios', 'counts|original|cum_sum_eigenvalues'
    obsm: 'scaled|original|X_pca', 'counts|original|X_pca'
    varm: 'scaled|original|pca_loadings', 'counts|original|pca_loadings'
    layers: 'counts', 'soupX_counts', 'scaled', 'lognorm'

我们可以使用embedding函数,来对比基于两种不同的layers计算所得出的pca的差异

import matplotlib.pyplot as plt
fig,axes=plt.subplots(1,2,figsize=(8,4))
ov.utils.embedding(adata,
                basis='scaled|original|X_pca',frameon='small',
                color='MS4A1',show=False,ax=axes[0])
ov.utils.embedding(adata,
                basis='counts|original|X_pca',frameon='small',
                color='MS4A1',show=False,ax=axes[1])
<AxesSubplot: title={'center': 'MS4A1'}, xlabel='counts|original|X_pca1', ylabel='counts|original|X_pca2'>
PCA对比

我们会发现基于scaled的pca结果,第一主成分和第二主成分有着相似的数量级,而基于counts的pca结果,第一主成分和第二主成分的数量级则有所差异,这对于后续的2维投影可能会有显著的影响,我们在下面的教程中会进行对比分析。

2. t-SNE

t-SNE 是一种基于图的、非线性的降维技术,它将高维数据投影到 2D 或 3D 组件上。该方法基于数据点之间的高维欧几里得距离定义了一个高斯概率分布。随后,使用 t 分布在低维空间中重建概率分布,其中嵌入通过梯度下降进行优化。

sc.tl.tsne(adata, use_rep="scaled|original|X_pca")
#tsne函数默认是存放在adata.obsm['X_tsne']中的,我们将其存放在adata.obsm['X_tsne_scaled']中来区分counts的结果
adata.obsm['X_tsne_scaled']=adata.obsm['X_tsne']
sc.tl.tsne(adata, use_rep="counts|original|X_pca")
adata.obsm['X_tsne_counts']=adata.obsm['X_tsne']
adata
computing tSNE
    using sklearn.manifold.TSNE
    finished: added
    'X_tsne', tSNE coordinates (adata.obsm) (0:01:33)
computing tSNE
    using sklearn.manifold.TSNE
    finished: added
    'X_tsne', tSNE coordinates (adata.obsm) (0:01:51)





AnnData object with n_obs × n_vars = 14814 × 2000
    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: 'hvg', 'layers_counts', 'log1p', 'scaled|original|pca_var_ratios', 'scaled|original|cum_sum_eigenvalues', 'counts|original|pca_var_ratios', 'counts|original|cum_sum_eigenvalues', 'tsne'
    obsm: 'scaled|original|X_pca', 'counts|original|X_pca', 'X_tsne', 'X_tsne_scaled', 'X_tsne_counts'
    varm: 'scaled|original|pca_loadings', 'counts|original|pca_loadings'
    layers: 'counts', 'soupX_counts', 'scaled', 'lognorm'
import matplotlib.pyplot as plt
fig,axes=plt.subplots(1,2,figsize=(8,4))
ov.utils.embedding(adata,
                basis='X_tsne_scaled',frameon='small',
                color='MS4A1',show=False,ax=axes[0])
ov.utils.embedding(adata,
                basis='X_tsne_counts',frameon='small',
                color='MS4A1',show=False,ax=axes[1])
<AxesSubplot: title={'center': 'MS4A1'}, xlabel='X_tsne_counts1', ylabel='X_tsne_counts2'>
TSNE对比

3.UMAP

UMAP 是一种基于图的、非线性的降维技术,原理上与 t-SNE 类似。它构建了数据集的高维图表示,并优化低维图表示,使其在结构上尽可能地与原始图相似。

我们首先基于PCA的结果,在单细胞数据上构建一个邻域图

sc.pp.neighbors(adata, n_neighbors=15, n_pcs=50,
               use_rep='scaled|original|X_pca')
sc.tl.umap(adata)
#umap函数默认是存放在adata.obsm['X_umap']中的,我们将其存放在adata.obsm['X_umap_scaled']中来区分counts的结果
adata.obsm['X_umap_scaled']=adata.obsm['X_umap']

sc.pp.neighbors(adata, n_neighbors=15, n_pcs=50,
               use_rep='counts|original|X_pca')
sc.tl.umap(adata)
adata.obsm['X_umap_counts']=adata.obsm['X_umap']
computing neighbors


OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.


    finished: added to `.uns['neighbors']`
    `.obsp['distances']`, distances for each pair of neighbors
    `.obsp['connectivities']`, weighted adjacency matrix (0:00:08)
computing UMAP
    finished: added
    'X_umap', UMAP coordinates (adata.obsm) (0:00:06)
computing neighbors
    finished: added to `.uns['neighbors']`
    `.obsp['distances']`, distances for each pair of neighbors
    `.obsp['connectivities']`, weighted adjacency matrix (0:00:00)
computing UMAP
    finished: added
    'X_umap', UMAP coordinates (adata.obsm) (0:00:06)
import matplotlib.pyplot as plt
fig,axes=plt.subplots(1,2,figsize=(8,4))
ov.utils.embedding(adata,
                basis='X_umap_scaled',frameon='small',
                color='MS4A1',show=False,ax=axes[0])
ov.utils.embedding(adata,
                basis='X_umap_counts',frameon='small',
                color='MS4A1',show=False,ax=axes[1])
<AxesSubplot: title={'center': 'MS4A1'}, xlabel='X_umap_counts1', ylabel='X_umap_counts2'>
UMAP对比

我们发现基于counts的pca无法构建出合理的细胞分布,MS4A1是B细胞的marker基因,这一定程度上证实了scale对于pca的重要性。除此之外,如果你的电脑有GPU,那么可以尝试使用mde来绘制UMAP图,pymde包是GPU加速的UMAP绘图函数。mde函数内自带neighbor的计算,所以我们可以直接输入pca结果

adata.obsm["X_mde_scaled"] = ov.utils.mde(adata.obsm["scaled|original|X_pca"])
adata.obsm["X_mde_counts"] = ov.utils.mde(adata.obsm["counts|original|X_pca"])
import matplotlib.pyplot as plt
fig,axes=plt.subplots(1,2,figsize=(8,4))
ov.utils.embedding(adata,
                basis='X_mde_scaled',frameon='small',
                color='MS4A1',show=False,ax=axes[0])
ov.utils.embedding(adata,
                basis='X_mde_counts',frameon='small',
                color='MS4A1',show=False,ax=axes[1])
<AxesSubplot: title={'center': 'MS4A1'}, xlabel='X_mde_counts1', ylabel='X_mde_counts2'>
GPU版UMAP对比

4.检查质量控制指标

现在我们也可以在我们之前计算的 PCA、TSNE 或 UMAP 图中检查我们之前计算的质量控制指标,并识别出潜在的低质量的细胞。在论文中,我们一般会检查三个指标,nUMI计数,nGene计数,以及线粒体基因的比例。这里再重复一次,nUMI是基因数加上每个基因的数量,nGene代表的是基因的种类。

如果你前面使用的是omicverse.pp.qc,那么你将直接得到nUMIs,detected_genes,mito_perc三个变量,如果你使用的是scanpy进行的qc,那么你得到的将是total_counts,n_genes_by_counts, 和pct_counts_mt三个变量,我们再2-1的教程中对比过三个变量的值发现是一样的,只是不同的包给出的名字不同。

我们使用numpy中的log2对数化,将数据可视化区间缩小,同时,我们定义最大最小值来衡量我们的数据质量,我们希望nUMIs大于250,250的对数值是7.96,所以我们最小值设定为8,最大值则定义为30,000,即15,而线粒体的比例则在0-100的范围内。

import numpy as np
adata.obs['log2_nUMIs']=np.log2(adata.obs['total_counts'])
adata.obs['log2_nGenes']=np.log2(adata.obs['n_genes_by_counts'])
ov.utils.embedding(adata,
                basis='X_mde_scaled',
                color=['log2_nUMIs','log2_nGenes','pct_counts_mt'],
                title=['log2#(nUMIs)','log2#(nGenes)','Mito_Perc'],
                vmin=[0,0,0],
                vmax=[15,15,100],show=False,frameon='small',)
[<AxesSubplot: title={'center': 'log2#(nUMIs)'}, xlabel='X_mde_scaled1', ylabel='X_mde_scaled2'>,
 <AxesSubplot: title={'center': 'log2#(nGenes)'}, xlabel='X_mde_scaled1', ylabel='X_mde_scaled2'>,
 <AxesSubplot: title={'center': 'Mito_Perc'}, xlabel='X_mde_scaled1', ylabel='X_mde_scaled2'>]
scRNA-seq质控标准

我们发现数据的质量较好,这三幅图即是单细胞质控所用到的基本三幅图,此外,对于特定的任务,我们还会衡量细胞周期基因,详细的计算可参考scanpy中的教程。

adata.write_h5ad('s4d8_dimensionality_reduction.h5ad', compression='gzip')

5. 思考

  • tsne与umap哪一个算法更好,为什么?
  • 我们为什么要用mde来取代umap?
  • 在原始计数值和PCA值中分别得到的umap图有什么区别?造成这种区别的原因是什么?

可以关注我们的github仓库获取最新消息:https://github.com/Starlitnightly/omicverse

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容