数据源与获取:常用药物-靶点数据库的API使用与数据清洗

做药物图谱,第一步就是找数据。说实话,这个环节往往决定了后面所有工作的成败。我见过不少项目,模型调得再花哨,最后发现源头数据有坑,全白搭。今天我们就来聊聊怎么从三大主流数据库——DrugBank、STITCH和String——里把数据稳当地捞出来,再洗得干干净净。

3.1 DrugBank:结构化药物数据的金矿

DrugBank是药物-靶点信息的首选来源。它把数据组织得相当规整,支持RESTful API调用。

注册与API Key获取

需要先注册账号,在个人中心里申请一个API Key。记住,免费版每天有调用次数限制,我做项目时曾经一次性拉取整个数据库,结果被限流了整整24小时……嗯,后来我学乖了,分批拉。

查询药物-靶点关系

我常用的查询方式是利用药物ID或名称,获取其关联的所有靶点。下面是一个简单的Python示例:

import requests
import json

# 这里换成你自己的Key
API_KEY = "your_drugbank_api_key"
BASE_URL = "https://api.drugbankplus.com/v1"

def get_drug_targets(drug_id):
    url = f"{BASE_URL}/drugs/{drug_id}"
    headers = {"Authorization": f"Bearer {API_KEY}"}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        data = response.json()
        targets = data.get("targets", [])
        return [t["id"] for t in targets]
    else:
        print(f"请求失败: {response.status_code}")
        return []

# 示例:布洛芬
ibuprofen_targets = get_drug_targets("DB01050")
print(ibuprofen_targets)

空响应怎么办?DrugBank有时会因为药物已撤市或信息不完整而返回空列表。我曾遇到过一批老药,API返回了200但targets字段是空的。建议在代码里加个判空处理,不然下游处理会崩。

数据清洗要点

API拿回来的JSON看着规整,但里面有不少噪声:

  • 靶点名称不统一:同个靶点可能有多个别名,比如“EGFR”和“ErbB-1”其实是同一个东西。我个人习惯用UniProt ID做统一标识符。
  • 作用类型混淆:有的数据标着“antagonist”,有的标“inhibitor”,其实一个意思。我一般会建一个映射表来做归一化。
  • 缺失值处理:有些药物靶点关系缺少IC50或Ki值。不补吗?也行,但建模时那些值会用不上。我曾遇到过一批数据,IC50值全靠文献的文本描述提取,结果格式千奇百怪,清洗了两天才搞定。

批量拉取策略

假如要拉一批药物,直接用循环一个接一个请求?太慢了。我习惯配合异步加合理间隔:

import asyncio
import aiohttp

async def fetch_drug(session, drug_id):
    url = f"{BASE_URL}/drugs/{drug_id}"
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with session.get(url, headers=headers) as resp:
        return await resp.json()

async def batch_fetch(drug_ids):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_drug(session, did) for did in drug_ids]
        results = await asyncio.gather(*tasks)
        return results

# 用法
drug_ids = ["DB01050", "DB00201", "DB00619"]
data = asyncio.run(batch_fetch(drug_ids))

注意速率限制!我曾经在没加延时的情况下并行发了50个请求,直接被封IP两小时。建议每批请求之间加至少1秒的sleep。

3.2 STITCH:化学物质-蛋白质关联的桥梁

STITCH数据库专门存放化学小分子与蛋白质之间的相互作用。它和DrugBank不一样,STITCH更关注化合物层面的关联,不局限于已上市的药物。

API查询方式

STITCH支持多种查询方式,我常用的是通过化合物SMILES或PubChem CID来查蛋白质靶点:

import requests

STITCH_URL = "https://stitch.embl.de/api/"

def query_stitch(cid, species="9606"):  # 9606 是人类
    params = {
        "identifier": cid,
        "species": species,
        "format": "json"
    }
    r = requests.get(STITCH_URL, params=params)
    return r.json()

# 示例:使用PubChem CID
result = query_stitch("3672")  # 布洛芬的CID
proteins = result.get("proteins", [])
print([p["preferred_name"] for p in proteins])

数据清洗的坑

STITCH返回的数据里有个很麻烦的问题:置信度评分。每个蛋白质-化合物关联都带有一个combined_score,取值范围是0到1000。我的经验是直接一刀切滤掉低于400的关联,不然噪声太大。

还有一个坑——异构体问题。比如布洛芬有S和R两种光学异构体,STITCH数据库会分别记录,但很多下游工具并不区分。我建议看情况合并或只保留最常见的异构体。

STITCH数据中大约30%的关联评分低于400,这些关联在后续分子对接或药效团建模中基本派不上用场,尽早剔除能省不少时间。

3.3 String:蛋白质-蛋白质相互作用的枢纽

String数据库存的是蛋白质之间的相互作用网络。在药物图谱里,我们经常需要看靶点蛋白之间有没有关联,甚至构建靶点-靶点互作网络。

API调用

String的API非常简洁,输入一组UniProt ID或者Ensembl ID,返回它们之间的全部互作关系:

import requests
import json

STRING_API_URL = "https://string-db.org/api/json/network"

def get_ppi_network(protein_ids):
    params = {
        "identifiers": "%0d".join(protein_ids),
        "species": 9606
    }
    response = requests.post(STRING_API_URL, data=params)
    return response.json()

proteins = ["P00533", "P04626", "P00535"]  # EGFR, HER2, SRC
network = get_ppi_network(proteins)
for edge in network:
    print(f"{edge['preferredName_A']} --> {edge['preferredName_B']}: score={edge['score']}")

数据质量评判

String每条边都有多个置信度指标:

指标 含义 我个人常用的阈值
neighborhood 基因相邻 0.15
fusion 基因融合 0.5(很少用)
cooccurrence 共现 0.2
coexpression 共表达 0.4
experimental 实验验证 0.5
database 已知数据库 0.5
textmining 文本挖掘 0.3(噪声多)

我个人习惯用combined_score > 700作为筛选条件。为什么?因为有一次我做了个网络分析,用400的阈值,结果图里塞满了文本挖掘出来的假阳性关联,核心模块根本看不出来。后来提高到700,网络才清晰起来。

3.4 跨库数据对齐与清洗

三个库的数据格式各有不同。我通常会做一个统一的中间结构来对齐:

# 统一的药物-靶点-相互作用记录
{
    "drug_id": "DB01050",          # DrugBank ID
    "drug_name": "Ibuprofen",
    "target_id": "P23219",         # UniProt ID
    "target_name": "PTGS1",
    "interaction_type": "inhibitor",
    "confidence_score": 0.85,      # 归一化到0-1
    "source": "DrugBank/STITCH"
}

标识符映射

不同数据库用不同的ID体系,需要互相转换。我常用UniProt ID作为权威ID,因为它在三个库中都能找到对应关系。下面是ID映射常用的方法:

from bioservices import UniProt

u = UniProt()
def map_to_uniprot(identifier, source_db):
    if source_db == "drugbank":
        # DrugBank用UniProt ID作为target ID
        return identifier
    elif source_db == "stitch":
        # STITCH返回的蛋白质带NCBI GI号,要转一次
        result = u.mapping(fr="P_GI", to="ID", query=identifier)
        return result.get(identifier, None)
    elif source_db == "string":
        # String自己的ID,用它的API转
        # https://string-db.org/api/tsv/get_string_ids
        pass
    return None

别完全信任自动映射。我曾经映射过一批ID,结果发现有10%左右映射错了——字符串里多了一个空格或者大小写不一致。每批映射完了,我一般都会抽检20条手动核对一下。

3.5 知识体系框架

讲到这里,我把三个数据源的关系和清洗流程画一下,方便你理解整体的数据获取链路:


DrugBank
  |
  |-- RESTful API → JSON
  |-- 解析targets列表
  |-- 清洗:归一化作用类型, 去重, 补缺失ID
  |
STITCH
  |
  |-- RESTful API → JSON
  |-- 解析proteins列表
  |-- 清洗:按combined_score过滤, 合并异构体, ID映射到UniProt
  |
String
  |
  |-- POST API → JSON
  |-- 解析edges列表
  |-- 清洗:按combined_score过滤, 解析置信度指标, 去重
  |
  v
统一中间层
  |
  |-- 格式统一:{drug_id, target_id, score, source}
  |-- ID统一:全部转为UniProt
  |-- 冲突解决:同一条关联取最高分数
  |
  v
清洗后的药物-靶点-互作矩阵 → 下游建模

说白了,这个框架的核心就是“统一ID + 归一化分数 + 去除噪声”。这三步走完,数据基本就能喂给模型了。

3.6 推荐的数据存储策略

清洗完了,数据怎么存?我一般不走关系型数据库,而是用Parquet格式存成列式文件:

import pandas as pd

# 假设清洗完的数据在列表中
records = [
    {"drug_id": "DB01050", "target_id": "P23219", "score": 0.85, "source": "DrugBank"},
    {"drug_id": "DB01050", "target_id": "P35354", "score": 0.75, "source": "DrugBank"},
    # ...
]

df = pd.DataFrame(records)
df.to_parquet("drug_target_network.parquet", index=False)

# 读取回来
df_read = pd.read_parquet("drug_target_network.parquet")
print(df_read.shape)

为什么不用CSV?因为药物图谱数据动辄几百万条,CSV读一遍要半分钟,Parquet几秒钟就搞定。我在一个项目里用过CSV,每次迭代清洗都要等,后来换成Parquet,效率直接翻倍。

嗯,三个数据库的API怎么调、数据怎么洗、怎么对齐,大概就是这么一套流程。我个人觉得,花在数据清洗上的时间,最后都会在建模和调参阶段回报给你。