043

MMPA 配对分子分析:如何发现结构变化与活性变化的关系

MMPA 自动发现「结构变化—活性变化」的规律。这篇讲清它的原理、实现与相对 R 基团分解的优势。

匹配分子对分析(MMPA)自动找出只差一个片段的分子对,并统计这个「变换」对性质的影响。它不需要预先指定骨架,能从大规模数据中自动挖掘 SAR 规律——这是它相对 R 基团分解的核心优势。

基本概念

# 【匹配分子对(MMP)】:
#   两个分子只在一个位置不同
#
#   例:
#     A: CC(=O)Nc1ccc(F)cc1     pIC50 = 6.2
#     B: CC(=O)Nc1ccc(Cl)cc1    pIC50 = 6.8
#   → 【变换】:F → Cl
#   → 【效应】:ΔpIC50 = +0.6
#
# 【MMPA】:
#   在整个数据集中找出所有这样的对,
#   按「变换」分组统计
#
#   变换          n     中位 ΔpIC50   分布
#   F → Cl        23    +0.5          ...
#   Me → CF3      18    +0.8          ...
#   OMe → OH      15    -0.3          ...
#
# 【价值】:
#   1) 【基于真实实验数据】——不是模型的猜测
#   2) 自动发现,不需要预先假设
#   3) 【结论直接可行动】:
#      「把甲基换成三氟甲基,中位提升 0.8」
#      → 化学家可以直接据此设计下一批分子
#   4) 【可以跨项目积累】:
#      在历史数据上做 MMPA,得到公司内部的
#      「变换效应知识库」

片段化:MMP 的技术基础

# 【核心算法】:
#   把每个分子在可断裂的键上切开,
#   得到 (核心, 片段) 对
#   → 核心相同的两个分子,就是一个 MMP
#
# 【切哪些键】:
#   通常切「单个非环单键」(single cut)
#   也可以切两个或三个键(double/triple cut)
#   → 切得越多,能找到的对越多,但计算量爆炸

from rdkit import Chem
from rdkit.Chem import rdMMPA
from collections import defaultdict

def fragment_molecules(smiles_list, max_cuts=1):
    """片段化并按核心索引"""
    cores = defaultdict(list)
    for idx, smi in enumerate(smiles_list):
        mol = Chem.MolFromSmiles(smi)
        if mol is None:
            continue
        try:
            frags = rdMMPA.FragmentMol(mol, maxCuts=max_cuts,
                                       resultsAsMols=False)
        except Exception:
            continue
        for core, chains in frags:
            if core:      # 单切时 core 可能为空
                cores[core].append((idx, chains))
    return cores

def find_mmps(smiles_list, values, min_pairs=3):
    """找出匹配分子对并统计变换效应"""
    import numpy as np
    cores = fragment_molecules(smiles_list)
    transforms = defaultdict(list)

    for core, members in cores.items():
        for i in range(len(members)):
            for j in range(i + 1, len(members)):
                idx_a, frag_a = members[i]
                idx_b, frag_b = members[j]
                if frag_a == frag_b:
                    continue
                delta = values[idx_b] - values[idx_a]
                transforms[(frag_a, frag_b)].append(delta)
                transforms[(frag_b, frag_a)].append(-delta)

    results = []
    for (a, b), deltas in transforms.items():
        if len(deltas) >= min_pairs:
            deltas = np.array(deltas)
            results.append({
                "from": a, "to": b, "n": len(deltas),
                "median": float(np.median(deltas)),
                "mean": float(deltas.mean()),
                "std": float(deltas.std()),
                # 【一致性:多少比例是同向的】
                "consistency": float((deltas > 0).mean()),
            })
    return sorted(results, key=lambda r: -abs(r["median"]))

# 【专用工具】:mmpdb 是更成熟的实现
#   pip install mmpdb
#   mmpdb fragment data.smi -o data.fragments
#   mmpdb index data.fragments -o data.mmpdb
#   mmpdb loadprops -p data.props data.mmpdb
#   mmpdb transform --smiles "CC(=O)Nc1ccccc1" data.mmpdb

结果的正确解读

