4. 批次效应检测与可视化

多批次的 ADMET 数据放一起,最怕什么?

不是数据量不够,也不是特征维度高。我最怕的是——批次效应。说白了,就是不同批次的数据,因为实验条件、仪器、操作人员的差异,产生了非生物学的系统性偏差。你想想看,如果这个偏差没被发现,模型学到的可能就是"哪个批次"而不是"哪个化合物有活性"。

4.1 批次效应从哪来

我刚开始做 ADMET 整合时,觉得把几个批次的数据直接拼接就完事了。结果模型在验证集上性能惨不忍睹。后来一查,是批次效应在作祟。

常见的来源有这些:

  • 仪器漂移:同一台仪器不同时间段的信号会偏移
  • 试剂批次差异:不同批次的试剂活性可能不同
  • 操作人员:不同人做实验,手法总有差异
  • 环境变化:温度、湿度对某些 ADMET 检测很敏感
  • 数据预处理方式:归一化参数、基线校正方法不同
注意:批次效应不等于异常值。它是系统性的,不是随机噪声。不处理的话,模型泛化能力会大打折扣。

4.2 检测方法一览

检测批次效应,我习惯分成两类来看:无监督和有监督。下面这个表格对比了常用方法:

方法 类型 核心原理 适用场景
PCA 无监督 方差最大化投影 快速初筛,高维数据
t-SNE 无监督 概率分布匹配 复杂非线性结构
UMAP 无监督 流形学习 大规模数据,速度快
层次聚类 无监督 距离矩阵 + 链接 样本量较小时
ANOVA 有监督 方差分析 单变量,已知批次标签
RLE (相对表达) 半监督 中位数对齐偏差 归一化效果评估

我个人习惯是:先用 PCA 快速扫一遍。如果不同批次在主成分空间里明显分开,那批次效应基本实锤了。

小技巧:PCA 跑完后,看第一主成分的方差占比。如果 PC1 就占了 40% 以上,而且和批次标签高度相关——嗯,要警惕了。

下面这张图概括了批次效应检测与可视化的核心流程:

批次效应检测与可视化流程 多批次 ADMET 数据 PCA 降维分析 t-SNE / UMAP ANOVA / RLE 检验 可视化诊断:散点图|热图|箱线图|RLE 图 量化评估:ASW 轮廓系数|PCR 主成分回归 决策:存在批次效应?→ 矫正 / 弃用 建议:先用 PCA 快速筛查,再用统计检验确认,最后用量化指标评估严重程度

4.3 可视化诊断细节

光看数字不够直观。我习惯把检测结果画出来,一眼就能看出问题。

4.3.1 PCA 散点图

最经典的做法。用前两个主成分做散点图,按批次着色。如果不同批次各自抱团,那就是有批次效应。

4.3.2 热图

看样本间的距离矩阵。批次内部的样本距离近,批次之间距离远——热图上会形成明显的块状结构。

4.3.3 箱线图 / RLE 图

RLE(Relative Log Expression)图是我觉得最直观的方法之一。它看每个样本的表达值偏离整体中位数的程度。如果某个批次的样本整体偏离,那就有问题。

核心经验:RLE 图中,理想状态下所有箱线应该以 0 为中心对称分布。如果某个批次的箱子整体上移或下移,说明该批次存在系统性偏差。

4.4 量化评估指标

可视化靠肉眼,但最终做决策还是得看数字。我常用的指标有三个:

指标 全称 范围 说明
ASW Average Silhouette Width [-1, 1] 按批次标签计算,越接近 0 说明批次混合得越好
PCR Principal Component Regression [0, 1] 用主成分预测批次标签,R² 越高说明批次效应越强
批次熵 Batch Entropy [0, ∞) 衡量批次混合程度,值越大混合越均匀
我的判断标准:ASW 大于 0.3 就需要注意了;PCR 的 R² 超过 0.2 说明批次效应已经不可忽视。

4.5 动手实践:用 Python 检测批次效应

说这么多,不如跑一段代码。下面是我常用的检测流程:

import pandas as pd
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

# 模拟三批 ADMET 数据
np.random.seed(42)
n_per_batch = 60
n_features = 12

batch1 = np.random.normal(0, 0.8, (n_per_batch, n_features))
batch2 = np.random.normal(1.2, 0.9, (n_per_batch, n_features))
batch3 = np.random.normal(0.5, 1.1, (n_per_batch, n_features))

X = np.vstack([batch1, batch2, batch3])
batches = ['Batch-A']*n_per_batch + ['Batch-B']*n_per_batch + ['Batch-C']*n_per_batch

# 标准化 → PCA
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

print(f'PC1 方差占比: {pca.explained_variance_ratio_[0]:.3f}')
print(f'PC2 方差占比: {pca.explained_variance_ratio_[1]:.3f}')

# 可视化
plt.figure(figsize=(8, 6))
colors = {'Batch-A': '#3498db', 'Batch-B': '#e74c3c', 'Batch-C': '#2ecc71'}
for b in ['Batch-A', 'Batch-B', 'Batch-C']:
    idx = [i for i, v in enumerate(batches) if v == b]
    plt.scatter(X_pca[idx, 0], X_pca[idx, 1],
                c=colors[b], label=b, alpha=0.7, edgecolors='w', s=50)
plt.xlabel('PC1 ({:.1f}%)'.format(pca.explained_variance_ratio_[0]*100))
plt.ylabel('PC2 ({:.1f}%)'.format(pca.explained_variance_ratio_[1]*100))
plt.title('PCA 批次效应检测')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

运行这段代码,你会看到 Batch-B 明显向右偏移。这说明批次效应存在,需要进一步处理。

4.5.1 计算 ASW 指标

from sklearn.metrics import silhouette_score

# 用所有主成分计算轮廓系数
pca_full = PCA(n_components=min(X.shape[0], X.shape[1]))
X_pca_full = pca_full.fit_transform(X_scaled)

asw = silhouette_score(X_pca_full, batches)
print(f'ASW (按批次): {asw:.3f}')

if asw > 0.3:
    print('⚠️ 批次效应明显,建议进行矫正')
else:
    print('✅ 批次效应在可接受范围')
注意:ASW 的计算使用 PCA 全部主成分,不要只用前两个。否则会丢失高维信息,导致评估结果偏乐观。

4.6 避坑指南

我曾经在一个项目中,用 PCA 看到三个批次完全重叠,就以为没有批次效应。结果模型上线后,预测结果按批次分层严重。后来才发现——PCA 对非线性批次效应不敏感。

  • 不要只依赖一种方法。PCA + t-SNE + 统计检验,至少两种互相印证。
  • 不要只看可视化,不看量化指标。肉眼容易骗自己,数字不会。
  • 注意样本量差异。如果某个批次样本特别少,PCA 可能把它"吞"掉,看起来像是混合了,其实只是被淹没了。
  • 批次标签要准确。我曾经遇到过批次标签搞混的情况,结果检测出来"批次效应"其实是错误标签导致的。

总结一句话:批次效应检测不是走形式,而是为后续的矫正或整合策略提供依据。检测做扎实了,后面的建模才能站得住脚。