单细胞RNA-seq数据入门:表达矩阵与元数据格式归纳总结

单细胞数据分析的第一步,就是搞清楚你手里拿到的数据到底是什么样的。

很多新手一上来就急着做降维聚类,结果发现画出来的图乱七八糟。为什么?因为连基础的表达矩阵结构和元数据都没搞明白。我刚开始接触单细胞时也犯过这个错——拿到一个rds文件就直接跑,后来才发现里面根本沒有基因名,只有一堆EnsemblID,搞得我只能在cluster里猜功能,相当尴尬。

所以,这一章我们把最底层的东西说透。说白了就是两样东西:表达矩阵 和 元数据。你掌握好它们,后面的质控、归一化、整合才能顺风顺水。

表达矩阵:每个格子的意义

单细胞表达矩阵本质上就是个二维表格:

  • 行 = 基因(或feature,包括蛋白、CRISPR等)
  • 列 = 细胞(每个barcode一行)
  • 值 = 该基因在该细胞中的表达量

但这里的“表达量”有好几种,你得区分开:

类型 说明 典型值范围 我用过的场景
UMI counts 基于唯一分子标识的真实分子计数,整数 0 ~ 几千 10X、Drop-seq常见,我个人习惯用这个做质控
Read counts 比对到该基因的测序片段数,可能重复 0 ~ 上万 Smart-seq2等全长方法,注意PCR重复需要去重
CPM/TPM 文库大小归一化后的值,浮点数 0 ~ 几十 比较不同样本时可用,但质控阶段不推荐
Log-normalized 对数归一化后的值,常为ln(CPM+1) 0 ~ 10+ 下游聚类可视化常用,但scran质控工具要求原始counts
注意:scran的质控函数如addPerCellQC()、quickPerCellQC(),它们都只接受原始整数计数矩阵。如果你传入的是对数归一化后的数据,每个细胞的总UMI/基因数计算就会完全错误。我曾经见过有人直接拿Seurat的NormalizeData结果做质控,结果过滤掉了90%的好细胞——血泪教训。

元数据:给数据贴标签

表达矩阵只告诉你哪个基因在哪个细胞里有多少。但你还想知道:

  • 这个细胞来自哪个样本?哪个批次?
  • 这个细胞是活细胞还是空液滴?
  • 这个细胞经过了哪些质控指标?
  • 这个基因是哪个物种的?是蛋白编码还是lncRNA?

这些信息就叫元数据。元数据分为两类:

  1. 细胞元数据(cell-level metadata):每个细胞一条记录,列包括barcode、样本ID、批次、FACS门控、质控指标(总UMI、基因数、线粒体比例等)。
  2. 基因元数据(gene-level metadata):每个基因一条记录,列包括gene symbol、Ensembl ID、基因类型、染色体位置等。

在实际项目中,我建议你养成一个习惯:每做一步处理,都在元数据里加上对应的时间戳和参数。比如“这一步用了min_genes=200过滤”,直接写到元数据的一个列里。这样几个月后你看回代码,还能知道当时怎么想的。

常见存储格式对比

单细胞数据格式五花八门,你至少得认识下面这几种:

格式 典型文件 包含内容 谁在用 我的评价
10X自带格式 matrix.mtx, barcodes.tsv, features.tsv 稀疏表达矩阵 + 细胞barcode + 基因信息 Cell Ranger输出 最原始,推荐作为存档格式
HDF5 (h5) filtered_feature_bc_matrix.h5 同一个内容但打包成一个文件 10X官方、HDF5爱好 占用空间小,但需要额外库读取
SingleCellExperiment .rds 或内存对象 表达矩阵 + 细胞元数据 + 基因元数据 + 降维结果 scran/scater用户 scran全家桶的天然容器,我所有项目都用它
Seurat对象 .rds 类似SCE,但数据结构不同 Seurat用户 可用as.SingleCellExperiment()转换
H5AD (AnnData) .h5ad Python版单细胞对象 Scanpy用户 与R互转需要zellkonverter或sceasy
我的建议:只要项目允许,统一用SingleCellExperiment对象做内部存储。它结构清晰,能同时装下counts矩阵、logcounts矩阵、元数据、降维坐标、甚至差异表达结果。scran的质控、归一化函数也直接操作这个对象。如果你从10X或Seurat开始,记得先转成SCE再跑scran的流程。

从文件到SCE对象:快速上手代码

假设你刚跑完Cell Ranger,拿到了filtered_feature_bc_matrix目录。用下面的代码就能读进来:

library(DropletUtils)
library(SingleCellExperiment)

# 从三个文件读入
sce <- read10xCounts("filtered_feature_bc_matrix/")
# 返回一个SingleCellExperiment对象
# 其中 counts(sce) 就是原始UMI矩阵
# colData(sce) 包含barcode
# rowData(sce) 包含基因名、ID、类型

# 你也可以从HDF5读
sce_h5 <- read10xCounts("filtered_feature_bc_matrix.h5")

如果要转换Seurat对象:

library(Seurat)
library(scater)

seurat_obj <- readRDS("my_seurat.rds")
sce <- as.SingleCellExperiment(seurat_obj)
# 默认会把counts和normalized数据都带过来
关于稀疏矩阵:单细胞数据90%以上的数值是0,所以存储时都用稀疏格式(dgCMatrix)。scran内部处理也是基于稀疏矩阵的,不用担心内存。我自己处理过20万个细胞的表达矩阵,SCE对象在内存中也就几个GB,完全可行。

知识体系总览

下面这张图概括了单细胞原始数据到标准分析对象的链路:

单细胞数据组成与格式转换全景图 原始测序Fastq Cell Ranger流程 表达矩阵 基因 × 细胞 原始UMI counts 元数据 细胞信息 + 基因信息 样本、批次、QC指标 10X格式 (mtx/h5) SingleCellExperiment Seurat / H5AD 核心:用 read10xCounts() 读入后先检查 counts() 和 colData(),再开始质控

记一个检查清单:每次拿到新的单细胞数据,先跑下面几行:

# 检查表达矩阵
dim(counts(sce))           # 基因数 × 细胞数
class(counts(sce))         # 应该是 dgCMatrix 或其他稀疏格式
head(counts(sce)[,1:5])    # 看一下第一行前5个细胞的值

# 检查元数据
colnames(colData(sce))     # 细胞元数据列名
rowData(sce)$Symbol        # 记得确认基因名列是否存在