4. 分子指纹:ECFP、MACCS、PubChem 实战归纳与选择

说到分子指纹,其实每个做虚拟筛选的工程师心里都有一杆秤。我个人最早入行的时候,天真的以为有个软件能自动搞定一切。结果第一次跑完筛选,看着一长串假阳性结果,我愣住了。后来才明白,你选哪把钥匙,决定了你开哪扇门。

今天咱们就把三种最核心的指纹——ECFP、MACCS 和 PubChem——摊开来细聊。我会把它们的底层逻辑、适用场景,以及我踩过的坑都讲清楚。

4.1 核心三件套对比总览

先看个总表。这三者代表了不同的设计哲学:ECFP 是“局部结构完整主义”,MACCS 是“经典片段决定论”,PubChem 则是“大而全的普查式”。

特征 ECFP(扩展连通性指纹) MACCS(分子访问系统) PubChem 指纹
编码原理 圆形子结构哈希,迭代邻居扩展 166 个预定义 SMART 模式匹配 881 位结构键,包含原子、环、电荷等
位向量长度 1024 / 2048 / 4096(可配置) 固定 166 位 固定 881 位
设计哲学 局部结构不变性,关注拓扑邻域 是否存在特定药效团/子结构 综合描述子,囊括元素和性质
典型应用 相似性搜索、聚类 子结构筛选、虚拟筛选 候选化合物过滤、多任务学习
我的偏好 相似性召回 > 精确匹配 经典线索,稳定可靠 批量过滤,防漏检
一点小建议:刚入门时,我总想着用更长的指纹(比如 ECFP 4096),觉得信息多就是好。其实很多时候 1024 位已经够用,更长的指纹反而可能引入噪声,让模型过拟合。建议你先从 1024 位开始试。

4.2 ECFP:局部结构的不变密码

ECFP 是我最常用的指纹之一。它的底层逻辑很简单:从每个重原子出发,沿着邻域扩展一个圆形子结构(radius=2 是最常见的配置),然后将这些子结构哈希成固定长度的位向量。

关键设计点在于:

  • 不变性: 同一个子结构,无论其在分子中出现在哪里,都会映射到相同的位置。
  • 稀疏性: 大部分位为 0,只代表分子真正具有的特征。我在处理百万级化合物库时,ECFP 的稀疏性对内存非常友好。
  • 哈希冲突: 不同的子结构可能映射到同一位。虽然概率低,但确实存在。

用 Python 实现起来非常直观:

from rdkit import Chem
from rdkit.Chem import rdMolDescriptors

mol = Chem.MolFromSmiles('CCO')  # 乙醇
ecfp = rdMolDescriptors.GetMorganFingerprintAsBitVect(
    mol, 
    radius=2,      # 扩展半径
    nBits=1024     # 指纹长度
)

print(ecfp.ToBitString()[:64])  # 查看前 64 位

4.3 MACCS:经典且保守的子结构图鉴

MACCS 指纹,我觉得它是一个老派但值得信赖的家伙。它只有 166 个键,每个键对应一个特定的 SMART 模式或者子结构特征。比如,第 1 位通常表示是否存在芳香环,第 100 多位表示是否存在特定官能团。

我为什么喜欢它?因为它稳定。我在处理历史数据时发现,不同版本的 RDKit 对 MACCS 的生成结果几乎一致,不会像 ECFP 那样因为哈希种子不同产生差异。所以当项目需要严格复现的时候,我基本不会犹豫,直接选 MACCS。

为什么说它保守?因为它只告诉你“有没有”,不告诉你“在哪里”或“有多大”。比如,存在一个含氮杂环还是两个含氮杂环,对于 MACCS 来说,结果都是 1。这就导致它对分子拓扑细节的区分能力有限。

生成 MACCS 代码:

from rdkit import Chem
from rdkit.Chem import MACCSkeys

mol = Chem.MolFromSmiles('CCO')
maccs = MACCSkeys.GenMACCSKeys(mol)

print(f"MACCS 位向量长度: {len(maccs)}")   # 输出 166
print(maccs.ToBitString()[:166])

4.4 PubChem 指纹:大而全的普查式筛选器

PubChem 指纹有 881 位,听起来很吓人,但它是我在多重过滤管线中的秘密武器。它的设计初衷就是尽可能全面地描述一个分子,从原子数量(第一位)到特殊原子类型,再到环的大小和个数,无一不包。

