一个一万个分子的库,如果全是同一系列的类似物,它提供的信息量远小于一千个多样化的分子。多样性评估的目的,是回答「这批分子值不值得全部测试」这个实际问题。
多样性的多个维度
| 维度 | 衡量方式 | 关注 |
|---|---|---|
| 结构多样性 | 平均两两 Tanimoto 距离 | 整体差异程度 |
| 骨架多样性 | 不同骨架的数量 | 化学系列的数量(见 041《Bemis–Murcko Scaffold》) |
| 簇多样性 | 聚类后的簇数 | 见 050《分子相似性聚类》 |
| 性质空间覆盖 | MW、logP 等的分布范围 | 是否覆盖目标范围 |
| 官能团多样性 | 各类官能团的出现频率 | 化学反应性的多样性 |
| 三维形状多样性 | 形状描述符的分布 | 见 093《形状互补 Shape Complementarity》 |
不同维度的多样性可能不一致:一批分子可能骨架很多样,但性质高度集中(都是高 logP);或者性质分布很宽,但只有三个骨架。应该分维度评估。
量化指标
import numpy as np
from rdkit import Chem, DataStructs
from rdkit.Chem import rdFingerprintGenerator, Descriptors
from rdkit.Chem.Scaffolds import MurckoScaffold
from collections import Counter
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
def diversity_metrics(smiles_list, n_sample=2000, seed=0):
"""计算多个维度的多样性指标"""
mols, fps = [], []
for s in smiles_list:
m = Chem.MolFromSmiles(s)
if m is not None:
mols.append(m)
fps.append(gen.GetFingerprint(m))
n = len(mols)
# ---- 1) 内部多样性(平均两两距离)----
rng = np.random.default_rng(seed)
if n > n_sample:
idx = rng.choice(n, n_sample, replace=False)
sub_fps = [fps[i] for i in idx]
else:
sub_fps = fps
dists = []
for i in range(1, len(sub_fps)):
sims = DataStructs.BulkTanimotoSimilarity(sub_fps[i], sub_fps[:i])
dists.extend([1 - s for s in sims])
internal_diversity = float(np.mean(dists))
# ---- 2) 骨架多样性 ----
scaffolds = Counter()
generics = Counter()
for m in mols:
scaf = MurckoScaffold.GetScaffoldForMol(m)
scaffolds[Chem.MolToSmiles(scaf)] += 1
try:
generics[Chem.MolToSmiles(
MurckoScaffold.MakeScaffoldGeneric(scaf))] += 1
except Exception:
pass
# ---- 3) 性质空间 ----
props = {"MW": [], "logP": [], "TPSA": [], "RotB": []}
for m in mols:
props["MW"].append(Descriptors.MolWt(m))
props["logP"].append(Descriptors.MolLogP(m))
props["TPSA"].append(Descriptors.TPSA(m))
props["RotB"].append(Descriptors.NumRotatableBonds(m))
return {
"n_molecules": n,
"internal_diversity": round(internal_diversity, 3),
"n_scaffolds": len(scaffolds),
"scaffold_ratio": round(len(scaffolds) / n, 3),
"n_generic_scaffolds": len(generics),
"largest_scaffold_frac": round(
scaffolds.most_common(1)[0][1] / n, 3),
"singleton_scaffolds": sum(1 for c in scaffolds.values() if c == 1),
"property_ranges": {
k: (round(np.percentile(v, 5), 1),
round(np.percentile(v, 95), 1))
for k, v in props.items()
},
}
# 【判读】:
# internal_diversity:
# > 0.85 高度多样(如随机的商业库)
# 0.7~0.85 中等
# < 0.6 【高度相似,可能是同一系列】
#
# scaffold_ratio(骨架数/分子数):
# 接近 1 每个分子一个骨架 → 极度多样
# < 0.1 【平均每 10 个分子共享一个骨架
# → 典型的项目化合物库】
#
# largest_scaffold_frac:
# > 0.3 【一个骨架占了 30% 以上
# → 库高度偏向某个系列】
多样性 vs 聚焦:不是越多样越好
# 【常见的误解】:多样性越高越好
#
# 【实际情况取决于阶段】:
#
# 【苗头发现阶段】:
# 目标:广撒网,找到任何有活性的化学类型
# → 【多样性优先】
# → 用 MaxMin 或聚类采样(见 050)
#
# 【苗头到先导阶段】:
# 目标:围绕已有苗头探索
# → 【聚焦优先】:相似性搜索、类似物
# → 多样性反而会浪费资源
#
# 【先导优化阶段】:
# 目标:在一个系列内系统改造
# → 【高度聚焦】
# → 「多样性」变成「取代基的多样性」
#
# 【片段筛选】(见 054):
# → 【极高的多样性】
# 因为片段小,需要覆盖尽可能多的化学类型
#
# 【结论】:
# 问「这个库是为什么阶段准备的」,
# 再判断多样性是否合适
#
# 【一个实用的组合策略】:
# 采购 500 个化合物做筛选:
# 300 个多样性覆盖(探索新化学类型)
# 150 个针对已知苗头的类似物(深化)
# 50 个基于结构设计的(假设驱动)
# → 【平衡探索与利用】
库设计的实践
# 【场景:从 100 万商业库中选 5000 个做筛选】
def design_screening_library(library_smiles, n_select=5000,
property_filter=True):
from rdkit.SimDivFilters import rdSimDivPickers
# ---- 步骤 1:性质过滤 ----
# 先排除明显不合适的(见 064、069)
candidates = []
for s in library_smiles:
m = Chem.MolFromSmiles(s)
if m is None:
continue
if property_filter:
mw = Descriptors.MolWt(m)
logp = Descriptors.MolLogP(m)
if not (200 <= mw <= 500 and -1 <= logp <= 5):
continue
if Descriptors.NumRotatableBonds(m) > 10:
continue
candidates.append(s)
print(f"性质过滤: {len(library_smiles)} → {len(candidates)}")
# ---- 步骤 2:结构警示过滤 ----
from rdkit.Chem import FilterCatalog
params = FilterCatalog.FilterCatalogParams()
params.AddCatalog(FilterCatalog.FilterCatalogParams.FilterCatalogs.PAINS)
catalog = FilterCatalog.FilterCatalog(params)
candidates = [s for s in candidates
if not catalog.HasMatch(Chem.MolFromSmiles(s))]
print(f"警示过滤后: {len(candidates)}")
# ---- 步骤 3:多样性选择 ----
fps = [gen.GetFingerprint(Chem.MolFromSmiles(s)) for s in candidates]
picker = rdSimDivPickers.MaxMinPicker()
def dist(i, j):
return 1 - DataStructs.TanimotoSimilarity(fps[i], fps[j])
indices = picker.LazyPick(dist, len(fps), n_select, seed=42)
selected = [candidates[i] for i in indices]
# ---- 步骤 4:验证多样性 ----
print("\n选出的库的多样性:")
for k, v in diversity_metrics(selected).items():
print(f" {k}: {v}")
return selected
# 【顺序很重要】:
# 【先过滤再选多样性】
# 如果先选多样性再过滤,
# 会因为过滤掉一部分而破坏多样性的均衡
评估筛选结果的多样性
# 【筛选之后的问题】:
# 命中的 50 个分子,代表了几个化学系列?
def analyze_hits(hit_smiles, cutoff=0.4):
"""分析命中化合物的多样性"""
from rdkit.ML.Cluster import Butina
metrics = diversity_metrics(hit_smiles)
print(f"命中数: {metrics['n_molecules']}")
print(f"骨架数: {metrics['n_scaffolds']}")
print(f"内部多样性: {metrics['internal_diversity']}")
# 聚类看有几个独立的化学系列
fps = [gen.GetFingerprint(Chem.MolFromSmiles(s))
for s in hit_smiles if Chem.MolFromSmiles(s)]
dists = []
for i in range(1, len(fps)):
sims = DataStructs.BulkTanimotoSimilarity(fps[i], fps[:i])
dists.extend([1 - s for s in sims])
clusters = Butina.ClusterData(dists, len(fps), cutoff, isDistData=True)
print(f"【独立的化学系列: {len(clusters)}】")
for i, cl in enumerate(sorted(clusters, key=len, reverse=True)[:5]):
print(f" 系列 {i+1}: {len(cl)} 个分子")
return clusters
# 【为什么这个分析重要】:
# 50 个命中分散在 8 个系列 → 【很好,有多个起点】
# 50 个命中全在 1 个系列 → 【风险高,这个系列失败就没了】
# → 【直接影响下一步的资源分配决策】
关键要点
- 不同维度的多样性可能不一致——骨架多样但性质集中是常见情况;
- 不是越多样越好——苗头发现要多样,先导优化要聚焦;
- 库设计时先过滤再选多样性,顺序反了会破坏均衡;
- 命中分散在多个系列比集中在一个系列的风险低得多,直接影响资源分配。
延伸资源
- 聚类:050《分子相似性聚类》;化学空间可视化:052《化学空间可视化》;骨架:041《Bemis–Murcko Scaffold》;
- 片段空间:054《药物片段与片段空间》;结构警示:069《Brenk Alert》;类药性:064《Lipinski 五规则》。