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、是否重复、失败原因。对失败记录不要静默丢弃,而应进入“待人工复核”队列。最后用一批已知药物分子做回归测试,确认清洗前后分子式、电荷和手性中心数量符合预期。
整条流水线的关键不是“跑通”,而是“可解释、可回滚、可审计”。每一步都应保留中间产物,任何标准化决策都要有记录。对同一批数据,建议先用小样本(如 1000 条)验证规则,再全量执行;对失败和冲突记录,进入人工复核队列,而不是直接删除。只有把清洗做成工程化流水线,后续的分子图表示学习才有可靠的数据基础。