4. 分子指纹:ECFP、MACCS、PubChem 实战归纳与选择
说到分子指纹,其实每个做虚拟筛选的工程师心里都有一杆秤。我个人最早入行的时候,天真的以为有个软件能自动搞定一切。结果第一次跑完筛选,看着一长串假阳性结果,我愣住了。后来才明白,你选哪把钥匙,决定了你开哪扇门。
今天咱们就把三种最核心的指纹——ECFP、MACCS 和 PubChem——摊开来细聊。我会把它们的底层逻辑、适用场景,以及我踩过的坑都讲清楚。
4.1 核心三件套对比总览
先看个总表。这三者代表了不同的设计哲学:ECFP 是“局部结构完整主义”,MACCS 是“经典片段决定论”,PubChem 则是“大而全的普查式”。
| 特征 | ECFP(扩展连通性指纹) | MACCS(分子访问系统) | PubChem 指纹 |
|---|---|---|---|
| 编码原理 | 圆形子结构哈希,迭代邻居扩展 | 166 个预定义 SMART 模式匹配 | 881 位结构键,包含原子、环、电荷等 |
| 位向量长度 | 1024 / 2048 / 4096(可配置) | 固定 166 位 | 固定 881 位 |
| 设计哲学 | 局部结构不变性,关注拓扑邻域 | 是否存在特定药效团/子结构 | 综合描述子,囊括元素和性质 |
| 典型应用 | 相似性搜索、聚类 | 子结构筛选、虚拟筛选 | 候选化合物过滤、多任务学习 |
| 我的偏好 | 相似性召回 > 精确匹配 | 经典线索,稳定可靠 | 批量过滤,防漏检 |
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 实战中的选择策略:三件套怎么搭
很多朋友会在中问我:“罗工,我该选哪个?”我的答案是——别只用一个。你应该根据任务把三者组合起来,形成管线。
我个人习惯的做法分三步:
- 粗筛(快速过滤):用 PubChem 指纹 + XGBoost。这一层主要干掉明显不合理的分子,比如分子量过大、环系过于密集等。由于 PubChem 指纹的 881 位涵盖大量统计性质,模型泛化能力很强。
- 中筛(相似性聚类):用 ECFP。这一层探索分子的局部相似性,比如构效关系。你可以用 ECFP 做 Tanimoto 相似度计算,把结构相似的候选分子聚在一起,避免重复筛选。
- 精筛(子结构确认):用 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。
4.7 一张图理清选择逻辑
下面这张图是我自己整理的三指纹选择框架,你看一眼基本就能明白什么时候用什么。
4.8 小结:你的工具箱里始终要有这三样
总结一句话:ECFP 管局部结构细节,MACCS 管经典子结构的存在性确认,PubChem 管全局性质普查。这三者在管线里各司其职,彼此配合才能打出漂亮的筛选仗。
我的工具箱里一直放着这三样,根据任务随意组合。下次当你面对一堆分子时,不妨也试试这个思路:先用 PubChem 键做一次快速过滤,再用 ECFP 做相似性排序,最后用 MACCS 确认关键药效团。你可能会发现,筛选出的候选分子命中率真的能高出不少。