第3章:数据清洗与标准化

数据清洗这事儿,说起来不复杂,做起来真能让人抓狂。我在做多批次ADMET整合时,遇到过最夸张的一次——同一批化合物在三个批次里用了三种不同的浓度单位。嗯,这要是直接合并,后面的模型全得崩。

说白了,多批次数据整合的核心难点不在「合」,而在「洗」。你想想看,不同实验室、不同时间、不同仪器出来的数据,格式不统一、单位不一致、缺失值满天飞,这些才是真正的拦路虎。这一章咱们就专门聊聊怎么把这些「脏数据」洗得干干净净。

3.1 多批次ADMET数据的「脏」从哪来

先看看常见的问题类型。我把它归纳成五类,每一类我都踩过坑。

问题类型 典型表现 常见原因
缺失值 部分ADMET指标为空 实验未完成、仪器故障、记录遗漏
异常值 数值偏离正常范围几个数量级 操作失误、仪器漂移、单位误用
格式不统一 日期格式、化合物命名、SMILES写法不同 不同批次使用不同模板
单位不一致 浓度从μM到nM到mg/mL都有 实验室习惯不同、未约定标准
重复记录 同一化合物出现多次但数值不同 批次交叉、合并时未去重
注意: 千万别觉得数据「看起来还行」就直接合并。我见过太多团队跳过清洗步骤,结果模型训练出来全是噪声。清洗这一步省下来的时间,后面会花十倍去排查。

3.2 数据清洗的通用流程

我自己总结了一套流程,这些年一直在用。不一定适合所有场景,但八九不离十。

  1. 概览扫描 —— 先看数据长什么样,心里有个底
  2. 缺失值处理 —— 删、补、标记,三选一
  3. 异常值检测 —— 用统计方法或业务规则揪出来
  4. 格式标准化 —— 统一命名、日期、SMILES
  5. 单位统一 —— 全部转成标准单位
  6. 去重与校验 —— 去掉重复,交叉验证一致性

下面这张图能帮你快速建立整体认知。

数据清洗与标准化流程 多批次ADMET数据清洗与标准化流程 原始多批次数据 ① 缺失值处理 ② 异常值检测 ③ 格式标准化 单位统一(μM ↔ nM ↔ mg/mL) 命名统一(SMILES ⇔ InChI ⇔ ID) 重复记录去重 + 交叉校验 ✅ 清洗后标准化数据集 💡 我的习惯 每清洗一步,都保存中间结果 方便回溯排查问题 别把所有操作揉在一个单元格
个人习惯: 我每做完一步清洗,都会把中间结果存一份CSV。要是后面发现数据对不上,能快速回溯是哪一步出了问题。别笑,这习惯救过我好几回。

3.3 缺失值处理

缺失值怎么处理?没有万能答案。我个人把它分成三种情况:

  • 直接删除 —— 缺失比例小于5%,且随机分布
  • 填充 —— 用均值、中位数、或者同类批次的平均值补上
  • 标记为特殊值 —— 比如用 -999 或 NaN,让模型自己处理

下面这段代码是我常用的缺失值扫描模板:

import pandas as pd
import numpy as np

def scan_missing(df, batch_col='batch_id'):
    """扫描每个批次的缺失情况"""
    report = []
    for batch in df[batch_col].unique():
        subset = df[df[batch_col] == batch]
        missing_pct = subset.isnull().mean() * 100
        report.append({
            'batch': batch,
            'total_rows': len(subset),
            'missing_cols': missing_pct[missing_pct > 0].to_dict()
        })
    return pd.DataFrame(report)

# 使用示例
df = pd.read_csv('admet_raw.csv')
report = scan_missing(df)
print(report[['batch', 'total_rows', 'missing_cols']])
关键点: 缺失值处理一定要按批次分别看。有些批次的某个指标可能全是空的,那就得回溯确认是实验设计问题还是记录问题。我之前有个项目,批次的Caco-2渗透性数据全为空,后来发现是那个批次根本就没做这个实验。

3.4 异常值检测

异常值检测我只用两种方法,简单但够用。

方法一:Z-score 法

from scipy import stats

def detect_outliers_zscore(df, col, threshold=3):
    z = np.abs(stats.zscore(df[col].dropna()))
    return df[col].dropna().index[z > threshold]

