单细胞处理中Seurat Object数据结构浅析

最近在分析单细胞数据,因此和Seurat包打交道了不少。由于seurat的对象设计太复杂了,遂花了一些时间去学习其数据结构以及属性访问方法,整理为笔记特放置于此,谨供需要的朋友们参考。

一、背景知识:R语言的S3,R4,RC,R6对象

另外参考博客往期文章: 《R语言的对象系统(S3-R6)》

R语言脱胎于S语言,因此在设计上继承了许多S语言的特性。近年来,随着编程技术的发展,R语言又吸收了许多新的设计思想,因此在原先的特性的基础上产生了许多扩展。这就是R语言对象系统的复杂性,其按照出现的先后顺序分为S3,R4,RC,R6。 在本文中我们将要详细介绍的seurat就属于R4对象系统。

  • S3 对象系统:R语言最原始的对象系统,基于一种特殊的泛型函数实现。使用 attr(obj,"class") 进行对象创建和数据设置,使用 UseMethod() 创建泛型函数。Seurat当中几乎不涉及S3对象系统。
  • R4对象系统:这是标准的 R 语言面向对象实现方式,比 S3 的定义更加严格,S4 对象有专门的函数用于定义类(setClass)、泛型函数(setGeneric)、方法(setMethod)以及实例化对象(new),提供了参数检查,多重继承功能。另外, S4 有一个重要的组件 slot,它是对象的属性组件,可以使用专门的运算符 @来访问(对于Seurat对象,我们还会经常碰到这个运算符)。
  • RC对象系统:全名是Reference Classes,是在 R 2.12 版本开始引入的新一代的面向对象系统,通过类似 C++ 语言的风格进行类定义和方法封装。使用 $ 符号来调用方法,获取和修改对象的属性,调用方法或设置属性的值会修改对象,这种方式不同于常用的函数式编程模型。
  • R6对象系统:这是一个第三方的 R 面向对象编程的实现,比内置的 RC 类更简单,更快,更轻量级。

二、Seurat Object的结构

使用 str() 函数可以查看一个对象的结构。下面我们以Seurat官方文档的示例数据为例,展示一下其内部结构:

读取数据:

1
2
3
4
5
6
7
8
library(Seurat)
data_dir = "D:/linux/R/Seurat-demo/filtered_gene_bc_matrices/hg19"
# 导入示例数据
pbmc.data <- Read10X(data.dir = data_dir)#自行填写数据所在文件夹
# 创建Seurat对象
pbmc <- CreateSeuratObject(counts = pbmc.data, project = "pbmc3k", min.cells = 3, min.features = 200)
# 查看对象结构
str(pbmc)

下面是使用 str() 展示的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
> str(pbmc)
Formal class 'Seurat' [package "SeuratObject"] with 13 slots
..@ assays :List of 1
.. ..$ RNA:Formal class 'Assay5' [package "SeuratObject"] with 8 slots
.. .. .. ..@ layers :List of 1
.. .. .. .. ..$ counts:Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
.. .. .. .. .. .. ..@ i : int [1:2282976] 29 73 80 148 163 184 186 227 229 230 ...
.. .. .. .. .. .. ..@ p : int [1:2701] 0 779 2131 3260 4220 4741 5522 6304 7094 7626 ...
.. .. .. .. .. .. ..@ Dim : int [1:2] 13714 2700
.. .. .. .. .. .. ..@ Dimnames:List of 2
.. .. .. .. .. .. .. ..$ : NULL
.. .. .. .. .. .. .. ..$ : NULL
.. .. .. .. .. .. ..@ x : num [1:2282976] 1 1 2 1 1 1 1 41 1 1 ...
.. .. .. .. .. .. ..@ factors : list()
.. .. .. ..@ cells :Formal class 'LogMap' [package "SeuratObject"] with 1 slot
.. .. .. .. .. ..@ .Data: logi [1:2700, 1] TRUE TRUE TRUE TRUE TRUE TRUE ...
.. .. .. .. .. .. ..- attr(*, "dimnames")=List of 2
.. .. .. .. .. .. .. ..$ : chr [1:2700] "AAACATACAACCAC-1" "AAACATTGAGCTAC-1" "AAACATTGATCAGC-1" "AAACCGTGCTTCCG-1" ...
.. .. .. .. .. .. .. ..$ : chr "counts"
.. .. .. .. .. ..$ dim : int [1:2] 2700 1
.. .. .. .. .. ..$ dimnames:List of 2
.. .. .. .. .. .. ..$ : chr [1:2700] "AAACATACAACCAC-1" "AAACATTGAGCTAC-1" "AAACATTGATCAGC-1" "AAACCGTGCTTCCG-1" ...
.. .. .. .. .. .. ..$ : chr "counts"
.. .. .. ..@ features :Formal class 'LogMap' [package "SeuratObject"] with 1 slot
.. .. .. .. .. ..@ .Data: logi [1:13714, 1] TRUE TRUE TRUE TRUE TRUE TRUE ...
.. .. .. .. .. .. ..- attr(*, "dimnames")=List of 2
.. .. .. .. .. .. .. ..$ : chr [1:13714] "AL627309.1" "AP006222.2" "RP11-206L10.2" "RP11-206L10.9" ...
.. .. .. .. .. .. .. ..$ : chr "counts"
.. .. .. .. .. ..$ dim : int [1:2] 13714 1
.. .. .. .. .. ..$ dimnames:List of 2
.. .. .. .. .. .. ..$ : chr [1:13714] "AL627309.1" "AP006222.2" "RP11-206L10.2" "RP11-206L10.9" ...
.. .. .. .. .. .. ..$ : chr "counts"
.. .. .. ..@ default : int 1
.. .. .. ..@ assay.orig: chr(0)
.. .. .. ..@ meta.data :'data.frame': 13714 obs. of 0 variables
.. .. .. ..@ misc :List of 1
.. .. .. .. ..$ calcN: logi TRUE
.. .. .. ..@ key : chr "rna_"
..@ meta.data :'data.frame': 2700 obs. of 3 variables:
.. ..$ orig.ident : Factor w/ 1 level "pbmc3k": 1 1 1 1 1 1 1 1 1 1 ...
.. ..$ nCount_RNA : num [1:2700] 2419 4903 3147 2639 980 ...
.. ..$ nFeature_RNA: int [1:2700] 779 1352 1129 960 521 781 782 790 532 550 ...
..@ active.assay: chr "RNA"
..@ active.ident: Factor w/ 1 level "pbmc3k": 1 1 1 1 1 1 1 1 1 1 ...
.. ..- attr(*, "names")= chr [1:2700] "AAACATACAACCAC-1" "AAACATTGAGCTAC-1" "AAACATTGATCAGC-1" "AAACCGTGCTTCCG-1" ...
..@ graphs : list()
..@ neighbors : list()
..@ reductions : list()
..@ images : list()
..@ project.name: chr "pbmc3k"
..@ misc : list()
..@ version :Classes 'package_version', 'numeric_version' hidden list of 1
.. ..$ : int [1:3] 5 0 2
..@ commands : list()
..@ tools : list()
>