比如,第 2 位表示分子中碳原子数是否 ≥ 8,第 3 位表示分子中有机元素种类是否 ≥ 3。这种“普查式”的设计,让它非常适合做快速粗筛:如果你想要找分子量适中、环数不太多的候选分子,PubChem 指纹的前面几十位就能帮你在几秒内过滤掉一大批。

我曾经参与过一个从 PubChem 库中筛选特定 G 蛋白偶联受体配体的项目。用 PubChem 指纹 + 随机森林,在不到 15 分钟里就从 2000 万个分子里筛到了约 5 万个候选。嗯,后来实验验证,真的有 3 个命中。

生成 PubChem 指纹:

from rdkit import Chem
from rdkit.Chem import rdMolDescriptors

mol = Chem.MolFromSmiles('CCO')
pubchem = rdMolDescriptors.GetPubChemFingerprint(mol)

print(f"PubChem 指纹长度: {len(pubchem)}")   # 输出 881
print(pubchem.ToBitString()[:128])

4.5 实战中的选择策略:三件套怎么搭

很多朋友会在中问我:“罗工,我该选哪个?”我的答案是——别只用一个。你应该根据任务把三者组合起来,形成管线。

我个人习惯的做法分三步:

  1. 粗筛(快速过滤):用 PubChem 指纹 + XGBoost。这一层主要干掉明显不合理的分子,比如分子量过大、环系过于密集等。由于 PubChem 指纹的 881 位涵盖大量统计性质,模型泛化能力很强。
  2. 中筛(相似性聚类):用 ECFP。这一层探索分子的局部相似性,比如构效关系。你可以用 ECFP 做 Tanimoto 相似度计算,把结构相似的候选分子聚在一起,避免重复筛选。
  3. 精筛(子结构确认):用 MACCS。这一层严格确认候选分子是否包含目标药效团。MACCS 的 166 位虽然短,但非常稳定且可解释。

这一步一步下来,筛选的精度和效率都能兼顾。我曾经负责过一个从百万分子库中筛选 Top 50 候选分子的项目,就用这套策略,最后只有 2 个分子在实验阶段因为代谢问题被淘汰,其余都表现不错。

4.6 避坑指南:我曾经犯过的错

嗯,这里我要认真说几句。这些都是我真正踩过的坑:

  • 滥用 ECFP 的半径参数: 我曾经为了捕捉更大的结构环境,把 radius 设成 4,结果模型学到了一大堆无关的远程耦合噪声,最终筛选结果一塌糊涂。对于小分子(MW < 500),radius=2 基本够用。
  • 忽略 MACCS 的位缺失: MACCS 只代表“有”,不代表“精”。比如,一个分子含有羧基,MACCS 第 101 位(假设)会置 1,但分子中可以有 1 个或 3 个羧基。所以,如果你用 MACCS + Lasso 回归预测溶解度,很容易低估多官能团分子的负贡献。
  • 混淆 ECFP 与 FCFP 的特征类型: ECFP 使用原子类型(如碳、氧、氮等拓扑特征),FCFP 使用功能类别(如氢键供体、受体、芳香性等)。如果你需要做性质预测(如 log P、水溶性),建议用 FCFP;做结构聚类则用 ECFP。
请注意: 另一点容易踩坑的是哈希冲突。虽然理论上每次哈希碰撞的概率不大,但当指纹长度很短(比如 512 位)且数据量很大时,冲突会显著增加。如果你在做大规模虚拟筛选,一定要用较长的指纹(比如 2048 位),并定期用相似性回溯验证哈希一致性。

4.7 一张图理清选择逻辑

下面这张图是我自己整理的三指纹选择框架,你看一眼基本就能明白什么时候用什么。

分子指纹选择决策图 分子指纹选择决策框架 输入分子结构 ECFP 核心算法 圆形子结构扩展 ➜ 哈希 ➜ 位向量 MACCS 166 键 166 位子结构模式匹配 PubChem 881 位指纹 原子/环/结构枚举普查 粗筛:PubChem 中筛:ECFP 精筛:MACCS 根据任务目标组合:粗筛 → 中筛 → 精筛

4.8 小结:你的工具箱里始终要有这三样

总结一句话:ECFP 管局部结构细节,MACCS 管经典子结构的存在性确认,PubChem 管全局性质普查。这三者在管线里各司其职,彼此配合才能打出漂亮的筛选仗。

我的工具箱里一直放着这三样,根据任务随意组合。下次当你面对一堆分子时,不妨也试试这个思路:先用 PubChem 键做一次快速过滤,再用 ECFP 做相似性排序,最后用 MACCS 确认关键药效团。你可能会发现,筛选出的候选分子命中率真的能高出不少。