# 使用
outliers = detect_outliers_zscore(df, 'logD', threshold=3)
print(f"发现 {len(outliers)} 个潜在异常值")

方法二:IQR 法

def detect_outliers_iqr(df, col):
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return df[(df[col] < lower) | (df[col] > upper)].index

outliers_iqr = detect_outliers_iqr(df, 'solubility')
踩坑提醒: 我遇到过一批数据logP值都在1~3之间,突然来了个12.5。一查,原来是单位写错了,把mg/mL当成了mol/L。这种异常值不是统计学能解决的,必须结合业务知识判断。所以我的习惯是:先用统计方法筛一遍,再人工复查疑似异常。

3.5 格式与单位标准化

这一块最琐碎,但也最重要。我把它拆成三个维度:

维度 常见差异 标准化方案
化合物标识 SMILES / InChI / 自定义ID混用 统一转为标准SMILES(用rdkit标准化)
数值单位 μM / nM / mg/mL / % 全部转为μM或标准SI单位
日期格式 2024/01/01 / 01-01-2024 / 20240101 统一为 ISO 8601: YYYY-MM-DD

来个实际例子。假设我们要把浓度统一成μM:

def standardize_concentration(value, unit):
    """
    将不同单位的浓度统一转为 μM
    """
    conversion = {
        'mM': 1000,
        'μM': 1,
        'nM': 0.001,
        'mg/mL': 'need_mw',  # 需要分子量
    }
    if unit in conversion:
        if conversion[unit] == 'need_mw':
            # 需要额外传分子量做换算
            raise ValueError("mg/mL 需要分子量信息才能换算")
        return value * conversion[unit]
    else:
        raise ValueError(f"未知单位: {unit}")

# 批量处理
df['conc_μM'] = df.apply(
    lambda row: standardize_concentration(row['conc'], row['conc_unit']),
    axis=1
)
建议: 单位换算的映射表一定要单独维护,别硬编码在代码里。我之前就是硬编码,结果换了个项目全得重写。现在都改成JSON配置文件,改配置不改代码。

3.6 去重与交叉校验

多批次数据最容易出现同一个化合物在不同批次里重复。怎么去重?我一般分两步:

  1. 基于结构去重 —— 用标准化SMILES做唯一键
  2. 保留最优批次 —— 根据数据质量和实验日期排序
def deduplicate_by_structure(df, smiles_col='SMILES', date_col='assay_date'):
    """
    基于标准化SMILES去重,保留最新批次的数据
    """
    # 先标准化SMILES
    from rdkit import Chem
    df['canonical_smiles'] = df[smiles_col].apply(
        lambda x: Chem.MolToSmiles(Chem.MolFromSmiles(x)) if pd.notna(x) else x
    )

    # 按SMILES分组,保留日期最新的那条
    df_sorted = df.sort_values(date_col, ascending=False)
    df_dedup = df_sorted.drop_duplicates(subset='canonical_smiles', keep='first')

    return df_dedup.reset_index(drop=True)

df_clean = deduplicate_by_structure(df, smiles_col='SMILES', date_col='assay_date')
print(f"去重前: {len(df)} 行, 去重后: {len(df_clean)} 行")
交叉校验的小技巧: 用同一种化合物在不同批次中的QC(质量控制)数据做比对。如果QC结果偏差超过预设阈值(比如logP > 0.3),那这批数据就得标记为「存疑」,不能直接进最终数据集。这是个好习惯,我每个项目都会做。

3.7 清洗后的数据质量检查

清洗完别急着往下走。我通常会跑一个质量检查清单:

  • 每个批次的缺失率是否都低于5%?
  • 所有数值列的分布是否在预期范围内?
  • 单位是否全部统一?
  • 是否有重复的化合物ID?
  • SMILES是否都能被rdkit正确解析?

这个检查清单我直接写在代码里,每次清洗完自动跑一遍,不过关就打回重洗。

嗯,这里要注意: 质量检查的标准不是固定的。早期探索阶段可以放宽一些,到了建模阶段就得严格。我自己会在项目开始前就跟团队定好「清洗通过标准」,省得后面扯皮。

数据清洗和标准化这一步,看着琐碎,但真的值得花时间。你想想看,如果数据质量不过关,后面再牛的模型也是白搭。我这些年最大的体会就是——清洗做得越细,建模越省心。