单细胞数据读取与创建 Seurat 对象:从原始文件到可分析的数据结构

拿到单细胞数据的第一件事是什么?不是跑分析,是先把数据读进来。我见过不少同学,把10X的matrix.mtx当作普通文本直接读,结果搞了一整天……嗯,这活确实能手动做,但Seurat给你封装好了,咱们就好好用。

这章的核心就两件事:不同平台的数据怎么读,以及CreateSeuratObject的参数怎么调。说白了,就是把外部的原始数据变成Seurat能理解的"对象",后面所有分析都基于这个对象展开。

单细胞数据读取与Seurat对象创建流程 数据读取 → 创建Seurat对象 10X Genomics (H5 / MEX / 多组学) Drop-seq / Smart-seq2 (表达矩阵 + 元数据) H5AD / Loom / 其他 (跨平台转换) Read10X() / Read10X_h5() Read10X_Multiome() ReadDroplet() / ReadSmartSeq() 直接 read.table() ReadH5AD() / as.Seurat() 格式转换 CreateSeuratObject() counts = 原始UMI project = 项目名 min.cells = 3 min.features = 200 assay = "RNA" Seurat 对象 不同平台数据格式差异大 读取前先确认文件结构

2.1 数据格式千差万别,读取方法也得跟着变

单细胞测序平台五花八门,输出格式自然不统一。我最早做项目时,实验室既有10X又有Smart-seq2,每次换数据集都得翻文档……后来总结出一个规律:先看文件扩展名,再选读取函数。

平台 典型文件结构 推荐的读取方法
10X Genomics (v2/v3) matrix.mtx.gz, barcodes.tsv.gz, features.tsv.gz 或 raw_feature_bc_matrix.h5 Read10X() 或 Read10X_h5()
10X Multiome (RNA + ATAC) 多组学H5文件,包含多个assay Read10X_Multiome()
Drop-seq 数字表达矩阵 (DGE) 或 *.dge.txt.gz ReadDGE() 或 read.table()
Smart-seq2 每个细胞一个文件,或整合后的表达矩阵 ReadSmartSeq() 或直接读矩阵
H5AD / Loom 单文件HDF5格式 (常见于Scanpy生态) ReadH5AD() 或 ReadLoom() 然后 as.Seurat()

2.1.1 10X数据——最流行,也最不容易出错

如果你做的是10X数据,恭喜你,Seurat为它提供了最完善的"一站式"读取方案。我个人习惯优先用H5版本,因为一个文件搞定所有,不容易漏文件。但有些实验室给的是MEX格式(三个文件),也没问题。

# 读取H5文件(推荐)
library(Seurat)
counts_h5 <- Read10X_h5("raw_feature_bc_matrix.h5")

# 读取MEX格式(三个文件在同一个目录下)
counts_mex <- Read10X(data.dir = "filtered_feature_bc_matrix/")

# 多组学数据(如10X Multiome)
counts_multi <- Read10X_Multiome(data.dir = "filtered_feature_bc_matrix/")
# 返回一个list,包含"Gene Expression"和"Peaks"两个矩阵

注意区分:Read10X() 默认找 matrix.mtx,如果文件夹下有多个子目录(如 raw_feature_bc_matrix),记得把路径指向正确的子目录。我曾经因为路径写错了一层,卡了半小时……

2.1.2 Drop-seq与Smart-seq2——矩阵在手,天下我有

这两个平台的数据,本质都是一张基因 × 细胞的计数矩阵。区别在于Drop-seq通常提供稀疏矩阵格式(DGE),而Smart-seq2更常见的是稠密矩阵(行是基因,列是细胞,值可能是FPKM或TPM,但建议用原始计数)。

# Drop-seq DGE格式
dge <- ReadDGE("sample.dge.txt.gz")
# 返回的是稀疏矩阵,可以直接传给CreateSeuratObject

# Smart-seq2——直接用read.table读
expr_matrix <- read.table("expression_matrix.tsv", header = TRUE, row.names = 1, sep = "\t")
# 如果值是TPM或FPKM,最好做转换?别急,后面我们会讲标准化,但CreateSeuratObject只认整数计数
# 对于非UMI数据,保留原始值即可,但最好注明是"log-normalized"还是"raw counts"

一个经验:Smart-seq2数据经常有大量dropout(零值),读进来后记得用FeatureScatter检查一下基因表达分布。如果发现很多基因只在1~2个细胞中表达,后面min.cells参数得调大一些。

2.2 CreateSeuratObject——把原始数据"包装"成标准对象

数据读进来了,现在我们要调用CreateSeuratObject()函数。这个函数就像一台打包机——你把原始计数矩阵塞进去,它吐出一个含有assay、meta.data、reductions等空壳的Seurat对象。注意,这时候对象里还没有降维、聚类,只有原始表达矩阵和最基本的质量控制信息。

