2 批次效应可视化:PCA、t-SNE、UMAP 展示批次效应的方法
做批次效应校正,第一步不是调参数,而是看数据。
我见过太多人拿到数据就直接上校正算法,结果越调越乱。其实,在你动任何校正之前,先拿可视化“拍个片子”,往往能省下大把时间。说白了,可视化就是批次效应的“X光”。
这一讲,我带你走一遍 PCA、t-SNE 和 UMAP —— 这三样工具,基本能覆盖 90% 的批次观察需求。
为什么非得可视化?
你想想看,一个表达矩阵,几千行、几百列,光看数字谁能看出批次?只有把高维数据压到 2D 或 3D,再按批次(或实验日期、测序 lane、操作员)着色,才能一眼看出样本是不是“抱团”了。
- 分离明显:不同批次的点各自成簇,说明批次效应很强,必须校正。
- 完全混合:批次之间点混在一起、没有结构,那恭喜你,或者批次效应弱,或者校正已经到位。
- 部分重叠:最麻烦的情况,有些批次局部偏差,可能还和生物学变量纠缠在一起。
PCA:最直观的线性降维
PCA(主成分分析)是线性方法,计算快,解释性强。它找到方差最大的方向,把数据投影过去。
展示批次效应的核心思路:把样本按前两个主成分(PC1, PC2)画散点图,再用颜色区分批次。
代码示例
import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
# 假设 data 是基因表达矩阵,行为样本,列为基因
# batch_labels 是批次标签列表
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
pca = PCA(n_components=2)
pca_result = pca.fit_transform(data_scaled)
plt.figure(figsize=(8,6))
for batch in np.unique(batch_labels):
idx = batch_labels == batch
plt.scatter(pca_result[idx,0], pca_result[idx,1],
label=f'Batch {batch}', alpha=0.7)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
plt.legend()
plt.title('PCA colored by batch')
plt.show()
注意:刚拿到 RNA-seq 或 scRNA-seq 表达量时,一定要先做标准化(logCPM、TPM、或标准化 z-score),否则高表达基因会把主成分带偏。PCA 对量级很敏感,不标准化等于白做。
另外,解释方差占比也很重要。如果 PC1 才占 5%,说明数据噪声大,或批次效应不在前两个成分里。可以看碎石图,或用前 10 个主成分做 pairplot。
t-SNE:局部结构保留的利器
PCA 只能看到全局线性的分离,但生物数据往往是非线性的。t-SNE 擅长保留局部邻近关系,把高维相似的点在 2D 上“拉”到一起。
我在项目中遇到过一个问题:PCA 图上所有样本混在一起,什么都看不出。但换成 t-SNE 后,批次之间出现了微妙的偏移,一条 lane 的细胞明显和另一条 lane 的细胞有距离。那一幕让我意识到:线性方法有时会“骗”我们。
代码示例
from sklearn.manifold import TSNE
# 建议先 PCA 降维到 30-50 维再丢进 t-SNE,能去掉噪声
pca50 = PCA(n_components=50).fit_transform(data_scaled)
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
tsne_result = tsne.fit_transform(pca50)
plt.figure(figsize=(8,6))
for batch in np.unique(batch_labels):
idx = batch_labels == batch
plt.scatter(tsne_result[idx,0], tsne_result[idx,1],
label=f'Batch {batch}', alpha=0.7, s=5)
plt.legend()
plt.title('t-SNE colored by batch')
plt.show()
- perplexity 不是越大越好。我建议在小样本(< 100)时用 5-15,大样本(> 1000)时用 30-50。选错了,点会聚成均匀的“饼”或者碎成小团。
- t-SNE 每次运行结果不同,设置
random_state保证可重复。 - t-SNE 的距离没有物理意义,不要用坐标差来度量批次效应强弱。
UMAP:兼顾全局与局部
UMAP 近年特别火,它基于拓扑学,比 t-SNE 快得多,而且能保留更多全局结构。
我自己的经验:用 UMAP 时,批次效应往往比 t-SNE 更“清晰”,而且不同参数的鲁棒性也更好。UMAP 可以直接从原始高维数据跑,但我仍然建议先 PCA 降噪。
代码示例
import umap
reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)
umap_result = reducer.fit_transform(data_scaled)
plt.figure(figsize=(8,6))
for batch in np.unique(batch_labels):
idx = batch_labels == batch
plt.scatter(umap_result[idx,0], umap_result[idx,1],
label=f'Batch {batch}', alpha=0.7, s=5)
plt.legend()
plt.title('UMAP colored by batch')
plt.show()
参数 tuning:n_neighbors 控制局部 vs 全局平衡(5-50);min_dist 控制点的聚集程度(0.0-0.99)。如果想看批次分离,可以调大 n_neighbors 让全局结构更明显。我曾经追一个样本批次问题,把 n_neighbors 从 30 调到 15,原本模糊的分离瞬间清晰了。
三种方法对比
| 方法 | 原理 | 保留全局结构 | 计算速度 | 参数敏感度 | 批次效应常见表现 |
|---|---|---|---|---|---|
| PCA | 线性,方差最大方向 | ✅ 是 | 极快 | 低(标准化影响大) | 不同批次沿 PC 方向分离 |
| t-SNE | 非线性,概率分布 | ❌ 否 | 慢(样本>5K 吃力) | 高(perplexity) | 局部局部偏移、小簇 |
| UMAP | 非线性,拓扑图 | ✅ 部分保留 | 快(支持大数据) | 中等(n_neighbors) | 连续渐变或局部集中 |
一句话总结:PCA 做快速筛查,t-SNE 看局部细节,UMAP 兼顾速度与视野。
可视化批次效应的标准流程
下面的图是我在实际项目中画的流程,每次校正检测都走一遍这四步。
图上四个步骤:原始矩阵 → 标准化 → 三种降维 → 着色判断。我一般先跑 PCA 看个大概,如果 PCA 看不出明显分离,再上 UMAP 和 t-SNE 试试。但注意:t-SNE 和 UMAP 看到的“分离”不一定是全局批次,要结合生物学先验判断。
避坑指南:我曾经踩过的几个坑
- 不标准化直接跑 PCA:我第一次做 scRNA-seq 批次检测时就没标准化,结果 PC1 几乎被某个高表达基因(比如线粒体基因)完全解释。后来才意识到,标准化是必须的。
- t-SNE 用默认参数直接跑几千个细胞:结果等了半小时还卡死。现在我都习惯先用 PCA 降到 50 维,再跑 t-SNE,又快又稳。
- 过度解读 UMAP 中的“小簇”:UMAP 的簇边界可能被参数影响,尤其是
min_dist设小了容易“无中生有”。如果看到一个小簇全是某批次样本,建议用 PCA 或差异表达再验证一下。 - 忽略了批次标签与生物学标签的混杂:比如一批全是健康,另一批全是病人。这时候可视化里的“分离”到底是批次还是生物学差异?需要复着色双重验证(batch 和 condition 用不同形状/颜色)。
小结
可视化是你和数据之间的第一座桥。PCA、t-SNE、UMAP 各有侧重,但目标一致:把隐藏的批次结构挖出来。记住,不要只依赖一种方法,交叉验证才是靠谱的。