# 【三个必看的统计量】:
#
# 1) 【n(对数)】
#    n < 5   → 统计意义弱,只能作为提示
#    n > 20  → 较可靠
#
# 2) 【中位数 vs 均值】
#    用【中位数】更稳健(对异常值不敏感)
#
# 3) 【一致性(consistency)】
#    有多少比例的对是同向变化的?
#
#    consistency = 0.9, median = +0.8
#      → 【这个变换几乎总是提升活性】
#      → 可靠的规律
#
#    consistency = 0.5, median = +0.1
#      → 【效应不一致,取决于上下文】
#      → 【这才是常态】
#
# 【最重要的认识】:
#   多数变换的效应是【上下文依赖的】
#   同一个变换在不同骨架、不同位置上
#   可能有相反的效果
#
#   → 【这正是「加性 SAR」假设失效的原因】
#   → 不要盲目套用其它项目的 MMPA 结论
#
# 【应对:分层分析】
#   按骨架、按靶点、按环境分组做 MMPA
#   → 得到更有针对性的规律

def contextual_mmpa(df, transform_from, transform_to, group_col="scaffold"):
    """分组看同一个变换的效应"""
    for group, sub in df.groupby(group_col):
        results = find_mmps(sub["smiles"].tolist(),
                            sub["pIC50"].tolist(), min_pairs=2)
        for r in results:
            if r["from"] == transform_from and r["to"] == transform_to:
                print(f"{group}: median {r['median']:+.2f} (n={r['n']})")
# 【常常发现同一变换在不同骨架上效应不同,甚至相反】

MMPA 的多种用途

用途 说明
SAR 挖掘 自动发现「什么变换有效」
生成设计建议 「这个分子可以试试把 X 换成 Y」
解释模型 比注意力可靠得多(见 168《可解释性 AI》
多参数优化 同时看活性、logP、溶解度的变化
活性悬崖发现 ΔpIC50 很大的对
数据质量检查 异常大的 Δ 可能是数据错误
生物电子等排体验证 验证经典的替换规则在自家数据上是否成立

多参数的 MMPA

# 【实际项目从来不是单目标】
#   一个变换可能提升活性但损害溶解度
#
# 【多参数 MMPA】:
#   同一个变换,同时统计对多个性质的影响

def multiparameter_mmpa(df, property_cols, min_pairs=3):
    import numpy as np
    cores = fragment_molecules(df["smiles"].tolist())
    transforms = defaultdict(lambda: defaultdict(list))

    for core, members in cores.items():
        for i in range(len(members)):
            for j in range(i + 1, len(members)):
                idx_a, frag_a = members[i]
                idx_b, frag_b = members[j]
                if frag_a == frag_b:
                    continue
                for prop in property_cols:
                    va, vb = df[prop].iloc[idx_a], df[prop].iloc[idx_b]
                    if np.isnan(va) or np.isnan(vb):
                        continue
                    transforms[(frag_a, frag_b)][prop].append(vb - va)

    rows = []
    for (a, b), props in transforms.items():
        if len(props.get(property_cols[0], [])) < min_pairs:
            continue
        row = {"from": a, "to": b}
        for prop, deltas in props.items():
            row[f"d_{prop}"] = float(np.median(deltas))
            row[f"n_{prop}"] = len(deltas)
        rows.append(row)
    return pd.DataFrame(rows)

# 【理想的变换】:
#   活性提升 + logP 下降 + 溶解度提升
#   → 【这类变换是先导优化的金矿】
#   → 用这个分析能系统地找出来

MMPA 与 R 基团分解的对比

R 基团分解(见 042《R-group Decomposition》 MMPA
需要指定核心 否——自动发现
适用范围 同骨架的一个系列 整个数据集,跨系列
输出 R 基团表 变换效应统计
可视化 直观(表格 + 分子图) 需要额外处理
规模 几十到几百个分子 可处理数十万
适合 深入分析一个系列 大规模规律挖掘

两者互补:用 MMPA 在全库上找出有希望的变换,用 R 基团分解深入分析具体系列。

关键要点

  • 基于真实实验数据,结论直接可行动——这是它比模型解释可靠的原因;
  • 必看三个统计量:对数 n、中位数、一致性
  • 多数变换的效应是上下文依赖的——不要盲目套用其它项目的结论;
  • 多参数 MMPA 能系统找出「活性升 + logP 降」这类先导优化的金矿。

延伸资源