函数签名长这样(我挑最常用的参数讲):

CreateSeuratObject(
  counts,          # 原始计数矩阵(稀疏/稠密均可)
  project = "SeuratProject",  # 项目名称,用于标识
  assay = "RNA",   # 测序类型,默认为RNA
  names.field = 1, # 从细胞barcode中提取分组信息
  names.delim = "_", # 分隔符
  meta.data = NULL, # 额外的细胞元数据
  min.cells = 3,   # 至少在多少细胞中表达的基因才保留
  min.features = 200 # 一个细胞至少要有多少基因表达
)

我见过太多人直接带着默认参数跑了。这样行不行?也行,但结果往往不够干净。咱们逐个拆解一下关键参数。

2.2.1 counts——唯一必须手动传的参数

原始计数矩阵,必须是整数。Seurat内部会把它存储为稀疏矩阵(dgCMatrix),所以即使你传的是稠密矩阵,它也会帮你转换,节省内存。

2.2.2 min.cells 和 min.features——第一道质控门

这两个参数是创建对象时就执行的过滤,而且过滤速度非常快(因为只做计数筛选)。

  • min.cells = 3:基因至少在3个细胞中出现,才保留。这能去掉那些"幽灵基因"——比如只在1个细胞中意外捕获的噪声。
  • min.features = 200:每个细胞至少检测到200个基因。如果低于这个数,大概率是空液滴或死细胞。

你可能会问:那后续不是还有更细致的质控吗?对,这里只是粗略过滤,但能极大减小对象大小。我做过的一个PBMC数据集,原始矩阵含3万个基因,设了min.cells = 3后只剩1.8万,节省了40%内存。

我曾经踩过的坑:有一批Smart-seq2数据,部分细胞的测序深度很低,min.features设成200后,丢了一半细胞。后来我把阈值降到100,先建对象,再在质控阶段用subset()结合其他指标(如线粒体比例)做精细过滤。所以阈值不要一刀切,结合你的实验设计来定。

2.2.3 project——给对象打标签

这个参数默认是"SeuratProject"。如果你有多个样本,建议改成有意义的名字,比如"PBMC_healthy_1"。后面合并对象时,project字段会作为默认的样本标识,省得你自己再去加。

pbmc <- CreateSeuratObject(counts = counts_h5,
                            project = "PBMC_Control_DonorA",
                            assay = "RNA",
                            min.cells = 3,
                            min.features = 200)

一个小技巧:如果数据来自不同批次,可以在meta.data中预置批次信息,比如:

meta <- data.frame(row.names = colnames(counts), batch = rep("batch1", ncol(counts)))
pbmc <- CreateSeuratObject(counts = counts, meta.data = meta)

这样后面做整合时,就不用手动添加了。

2.2.4 names.field 和 names.delim——解析细胞barcode里的"密码"

某些10X数据,细胞barcode可能长这样:AAACCCAAGAAACTCA-1。如果names.delim = "-"且names.field = 1,Seurat会把"-"后面的数字(1)提取出来,存到meta.data的orig.ident列中。如果你每个样本都单独跑10X管道,这个数字就是样本索引。不过我个人习惯在project里直接指定,所以一般保持默认不额外设置。

2.3 创建完对象后,先检查什么?

用上面代码创建好对象后,别急着做下一步。先简单看看:

# 打印对象信息
pbmc
# 输出类似:
# An object of class Seurat 
# 2000 features across 1000 samples within 1 assay 
# Active assay: RNA (2000 features, 0 variable features)

# 查看前6个细胞的barcode和细胞数
head(colnames(pbmc))
# 查看meta.data中的列
head(pbmc@meta.data)

注意看nCount_RNA和nFeature_RNA这两列——它们是创建对象时自动计算的,分别表示每个细胞的UMI总数和检测到的基因数。如果发现有细胞nFeature_RNA为0,那可能是空液滴或者数据读取有问题,需要排查原始数据。

2.4 特殊场景:多模态数据(CITE-seq / ASAP-seq)

如果你的数据同时包含RNA和蛋白质(CITE-seq),或者RNA和ATAC,你可以在创建对象时指定多个assay。但Seurat官方推荐的做法是:先用RNA数据创建一个标准对象,再用CreateAssayObject()添加其他模态。这部分我会在第5章详细讲,这里先提一句。

还记得我一开始说的吗?单细胞分析的起点就是把东西读进来。现在你有了一个干净、标准、可复现的Seurat对象。接下来所有分析——质控、标准化、降维、聚类、注释——都跑在这个对象上。踩稳这一步,后面的路会顺很多。