Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Target Landscape

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.

The six answers, on KRAS

pip install -r requirements-web.txt
uvicorn web.app:app --reload          # → http://127.0.0.1:8000

On 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.html

The interface

Each 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.

Six tiles, on a real target

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

What each running trial could prove

Mechanism classes and the pathway

Terminations — the sponsor's own words, classified, with the matched phrase shown so you can overrule the call:

Termination analysis

Licensing — what has been transacted and what has not, no score on either:

Deals on file, and where each programme stands

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 network

Deploying

Two 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 someone

How the analysis is built

Two halves: getting a defensible record out of five public APIs, and deciding what may be said about it.

What is fetched, and what is discarded

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.

How a finding is worded

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.


Quickstart

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"

doctor

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.

As a library

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))

Data sources

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.


Known limits

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/.
  • whyStopped is 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.

Repository

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.

Licence

MIT for the code. The data carries its own licences — see the table above.

About

Drug-target landscapes that put the science and the business of a target on one page — every line links to the record it came from. Offline Python engine over Open Targets, ChEMBL, ClinicalTrials.gov and UniProt. No LLM anywhere in the pipeline.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages