第3章 批次效应可视化:PCA、t-SNE等展示数据分布

你拿到一批质谱数据,第一件事是什么?我个人习惯,不是急着跑差异分析,而是先做可视化。说白了,就是先看看数据长什么样,有没有明显的批次结构。这一步做扎实了,后面建模才心里有底。

可视化批次效应,主流方法就那几种:PCA、t-SNE、UMAP,偶尔也用到MDS。它们背后的数学原理可以很复杂,但在咱们实战里,你只需要记住一件事:这些方法能把高维数据压到2维或3维,让你用肉眼判断样品是否按批次聚类。我曾经见过一个团队,直接拿原始强度值跑PCA,结果全乱套——因为方差最大的主成分被几个高丰度代谢物带偏了。所以,先处理数据再用可视化,顺序不能错。

3.1 数据预处理:从原始矩阵到降维输入

先给原始强度矩阵做个log2转换,这是质谱数据的常规操作。然后按特征(代谢物/峰)做z-score标准化,让每个特征均值为0、方差为1。

为什么要标准化?如果不做,PCA会偏向方差大的特征——那些高丰度的代谢物可能压掉真正的批次差异。我再告诉你一个坑:标准化之前一定要检查缺失值。我带的某个项目中,一批QC样本测出来的峰面积全为0,如果不剔除,z-score之后那批样本全部被压缩成一个点,视觉上变成离群簇,误导了整整一周的分析。

下面是个典型的R语言预处理代码,我习惯用scale()一步到位:

# 读取原始强度矩阵 (行: 样本, 列: 代谢物)
data_raw <- read.csv("peak_intensity.csv", row.names = 1)

# log2转换 (加1避免log0)
data_log <- log2(data_raw + 1)

# z-score标准化 (按列)
data_scaled <- scale(data_log)

# 检查缺失: 任意一列有NA就剔除或插补
data_scaled <- data_scaled[, colSums(is.na(data_scaled)) == 0]
小提示:如果样本量少于特征数,PCA照样能算,但结果可能不稳定。这时可以先用方差过滤,只保留变异系数前500-1000的特征。

预处理后的矩阵就是降维的输入了。记住,PCA和t-SNE接收的都是样本×特征矩阵,行是样本,列是变量。

3.2 主成分分析(PCA)——快速检查批次效应

PCA是我最早接触的可视化方法。它的核心思想是找到一组正交方向(主成分),让投影后的样本方差最大。前两个PC通常能解释总变异的30%~60%,如果批次效应足够强,你甚至不需要调参数就能看到明显分离。

实战代码,还是用R的prcomp():

# PCA
pca_result <- prcomp(data_scaled, center = FALSE, scale. = FALSE)

# 提取前两主成分得分
pca_df <- as.data.frame(pca_result$x[, 1:2])
pca_df$Batch <- factor(sample_meta$Batch)  # 批次标签来自样本信息表

# 绘制散点图
library(ggplot2)
ggplot(pca_df, aes(x = PC1, y = PC2, color = Batch)) +
  geom_point(size = 3) +
  stat_ellipse(level = 0.95) +
  theme_minimal() +
  labs(title = "PCA: Sample Distribution by Batch")

你得到了这张图,怎么看?如果不同批次的样本在PC1/PC2平面上明显分开,比如一批全在左边、一批全在右边,那就是典型的批次效应。但如果只是彼此交错,或者只有个别离群点,问题可能不大。

避坑:我曾经遇到一个情况,PCA图上看批次完全重叠,我还以为数据很干净。后来用t-SNE才发现,原来批次差异隐藏在高维局部结构里,PCA只能看到全局方差最大的方向,错过了细微但真实的批次漂移。所以,PCA只能当作初筛,不能完全相信它。

3.3 t-SNE——局部结构下的批次发现

t-SNE(t-distributed Stochastic Neighbor Embedding)和PCA的思路完全不同。它不关心全局方差,而是尽量保留样本间的局部邻域关系——说白了,就是让原来离得近的样本在二维图上仍然挨着,离得远的样本彻底分开。这使得t-SNE特别擅长揭示那些在PCA中看不到的“小批次效应”。

