415

化合物优先级排序:多参数优化 MPO 实战

多参数优化是化合物排序的核心方法。这篇给出可运行的 MPO 实现、期望度函数设计与不确定性处理。

化合物优先级排序的本质是多参数优化(MPO):把活性、选择性、溶解度、代谢稳定性、安全性等多个指标综合成一个可比较的判断。做得好能显著提高决策质量,做得差则会系统性地误导团队。

为什么不能用硬阈值

# 传统做法:设一组硬阈值,全部满足的才保留
#
#   MW < 500 且 logP < 5 且 溶解度 > 50 μM 且 hERG > 10 μM
#
# 三个根本问题:
#
# 1) 阈值附近的悬崖
#    logP = 5.01 被淘汰、5.00 被保留
#    而预测/测量误差远大于 0.01 —— 这个切分毫无道理
#
# 2) 忽略了「差多少」
#    溶解度 49 μM 和 5 μM 都算「不达标」,但差别巨大
#
# 3) 无法权衡
#    一个活性极好但溶解度略低的分子,
#    与一个各项平庸但都达标的分子,哪个更值得做?
#    硬阈值给不出答案
#
# MPO 的解法:把每个指标映射到连续的「期望度」,再综合

期望度函数

import numpy as np

def desirability_higher(v, low, high):
    """越大越好:low 以下接近 0,high 以上接近 1"""
    if v is None or np.isnan(v):
        return 0.5
    k = max((high - low) / 6, 1e-9)
    return float(1 / (1 + np.exp(-(v - (low + high) / 2) / k)))

def desirability_lower(v, low, high):
    """越小越好"""
    if v is None or np.isnan(v):
        return 0.5
    k = max((high - low) / 6, 1e-9)
    return float(1 / (1 + np.exp((v - (low + high) / 2) / k)))

def desirability_range(v, low, high):
    """落在区间内最好(如 logD)"""
    if v is None or np.isnan(v):
        return 0.5
    center, width = (low + high) / 2, max((high - low) / 2, 1e-9)
    return float(np.exp(-((v - center) / width) ** 2))

# 关键设计选择:
#   1) 用平滑函数而非阶跃 → 消除阈值悬崖
#   2) 缺失值给 0.5(中性)而非 0 → 避免因缺数据被误杀
#   3) 上下限从 TPP 反推(见 414),而非拍脑袋

综合:几何平均而非算术平均

def mpo_score(compound, criteria):
    """criteria: {name: (fn, low, high, weight)}"""
    scores, weights = [], []
    for name, (fn, low, high, w) in criteria.items():
        d = fn(compound.get(name), low, high)
        scores.append(max(d, 1e-6) ** w)
        weights.append(w)
    return float(np.prod(scores) ** (1 / sum(weights)))

CRITERIA = {
    "pIC50":       (desirability_higher, 7.0, 9.0,  2.0),
    "selectivity": (desirability_higher, 1.0, 2.0,  1.5),
    "logD":        (desirability_range,  1.0, 3.5,  1.0),
    "solubility":  (desirability_higher, 10,  100,  1.5),
    "HLM_CLint":   (desirability_lower,  10,  50,   1.5),
    "hERG_pIC50":  (desirability_lower,  4.5, 5.5,  1.5),
    "Papp":        (desirability_higher, 2,   10,   1.0),
}

# 为什么用几何平均:
#   算术平均:一个 0 分项可被其它高分项掩盖
#     (0 + 1 + 1 + 1) / 4 = 0.75  ← 看着还行,但有致命缺陷
#   几何平均:任一项接近 0 则整体接近 0
#     (0 × 1 × 1 × 1)^(1/4) = 0   ← 正确反映「有致命缺陷」
#
# 这更符合「每一项都必须过关」的实际要求

不确定性感知

def mpo_with_uncertainty(predictions, uncertainties, criteria, n_sim=2000):
    """蒙特卡洛传播预测不确定性"""
    scores = []
    for _ in range(n_sim):
        sampled = {}
        for k, v in predictions.items():
            sd = uncertainties.get(k, 0.0)
            sampled[k] = np.random.normal(v, sd) if sd > 0 else v
        scores.append(mpo_score(sampled, criteria))
    scores = np.array(scores)
    return {
        "mean": float(scores.mean()),
        "std": float(scores.std()),
        "ci_90": [float(np.percentile(scores, 5)),
                  float(np.percentile(scores, 95))],
    }

def is_significantly_better(res_a, res_b, overlap_threshold=0.3):
    """判断 A 是否显著优于 B"""
    a_lo, a_hi = res_a["ci_90"]
    b_lo, b_hi = res_b["ci_90"]
    overlap = max(0, min(a_hi, b_hi) - max(a_lo, b_lo))
    span = max(a_hi - a_lo, b_hi - b_lo, 1e-9)
    return (res_a["mean"] > res_b["mean"]) and (overlap / span < overlap_threshold)

# 最重要的产出不是排名,而是:
#   「哪些化合物之间的差异在统计上不显著」
#
# 如果 A 和 B 的置信区间高度重叠,
# 就不该仅凭 MPO 分数选 A 弃 B ——
# 此时应引入其它因素:合成难度、新颖性、专利空间、化学多样性

权重怎么定

  • 从 TPP 反推(见 414《目标产品特征 TPP》):TPP 要求口服每日一次,那么代谢稳定性的权重就应该高;
  • 按项目阶段调整:早期活性权重高,后期性质权重上升;
  • 按当前瓶颈调整如果所有分子都卡在溶解度上,就提高溶解度的权重,引导优化方向;
  • 做敏感性分析:改变权重,看排名是否稳定。如果排名对权重极其敏感,说明候选之间的差异本来就不明显;
  • 让跨职能团队共同确定:权重反映的是价值判断,不应由单一角色决定。

完整的排序流程

import pandas as pd

def rank_compounds(df, criteria, uncertainty_cols=None):
    results = []
    for _, row in df.iterrows():
        preds = {k: row.get(k) for k in criteria}
        uncs = ({k: row.get(f"{k}_std", 0) for k in criteria}
                if uncertainty_cols else {})
        r = (mpo_with_uncertainty(preds, uncs, criteria)
             if uncs else {"mean": mpo_score(preds, criteria),
                           "ci_90": [None, None]})
        results.append({"id": row["id"], "smiles": row["smiles"],
                        "mpo": r["mean"], "ci_low": r["ci_90"][0],
                        "ci_high": r["ci_90"][1]})
    out = pd.DataFrame(results).sort_values("mpo", ascending=False)

    # 关键补充:标出哪些与第一名无显著差异
    if out["ci_low"].notna().all():
        top = out.iloc[0]
        out["tied_with_top"] = out.apply(
            lambda r: not (top["ci_low"] > r["ci_high"]), axis=1)
    return out

# 最终选择时,在「与第一名无显著差异」的分子中,
# 按化学多样性、合成难度、新颖性等因素挑选
# → 这比死板地取 top N 合理得多

常见误区

  • 把 MPO 分数当作绝对判断:它是排序工具,不是「这个分子有多好」的度量;
  • 忽略预测不确定性:用预测值做 MPO 而不考虑误差,会给出虚假的精确排名;
  • 权重拍脑袋定:应从 TPP 反推并做敏感性分析;
  • 只取 top N:应该在「统计上无显著差异」的集合中考虑多样性;
  • 用算术平均:会掩盖致命缺陷;
  • 不区分预测值与实测值两者的可信度完全不同,应分别处理或在不确定性中体现。

关键要点

  • 用平滑期望度函数替代硬阈值,消除阈值悬崖;
  • 用几何平均而非算术平均,防止致命缺陷被高分项掩盖;
  • 最有价值的产出是「哪些化合物差异不显著」,而非精确排名;
  • 权重应从 TPP 反推,并做敏感性分析验证排名稳定性。

延伸资源