mcpField
U
unichem-database

unichem-database

github.com/jaechang-hitsデータ・データベース38468 回インストール6 日前に更新
リポジトリ

unichem-database について

UniChemは、公共の化学データベースを横断して調査する研究者向けの、EMBL-EBIによる化学構造クロスリファレンスサービスです。InChIベースの識別子で20以上のデータベースのレコードを対応付け、結合性検索も提供します。

主な機能

  1. 01InChIKeyによるデータベース横断検索
  2. 02データソース固有IDによる化合物検索
  3. 03結合性に基づく類縁化合物の検索
  4. 04利用可能なデータソース一覧
  5. 05REST APIによるクロスリファレンス取得

活用シーン

  1. 01ChEMBL IDをPubChem CIDに変換
  2. 02複数データベース間で化合物の同一性を確認
  3. 03立体異性体や塩形のレコードを検索
  4. 04生物活性テーブルと構造DBの識別子を結合

インストール

コマンドをコピーしてターミナルで実行します。

$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 として実装されているのはカタログエンドポイント GET /sources のみです。

使用する場面

  • 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 文字の XXXXXXX… 形式)、ソース固有の ID(例:CHEMBL25)、または開始点となる PubChem CID
  • 環境:インターネット接続。API キーは不要です
  • レート制限:約 10 リクエスト/秒。バッチループではリクエスト間に time.sleep(0.1) を追加してください。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 には 1 件のエントリーが含まれ、各エントリーには 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 リスト(関連する全化合物にまたがるデータベースレコードを 1 件ずつ格納)に加えて、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:すべてのデータソースを一覧表示

ID、名前、説明、ウェブサイト URL を含む UniChem の全データソース一覧を取得します。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 で受け付けられるのは、1 リクエストにつき 1 つの化合物のみです。{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 ごとに 1 回の 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 International Chemical Identifier)と、そのハッシュ形式である InChIKey を使用します。InChIKey は 27 文字の文字列で、3 つのブロックに分かれています。最初の 14 文字は結合層(重原子と結合)を表し、次の 8 文字は立体化学と電荷を表し、最後の文字はバージョンフラグです。UniChem は標準 InChIKey が一致することを条件に化合物を相互参照し、データベース間で同じ化学種を確実に特定します。

ソース ID 参照表(/sources とライブ照合済み)
ソース IDデータベース対象範囲
1ChEMBL生物活性分子、創薬
2DrugBank承認薬、薬理学
3RCSB PDB結晶構造中のリガンド(米国)
4Guide to Pharmacology薬理学の標的/リガンド
5PDBe結晶構造中のリガンド(欧州)
7ChEBI化学オントロジー、代謝物
14FDA SRSFDA Substance Registration System
15SureChEMBL特許化学
18HMDBHuman Metabolome Database
22PubChem一般的な化合物リポジトリ
31BindingDB結合親和性データ
32CompTox環境毒性ダッシュボード
33LIPID MAPS脂質構造
34DrugCentral承認薬と薬理学
37BRENDA酵素基質/生成物
38Rhea生化学反応
41SwissLipids脂質構造
49Probes-and-Drugs化学プローブ
フィールド名:id と sourceID

UniChem のスクリプト作成で最もよくある間違いです。2 つのエンドポイントでは、ソースデータベース識別子に異なるフィールド名を使用します。

  • GET /sources は、sourceID フィールド(ID の I が大文字)のオブジェクトとしてデータベースを一覧表示します。
  • POST /compounds と POST /connectivity は、データベースごとのヒットを sources リスト内に返します。この場合、ソース識別子は通常の id フィールドです。これらのヒットレコードには sourceID または sourceId キーはありません。

化合物または接続性のレスポンスを反復処理する場合は常に src["id"] を使用し、/sources カタログを反復処理する場合は s["sourceID"] を使用してください。

接続性と標準 InChIKey の照合

POST /compounds は InChIKey が完全一致するもの(立体化学、塩、同位体が同一)を返します。POST /connectivity は結合トポロジーを共有するすべての化合物を返します。これは、ラセミ体、立体異性体、遊離酸/塩基、共結晶の相手を見つけるのに役立ちます。接続性のレスポンスには各ヒットの比較 dict が含まれ、一致した 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, sourceIDcompound 内の識別子の種類を選択します。inchikey には標準 InChIKey(27 文字)が必要です。sourceID の場合は、数値の sourceID フィールドも必要です。
compound(本文)POST /compounds, POST /connectivity-文字列 1 つ(リスト不可)検索する識別子です。UniChem はリストを使ったバッチ形式をサポートしていないため、化合物ごとに POST を 1 回実行します。
sourceID(本文)POST /compounds(type=sourceID の場合)-/sources の整数 DB ID入力した化合物が属するソースデータベースです(1=ChEMBL、2=DrugBank、22=PubChem、...)。
ヒットごとの id(レスポンス)POST /compounds, POST /connectivity-整数sources 内の各ヒットのソースデータベース ID です。ヒットごとのこれらのレコードでは、src["sourceID"] ではなく src["id"] を使用してください。
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 を使用する(非標準のものは使用しない):UniChem は標準 InChIKey をインデックス化しています。非標準の InChIKey では結果が返されません。次の方法で確認できます:from rdkit.Chem.inchi import MolToInchiKey; MolToInchiKey(mol)。

  4. 完全一致しない場合はコネクティビティ検索に切り替える:化合物が DrugBank では塩(塩酸塩など)として登録されていて、手元にあるのが遊離塩基の InChIKey の場合、標準検索では見つかりません。医薬品の相互参照を行う際は、フォールバックとしてコネクティビティ検索を実行してください。

  5. 完全な InChIKey を /connectivity に渡す:このエンドポイントには、標準 InChIKey 全体(27 文字)が必要です。コネクティビティ部分の 14 文字だけを送信すると、{response: Not found} が返されます。UniChem は内部で立体化学/電荷レイヤーを取り除きます。

  6. 起動時にソースリストをキャッシュする:/sources を 1 回呼び出し、ID をハードコーディングするのではなく、{sourceID: nameLabel} の辞書を作成します。

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 が返されます。呼び出しの間に time.sleep(0.1) を挟んで、POST を繰り返します(約 10 リクエスト/秒)。

よく使うレシピ

レシピ:任意の化合物 ID を InChIKey に変換する

使用する場面:PubChem CID または ChEBI ID があり、UniChem やその他のサービスで検索するための InChIKey が必要な場合です。

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
レシピ:化合物が承認済み医薬品かどうかを確認する

使用する場面:InChIKey を使って、化合物が 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}")
レシピ:化合物セットのソース網羅状況を要約する

使用する場面:化合物リストをどのデータベースがカバーしているかを監査する場合です。後続の分析に使用するデータベースを選ぶ前に役立ちます。

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 の sources リストでは 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、薬物相互作用の詳細データを取得します

参考資料

コメント