chembl-database-bioactivity · diff
git:20260428.81ea2d2 to git:20260522.6a6fd41
401 added, 273 removed. Audit A to A.
---
name: chembl-database-bioactivity
- description: Query ChEMBL via Python SDK. Search compounds by structure/properties, retrieve bioactivity (IC50, Ki, EC50), find target inhibitors, run SAR, access drug mechanism/indication data.
+ description: Query ChEMBL (2M+ compounds, 19M+ bioactivity measurements, 13K+ targets) via the public REST/JSON API with plain `requests` — no SDK install required. Search compounds, retrieve IC50/Ki/EC50 bioactivities, find target inhibitors, run SAR, access drug mechanism/indication data.
license: CC-BY-SA-3.0
---
# ChEMBL Database — Bioactivity Queries
+ > **Why no SDK?** The `chembl_webresource_client` package is convenient sugar over a public, no-auth REST/JSON API at `https://www.ebi.ac.uk/chembl/api/data/`. When the SDK is unavailable, every operation can be reproduced with plain `requests` and URL parameters. This SKILL.md uses the REST path throughout so the code runs in any environment with `requests` installed. Django-style filter syntax (`field__icontains=…`, `field__lte=…`, `field__range=a,b`) works as URL query parameters.
+
## Overview
- Query the ChEMBL bioactive molecule database (2M+ compounds, 19M+ bioactivity measurements, 13K+ targets) using the `chembl_webresource_client` Python SDK. Covers compound search, target lookup, bioactivity retrieval, structure-based search, and drug information access.
+ ChEMBL is EMBL-EBI's bioactive molecule database: 2M+ compounds, 19M+ bioactivity measurements (IC50, Ki, EC50, Kd, …), 13K+ targets. The REST API at `https://www.ebi.ac.uk/chembl/api/data/` returns JSON (append `.json`) or XML/YAML, requires no authentication, and supports Django-style query filters via URL parameters plus cursor-style pagination via `page_meta.next`.
## When to Use
- Finding compounds by name, ChEMBL ID, or physicochemical properties
- Querying bioactivity data (IC50, Ki, EC50) for specific targets
- Performing similarity or substructure searches using SMILES
- Retrieving drug mechanisms of action and clinical indications
- Identifying inhibitors, agonists, or bioactive molecules for a target
- Analyzing structure-activity relationships (SAR) across compound series
- Filtering molecules by Lipinski rule-of-5 or other drug-likeness criteria
- - For general cheminformatics (SMILES manipulation, fingerprints, descriptors) use rdkit-cheminformatics instead
+ - For general cheminformatics (SMILES manipulation, fingerprints, descriptors) use `rdkit-cheminformatics` instead
+ - For an alternative compound database (NIH, broader coverage) use `pubchem-compound-search`
## Prerequisites
+ - **Python packages**: `requests` (only requirement). Optional: `pandas` for tabular analysis.
+ - **No API key required**: ChEMBL is freely accessible.
+ - **Rate limits**: No published hard limit. The infrastructure is shared — add `time.sleep(0.2-0.5)` between requests in batch loops; back off on HTTP 429.
+
```bash
- uv pip install chembl_webresource_client
- # Optional: pandas for tabular analysis
- uv pip install pandas
+ pip install requests
+ # Optional, for DataFrame work:
+ pip install pandas
```
- **Rate limiting**: The SDK handles rate limiting internally via automatic retries and caching. No `time.sleep()` needed between queries. For large-scale data retrieval (100K+ records), consider ChEMBL bulk downloads instead of API queries.
-
## Quick Start
```python
- from chembl_webresource_client.new_client import new_client
+ import requests
- # Each entity type has its own client endpoint
- molecule = new_client.molecule
- target = new_client.target
- activity = new_client.activity
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
# Retrieve a molecule by ChEMBL ID
- aspirin = molecule.get('CHEMBL25')
+ r = requests.get(f"{BASE}/molecule/CHEMBL25.json", timeout=15)
+ r.raise_for_status()
+ aspirin = r.json()
print(f"{aspirin['pref_name']}: MW={aspirin['molecule_properties']['mw_freebase']}")
+ # ASPIRIN: MW=180.16
- # Search targets by name
- egfr_targets = target.filter(pref_name__icontains='EGFR', target_type='SINGLE PROTEIN')
- print(f"Found {len(list(egfr_targets))} EGFR-related targets")
+ # Search targets by full name (acronyms like 'EGFR' don't match pref_name — use full term)
+ r = requests.get(
+ f"{BASE}/target.json",
+ params={"pref_name__icontains": "epidermal growth factor receptor",
+ "target_type": "SINGLE PROTEIN", "limit": 5},
+ timeout=15,
+ )
+ targets = r.json()["targets"]
+ print(f"EGFR-like targets: {len(targets)}, first={targets[0]['target_chembl_id']}")
- # Query bioactivities with filters
- potent = activity.filter(
- target_chembl_id='CHEMBL203', # EGFR
- standard_type='IC50',
- standard_value__lte=100, # <= 100 nM
- standard_units='nM'
+ # Potent bioactivities: EGFR (CHEMBL203) IC50 <= 100 nM
+ r = requests.get(
+ f"{BASE}/activity.json",
+ params={"target_chembl_id": "CHEMBL203",
+ "standard_type": "IC50",
+ "standard_value__lte": 100,
+ "standard_units": "nM",
+ "limit": 5},
+ timeout=30,
)
+ data = r.json()
+ print(f"EGFR IC50 ≤ 100 nM records: {data['page_meta']['total_count']}")
```
## Key Concepts
- ### Filter Operators
+ ### Filter Operators (Django-style, as URL parameters)
- ChEMBL uses Django-style query filters on all endpoints:
+ The SDK's `field__operator=value` syntax maps 1:1 to URL query parameters. Use `&` to combine filters.
- | Operator | Meaning | Example |
- |----------|---------|---------|
- | `__exact` | Exact match (default) | `target_type__exact='SINGLE PROTEIN'` |
- | `__iexact` | Case-insensitive exact | `pref_name__iexact='aspirin'` |
- | `__contains` | Substring match | `pref_name__contains='kinase'` |
- | `__icontains` | Case-insensitive substring | `pref_name__icontains='egfr'` |
- | `__startswith` | Prefix match | `pref_name__startswith='Epi'` |
- | `__endswith` | Suffix match | `pref_name__endswith='nib'` |
- | `__gt` / `__gte` | Greater than (or equal) | `standard_value__gte=10` |
- | `__lt` / `__lte` | Less than (or equal) | `standard_value__lte=100` |
- | `__range` | Value in range | `mw_freebase__range=[300, 500]` |
- | `__in` | Value in list | `target_chembl_id__in=['CHEMBL203', 'CHEMBL240']` |
- | `__isnull` | Null check | `pchembl_value__isnull=False` |
- | `__regex` | Regular expression | `pref_name__regex='^EGF.*kinase$'` |
- | `__search` | Full-text search | `description__search='apoptosis'` |
+ | Operator | URL pattern | Example URL fragment |
+ |----------|-------------|----------------------|
+ | `__exact` | `field=value` | `target_type=SINGLE+PROTEIN` |
+ | `__iexact` | `field__iexact=value` | `pref_name__iexact=aspirin` |
+ | `__contains` / `__icontains` | `field__icontains=value` | `pref_name__icontains=kinase` |
+ | `__startswith` / `__endswith` | `field__startswith=Epi` | `pref_name__endswith=nib` |
+ | `__gt` / `__gte` / `__lt` / `__lte` | `field__lte=100` | `standard_value__lte=100` |
+ | `__range` | `field__range=lo,hi` | `molecule_properties__mw_freebase__range=300,500` |
+ | `__in` | `field__in=a,b,c` | `standard_type__in=IC50,Ki,Kd` |
+ | `__isnull` | `field__isnull=False` (Python `False`/`True` strings) | `pchembl_value__isnull=False` |
+ | `__regex` | `field__regex=…` | `pref_name__regex=^EGF.*kinase$` |
+ | `__search` | `field__search=…` | `description__search=apoptosis` |
+ When passed via `requests.get(..., params={...})`, the library handles URL encoding automatically (including the commas in `__range` and `__in`).
+
### Core Endpoints
- | Endpoint | Access | Description |
- |----------|--------|-------------|
- | `molecule` | `new_client.molecule` | Compound structures, properties, synonyms |
- | `target` | `new_client.target` | Protein and non-protein biological targets |
- | `activity` | `new_client.activity` | Bioassay measurement results |
- | `assay` | `new_client.assay` | Experimental assay details |
- | `drug` | `new_client.drug` | Approved pharmaceutical information |
- | `mechanism` | `new_client.mechanism` | Drug mechanism of action data |
- | `drug_indication` | `new_client.drug_indication` | Drug therapeutic indications |
- | `similarity` | `new_client.similarity` | Tanimoto similarity search |
- | `substructure` | `new_client.substructure` | Substructure search |
- | `image` | `new_client.image` | SVG molecular structure images |
- | `molecule_form` | `new_client.molecule_form` | Parent/salt forms |
- | `protein_class` | `new_client.protein_class` | Protein classification hierarchy |
- | `target_component` | `new_client.target_component` | Target component details |
- | `cell_line` | `new_client.cell_line` | Cell line information |
- | `tissue` | `new_client.tissue` | Tissue type information |
- | `compound_structural_alert` | `new_client.compound_structural_alert` | Structural alerts for toxicity |
- | `document` | `new_client.document` | Literature source references |
+ All endpoints accept `.json`, `.xml`, or `.yaml` suffix. JSON is the default below.
+ | Endpoint URL | Returns | Key fields |
+ |--------------|---------|------------|
+ | `/molecule/{chembl_id}.json` | Compound by ID | `pref_name`, `molecule_chembl_id`, `molecule_properties`, `molecule_structures` |
+ | `/molecule.json?<filters>` | Compound search | paginated `molecules[]` |
+ | `/target/{chembl_id}.json` | Target by ID | `pref_name`, `target_type`, `organism`, `target_components` |
+ | `/target.json?<filters>` | Target search | paginated `targets[]` |
+ | `/activity.json?<filters>` | Bioactivity records | paginated `activities[]` |
+ | `/assay.json?<filters>` | Assay details | paginated `assays[]` |
+ | `/drug.json?<filters>` | Approved drug info | paginated `drugs[]`; supports `/drug/{chembl_id}.json` |
+ | `/mechanism.json?<filters>` | Mechanism of action | paginated `mechanisms[]` |
+ | `/drug_indication.json?<filters>` | Therapeutic indications | paginated `drug_indications[]` |
+ | `/similarity/{smiles}/{tanimoto}.json` | Tanimoto similarity (0–100) | paginated `molecules[]` with `similarity` field |
+ | `/substructure/{smiles}.json` | Substructure search | paginated `molecules[]` |
+ | `/image/{chembl_id}.svg` | SVG structure image | binary SVG (NOT JSON) |
+ | `/molecule_form/{chembl_id}.json` | Parent/salt forms | `molecule_forms[]` |
+ | `/protein_class.json` | Protein classification hierarchy | hierarchical browse |
+ | `/document.json?<filters>` | Literature source records | paginated `documents[]` |
+
+ ### Response Shape
+
+ ```jsonc
+ {
+ "page_meta": {
+ "limit": 20,
+ "offset": 0,
+ "total_count": 12145,
+ "next": "/chembl/api/data/activity.json?...&offset=20",
+ "previous": null
+ },
+ "activities": [ /* or molecules[], targets[], etc. */ ]
+ }
+ ```
+
+ Walk `page_meta.next` (a relative URL — prefix with `https://www.ebi.ac.uk`) until it becomes `null`.
+
### Molecular Properties
- Properties accessible via `molecule['molecule_properties']`:
+ Properties accessible via `molecule_properties` on each record:
| Field | Description |
|-------|-------------|
| `mw_freebase` | Molecular weight (free base) |
| `full_mwt` | Full molecular weight (including salts) |
| `alogp` | Calculated LogP |
| `hba` | Hydrogen bond acceptors |
| `hbd` | Hydrogen bond donors |
| `psa` | Polar surface area |
| `rtb` | Rotatable bonds |
| `num_ro5_violations` | Lipinski rule-of-5 violations |
| `ro3_pass` | Rule of 3 compliance |
- | `cx_most_apka` | Most acidic pKa |
- | `cx_most_bpka` | Most basic pKa |
+ | `cx_most_apka` / `cx_most_bpka` | Most acidic / basic pKa |
### Target Information Fields
- Key fields in target records:
-
| Field | Description |
|-------|-------------|
| `target_chembl_id` | ChEMBL target identifier |
- | `pref_name` | Preferred target name |
- | `target_type` | Type: SINGLE PROTEIN, PROTEIN COMPLEX, ORGANISM |
- | `organism` | Target organism (e.g., Homo sapiens) |
+ | `pref_name` | Preferred (full) target name — acronyms like "EGFR" do NOT match; use the spelled-out term |
+ | `target_type` | `SINGLE PROTEIN`, `PROTEIN COMPLEX`, `ORGANISM`, … |
+ | `organism` | Target organism (e.g., `Homo sapiens`) |
| `tax_id` | NCBI taxonomy ID |
- | `target_components` | Component details (UniProt accession, etc.) |
+ | `target_components[]` | Components (UniProt accession, sequence, …) |
### Bioactivity Data Fields
- Key fields in activity records:
-
| Field | Description |
|-------|-------------|
- | `standard_type` | Activity type: IC50, Ki, Kd, EC50, etc. |
+ | `standard_type` | Activity type: `IC50`, `Ki`, `Kd`, `EC50`, … |
| `standard_value` | Numerical activity value |
- | `standard_units` | Units: nM, uM, etc. |
- | `pchembl_value` | Normalized activity (-log10 scale, comparable across types) |
+ | `standard_units` | Units: `nM`, `uM`, … |
+ | `pchembl_value` | Normalized -log10 activity (>6 = potent) |
| `activity_comment` | Activity annotations |
| `data_validity_comment` | Data quality flags (check before analysis) |
| `potential_duplicate` | Duplicate flag |
## Core API
### 1. Molecule Queries
```python
- molecule = new_client.molecule
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
# By ChEMBL ID
- aspirin = molecule.get('CHEMBL25')
-
- # By name (case-insensitive)
- results = molecule.filter(pref_name__icontains='imatinib')
+ r = requests.get(f"{BASE}/molecule/CHEMBL25.json", timeout=15)
+ aspirin = r.json()
+ print(f"{aspirin['pref_name']}: MW={aspirin['molecule_properties']['mw_freebase']}")
- # By properties (Lipinski rule-of-5 compliant)
- drug_like = molecule.filter(
- molecule_properties__mw_freebase__lte=500,
- molecule_properties__alogp__lte=5,
- molecule_properties__hba__lte=10,
- molecule_properties__hbd__lte=5
- )
+ # By name (case-insensitive substring)
+ r = requests.get(f"{BASE}/molecule.json",
+ params={"pref_name__icontains": "imatinib", "limit": 5},
+ timeout=15)
+ for mol in r.json()["molecules"]:
+ print(f" {mol['molecule_chembl_id']} {mol.get('pref_name')!r}")
- # By property range
- mid_weight = molecule.filter(
- molecule_properties__mw_freebase__range=[300, 500]
- )
+ # By Lipinski-compliant property ranges
+ r = requests.get(f"{BASE}/molecule.json",
+ params={"molecule_properties__mw_freebase__range": "300,500",
+ "molecule_properties__alogp__lte": 5,
+ "molecule_properties__hba__lte": 10,
+ "molecule_properties__hbd__lte": 5,
+ "limit": 3},
+ timeout=15)
+ print(f"Lipinski-compliant total: {r.json()['page_meta']['total_count']}")
```
### 2. Target Queries
```python
- target = new_client.target
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
# By ChEMBL ID
- egfr = target.get('CHEMBL203')
- print(f"{egfr['pref_name']} ({egfr['organism']})")
+ r = requests.get(f"{BASE}/target/CHEMBL203.json", timeout=15)
+ egfr = r.json()
+ print(f"{egfr['pref_name']} ({egfr['organism']}) — type={egfr['target_type']}")
- # Search by name and type
- kinases = target.filter(
- target_type='SINGLE PROTEIN',
- pref_name__icontains='kinase'
- )
+ # Search by full name (NOT acronym) + type
+ r = requests.get(f"{BASE}/target.json",
+ params={"pref_name__icontains": "kinase",
+ "target_type": "SINGLE PROTEIN", "limit": 5},
+ timeout=15)
+ d = r.json()
+ print(f"Kinase SINGLE_PROTEIN targets: total={d['page_meta']['total_count']}")
+ for t in d["targets"][:5]:
+ print(f" {t['target_chembl_id']:12s} {t.get('pref_name')!r} ({t['organism']})")
# By organism
- human_targets = target.filter(organism='Homo sapiens')
+ r = requests.get(f"{BASE}/target.json",
+ params={"organism": "Homo sapiens", "limit": 3},
+ timeout=15)
+ print(f"Human targets: total={r.json()['page_meta']['total_count']}")
```
### 3. Bioactivity Data
```python
- activity = new_client.activity
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- # Potent inhibitors for a target
- potent = activity.filter(
- target_chembl_id='CHEMBL203',
- standard_type='IC50',
- standard_value__lte=100,
- standard_units='nM'
- )
+ # Potent inhibitors for a target (EGFR = CHEMBL203)
+ r = requests.get(f"{BASE}/activity.json",
+ params={"target_chembl_id": "CHEMBL203",
+ "standard_type": "IC50",
+ "standard_value__lte": 100,
+ "standard_units": "nM",
+ "limit": 5},
+ timeout=30)
+ data = r.json()
+ print(f"EGFR IC50≤100nM: total={data['page_meta']['total_count']}")
+ for act in data["activities"][:5]:
+ print(f" {act['molecule_chembl_id']:14s} IC50={act['standard_value']} nM "
+ f"pChEMBL={act.get('pchembl_value')}")
- # All activities for a compound (with pChEMBL values)
- compound_acts = activity.filter(
- molecule_chembl_id='CHEMBL25',
- pchembl_value__isnull=False
- )
+ # All pChEMBL-tagged activities for a compound
+ r = requests.get(f"{BASE}/activity.json",
+ params={"molecule_chembl_id": "CHEMBL25",
+ "pchembl_value__isnull": "False",
+ "limit": 5},
+ timeout=30)
+ print(f"Aspirin pChEMBL activities: total={r.json()['page_meta']['total_count']}")
- # Multiple activity types
- ki_data = activity.filter(
- target_chembl_id='CHEMBL240',
- standard_type__in=['IC50', 'Ki', 'Kd']
- )
+ # Multiple activity types (CHEMBL240 = D2 dopamine receptor)
+ r = requests.get(f"{BASE}/activity.json",
+ params={"target_chembl_id": "CHEMBL240",
+ "standard_type__in": "IC50,Ki,Kd",
+ "limit": 5},
+ timeout=30)
+ print(f"D2 receptor IC50/Ki/Kd: total={r.json()['page_meta']['total_count']}")
```
### 4. Structure-Based Search
```python
- # Similarity search (Tanimoto)
- similarity = new_client.similarity
- similar = similarity.filter(
- smiles='CC(=O)Oc1ccccc1C(=O)O', # aspirin
- similarity=85 # >=85% similarity
- )
+ import requests
+ from urllib.parse import quote
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
+ # Similarity search (Tanimoto ≥ 85%)
+ # Path-style endpoint: /similarity/{smiles}/{threshold}
+ # The SMILES MUST be URL-encoded (it contains '/', '(', ')' etc.)
+ aspirin_smiles = quote("CC(=O)Oc1ccccc1C(=O)O", safe="")
+ r = requests.get(f"{BASE}/similarity/{aspirin_smiles}/85.json",
+ params={"limit": 5}, timeout=30)
+ data = r.json()
+ print(f"Similar to aspirin (≥85% Tanimoto): total={data['page_meta']['total_count']}")
+ for m in data["molecules"][:5]:
+ print(f" {m['molecule_chembl_id']} similarity={m.get('similarity')}")
+ ```
+
+ ```python
# Substructure search
- substructure = new_client.substructure
- benzimidazoles = substructure.filter(smiles='c1ccc2[nH]cnc2c1')
+ benzimidazole = quote("c1ccc2[nH]cnc2c1", safe="")
+ r = requests.get(f"{BASE}/substructure/{benzimidazole}.json",
+ params={"limit": 3}, timeout=30)
+ print(f"Benzimidazole substructure total: {r.json()['page_meta']['total_count']}")
```
### 5. Drug and Mechanism Data
```python
- drug = new_client.drug
- mechanism = new_client.mechanism
- drug_indication = new_client.drug_indication
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- # Drug details
- drug_info = drug.get('CHEMBL25')
+ # Drug record (max clinical phase, ATC class, etc.)
+ r = requests.get(f"{BASE}/drug/CHEMBL941.json", timeout=15) # imatinib
+ drug = r.json()
+ print(f"Imatinib max_phase={drug.get('max_phase')}")
- # Mechanisms of action
- mechs = mechanism.filter(molecule_chembl_id='CHEMBL941')
- for m in mechs:
- print(f"{m['mechanism_of_action']} → {m.get('target_chembl_id')}")
+ # Mechanisms of action — note: not every drug has mechanism records.
+ # Imatinib (CHEMBL941) returns 0 mechanism rows; sunitinib (CHEMBL535) has many.
+ r = requests.get(f"{BASE}/mechanism.json",
+ params={"molecule_chembl_id": "CHEMBL535"}, timeout=15)
+ for m in r.json()["mechanisms"]:
+ print(f" {m['mechanism_of_action']} → target {m.get('target_chembl_id')}")
# Therapeutic indications
- indications = drug_indication.filter(molecule_chembl_id='CHEMBL941')
- for ind in indications:
- print(f"{ind.get('mesh_heading')} (Phase {ind.get('max_phase_for_ind')})")
+ r = requests.get(f"{BASE}/drug_indication.json",
+ params={"molecule_chembl_id": "CHEMBL941", "limit": 5},
+ timeout=15)
+ for ind in r.json()["drug_indications"]:
+ print(f" {ind.get('mesh_heading')!r} max_phase_for_ind={ind.get('max_phase_for_ind')}")
+ ```
- # SVG molecular image
- image = new_client.image
- svg_data = image.get('CHEMBL25')
- with open('aspirin.svg', 'w') as f:
- f.write(svg_data)
+ ```python
+ # SVG molecular structure image — direct binary response, NOT JSON
+ # Do NOT call /image/{cid}.json — that endpoint raises JSONDecodeError.
+ r = requests.get(f"{BASE}/image/CHEMBL25.svg", timeout=15)
+ r.raise_for_status()
+ with open("aspirin.svg", "w") as f:
+ f.write(r.text)
+ print(f"Saved aspirin.svg ({len(r.text)} bytes, looks_svg={'<svg' in r.text})")
```
## Common Workflows
### Workflow 1: Find Inhibitors for a Target
+ **Note:** `pref_name__icontains` matches the spelled-out name. Acronyms like `'EGFR'` or `'BRAF'` return 0 results — use `'epidermal growth factor receptor'` or `'B-raf'` (with the hyphen).
+
```python
- from chembl_webresource_client.new_client import new_client
- import pandas as pd
+ import requests, pandas as pd, time
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- # Step 1: Identify the target
- targets = new_client.target.filter(pref_name__icontains='BRAF', target_type='SINGLE PROTEIN')
- target_id = list(targets)[0]['target_chembl_id']
+ # Step 1: Resolve the target by full name
+ r = requests.get(f"{BASE}/target.json",
+ params={"pref_name__icontains": "B-raf",
+ "target_type": "SINGLE PROTEIN", "limit": 5},
+ timeout=15)
+ targets = r.json()["targets"]
+ human_braf = next(t for t in targets if t["organism"] == "Homo sapiens")
+ target_id = human_braf["target_chembl_id"]
+ print(f"Using {target_id} — {human_braf['pref_name']}")
- # Step 2: Query potent activities
- activities = new_client.activity.filter(
- target_chembl_id=target_id,
- standard_type='IC50',
- standard_value__lte=100,
- standard_units='nM',
- pchembl_value__isnull=False
- )
+ # Step 2: Paginate all potent IC50 activities (cap at 500 for demo)
+ url = (f"{BASE}/activity.json"
+ f"?target_chembl_id={target_id}"
+ f"&standard_type=IC50"
+ f"&standard_value__lte=100"
+ f"&standard_units=nM"
+ f"&pchembl_value__isnull=False"
+ f"&limit=200")
+ records = []
+ while url and len(records) < 500:
+ r = requests.get(url, timeout=30)
+ r.raise_for_status()
+ data = r.json()
+ records.extend(data["activities"])
+ nxt = data["page_meta"].get("next")
+ url = f"https://www.ebi.ac.uk{nxt}" if nxt else None
+ time.sleep(0.2)
- # Step 3: Convert to DataFrame for analysis
- df = pd.DataFrame(list(activities))
- df['standard_value'] = pd.to_numeric(df['standard_value'])
- print(f"Found {len(df)} potent compounds")
- print(df[['molecule_chembl_id', 'standard_value', 'pchembl_value']].head(10))
+ df = pd.DataFrame(records)
+ df["standard_value"] = pd.to_numeric(df["standard_value"])
+ print(f"Retrieved {len(df)} potent {target_id} compounds")
+ print(df[["molecule_chembl_id", "standard_value", "pchembl_value"]].head(10))
```
### Workflow 2: Analyze a Known Drug
```python
- from chembl_webresource_client.new_client import new_client
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- chembl_id = 'CHEMBL941' # Imatinib
+ # Sunitinib (CHEMBL535) — has documented mechanisms + indications.
+ # Imatinib (CHEMBL941) sometimes returns 0 mechanism rows depending on ChEMBL release.
+ chembl_id = "CHEMBL535"
- # Drug information
- drug_info = new_client.molecule.get(chembl_id)
- print(f"Name: {drug_info['pref_name']}")
- print(f"MW: {drug_info['molecule_properties']['mw_freebase']}")
+ # Molecule record
+ m = requests.get(f"{BASE}/molecule/{chembl_id}.json", timeout=15).json()
+ print(f"Name: {m['pref_name']}")
+ print(f"MW : {m['molecule_properties']['mw_freebase']}")
- # Mechanisms of action
- mechs = list(new_client.mechanism.filter(molecule_chembl_id=chembl_id))
- for m in mechs:
- print(f"Mechanism: {m['mechanism_of_action']}")
+ # Mechanisms
+ mechs = requests.get(f"{BASE}/mechanism.json",
+ params={"molecule_chembl_id": chembl_id},
+ timeout=15).json()["mechanisms"]
+ for mc in mechs:
+ print(f" Mechanism: {mc['mechanism_of_action']}")
# Indications
- indications = list(new_client.drug_indication.filter(molecule_chembl_id=chembl_id))
- for ind in indications:
- print(f"Indication: {ind.get('mesh_heading')} (Phase {ind.get('max_phase_for_ind')})")
+ inds = requests.get(f"{BASE}/drug_indication.json",
+ params={"molecule_chembl_id": chembl_id, "limit": 5},
+ timeout=15).json()["drug_indications"]
+ for ind in inds:
+ print(f" Indication: {ind.get('mesh_heading')} "
+ f"(Phase {ind.get('max_phase_for_ind')})")
- # All bioactivity data
- activities = list(new_client.activity.filter(
- molecule_chembl_id=chembl_id, pchembl_value__isnull=False
- ))
- print(f"Total bioactivity records: {len(activities)}")
+ # Bioactivity record count
+ total = requests.get(f"{BASE}/activity.json",
+ params={"molecule_chembl_id": chembl_id,
+ "pchembl_value__isnull": "False",
+ "limit": 1},
+ timeout=30).json()["page_meta"]["total_count"]
+ print(f"Total bioactivity records (pChEMBL-tagged): {total}")
```
### Workflow 3: SAR Study
```python
- from chembl_webresource_client.new_client import new_client
- import pandas as pd
+ import requests, pandas as pd, time
+ from urllib.parse import quote
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- # Step 1: Find similar compounds to lead
- similar = new_client.similarity.filter(
- smiles='c1ccc2c(c1)cc(nc2N)c3ccc(cc3)NC(=O)c4ccccc4', # lead compound
- similarity=80
- )
- analogs = list(similar)
+ # Step 1: Similar compounds to a lead (e.g., quinoline scaffold)
+ lead_smiles = "c1ccc2c(c1)cc(nc2N)c3ccc(cc3)NC(=O)c4ccccc4"
+ r = requests.get(f"{BASE}/similarity/{quote(lead_smiles, safe='')}/80.json",
+ params={"limit": 20}, timeout=30)
+ analogs = r.json()["molecules"]
+ print(f"Analogs found: {len(analogs)}")
- # Step 2: Collect activities for each analog
+ # Step 2: Collect bioactivities for each analog
records = []
- for compound in analogs[:20]: # limit for demo
- cid = compound['molecule_chembl_id']
- acts = list(new_client.activity.filter(
- molecule_chembl_id=cid,
- standard_type='IC50',
- pchembl_value__isnull=False
- ))
+ for compound in analogs[:20]:
+ cid = compound["molecule_chembl_id"]
+ acts = requests.get(f"{BASE}/activity.json",
+ params={"molecule_chembl_id": cid,
+ "standard_type": "IC50",
+ "pchembl_value__isnull": "False",
+ "limit": 20},
+ timeout=30).json()["activities"]
for act in acts:
records.append({
- 'chembl_id': cid,
- 'target': act.get('target_pref_name'),
- 'IC50_nM': act.get('standard_value'),
- 'pchembl': act.get('pchembl_value'),
- 'mw': compound.get('molecule_properties', {}).get('mw_freebase'),
- 'alogp': compound.get('molecule_properties', {}).get('alogp')
+ "chembl_id": cid,
+ "target": act.get("target_pref_name"),
+ "IC50_nM": act.get("standard_value"),
+ "pchembl": act.get("pchembl_value"),
+ "mw": (compound.get("molecule_properties") or {}).get("mw_freebase"),
+ "alogp": (compound.get("molecule_properties") or {}).get("alogp"),
})
+ time.sleep(0.2)
- # Step 3: Analyze property-activity relationships
df = pd.DataFrame(records)
if not df.empty:
- df['IC50_nM'] = pd.to_numeric(df['IC50_nM'])
- print(df.groupby('target')['IC50_nM'].describe())
+ df["IC50_nM"] = pd.to_numeric(df["IC50_nM"])
+ print(df.groupby("target")["IC50_nM"].describe())
```
## Common Recipes
- ### Recipe: Virtual Screening Filter (Lipinski + Activity)
+ ### Recipe: Virtual Screening Filter (Lipinski rule-of-5)
```python
- from chembl_webresource_client.new_client import new_client
-
- candidates = new_client.molecule.filter(
- molecule_properties__mw_freebase__range=[300, 500],
- molecule_properties__alogp__lte=5,
- molecule_properties__hba__lte=10,
- molecule_properties__hbd__lte=5,
- molecule_properties__num_ro5_violations=0
- )
- print(f"Drug-like candidates: {len(list(candidates))}")
+ import requests
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
+ r = requests.get(f"{BASE}/molecule.json",
+ params={"molecule_properties__mw_freebase__range": "300,500",
+ "molecule_properties__alogp__lte": 5,
+ "molecule_properties__hba__lte": 10,
+ "molecule_properties__hbd__lte": 5,
+ "molecule_properties__num_ro5_violations": 0,
+ "limit": 1},
+ timeout=15)
+ print(f"Drug-like candidates: {r.json()['page_meta']['total_count']}")
```
- ### Recipe: Client Configuration
+ ### Recipe: Paginate Activities to CSV
```python
- from chembl_webresource_client.settings import Settings
+ import requests, pandas as pd, time
+ BASE = "https://www.ebi.ac.uk/chembl/api/data"
- Settings.Instance().CACHING = True # enable/disable cache
- Settings.Instance().CACHE_EXPIRE = 86400 # cache duration (seconds)
- Settings.Instance().TIMEOUT = 30 # request timeout (seconds)
- Settings.Instance().TOTAL_RETRIES = 3 # retry count on failure
+ url = (f"{BASE}/activity.json"
+ f"?target_chembl_id=CHEMBL203"
+ f"&standard_type=IC50"
+ f"&pchembl_value__isnull=False"
+ f"&limit=500")
+ all_acts = []
+ while url:
+ r = requests.get(url, timeout=60)
+ r.raise_for_status()
+ data = r.json()
+ all_acts.extend(data["activities"])
+ nxt = data["page_meta"].get("next")
+ url = f"https://www.ebi.ac.uk{nxt}" if nxt else None
+ time.sleep(0.3)
+
+ df = pd.DataFrame(all_acts)
+ df.to_csv("egfr_activities.csv", index=False)
+ print(f"Exported {len(df)} records → egfr_activities.csv")
```
- ### Recipe: Export Activities to CSV
+ ### Recipe: Robust Session with Retries
```python
- import pandas as pd
- from chembl_webresource_client.new_client import new_client
+ import requests
+ from requests.adapters import HTTPAdapter
+ from urllib3.util.retry import Retry
- activities = new_client.activity.filter(
- target_chembl_id='CHEMBL203',
- standard_type='IC50',
- pchembl_value__isnull=False
- )
- df = pd.DataFrame(list(activities))
- df.to_csv('egfr_activities.csv', index=False)
- print(f"Exported {len(df)} records")
+ def chembl_session(retries=3, backoff=1.0):
+ s = requests.Session()
+ s.headers.update({"Accept": "application/json"})
+ s.mount("https://", HTTPAdapter(max_retries=Retry(
+ total=retries, backoff_factor=backoff,
+ status_forcelist=[429, 500, 502, 503, 504],
+ allowed_methods=["GET"])))
+ return s
+
+ session = chembl_session()
+ r = session.get("https://www.ebi.ac.uk/chembl/api/data/molecule/CHEMBL25.json", timeout=15)
+ print(r.json()["pref_name"])
```
+ ### Recipe: Download SVG Structure Image
+
+ ```python
+ import requests
+ r = requests.get("https://www.ebi.ac.uk/chembl/api/data/image/CHEMBL25.svg", timeout=15)
+ r.raise_for_status()
+ with open("aspirin.svg", "w") as f:
+ f.write(r.text)
+ ```
+
## Key Parameters
| Parameter | Endpoint | Default | Description |
|-----------|----------|---------|-------------|
- | `similarity` | `similarity.filter()` | — | Tanimoto threshold (0-100), typically 70-90 |
- | `standard_type` | `activity.filter()` | — | Activity type: IC50, Ki, Kd, EC50 |
- | `standard_value__lte` | `activity.filter()` | — | Max activity value (nM) |
- | `pchembl_value` | `activity.filter()` | — | Normalized -log10 activity (>6 = potent) |
- | `target_type` | `target.filter()` | — | SINGLE PROTEIN, PROTEIN COMPLEX, ORGANISM |
- | `CACHING` | `Settings` | `True` | Enable HTTP response caching |
- | `CACHE_EXPIRE` | `Settings` | `86400` | Cache TTL in seconds |
- | `TIMEOUT` | `Settings` | `30` | HTTP request timeout in seconds |
- | `TOTAL_RETRIES` | `Settings` | `3` | Auto-retry count on failure |
+ | `limit` | all list endpoints | `20` | Page size; max 1000 |
+ | `offset` | all list endpoints | `0` | Pagination offset (or follow `page_meta.next`) |
+ | `format` | all endpoints | `json` (via `.json` suffix) | Also `.xml`, `.yaml` |
+ | `pref_name__icontains` | `/target`, `/molecule` | — | Substring on full name; **acronyms don't match**, use full term |
+ | `target_chembl_id` | `/activity` | — | E.g., `CHEMBL203` (EGFR), `CHEMBL240` (D2 receptor) |
+ | `molecule_chembl_id` | `/activity`, `/mechanism`, `/drug_indication` | — | E.g., `CHEMBL25` (aspirin) |
+ | `standard_type` | `/activity` | — | `IC50`, `Ki`, `Kd`, `EC50` |
+ | `standard_value__lte` | `/activity` | — | Max activity value (paired with `standard_units`) |
+ | `pchembl_value__isnull` | `/activity` | — | `"False"` to require pChEMBL-tagged data |
+ | `target_type` | `/target` | — | `SINGLE PROTEIN`, `PROTEIN COMPLEX`, `ORGANISM`, … |
+ | `{tanimoto}` (path) | `/similarity/{smiles}/{tanimoto}` | — | `0`–`100` Tanimoto threshold |
+ | `{smiles}` (path) | `/similarity`, `/substructure` | — | URL-encoded SMILES (`urllib.parse.quote(s, safe="")`) |
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
- | Empty results from filter | No matches or too strict filters | Relax filters; verify IDs exist with `.get()` first |
- | `KeyError` on molecule properties | Not all molecules have full property data | Use `.get('molecule_properties', {}).get('field')` |
- | Query returns unexpectedly few results | Lazy evaluation not consumed | Convert to `list()` before checking length |
- | Slow queries | Large result sets paginated automatically | Add more filters to narrow results; use `__range` |
- | `404` on `.get()` | Invalid ChEMBL ID | Verify ID format (e.g., CHEMBL25, not 25) |
- | Stale data | Aggressive caching | Set `Settings.Instance().CACHING = False` or clear cache |
- | Timeout errors | Server overload or large query | Increase `TIMEOUT`; split into smaller queries |
- | Mixed units in activity data | Different assays use different units | Filter by `standard_units='nM'` or use `pchembl_value` |
- | Duplicate activity records | Same measurement from different sources | Check `potential_duplicate` and `data_validity_comment` |
+ | `pref_name__icontains=EGFR` (or `BRAF`) returns 0 | ChEMBL stores spelled-out names; acronyms don't match | Use `"epidermal growth factor receptor"`; for BRAF use `"B-raf"` with the hyphen |
+ | `mechanism.json?molecule_chembl_id=CHEMBL941` returns empty | Not every drug has mechanism rows in every release (e.g., imatinib has 0 in current data) | Use `CHEMBL535` (sunitinib) or `CHEMBL192` (sildenafil) as known-populated examples |
+ | `JSONDecodeError` on `/image/{cid}.json` | The image endpoint is binary, not JSON | Always use `.svg` or `.png` suffix: `/image/{cid}.svg` |
+ | 404 on `/molecule/{id}` | Invalid ChEMBL ID format | IDs must include the prefix: `CHEMBL25`, not `25` |
+ | 400 on similarity search | Unencoded SMILES (`/` collides with URL path) | URL-encode: `urllib.parse.quote(smiles, safe="")` |
+ | Empty `next` page but `total_count` higher | Reached internal limit (typically 10000 with `offset` pagination) | Narrow filters (date range, target class) and re-paginate; or use the ChEMBL FTP downloads for >100K records |
+ | `HTTP 429 Too Many Requests` | Burst pace | Add `time.sleep(0.3)`; mount a `Retry` adapter (see Recipe) |
+ | Mixed units in `activity` records | Different assays report in nM / µM / % inhibition | Filter `standard_units="nM"` and prefer `pchembl_value` for cross-assay comparison |
+ | `data_validity_comment` is non-empty | Curation flag (e.g., "Potential transcription error", "Outside typical range") | Drop these rows before SAR/regression analysis |
+ | Duplicate activity records | Same measurement reported in multiple sources | Check `potential_duplicate=True` and dedupe |
## Best Practices
- - **Use `pchembl_value`** for cross-study comparisons — it normalizes IC50/Ki/EC50 to a comparable -log10 scale
- - **Always check `data_validity_comment`** before using activity values — flags data quality issues
- - **Filter by `standard_units`** to ensure consistent units across results
- - **Pagination is automatic**: the SDK handles pagination transparently — iterate directly over query results without manual page handling. Convert to `list()` only when you need all results in memory
- - **Use lazy evaluation**: queries execute only when iterated — convert to `list()` only when needed
- - **Cache results**: the SDK caches for 24h by default — leverage this for repeated queries
- - **For bulk data** (>100K records): use ChEMBL FTP downloads rather than API queries
+ - **Use `pchembl_value`** for cross-study comparisons — it normalizes IC50/Ki/EC50 to a comparable -log10 scale.
+ - **Always check `data_validity_comment`** before computing aggregates — flagged rows can skew distributions.
+ - **Pin `standard_units="nM"`** in activity queries to avoid mixing nM with µM.
+ - **Follow `page_meta.next` for pagination** instead of incrementing `offset` manually — the URL already carries the right cursor.
+ - **URL-encode SMILES** in path-style endpoints (`/similarity/{smiles}/...`, `/substructure/{smiles}`) with `urllib.parse.quote(smi, safe="")`.
+ - **Use a `Session` with retry adapter** for batch work (see Recipe) — ChEMBL handles a fair amount of traffic and occasionally returns 502/503.
+ - **For >100K records** prefer the [ChEMBL FTP downloads](https://chembl.gitbook.io/chembl-interface-documentation/downloads) over paginated API calls.
+ - **Be deliberate about acronyms in `pref_name__icontains`** — `EGFR`, `BRAF`, `HER2` all return 0 hits. Use the spelled-out term or filter via `target_components__accession=<UniProt>` instead.
## Related Skills
- - `rdkit-cheminformatics` — SMILES manipulation, molecular descriptors, fingerprints
- - `datamol-cheminformatics` — Molecular preprocessing and featurization
- - `pubchem-compound-search` — Alternative compound database (NIH)
+ - `rdkit-cheminformatics` — SMILES manipulation, fingerprints, descriptors
+ - `datamol-cheminformatics` — molecular preprocessing & featurization
+ - `pubchem-compound-search` — alternative compound database (NIH; broader coverage but less bioactivity depth)
+ - `pdb-database` — 3D structures of ChEMBL targets via RCSB PDB REST API
+ - `opentargets-database` — links ChEMBL drug-target evidence to disease associations
## References
- ChEMBL website: https://www.ebi.ac.uk/chembl/
- - API documentation: https://www.ebi.ac.uk/chembl/api/data/docs
- - Python client: https://github.com/chembl/chembl_webresource_client
- - Interface docs: https://chembl.gitbook.io/chembl-interface-documentation/
- - Example notebooks: https://github.com/chembl/notebooks
-
- ## Bundled Resources
-
- **Self-contained entry** (no `references/` directory). Original total: 662 lines (SKILL.md 389 + api_reference.md 273). Scripts: 279 lines (example_queries.py).
-
- **Original file disposition**:
- - `SKILL.md` (389 lines) → Core API, Workflows, Quick Start. "Common Use Cases" consolidated (rule 7b): Find Kinase Inhibitors → Workflow 1 pattern, Virtual Screening → Recipe, Drug Repurposing → omitted (trivial loop over drug endpoint, not a distinct analytical workflow). "Important Notes" section routed to Best Practices and Troubleshooting (rule 9)
- - `references/api_reference.md` (273 lines) → Consolidated inline. Filter Operators → Key Concepts table. Core Endpoints listing → Key Concepts table (all endpoints). Molecular Properties → Key Concepts table. Bioactivity Data Fields → Key Concepts table. Target Information Fields → Key Concepts table. Configuration/Settings → Common Recipes. Error handling/rate limiting → Troubleshooting + Best Practices. Response formats (JSON/XML/YAML) → omitted (JSON is default and only format used via Python SDK). Advanced query examples already covered in Core API
- - `scripts/example_queries.py` (279 lines) → Thin-wrapper functions absorbed into Core API modules: `get_molecule_info` / `search_molecules_by_name` / `find_molecules_by_properties` → Module 1 (Molecule Queries); `get_target_info` / `search_targets_by_name` → Module 2 (Target Queries); `get_bioactivity_data` / `get_compound_bioactivities` → Module 3 (Bioactivity Data); `find_similar_compounds` / `substructure_search` → Module 4 (Structure-Based Search); `get_drug_info` → Module 5 (Drug and Mechanism Data); `find_kinase_inhibitors` → Workflow 1; `export_to_dataframe` → Workflow 1 + Recipe (Export)
-
- **Retention**: ~460 lines / 662 original = ~69%. Vendor metadata stripped (rule 13). Agent-behavior section stripped (rule 4).
+ - REST API root: https://www.ebi.ac.uk/chembl/api/data/
+ - API docs: https://www.ebi.ac.uk/chembl/api/data/docs
+ - Interface docs (Django filter syntax): https://chembl.gitbook.io/chembl-interface-documentation/web-services
+ - Bulk downloads (for >100K records): https://chembl.gitbook.io/chembl-interface-documentation/downloads
+ - For SDK-based usage, see the `chembl_webresource_client` PyPI package; this SKILL.md uses the underlying REST API directly so no SDK install is needed.