数据源与获取:常用药物-靶点数据库的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怎么调、数据怎么洗、怎么对齐,大概就是这么一套流程。我个人觉得,花在数据清洗上的时间,最后都会在建模和调参阶段回报给你。