mcpField
U
unichem-database

unichem-database

github.com/jaechang-hits数据与数据库38468 次安装6 天前更新
代码仓库

关于 unichem-database

UniChem 是 EMBL-EBI 提供的化学结构交叉引用服务,面向需要整合公共化学数据库的研究人员。它利用 InChI 标识符,将化合物映射到 20 多个数据库的记录,并提供结构连接性检索。

主要功能

  1. 01按 InChIKey 查询跨库记录
  2. 02通过来源数据库 ID 查询
  3. 03检索结构连接性相关化合物
  4. 04列出可用数据源目录
  5. 05以 REST API 返回交叉引用

适用场景

  1. 01将 ChEMBL ID 转换为 PubChem CID
  2. 02核对多个数据库中的化合物身份
  3. 03查找立体异构体或盐形式的相关记录
  4. 04合并生物活性表与结构数据库标识符

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill unichem-database

安装后在 Claude Code 中输入/unichem-database确认已加载。

详细介绍

UniChem 数据库

概述

UniChem 是 EMBL-EBI 提供的一项化学结构交叉引用服务,使用基于 InChI 的标识符关联 20 多个公共化学数据库中的化合物记录。 它会将单个化学实体映射到其在 ChEMBL、DrugBank、PubChem、ChEBI、PDB(RCSB 和 PDBe)、SureChEMBL、HMDB、DrugCentral、BindingDB 等数据库中的对应 ID。 可通过 https://www.ebi.ac.uk/unichem/api/v1/ 上的免费 REST API 访问,无需 API 密钥。 重要提示:所有交叉引用查询都通过带 JSON 正文的 POST 发送;只有目录端点 GET /sources 使用 GET 实现。

适用场景

  • 将 ChEMBL 化合物 ID 转换为 PubChem CID、DrugBank 登录号或 ChEBI ID,以便进行跨数据库分析
  • 根据 InChIKey 查找化合物出现在哪些数据库来源中
  • 使用连通性搜索,在各数据库中查找所有结构相关化合物(连通性相同,但立体化学/盐形式不同)
  • 在合并多个数据库的数据集之前,验证不同来源中的化合物身份
  • 为药物发现项目构建化合物交叉引用表(将 ChEMBL 中的生物活性数据关联到 PDB 中的结构数据)
  • 通过 InChIKey 检查合成化合物或供应商化合物是否存在于任何公共数据库中
  • 如需完整的生物活性谱(IC50、Ki),请使用 chembl-database-bioactivity;UniChem 仅提供 ID 交叉引用,不提供实验数据
  • 如需化合物性质预测或子结构搜索,请使用 pubchem-compound-search;UniChem 仅用于标识符转换

前提条件

  • Python 包:requests、pandas、matplotlib
  • 数据要求:化合物 InChIKey(标准 27 字符格式 XXXXXXXXXXXXXX-XXXXXXXXXX-X)、特定来源的 ID(例如 CHEMBL25),或作为起始点的 PubChem CID
  • 环境:需要互联网连接;无需 API 密钥
  • 速率限制:约 10 次请求/秒;在批量循环的请求之间添加 time.sleep(0.1);无每日配额
pip install requests pandas matplotlib

快速开始

UniChem /compounds 端点仅支持 POST——GET 会返回 405 Method Not Allowed。提交 JSON 正文 {type: inchikey, compound: KEY},并从 compounds[0][sources] 中读取各数据库的命中结果。每条来源记录都包含 id(数据库的数字 ID)和 compoundId(该数据库中的 ID)。

import requests

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def unichem_post(endpoint: str, body: dict) -> dict:
    """POST request to UniChem API; raise on HTTP errors."""
    r = requests.post(f"{UNICHEM_API}/{endpoint}", json=body, timeout=20)
    r.raise_for_status()
    return r.json()

# Find all database sources for aspirin by InChIKey
inchikey = "BSYNRYMUTXBXSQ-UHFFFAOYSA-N"  # aspirin
result = unichem_post("compounds", {"type": "inchikey", "compound": inchikey})
compounds = result.get("compounds", [])
print(f"Found {len(compounds)} compound record(s) for {inchikey}")
if compounds:
    sources = compounds[0].get("sources", [])
    print(f"  Present in {len(sources)} database records")
    seen = set()
    for src in sources:
        if src["id"] in seen:
            continue
        seen.add(src["id"])
        print(f"  source id={src['id']:>3} ({src['shortName']:>12}): {src['compoundId']}")
        if len(seen) >= 5:
            break
