329

去盐、去重复、去异常结构:数据质量决定模型上限

去盐、去重、去异常结构决定了模型的上限。这篇给出可运行的过滤代码、各项阈值的依据,以及过滤强度的权衡。

模型的上限由数据质量决定。这一步要处理三类问题:盐与混合物、重复记录、以及不适合建模的异常结构。每一类都有明确的判断标准,但过滤强度需要权衡——过松则噪声大,过严则丢失有效数据。

一、结构合理性过滤

from rdkit import Chem
from rdkit.Chem import Descriptors, rdMolDescriptors

ORGANIC = {1, 5, 6, 7, 8, 9, 14, 15, 16, 17, 34, 35, 53}   # H,B,C,N,O,F,Si,P,S,Cl,Se,Br,I

def structure_filter(smi):
    """返回 (是否保留, 原因)"""
    mol = Chem.MolFromSmiles(smi)
    if mol is None:
        return False, "unparsable"

    n_heavy = mol.GetNumHeavyAtoms()
    if n_heavy < 6:
        return False, "too_small"          # 小于 6 个重原子基本不是类药分子
    if n_heavy > 70:
        return False, "too_large"          # 超出小分子范畴

    # 元素检查:排除金属配合物等
    elems = {a.GetAtomicNum() for a in mol.GetAtoms()}
    if not elems.issubset(ORGANIC):
        return False, f"unusual_elements:{elems - ORGANIC}"

    # 混合物(清洗后仍多片段说明去盐没处理干净)
    if "." in Chem.MolToSmiles(mol):
        return False, "mixture"

    # 电荷检查
    charge = Chem.GetFormalCharge(mol)
    if abs(charge) > 2:
        return False, f"extreme_charge:{charge}"

    # 基本性质范围(宽松,只挡明显异常)
    mw = Descriptors.MolWt(mol)
    if not (100 <= mw <= 900):
        return False, f"mw_out_of_range:{mw:.0f}"

    return True, None

二、PAINS 与反应性基团过滤

from rdkit.Chem import FilterCatalog

params = FilterCatalog.FilterCatalogParams()
params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS_A)
params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS_B)
params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS_C)
catalog_pains = FilterCatalog.FilterCatalog(params)

params_b = FilterCatalog.FilterCatalogParams()
params_b.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.BRENK)
catalog_brenk = FilterCatalog.FilterCatalog(params_b)

def flag_alerts(smi):
    mol = Chem.MolFromSmiles(smi)
    if mol is None:
        return {"pains": None, "brenk": None}
    p = catalog_pains.GetFirstMatch(mol)
    b = catalog_brenk.GetFirstMatch(mol)
    return {
        "pains": p.GetDescription() if p else None,
        "brenk": b.GetDescription() if b else None,
    }
过滤器 目标 建模时的处理
PAINS 频繁假阳性结构(如儿茶酚、醌类) 标记但未必删除,见下文
Brenk 不适合先导化合物的基团(如迈克尔受体、烷化剂) 虚拟筛选时删,建模时标记
NIH / ZINC 其他经验规则集 按需

PAINS 该不该删,取决于用途

  • 虚拟筛选的输出:应该删——避免买到假阳性化合物;
  • 训练活性预测模型建议标记而非删除。PAINS 只是「容易造成干扰」,不代表没有真实活性;删掉可能损失有效数据。更好的做法是加一列标记,建模时作为特征或用于结果解读。
  • 训练 HTS 数据模型:应该删——这类数据的假阳性问题最严重(见 245《TDC HTS Group》)。

三、去重(含隐性重复)

from rdkit.Chem import rdMolDescriptors
import pandas as pd

def get_inchikey(smi):
    mol = Chem.MolFromSmiles(smi)
    return Chem.MolToInchiKey(mol) if mol else None

df["inchikey"] = df["clean_smiles"].map(get_inchikey)

# 完整 InChIKey 去重(区分立体化学)
exact_dups = df["inchikey"].duplicated().sum()

# 骨架层(InChIKey 前 14 位,忽略立体化学与质子化)
df["skeleton"] = df["inchikey"].str[:14]
stereo_dups = df.groupby("skeleton").size()
multi = stereo_dups[stereo_dups > 1]
print(f"完全重复 {exact_dups} 条;"
      f"{len(multi)} 个骨架有多个立体异构体")

# 立体异构体是否该合并?取决于任务:
#   活性可能因立体化学差数十倍 → 不应合并
#   但要确认它们不会跨越训练/测试划分(隐性泄漏)

立体异构体跨划分是很隐蔽的泄漏形式:R 构型在训练集、S 构型在测试集,2D 指纹模型看它们几乎一样,测试指标就虚高了。划分时应按 InChIKey 前 14 位(骨架层)分组

四、异常活性值检测

import numpy as np

# 1) 分布检查
q1, q3 = df["pIC50"].quantile([0.25, 0.75])
iqr = q3 - q1
lo, hi = q1 - 3 * iqr, q3 + 3 * iqr
outliers = df[(df["pIC50"] < lo) | (df["pIC50"] > hi)]
print(f"分布离群点:{len(outliers)} 条(不一定错,需人工看)")

# 2) 活性悬崖检测:结构极相似但活性差很多
#    这些可能是真实的 activity cliff(有价值),
#    也可能是数据错误(需核实)
from rdkit.Chem import rdFingerprintGenerator, DataStructs
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
fps = [gen.GetFingerprint(Chem.MolFromSmiles(s)) for s in df["clean_smiles"]]

cliffs = []
for i in range(len(fps)):
    sims = DataStructs.BulkTanimotoSimilarity(fps[i], fps[i+1:])
    for j, s in enumerate(sims, start=i+1):
        if s > 0.9 and abs(df["pIC50"].iloc[i] - df["pIC50"].iloc[j]) > 2:
            cliffs.append((i, j, s))
print(f"疑似活性悬崖/数据错误:{len(cliffs)} 对,建议抽查")

过滤强度的权衡

过滤强度 适用 风险
宽松(只删明显异常) 数据量少(< 500) 噪声大
中等(+ 元素、性质范围) 多数情况 平衡
严格(+ PAINS/Brenk 全删) 数据充足、要求高纯度 可能丢失有效数据、缩小适用域

关键原则:记录每一步删掉了多少、为什么删。过滤流程必须可审计——半年后要能回答「为什么这个化合物不在数据集里」。

常见坑与提示

  • PAINS 在建模时建议标记而非删除,在虚拟筛选输出时才该删;
  • 立体异构体跨划分是隐蔽泄漏,划分应按 InChIKey 前 14 位分组;
  • 活性悬崖可能是真实 SAR 也可能是数据错误,必须抽查核实;
  • 记录每步的删除量与原因,过滤流程必须可审计。

延伸资源