3. 分子图的数据清洗与标准化

分子图数据工程的第一步不是建模,而是把来源各异、质量参差的分子记录整理成“同一套语义、同一套表示”的干净图。原始数据可能来自 ChEMBL、PubChem、专利文本、供应商 SDF 或自建实验记录,同一化合物常以盐、水合物、不同互变异构体、不同电荷形式甚至不同立体描述出现。若不清洗,后续指纹、GNN 消息传递和相似性检索都会把“同一分子”当成多个对象,导致标签泄漏、重复计数和模型虚高。本节围绕结构解析、去盐、去重、互变异构体标准化、电荷与立体化学处理,给出一条可落地的清洗流水线。

一、结构解析:把 SMILES/SDF 变成可信的分子图

结构解析的目标是得到原子、键、电荷、氢原子数、芳香性和立体信息的完整描述。常用工具是 RDKit。解析时不能只调用一次 MolFromSmiles 就结束,而要检查返回是否为 None,并做 sanitize 与价态校验。例如:

from rdkit import Chem
from rdkit.Chem import Descriptors

def parse_mol(smi):
    mol = Chem.MolFromSmiles(smi, sanitize=True)
    if mol is None:
        return None, "parse_fail"
    try:
        Chem.SanitizeMol(mol)
    except Exception as e:
        return None, f"sanitize_fail:{e}"
    if any(a.GetNumRadicalElectrons() > 0 for a in mol.GetAtoms()):
        return None, "radical"
    return mol, "ok"

解析阶段要记录失败原因,而不是简单丢弃。常见失败包括:非法价态(如五价碳)、未闭合环、芳香性无法感知、金属配位未标注。对 SDF 还要检查 MolFromMolBlock 是否保留手性块和电荷块。解析结果建议统一转为 canonical SMILES 作为中间键,但 canonical SMILES 本身不解决盐和互变异构问题,只是为后续步骤提供稳定入口。

二、去盐与去溶剂:保留最大有机片段

盐和溶剂会显著改变分子量、电荷和指纹。去盐的通用策略是:按片段拆分,保留最大的有机共价片段,同时记录被剥离的反离子和溶剂,便于审计。RDKit 可用 GetMolFrags 或 rdMolStandardize 中的 LargestFragmentChooser。示例:

from rdkit.Chem.MolStandardize import rdMolStandardize

def desalt(mol):
    chooser = rdMolStandardize.LargestFragmentChooser()
    parent = chooser.choose(mol)
    return parent

需要注意:并非所有“最大片段”都是母体。例如季铵盐的阳离子是活性部分,而盐酸盐的 Cl⁻ 应被去除;某些药物以钠盐形式存在,Na⁺ 应剥离。对含金属的配合物,去盐规则要单独配置,不能一刀切。去盐后应重新计算电荷和氢原子数,并保留原始记录 ID 与去盐后 SMILES 的映射表。

三、去重:从字符串去重到图同构去重

去重不能只比较原始 SMILES 字符串,因为同一分子可能有多种写法。正确做法是:先做标准化(去盐、电荷归一、互变异构归一),再生成 canonical SMILES 或 InChIKey 作为去重键。InChIKey 的前 14 位(骨架层)可用于快速粗筛,完整 27 位用于精确去重。若数据量在百万级,建议用哈希表存储 canonical SMILES,并保留首次出现的记录作为代表,其余记录标记为重复并记录来源。对于立体异构体,若业务需要区分对映体,则去重键必须包含立体层;若只关心骨架,则使用骨架层 InChIKey。

四、互变异构体标准化:让同一分子收敛到同一形式

互变异构是分子图清洗中最容易被忽视的问题。酮-烯醇、酰胺-亚胺醇、咪唑等体系在不同来源中可能写成不同形式。RDKit 提供 TautomerEnumerator 与 CanonicalTautomer,可把互变异构体集合收敛到规范形式。示例:

from rdkit.Chem.MolStandardize import rdMolStandardize

def canonical_tautomer(mol):
    enumerator = rdMolStandardize.TautomerEnumerator()
    enumerator.SetMaxTautomers(1000)
    return enumerator.Canonicalize(mol)

标准化时要设定规则:是否允许电荷参与互变异构、是否保留特定官能团形式。对含氮杂环,建议统一为更稳定的酰胺或芳香形式;对糖类,互变异构可能涉及开链-环状平衡,通常不强行归一,而应保留原始形式并标注。标准化后要重新生成 canonical SMILES,并检查分子式是否变化——若变化,说明标准化过程引入了错误。

五、电荷与立体化学处理

电荷处理包括:中和可电离基团、统一电荷形式、去除无意义的电荷分离。常用规则是:羧酸根质子化、胺基质子化、四价氮保留正电荷。RDKit 的 Uncharger 可做部分中和,但需人工确认。立体化学处理要区分“未指定”“指定但不确定”“明确 R/S”。对未指定手性的分子,不能随意补全;对明确手性,应保留并检查楔形键是否与 3D 一致。若下游任务不区分对映体,可去除立体信息,但必须记录该决策。

六、清洗流水线设计

把上述步骤串成可复现的流水线,建议按“解析→去盐→电荷归一→互变异构标准化→立体处理→去重→输出”的顺序执行,每一步都记录输入输出和失败原因。下面给出一个可运行的流水线骨架:

def clean_pipeline(smi, keep_stereo=True):
    mol, status = parse_mol(smi)
    if mol is None:
        return None, status
    mol = desalt(mol)
    mol = rdMolStandardize.Uncharger().uncharge(mol)
    mol = canonical_tautomer(mol)
    if not keep_stereo:
        Chem.RemoveStereochemistry(mol)
    can_smi = Chem.MolToSmiles(mol, isomericSmiles=keep_stereo)
    return can_smi, "ok"

流水线要输出一张审计表,字段包括:原始 ID、原始 SMILES、解析状态、去盐后 SMILES、标准化后 SMILES、InChIKey、是否重复、失败原因。对失败记录不要静默丢弃,而应进入“待人工复核”队列。最后用一批已知药物分子做回归测试,确认清洗前后分子式、电荷和手性中心数量符合预期。

分子图清洗流水线:从原始记录到标准化分子图 原始记录 SMILES / SDF / 专利 结构解析 sanitize / 价态校验 去盐 / 去溶剂 最大有机片段 电荷归一 中和 / 保留季铵 互变异构标准化 CanonicalTautomer 立体化学处理 保留 / 去除手性 去重 InChIKey / canonical 标准化分子图 canonical SMILES 审计表字段(示例) 原始 ID 原始 SMILES 标准化 SMILES InChIKey 是否重复 失败原因 解析状态 去盐后 SMILES 手性中心数 电荷 分子式 来源批次 待人工复核 解析失败 价态异常 金属配合物 互变异构冲突 立体信息缺失

整条流水线的关键不是“跑通”,而是“可解释、可回滚、可审计”。每一步都应保留中间产物,任何标准化决策都要有记录。对同一批数据,建议先用小样本(如 1000 条)验证规则,再全量执行;对失败和冲突记录,进入人工复核队列,而不是直接删除。只有把清洗做成工程化流水线,后续的分子图表示学习才有可靠的数据基础。