A whole-picture view of a drug target, from biology and clinical development to competition and transactions.
Open the site · built on Open Targets, ChEMBL, ClinicalTrials.gov, UniProt and Europe PMC · no API key, no model, nothing to sign up for.
Target Landscape is built for people making early decisions around a drug target: scientists asking whether a target is worth developing, or where an opportunity may still exist; early-stage biotech investors evaluating a new company or programme; and BD or licensing teams screening targets and assets before deeper diligence.
The problem is rarely a lack of information. It is knowing what information matters for the decision you are trying to make. Target data is spread across genetics, biology, clinical trials, drug programmes, failures, competitive pipelines and publications, and even where it is all publicly available, an early reviewer may not know which questions need answering, which evidence is decision-relevant, or where to find it among hundreds of records.
Target Landscape starts from the questions behind the decision, not from the databases. For each target, it asks a structured set of six questions, three on whether the target works and three on what can be had here:
| Target characteristics | What the protein is, where it sits, and how far anyone has got with it | science |
| Trial design and readouts | How much of the running activity is a controlled efficacy trial, and how much only looks like one | science |
| Termination reasons | Why programmes stopped, and what that does and does not say about the target | science |
| Competitive landscape | Who is developing what, grouped by mechanism, phase-weighted rather than counted | business |
| Licensing and deals | What has been licensed, on whose announcement, and which programmes have never been on a deal | business |
| Untried | A modality that could reach this protein and has not been tried, or a disease with evidence and nothing in the clinic | business |
Each question is paired with the specific evidence needed to examine it, rather than an undifferentiated pile of records — and wherever possible, the underlying record, its source and its date stay visible, so the evidence can be traced back to where it came from.
Target Landscape is designed to support judgement, not replace it. It does not tell users whether to develop, invest in or license a target. Instead, it helps them understand what needs to be considered, brings the relevant evidence together, and makes that evidence easier to inspect and challenge.
pip install -r requirements-web.txt
uvicorn web.app:app --reload # → http://127.0.0.1:8000On Windows, double-click 1-SETUP.bat then 2-START-WEBSITE.bat — no
terminal required. See WINDOWS.md.
The same analysis runs as a command line tool and as a document:
python -m landscape KRAS # → out/KRAS_landscape.htmlEach target page opens with the six answers above — one sentence of judgement per question, at most three lines of evidence, the table behind a disclosure. No summary strip of raw counts: a number with no verdict attached hands the reader the work this page exists to do. Raw data is linked out to Open Targets, ClinicalTrials.gov and Europe PMC rather than copied — they do tables better than this page can.
The home page shows what a target page holds instead of describing it: six tiles carrying the real figures from a real target, derived from the same stored landscape the target page renders.
Then the views — one filter state drives all of them, so narrowing to antibodies narrows the mechanism list, the trial table and the termination notices together:
| View | Answers |
|---|---|
| Assets | Every programme, sortable, with a panel per asset showing its trials and the source of each field |
| Mechanisms | Grouped by what each asset does to the target — plus where the target sits on its pathway, its drugged neighbours, and which modalities could reach it and have not been tried |
| Trials | What reads out next and what each result could actually prove, from the registered design |
| Terminations | The sponsor's own stated reason, classified and quoted |
| Licensing | Deals already done, each with its source — then every programme on the target and whether one is on file for it |
| Untried | Well-evidenced diseases with nothing in the clinic, and modalities nobody has taken |
| Reading | The most-cited reviews and recent mechanism work, from Europe PMC |
Terminations — the sponsor's own words, classified, with the matched phrase shown so you can overrule the call:
Licensing — what has been transacted and what has not, no score on either:
Investor / Scientist reorders the page — it never hides anything. In Investor mode the six-question read leads and the mechanism and pathway view is one tab away; in Scientist mode the mechanism leads and the six-question read is one tab away. Every figure is reachable from both — the toggle changes what comes first, not what exists.
Search finds a target under whatever name you actually use for it, not
just its official symbol. Type BAFF-R and it resolves to TNFRSF13C; type
HER2 and it resolves to ERBB2 — any alias in the index works the same way,
not only the two shown here. Instant and offline, because the alias list
ships in the repo rather than living behind a search API.
python scripts/seed_target_index.py # ~200 hand-checked drug targets, ships in the repo
python scripts/build_target_index.py # expands it to every approved human gene (HGNC)Two tiers, and the page always says which one you're reading. Curated targets are precomputed and committed: instant, and cannot fail, because serving one touches no external API. Anything else is built live from the same public sources — usually 20 to 40 seconds — and cached once built.
python scripts/precompute.py --set calibration/targets.txt # seed the curated tier
python scripts/recompute_curated.py # re-run current rules, no networkTwo deployments run from this repository, and they are genuinely different, not two hosts for the same file.
Static — GitHub Pages. Every curated target is precomputed and
committed, so serving the page touches no server and no external API:
make_static_demo.py --out-dir site writes a shell index.html plus one
JSON file per curated target, and .github/workflows/pages.yml rebuilds
that from a clean checkout on every push, so what is online is what the
repository can rebuild. Looking up a target outside the curated set opens
onto a page that says so, rather than a search it can't honour.
Live — Vercel. The same FastAPI app (web/app.py) runs as a Python
serverless function (api/index.py, configured in vercel.json), so any of
the roughly 28,000 indexed human genes can be looked up, not only the
curated set. A lookup outside the curated tier triggers a live build against
Open Targets, ChEMBL and ClinicalTrials.gov — 20 to 40 seconds, run inside
the one request rather than polled as a background job, since a serverless
function does not keep a process running between requests the way a normal
server does.
python scripts/make_static_demo.py --out-dir site # a shell plus one file per target — what GitHub Pages runs
python scripts/make_static_demo.py --out out/demo.html # one self-contained file, for sending to someoneTwo halves: getting a defensible record out of five public APIs, and deciding what may be said about it.
Target identity and biology. Open Targets resolves the symbol to an Ensembl gene ID, and every other source is joined on that ID rather than on a name. UniProt supplies the function brief, because its FUNCTION comments are written by curators reading the primary literature and carry their own evidence citations rather than being text-mined. Nothing in the pipeline is ever matched on a protein's descriptive name.
Drug programmes. Open Targets' known-drugs list, merged with ChEMBL
queried directly for the three fields Open Targets flattens away —
action_type, molecule_type and withdrawn_flag. Identity is the ChEMBL ID
where both sources have one, otherwise a normalised key built from every known
synonym; phase is the maximum across sources, because ChEMBL's max_phase
lags a Phase 3 already registered on ClinicalTrials.gov. The same layer folds
salt forms of one drug, catches ChEMBL synonym collisions that would otherwise
merge two distinct molecules, and drops rows that were never drugs —
diagnostic assays, biomarker cohorts, comparator arms ("investigator's
choice"), and class labels like "PD-1/PD-L1 inhibitor", which is a protocol
letting the investigator pick any drug in the class, not a molecule.
Trials. ClinicalTrials.gov v2, in two passes: the trials Open Targets has already linked to a drug, plus a free-text sweep over the symbol, its real aliases and every known drug name. The sweep is where the programmes no drug database has caught up with live — academic INDs, Chinese and Japanese sponsors, cell therapies — and on many targets that is a third of the real landscape.
It is also where the whole thing nearly broke. Searching a protein's full descriptive name against a registry whose intervention search is stemmed returned, for MAP3K14, 228 assets and 13 "approved drugs" — the first of them hydrochlorothiazide, out of a study called "Salt Loading and Thiazide Intervention Study", because a phrase that long matches almost anything. Two rules came out of that, and the second is the one that matters. Only symbols, real aliases and known drug names are sent to the registry. And whatever comes back earns its place only if the trial's own text names the target or one of its drugs — matched on word boundaries across title, interventions and the sponsor's brief summary, with words that appear in half the proteins in the genome ("protein", "receptor", "kinase") excluded from counting as a match. Population context in a condition field ("HER2-positive") is scored separately from the intervention field, so a phrase cannot poison a neighbouring one. A trial that names nothing is dropped: a missing trial makes the picture thin, an invented one makes it wrong.
Termination notices. whyStopped, from those same records — the sponsor's
own one-line account of why a trial stopped. It is the highest-value free
field in competitive intelligence and almost nothing uses it, because it is
unstructured text behind a paginated API.
Deals. Entered by hand, one CSV per target, every row requiring a source URL to the parties' own announcement. No free, redistributable database of deal terms exists at any price — the subscription platforms cover this and their licences forbid republishing it. Two further layers are hand-maintained on the same terms: programmes the registries name only by a code, and where a target sits on its pathway, cited to UniProt.
Literature. Europe PMC, in two queries: the most-cited reviews, and recent mechanism work. Ranked by citation count rather than date, because the most-cited review on a target is close to the field's own consensus summary and the newest one is not.
The rules above decide what is in the table. These decide what the page is allowed to say about it.
A partial fact is stated as partial. Where a record says what a drug does
but not what kind of molecule it is, the label reads inhibition — modality
unresolved, rather than discarding the half that is known or guessing the
half that is not. Modality itself is read from WHO INN stems first (-mab,
-cept, -leucel, -siran) — deterministic, and the only signal that works
on an asset with no database record at all — and from mechanism text second.
Absence is stated. No data is not. If a target has trials and none of them
stopped, the read says so: None stopped · 22 trials on record. If there is no
trial at all, that line is not shown, because there is nothing for it to be a
statement about. The same distinction holds throughout: a missing whyStopped
is reported as unstated, never as zero, and the share of stopped trials that
gave any reason is printed beside the classification.
Every classification shows the words it matched, so it can be overruled. Terminations are classified from the sponsor's own text into five causes (efficacy/safety/PK, business, operational, manufacturing, unstated), with the verbatim notice kept beside the call. The hard part is negation, and the discriminator is grammatical rather than lexical: a negated noun ("no new safety signals were identified") is a reassurance and is masked out, while a negated achievement verb ("did not meet the target criteria for progression") is the failure statement itself and is read before masking. Novartis's BAFF-R Phase 2b notice contains both sentences. A keyword scan sees "safety" and files it as a toxicity failure — the opposite of what happened. This reads it as a portfolio decision and shows that it also matched efficacy, so the reader can take the call apart instead of inheriting it.
A word that came out of a threshold is printed with its threshold. "Contested" is a band over a phase-weighted score, not a count: one Phase 3 competitor outweighs four Phase 1s, and programmes whose every trial has stopped are reported as dormant rather than folded into the live count, so a target is not read as crowded on the strength of five abandoned attempts. The weights and the band edges are on the page, because a number nobody can recompute from what is shown is an opinion with decimals.
A number is shown with the set it is over. "6 of 16 running trials are
controlled efficacy studies", not "6 controlled trials". Across modules,
consistency.py asserts the relations that have to hold between figures
derived separately — an approved-drug count from one module against an asset
table from another — so a drift is caught on the next refresh rather than by a
reader noticing that two numbers disagree.
Where a claim needs three things, two is not enough. A disease is called untried only when its association clears a threshold and carries independent genetic support and has nothing past preclinical; any one of the three missing and it is dropped rather than counted as whitespace. Disease names are compared by stemmed token overlap, because substring matching fails on the normal case ("Sjogren syndrome" and "Sjogren's Disease" share no substring). Even then the section is labelled a screen to check by hand, not a discovery.
Nothing is compressed into a score where the score would be false precision. Licensing is two lists — deals done, each with its announcement linked, and every programme with its holder, stage and readout — because "how available is this target" is two questions with two different answers. An earlier version had an availability score and a median deal value. Both are gone: the weights behind the score had never been checked against a real negotiation, and a median over a handful of hand-typed rows is a sample size a reader should reject on sight.
No verdict sentence is generated. There was a function here that wrote one line summarising what the six answers added up to. It was deleted rather than left unused, because a verdict generator sitting in a codebase is one refactor away from being called again.
No model runs any of this. Every figure above is a rule over a public record — reproducible offline, and traceable line by line to the record that produced it. The one place a model would genuinely help is reading free text at scale, and the shape for that is kept offline too: the model drafts candidates, a person confirms them, and the result is committed as a sourced CSV like the other hand-maintained layers. The pipeline that actually runs never depends on one.
Python 3.9+. The pipeline itself needs nothing beyond the standard library.
git clone https://github.com/lfr53/Target-Landscape.git
cd Target-Landscape
python -m landscape doctor # check the APIs before trusting a run
python -m landscape KRAS # live run against the public APIs
python -m landscape KRAS ERBB2 PDCD1 --out out/ # several targets, plus an index comparing them
python -m landscape --fixture fixtures/TNFRSF13C.json # offline demo, no network
python -m landscape --calibrate --out out/calibration # reference set, one density scale
python -m landscape KRAS \
--out out/ \
--format html,md,json \
--deals data/deals/KRAS.csv \
--save-fixture fixtures/KRAS.json
python -m unittest discover -s . -p "test_*.py"The three public APIs rename fields between releases with no warning. The
first symptom is not a crash — it's a memo that renders perfectly and says
nothing. doctor probes each source with the pipeline's own queries and
names the field that moved:
Open Targets
[ ok ] reachable
[ ok ] resolves a gene symbol
[ FAIL ] target core fields
empty fields: tractability
→ update _TARGET_QUERY in sources/opentargets.py
Responses are cached under ~/.cache/target-landscape, so re-running a
target is instant and doesn't re-hit the APIs.
from landscape import build, to_html
ls = build("KRAS")
print(ls.crowding["verdict"], ls.crowding["weighted_score"])
for a in ls.assets[:5]:
print(a.name, a.phase_label, a.mechanism_class, a.sponsor)
open("memo.html", "w").write(to_html(ls))| Source | Carries | Licence |
|---|---|---|
| Open Targets Platform | target identity, tractability, known drugs, disease associations with genetic evidence | CC0 |
| ChEMBL | action_type, molecule type, max phase, withdrawal flags |
CC BY-SA 3.0 |
| ClinicalTrials.gov API v2 | trials, phases, sponsors, conditions, geography, whyStopped |
US Government public domain |
| UniProt | curated function, subcellular location, domains | CC BY 4.0 |
| Europe PMC | reviews and recent mechanism papers | per-record |
Three layers are maintained by hand, each row requiring a source URL:
data/deals/*.csv (deal terms, from the parties' own announcements),
data/assets/*.csv (programmes the registries name only by a code), and
data/pathways.csv (where a target sits, cited to UniProt). Subscription
databases aren't used — their licences forbid redistributing what they
contain.
Stated here and on every page the tool produces:
- The published site carries the curated set. Anything else opens onto a page saying it isn't built. Running the project locally builds any human gene from the same public sources.
- Registry coverage. Only ClinicalTrials.gov is swept — China- and Japan-only registrations are under-represented.
- Preclinical and undisclosed programmes are invisible.
- The deal layer is small and hand-entered — sourced rows, not a market sample, which is why nothing is averaged over it.
- A registry entry with only a code name can't be classified. Those rows
say so instead of being guessed at, and are resolved by hand into
data/assets/. whyStoppedis self-reported and often absent — the page prints the reporting rate.- Disease-name matching is stemmed token overlap, not ontology mapping.
- The density bands are a convention, printed on the page and documented in ARCHITECTURE.md — not calibrated against outcomes.
landscape/
models.py data model; every fact carries provenance
config.py endpoints and every constant that feeds a judgement
http.py cached, retrying stdlib HTTP client
sources/ Open Targets · ChEMBL · ClinicalTrials.gov · UniProt · Europe PMC
relevance.py does this record actually name this target, and this drug
normalize.py reconciliation — identity, phase, modality, sponsor
consistency.py invariants: a headline figure must equal its own table
analysis/
mechanism.py INN stems + mechanism vocabulary → mechanism classes
failures.py whyStopped → five causes
crowding.py phase-weighted density, indications, untried diseases
licensing.py deals on file, and where each programme stands
feasibility.py the six questions
showcase.py the six tiles, from the same stored landscape
render.py HTML + Markdown memo, and the multi-target index
doctor.py API health and schema-drift check
store.py curated tier + live-build cache + the hand-maintained layers
targets.py the searchable index: symbols, aliases, ranked lookup
pipeline.py orchestration; sources degrade, runs do not abort
web/
app.py FastAPI: search, lookup, build jobs, exports
static/ the interface — no framework, no build step
data/curated/ precomputed targets, committed
data/deals/ deal terms, hand-entered, one CSV per target
data/assets/ programmes the registries name only by a code
data/pathways.csv where each target sits, cited to UniProt
data/target_index.json the searchable target index
scripts/ precompute, recompute, static export
fixtures/ offline demo + regression input
calibration/ reference target set for deriving the density bands
tests/ 262 tests — parser contracts against realistic payloads,
the judgement layer, and the web layer; all offline
The engine (landscape/) has no web dependencies and doesn't know the site
exists — only web/ needs FastAPI. That's what lets one analysis serve a web
request, a command line run and a CI job without three versions of it.
Every test runs without network, against payloads in tests/payloads.py
shaped like what these APIs actually emit, malformed records included.
See ARCHITECTURE.md for the design decisions and how the constants were calibrated.
MIT for the code. The data carries its own licences — see the table above.





