真实的化学数据中总有一些不该进入训练集的结构:解析错误的、化学上不合理的、超出建模范围的。识别并处理它们,是数据清洗中判断成分最多的一步——因为「异常」的定义取决于你的建模目标。
异常结构的类型
| 类型 | 例子 | 处理 |
|---|---|---|
| 解析失败 | SMILES 语法错误 | 删除并记录 |
| 化学不合理 | 五价碳、异常价态 | 删除 |
| 超出范围 | 分子量 > 2000、单原子 | 视任务决定 |
| 含罕见元素 | 放射性元素、稀有金属 | 通常删除 |
| 混合物 | 去盐后仍有多个片段 | 检查后决定 |
| 聚合物/重复单元 | 含 * 的结构 |
删除 |
| 活性数据异常 | pIC50 = 15(不可能的活性) | 检查后删除 |
| 结构-活性矛盾 | 同结构不同活性 | 见 047《去盐与去重复》 |
系统的检查流程
from rdkit import Chem
from rdkit.Chem import Descriptors, rdMolDescriptors
import numpy as np
ALLOWED_ELEMENTS = {"H", "C", "N", "O", "S", "P",
"F", "Cl", "Br", "I", "B", "Si"}
def check_structure(smiles, mw_range=(50, 1000),
max_fragments=1, allowed=None):
"""返回该结构的所有问题"""
allowed = allowed or ALLOWED_ELEMENTS
issues = []
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return ["解析失败"]
# 1) 片段数
frags = Chem.GetMolFrags(mol)
if len(frags) > max_fragments:
issues.append(f"多组分({len(frags)} 个片段)")
# 2) 元素检查
elements = {a.GetSymbol() for a in mol.GetAtoms()}
unusual = elements - allowed
if unusual:
issues.append(f"罕见元素: {unusual}")
# 3) 分子量范围
mw = Descriptors.MolWt(mol)
if not (mw_range[0] <= mw <= mw_range[1]):
issues.append(f"分子量超范围: {mw:.1f}")
# 4) 原子数
n_heavy = mol.GetNumHeavyAtoms()
if n_heavy < 3:
issues.append(f"原子太少: {n_heavy}")
# 5) 【价态检查】
try:
Chem.SanitizeMol(Chem.Mol(mol))
except Exception as e:
issues.append(f"价态问题: {e}")
# 6) 【通配符/聚合物标记】
if "*" in smiles or any(a.GetAtomicNum() == 0 for a in mol.GetAtoms()):
issues.append("含通配符(可能是聚合物或 Markush)")
# 7) 【电荷检查】
charge = Chem.GetFormalCharge(mol)
if abs(charge) > 2:
issues.append(f"净电荷异常: {charge}")
# 8) 【环的大小】
ri = mol.GetRingInfo()
for ring in ri.AtomRings():
if len(ring) > 12:
issues.append(f"超大环: {len(ring)} 元")
break
# 9) 【同位素】
if any(a.GetIsotope() > 0 for a in mol.GetAtoms()):
issues.append("含同位素标记")
return issues
# 批量检查
def audit_dataset(smiles_list, **kwargs):
from collections import Counter
all_issues = Counter()
flagged = []
for i, smi in enumerate(smiles_list):
issues = check_structure(smi, **kwargs)
if issues:
flagged.append((i, smi, issues))
for issue in issues:
all_issues[issue.split(":")[0]] += 1
print(f"【{len(flagged)}/{len(smiles_list)} 个结构有问题】")
for issue, count in all_issues.most_common():
print(f" {issue}: {count}")
return flagged
活性数据的异常检查
import numpy as np
def check_activity_values(df, value_col="pActivity",
plausible_range=(2, 12)):
"""检查活性值是否合理"""
v = df[value_col]
# 1) 【物理上不可能的值】
# pIC50 > 12 意味着 IC50 < 1 pM
# → 极少数超强结合物才可能
# pIC50 < 2 意味着 IC50 > 10 mM
# → 通常这类数据没有意义
out_of_range = df[(v < plausible_range[0]) | (v > plausible_range[1])]
print(f"【超出合理范围的: {len(out_of_range)}】")
# 2) 【分布检查】
print(v.describe())
# 分布过窄 → 建模没有意义
# 多峰 → 可能混合了不同的测定体系
# 3) 【堆积在某个值上】
# 大量数据恰好等于某个值(如 5.0)
# → 【可能是删失数据被错误处理】
counts = v.round(2).value_counts()
if counts.iloc[0] > 0.05 * len(v):
print(f"【警告:{counts.index[0]} 出现 {counts.iloc[0]} 次"
f"({counts.iloc[0]/len(v):.1%})】")
print(" → 检查是否为删失数据的占位值")
# 4) 【与分子大小的关系】
# 如果活性与分子量高度相关,
# 可能是「大分子在测定中假阳性」
from rdkit import Chem
from rdkit.Chem import Descriptors
mw = df["smiles"].apply(
lambda s: Descriptors.MolWt(Chem.MolFromSmiles(s))
if Chem.MolFromSmiles(s) else np.nan)
corr = np.corrcoef(mw.dropna(), v[mw.notna()])[0, 1]
print(f"活性与分子量的相关性: {corr:.3f}")
if abs(corr) > 0.5:
print(" 【警告:相关性过高,可能有系统性偏倚】")
return out_of_range
「该删该留」的判断
# 【明确该删的】:
# ✗ 解析失败的
# ✗ 价态错误的
# ✗ 含通配符/Markush 结构的
# ✗ 活性值物理上不可能的
# ✗ 【重复但活性严重矛盾的】(见 047)
#
# 【需要判断的】:
#
# ? 【分子量超范围】
# 取决于建模目标:
# 做小分子药物 → MW > 1000 通常删
# 做大环、多肽 → 【范围要放宽】(见 373)
# → 【不要盲目套用 Lipinski 范围】
#
# ? 【含金属的化合物】
# 铂类药物、含硼药物是真实存在的
# → 如果建模范围包含它们,应该保留
# → 但指纹与描述符对它们的表达可能不足
#
# ? 【多组分】
# 共晶、共药可能是有意义的
# → 检查后决定
#
# ? 【极端的活性值】
# 可能是真实的超强结合物
# → 【不要仅因为「离群」就删除】
# → 应该回查原始文献
#
# 【最重要的原则】:
# 1) 【删除前记录】:删了什么、为什么
# 2) 【不要因为「模型预测不准」就删数据】
# → 【这是在拟合噪声,会让评测虚高】
# 3) 【删除标准应该在看到模型结果之前确定】
# → 否则就是数据窥探
#
# 【一个反面例子】:
# 「删掉模型预测误差最大的 5% 数据,
# R² 从 0.6 提升到 0.75」
# → 【这个提升是虚假的】
# → 实际部署时同样的分子仍然会来
用 ChEMBL Structure Pipeline 的检查器
# pip install chembl_structure_pipeline
from chembl_structure_pipeline import checker
# checker 返回 (严重程度, 问题描述) 的列表
# 严重程度:
# 2 轻微(如立体化学未定义)
# 5 中等
# 6 较严重
# 7 严重(如价态错误)
# 9 极严重
def chembl_check(molblock):
issues = checker.check_molblock(molblock)
return issues
# 【典型的问题类型】:
# - InChI 生成失败
# - 原子价态错误
# - 立体化学定义有冲突
# - 多个片段
# - 含 R 基团/通配符
# - 分子太大
#
# 【实用做法】:
# 严重程度 >= 6 的自动剔除
# 2~5 的标记出来供检查
# → 【这个分级比自己写规则更全面】
关键要点
- 「异常」的定义取决于建模目标——做大环时不能套用小分子的分子量范围;
- 大量数据堆积在某个值上常常是删失数据被错误处理的信号;
- 删除标准必须在看到模型结果之前确定——事后删除误差大的数据是数据窥探;
- 删除前记录删了什么与为什么;ChEMBL Structure Pipeline 的分级检查比自写规则全面。
延伸资源
- 分子标准化:046《分子标准化》;去盐去重:047《去盐与去重复》;
- QSAR:044《QSAR 入门》;结构警示:069《Brenk Alert》;ADMET 数据:156《AI ADMET 预测模型》。