匹配分子对分析(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 降」这类先导优化的金矿。
延伸资源
- R 基团分解:042《R-group Decomposition》;SAR:073《SAR 构效关系》;可解释性:168《可解释性 AI》;
- 骨架:040《Scaffold 骨架》;先导优化:405《Lead Optimization》。