但t-SNE有个麻烦的参数:perplexity(困惑度)。它大致代表每个点的“有效邻居数”。经验值一般在5~50之间,太小了图会成团分散,太大了又过于平滑。我的经验是,对于几百个样本的数据,先设30,如果看不清楚再往小调。而且一定要固定随机种子,否则每次运行结果都不一样。

R语言实现,用Rtsne包:

library(Rtsne)

set.seed(42)  # 固定随机种子
tsne_result <- Rtsne(data_scaled, 
                     dims = 2, 
                     perplexity = 30, 
                     verbose = TRUE)

tsne_df <- as.data.frame(tsne_result$Y)
colnames(tsne_df) <- c("tSNE1", "tSNE2")
tsne_df$Batch <- factor(sample_meta$Batch)

ggplot(tsne_df, aes(x = tSNE1, y = tSNE2, color = Batch)) +
  geom_point(size = 3) +
  theme_minimal() +
  labs(title = "t-SNE: Sample Distribution by Batch")
重点:t-SNE的迭代过程是随机的,哪怕用同样的perplexity,不同运行也会产生略微不同的图。我建议跑3次,如果3次的聚类模式基本一致,那这个批次效应就是可靠的。如果每次的簇不一样,别慌,先检查数据,可能批次效应本身很弱。

你可能会问:为什么t-SNE比PCA更容易发现批次效应?因为质谱数据中,批次往往体现在局部特征模式的漂移(比如某段保留时间内的峰强度集体偏移),这些变化对全局方差贡献不大,但对局部邻域的影响却很显著。t-SNE刚好擅长捕捉这种模式。我曾在一次合作项目中,PCA图显示两批样本混在一起,但t-SNE却分出了两个明显的簇——后来追踪实验原始记录,发现第二批的色谱柱确实换了。那一次的经验让我从此把t-SNE作为必选项。

3.4 其他可视化方法(UMAP、MDS等)

除了PCA和t-SNE,还有几个常用方法,简单对比一下:

方法原理类型全局/局部计算速度典型场景
PCA线性降维全局极快快速初筛
t-SNE非线性降维局部较慢发现小批次效应
UMAP非线性降维兼顾局部+全局快大数据集批次可视化
MDS线性/非线性取决于距离度量中等当需要保留原始距离时

我个人在项目中,PCA + t-SNE是最常用的组合。如果样本量超过2000,我会换用UMAP,因为它快且能保留更多全局结构。UMAP的R包是umap,Python里是umap-learn,用法和t-SNE类似。

3.5 综合判断批次效应的可视化策略

最后,我画一张流程图来总结这个可视化决策流程——你每次拿到新数据,都可以照着走一遍。

批次效应可视化决策流程 原始强度矩阵 log2 + z-score 缺失值处理 PCA 初筛 批次是否 明显分离? 是 批次效应显著 记录并校正 否 t-SNE / UMAP 局部簇是否 按批次分组? 是 存在批次漂移 考虑校正 否 未见明显批次效应 可以直接融合 说明 t-SNE/UMAP 需多次运行 确认聚类稳定性。 perplexity/n_neighbors 需要微调。

这个流程图的逻辑很直白:先跑PCA快速看全局,如果批次明显分离就直接确认;如果看不出分离,再用t-SNE或UMAP深入看局部。局部也看不出,那这批次效应可能真的弱到可以忽略,你放心融合就是了。

经验之谈:我建议每次把PCA和t-SNE的结果放在一起,生成一个“双视图”报告。PCA能让你跟合作者解释“全局怎么看”,t-SNE能帮你发现“到底有没有隐藏的坑”。两者互相印证,结论才站得住。

最后提一句:可视化只是定性手段,要想量化批次效应的强度,你还需要配合后面章节会讲到的统计检验(如ANOVA、PERMANOVA)。不过先把图看顺了,大脑的直觉往往比数字更敏锐。今天就先到这里,你可以拿自己的数据试试,按照这个流程走一遍。