2. 数据获取:公共ADMET数据库与格式转换

做ADMET建模,第一步不是调参,而是找数据。我见过太多同学上来就怼模型,结果发现数据格式不对,或者压根就是从二手渠道抄来的SMILES。嗯,这步走稳了,后面至少省一半时间。

咱们这章聚焦三个公共数据库:ChEMBL、PubChem和DrugBank。我会告诉你每个库怎么下载、怎么转换格式。说白了,你得学会把数据从「别人家的仓库」搬到你自己的工作台上。

2.1 为什么选这三个数据库?

市面上生物活性数据库几十个,为什么我专挑这三个?原因很简单:

  • ChEMBL:手工 curated 过的药物化学数据,质量最高。我当年做CYP450模型,全是从ChEMBL拉的。
  • PubChem:量大管饱。BioAssay板块有近300万条测试数据,做训练集的好选择。
  • DrugBank:已上市或临床阶段的药物数据。做ADMET预测,你就是想预测这类分子嘛。
【个人经验】我在项目里一般先用ChEMBL标定正负样本,再用PubChem做数据增强,最后用DrugBank做外部验证。三个库组合打,效果比我单用任何一个都好。

2.2 数据下载实操:以ChEMBL为例

先讲ChEMBL。它提供了官方Python API——chembl_webresource_client。安装只有一行:

pip install chembl_webresource_client

然后我们就能通过REST接口拉数据了。举个具体例子,假设你想拉关于「logD」这个ADMET属性的数据:

from chembl_webresource_client.new_client import new_client

# 搜索目标属性
properties = new_client.component_property
results = properties.filter(
    property_name__icontains='logD'
).only(['component_id', 'property_name', 'value', 'units'])

# 获取前5条看看
for r in results[:5]:
    print(r['component_id'], r['property_name'], r['value'])

注意这里有个坑:ChEMBL返回的是嵌套JSON。你需要自己flatten成表格。我习惯直接用pandas一把梭:

import pandas as pd

df = pd.json_normalize(results)
print(df.shape)
【避坑指南】我曾经直接从ChEMBL拉了10万条Caco-2数据,结果发现同一条化合物重复了N次。因为ChEMBL里多批次数据会生成多条记录。后来我都是用molregno加assay_id做唯一键去重的。

2.3 PubChem的数据获取

PubChem比ChEMBL更大更杂。它的官方接口是PUG REST。我一般这么用:

import requests

# 获取化合物的SDF格式
url = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2244/SDF"
resp = requests.get(url)

# 保存到本地
with open("aspirin.sdf", "wb") as f:
    f.write(resp.content)

但如果你要批量拉,千万别一个一个请求。用listkey机制:

# 先创建一个化合物列表
identifiers = {"cid": [2244, 3672, 5161]}
resp = requests.post(
    "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/SDF",
    json=identifiers
)

# 保存
with open("batch.sdf", "wb") as f:
    f.write(resp.content)

PubChem的BioAssay数据则是通过AID(Assay ID)获取。我建议你从BioAssay Browser先搜adhoc条件,再复制AID下来用。

2.4 DrugBank的下载与解析

DrugBank有点特殊。它限制免费用户一年只能下载一次XML格式的全库。我个人习惯每年初下载一次,然后本地维护。

解析XML是重头戏。DrugBank的XML结构里埋着SMILES、logP、pKa等ADMET属性。我用xml.etree.ElementTree解析:

import xml.etree.ElementTree as ET

tree = ET.parse("drugbank_all_full_database.xml")
root = tree.getroot()

ns = {"db": "http://www.drugbank.ca"}

# 提取每个药物
drugs = []
for drug in root.findall("db:drug", ns):
    name = drug.find("db:name", ns).text
    smiles = drug.find("db:calculated-properties/db:property[db:kind='SMILES']/db:value", ns)
    if smiles is not None:
        drugs.append({"name": name, "smiles": smiles.text})

print(f"一共解析了 {len(drugs)} 个药物")
【小技巧】DrugBank的XML里有些字段是嵌套的,比如logP可能在calculated-properties下,也可能在experimental-properties下。我写代码时两种都查一遍,取非空值。

2.5 格式转换:SMILES vs SDF vs CSV

数据拿下来了,接下来就是格式问题。做ADMET建模,三种格式你必须会互转:

格式 优点 缺点 我什么时候用它
SMILES 轻量,一行一条,适合存文本 丢失立体结构、不能存3D坐标 数据清洗、建模标签匹配
SDF 保存完整分子结构、3D坐标 文件大,解析慢 分子指纹生成、3D描述符计算
CSV 最通用,pandas直接读 会丢失分子结构信息 最终训练集、测试集

转换工具我用RDKit,没有之一。来看看最常用的几个场景:

场景1:SMILES → SDF

from rdkit import Chem
from rdkit.Chem import AllChem

# 读CSV中的SMILES
import pandas as pd
df = pd.read_csv("molecules.csv")

