041

Bemis–Murcko Scaffold:标准骨架提取方法入门

Bemis–Murcko 是标准的骨架提取方法。这篇讲清算法规则、变体与实际使用中的边界情况。

Bemis–Murcko 骨架是化学信息学中提取分子核心结构的标准方法。它的规则简单明确——正因如此,它成为了骨架划分、多样性分析、SAR 组织的通用基础。

算法规则

# 【核心规则】:
#   保留:
#     1) 所有的环系
#     2) 连接环系的链(linker)
#   去掉:
#     3) 所有末端的取代基(不在环与环之间路径上的部分)
#
# 【具体做法】:
#   反复删除「度为 1 且不在环中」的原子,
#   直到没有可删的为止
#
# 【例子】:
#   CC(=O)Nc1ccc(OC(C)C)cc1
#   → 去掉乙酰基与异丙基
#   → 骨架:c1ccc(N)cc1 的相关形式
#
# 【注意】:
#   连接两个环的链会被保留
#   c1ccccc1CCc1ccccc1  →  骨架包含中间的 CC

from rdkit import Chem
from rdkit.Chem.Scaffolds import MurckoScaffold

examples = [
    "CC(=O)Nc1ccc(O)cc1",              # 对乙酰氨基酚
    "CCCCCCCC",                         # 无环 → 空骨架
    "c1ccccc1CCc1ccccc1",              # 两环 + 连接链
    "CC(C)Cc1ccc(cc1)C(C)C(=O)O",      # 布洛芬
]

for smi in examples:
    mol = Chem.MolFromSmiles(smi)
    scaf = MurckoScaffold.MurckoScaffoldSmiles(mol=mol)
    print(f"{smi:35s} → {scaf if scaf else '【空】'}")

三个变体

变体 保留什么 用途
标准 Murcko 环 + 连接链,保留原子类型与键级 最常用
Generic(图框架) 只保留拓扑,所有原子变碳、键变单键 更宽松的分组
Ring system 只保留独立的环系,去掉连接链 环系多样性分析
mol = Chem.MolFromSmiles("c1ccc(cc1)C(=O)Nc1ccncc1")

# 标准
scaf = MurckoScaffold.GetScaffoldForMol(mol)
print("标准:", Chem.MolToSmiles(scaf))

# Generic
generic = MurckoScaffold.MakeScaffoldGeneric(scaf)
print("Generic:", Chem.MolToSmiles(generic))
# 【苯环与吡啶环变成同一个 C1CCCCC1】

# 【选择依据】:
#   标准骨架 → 严格区分杂环类型
#     适合:SAR 分析、专利检索
#   Generic → 只看拓扑形状
#     适合:
#       - 【更严格的数据划分】(见 051)
#         因为把苯与吡啶归为一类,划分更严
#       - 拓扑多样性评估
#       - 【判断是否真的发生了骨架跃迁】(见 352)
#         只换杂原子不算真正的跃迁

边界情况

