2. 批次效应的可视化检测:PCA、UMAP、热图与箱线图
数据拿到手,别急着做校正。我个人的习惯是——先可视化,看看批次效应到底存不存在、有多严重。这一步省不了,而且往往决定了后续校正策略的选择。
说白了,可视化检测就是给你的数据"拍X光片"。PCA、UMAP、热图、箱线图这四种方法,各有各的视角。结合起来看,基本能把批次效应的底细摸清楚。
2.1 PCA — 看全局分离度
PCA是第一个上场的。它把高维数据压到2-3个主成分上,保留最大的方差信息。你只需要把前两个主成分画成散点图,用颜色区分批次,就能看出个大概。
我在项目中遇到过一批代谢组学数据,六个批次,PCA一跑,前两个主成分直接分出三个簇。当时我就知道——这事不简单,校正跑不掉了。
代码示例:PCA检测批次效应
import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import seaborn as sns
# 模拟数据:100个样本,50个特征,3个批次
np.random.seed(42)
n_samples, n_features, n_batches = 100, 50, 3
data = []
batch_labels = []
for b in range(n_batches):
# 每个批次加入偏移
offset = np.random.normal(0, 2, n_features)
batch_data = np.random.normal(0, 1, (n_samples // n_batches, n_features)) + offset
data.append(batch_data)
batch_labels.extend([f'Batch_{b+1}'] * (n_samples // n_batches))
data = np.vstack(data)
batch_labels = np.array(batch_labels)
# PCA
pca = PCA(n_components=2)
pca_result = pca.fit_transform(data)
# 绘图
plt.figure(figsize=(8, 6))
sns.scatterplot(x=pca_result[:, 0], y=pca_result[:, 1],
hue=batch_labels, palette='Set2', s=60)
plt.title('PCA: 批次效应可视化')
plt.xlabel('PC1 ({:.1f}%)'.format(pca.explained_variance_ratio_[0] * 100))
plt.ylabel('PC2 ({:.1f}%)'.format(pca.explained_variance_ratio_[1] * 100))
plt.legend()
plt.show()
怎么看结果?如果不同批次的点混在一起、边界模糊,说明批次效应不明显。如果各批次形成独立团簇、边界清晰,那你得警惕了。
2.2 UMAP — 捕捉局部结构
UMAP和PCA的视角不同。它更擅长保留数据的局部邻域结构。什么意思呢?就是同一批次的样本应该靠得近,不同批次如果被强行分开,UMAP会把它放大。
我记得有一次做单细胞转录组数据,PCA看起来三个批次重叠得挺好,但UMAP一跑,其中一批次明显漂出去一个"小尾巴"。这就是局部结构暴露出来的问题。
代码示例:UMAP检测批次效应
import umap
# UMAP降维
reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)
umap_result = reducer.fit_transform(data)
# 绘图
plt.figure(figsize=(8, 6))
sns.scatterplot(x=umap_result[:, 0], y=umap_result[:, 1],
hue=batch_labels, palette='Set2', s=60)
plt.title('UMAP: 批次效应可视化(局部结构保持)')
plt.xlabel('UMAP_1')
plt.ylabel('UMAP_2')
plt.legend()
plt.show()
UMAP有两个重要参数:n_neighbors 控制局部 vs 全局的平衡,min_dist 控制点的聚集程度。你想想看,如果批次效应很细微,把 n_neighbors 调小一点,更容易把局部差异暴露出来。
2.3 热图 — 看表达模式变化
热图展示的是所有样本在所有特征上的表达强度,配合行/列聚类,能直观看到批次是否形成独立的表达模式块。
说白了,如果热图的列聚类把样本按批次分开,形成明显的"色块条带",那就是批次效应的铁证。
代码示例:热图检测批次效应
from scipy.cluster.hierarchy import linkage
import seaborn as sns
# 取前30个特征做展示
data_sub = data[:, :30]
# 行:特征,列:样本
sns.clustermap(data_sub.T, method='ward', metric='euclidean',
col_colors=batch_labels, cmap='RdBu_r',
figsize=(10, 8), xticklabels=False)
plt.show()
我在项目中遇到过一种情况:热图的列聚类先按批次分了三大块,但每块内部又有细分。这说明批次效应存在层级结构——可能是不同实验批次下的子批次差异。这种情况只用PCA很容易忽略。
RdBu_r 或 vlag 这类发散型色板,更容易突出差异。
2.4 箱线图 — 看分布偏移
箱线图看的是单个特征的分布在批次间的变化。虽然它只盯一个特征,但能告诉你偏移的方向和幅度。
比如某个特征在批次1的中位数是5,在批次2变成了8,箱体位置明显错开——这就是分布偏移。多个特征都出现这种情况,批次效应就跑不掉了。
代码示例:箱线图检测批次效应
# 选3个特征展示
features_show = [0, 5, 12]
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for i, feat_idx in enumerate(features_show):
df_plot = pd.DataFrame({
'value': data[:, feat_idx],
'batch': batch_labels
})
sns.boxplot(x='batch', y='value', data=df_plot, ax=axes[i],
palette='Set2')
axes[i].set_title(f'Feature {feat_idx}')
axes[i].set_xlabel('')
plt.tight_layout()
plt.show()
嗯,这里要注意:箱线图对样本量敏感。如果某个批次只有几个样本,箱线图的四分位范围可能不稳定,这时候换成个体数据点的 strip plot 或者 violin plot 会更靠谱。
2.5 四种方法对比与选择
到底用哪种?我建议不要单选,而是组合使用。下面这张表是我在实际项目中的经验总结:
| 方法 | 视角 | 优点 | 不足 | 最适用场景 |
|---|---|---|---|---|
| PCA | 全局方差 | 稳定、可重复、解释性强 | 对局部偏移不敏感 | 初步筛查、整体评估 |
| UMAP | 局部邻域 | 能发现细微分离、可视化效果好 | 随机性较大、参数敏感 | 深度检测、探索性分析 |
| 热图 | 表达模式 | 能同时看所有特征、可发现层级结构 | 特征多时视觉拥挤、需聚类 | 多特征模式识别、层级批次效应 |
| 箱线图 | 单特征分布 | 定量、能看偏移方向和幅度 | 一次只能看少量特征 | 关键特征验证、分布偏移诊断 |
你想想看,这四种方法就像四个不同角度的探照灯。PCA照全局,UMAP照局部,热图照整体模式,箱线图照单特征细节。组合起来,才能把批次效应看得透彻。
2.6 实战中我常用的分析流程
光讲理论不够,我把实际项目中的操作步骤列出来,你可以直接照着来:
- 第一步:PCA快速筛查 — 跑个PCA散点图,看批次是否明显分离。这一步30秒出图,心里先有个数。
- 第二步:UMAP深度检测 — 如果PCA看起来还行,用UMAP再查一遍。调小
n_neighbors到10左右,看局部是否有隐藏偏移。 - 第三步:热图确认模式 — 取表达量变化最大的前50个特征,画聚类热图。看列聚类是否按批次分组。
- 第四步:箱线图定位特征 — 找到热图中差异最明显的几个特征,画箱线图确认分布偏移的方向和幅度。
- 第五步:综合判断 — 如果四种方法超过两种指向"有明显批次效应",那就准备校正吧。
2.7 小结
这一章我们聊了四种可视化检测方法。PCA看全局,UMAP看局部,热图看模式,箱线图看分布。没有哪一种方法是万能的,组合使用才能把批次效应的底细摸清楚。
我个人的工作流是:PCA + UMAP 做快速筛查,热图 + 箱线图做深度确认。四张图出来,批次效应有没有、在哪些特征上、严重到什么程度,心里基本有数了。
可视化检测做扎实了,下一步的校正才能有的放矢。不然你连问题都没看清就急着上手校正,很容易搞出"校正后比校正前更糟"的局面。