3、批次效应的来源与可视化:PCA降维、t-SNE/UMAP中的批次分离现象
做单细胞联合分析,最头疼的问题是什么?我个人觉得,十有八九是批次效应。你想想看,明明同一种细胞类型,就因为来自不同的实验批次,数据分析时硬是给你分成两伙,这种尴尬相信不少人都遇到过。
本章我们就来扒一扒批次效应的老底,看看它从哪里来,在PCA、t-SNE、UMAP这些降维图上又是什么嘴脸。只有把问题看清楚了,后面用MNN方法去矫正时,你心里才有底。
3.1 批次效应的本质与常见来源
首先我们得明确一下,什么是批次效应?说白了,就是非生物学因素的干扰。测序过程中,除了细胞本身的状态差异,任何操作环节的变化都会给数据带来系统性偏移。
| 来源类别 | 具体因素 | 影响程度 |
|---|---|---|
| 实验操作 | 不同操作人员、不同的解离时间、不同的建库批次 | 强 |
| 试剂耗材 | 不同批号的试剂、不同厂商的捕获磁珠 | 中-强 |
| 测序仪器 | 不同测序仪、不同测序通道(Flow Cell) | 中 |
| 环境条件 | 温度、湿度、RNA降解程度 | 弱-中 |
| 数据处理 | 不同版本的比对软件、不同的定量参数 | 弱 |
我在项目中遇到过最极端的情况:同一个实验室,同一个操作员,只是中间隔了两周时间,两次测出来的同一类细胞的转录组图谱就有明显偏移。所以,不要抱有“我的实验控制得很好”这种侥幸心理。
3.2 PCA降维中的批次分离现象
PCA(主成分分析)是我们最先接触的降维方法,它也是一个特别诚实的镜子。为什么要这么说?因为PCA完全基于数据的方差结构,它不会刻意去抹平任何差异,哪怕是来自批次效应的差异。
当你把两个批次的细胞放到一个PCA空间里,通常会看到什么?如果批次效应存在,同一类型的细胞不会自然地聚到一起,而是按批次分开,形成平行的“流形”。
经验观察:在PCA的前两个主成分(PC1和PC2)上,如果细胞按照实验批次明显分离,说明批次效应已经盖过了生物学差异。这时候不要急着做下游分析,先看看数据再说。
为什么会这样?原因很简单。如果批次之间的均值差异足够大,PCA会把这种差异当作“最大的方差来源”,自动安排到PC1上。于是你的细胞分层就不是按细胞类型,而是按实验日期。
3.3 t-SNE与UMAP的非线性可视化
PCA看完了,很多人会接着用t-SNE(分布随机邻域嵌入)或者UMAP(均匀流形逼近与投影)。这两个是非线性降维方法,用在高维数据上效果确实顶呱呱。但是!它们也有个非常tricky的点——对批次效应特别敏感。
t-SNE和UMAP的底层逻辑都是保持局部邻域结构。也就是说,它会把附近的数据点尽量拉近,把远处的点尽量推远。
避坑指南:我曾经过分依赖于t-SNE图中的“分离”来判断细胞类型是否存在。后来才发现,那很可能仅仅是批次效应,而不是细胞类型差异。t-SNE对这种全局性的批次偏移非常敏感,即使只占整体方差的一小部分,它也能在图上把两个批次完全分开——这就非常具有欺骗性了。
我个人的习惯是:先用PCA快速判断批次效应的强弱,再用t-SNE/UMAP观察细微的细胞亚群结构,但绝不只看t-SNE就下结论。如果PCA已经看到明显分离,你不去做矫正就直接扔进t-SNE,那结果大概率是你的技术操作比生物学故事更精彩——这不是我们想要的。
3.4 批次效应的量化与诊断
光靠肉眼看图,有时候是不够的。我们还需要一些量化指标来帮助我们判断批次效应的严重程度。
常用的诊断思路有这么几条:
- 检查PCA加载矩阵:看看前几个主成分是否明显与批次标签相关。你可以跑一个简单的线性模型,用批次标签预测主成分得分,看R²值。
- 计算批次的Silhouette得分:每个细胞的“批次内距离” vs “批次间距离”,得分接近1说明不同批次的重合度很低。
- 观察基因表达的中位数/方差随批次的变化:如果某个基因的表达水平在两个批次之间发生了系统性的升降(而非细胞类型特异的升降),那你就要怀疑了。
注意:并不是所有跨批次的差异都是批次效应。如果两个批次确实包含了不同的细胞类型比例(比如批次1全是T细胞,批次2全是B细胞),那PCA在PC1上自然会把它们分开——但这不是批次效应,而是生物学差异。你需要排除这种可能性,常见的方法是用已知的细胞类型标签做着色验证。
3.5 实战代码详解:从数据加载到可视化诊断
说了这么多理论,我们来看看怎么实际操作。下面这个流程是我在项目中反复使用的标准诊断流程,用R语言和Seurat包实现。
首先,我们要组装一个包含两个批次数据的Seurat对象:
library(Seurat)
library(ggplot2)
library(patchwork)
# 假设我们有两个矩阵:matrix_batch1 和 matrix_batch2
# 分别为不同批次的数据,行是基因,列是细胞
seurat_batch1 <- CreateSeuratObject(counts = matrix_batch1, project = "batch1")
seurat_batch2 <- CreateSeuratObject(counts = matrix_batch2, project = "batch2")
# 合并两个Seurat对象
merged_seurat <- merge(seurat_batch1, y = seurat_batch2,
add.cell.ids = c("B1", "B2"))
# 标准预处理:NormalizeData、FindVariableFeatures、ScaleData
merged_seurat <- NormalizeData(merged_seurat)
merged_seurat <- FindVariableFeatures(merged_seurat, selection.method = "vst", nfeatures = 2000)
merged_seurat <- ScaleData(merged_seurat)
# PCA降维
merged_seurat <- RunPCA(merged_seurat, npcs = 30, verbose = FALSE)
接下来,我们用组合PCA分面图来快速诊断:
# 绘制PCA结果,分别按批次和细胞类型着色
p1 <- DimPlot(merged_seurat, reduction = "pca", group.by = "orig.ident") +
ggtitle("PCA按批次着色")
p2 <- DimPlot(merged_seurat, reduction = "pca", group.by = "celltype") +
ggtitle("PCA按细胞类型着色")
# 使用patchwork并排展示
p1 + p2
从组合PCA图中,你可以一目了然地判断:如果按批次着色的图显示出明显的分离(比如两个群体完全分开),而按细胞类型着色的图却没有相应地显示细胞类型聚集,那就基本可以确认批次效应显著。
如果我们想看t-SNE和UMAP的效果:
# 用PCA结果跑t-SNE和UMAP
merged_seurat <- RunTSNE(merged_seurat, dims = 1:20, verbose = FALSE)
merged_seurat <- RunUMAP(merged_seurat, dims = 1:20, verbose = FALSE)
# 绘制t-SNE与UMAP
p3 <- DimPlot(merged_seurat, reduction = "tsne", group.by = "orig.ident") +
ggtitle("t-SNE按批次着色")
p4 <- DimPlot(merged_seurat, reduction = "umap", group.by = "orig.ident") +
ggtitle("UMAP按批次着色")
p3 + p4
你可能会问,到底哪个方法更适合用来观察批次效应?我的回答是——都用,但要注意区别。
- PCA:适合判断是否有全局性的批次偏移,但细节不够丰富。
- t-SNE:非常敏感,哪怕很小的批次差异也能显现出来,但容易过度分离。
- UMAP:相比之下对批次效应的敏感度略低于t-SNE,局部结构保持得更好。
我个人习惯是用PCA做“快筛”,如果PCA上没有明显分离,再用UMAP去微调看亚群细节。如果报告里需要有更直观的证据,我通常也会附上一张按批次着色的UMAP图,让审稿人一目了然。
下面是我总结的一个批次效应不同降维方法的可视化对比表:
| 特征 | PCA | t-SNE | UMAP |
|---|---|---|---|
| 对批次效应的识别能力 | 中(仅捕获全局线性差异) | 强(过分敏感,易产假阳性) | 强(平衡较好) |
| 批次效应的典型表现 | 按批次出现多簇偏移 | 批次完全分离,出现独立的簇 | 批次部分分离或形成桥接结构 |
| 计算速度 | 快速 | 慢 | 中等 |
| 推荐使用场景 | 快速诊断、构建降维基器 | 展示最终矫正效果 | 评估批次矫正后局部结构的保留 |
嗯,说到这儿你应该明白了——批次效应的诊断不是一锤子买卖,它需要你结合多个可视化手段去综合判断。之前提到的那些代码片段,每一个都值得你在自己的数据上跑一遍,形成自己的直觉。