Table of Contents
Ranking
api/app/services/triage.py. The rank is a weighted mean of four components a reviewer can
audit, not a black box.
| Component | Weight | Scores when |
|---|---|---|
| phenotype fit | 0.35 | always |
| rarity in gnomAD | 0.25 | the run looked up frequencies |
| consequence severity | 0.20 | always |
| model P(pathogenic) | 0.20 | the run has CADD or AlphaMissense |
ClinVar is deliberately not an input. It is displayed beside the result as independent confirmation, so nothing ranks highly merely because ClinVar already called it pathogenic.
Rarity and consequence filter — the usual first pass. Phenotype only ranks, because a real diagnosis can sit in a gene nobody has annotated yet, and filtering on phenotype would hide exactly that case.
Evidence that was never looked up abstains
This is the most important rule in the file, and it replaced a real bug.
Run without a VEP cache there are no allele frequencies. rarity_score(None) read that as absent
from gnomAD, therefore maximally rare and gave every variant a free 0.25. The model separately
returned the same 0.887 for every variant, from features it had never been given. Two of four
components were fiction and the total looked fully informed.
Now jobs.has_frequencies and jobs.has_effect_scores record what the run actually produced, a
component with no evidence returns None rather than a number, and combine() renormalises the
weights over whatever is left. The score stays on 0–1 and means the same thing; the UI prints
"not looked up" instead of drawing a bar.
def combine(components):
weight = sum(WEIGHTS[n] for n, v in components.items() if v is not None)
return sum(WEIGHTS[n] * v for n, v in components.items() if v is not None) / weight
If you add a component, decide what makes it abstain before you decide its weight.
Phenotype fit
Information-content-weighted recall: the share of the total specificity of the patient's terms that this gene accounts for.
score = Σ IC(matched terms) / Σ IC(all the patient's terms)
IC(term) = -ln(fraction of annotated genes carrying it), computed at load time. It matters:
"Dilated left subclavian artery" scores 7.88, "Global developmental delay" 0.93 — an 8.5× gap that
plain term counting threw away.
A term HPO has never annotated to any gene gets DEFAULT_IC, keeping it in the denominator so it
depresses every gene equally. That is the neutral choice, not an oversight.
The model
Trained on ClinVar, held out by gene rather than by variant. Allele frequency is deliberately
not a feature — it double-counted against rarity_score and was circular, since ACMG assigns
ClinVar's benign labels using frequency. Removing it dropped missense AUROC from 0.872 to 0.500,
exactly random, which is why the model now abstains without CADD or AlphaMissense.
Full working in Benchmarks.
Worked example
The published Loeys–Dietz case, run in VEP database mode:
| score | phenotype (0.64) | rarity | consequence (0.36) | model | |
|---|---|---|---|---|---|
| TGFBR2 3:30672252 missense | 0.855 | 1.00 (30/30) | not looked up | 0.60 | not looked up |
| OSBPL10 3:31748090 missense | 0.218 | 0.00 | not looked up | 0.60 | not looked up |
Both are rare missense variants, identical on every piece of evidence this run holds except one. The phenotype is what separates a published diagnosis from an incidental variant.
rarelens
Understanding it
Working on it
Running it
When it goes wrong