我们可以看到,在pbmc对象的第一层,是几个数据槽,包括@assays , @meta.data 等。 其中 @assays 槽是最为关键的数据槽,其中存储了表达矩阵的信息(即 $RNA )。

另外,毋庸置疑的,当我们继续运行seurat的分析流程,对pbmc对象进行更深层次的分析(如归一化、降维聚类、细胞分群与注释)以后, str(pbmc) 给出的列表还会继续增长:因为Seurat的R4对象会将一切数据都存储在对象结构内部。

那么,如何访问呢?这是下一小节的内容。

三、Seurat对象内部属性的访问方法

3.1 使用R4数据槽操作符 @ 进行访问

这是一种很直观但并 被官方推荐的方法,原因在于Seurat在不断更新,也许某一个数据槽在新版本Seurat中就会改叫其他的名字;另外,这种方式过于底层,容易破坏对象的完整性和一致性。如果直接修改槽内容,可能导致不可预期的行为。

不过,我们还是在这里介绍一下访问方法。如下:

1
2
3
4
# 读取数据集的元数据
metadata = pbmc@meta.data
class(metadata)
metadata[1:10,1:3]

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> metadata = pbmc@meta.data
> class(metadata)
[1] "data.frame"
> metadata[1:10,1:3]
orig.ident nCount_RNA nFeature_RNA
AAACATACAACCAC-1 pbmc3k 2419 779
AAACATTGAGCTAC-1 pbmc3k 4903 1352
AAACATTGATCAGC-1 pbmc3k 3147 1129
AAACCGTGCTTCCG-1 pbmc3k 2639 960
AAACCGTGTATGCG-1 pbmc3k 980 521
AAACGCACTGGTAC-1 pbmc3k 2163 781
AAACGCTGACCAGT-1 pbmc3k 2175 782
AAACGCTGGTTCTT-1 pbmc3k 2260 790
AAACGCTGTAGCCA-1 pbmc3k 1275 532
AAACGCTGTTTCTG-1 pbmc3k 1103 550
>

如果我们想要看表达矩阵的信息,则可以像下面这样(虽然比较麻烦)

1
2
3
4
5
# 通过assays槽访问RNA对象,这是一个R4对象,里面存储了许多东西,包括表达矩阵
RNA = pbmc@assays$RNA
# 通过layers槽访问counts对象,后者是dataframe格式的原始表达矩阵
counts = RNA@layers$counts
counts [1:15,1:15]

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> counts [1:15,1:15]
15 x 15 sparse Matrix of class "dgCMatrix"

