This tutorial constructs an end-to-end autonomous workflow for discovering next-generation EGFR inhibitors, specifically targeting the C797S mutation that causes osimertinib resistance in non-small cell lung cancer. The process retrieves curated IC50 bioactivity records from ChEMBL, converts them to pIC50 values, and uses RDKit to standardize molecules and remove salts. It aggregates replicate measurements, computes Morgan fingerprints, and extracts physicochemical descriptors. The workflow trains a scaffold-split Random Forest QSAR model to ensure generalisation to unseen chemotypes, interprets potency-driving features using SHAP, and visualises influential molecular substructures. Finally, it moves to generative design by recombining BRICS fragments from potent actives, scoring virtual analogs for potency, drug-likeness, synthesizability, novelty, and developability, then cross-checking candidates against PubChem.
EGFR Target Setup
import sys, subprocess, importlib, warnings, time, os, random, json
warnings.filterwarnings("ignore")
def _pip(*pkgs):
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *pkgs], check=False)
for mod, pkg in [("rdkit", "rdkit"), ("shap", "shap"), ("requests", "requests")]:
try:
importlib.import_module(mod)
except Exception:
print(f"Installing {pkg} ...")
_pip(pkg)
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
from scipy.stats import spearmanr
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score, mean_squared_error, roc_auc_score
from sklearn.decomposition import PCA
from rdkit import Chem, DataStructs, RDLogger
from rdkit.Chem import Descriptors, Draw, QED, rdMolDescriptors, BRICS, rdFingerprintGenerator
from rdkit.Chem.Scaffolds import MurckoScaffold
RDLogger.DisableLog("rdApp.*")
try:
from rdkit.Chem.MolStandardize import rdMolStandardize
_HAS_STD = True
except Exception:
_HAS_STD = False
try:
from rdkit.Chem import RDConfig
sys.path.append(os.path.join(RDConfig.RDContribDir, "SA_Score"))
import sascorer
_HAS_SA = True
except Exception:
_HAS_SA = False
TARGET_CHEMBL_ID = "CHEMBL203"
TARGET_QUERY = "Epidermal growth factor receptor"
FALLBACK_CHEMBL_ID = "CHEMBL203"
NBITS, RADIUS = 2048, 2
RANDOM_STATE = 42
MAX_ACTIVITIES = 9000
MAX_UNIQUE = 4000
ACTIVE_PIC50 = 7.0
BRICS_MAX_TRIES = 4000
N_FRAG_PARENTS = 60
N_SHORTLIST = 12
np.random.seed(RANDOM_STATE); random.seed(RANDOM_STATE)
BASE = "https://www.ebi.ac.uk/chembl/api/data"
HDRS = {"Accept": "application/json", "User-Agent": "ai-coscientist-tutorial/1.0"}
def banner(title):
print("\n" + "=" * 86 + f"\n {title}\n" + "=" * 86)
def http_json(url, params=None, tries=3, timeout=45):
for k in range(tries):
try:
r = requests.get(url, params=params, headers=HDRS, timeout=timeout)
if r.status_code == 200:
return r.json()
if r.status_code == 404:
return None
except Exception:
pass
time.sleep(1.5 * (k + 1))
return None
banner("[1/9] TARGET INTELLIGENCE (ChEMBL + UniProt)")
print("Question: What target are we drugging, and why is it hard?\n")
def ic50_count(tid):
js = http_json(f"{BASE}/activity", {"target_chembl_id": tid, "standard_type": "IC50",
"pchembl_value__isnull": "false", "limit": 1, "format": "json"})
try:
return int(js["page_meta"]["total_count"])
except Exception:
return 0
target_id, target_name, uniprot_acc = None, TARGET_QUERY, None
if TARGET_CHEMBL_ID:
target_id = TARGET_CHEMBL_ID
else:
srch = http_json(f"{BASE}/target/search", {"q": TARGET_QUERY, "format": "json"})
cands = []
if srch:
for t in srch.get("targets", []):
if t.get("organism") == "Homo sapiens" and t.get("target_type") == "SINGLE PROTEIN":
cands.append(t)
cands = sorted(cands, key=lambda t: float(t.get("score", 0)), reverse=True)[:8]
scored = [(t, ic50_count(t["target_chembl_id"])) for t in cands]
scored = [(t, n) for t, n in scored if n > 0]
if scored:
best = max(scored, key=lambda x: x[1])[0]
target_id, target_name = best["target_chembl_id"], best.get("pref_name", TARGET_QUERY)
print(f" Auto-resolved '{TARGET_QUERY}' by data volume -> {target_id}")
else:
target_id = FALLBACK_CHEMBL_ID
print(f" Auto-resolve found no data; falling back to {FALLBACK_CHEMBL_ID}")
det = http_json(f"{BASE}/target/{target_id}", {"format": "json"})
if det and det.get("pref_name"):
target_name = det["pref_name"]
if det:
for comp in det.get("target_components", []):
if comp.get("accession"):
uniprot_acc = comp["accession"]; break
print(f" Resolved target : {target_name}")
print(f" ChEMBL ID : {target_id}")
print(f" UniProt : {uniprot_acc}")
if uniprot_acc:
uni = http_json(f"https://rest.uniprot.org/uniprotkb/{uniprot_acc}.json")
if uni:
try:
fn = next(c["texts"][0]["value"] for c in uni.get("comments", [])
if c.get("commentType") == "FUNCTION")
print("\n Function (UniProt):")
print(" ", (fn[:340] + " ...") if len(fn) > 340 else fn)
except Exception:
pass
print("""
Resistance context: 1st/2nd/3rd-gen EGFR TKIs lose potency once tumours acquire the
C797S mutation, which abolishes the covalent cysteine anchor exploited by osimertinib.
Goal of this run: learn the chemistry of known EGFR inhibitors and propose NOVEL,
drug-like analogs as starting points for a C797S-active 4th-generation series.""")
The script sets up the scientific computing environment by installing missing chemistry, modeling, plotting, and API dependencies. It configures EGFR target settings, defines modeling constants, and initialises reproducible random seeds. Helper functions manage banners and JSON API calls. The code resolves the ChEMBL target, retrieves UniProt context if available, and frames the biological motivation around EGFR C797S resistance.
Mining ChEMBL Bioactivity Data
banner("[2/9] BIOACTIVITY MINING (ChEMBL activities -> pIC50)")
def pull_activities(tid, cap):
url, rows = f"{BASE}/activity", []
params = {"target_chembl_id": tid, "standard_type": "IC50",
"pchembl_value__isnull": "false", "limit": 1000, "format": "json"}
js = http_json(url, params)
pages = 0
while js and pages < 60:
rows.extend(js.get("activities", []))
pages += 1
if len(rows) >= cap:
break
nxt = js.get("page_meta", {}).get("next")
if not nxt:
break
nurl = nxt if nxt.startswith("http") else "https://www.ebi.ac.uk" + nxt
js = http_json(nurl)
return rows[:cap]
raw = pull_activities(target_id, MAX_ACTIVITIES)
print(f" Pulled {len(raw)} raw IC50 records with a curated pChEMBL value.")
recs = []
for a in raw:
smi, pv = a.get("canonical_smiles"), a.get("pchembl_value")
if not smi or pv in (None, ""):
continue
if a.get("standard_relation") != "=":
continue
if a.get("standard_units") not in ("nM", None):
continue
try:
pv = float(pv)
except Exception:
continueSource Read original →

![ML for UFC predictions: logistic regression vs random forest? [P]](https://ai-maestro.online/wp-content/uploads/2026/05/ml-for-ufc-predictions-logistic-regression-vs-random-forest--768x432.jpg)

