第2章 分子数据常见质量问题

做化学信息学的人,十个有九个都栽在数据质量上。我刚入行那会,从公开数据库拉了几百万条SMILES,自以为拿到宝藏了,一跑模型效果烂得没法看。后来一条条排查才发现——嗯,数据里全是坑。格式错、表示乱、重复多、甚至结构根本画不出来。今天我们就掰开揉碎聊聊这四类最常见的问题。

一句话总结:SMILES数据清洗如果只做一步,那必须是去重+合法校验。其他问题可以分批处理,但这两步不做,后面的建模全是废的。

2.1 格式错误——你以为你写对了,其实并没有

格式错误是最容易发现但又最容易忽略的问题。说白了,就是SMILES字符串根本不符合语法规则。比如括号不匹配、元素符号大小写搞反、环数字号重复等等。

我在一个合作项目里遇到过这样的情况:对方给的SMILES里出现了 Cl 写成了 C l(中间多了空格),还有把 [Na+] 写成 Na+ 不带方括号。当时我写了一个正则去解析,结果一堆漏掉的。后来还是用的RDKit的 MolFromSmiles 一把抓——能转成分子对象的就是合法的,转不了的就是有问题的。

常见格式错误类型 错误示例 正确写法
括号不配对 CC(C)C CC(C)C
元素符号大小写错误 c1ccccc1(本是C 大写) C1CCCCC1
电荷写法不规范 [Fe+3] 写成 Fe+3 [Fe+3]
环数字号重复或缺失 C1CC1C2CC2C1(一号用两次) C1CC1C2CC2

避坑指南:我曾经碰到一条SMILES长到上百个字符,里面某个环数字号写错了,RDKit照样能解析但给出的分子结构是错的。所以别光看能否转成分子,还要检查原子数、环数等是否合理。建议用 Chem.SanitizeMol 做一次标准化。

2.2 非标准化表示——同一个分子,一百个人有一百种写法

你有没有想过,苯环的SMILES可以写成 c1ccccc1 也可以写成 C1=CC=CC=C1,甚至有人写成 c1c[cH:1]ccc1。这些写法都对,但放在数据库里就是灾难。模型训练时,同一个分子因为SMILES表示不同,会被当成两个样本。

我个人习惯做标准化时,统一用RDKit的 MolToSmiles 重新输出一遍。它会按标准顺序排原子、处理芳香性、加上显式氢等。举个例子:

from rdkit import Chem
smiles_variants = ["c1ccccc1", "C1=CC=CC=C1", "C1CCCC=C1"]
for s in smiles_variants:
    mol = Chem.MolFromSmiles(s)
    print(Chem.MolToSmiles(mol))
# 输出全部是:c1ccccc1

你看,不管输入多乱,输出都是统一的。你想想看,这能省多少清洗的功夫。

小技巧:除了分子层面的标准化,有时候还需要考虑同位素、手性、电荷表示的一致性。比如 [C@@H] 和 [C@H],如果项目里不在意立体化学,可以用 Chem.RemoveStereochemistry 去掉手性信息。我在做QSAR模型时就是这么干的,立体化学信息反而增加了噪声。

2.3 重复记录——你以为的“大数据”其实是水分

重复记录这个问题,我估计每个做数据库的人都踩过。公开数据集里重复率能达到10%~30%。有些重复是明面上的——SMILES字符串完全一样。有些则是隐式重复——比如 CCO 和 CCO 是不是同一个?是。但前面我们说了非标准化表示,可能导致 CCO 和 C(O)C 其实是同一个分子(乙醇),但字符串不同。

所以去重不能只靠字符串判等。我的做法是:先用RDKit标准化所有SMILES,再对标准化后的字符串去重。如果遇到极大数据集(上千万条),我会先用 InChI 或 InChIKey 做哈希去重——InChIKey是固定长度的哈希值,适合做索引。代码大概这样:

from rdkit import Chem
def canonical_smiles(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    return Chem.MolToSmiles(mol, canonical=True)

# 然后用 set 或者 pandas drop_duplicates
smiles_set = set()
for s in raw_smiles_list:
    can = canonical_smiles(s)
    if can:
        smiles_set.add(can)

嗯,这里要注意:InChIKey虽然快,但偶尔会有碰撞(尽管概率极低)。如果是药物分子这种小规模数据,直接用标准化SMILES去重就够用了。

数据对比:我曾处理过一个约200万条SMILES的库,直接用字符串去重后剩180万,用标准化SMILES去重后剩165万,又额外排除了15万条“假重复”。这些假重复里面就有很多是苯环的两种写法。

2.4 无效结构——能写出来不代表能存在

最后这个问题最隐蔽。有些SMILES语法上完全正确,标准化也没问题,但实际化学上根本不存在。比如五配位碳、违反八隅体规则、环张力过大(三元环连三个硼原子)等等。RDKit默认的 SanitizeMol 会检查化合价、Kekule式、芳香性等,但并不会检查所有可能的不合理结构。

我自己的经验是,先跑一遍 SanitizeMol 看看有没有报错,然后做一些基本化学规则检查。比如:

  • 原子化合价是否在合理范围内(C最多4,N最多5,O最多2等等)
  • 是否有原子数在3以下的奇怪环(三元环通常不稳定,但如果是环丙烷还算合理)
  • 分子量是否超出合理范围(比如大于1000的有机小分子可能错误)

下面这个函数是我常用的“快速无效结构过滤器”:

def is_valid_molecule(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return False
    try:
        Chem.SanitizeMol(mol)
    except:
        return False
    # 额外检查
    for atom in mol.GetAtoms():
        if atom.GetAtomicNum() == 6 and atom.GetDegree() > 4:
            return False  # 碳原子配位数超过4
    return True

当然,这个检查并不完美。我遇到过一种情况:某种含硼配合物,RDKit检查通过,但后续能量优化发现几何构型不合理。所以如果你做结构活性研究,最好再结合力场或DFT验证一下。

再次提醒:无效结构的定义取决于你的项目目标。如果只是做分子指纹或分类,稍微不合理的结构可能影响不大;但如果要生成3D构象或对接,必须严格过滤。我曾经在分子对接项目里忽略了“五配位碳”的问题,结果对接分数异常高,后来发现那个结构根本无法合成。

SMILES分子数据常见质量问题分类 分子数据质量问题 格式错误 括号、大小写、环编号 非标准化表示 芳香性、电荷写法 重复记录 显式/隐式重复 无效结构 化合价、环张力 四个问题之间的关系 • 格式错误是最基础的检查,必须优先处理 • 非标准化表示常与格式错误并存,建议标准化后统一去重 • 无效结构需要结合化学规则过滤,是清洗流程的最后防线

好了,这一章的内容差不多就这些。总结下来,清洗SMILES数据就像筛沙子——先筛掉大石块(格式错误),再筛掉小石子(非标准化),然后淘掉重复的颗粒(重复记录),最后还得用磁铁把里面夹带的铁屑吸出来(无效结构)。每多一道工序,你的数据质量就上一个台阶。后面我们会具体讲怎么用工具实现这些清洗步骤,尤其是用RDKit写清洗流水线的技巧。