[1,] . . . . . . . . . . . . . . .
[2,] . . . . . . . . . . . . . . .
[3,] . . . . . . . . . . . . . . .
[4,] . . . . . . . . . . . . . . .
[5,] . . . . . . . . . . . . . . .
[6,] . . . . . . . . . . . 1 . . .
[7,] . . . . . . . . . . . . . . .
[8,] . . . . . . . . . . . . . . .
[9,] . . . . . . . . . . . . . . .
[10,] . . . . . . . . . . . . . . .
[11,] . . . . . . . . . . . . . . .
[12,] . . 1 9 . 1 . . . 3 . . 1 5 .
[13,] . . . . . . . . . . . . . . .
[14,] . . . . . . . . . . . . . . .
[15,] . 2 . . . . . . . . . . . . .
>

3.2 使用GetAssayData()LayerData()更安全的访问数据

GetAssayData()LayerData() 是Seurat对数据访问接口的封装。官方也更推荐用这个进行访问。

二者其实是同一个函数,只不过一个是在Seurat V4当中的名称(GetAssayData),一个是在Seurat V5当中的名称( LayerData ),使用前需要检查Seurat的版本,然后根据版本使用正确的函数名。

笔者电脑上的Seurat 版本为V5,因此下面的演示以LayerData()为例。下面的代码块展示了如何从一个 seurat_obj 中获得原始表达矩阵和归一化的表达矩阵。

1
2
counts <- LayerData(seurat_obj, assay = "RNA", layer = "counts")
normalized_data <- LayerData(seurat_obj, assay = "SCT", layer = "data")

例如,上述pbmc的数据:(注意,SCTransformer矫正前,已经根据基因表达量过滤了一部分细胞,因此原始表达矩阵counts和归一化以后的表达矩阵normalized_data的行和列并非完全一一对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
> counts [1:15,1:15]
15 x 15 sparse Matrix of class "dgCMatrix"
[[ suppressing 15 column names ‘AAACATACAACCAC-1’, ‘AAACATTGAGCTAC-1’, ‘AAACATTGATCAGC-1’ ... ]]

AL627309.1 . . . . . . . . . . . . . . .
AP006222.2 . . . . . . . . . . . . . . .
RP11-206L10.2 . . . . . . . . . . . . . . .
RP11-206L10.9 . . . . . . . . . . . . . . .
LINC00115 . . . . . . . . . . . . . . .
NOC2L . . . . . . . . . . . 1 . . .
KLHL17 . . . . . . . . . . . . . . .
PLEKHN1 . . . . . . . . . . . . . . .
RP11-54O7.17 . . . . . . . . . . . . . . .
HES4 . . . . . . . . . . . . . . .
RP11-54O7.11 . . . . . . . . . . . . . . .
ISG15 . . 1 9 . 1 . . . 3 . . 1 5 .
AGRN . . . . . . . . . . . . . . .
C1orf159 . . . . . . . . . . . . . . .
TNFRSF18 . 2 . . . . . . . . . . . . .
> normalized_data [1:15,1:5]
15 x 5 sparse Matrix of class "dgCMatrix"
AAACATACAACCAC-1 AAACATTGAGCTAC-1 AAACATTGATCAGC-1 AAACCGTGCTTCCG-1 AAACCGTGTATGCG-1
AL627309.1 . . . . .
RP11-206L10.2 . . . . .
LINC00115 . . . . .
NOC2L . . . . .
KLHL17 . . . . .
PLEKHN1 . . . . .
HES4 . . . . .
ISG15 . . 0.6931472 2.197225 .
AGRN . . . . .
C1orf159 . . . . .
TNFRSF18 . 0.6931472 . . .
TNFRSF4 . . . . .
SDF4 . . 0.6931472 . .
B3GALT6 . . . . .
UBE2J2 . . . . .
>

3.3 使用 [[ 访问嵌套列表中的元素

  • 作用:用于访问 Reductions、Graphs 等嵌套列表中的具体对象。
  • 例子
1
2
seurat_obj[["reductions"]][["pca"]]  # 获取 PCA 降维结果
seurat_obj[["graphs"]][["SNN"]] # 获取 SNN 图
  • 适用场景:当需要直接操作嵌套列表时使用(但推荐用封装函数如 Embeddings())。
  • 注意:这种方式较为灵活,但也容易出错,建议结合封装函数使用。

四、一点补充说明:Seurat对象中的四个表达量矩阵

这一部分是我在工作中的发现。

跟着Seurat官方教程跑完了整个pipeline(包括归一化、聚类、分群),检查数据时发现数据当中存在多个表达矩阵(如下表第一列)。

查询各种资料以及文档以后,了解到了他们的区别如下:

访问方法 数据说明
GetAssayData(pbmc, assay = "RNA", slot = "counts") counts(原始UMI)
GetAssayData(pbmc, assay = "SCT", slot = "counts") counts(过滤后)
GetAssayData(pbmc, assay = "SCT", slot = "data") SCTransform归一化数据data
GetAssayData(pbmc, assay = "SCT", slot = "scale.data") 经过z-score标准化之后的SCTransform归一化数据data

如果要访问原始矩阵,推荐使用 GetAssayData(pbmc, assay = "RNA", slot = "counts") 。如果要查询SCTransform归一化并经过z-score标准化以后的数据,则需要使用 GetAssayData(pbmc, assay = "SCT", slot = "scale.data")

以上。