数据预处理基础:清洗、标准化、缺失值处理

做药物批次一致性校验,最耗时间的往往不是模型调参,而是数据预处理。
我见过太多团队拿着原始实验数据直接跑校验,结果被异常值、批次漂移、缺失孔位搞得头大。
说白了,数据预处理做不好,后面所有统计都是白搭。

这一章我带你过一遍数据预处理的三块核心:清洗、标准化、缺失值处理。
这些都是我在实际项目中反复踩过坑之后总结出来的套路。

药物数据预处理流程 原始实验数据 数据清洗 去重·去掉无效孔 标准化 Z-score / 批次校准 缺失值处理 删除 / 填充 / 插值 预处理完成数据 用于批次一致性校验 💡 预处理要按顺序? 不一定严格先后,但建议先清洗再标准化,缺失值处理看实际情况灵活调整。 我习惯先做清洗(去掉明显问题数据),再做缺失值评估,最后标准化。

1. 数据清洗:先把烂数据筛出去

药物筛选实验的原始数据里,总有一些不靠谱的记录。
比如整孔死光、加样失误、仪器报错、或者人员操作弄反了板位。

我个人习惯先做三件事:

  • 去除重复记录——同一化合物在两个孔里数据完全一致?要么是系统重复读了,要么是人工复制没处理干净。
  • 去除无效孔——比如阳性对照孔全无效,或者阴性对照整体飘移;我一般设一个阈值,超过±3倍中位绝对偏差的直接标红。
  • 标记异常值——不一定要删除,但至少要在后续分析时知道这些点哪里有问题。

我曾经在一个项目里,发现某批次所有数据都比历史批次低了30%。查了半天,原来那一板的激发光读错波长。清洗阶段如果只靠自动化规则,这种系统偏差根本发现不了。所以我建议:清洗一定要配合可视化——画个板图热力图,一眼就能看出哪块板有问题。

你知道为什么清洗要放在第一步吗?
因为标准化和缺失值处理都会受异常值干扰。如果数据里混着一个跑偏500倍的孔,Z-score算出来的均值和标准差会变形,影响整批数据。

# 简单的清洗规则示例(Python风格伪代码)
def cleaner(df):
    # 1. 去重
    df = df.drop_duplicates(subset=["compound_id", "batch_id", "well_id"])
    # 2. 去掉无效孔(比如死细胞对照)
    df = df[df["cell_viability"] < 5]  # 死细胞标志
    # 3. 标记极端异常(不做删除,加列标记)
    mean = df["response"].mean()
    std  = df["response"].std()
    df["outlier_flag"] = (df["response"] - mean).abs() > 3 * std
    return df

2. 标准化:让不同批次能放在一张图里比

标准化是我在批次一致性校验里最看重的一步。
不同时间做的实验,即使操作完全相同,也会因为试剂老批次、室温差异、仪器漂移产生系统性偏移。

常用的标准化方法:

方法适用场景我在项目中踩过的坑
Z-score 标准化数据近似正态分布,且批次间分布形状相似中位数绝对偏差(MAD)比标准差更稳健;我吃过异常值拖拽均值的亏
Min-Max 归一化数据有明确上下界,例如活性百分比新批次的min/max可能超出历史范围,归一化后边界外数据被压扁
中位数中心化非参数情况,分布不对称稳健性强,但会丢失绝对量级信息——适合比值型指标
批次效应消除(ComBat等)多批次、多个协变量需要足够的样本量,否则过度校正产生反效果

注意:标准化参数必须在所有批次上统一计算,不能每个批次单独标准化。否则你相当于把每个批次自己的尺子重新刻了一遍,批次间差异反而被掩盖了。

我记得有一次做药物复筛,两个批次用Z-score分别标准化,结果两批数据的IC50曲线几乎重合。客户很兴奋,说“数据一致性很好”。但我知道这事有问题——后来我改用统一参比(把所有批次合并算均值和标准差),重新标准化,才暴露出第二批次整体右移了半个量级。你想想看,这种“假一致性”多坑人?

# 统一参数标准化的做法(所有批次用一个scale)
all_data = pd.concat([batch1, batch2, batch3])
mean_all = all_data["response"].mean()
std_all  = all_data["response"].std()

for batch in [batch1, batch2, batch3]:
    batch["response_std"] = (batch["response"] - mean_all) / std_all

3. 缺失值处理:不是所有空位都需要填

药物筛选数据经常有缺失值。原因很多:加样时气泡、仪器读到一半信号丢失、板位编号写错导致匹配不上。

我一般按缺失率分层处理:

  • 缺失率 < 5%:直接删除该孔数据,或者用同一化合物其他批次的均值填充。
  • 缺失率 5%–20%:用插值方法,比如邻近孔均值、时间序列插值、或基于同板对照的回归填充。
  • 缺失率 > 20%:该板或该条件应该整体重测,不要强行填充。

特别提醒:不要对阳性对照和阴性对照的缺失值做复杂填充!对照孔的目的是定义实验窗口,填错了整个窗口就歪了。我有个学员就犯过这错——用其他孔均值填入阳性对照,结果导致整板活性计算都偏低。

缺失值处理的另一个关键是:
填充之后一定要标记哪些值是真实值,哪些是填充值。我在数据集中专门加一列 is_imputed,后续统计时单独对比填充前后的分布是否一致。

为什么这样做?
举个例子:你用最近邻填充了一个孔的数据,但这个孔刚好在边缘,填充值可能带系统性偏差。如果后续模型不区分原始值和填充值,就会引入偏差。所以我宁愿多花一点标记的成本,也不愿把坏数据混进去。

# 简单缺失值填充示例(使用同一化合物不同批次的均值)
compound_means = df.groupby("compound_id")["response"].transform("median")
df["response"].fillna(compound_means, inplace=True)

# 标记填充来源
df["is_imputed"] = df["response"].isna()  # 填充前

我个人还有一个习惯:在预处理结束后,单独输出一份“预处理报告”。报告里记录清洗了哪些孔、标准化参数是什么、缺失值填充了多少个。这样后续校验出问题时,可以快速回溯是哪个环节导致的。

好了,数据预处理的基础就这些。别小看这一步,做扎实了,后面的批次一致性校验才能真的靠得住。