053

分子多样性评估:如何避免筛选库过于相似

筛选库过于相似会浪费实验资源。这篇给出多样性的量化指标、评估方法与库设计的实践。

一个一万个分子的库,如果全是同一系列的类似物,它提供的信息量远小于一千个多样化的分子多样性评估的目的,是回答「这批分子值不值得全部测试」这个实际问题。

多样性的多个维度

维度 衡量方式 关注
结构多样性 平均两两 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 个系列   → 【风险高,这个系列失败就没了】
#   → 【直接影响下一步的资源分配决策】

关键要点

  • 不同维度的多样性可能不一致——骨架多样但性质集中是常见情况;
  • 不是越多样越好——苗头发现要多样,先导优化要聚焦;
  • 库设计时先过滤再选多样性,顺序反了会破坏均衡;
  • 命中分散在多个系列比集中在一个系列的风险低得多,直接影响资源分配。

延伸资源