第2章 单细胞RNA测序数据基础:表达矩阵与元数据

做单细胞分析第一步,拿到数据后别急着跑流程。我见过不少新手上来就调参数,结果跑出一堆垃圾还找不到原因。其实关键就在最基础的两个东西:表达矩阵和元数据。搞懂它们,你后面调参才有底气。

说白了,单细胞数据就是一张大表格。你想想看,一次实验测几万个细胞,每个细胞里几万个基因的表达量,怎么组织?两个维度就搞定了。咱们用10X Genomic的CellRanger输出的数据作为例子,来拆解一下。

单细胞RNA测序数据核心结构 表达矩阵 行:基因 (~30,000) 列:细胞 (~5,000-10,000) 值:UMI计数 (稀疏矩阵) 元数据 行:细胞 (与表达矩阵一致) 列:注释信息 常用列: orig.ident - 样本来源 nCount_RNA - 总UMI数 nFeature_RNA - 检测到的基因数 percent.mt - 线粒体比例 seurat_clusters - 分群结果 按细胞ID 一一对应 ⚠ 元数据的行数必须等于表达矩阵的列数,顺序可以不同,Seurat会自动根据细胞barcode匹配

2.1 表达矩阵长什么样

从CellRanger的filtered_feature_bc_matrix目录里,你会得到三个文件:barcodes.tsv(细胞条形码)、features.tsv(基因特征)、matrix.mtx(稀疏矩阵)。读过之后,咱们用R把它变成Seurat能认的格式。

# 读入10X标准输出
library(Seurat)
data_dir <- "path_to_filtered_feature_bc_matrix/"
counts <- Read10X(data.dir = data_dir)
# 查看维度
dim(counts)
# [1] 36601  9780
# 意味着36601个基因,9780个细胞

这个counts对象本质上是dgCMatrix格式,也就是列存储的稀疏矩阵。为什么要用稀疏矩阵?你想啊,单细胞数据大部分基因在某一个细胞里表达量为0,存成稠密矩阵,光内存就能把服务器撑爆。我曾经在一个30万细胞的数据集上试过直接转成普通矩阵,直接OOM——嗯,从那以后我再也不敢抱着侥幸心理了。

2.2 元数据:给每个细胞贴标签

表达矩阵告诉你每个细胞里每个基因表达了多少。但光有数字不够啊——你总得知道哪个细胞来自哪个样本、是不是死细胞、质量好不好。这些信息就放在元数据里。

Seurat对象自带一个meta.data槽,本质上是一个data.frame,行数等于细胞数。创建对象时,Seurat会自动算几个基本指标:

# 创建Seurat对象,同时自动计算QC指标
pbmc <- CreateSeuratObject(
  counts = counts,
  project = "PBMC_3k",
  min.cells = 3,    # 至少在3个细胞中表达的基因才保留
  min.features = 200 # 一个细胞至少检测到200个基因
)
# 查看元数据
head(pbmc@meta.data)
#                  orig.ident nCount_RNA nFeature_RNA
# AAACATACAACCAC-1    PBMC_3k       2419          735
# AAACATACAGCCGAA-1    PBMC_3k        648          208

关键点:nCount_RNA是该细胞所有基因的UMI总数,nFeature_RNA是检测到多少种不同的基因。这两个指标是后续质控过滤的核心依据。

我个人的习惯是,创建完对象后第一时间手动添加样本来源、实验条件等自定义信息,这样后面做整合、去批次时不会手忙脚乱。

# 手动增加列
pbmc$sample_id <- "sample1"
pbmc$treatment <- "control"
# 批量添加时,可以直接用Seurat的AddMetaData
new_meta <- data.frame(
  row.names = colnames(pbmc),
  cell_barcode = colnames(pbmc),
  condition = rep(c("A", "B"), each = ncol(pbmc)/2) # 示意
)
pbmc <- AddMetaData(pbmc, metadata = new_meta)

2.3 常见坑和避坑指南

我曾经踩过的:元数据和表达矩阵的细胞barcode不匹配。有一次我从别人那拿到一个公共数据集,直接用CreateSeuratObject报错,查了半天发现barcode末尾的“-1”后缀不统一。后来我养成了一个习惯,先用colnames()检查一下:

# 检查barcode格式
all(colnames(counts) == rownames(meta.data))
# 如果不匹配,可以去除后缀
colnames(counts) <- gsub("-1$", "", colnames(counts))
rownames(meta.data) <- gsub("-1$", "", rownames(meta.data))

另一个常见问题是基因名重复。多个feature之间的基因名一样,Seurat创建对象时会强制去重(加后缀),但这样会丢失信息。我的建议是:在读取之前,自己先检查去重。

# 读取feature文件,检查重复
features <- read.table("features.tsv", sep = "\t", header = FALSE)
sum(duplicated(features[,2]))
# 如果有重复,一般选第一个或合并计数
# 也可以直接用Ensembl ID作为基因名

2.4 对比表:表达矩阵 vs 元数据

维度 表达矩阵 (counts) 元数据 (meta.data)
行/列 基因 × 细胞 细胞 × 注释列
数据类型 数值(通常稀疏矩阵) 因子/字符/数值
主要用途 标准化、降维、差异表达 质控、分组、可视化标注
存储格式 dgCMatrix / 普通矩阵 data.frame
行名/列名要求 行=基因ID,列=细胞barcode 行名=细胞barcode(与表达矩阵列名对应)

2.5 用Seurat管理元数据的最佳实践

你可以在Seurat对象上随意增删元数据列。不过要注意几点:

  • 元数据中的因子水平会影响后续可视化(比如标签顺序)。我个人习惯在创建时就把条件、样本设为因子。
  • 不要删除必要的元数据列,如nCount_RNA和nFeature_RNA,Seurat后续的NormalizeData、FindVariableFeatures等函数会依赖它们。
  • 如果想更新某列,直接赋值就行:pbmc$new_col <- ...。
# 将样本来源设为因子,控制组别顺序
pbmc$orig.ident <- factor(pbmc$orig.ident, levels = c("control", "treatment"))
# 或使用FetchData获取特定列用于绘图
head(FetchData(pbmc, vars = c("nCount_RNA", "nFeature_RNA", "sample_id")))

小技巧:当数据集很大时(比如10万细胞以上),频繁操作pbmc@meta.data可能变慢。你可以用SeuratObject::FetchData()只提取你需要的列,而不是整个meta.data框。

好了,表达矩阵和元数据这两个基础概念就这么简单。搞明白它们,后面不管是调NormalizeData的参数,还是改Harmony的协变量,你都能清楚知道数据在哪一层被修改。