第2章 单细胞RNA测序数据基础:表达矩阵与元数据
做单细胞分析第一步,拿到数据后别急着跑流程。我见过不少新手上来就调参数,结果跑出一堆垃圾还找不到原因。其实关键就在最基础的两个东西:表达矩阵和元数据。搞懂它们,你后面调参才有底气。
说白了,单细胞数据就是一张大表格。你想想看,一次实验测几万个细胞,每个细胞里几万个基因的表达量,怎么组织?两个维度就搞定了。咱们用10X Genomic的CellRanger输出的数据作为例子,来拆解一下。
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的协变量,你都能清楚知道数据在哪一层被修改。