# Found 1 compound record(s) for BSYNRYMUTXBXSQ-UHFFFAOYSA-N
#   Present in many database records
#   source id=  1 (      chembl): CHEMBL25
#   source id=  2 (    drugbank): DB00945
#   source id=  3 (    rcsb_pdb): AIN

核心 API

查询 1:InChIKey 查询——所有来源

使用标准 InChIKey 搜索化合物并获取所有数据库记录。这是主要的交叉引用方法。端点为 POST /compounds;如果找到结果,响应会在 compounds 中包含一个条目,每个条目都有一个 sources 列表,其中的记录使用 id(来源数据库)和 compoundId(该数据库中的 ID)。

import requests, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

# Common source IDs (verify with the /sources endpoint - see Query 4)
SOURCE_NAMES = {
    1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 4: "GtoPdb", 5: "PDBe",
    7: "ChEBI", 14: "FDA SRS", 15: "SureChEMBL", 18: "HMDB", 22: "PubChem",
    31: "BindingDB", 32: "CompTox", 33: "LIPID MAPS", 34: "DrugCentral",
    37: "BRENDA", 38: "Rhea", 41: "SwissLipids", 49: "Probes-and-Drugs",
}

def lookup_by_inchikey(inchikey: str) -> pd.DataFrame:
    """Return all database cross-references for an InChIKey."""
    r = requests.post(f"{UNICHEM_API}/compounds",
                      json={"type": "inchikey", "compound": inchikey}, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    if not compounds:
        return pd.DataFrame()
    rows = []
    for src in compounds[0].get("sources", []):
        rows.append({
            "source_id": src["id"],
            "source_name": SOURCE_NAMES.get(src["id"], src.get("shortName", "")),
            "compound_id": src["compoundId"],
            "url": src.get("url", ""),
        })
    return pd.DataFrame(rows).sort_values(["source_id", "compound_id"])

# Triclosan cross-references
df = lookup_by_inchikey("XEFQLINVKFYRCS-UHFFFAOYSA-N")
print(f"Triclosan found in {df['source_id'].nunique()} distinct databases ({len(df)} records):")
print(df[["source_name", "compound_id"]].head(8).to_string(index=False))
# Triclosan found in 16 distinct databases
#   ChEMBL    CHEMBL849
#   DrugBank  DB08604
#   RCSB PDB  TCL
#   ChEBI     CHEBI:164200
# Extract specific source IDs from cross-reference table
def get_id_for_source(inchikey: str, source_id: int) -> str | None:
    """Return the compound ID in a specific database, or None if not found."""
    r = requests.post(f"{UNICHEM_API}/compounds",
                      json={"type": "inchikey", "compound": inchikey}, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    if not compounds:
        return None
    for src in compounds[0].get("sources", []):
        if src["id"] == source_id:
            return src["compoundId"]
    return None

triclosan = "XEFQLINVKFYRCS-UHFFFAOYSA-N"
chembl_id   = get_id_for_source(triclosan, source_id=1)   # ChEMBL
pubchem_id  = get_id_for_source(triclosan, source_id=22)  # PubChem
drugbank_id = get_id_for_source(triclosan, source_id=2)   # DrugBank
print(f"Triclosan: ChEMBL={chembl_id}, PubChem={pubchem_id}, DrugBank={drugbank_id}")
# Triclosan: ChEMBL=CHEMBL849, PubChem=5564, DrugBank=DB08604
查询 2:按特定来源 ID 查询化合物

已知特定来源数据库中的化合物 ID(例如 ChEMBL ID)时,可获取所有交叉引用。使用 type: sourceID,并在正文中同时提供化合物的来源 ID 和数字形式的 sourceID。返回的数据结构与 InChIKey 查询相同。

import requests

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def get_sources_for_compound(compound_id: str, source_id: int) -> list:
    """Get all database cross-references for a compound identified in a specific source.

    Args:
        compound_id: The ID in the source database (e.g., CHEMBL192)
        source_id: UniChem source ID (1=ChEMBL, 2=DrugBank, 22=PubChem, 7=ChEBI)
    """
    body = {"type": "sourceID", "compound": compound_id, "sourceID": source_id}
    r = requests.post(f"{UNICHEM_API}/compounds", json=body, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    if not compounds:
        return []
    return compounds[0].get("sources", [])

# Sildenafil (Viagra): look up starting from ChEMBL ID
sources = get_sources_for_compound("CHEMBL192", source_id=1)
distinct_dbs = {s["id"] for s in sources}
print(f"Sildenafil (CHEMBL192): {len(sources)} source records across {len(distinct_dbs)} databases")
seen = set()
for s in sources:
    if s["id"] in seen:
        continue
    seen.add(s["id"])
    print(f"  [{s['id']:>3}] {s['shortName']:>15}: {s['compoundId']}")
    if len(seen) >= 8:
        break
# Sildenafil (CHEMBL192): 272 source records across 19 databases
查询 3:连通性搜索——结构相关化合物

查找核心结构相同、但立体化学、盐形式、同位素标记或质子化状态不同的化合物。端点为 POST /connectivity,接受完整的标准 InChIKey(API 会对 14 字符片段返回 404 Not found)。UniChem 内部使用连通性层进行匹配,但返回的命中结果可能在立体化学/电荷/同位素层有所不同。

与 /compounds 不同,连通性响应会返回一个扁平的 sources 列表(包含所有相关化合物在各数据库中的命中结果,每个命中一条),以及查询到的 searchedCompound 和 totalCompounds/totalSources 汇总字段。每个命中中的 comparison 字典会显示哪些 InChI 层匹配。

import requests, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def connectivity_search(inchikey: str) -> dict:
    """Find all compounds related by InChI connectivity (same skeleton, possibly different stereo/salts).

    Pass a FULL standard InChIKey (27 chars). The server rejects 14-char fragments.
    """
    body = {"type": "inchikey", "compound": inchikey}
    r = requests.post(f"{UNICHEM_API}/connectivity", json=body, timeout=30)
    r.raise_for_status()
    return r.json()

# Warfarin: find all stereoforms, racemates, and salt forms
warfarin_inchikey = "PJVWKTKQMONHTI-UHFFFAOYSA-N"
data = connectivity_search(warfarin_inchikey)
print(f"Warfarin connectivity relatives: {data['totalCompounds']} unique compounds, "
      f"{data['totalSources']} database records")
hits = data.get("sources", [])
by_source = {}
for h in hits:
    by_source.setdefault(h["shortName"], []).append(h["compoundId"])
for name, ids in sorted(by_source.items(), key=lambda kv: -len(kv[1]))[:8]:
    print(f"  {name:>15}: {len(ids):>4} IDs (e.g. {ids[0]})")
# Warfarin connectivity relatives: 13 unique compounds, 353 database records
# Compare source coverage across connectivity relatives
def compare_coverage(inchikey: str) -> pd.DataFrame:
    """Show connectivity relatives split by their source-database coverage."""
    data = connectivity_search(inchikey)
    rows = []
    for src in data.get("sources", []):
        rows.append({
            "source_id":    src["id"],
            "source_name":  src["shortName"],
            "compound_id":  src["compoundId"],
            "stereo_match": src["comparison"].get("stereoSp3", False),
            "salt_match":   src["comparison"].get("protonation", False),
        })
    return pd.DataFrame(rows)

df = compare_coverage("PJVWKTKQMONHTI-UHFFFAOYSA-N")
print(f"Total records: {len(df)}")
print(df.head(10).to_string(index=False))
print(f"Records with stereo mismatch (skeleton matches, stereo differs): {(~df['stereo_match']).sum()}")
查询 4:列出所有数据来源

获取 UniChem 数据来源的完整列表,包括其 ID、名称、描述和网站 URL。这是唯一通过 GET 提供的端点。每个来源条目中的响应字段使用 sourceID(大写 ID)。

import requests, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def list_sources() -> pd.DataFrame:
    """Return all UniChem data sources as a DataFrame."""
    r = requests.get(f"{UNICHEM_API}/sources", timeout=15)
    r.raise_for_status()
    sources = r.json().get("sources", [])
    rows = []
    for s in sources:
        rows.append({
            "source_id":  s["sourceID"],
            "name":       s.get("nameLong") or s.get("nameLabel", ""),
            "label":      s.get("nameLabel", ""),
            "short_name": s.get("name", ""),
            "uci_count":  s.get("UCICount"),
            "url":        s.get("baseIdUrl", ""),
        })
    return pd.DataFrame(rows).sort_values("source_id")

sources_df = list_sources()
print(f"Total UniChem sources: {len(sources_df)}")
print(sources_df[["source_id", "label", "uci_count"]].to_string(index=False))
# Total UniChem sources: 23
#   1   ChEMBL          2854815
#   2   DrugBank          14622
#  22   PubChem      123392679
查询 5:逐个化合物循环(无批量端点)

UniChem 不支持列表批量处理形式——/compounds POST 请求每次只能接受一个化合物。提交 {compounds: [...]} 或 {inchikeys: [...]} 会返回 400 illegal_argument_exception。对于多个输入,请逐个处理,并短暂休眠,以遵守约 10 req/s 的速率限制。

import requests, time, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def batch_translate(inchikeys: list[str],
                    target_source_ids=(1, 2, 7, 22)) -> pd.DataFrame:
    """Translate a list of InChIKeys to IDs in multiple target databases.

    Loops one POST per InChIKey (UniChem has no list-batch endpoint).
    """
    SOURCE_NAMES = {1: "chembl", 2: "drugbank", 3: "pdb", 7: "chebi",
                    14: "fda_srs", 22: "pubchem", 34: "drugcentral"}
    rows = []
    for ik in inchikeys:
        row = {"inchikey": ik}
        for sid in target_source_ids:
            row[SOURCE_NAMES.get(sid, f"src_{sid}")] = None
        try:
            r = requests.post(f"{UNICHEM_API}/compounds",
                              json={"type": "inchikey", "compound": ik}, timeout=20)
            r.raise_for_status()
            compounds = r.json().get("compounds", [])
            if compounds:
                for src in compounds[0].get("sources", []):
                    if src["id"] in target_source_ids:
                        col = SOURCE_NAMES.get(src["id"], f"src_{src['id']}")
                        if row[col] is None:
                            row[col] = src["compoundId"]
        except requests.RequestException as e:
            row["error"] = str(e)
        rows.append(row)
        time.sleep(0.1)  # respect ~10 req/s rate limit
    return pd.DataFrame(rows)

# Translate a set of NSAIDs by InChIKey
nsaid_inchikeys = [
    "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",  # aspirin
    "HEFNNWSXXWATRW-UHFFFAOYSA-N",  # ibuprofen
    "CMWTZPSULFXXJA-VIFPVBQESA-N",  # naproxen
    "DCOPUUMXTXDBNB-UHFFFAOYSA-N",  # diclofenac (free acid)
]
df = batch_translate(nsaid_inchikeys, target_source_ids=[1, 2, 7, 22])
print(df.to_string(index=False))
df.to_csv("nsaid_xrefs.csv", index=False)
print(f"Saved nsaid_xrefs.csv ({len(df)} compounds)")
查询 6:使用来源 ID 输入逐个化合物循环

如果起始标识符不是 InChIKey,而是特定来源的 ID(例如来自生物活性表的一组 ChEMBL ID),请使用 type=sourceID 并逐个循环,同样每个 ID 发送一个 POST 请求。

import requests, time

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def translate_source_ids(ids: list[str], from_source: int,
                         to_sources=(2, 22, 34)) -> list[dict]:
    """Translate IDs in one source database to IDs in target databases.

    Args:
        ids: list of compound IDs in the from_source database
        from_source: source ID of the input list (1=ChEMBL, 22=PubChem, ...)
        to_sources: iterable of target source IDs to extract
    """
    out = []
    for cid in ids:
        body = {"type": "sourceID", "compound": cid, "sourceID": from_source}
        r = requests.post(f"{UNICHEM_API}/compounds", json=body, timeout=20)
        row = {"input": cid}
        if r.ok:
            compounds = r.json().get("compounds", [])
            if compounds:
                row["inchikey"] = compounds[0].get("standardInchiKey")
                hits = {s["id"]: s["compoundId"] for s in compounds[0].get("sources", [])}
                for tsid in to_sources:
                    row[f"src_{tsid}"] = hits.get(tsid)
        out.append(row)
        time.sleep(0.1)
    return out

# Three kinase inhibitors known by ChEMBL ID
chembl_inputs = ["CHEMBL535", "CHEMBL553", "CHEMBL941"]  # nilotinib, dasatinib, imatinib
rows = translate_source_ids(chembl_inputs, from_source=1, to_sources=(2, 22, 34))
for row in rows:
    ik = (row.get("inchikey") or "?")[:14]
    print(f"{row['input']:>10}  ik={ik}...  "
          f"DrugBank={row.get('src_2')}, PubChem={row.get('src_22')}, "
          f"DrugCentral={row.get('src_34')}")

核心概念

InChI 与 InChIKey

UniChem 使用 InChI(IUPAC 国际化学标识符)及其哈希形式 InChIKey 作为化合物的标准标识。InChIKey 是由 27 个字符组成的字符串,分为三个部分:前 14 个字符编码连接层(重原子和键),接下来的 8 个字符编码立体化学和电荷,最后一个字符是版本标记。UniChem 通过要求标准 InChIKey 完全一致来交叉引用化合物,确保不同数据库中的化学实体相同。

来源 ID 参考表(已通过 /sources 实时验证)
来源 ID数据库范围
1ChEMBL生物活性分子、药物发现
2DrugBank已批准药物、药理学
3RCSB PDB晶体结构中的配体(美国)
4Guide to Pharmacology药理学靶标/配体
5PDBe晶体结构中的配体(欧洲)
7ChEBI化学本体、代谢物
14FDA SRSFDA 物质注册系统
15SureChEMBL专利化学
18HMDB人类代谢组数据库
22PubChem通用化合物库
31BindingDB结合亲和力数据
32CompTox环境毒理学仪表板
33LIPID MAPS脂质结构
34DrugCentral已批准药物 + 药理学
37BRENDA酶底物/产物
38Rhea生化反应
41SwissLipids脂质结构
49Probes-and-Drugs化学探针
字段命名:id 与 sourceID

这是编写 UniChem 脚本时最常见的错误。两个端点对来源数据库标识符使用不同的字段名:

  • GET /sources 将数据库列为对象,并使用 sourceID 字段(ID 大写)。
  • POST /compounds 和 POST /connectivity 会在 sources 列表中返回每个数据库的匹配项,其中来源标识符使用普通的 id 字段。这些匹配记录中没有 sourceID 或 sourceId 键。

遍历化合物或连接性响应时,始终使用 src["id"];遍历 /sources 目录时,使用 s["sourceID"]。

连接性匹配与标准 InChIKey 匹配

POST /compounds 返回精确的 InChIKey 匹配项(立体化学、盐、同位素均相同)。POST /connectivity 返回所有键拓扑相同的化合物——可用于查找外消旋体、立体异构体、游离酸/碱以及共晶搭档。连接性响应会为每个匹配项提供一个比较字典,指出哪些 InChI 层匹配(stereoSp3、protonation、isotope 等);可用它筛选骨架相同、仅立体化学不同的相关化合物。

常见工作流

工作流 1:药物化合物交叉引用报告

目标:给定一组药物名称(或 ChEMBL ID),解析出所有主要数据库 ID 并导出为 CSV。

import requests, time, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
CHEMBL_API = "https://www.ebi.ac.uk/chembl/api/data"

SOURCE_NAMES = {1: "chembl", 2: "drugbank", 3: "rcsb_pdb",
                7: "chebi", 22: "pubchem", 14: "fda_srs", 34: "drugcentral"}

def chembl_to_inchikey(chembl_id: str) -> str | None:
    """Look up the standard InChIKey for a ChEMBL compound ID."""
    r = requests.get(f"{CHEMBL_API}/molecule/{chembl_id}.json", timeout=15)
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json().get("molecule_structures", {}).get("standard_inchi_key")

def inchikey_to_sources(inchikey: str) -> dict:
    """Return source_id -> compound_id dict for an InChIKey (first hit per source)."""
    r = requests.post(f"{UNICHEM_API}/compounds",
                      json={"type": "inchikey", "compound": inchikey}, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    if not compounds:
        return {}
    out = {}
    for s in compounds[0].get("sources", []):
        out.setdefault(s["id"], s["compoundId"])
    return out

# Example: top cardiovascular drugs
drug_chembl_ids = {
    "atorvastatin": "CHEMBL1487",
    "lisinopril":   "CHEMBL1237",
    "metoprolol":   "CHEMBL13",
    "amlodipine":   "CHEMBL1491",
    "warfarin":     "CHEMBL1464",
}

rows = []
for name, chembl_id in drug_chembl_ids.items():
    ik = chembl_to_inchikey(chembl_id)
    row = {"drug": name, "chembl_id": chembl_id, "inchikey": ik}
    if ik:
        srcs = inchikey_to_sources(ik)
        for sid, col in SOURCE_NAMES.items():
            row[col] = srcs.get(sid)
    rows.append(row)
    time.sleep(0.2)

df = pd.DataFrame(rows)
df.to_csv("drug_xrefs.csv", index=False)
print(df[["drug", "chembl", "drugbank", "pubchem", "chebi"]].to_string(index=False))
print(f"Saved drug_xrefs.csv ({len(df)} drugs)")
工作流 2:结构相关化合物发现与可视化

目标:查找某个化合物的所有结构相关化合物,汇总其数据库覆盖情况,并绘制条形图展示来源分布。

import requests, pandas as pd
import matplotlib.pyplot as plt
from collections import Counter

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

SOURCE_NAMES = {1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 5: "PDBe",
                7: "ChEBI", 14: "FDA SRS", 15: "SureChEMBL",
                22: "PubChem", 31: "BindingDB", 34: "DrugCentral"}

# Aspirin connectivity relatives (covers acetylsalicylate salts)
query_inchikey = "BSYNRYMUTXBXSQ-UHFFFAOYSA-N"

r = requests.post(f"{UNICHEM_API}/connectivity",
                  json={"type": "inchikey", "compound": query_inchikey}, timeout=30)
r.raise_for_status()
data = r.json()
hits = data.get("sources", [])
print(f"Aspirin connectivity relatives: {data['totalCompounds']} unique compounds, "
      f"{len(hits)} database records")

# Count how often each named database appears
source_counter = Counter()
for h in hits:
    if h["id"] in SOURCE_NAMES:
        source_counter[SOURCE_NAMES[h["id"]]] += 1

labels = [k for k, _ in source_counter.most_common()]
counts = [v for _, v in source_counter.most_common()]

fig, ax = plt.subplots(figsize=(9, 4))
bars = ax.bar(labels, counts, color="#2E86AB", edgecolor="white")
ax.bar_label(bars, padding=2)
ax.set_xlabel("Database")
ax.set_ylabel("Number of Source Records (relatives x hits)")
ax.set_title("UniChem Connectivity Records - Aspirin Skeleton")
plt.xticks(rotation=30, ha="right")
plt.tight_layout()
plt.savefig("unichem_connectivity_coverage.png", dpi=150, bbox_inches="tight")
print("Saved unichem_connectivity_coverage.png")
plt.close(fig)

# DataFrame summary by source
df = pd.DataFrame(source_counter.most_common(), columns=["database", "records"])
print(df.to_string(index=False))
工作流 3:将 ChEMBL 生物活性数据与 PubChem CID 合并

目标:为 ChEMBL 生物活性表补充 PubChem CID,以便在使用 PubChem 标识符的工具中进行后续分析。

import requests, time, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def add_pubchem_cids(df: pd.DataFrame,
                     inchikey_col: str = "standard_inchi_key") -> pd.DataFrame:
    """Add pubchem_cid column to a DataFrame that has an InChIKey column."""
    unique_keys = df[inchikey_col].dropna().unique()
    mapping = {}
    for ik in unique_keys:
        try:
            r = requests.post(f"{UNICHEM_API}/compounds",
                              json={"type": "inchikey", "compound": ik}, timeout=15)
            r.raise_for_status()
            compounds = r.json().get("compounds", [])
            if compounds:
                for src in compounds[0].get("sources", []):
                    if src["id"] == 22:  # PubChem
                        mapping[ik] = src["compoundId"]
                        break
        except requests.RequestException:
            pass
        time.sleep(0.1)
    df = df.copy()
    df["pubchem_cid"] = df[inchikey_col].map(mapping)
    return df

# Simulate a small ChEMBL activity table
chembl_data = pd.DataFrame({
    "compound_name": ["aspirin", "ibuprofen", "naproxen"],
    "standard_inchi_key": [
        "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
        "HEFNNWSXXWATRW-UHFFFAOYSA-N",
        "CMWTZPSULFXXJA-VIFPVBQESA-N",
    ],
    "ic50_nm": [2500.0, 13000.0, 1600.0],
})

enriched = add_pubchem_cids(chembl_data)
print(enriched[["compound_name", "ic50_nm", "pubchem_cid"]].to_string(index=False))
enriched.to_csv("chembl_with_pubchem.csv", index=False)
print("Saved chembl_with_pubchem.csv")

关键参数

参数端点默认值范围 / 选项效果
type(请求体)POST /compounds, POST /connectivity-inchikey, sourceID选择 compound 中标识符的类型。inchikey 需要 27 个字符的标准 InChIKey;sourceID 还需要数值型 sourceID 字段。
compound(请求体)POST /compounds, POST /connectivity-单个字符串(不能是列表)要查询的标识符。UniChem 不支持列表批量格式;每个 compound 循环发送一次 POST 请求。
sourceID(请求体)POST /compounds(type=sourceID 时)-来自 /sources 的整数数据库 ID输入 compound 所属的源数据库(1=ChEMBL,2=DrugBank,22=PubChem,……)。
单条命中记录的 id(响应)POST /compounds, POST /connectivity-整数sources 中每条命中记录对应的源数据库 ID。这些单条命中记录请使用 src["id"],而不是 src["sourceID"]。
sourceID(响应)GET /sources-整数目录中每个数据库条目的数字 ID(大写 ID)。
timeout所有请求20s任意正整数请求失败前等待的秒数;对常见骨架的 /connectivity 请求,可将其提高到 30s。

最佳实践

  1. 除 /sources 外一律使用 POST:GET /compounds 返回 405 Method Not Allowed;GET /compounds/{src}/{id} 和 GET /connectivity/{key} 返回 404。唯一的 GET 端点是 /sources。始终使用 JSON 请求体发送 POST 请求。

  2. 使用正确的字段 - 命中记录用 id,/sources 用 sourceID:在 POST /compounds 和 POST /connectivity 返回的 sources 列表中,每条记录的源数据库位于 id 键中。读取 src["sourceID"] 会引发 KeyError。大写 ID 字段 sourceID 仅出现在 GET /sources 返回的目录中。

  3. 使用标准 InChIKey(而非非标准 InChIKey):UniChem 对标准 InChIKey 建立了索引。非标准 InChIKey 不会返回结果。请使用以下代码验证:from rdkit.Chem.inchi import MolToInchiKey; MolToInchiKey(mol)。

  4. 精确匹配失败时,改用 connectivity 搜索:如果 DrugBank 中的 compound 是盐(例如盐酸盐),而你持有的是游离碱的 InChIKey,那么标准查询会找不到结果。可使用 connectivity 搜索作为药物交叉引用的备用方案。

  5. 向 /connectivity 传入完整的 InChIKey:该端点要求完整的 27 个字符的标准 InChIKey。只提交 14 个字符的连接性片段会返回 {response: Not found}。UniChem 会在内部去除立体化学 / 电荷层。

  6. 启动时缓存来源列表:调用一次 /sources 并构建 {sourceID: nameLabel} 字典,而不是将 ID 硬编码。

def load_source_map() -> dict:
    r = requests.get(f"{UNICHEM_API}/sources", timeout=15)
    r.raise_for_status()
    return {s["sourceID"]: s.get("nameLabel", str(s["sourceID"]))
            for s in r.json().get("sources", [])}
  1. 没有批量端点 - 循环请求并添加 sleep:提交 {compounds: [...]} 或 {inchikeys: [...]} 会返回 400 illegal_argument_exception。循环发送 POST 请求,并在调用之间执行 time.sleep(0.1)(约 10 req/s)。

常见用法

用法:将任意 Compound ID 解析为 InChIKey

适用场景:你有 PubChem CID 或 ChEBI ID,需要 InChIKey 来查询 UniChem 或其他服务。

import requests

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
PUBCHEM_API = "https://pubchem.ncbi.nlm.nih.gov/rest/pug"

def pubchem_cid_to_inchikey(cid) -> str | None:
    """Resolve a PubChem CID to a standard InChIKey via PubChem."""
    r = requests.get(f"{PUBCHEM_API}/compound/cid/{cid}/property/InChIKey/JSON", timeout=10)
    if r.status_code == 404:
        return None
    r.raise_for_status()
    props = r.json()["PropertyTable"]["Properties"]
    return props[0]["InChIKey"] if props else None

def cid_to_all_sources(cid) -> list:
    """PubChem CID -> InChIKey -> UniChem cross-references."""
    ik = pubchem_cid_to_inchikey(cid)
    if not ik:
        return []
    r = requests.post(f"{UNICHEM_API}/compounds",
                      json={"type": "inchikey", "compound": ik}, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    return compounds[0].get("sources", []) if compounds else []

sources = cid_to_all_sources(2244)  # PubChem CID for aspirin
distinct = {s["id"] for s in sources}
print(f"Aspirin (CID=2244) is in {len(distinct)} UniChem databases ({len(sources)} records)")
seen = set()
for s in sources:
    if s["id"] in seen:
        continue
    seen.add(s["id"])
    print(f"  [{s['id']:>3}] {s['shortName']:>15}: {s['compoundId']}")
    if len(seen) >= 6:
        break
用法:检查 Compound 是否为已批准药物

适用场景:使用 InChIKey 快速标记 compound 是否出现在 DrugBank(来源 2)中。

import requests

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"

def is_approved_drug(inchikey: str) -> tuple[bool, str | None]:
    """Check if compound appears in DrugBank (id=2). Returns (is_drug, DrugBank_ID)."""
    r = requests.post(f"{UNICHEM_API}/compounds",
                      json={"type": "inchikey", "compound": inchikey}, timeout=20)
    r.raise_for_status()
    compounds = r.json().get("compounds", [])
    if not compounds:
        return False, None
    for src in compounds[0].get("sources", []):
        if src["id"] == 2:  # DrugBank
            return True, src["compoundId"]
    return False, None

# Test a few compounds
test_compounds = {
    "aspirin":      "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
    "triclosan":    "XEFQLINVKFYRCS-UHFFFAOYSA-N",
    "sildenafil":   "BNRNXUUZRGQAQC-UHFFFAOYSA-N",
}
for name, ik in test_compounds.items():
    is_drug, db_id = is_approved_drug(ik)
    status = f"DrugBank:{db_id}" if is_drug else "not in DrugBank"
    print(f"{name:>12}: {status}")
用法:汇总一组 Compound 的来源覆盖情况

适用场景:审查哪些数据库收录了你的 compound 列表,这有助于决定后续分析使用哪个数据库。

import requests, time, pandas as pd

UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
SOURCE_NAMES = {1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 7: "ChEBI",
                14: "FDA SRS", 22: "PubChem", 34: "DrugCentral"}

def source_coverage_matrix(inchikeys: list[str]) -> pd.DataFrame:
    """Return a boolean matrix: rows=compounds, columns=databases."""
    rows = []
    for ik in inchikeys:
        r = requests.post(f"{UNICHEM_API}/compounds",
                          json={"type": "inchikey", "compound": ik}, timeout=15)
        row = {"inchikey": ik}
        for sid, name in SOURCE_NAMES.items():
            row[name] = False
        if r.ok:
            compounds = r.json().get("compounds", [])
            if compounds:
                ids_present = {s["id"] for s in compounds[0].get("sources", [])}
                for sid, name in SOURCE_NAMES.items():
                    row[name] = sid in ids_present
        rows.append(row)
        time.sleep(0.1)
    return pd.DataFrame(rows)

sample_keys = [
    "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",  # aspirin
    "HEFNNWSXXWATRW-UHFFFAOYSA-N",  # ibuprofen
    "XEFQLINVKFYRCS-UHFFFAOYSA-N",  # triclosan
]
coverage = source_coverage_matrix(sample_keys)
print(coverage.to_string(index=False))
print("Coverage per database:")
for col in list(SOURCE_NAMES.values()):
    print(f"  {col}: {coverage[col].sum()}/{len(coverage)}")

故障排查

问题原因解决方案
在 /compounds 上出现 405 Method Not Allowed使用 GET 请求访问端点UniChem /compounds 仅支持 POST。使用 requests.post(url, json={type:inchikey, compound:ik}) 发送请求。
每条命中记录出现 KeyError: sourceID(或 sourceId)读取了错误的字段名对 compounds[0][sources] 中的命中项和 /connectivity 返回的来源列表,使用 src["id"]。sourceID 仅存在于 /sources 目录中。
出现 400 illegal_argument_exception field name is null or empty尝试使用 {compounds: [...]} 或 {inchikeys: [...]} 批量请求不存在批量端点。请循环发送请求,并在 POST 请求之间调用 time.sleep(0.1)。
/connectivity 返回 {response: Not found}仅传入了 14 个字符的片段发送完整的 27 个字符的标准 InChIKey;UniChem 会在内部剥离立体化学层。
已知化合物的 compounds 列表为空InChIKey 非标准、盐形式不匹配,或缺少该化合物使用 RDKit 验证 InChIKey;尝试 POST /connectivity,以捕获盐形式或立体化学变体。
connectivity 返回的记录过多同一骨架匹配到多个 SureChEMBL 专利 ID按 src["id"] 过滤以排除来源 15(SureChEMBL),或使用 comparison.stereoSp3 == True 过滤。
requests.exceptions.Timeout负载较高时 API 响应缓慢将 /connectivity 的超时时间延长至 30s;使用指数退避重试一次。
来源 URL 字段为空并非所有来源都提供 URL 模板使用 /sources 端点返回的 baseIdUrl 与 compoundId 手动构造链接。

相关技能

  • chembl-database-bioactivity - 使用通过 UniChem 解析出的化合物 ID 查询 ChEMBL 的生物活性数据(IC50、Ki)
  • pubchem-compound-search - 使用来自 UniChem 的 PubChem CID 查询完整的化合物属性和生物测定数据
  • pdb-database - 使用通过 UniChem 解析出的 PDB 配体代码(来源 3 或 5)查找 3D 配体结构
  • drugbank-database-access - 使用来自 UniChem 的 DrugBank ID 查询详细的药理学、ADMET 和药物相互作用数据

参考资料

评论

支持 Markdown