从数据库拉下来的 SMILES 直接喂给模型,是新手最常见的错误。同一个化合物可能以盐、不同互变异构体、不同电荷状态出现,它们的指纹完全不同,模型会把它们当成不同分子——这既浪费数据,又会造成划分时的隐性泄漏。
完整的清洗管线
from rdkit import Chem, RDLogger
from rdkit.Chem.MolStandardize import rdMolStandardize
import pandas as pd
RDLogger.DisableLog("rdApp.*") # 关掉刷屏的警告
# 复用同一批标准化器对象,比每次新建快很多
_lfc = rdMolStandardize.LargestFragmentChooser()
_unc = rdMolStandardize.Uncharger()
_te = rdMolStandardize.TautomerEnumerator()
def clean_smiles(smi, canonical_tautomer=True):
"""返回 (标准化 SMILES, 失败原因);成功时原因为 None"""
if not isinstance(smi, str) or not smi.strip():
return None, "empty"
mol = Chem.MolFromSmiles(smi)
if mol is None:
return None, "parse_failed"
try:
mol = rdMolStandardize.Cleanup(mol) # 基础清理与价键规范
mol = _lfc.choose(mol) # 去盐:只留最大片段
mol = _unc.uncharge(mol) # 中和可中和的电荷
if canonical_tautomer:
mol = _te.Canonicalize(mol) # 统一互变异构体
except Exception as e:
return None, f"standardize_failed:{type(e).__name__}"
if mol is None or mol.GetNumAtoms() == 0:
return None, "empty_after_cleanup"
return Chem.MolToSmiles(mol, canonical=True), None
每一步在做什么
| 步骤 | 作用 | 例子 |
|---|---|---|
Cleanup |
规范价键、金属断键、规范化官能团写法 | 硝基的两种写法统一 |
LargestFragmentChooser |
去盐、去溶剂,保留主片段 | CCN.Cl → CCN |
Uncharger |
中和可中和的电荷 | CC(=O)[O-] → CC(=O)O |
TautomerEnumerator.Canonicalize |
选一个规范互变异构体 | 酮式/烯醇式统一 |
互变异构体标准化:要不要开
这一步耗时最长且有争议,需要按场景判断:
- 建议开启:做去重、算相似性、训练 2D 模型时——不统一会把同一化合物当成不同分子。
- 建议关闭或谨慎:需要保留特定互变异构态时(如已知某个互变体才是结合构型),或分子量很大、处理速度成为瓶颈时。
- 注意:RDKit 选的「规范互变异构体」不一定是生理条件下的主导形式,它只保证一致性,不保证正确性。
批量处理与统计
from concurrent.futures import ProcessPoolExecutor
from collections import Counter
df = pd.read_csv("egfr_ic50_raw.csv")
with ProcessPoolExecutor(max_workers=8) as ex:
results = list(ex.map(clean_smiles, df["smiles"].tolist(), chunksize=200))
df["clean_smiles"] = [r[0] for r in results]
df["fail_reason"] = [r[1] for r in results]
# 关键:一定要统计失败情况,而不是静默丢弃
print("失败统计:", Counter(r for r in df["fail_reason"] if r))
fail_rate = df["clean_smiles"].isna().mean()
print(f"失败率:{fail_rate:.2%}")
if fail_rate > 0.05:
print("⚠ 失败率偏高,应抽查失败样本找出原因")
print(df[df["clean_smiles"].isna()][["smiles", "fail_reason"]].head(10))
df = df.dropna(subset=["clean_smiles"])
「统计失败率」这一步经常被跳过,但它是发现数据问题最快的方式。失败率超过 5% 通常意味着数据源有系统性问题(如包含非小分子条目、格式错误)。
清洗后的去重与对照
# 按标准化 SMILES 去重
before = len(df)
agg = df.groupby("clean_smiles").agg(
pIC50=("pIC50", "median"),
n_dup=("pIC50", "size"),
spread=("pIC50", lambda x: x.max() - x.min()),
).reset_index()
print(f"去重前 {before} → 去重后 {len(agg)}(合并了 {before - len(agg)} 条)")
# 检查重复项之间是否一致
inconsistent = agg[(agg["n_dup"] > 1) & (agg["spread"] > 1.0)]
print(f"⚠ {len(inconsistent)} 个化合物的重复测量跨度 > 1 log,建议剔除")
agg = agg[~agg.index.isin(inconsistent.index)]
agg.to_csv("egfr_cleaned.csv", index=False)
去重合并的数量本身是个有用信号:如果清洗后合并掉了大量分子,说明原始数据里有很多盐型/互变异构的重复——而这些如果不合并,就会跨越训练/测试集造成泄漏。
常见坑与提示
MolFromSmiles失败返回None而不抛异常,必须显式检查并统计失败率;- 去盐、中和、互变异构统一三步都做完,才谈得上可靠去重;
- 去重后检查重复测量的一致性,跨度过大的整条剔除;
- 锁定 RDKit 版本——不同版本的标准化行为有过变化,会让历史模型的特征对不上。
延伸资源
- 上一步取数:326《从 ChEMBL 拉取靶点活性数据》;下一步:328《RDKit 标准化分子》、329《去盐、去重复、去异常结构》;
- 工具详解:171《RDKit》、218《Datamol》。