# 【情况一:无环分子】
mol = Chem.MolFromSmiles("CCCCCCCC")
print(repr(MurckoScaffold.MurckoScaffoldSmiles(mol=mol)))   # ''
#
# 【后果】:
#   所有无环分子被归为同一「骨架」(空字符串)
#   → 骨架划分时它们会全部落在同一边
#   → 【如果数据集中无环分子多,需要单独处理】
#
# 【应对】:
def safe_scaffold(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    scaf = MurckoScaffold.MurckoScaffoldSmiles(mol=mol)
    if not scaf:
        # 无环分子:用规范 SMILES 本身作为「骨架」
        return f"acyclic:{Chem.MolToSmiles(mol)}"
    return scaf

# 【情况二:骨架仍然很大】
#   多环分子的骨架可能仍然很复杂
#   → 每个分子几乎有自己的骨架
#   → 骨架划分退化成随机划分
#   【检查】:骨架数 / 分子数 的比例

# 【情况三:骨架过于笼统】
#   很多不同的分子共享「苯环」这个骨架
#   → 划分不够严格
#   【应对】:用 generic 或聚类划分(见 050)

# 【情况四:立体化学】
#   默认保留手性 → 对映体被视为不同骨架
scaf_no_chiral = MurckoScaffold.MurckoScaffoldSmiles(
    mol=mol, includeChirality=False)
# 【骨架划分时通常应该去掉手性】

# 【情况五:多组分(盐)】
#   骨架提取前应该先去盐(见 047)
from rdkit.Chem.MolStandardize import rdMolStandardize
mol = rdMolStandardize.FragmentParent(mol)

骨架的层次化组织

# 【Scaffold Tree 的思想】:
#   从一个复杂骨架出发,
#   逐步移除环,形成一棵层次树
#   → 可以在不同的抽象层次上分组分子
#
# 简化的实现思路:

from rdkit import Chem
from rdkit.Chem.Scaffolds import MurckoScaffold

def scaffold_hierarchy(smiles):
    """生成骨架的多个抽象层次"""
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return {}
    levels = {"molecule": Chem.MolToSmiles(mol)}

    scaf = MurckoScaffold.GetScaffoldForMol(mol)
    levels["murcko"] = Chem.MolToSmiles(scaf)

    try:
        levels["generic"] = Chem.MolToSmiles(
            MurckoScaffold.MakeScaffoldGeneric(scaf))
    except Exception:
        levels["generic"] = None

    # 环系数量与大小
    ri = scaf.GetRingInfo()
    levels["n_rings"] = ri.NumRings()
    levels["ring_sizes"] = sorted(len(r) for r in ri.AtomRings())
    return levels

# 【用途】:
#   在不同层次上分析多样性:
#     - 分子层面:完全不同的分子有多少?
#     - Murcko 层面:不同的骨架有多少?
#     - Generic 层面:【不同的拓扑有多少?】
#   → 【generic 层面的多样性才是真正的骨架多样性】

在数据划分中的应用

# 【骨架划分的标准实现】(见 051)

from collections import defaultdict

def scaffold_split(smiles_list, frac_train=0.8, frac_valid=0.1,
                   generic=False, balanced=True):
    """按骨架划分数据集"""
    groups = defaultdict(list)
    for i, smi in enumerate(smiles_list):
        mol = Chem.MolFromSmiles(smi)
        if mol is None:
            continue
        scaf = MurckoScaffold.GetScaffoldForMol(mol)
        if generic:
            try:
                scaf = MurckoScaffold.MakeScaffoldGeneric(scaf)
            except Exception:
                pass
        key = Chem.MolToSmiles(scaf) or f"acyclic_{i}"
        groups[key].append(i)

    sets = list(groups.values())
    if balanced:
        # 【大骨架优先进训练集】—— 让测试集包含更多罕见骨架
        sets = sorted(sets, key=len, reverse=True)

    n = len(smiles_list)
    n_train, n_valid = frac_train * n, frac_valid * n
    train, valid, test = [], [], []
    for s in sets:
        if len(train) + len(s) <= n_train:
            train += s
        elif len(valid) + len(s) <= n_valid:
            valid += s
        else:
            test += s
    return train, valid, test

# 【验证划分是否有效】:
#   计算测试集与训练集之间的最大 Tanimoto 相似度分布
#   → 如果很多测试分子的最大相似度 > 0.7,
#     说明骨架划分【不够严格】(见 167)
#   → 考虑用 generic 骨架或聚类划分

与其它分组方式的比较

方式 严格程度 特点
随机划分 最松 不对应任何真实场景
Murcko 骨架划分 标准做法
Generic 骨架划分 较严 把杂环变体归为一类
指纹聚类划分 整体相似的分子分在同一边(见 050《分子相似性聚类》
时间划分 最接近真实 需要时间戳(见 051《Scaffold Split》

关键要点

  • 算法是反复删除「度为 1 且不在环中」的原子,保留环系与连接链;
  • 无环分子的骨架是空字符串,需要单独处理否则会全部落在划分的同一边;
  • Generic 骨架把苯环与吡啶归为一类,用于更严格的划分与判断真正的骨架跃迁;
  • 骨架划分后应检查测试集与训练集的最大相似度分布,确认是否足够严格。

延伸资源