
Target Prioritization
- 1.8k installs
- 29 repo stars
- Updated August 2, 2026
- agents365-ai/365-skills
target-prioritization provides documented workflows for Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated
About
The target-prioritization skill prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR essentiality + gnomAD constraint), PubMed, the Human Protein Atlas (HPA), and ChEMBL tool compounds, then re-ranking by a composite score combining protein localization, druggability, disease genetics, tissue specificity (safety), focus-cell-type expression, CRISPR essentiality, LoF safety constraint, and research maturity. # Target Prioritization A multi-source drug-target due-diligence pipeline for ranked gene lists. ## When this skill triggers The user has a list of candidate genes (typically from a DE / DEG / scRNA-seq analysis) and wants a per-gene dossier across multiple evidence dimensions plus a composite re-ranking. The DE statistical rank is just the entry point; the final priority is informed by protein biology, genetics, druggability, and research maturity. Common input shapes: - A CSV with a `gene` column (DE output like `expression_table_pass_either_1s.csv`) - A plain-text gene list (one symbol per line) - A list of symbols inline in the user's message ## Output T.
- A CSV with a `gene` column (DE output like `expression_table_pass_either_1s.csv`)
- A plain-text gene list (one symbol per line)
- A list of symbols inline in the user's message
- **`targets_report.md`** - one section per gene, sorted by composite score, with a
- **`targets_summary.csv`** - flat table for sorting/filtering in Excel/pandas
Target Prioritization by the numbers
- 1,832 all-time installs (skills.sh)
- +118 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #93 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
target-prioritization capabilities & compatibility
- Capabilities
- a csv with a `gene` column (de output like `expr · a plain text gene list (one symbol per line) · a list of symbols inline in the user's message · **`targets_report.md`** one section per gene, · **`targets_summary.csv`** flat table for sorti
- Use cases
- documentation · planning
What target-prioritization says it does
# Target Prioritization A multi-source drug-target due-diligence pipeline for ranked gene lists.
## When this skill triggers The user has a list of candidate genes (typically from a DE / DEG / scRNA-seq analysis) and wants a per-gene dossier across multiple evidence dimensions plus a composite re
npx skills add https://github.com/agents365-ai/365-skills --skill target-prioritizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 29 |
| Last updated | August 2, 2026 |
| Repository | agents365-ai/365-skills ↗ |
How do I use target-prioritization for the task described in its SKILL.md triggers?
Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR essentiality + gnomAD con.
Who is it for?
Teams invoking target-prioritization when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR essentiality + gnomAD constraint), PubMed, the
What you get
Step-by-step guidance grounded in target-prioritization documentation and reference files.
- ranked target shortlist
- impact scoring rationale
By the numbers
- 361 installs on skills.sh
- Rank 6 in the agents365-ai/365-skills collection
Files
Target Prioritization
A multi-source drug-target due-diligence pipeline for ranked gene lists.
When this skill triggers
The user has a list of candidate genes (typically from a DE / DEG / scRNA-seq analysis) and wants a per-gene dossier across multiple evidence dimensions plus a composite re-ranking. The DE statistical rank is just the entry point; the final priority is informed by protein biology, genetics, druggability, and research maturity.
Common input shapes:
- A CSV with a
genecolumn (DE output likeexpression_table_pass_either_1s.csv) - A plain-text gene list (one symbol per line)
- A list of symbols inline in the user's message
Output
Three files inside <output_dir>/: 1. `targets_report.md` — one section per gene, sorted by composite score, with a short LLM-written rationale and recommended next step 2. `targets_summary.csv` — flat table for sorting/filtering in Excel/pandas 3. `raw_data/<source>.json` — raw API responses (audit trail, reusable across future re-scorings)
Pipeline
input gene list
│
▼
scripts/orchestrate.py
│
├─► fetch_uniprot.py → protein localization, surface, MHC, coding
├─► fetch_opentargets.py → tractability, approved drugs, associated
│ diseases (subsumes GWAS Catalog via OT's
│ integrated genetics evidence), DepMap CRISPR
│ essentiality, gnomAD LOEUF / pLI constraint
├─► fetch_pubmed.py → paper counts (total + focus_disease + cell_context)
├─► fetch_hpa.py → HPA tissue / single-cell specificity + nCPM,
│ expression cluster, cancer prognostics
└─► fetch_chembl.py → top-potency tool compounds per gene (pIC50,
IC50 nM, mechanism) — dossier-only, no score
│
▼
scripts/aggregate.py
│
▼
output_dir/
├─ raw_data/*.json
├─ targets_summary.csv ← composite-score-ranked
└─ targets_report.md ← Claude fills the rationale sectionsHow to invoke
python3 ~/myagents/myskills/target-prioritization/scripts/orchestrate.py \
--input <gene_list.csv_or_txt> \
--output <output_dir> \
[--gene-col gene] \
[--top 50]--inputaccepts a CSV (with--gene-col, defaultgene), a.txt/.tsv,
or any file where the first column has gene symbols. Skips header if first cell is gene/symbol/case-insensitive.
--toplimits the dossier to the top N input genes (default 50) — input
order is preserved up to that cut, then composite-score re-ranks within.
orchestrate.py runs the five fetchers in parallel (Python threads, since all calls are I/O-bound). Each writes a self-contained JSON to <output_dir>/raw_data/<source>.json. Then aggregate.py merges them, computes the composite score using weights.yaml, writes targets_summary.csv, and emits a targets_report.md skeleton with one section per gene — the rationale and risks fields are left blank for Claude to fill.
Composite score
Weights live in weights.yaml and can be overridden per-run with --weights. Defaults aim for "find druggable, genetically supported targets with clean therapeutic window and expression in the cell of interest":
composite_score = w1 * druggability_score (approved drugs, tractability, clin trials)
+ w2 * disease_genetics_score (OpenTargets disease associations + focus-disease bonus)
+ w3 * tractability_bonus (surface or secreted vs intracellular)
+ w4 * tissue_specificity (HPA tissue tag — narrow expression = cleaner window)
+ w5 * cell_context_score (HPA single-cell nCPM rank in FOCUS_CELL_TYPES)
+ w6 * essentiality_score (DepMap CRISPR % essential, pan-essentials capped)
+ w7 * safety_constraint_score (gnomAD LOEUF — high = LoF tolerated → safer to inhibit)
+ w8 * expression_score (from input DE if present)
+ w9 * novelty_bonus (favors moderately studied)
- w10 * over_studied_penalty (PubMed total > cap → diminishing returns)ChEMBL contributes dossier columns (chembl_target_id, chembl_best_pchembl, chembl_best_ic50_nm, chembl_top_compounds) but no score component — its job is to surface concrete tool compounds for the "Suggested next step" slot.
Each component is normalized to [0, 1]. The composite is therefore roughly in [-w7, sum(w1..w6)] and is min-max rescaled before reporting. Read `weights.yaml` for the current defaults.
Writing the rationale
After aggregate.py produces targets_report.md with blank rationale slots, Claude reads the per-gene dossier rows and writes a 2-3 sentence rationale per gene. Use the template in prompts/rationale_template.md — it specifies the structure (one line on the most compelling evidence, one line on the main risk, one line on the suggested next experimental step).
For the top 5–10 genes by composite score, also write a short executive summary at the top of the report. Keep it factual and grounded in the dossier data; do not hallucinate beyond what the JSONs contain.
Data source notes
All free, no API key needed. Rate limits handled in fetchers:
- UniProt REST — 100 req/sec, batched via
accessionquery - OpenTargets GraphQL — generous, single endpoint; provides disease genetics signal via integrated
associatedDiseases - PubMed E-utilities — 3 req/sec without key; fetchers respect this
- Human Protein Atlas —
search_download.phpfor symbol→ENSG, then per-ENSG/<ENSG>.json; no rate limit documented, fetcher sleeps 0.15s/gene - DepMap CRISPR essentiality — fetched via
target.depMapEssentialityinside the OpenTargets call (no separate endpoint) - gnomAD constraint — fetched via
target.geneticConstraintinside the OpenTargets call (avoids gnomAD's WAF on direct API access) - ChEMBL REST —
target/search.jsonthenactivity.json; ~5 req/sec friendly, fetcher sleeps 0.2s/gene
For deeper API details and field mappings, see references/api_endpoints.md.
Retargeting the focus disease + cell context
The skill ships with an autoimmunity / T-cell default but is intentionally disease-agnostic. Three edits switch the focus:
scripts/fetch_opentargets.pyandscripts/aggregate.py— change
FOCUS_DISEASE_TERMS to the lowercased substrings that should mark a drug or disease association as "in-scope" (e.g. ("cancer", "carcinoma", "lymphoma") for oncology; ("alzheimer", "parkinson", "huntington", "als") for neurodegeneration; ("diabetes", "obesity", "fatty liver", "nash") for metabolic disease).
scripts/aggregate.py— changeFOCUS_CELL_TYPESto the HPA single-cell
type names that should drive cell_context_score. Must match HPA's exact strings (case-sensitive); see comment block above the tuple for examples per domain.
scripts/fetch_pubmed.py— adjust thefocus_diseaseand
cell_context queries in CONTEXTS (these power the PubMed counts in the dossier).
No other code changes are needed; the CSV column names already use the neutral focus_disease_* / cell_context prefixes.
When NOT to use this skill
- Single-gene look-ups (overkill — just ask Claude to web-search)
- Non-human genes (most APIs are human-only; fetchers will silently return empty)
- Pure literature review without target ambition — use
scholar-deep-researchorliterature-reviewinstead
Iteration tips
The pipeline is designed to be re-runnable cheaply:
- Raw JSON cache means re-scoring with different
weights.yamlis a one-secondaggregate.pyrerun - To add a new evidence source, add
scripts/fetch_<source>.pythat writes
raw_data/<source>.json with the same {gene: {fields}} shape, then add a corresponding term in aggregate.py::compute_composite_score.
Per-gene rationale template
When filling the Rationale and Suggested next step slots in targets_report.md, follow this template per gene. Keep it tight — 2-3 sentences total for rationale, 1 sentence for next step.
Rationale (2-3 sentences)
Sentence 1 — most compelling evidence (pick the single strongest signal from the dossier row):
- If
is_focus_disease_associated→ "OpenTargets surfaces a focus-disease
association (score [max_focus_disease_assoc_score]) — likely backed by GWAS / text-mining evidence…"
- Else if
any_focus_disease_drug→ "Already targeted by an approved
focus-disease drug ([drug name])…"
- Else if
depmap_pct_essential >= 0.3ANDdepmap_pct_essential <= 0.8
→ "Selectively essential in [N]% of DepMap cell lines (mean geneEffect [val]) — clear dependency in the relevant lineage…"
- Else if
chembl_best_pchembl >= 9→ "ChEMBL surfaces a sub-nM tool
compound ([compound name], pIC50 [val]) — chemistry available now for ex-vivo validation…"
- Else if
hpa_focus_cell_hitsnon-empty ANDcell_context_score >= 0.7
→ "HPA single-cell data ranks [cell type] in the top-2 expressing cell types for this gene (nCPM [val]) — strong target-cell expression…"
- Else if
tissue_specificity == 1.0(HPATissue enriched/ `Group
enriched`) → "Narrow tissue expression in [top tissue] (HPA enriched) — cleaner therapeutic window than a broadly expressed target…"
- Else if
is_surfaceANDhighest_clinical_phase >= 3→ "Surface protein
with phase III drugs in adjacent indications…"
- Else if
is_surfaceANDmaturity_tag in {novel, moderate}→ "Surface
protein with moderate prior literature ([N] PubMed hits) — tractable for antibody / CAR / ADC approaches…"
- Else use whatever component scores highest in the breakdown row.
Sentence 2 — main risk or caveat:
maturity_tag = uncharted→ "Very thin literature ([N] hits) — risk of
unknown off-target biology."
maturity_tag = saturated→ "Heavily studied ([N] hits); likely IP
crowded."
is_mhc = True→ "MHC-family gene — broad-spectrum effects, hard to
inhibit selectively."
compositedriven by single dimension only → "Score concentrated in one
dimension — confirm with orthogonal evidence before pursuing."
- No disease association → "Stat signal from DE only; lacks genetic
corroboration."
tissue_specificity <= 0.2(HPALow tissue specificity) → "Broadly
expressed across tissues — narrow therapeutic window unlikely without a delivery / targeting strategy."
cell_context_score == 0andhpa_focus_cell_hitsempty → "Not in the
top-expressing cell types per HPA single-cell data — efficacy in the target population uncertain."
depmap_pct_essential >= 0.85→ "Pan-essential in DepMap (≥85% of cell
lines depend on it) — broad cytotoxicity risk; therapeutic window unlikely without selective delivery."
safety_constraint_score <= 0.4(LOEUF in top decile) → "gnomAD flags
this gene as highly LoF-constrained (LOEUF [val]) — full inhibition may approach haploinsufficient territory."
chembl_target_id is Noneorchembl_best_pchembl is None→ "No
potent IC50 tool compound in ChEMBL (pIC50 ≥ 7) — chemical biology starting point is limited."
Sentence 3 (optional) — specific project context if obvious from the dossier (e.g. "Persists in non-responder cells at post-treatment, consistent with [pathway] escape"; "Co-expressed with [marker] in the tumour microenvironment dataset"; "Up in [region] but absent from healthy control biopsies").
Suggested next step (1 sentence)
Be concrete and adapted to the user's experimental context. Examples across domains:
- Functional genomics: "siRNA / CRISPRi knockdown in the relevant
primary cell type; readout the disease-relevant secreted protein or phosphorylation marker by ELISA / flow."
- Histology: "IHC / RNAscope on patient vs healthy tissue from the
matching anatomical site to confirm protein-level upregulation."
- Pharmacology: "Treat ex-vivo with [tool compound from approved_drugs]
and compare against the standard-of-care arm."
- Replication: "Cross-check expression in an independent public
cohort with matched contrast (e.g. a GEO / ArrayExpress dataset)."
- Chemistry: "If no tool compound exists: structure-based virtual
screen against UniProt:[id], or commission a fragment-screen pilot."
Executive summary (top 5–10 genes)
3–5 sentences total at the top of the report. Cover: 1. How many genes scored Tier-1 vs Tier-2. 2. The 2–3 most compelling individual candidates and the headline reason. 3. Any pattern across the top genes (e.g. "5/10 are surface receptors in the same signalling cassette, suggesting [pathway] is the dominant axis"). 4. Caveats / what's missing (e.g. "All candidates derive from a single contrast — recommend cross-checking against an orthogonal one").
Keep it factual. Do not invent biology not supported by the dossier rows or the original DE context.
API endpoints used by target-prioritization
All endpoints are free and require no API key.
UniProt REST
- Base:
https://rest.uniprot.org/uniprotkb/search - Query format:
(gene:SYMBOL) AND (organism_id:9606) AND (reviewed:true) - Fields requested (comma-separated):
accession, gene_names, protein_name, cc_subcellular_location, protein_existence, keyword, ft_topo_dom, ft_transmem, ft_signal, cc_function
- Rate: ~100 req/sec; we sleep 0.1s between calls to be polite.
- Docs: https://www.uniprot.org/help/api_queries
OpenTargets GraphQL
- Endpoint:
https://api.platform.opentargets.org/api/v4/graphql - Two queries:
1. search(queryString: SYMBOL, entityNames: ["target"]) to resolve symbol → Ensembl ID 2. target(ensemblId: ENSG…) to get tractability, drugAndClinicalCandidates (returns rows with maxClinicalStage like APPROVAL, PHASE_3, etc.), associatedDiseases (integrates GWAS Catalog + other genetics evidence sources — so we do NOT call GWAS Catalog directly), depMapEssentiality (per-tissue + per-cell-line CRISPR geneEffect — we do NOT call the DepMap portal API directly), and geneticConstraint (gnomAD-derived LOEUF / oe_lof / upperBin — we do NOT call the gnomAD GraphQL directly, which is also useful for avoiding its WAF).
- Stage tokens are uppercase:
APPROVAL(=approved),PHASE_4..PHASE_1,
EARLY_PHASE_1, PRECLINICAL, WITHDRAWN, UNKNOWN. Mapping lives in PHASE_MAP inside fetch_opentargets.py.
- Tractability modality codes:
SM(small molecule),AB(antibody),
PR (protein degrader/other), OC (other clinical). Each modality has ordered tier labels — we pick the highest tier with value=True.
- Rate: generous; 0.2s sleep between targets to avoid hammering.
- Docs: https://platform-docs.opentargets.org/data-access/graphql-api
Human Protein Atlas
- Symbol → Ensembl resolver:
https://www.proteinatlas.org/api/search_download.php?search=SYMBOL&format=json&columns=g,eg&compress=no
- Per-entry JSON:
https://www.proteinatlas.org/<ENSG>.json(≈100 fields) - Subcellular fields are not consumed here — UniProt is authoritative.
HPA contributes:
RNA tissue specificity(tag like `Tissue enriched / Group enriched
/ Tissue enhanced / Low tissue specificity / Not detected) and RNA tissue specific nTPM` (dict tissue→nTPM)
RNA single cell type specificity(analogous tag) and
RNA single cell type specific nCPM (dict cell-type→nCPM)
Single cell expression cluster(label of HPA's UMAP cluster)Cancer prognostics - <cancer name>per-cancer dicts — aggregated into
n_prognostic_cancers + top-3 cancers with prognostic type + p_val, plus the global RNA cancer specificity tag
FOCUS_CELL_TYPESinscripts/aggregate.pymust match HPA's exact
cell-type strings (e.g. "T-cells", "Hepatocytes", "Microglial cells").
- No documented rate limit; fetcher sleeps 0.15s between calls.
- Docs: https://www.proteinatlas.org/about/help/dataaccess
PubMed E-utilities
- Endpoint:
https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi - Three queries per gene:
total,focus_disease,cell_context. The
focus_disease and cell_context query templates live in scripts/fetch_pubmed.py::CONTEXTS and can be re-pointed to any domain (oncology, neurodegeneration, metabolic, etc.) by editing the string.
- Rate: 3 req/sec without API key; we sleep 0.34s between requests.
With an NCBI API key (env NCBI_API_KEY), up to 10 req/sec — not implemented here, easy to add.
- Maturity bins (configurable thresholds):
<5= uncharted5-29= novel30-99= moderate100-499= well_studied≥500= saturated
ChEMBL REST
- Two HTTP calls per gene:
1. https://www.ebi.ac.uk/chembl/api/data/target/search.json?q=SYMBOL&limit=10 — filter results client-side to organism == "Homo sapiens" and target_type == "SINGLE PROTEIN"; prefer exact pref_name / synonym match. 2. https://www.ebi.ac.uk/chembl/api/data/activity.json?target_chembl_id=ID&standard_type=IC50&pchembl_value__gte=7&limit=5&order_by=-pchembl_value — pulls the 5 most-potent IC50 assays with pIC50 ≥ 7 (≈ 100 nM).
- Output is dossier-only: top compound ID, pIC50, IC50 (nM), optional
molecule_pref_name (named tool compounds like MOBOCERTINIB).
- No API key, ~5 req/sec friendly; fetcher sleeps 0.2s/gene.
- Docs: https://www.ebi.ac.uk/chembl/api/data/docs
Adding a new source
To plug in another evidence source (e.g. DGIdb, OMIM, OpenTargets Genetics, Reactome):
1. Write scripts/fetch_<source>.py accepting --genes SYM,SYM,… and --output PATH.json, writing {gene: {fields}}. 2. Add the (name, script_path) tuple to FETCHERS in orchestrate.py. 3. Add a component term to compute_components in aggregate.py. 4. Add corresponding weights: and (if needed) flags: keys to weights.yaml. 5. Surface the new fields in the markdown skeleton in aggregate.py.
No re-fetch needed for downstream re-runs — the raw JSON cache makes re-scoring effectively free.
#!/usr/bin/env python3
"""Aggregate raw fetcher JSONs → composite score → targets_summary.csv + targets_report.md (skeleton)."""
import argparse
import csv
import json
import sys
from pathlib import Path
def load_json(p: Path) -> dict:
if not p.exists():
return {}
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception as e:
print(f" warn: could not parse {p}: {e}", file=sys.stderr)
return {}
def load_weights(p: Path) -> dict:
"""Minimal YAML reader (no PyYAML dep) for our flat schema."""
out = {"weights": {}, "caps": {}, "flags": {}}
section = None
for raw in p.read_text(encoding="utf-8").splitlines():
line = raw.split("#", 1)[0].rstrip()
if not line.strip():
continue
if line.startswith(("weights:", "caps:", "flags:")):
section = line.split(":", 1)[0]
continue
if section and line.startswith((" ", "\t")):
try:
k, v = line.strip().split(":", 1)
out[section][k.strip()] = float(v.strip())
except Exception:
continue
return out
def clamp01(x: float) -> float:
return 0.0 if x < 0 else (1.0 if x > 1 else x)
# Override to retarget for your project — see fetch_opentargets.py for examples.
FOCUS_DISEASE_TERMS = ("crohn", "ulcerative colitis", "inflammatory bowel", "ibd")
# HPA single-cell type names that count as "in-scope" for cell_context_score.
# Must match HPA's exact strings (case-sensitive). Examples for retargeting:
# Oncology / tumor microenv: ("Macrophages", "Fibroblasts", "T-cells")
# Neurodegeneration: ("Excitatory neurons", "Microglial cells", "Astrocytes")
# Metabolic / liver: ("Hepatocytes", "Kupffer cells")
# Cardiovascular: ("Cardiomyocytes", "Endothelial cells")
# Pancreatic / diabetes: ("Pancreatic beta cells", "Pancreatic alpha cells")
FOCUS_CELL_TYPES = ("T-cells",)
def derive_essentiality(ot_entry: dict) -> float:
"""DepMap CRISPR essentiality → [0, 1].
pct_essential = fraction of screened cell lines where geneEffect < -0.5.
Pan-essential genes (>80% essential) are clipped, since they kill
every cell — bad therapeutic windows. Moderate selectivity gets max.
"""
pct = ot_entry.get("depmap_pct_essential") or 0.0
# Sweet spot at pct in [0.2, 0.8]; tapers above 0.8 (pan-essential is risky)
if pct <= 0.8:
return clamp01(pct / 0.8)
return clamp01(1.0 - (pct - 0.8) / 0.4) # pct=1.0 → 0.5
def derive_safety_constraint(ot_entry: dict) -> float:
"""gnomAD LOEUF → [0, 1].
High LOEUF (>=1) means natural LoF is tolerated in humans — a
"demonstrated safe to inhibit" signal. Low LOEUF (<0.35, top decile)
flags haploinsufficiency / hypothesized safety risk for full inhibition.
Missing LOEUF returns neutral 0.5.
"""
loeuf = ot_entry.get("loeuf")
if loeuf is None:
return 0.5
if loeuf >= 1.0:
return 1.0
if loeuf < 0.35:
return 0.3
return clamp01(0.5 + (loeuf - 0.35) * (0.5 / 0.65))
def derive_hpa_signal(hpa_entry: dict) -> dict:
"""Translate HPA entry into score-ready signals.
- tissue_specificity_score: cleaner therapeutic window → higher
- cell_context_score: focus-cell-type nCPM rank within this gene's
cell-type expression dict (1.0 if a focus cell is top-1, decays
down the rank). Returns 0 if no focus cell is expressed.
"""
tissue_tag = (hpa_entry.get("tissue_specificity_tag") or "").strip()
TISSUE_MAP = {
"Tissue enriched": 1.0,
"Group enriched": 1.0,
"Tissue enhanced": 0.7,
"Low tissue specificity": 0.2,
"Not detected": 0.0,
}
tissue_score = TISSUE_MAP.get(tissue_tag, 0.3)
cell_full = hpa_entry.get("cell_nCPM_full") or {}
if cell_full:
ranked = sorted(cell_full.items(), key=lambda kv: -kv[1])
focus_rank = None
focus_hits = []
for i, (ct, _val) in enumerate(ranked, 1):
if ct in FOCUS_CELL_TYPES:
focus_hits.append(ct)
if focus_rank is None:
focus_rank = i
if focus_rank is None:
cell_score = 0.0
else:
# rank 1 → 1.0, rank 2 → 0.7, rank 3 → 0.5, then linear decay
ladder = {1: 1.0, 2: 0.7, 3: 0.5}
cell_score = ladder.get(focus_rank, max(0.0, 0.5 - 0.05 * (focus_rank - 3)))
else:
focus_hits = []
cell_score = 0.0
return {
"tissue_score": tissue_score,
"cell_score": cell_score,
"focus_cell_hits": focus_hits,
}
def derive_disease_signal(ot_entry: dict) -> dict:
"""Derive a disease_genetics-style signal from OpenTargets associated diseases.
Replaces the dropped dedicated GWAS fetcher — OT integrates GWAS Catalog etc."""
rows = ot_entry.get("associated_diseases_top5") or []
any_assoc = bool(rows)
focus_hits = []
max_focus_score = 0.0
max_any_score = 0.0
for r in rows:
name = (r.get("name") or "").lower()
score = float(r.get("score") or 0)
max_any_score = max(max_any_score, score)
if any(t in name for t in FOCUS_DISEASE_TERMS):
focus_hits.append(r.get("name"))
max_focus_score = max(max_focus_score, score)
return {
"any_assoc": any_assoc,
"is_focus_disease_associated": bool(focus_hits),
"focus_disease_hits": focus_hits,
"max_focus_disease_score": max_focus_score,
"max_any_score": max_any_score,
}
def compute_components(g: str, uniprot: dict, ot: dict, pubmed: dict, hpa: dict,
weights: dict, input_expr: dict) -> dict:
w = weights["weights"]; c = weights["caps"]; f = weights["flags"]
u = (uniprot.get(g) or {})
o = (ot.get(g) or {})
gw = derive_disease_signal(o)
pm = (pubmed.get(g) or {})
hp = (hpa.get(g) or {})
hs = derive_hpa_signal(hp)
tissue_specificity = clamp01(hs["tissue_score"])
cell_context_score = clamp01(hs["cell_score"])
essentiality_score = derive_essentiality(o)
safety_constraint_score = derive_safety_constraint(o)
# 2) druggability score
drug = 0.0
if o.get("approved_drug_count", 0) > 0:
drug = max(drug, f.get("approved_drug_bonus", 0.7))
phase = o.get("highest_clinical_phase", 0) or 0
drug = max(drug, clamp01(phase / 4.0))
if o.get("any_focus_disease_drug"):
drug = max(drug, 0.85)
druggability = clamp01(drug)
# 3) disease genetics — derived from OpenTargets associated diseases
g_score = 0.0
if gw["any_assoc"]:
g_score += 0.4 * gw["max_any_score"]
if gw["is_focus_disease_associated"]:
g_score += f.get("focus_disease_assoc_bonus", 0.5) + 0.2 * gw["max_focus_disease_score"]
disease_genetics = clamp01(g_score)
# 4) tractability bonus
if u.get("is_surface"):
tract = f.get("surface_protein_bonus", 1.0)
elif u.get("is_secreted"):
tract = f.get("secreted_protein_bonus", 0.8)
else:
tract = f.get("intracellular_default", 0.3)
tractability = clamp01(tract)
# 5) expression score — from input DE table (if provided)
expr = input_expr.get(g)
if expr is not None:
expression = clamp01(expr / 3.0) # log1p(CP10K) ~ 0-3 typical range
else:
expression = 0.0
# 6) novelty bonus + 7) over-studied penalty
total = pm.get("pubmed_total", 0)
cap = c.get("pubmed_total_for_maturity", 100)
floor = c.get("pubmed_well_studied_floor", 5)
if total < floor:
novelty = 0.3 # too uncharted = risk
over_studied = 0.0
elif total <= cap:
novelty = 1.0
over_studied = 0.0
else:
novelty = clamp01(1.0 - (total - cap) / (10 * cap))
over_studied = clamp01((total - cap) / (5 * cap))
composite = (
w.get("druggability_score", 0) * druggability
+ w.get("disease_genetics_score", 0) * disease_genetics
+ w.get("tractability_bonus", 0) * tractability
+ w.get("tissue_specificity", 0) * tissue_specificity
+ w.get("cell_context_score", 0) * cell_context_score
+ w.get("essentiality_score", 0) * essentiality_score
+ w.get("safety_constraint_score", 0) * safety_constraint_score
+ w.get("expression_score", 0) * expression
+ w.get("novelty_bonus", 0) * novelty
- w.get("over_studied_penalty", 0) * over_studied
)
return {
"druggability": round(druggability, 3),
"disease_genetics": round(disease_genetics, 3),
"tractability": round(tractability, 3),
"tissue_specificity": round(tissue_specificity, 3),
"cell_context_score": round(cell_context_score, 3),
"essentiality_score": round(essentiality_score, 3),
"safety_constraint_score": round(safety_constraint_score, 3),
"expression": round(expression, 3),
"novelty": round(novelty, 3),
"over_studied": round(over_studied, 3),
"composite_raw": round(composite, 4),
}
def tier_for(score_norm: float) -> str:
if score_norm >= 0.75: return "Tier-1-priority"
if score_norm >= 0.50: return "Tier-2-candidate"
if score_norm >= 0.30: return "Tier-3-watchlist"
return "Tier-4-deprioritized"
def load_input_expr(csv_path: str) -> dict:
"""Read mean_g2 / mean_g1 / max_mean from a DE CSV if present, keyed by gene."""
expr = {}
if not csv_path:
return expr
p = Path(csv_path)
if not p.exists():
return expr
try:
with open(p, encoding="utf-8", errors="replace") as f:
reader = csv.DictReader(f)
for row in reader:
g = row.get("gene") or row.get("symbol")
if not g:
continue
vals = []
for k in ("mean_g2", "mean_g1", "sample_mean_g2", "sample_mean_g1"):
v = row.get(k)
try:
vals.append(float(v))
except (ValueError, TypeError):
continue
if vals:
expr[g] = max(vals)
except Exception:
pass
return expr
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--raw-dir", required=True)
ap.add_argument("--output-dir", required=True)
ap.add_argument("--weights", required=True)
ap.add_argument("--input-csv", default="", help="Original DE CSV (used for expression score)")
args = ap.parse_args()
raw = Path(args.raw_dir)
out_dir = Path(args.output_dir)
weights = load_weights(Path(args.weights))
genes = json.loads((raw / "genes.json").read_text(encoding="utf-8"))
uniprot = load_json(raw / "uniprot.json")
ot = load_json(raw / "opentargets.json")
pubmed = load_json(raw / "pubmed.json")
hpa = load_json(raw / "hpa.json")
chembl = load_json(raw / "chembl.json")
input_expr = load_input_expr(args.input_csv)
rows = []
for g in genes:
comp = compute_components(g, uniprot, ot, pubmed, hpa, weights, input_expr)
u = uniprot.get(g, {}) or {}
o = ot.get(g, {}) or {}
gw = derive_disease_signal(o)
pm = pubmed.get(g, {}) or {}
hp = hpa.get(g, {}) or {}
hs = derive_hpa_signal(hp)
cb = chembl.get(g, {}) or {}
rows.append({
"gene": g,
"composite_raw": comp["composite_raw"],
"druggability": comp["druggability"],
"disease_genetics": comp["disease_genetics"],
"tractability": comp["tractability"],
"tissue_specificity": comp["tissue_specificity"],
"cell_context_score": comp["cell_context_score"],
"essentiality_score": comp["essentiality_score"],
"safety_constraint_score": comp["safety_constraint_score"],
"expression": comp["expression"],
"novelty": comp["novelty"],
"over_studied_penalty": comp["over_studied"],
"uniprot_id": u.get("uniprot_id"),
"protein_name": u.get("protein_name"),
"subcellular_location": " | ".join(u.get("subcellular_location") or []),
"is_surface": u.get("is_surface"),
"is_secreted": u.get("is_secreted"),
"is_mhc": u.get("is_mhc"),
"has_transmembrane": u.get("has_transmembrane"),
"approved_drug_count": o.get("approved_drug_count", 0),
"highest_clinical_phase": o.get("highest_clinical_phase", 0),
"any_focus_disease_drug": o.get("any_focus_disease_drug", False),
"focus_disease_drugs": "; ".join(o.get("focus_disease_drugs") or []),
"tractability_small_molecule": o.get("tractability_small_molecule"),
"tractability_antibody": o.get("tractability_antibody"),
"any_disease_assoc": gw["any_assoc"],
"is_focus_disease_associated": gw["is_focus_disease_associated"],
"focus_disease_traits": "; ".join(gw["focus_disease_hits"]),
"max_focus_disease_assoc_score": round(gw["max_focus_disease_score"], 3),
"max_disease_assoc_score": round(gw["max_any_score"], 3),
"pubmed_total": pm.get("pubmed_total", 0),
"pubmed_focus_disease": pm.get("pubmed_focus_disease", 0),
"pubmed_cell_context": pm.get("pubmed_cell_context", 0),
"maturity_tag": pm.get("maturity_tag"),
"hpa_tissue_specificity_tag": hp.get("tissue_specificity_tag"),
"hpa_tissue_top_types": "; ".join(f"{k}={v}" for k, v in (hp.get("tissue_top_types") or [])),
"hpa_cell_specificity_tag": hp.get("cell_specificity_tag"),
"hpa_cell_top_types": "; ".join(f"{k}={v}" for k, v in (hp.get("cell_top_types") or [])),
"hpa_focus_cell_hits": "; ".join(hs["focus_cell_hits"]),
"hpa_expression_cluster": hp.get("expression_cluster"),
"hpa_n_prognostic_cancers": (hp.get("pathology") or {}).get("n_prognostic_cancers", 0),
"hpa_cancer_specificity": (hp.get("pathology") or {}).get("rna_cancer_specificity"),
"depmap_n_screens": o.get("depmap_n_screens", 0),
"depmap_mean_gene_effect": (round(o["depmap_mean_gene_effect"], 3) if o.get("depmap_mean_gene_effect") is not None else None),
"depmap_pct_essential": round(o.get("depmap_pct_essential", 0.0), 3),
"loeuf": o.get("loeuf"),
"constraint_oe_lof": o.get("constraint_oe_lof"),
"constraint_top_decile": o.get("constraint_top_decile", False),
"chembl_target_id": cb.get("chembl_target_id"),
"chembl_best_pchembl": cb.get("best_pchembl"),
"chembl_best_ic50_nm": cb.get("best_ic50_nm"),
"chembl_top_compounds": "; ".join(
f"{c.get('pref_name') or c.get('chembl_id')}(pIC50={c.get('pchembl_value')})"
for c in (cb.get("top_compounds") or [])[:3]
),
})
# Min-max rescale composite into [0,1] for tier assignment
raws = [r["composite_raw"] for r in rows]
lo, hi = (min(raws), max(raws)) if raws else (0.0, 1.0)
span = (hi - lo) or 1.0
for r in rows:
r["composite_score"] = round((r["composite_raw"] - lo) / span, 3)
r["tier"] = tier_for(r["composite_score"])
rows.sort(key=lambda r: -r["composite_score"])
# CSV
csv_path = out_dir / "targets_summary.csv"
if rows:
with open(csv_path, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print(f"aggregate: wrote {csv_path}")
# Markdown skeleton (rationale slots left for Claude to fill)
md = ["# Target Prioritization Report\n",
f"_{len(rows)} genes scored. Sorted by composite_score (descending)._\n",
"## Executive summary\n",
"_TO BE FILLED BY CLAUDE — 3–5 sentences on the top 5–10 genes._\n",
"## Per-gene dossier\n"]
for r in rows:
md.append(f"### {r['gene']} — composite {r['composite_score']:.3f} ({r['tier']})\n")
md.append("| Field | Value |")
md.append("|---|---|")
md.append(f"| UniProt | {r['uniprot_id'] or '—'} — {r['protein_name'] or '—'} |")
md.append(f"| Localization | {r['subcellular_location'] or '—'} |")
md.append(f"| Surface / secreted / MHC | surf={r['is_surface']} sec={r['is_secreted']} mhc={r['is_mhc']} TM={r['has_transmembrane']} |")
md.append(f"| Druggability | approved={r['approved_drug_count']} max_phase={r['highest_clinical_phase']} focus_disease_drug={r['any_focus_disease_drug']} focus_disease_drugs={r['focus_disease_drugs'] or '—'} |")
md.append(f"| Tractability | sm_mol={r['tractability_small_molecule'] or '—'} Ab={r['tractability_antibody'] or '—'} |")
md.append(f"| Disease assoc (OT) | any={r['any_disease_assoc']} focus={r['is_focus_disease_associated']} focus_traits={r['focus_disease_traits'] or '—'} max_score={r['max_disease_assoc_score']} |")
md.append(f"| PubMed | total={r['pubmed_total']} focus_disease={r['pubmed_focus_disease']} cell_context={r['pubmed_cell_context']} maturity={r['maturity_tag']} |")
md.append(f"| HPA tissue | tag={r['hpa_tissue_specificity_tag'] or '—'} top={r['hpa_tissue_top_types'] or '—'} |")
md.append(f"| HPA single-cell | tag={r['hpa_cell_specificity_tag'] or '—'} top={r['hpa_cell_top_types'] or '—'} focus_cell_hits={r['hpa_focus_cell_hits'] or '—'} cluster={r['hpa_expression_cluster'] or '—'} |")
md.append(f"| HPA pathology | n_prognostic_cancers={r['hpa_n_prognostic_cancers']} cancer_specificity={r['hpa_cancer_specificity'] or '—'} |")
md.append(f"| DepMap CRISPR | n_screens={r['depmap_n_screens']} mean_geneEffect={r['depmap_mean_gene_effect']} pct_essential={r['depmap_pct_essential']} |")
md.append(f"| gnomAD constraint | LOEUF={r['loeuf']} oe_lof={r['constraint_oe_lof']} top_decile={r['constraint_top_decile']} |")
md.append(f"| ChEMBL tool compounds | target={r['chembl_target_id'] or '—'} best_pIC50={r['chembl_best_pchembl']} best_IC50_nM={r['chembl_best_ic50_nm']} top3={r['chembl_top_compounds'] or '—'} |")
md.append(f"| Component breakdown | drug={r['druggability']} genetics={r['disease_genetics']} tract={r['tractability']} tissue_spec={r['tissue_specificity']} cell_ctx={r['cell_context_score']} ess={r['essentiality_score']} safety={r['safety_constraint_score']} expr={r['expression']} novelty={r['novelty']} over_studied={r['over_studied_penalty']} |")
md.append("")
md.append("**Rationale**: _TO BE FILLED BY CLAUDE — 2–3 sentences. Use prompts/rationale_template.md._")
md.append("")
md.append("**Suggested next step**: _TO BE FILLED BY CLAUDE — 1 concrete sentence (e.g. siRNA knockdown in the relevant cell type; orthogonal IHC; ex-vivo tool-compound challenge; cross-cohort replication)._")
md.append("")
md.append("---\n")
md_path = out_dir / "targets_report.md"
md_path.write_text("\n".join(md), encoding="utf-8")
print(f"aggregate: wrote {md_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""ChEMBL fetcher: per-gene top tool compounds + best IC50.
Two HTTP calls per gene:
1) target/search.json?q=<symbol> → pick the human SINGLE PROTEIN target
2) activity.json?target_chembl_id=<id>&standard_type=IC50&pchembl_value__gte=7
order_by=-pchembl_value → top 5 most potent compounds
Dossier-only: ChEMBL data does not contribute to the composite score. Its
purpose is to surface concrete tool compounds for downstream experimental
validation (read by Claude when writing the 'Suggested next step' slot).
"""
import argparse
import json
import time
import urllib.parse
import urllib.request
from pathlib import Path
CHEMBL = "https://www.ebi.ac.uk/chembl/api/data"
def http_get_json(url: str):
try:
with urllib.request.urlopen(url, timeout=30) as r:
return json.loads(r.read().decode("utf-8"))
except Exception:
return None
def resolve_target(gene: str) -> dict | None:
"""Pick the best human SINGLE PROTEIN ChEMBL target for a gene symbol."""
url = f"{CHEMBL}/target/search.json?{urllib.parse.urlencode({'q': gene, 'limit': 10})}"
data = http_get_json(url)
if not data:
return None
candidates = [
t for t in (data.get("targets") or [])
if t.get("organism") == "Homo sapiens" and t.get("target_type") == "SINGLE PROTEIN"
]
if not candidates:
return None
# Prefer exact pref_name or synonym match to the gene symbol
upper = gene.upper()
for t in candidates:
syns = " ".join(
(cs.get("component_synonym") or "")
for c in (t.get("target_components") or [])
for cs in (c.get("target_component_synonyms") or [])
).upper()
if upper in syns.split() or t.get("pref_name", "").upper() == upper:
return t
return candidates[0]
def fetch_top_compounds(target_id: str, limit: int = 5) -> list[dict]:
params = {
"target_chembl_id": target_id,
"standard_type": "IC50",
"pchembl_value__gte": 7,
"limit": limit,
"order_by": "-pchembl_value",
}
data = http_get_json(f"{CHEMBL}/activity.json?{urllib.parse.urlencode(params)}")
if not data:
return []
out = []
for a in (data.get("activities") or [])[:limit]:
out.append({
"chembl_id": a.get("molecule_chembl_id"),
"pref_name": a.get("molecule_pref_name"),
"pchembl_value": a.get("pchembl_value"),
"ic50_nm": a.get("standard_value"),
"units": a.get("standard_units"),
"assay_id": a.get("assay_chembl_id"),
})
return out
def fetch_one(gene: str) -> dict:
out = {
"gene": gene,
"chembl_target_id": None,
"chembl_target_name": None,
"n_potent_compounds": 0,
"best_pchembl": None,
"best_ic50_nm": None,
"top_compounds": [],
}
target = resolve_target(gene)
if not target:
return out
tid = target.get("target_chembl_id")
out["chembl_target_id"] = tid
out["chembl_target_name"] = target.get("pref_name")
compounds = fetch_top_compounds(tid)
if not compounds:
return out
out["top_compounds"] = compounds
out["n_potent_compounds"] = len(compounds)
try:
out["best_pchembl"] = float(compounds[0]["pchembl_value"])
except (TypeError, ValueError, KeyError):
pass
try:
out["best_ic50_nm"] = float(compounds[0]["ic50_nm"])
except (TypeError, ValueError, KeyError):
pass
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--genes", required=True)
ap.add_argument("--output", required=True)
ap.add_argument("--sleep", type=float, default=0.2)
args = ap.parse_args()
genes = [g.strip() for g in args.genes.split(",") if g.strip()]
result = {}
for i, g in enumerate(genes, 1):
result[g] = fetch_one(g)
if i % 10 == 0:
print(f" chembl: {i}/{len(genes)}", flush=True)
time.sleep(args.sleep)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"chembl: wrote {len(result)} entries to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Human Protein Atlas fetcher: tissue + single-cell + pathology per gene.
Per gene symbol:
1) Resolve symbol -> Ensembl ID via search_download.php
2) Fetch /<ENSG>.json and extract tissue, single-cell, pathology fields
Subcellular localization is NOT fetched here — UniProt is the authoritative
source (see scripts/fetch_uniprot.py). Cell-type / disease focus terms live
in scripts/aggregate.py (FOCUS_CELL_TYPES, FOCUS_DISEASE_TERMS).
"""
import argparse
import gzip
import json
import time
import urllib.parse
import urllib.request
from pathlib import Path
HPA_SEARCH = "https://www.proteinatlas.org/api/search_download.php"
HPA_ENTRY = "https://www.proteinatlas.org/{ensg}.json"
def http_get_json(url: str):
try:
with urllib.request.urlopen(url, timeout=30) as r:
raw = r.read()
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
return json.loads(raw.decode("utf-8"))
except Exception:
return None
def resolve_ensembl(gene: str) -> str | None:
params = {"search": gene, "format": "json", "columns": "g,eg", "compress": "no"}
data = http_get_json(f"{HPA_SEARCH}?{urllib.parse.urlencode(params)}")
if not data:
return None
for row in data:
if row.get("Gene") == gene:
return row.get("Ensembl")
return data[0].get("Ensembl") if data else None
def top_n_by_value(d, n: int = 3) -> list[tuple[str, float]]:
if not isinstance(d, dict):
return []
items = []
for k, v in d.items():
try:
items.append((k, float(v)))
except (TypeError, ValueError):
continue
items.sort(key=lambda x: -x[1])
return items[:n]
def to_float_dict(d) -> dict[str, float]:
if not isinstance(d, dict):
return {}
out = {}
for k, v in d.items():
try:
out[k] = float(v)
except (TypeError, ValueError):
continue
return out
def summarize_pathology(entry: dict) -> dict:
"""Aggregate per-cancer-type 'Cancer prognostics - ...' dicts."""
prognostic = []
for k, v in entry.items():
if not k.startswith("Cancer prognostics - ") or not isinstance(v, dict):
continue
if v.get("is_prognostic"):
prognostic.append({
"cancer": k.replace("Cancer prognostics - ", ""),
"type": v.get("prognostic type", ""),
"p_val": v.get("p_val"),
})
return {
"n_prognostic_cancers": len(prognostic),
"prognostic_top3": prognostic[:3],
"rna_cancer_specificity": entry.get("RNA cancer specificity"),
}
def fetch_one(gene: str) -> dict:
out = {
"gene": gene,
"ensembl_id": None,
"tissue_specificity_tag": None,
"tissue_top_types": [],
"cell_specificity_tag": None,
"cell_top_types": [],
"cell_nCPM_full": {},
"expression_cluster": None,
"pathology": {},
}
ensg = resolve_ensembl(gene)
if not ensg:
return out
out["ensembl_id"] = ensg
entry = http_get_json(HPA_ENTRY.format(ensg=ensg))
if not entry:
return out
out["tissue_specificity_tag"] = entry.get("RNA tissue specificity")
out["tissue_top_types"] = top_n_by_value(entry.get("RNA tissue specific nTPM"))
out["cell_specificity_tag"] = entry.get("RNA single cell type specificity")
cell_nCPM = entry.get("RNA single cell type specific nCPM")
out["cell_nCPM_full"] = to_float_dict(cell_nCPM)
out["cell_top_types"] = top_n_by_value(cell_nCPM)
out["expression_cluster"] = entry.get("Single cell expression cluster")
out["pathology"] = summarize_pathology(entry)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--genes", required=True, help="Comma-separated gene symbols")
ap.add_argument("--output", required=True)
ap.add_argument("--sleep", type=float, default=0.15)
args = ap.parse_args()
genes = [g.strip() for g in args.genes.split(",") if g.strip()]
result = {}
for i, g in enumerate(genes, 1):
result[g] = fetch_one(g)
if i % 10 == 0:
print(f" hpa: {i}/{len(genes)}", flush=True)
time.sleep(args.sleep)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"hpa: wrote {len(result)} entries to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""OpenTargets GraphQL fetcher: druggability + approved drugs per gene.
Queries by gene symbol → resolves Ensembl ID → drug + tractability summary.
Customize FOCUS_DISEASE_TERMS for your project. Examples:
- Autoimmunity: ("crohn", "ulcerative colitis", "inflammatory bowel")
- Oncology: ("cancer", "carcinoma", "lymphoma", "leukemia", "tumor")
- Neurodegen: ("alzheimer", "parkinson", "huntington", "als")
- Metabolic: ("diabetes", "obesity", "fatty liver", "nash")
A drug whose indication list contains any of these substrings is tagged
in `focus_disease_drugs` and `any_focus_disease_drug = True`.
"""
import argparse
import json
import time
import urllib.request
from pathlib import Path
OT_URL = "https://api.platform.opentargets.org/api/v4/graphql"
ID_QUERY = """
query Resolve($q: String!) {
search(queryString: $q, entityNames: ["target"], page: {index: 0, size: 1}) {
hits { id name entity }
}
}
"""
TARGET_QUERY = """
query Target($id: String!) {
target(ensemblId: $id) {
id approvedSymbol approvedName biotype
tractability { modality value label }
drugAndClinicalCandidates {
count
rows {
id
maxClinicalStage
drug { id name maximumClinicalStage drugType mechanismsOfAction { rows { mechanismOfAction } } }
diseases { diseaseFromSource disease { id name } }
}
}
associatedDiseases(page: {index:0, size:10}) {
rows { disease { id name } score }
}
depMapEssentiality {
tissueName
screens { cellLineName geneEffect }
}
geneticConstraint {
constraintType
oe
oeUpper
upperBin
}
}
}
"""
PHASE_MAP = {
"APPROVAL": 4, "PHASE_IV": 4, "PHASE_4": 4,
"PHASE_III": 3, "PHASE_3": 3,
"PHASE_II": 2, "PHASE_2": 2,
"PHASE_I": 1, "PHASE_1": 1,
"EARLY_PHASE_1": 1, "PRECLINICAL": 0,
"WITHDRAWN": -1, "UNKNOWN": 0, "": 0,
}
def stage_to_phase(stage: str | None) -> int:
if not stage:
return 0
return PHASE_MAP.get(stage.strip().upper(), 0)
def is_approved_stage(stage: str | None) -> bool:
return bool(stage and stage.strip().upper() in {"APPROVAL", "PHASE_IV", "PHASE_4"})
# Per-modality tractability label priority (highest tier first).
TRACTABILITY_PRIORITY = [
"Approved Drug", "Advanced Clinical", "Phase 1 Clinical",
"Structure with Ligand", "High-Quality Ligand", "High-Quality Pocket",
"Med-Quality Pocket", "Druggable Family",
"UniProt loc high conf", "GO CC high conf", "UniProt loc med conf",
"UniProt SigP or TMHMM", "GO CC med conf", "Human Protein Atlas loc",
"Small Molecule Binder", "Literature",
]
def best_label(entries: list[dict]) -> str | None:
"""Among entries whose value is True, return the highest-priority label."""
true_labels = {e.get("label") for e in entries if e.get("value")}
for lbl in TRACTABILITY_PRIORITY:
if lbl in true_labels:
return lbl
return None
# Lowercased substrings — a drug-indication string that contains any of
# these is tagged as a focus-disease drug. Override for your project.
FOCUS_DISEASE_TERMS = ("crohn", "ulcerative colitis", "inflammatory bowel")
def gql(query: str, variables: dict) -> dict:
body = json.dumps({"query": query, "variables": variables}).encode()
req = urllib.request.Request(
OT_URL, data=body,
headers={"Content-Type": "application/json", "Accept": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode())
except Exception as e:
return {"errors": [str(e)]}
def fetch_one(gene: str) -> dict:
out = {
"gene": gene,
"ensembl_id": None,
"approved_symbol": None,
"biotype": None,
"tractability_small_molecule": None,
"tractability_antibody": None,
"tractability_other": [],
"approved_drug_count": 0,
"approved_drugs": [],
"highest_clinical_phase": 0,
"any_focus_disease_drug": False,
"focus_disease_drugs": [],
"associated_diseases_top5": [],
"depmap_n_screens": 0,
"depmap_n_tissues": 0,
"depmap_mean_gene_effect": None,
"depmap_pct_essential": 0.0,
"loeuf": None,
"constraint_oe_lof": None,
"constraint_top_decile": False,
}
# resolve
r = gql(ID_QUERY, {"q": gene})
hits = (r.get("data") or {}).get("search", {}).get("hits", []) or []
if not hits:
return out
ensembl_id = hits[0]["id"]
out["ensembl_id"] = ensembl_id
out["approved_symbol"] = hits[0]["name"]
r = gql(TARGET_QUERY, {"id": ensembl_id})
tgt = (r.get("data") or {}).get("target")
if not tgt:
return out
out["biotype"] = tgt.get("biotype")
by_modality: dict[str, list[dict]] = {}
for tr in tgt.get("tractability", []) or []:
by_modality.setdefault((tr.get("modality") or "").upper(), []).append(tr)
out["tractability_small_molecule"] = best_label(by_modality.get("SM", []))
out["tractability_antibody"] = best_label(by_modality.get("AB", []))
for mod in ("PR", "OC"):
lbl = best_label(by_modality.get(mod, []))
if lbl:
out["tractability_other"].append(f"{mod}:{lbl}")
kd = tgt.get("drugAndClinicalCandidates") or {}
drugs_seen: dict[str, dict] = {}
for row in kd.get("rows", []) or []:
drug = row.get("drug") or {}
name = drug.get("name")
if not name:
continue
# Row-level max stage and drug-level max stage; take the larger
row_phase = stage_to_phase(row.get("maxClinicalStage"))
drug_phase = stage_to_phase(drug.get("maximumClinicalStage"))
ph = max(row_phase, drug_phase)
approved = is_approved_stage(row.get("maxClinicalStage")) or is_approved_stage(drug.get("maximumClinicalStage"))
out["highest_clinical_phase"] = max(out["highest_clinical_phase"], ph)
d = drugs_seen.setdefault(name, {
"name": name,
"approved": approved,
"max_phase": ph,
"mechanisms": set(),
"diseases": set(),
})
d["max_phase"] = max(d["max_phase"], ph)
if approved:
d["approved"] = True
moa = ((drug.get("mechanismsOfAction") or {}).get("rows") or [])
for m in moa:
if m.get("mechanismOfAction"):
d["mechanisms"].add(m["mechanismOfAction"])
for dis_item in (row.get("diseases") or []):
dis = (dis_item.get("disease") or {}).get("name") or dis_item.get("diseaseFromSource")
if not dis:
continue
d["diseases"].add(dis)
if any(t in dis.lower() for t in FOCUS_DISEASE_TERMS):
out["any_focus_disease_drug"] = True
if name not in out["focus_disease_drugs"]:
out["focus_disease_drugs"].append(name)
approved = [d for d in drugs_seen.values() if d["approved"]]
out["approved_drug_count"] = len(approved)
out["approved_drugs"] = [
{"name": d["name"], "max_phase": d["max_phase"],
"mechanisms": sorted(d["mechanisms"]), "diseases": sorted(d["diseases"])[:5]}
for d in approved[:10]
]
for row in (tgt.get("associatedDiseases") or {}).get("rows", []) or []:
out["associated_diseases_top5"].append({
"name": (row.get("disease") or {}).get("name"),
"score": row.get("score"),
})
# DepMap CRISPR essentiality — geneEffect < 0 means KO reduces fitness.
# We summarize across all screened cell lines; pan-essentials (>80%) and
# never-essentials (<5%) both flag low-priority targets via the cap.
effects: list[float] = []
tissues = tgt.get("depMapEssentiality") or []
for tissue in tissues:
for s in tissue.get("screens") or []:
ge = s.get("geneEffect")
if isinstance(ge, (int, float)):
effects.append(float(ge))
if effects:
out["depmap_n_screens"] = len(effects)
out["depmap_n_tissues"] = len(tissues)
out["depmap_mean_gene_effect"] = sum(effects) / len(effects)
out["depmap_pct_essential"] = sum(1 for e in effects if e < -0.5) / len(effects)
# gnomAD-derived constraint (loaded from OT's geneticConstraint table).
# The "lof" row carries LOEUF (oeUpper) — lower = more constrained.
for row in tgt.get("geneticConstraint") or []:
if row.get("constraintType") == "lof":
out["loeuf"] = row.get("oeUpper")
out["constraint_oe_lof"] = row.get("oe")
out["constraint_top_decile"] = (row.get("upperBin") == 1)
break
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--genes", required=True)
ap.add_argument("--output", required=True)
ap.add_argument("--sleep", type=float, default=0.2)
args = ap.parse_args()
genes = [g.strip() for g in args.genes.split(",") if g.strip()]
result = {}
for i, g in enumerate(genes, 1):
result[g] = fetch_one(g)
if i % 10 == 0:
print(f" opentargets: {i}/{len(genes)}", flush=True)
time.sleep(args.sleep)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"opentargets: wrote {len(result)} entries to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""PubMed E-utilities fetcher: total paper count + two configurable
context counts (disease focus + cell / lineage focus) + 5 most-recent
focus-disease PMIDs.
Customize CONTEXTS for your project. Examples:
- Autoimmunity: '... AND ("inflammatory bowel"[tiab] OR "Crohn"[tiab] OR "ulcerative colitis"[tiab])'
- Oncology: '... AND ("cancer"[tiab] OR "tumor"[tiab] OR "neoplasm"[tiab])'
- Neurodegen: '... AND ("Alzheimer"[tiab] OR "Parkinson"[tiab] OR "neurodegeneration"[tiab])'
- Cell context: '"T cell"', '"macrophage"', '"hepatocyte"', '"neuron"', etc.
"""
import argparse
import json
import time
import urllib.parse
import urllib.request
from pathlib import Path
ESEARCH = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
CONTEXTS = {
"total": '"{gene}"[tiab]',
"focus_disease": '"{gene}"[tiab] AND ("inflammatory bowel disease"[tiab] OR "Crohn"[tiab] OR "ulcerative colitis"[tiab] OR ibd[tiab])',
"cell_context": '"{gene}"[tiab] AND ("T cell"[tiab] OR "T-cell"[tiab] OR "T lymphocyte"[tiab])',
}
def esearch(term: str, retmax: int = 0) -> dict:
params = {"db": "pubmed", "term": term, "retmode": "json", "retmax": str(retmax), "sort": "pub_date"}
url = f"{ESEARCH}?{urllib.parse.urlencode(params)}"
try:
with urllib.request.urlopen(url, timeout=20) as r:
return json.loads(r.read().decode())
except Exception as e:
return {"error": str(e)}
def fetch_one(gene: str) -> dict:
out = {
"gene": gene,
"pubmed_total": 0,
"pubmed_focus_disease": 0,
"pubmed_cell_context": 0,
"maturity_tag": None,
"recent_focus_disease_pmids": [],
}
for key, tmpl in CONTEXTS.items():
term = tmpl.format(gene=gene)
retmax = 5 if key == "focus_disease" else 0
data = esearch(term, retmax=retmax)
try:
count = int(data.get("esearchresult", {}).get("count", 0))
except Exception:
count = 0
out[f"pubmed_{key}"] = count
if key == "focus_disease":
out["recent_focus_disease_pmids"] = data.get("esearchresult", {}).get("idlist", [])[:5]
time.sleep(0.34) # NCBI rate limit: 3/sec without API key
total = out["pubmed_total"]
if total < 5:
out["maturity_tag"] = "uncharted"
elif total < 30:
out["maturity_tag"] = "novel"
elif total < 100:
out["maturity_tag"] = "moderate"
elif total < 500:
out["maturity_tag"] = "well_studied"
else:
out["maturity_tag"] = "saturated"
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--genes", required=True)
ap.add_argument("--output", required=True)
args = ap.parse_args()
genes = [g.strip() for g in args.genes.split(",") if g.strip()]
result = {}
for i, g in enumerate(genes, 1):
result[g] = fetch_one(g)
if i % 5 == 0:
print(f" pubmed: {i}/{len(genes)}", flush=True)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"pubmed: wrote {len(result)} entries to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""UniProt fetcher: per gene → subcellular localization, surface/secreted flag,
MHC flag, coding status, protein class.
Uses UniProt REST search endpoint with human + reviewed filter."""
import argparse
import json
import re
import time
import urllib.parse
import urllib.request
from pathlib import Path
UNIPROT_URL = "https://rest.uniprot.org/uniprotkb/search"
FIELDS = "accession,gene_names,protein_name,cc_subcellular_location,protein_existence,keyword,ft_topo_dom,ft_transmem,ft_signal,cc_function"
MHC_PATTERNS = [
re.compile(r"^HLA-[A-Z]{1,3}$", re.I),
re.compile(r"^B2M$", re.I),
re.compile(r"^TAP[12]$", re.I),
re.compile(r"^TAPBP$", re.I),
re.compile(r"^CD1[A-E]?$", re.I),
]
def is_mhc(gene: str) -> bool:
return any(p.match(gene) for p in MHC_PATTERNS)
def fetch_one(gene: str) -> dict:
"""Query UniProt for a single human gene; return parsed fields."""
query = f'(gene:{gene}) AND (organism_id:9606) AND (reviewed:true)'
params = {"query": query, "fields": FIELDS, "format": "json", "size": "1"}
url = f"{UNIPROT_URL}?{urllib.parse.urlencode(params)}"
try:
with urllib.request.urlopen(url, timeout=30) as r:
data = json.loads(r.read().decode())
except Exception as e:
return {"gene": gene, "error": str(e)}
out = {
"gene": gene,
"uniprot_id": None,
"protein_name": None,
"subcellular_location": [],
"is_surface": False,
"is_secreted": False,
"is_mhc": is_mhc(gene),
"is_coding": True,
"has_transmembrane": False,
"has_signal_peptide": False,
"keywords": [],
"protein_existence": None,
"function_short": None,
}
results = data.get("results") or []
if not results:
return out
rec = results[0]
out["uniprot_id"] = rec.get("primaryAccession")
name = rec.get("proteinDescription", {}).get("recommendedName", {}).get("fullName", {}).get("value")
out["protein_name"] = name
out["protein_existence"] = rec.get("proteinExistence")
# Subcellular location
for cmt in rec.get("comments", []):
if cmt.get("commentType") == "SUBCELLULAR LOCATION":
for loc in cmt.get("subcellularLocations", []):
v = loc.get("location", {}).get("value")
if v:
out["subcellular_location"].append(v)
if cmt.get("commentType") == "FUNCTION" and not out["function_short"]:
for t in cmt.get("texts", []):
if t.get("value"):
out["function_short"] = t["value"][:240]
break
loc_lc = " | ".join(out["subcellular_location"]).lower()
if any(s in loc_lc for s in ("cell membrane", "plasma membrane", "cell surface")):
out["is_surface"] = True
if any(s in loc_lc for s in ("secreted", "extracellular")):
out["is_secreted"] = True
# Features
for ft in rec.get("features", []):
if ft.get("type") == "Transmembrane":
out["has_transmembrane"] = True
if ft.get("type") == "Signal":
out["has_signal_peptide"] = True
if out["has_transmembrane"]:
out["is_surface"] = True
if out["has_signal_peptide"] and not out["is_surface"]:
out["is_secreted"] = True
out["keywords"] = [k.get("name") for k in rec.get("keywords", []) if k.get("name")][:20]
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--genes", required=True, help="Comma-separated gene symbols")
ap.add_argument("--output", required=True)
ap.add_argument("--sleep", type=float, default=0.1)
args = ap.parse_args()
genes = [g.strip() for g in args.genes.split(",") if g.strip()]
result = {}
for i, g in enumerate(genes, 1):
result[g] = fetch_one(g)
if i % 20 == 0:
print(f" uniprot: {i}/{len(genes)}", flush=True)
time.sleep(args.sleep)
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"uniprot: wrote {len(result)} entries to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Orchestrator: read gene list, dispatch fetchers in parallel, then call aggregate."""
import argparse
import csv
import json
import os
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent
DEFAULT_TOP = 50
FETCHERS = [
("uniprot", SCRIPTS_DIR / "fetch_uniprot.py"),
("opentargets", SCRIPTS_DIR / "fetch_opentargets.py"),
("pubmed", SCRIPTS_DIR / "fetch_pubmed.py"),
("hpa", SCRIPTS_DIR / "fetch_hpa.py"),
("chembl", SCRIPTS_DIR / "fetch_chembl.py"),
]
def read_gene_list(path: Path, gene_col: str, top: int) -> list[str]:
genes: list[str] = []
with open(path, encoding="utf-8") as f:
first = f.readline().strip()
delim = "," if "," in first else ("\t" if "\t" in first else None)
if delim is None:
# plain text, one gene per line
symbol = first.strip()
if symbol and symbol.lower() not in {"gene", "symbol", "genes", "gene_symbol"}:
genes.append(symbol)
for line in f:
s = line.strip()
if s:
genes.append(s)
else:
header = [c.strip() for c in first.split(delim)]
if gene_col in header:
idx = header.index(gene_col)
else:
# fall back to first column
idx = 0
reader = csv.reader(f, delimiter=delim)
# if header wasn't really a header (single column matches a known gene), include it
if header[idx].lower() not in {"gene", "symbol", "genes", "gene_symbol", "feature"}:
genes.append(header[idx])
for row in reader:
if row and len(row) > idx and row[idx].strip():
genes.append(row[idx].strip())
# de-dup preserving order
seen = set()
unique = []
for g in genes:
if g not in seen:
seen.add(g)
unique.append(g)
return unique[:top]
def run_fetcher(name: str, script: Path, genes: list[str], out_dir: Path) -> tuple[str, bool, str]:
out_file = out_dir / "raw_data" / f"{name}.json"
cmd = [
sys.executable, str(script),
"--genes", ",".join(genes),
"--output", str(out_file),
]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
ok = proc.returncode == 0 and out_file.exists()
msg = proc.stderr.strip().splitlines()[-1] if proc.stderr.strip() else proc.stdout.strip()[:200]
return name, ok, msg
except subprocess.TimeoutExpired:
return name, False, "TIMEOUT (>600s)"
except Exception as e:
return name, False, f"ERROR: {e}"
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--input", required=True, help="CSV/TSV/TXT with gene symbols")
ap.add_argument("--output", required=True, help="Output directory")
ap.add_argument("--gene-col", default="gene", help="Column name for gene symbols (default: gene)")
ap.add_argument("--top", type=int, default=DEFAULT_TOP, help=f"Top-N input genes to process (default: {DEFAULT_TOP})")
ap.add_argument("--weights", default=str(SCRIPTS_DIR.parent / "weights.yaml"), help="Path to weights.yaml")
args = ap.parse_args()
inp = Path(args.input).expanduser().resolve()
out_dir = Path(args.output).expanduser().resolve()
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "raw_data").mkdir(exist_ok=True)
genes = read_gene_list(inp, args.gene_col, args.top)
print(f"[orchestrate] Read {len(genes)} genes from {inp.name}")
(out_dir / "raw_data" / "genes.json").write_text(json.dumps(genes, indent=2))
# Run fetchers in parallel
results = {}
with ThreadPoolExecutor(max_workers=len(FETCHERS)) as pool:
futures = {pool.submit(run_fetcher, name, script, genes, out_dir): name
for name, script in FETCHERS}
for fut in as_completed(futures):
name, ok, msg = fut.result()
results[name] = ok
status = "ok " if ok else "FAIL"
print(f"[orchestrate] {status} {name:14s} {msg}")
# Aggregate
agg = SCRIPTS_DIR / "aggregate.py"
cmd = [
sys.executable, str(agg),
"--raw-dir", str(out_dir / "raw_data"),
"--output-dir", str(out_dir),
"--weights", args.weights,
"--input-csv", str(inp) if inp.suffix.lower() in {".csv", ".tsv"} else "",
]
proc = subprocess.run(cmd, capture_output=False)
if proc.returncode != 0:
print(f"[orchestrate] aggregate.py failed (exit {proc.returncode})", file=sys.stderr)
sys.exit(proc.returncode)
print(f"[orchestrate] Done. See {out_dir / 'targets_report.md'} and {out_dir / 'targets_summary.csv'}")
if __name__ == "__main__":
main()
# Composite score weights for target-prioritization.
# Each component is normalized to [0, 1] before weighting.
# Edit and re-run aggregate.py to re-score without re-fetching APIs.
weights:
druggability_score: 0.20 # approved drugs, OpenTargets tractability
disease_genetics_score: 0.15 # OpenTargets associated diseases (incl. GWAS-derived signal), with focus-disease bonus
tractability_bonus: 0.15 # surface or secreted protein vs intracellular
tissue_specificity: 0.10 # HPA tissue specificity tag → therapeutic-window safety
cell_context_score: 0.10 # HPA single-cell nCPM rank in FOCUS_CELL_TYPES → efficacy in target population
essentiality_score: 0.10 # DepMap CRISPR % essential (selective sweet spot, pan-essential capped)
safety_constraint_score: 0.05 # gnomAD LOEUF — high LOEUF = LoF tolerated in humans → safer to inhibit
expression_score: 0.10 # from input DE table (mean expression in the contrast)
novelty_bonus: 0.05 # moderately studied genes preferred over uncharted / over-studied
over_studied_penalty: 0.10 # subtracted if pubmed total > cap
caps:
pubmed_total_for_maturity: 100 # above this, over_studied_penalty kicks in
pubmed_well_studied_floor: 5 # below this, novelty_bonus deflates (no evidence at all is bad)
flags:
focus_disease_assoc_bonus: 0.5 # added to disease_genetics_score if the gene is OT-associated with the focus disease
approved_drug_bonus: 0.7 # raises druggability_score floor to this if any approved drug
surface_protein_bonus: 1.0 # tractability_bonus value if surface
secreted_protein_bonus: 0.8 # tractability_bonus value if secreted
intracellular_default: 0.3 # tractability_bonus value if intracellular only
# Focus-disease + focus-cell-type + cell-context terms are configured in code, not here:
# - edit FOCUS_DISEASE_TERMS in scripts/fetch_opentargets.py + scripts/aggregate.py
# - edit FOCUS_CELL_TYPES in scripts/aggregate.py (must match HPA's exact cell-type strings)
# - edit CONTEXTS in scripts/fetch_pubmed.py
# Default terms target an autoimmunity (IBD) + T-cell example; see those files for
# alternative configurations (oncology, neurodegeneration, metabolic, ...).
Related skills
How it compares
Use target-prioritization for ranking competing ideas before build; switch to scoping or prototyping skills once a single target is selected.
FAQ
What does target-prioritization do?
Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR essentiality + gnomAD constraint), PubMed, the
When should I use target-prioritization?
Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR essentiality + gnomAD constraint), PubMed, the
What are common prerequisites?
--- name: target-prioritization description: Prioritize drug targets from a ranked gene list (e.g., scRNA-seq DE output) by orchestrating parallel API queries against UniProt, OpenTargets (with integrated DepMap CRISPR e