writer = Chem.SDWriter("molecules.sdf")
for idx, row in df.iterrows():
    mol = Chem.MolFromSmiles(row["SMILES"])
    if mol:
        # 生成3D坐标
        mol = Chem.AddHs(mol)
        AllChem.EmbedMolecule(mol)
        AllChem.MMFFOptimizeMolecule(mol)
        writer.write(mol)
writer.close()

场景2:SDF → CSV

supplier = Chem.SDMolSupplier("batch.sdf")
data = []
for mol in supplier:
    if mol is None:
        continue
    smiles = Chem.MolToSmiles(mol)
    # 提取属性
    props = mol.GetPropsAsDict()
    data.append({"SMILES": smiles, **props})

df = pd.DataFrame(data)
df.to_csv("batch.csv", index=False)

场景3:批量SMILES标准化

同一个分子在不同数据库里可能写成不同的SMILES。我去年做hERG模型时就吃过这个亏,后来加了标准化步骤:

def standardize_smiles(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    # 移除氢、去重、规范映射
    mol = Chem.RemoveHs(mol)
    return Chem.MolToSmiles(mol, canonical=True)

df["SMILES_std"] = df["SMILES"].apply(standardize_smiles)
【曾经踩过的坑】我在ChEMBL拿到一个分子的SMILES是"CCO",在PubChem查到同一个分子的SMILES是"C(C)O"。看似一样,但直接用字符串去重会漏掉。自从用了canonical SMILES,再没出过问题。

2.6 数据整合:多库联合查询

实际项目中,你不会只用单一数据库。我做过一个P-gp底物预测模型,数据源来自三个库:ChEMBL(人工标注)、PubChem(高通量筛选)、DrugBank(临床药物)。

整合流程如下图:


数据获取 → SMILES标准化 → 去重(基于InChIKey) → 标签合并 → 质量控制 → 最终CSV

这里说下去重。去重不能用SMILES字符串,得用InChIKey。它比SMILES更可靠:

import hashlib

def get_inchikey(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    return Chem.MolToInchiKey(mol)

# 合并数据并用InChIKey去重
combined_df = pd.concat([chembl_df, pubchem_df, drugbank_df], ignore_index=True)
combined_df["inchikey"] = combined_df["SMILES_std"].apply(get_inchikey)
combined_df = combined_df.drop_duplicates(subset=["inchikey"])

标签合并更头疼:一个分子在ChEMBL中标「active」,在PubChem里标「inactive」。我一般采取多数投票法,如果冲突则取体外数据优先级高的那个。

2.7 一个完整的端到端脚本

说了这么多,我直接给你一个能用的脚本,跑通从ChEMBL拉到CSV的整个流程:

#!/usr/bin/env python3
"""
pull_chembl_admet.py
从ChEMBL拉取ADMET数据并保存为CSV
"""

from chembl_webresource_client.new_client import new_client
import pandas as pd
from rdkit import Chem

def pull_chembl(property_name, limit=5000):
    """拉取指定ADMET属性数据"""
    # 1. 查询属性
    props = new_client.component_property
    results = props.filter(property_name__icontains=property_name)

    # 2. 构建DataFrame
    data = []
    for i, r in enumerate(results):
        if i >= limit:
            break
        # 获取化合物信息
        comp = new_client.component_property.get(r['component_property_id'])
        smiles = comp.get('smiles', '')
        data.append({
            'molecule_chembl_id': r['molecule_chembl_id'],
            'property_name': r['property_name'],
            'value': r['value'],
            'units': r['units'],
            'SMILES': smiles
        })

    df = pd.DataFrame(data)

    # 3. SMILES标准化
    def std_smiles(s):
        mol = Chem.MolFromSmiles(s)
        return Chem.MolToSmiles(mol, canonical=True) if mol else None

    df['SMILES_std'] = df['SMILES'].apply(std_smiles)
    df = df.dropna(subset=['SMILES_std'])

    # 4. InChIKey去重
    df['inchikey'] = df['SMILES_std'].apply(lambda x: Chem.MolToInchiKey(Chem.MolFromSmiles(x)))
    df = df.drop_duplicates(subset=['inchikey'])

    return df

# 使用示例
df_logd = pull_chembl('logD', limit=2000)
df_logd.to_csv('chembl_logd.csv', index=False)
print(f"拉取完成,共 {len(df_logd)} 条数据")

这个脚本你直接拷贝就能跑。注意第一次运行会下载缓存,需要等一会儿。

2.8 本章小结

  • 三个数据库各有侧重:ChEMBL精、PubChem大、DrugBank临床。
  • 格式转换的核心工具是RDKit:SMILES转SDF、SDF转CSV、SMILES标准化,一个库搞定。
  • 去重要用InChIKey:别用SMILES字符串,会出现假阳性去重。
  • 标签冲突用投票:数据源多不是坏事,关键是定好优先级规则。