PGS Score Compatibility Classification
How MODS decides, for each published polygenic score, whether it can compute that score for a patient — and if not, exactly why.
This document describes the compatibility classifier (backend/mods_prs_compat.py,
“Gate B” of the PRS rebuild). It is written to be read on its own: the first half is
concepts and rationale; the second half is the precise rules and worked examples.
1. The problem it solves
A polygenic score (PGS, also “PRS” for
polygenic risk score) estimates a person’s genetic predisposition to a trait by adding up
the effects of many genetic variants. Each variant contributes dosage × weight, where the
dosage is how many copies of the trait-associated allele the person carries (0, 1, or 2),
and the weight comes from a genome-wide association study.
The PGS Catalog publishes ~5,000+ such scores as
downloadable scoring files — each a table of variants, alleles, and weights.
Here is the catch: not every published score can be computed the same way, and some cannot be computed responsibly at all. A score might rely on complex biology (a whole haplotype, an interaction between variants, a copy-number change) that a simple additive sum gets wrong. Or it might use a variant whose position we cannot place on the genome, or an allele pair that is ambiguous about which DNA strand it refers to.
If we blindly computed every score, some numbers would be quietly, confidently wrong — the most dangerous kind of error in a clinical-decision context. So before computing anything, MODS classifies every scoring file into one of four compatibility classes and records why. The classifier is deliberately conservative: when a score’s format is unusual, it would rather defer the score than compute it as if it were simple.
A little vocabulary
- Effect allele / other allele. Each variant row names two alleles (versions of the DNA at that spot): the effect allele (whose count drives the score) and the other allele. Cf. the reference genome, the standard “consensus” sequence every allele is compared against.
- Dosage is the person’s genotype expressed as a count of effect alleles: 0, 1, or 2 (see zygosity).
- Harmonization is the PGS Catalog’s process of mapping every variant to genomic
coordinates on a common build (here GRCh38)
so scores from different studies line up. Harmonized columns are prefixed
hm_.
2. The four classes
| Class | Meaning | Outcome |
|---|---|---|
| A | Clean: additive, biallelic, autosomal SNVs, position-resolvable, explicit alleles, no complex rules. | Computed in production. |
| B | Conditionally supported: computable with care (indels, strand-ambiguous SNVs, X-chromosome, inferred alleles, …). | Computed only if every one of its complications is on the approved list (“promotion”, §5); otherwise deferred. |
| C | Needs a dedicated calculator: the biology is not a simple additive sum (haplotypes, interactions, dosage-specific weights, APOE / HLA / CYP star-alleles). | Deferred — returns “not calculated” with a reason, never a partial number. |
| D | Unavailable: cannot be placed on the genome at all. | ”Not calculated.” |
The guiding rule for C and D is the same: a missing score is honest; a wrong score is not. We never emit a partial or approximate number for a score we cannot compute correctly.
3. The signals the classifier reads
The classifier looks only at the structure of the scoring file — never at a patient. For each file it computes a small set of feature counts and inspects which columns the file contains.
| Signal | What it is | Why it matters for scoring |
|---|---|---|
| Unplaceable rows | Variant has no chromosome/position (hm_chr/hm_pos blank). | Our genotype pipeline is position-keyed; an unplaceable variant cannot be looked up. |
| Indel | An insertion or deletion — alleles longer than one base (A → ATTCT). | Indels need careful left-alignment and reference verification; the anchor base is shared between the two alleles, so naïve matching is wrong. |
| Palindromic SNV | A biallelic SNV whose two alleles are DNA complements: A/T or C/G. | Because A pairs with T and C with G, you cannot tell from the alleles alone which strand the study meant — a “strand flip” would silently invert the dosage. Resolvable only via allele frequency. |
| Multiallelic site | One genomic position carrying more than two alleles across the score. | Ordinary SNP scoring assumes two alleles per site; more than two needs special handling to match the right one. |
| X-chromosome variant | Variant on chromosome X. | Dosage on X depends on sex (dosage compensation): a person with one X is hemizygous (0/1), not 0/1/2. |
| Inferred other-allele | The “other” allele was guessed by the harmonizer (hm_inferOtherAllele), not stated by the study. | An inferred allele is not authoritative; trusting it like a stated one can misassign the reference vs. alternate and thus the dosage. |
| Missing other-allele | No other allele at all — not even an inferred one. | Without both alleles we cannot orient the variant against the reference to know a wild-type person’s dosage. |
| Dedicated-calculator columns | Columns like is_interaction, is_haplotype, is_diplotype, dosage_0/1/2_weight. | Their presence means the score is not a simple additive sum — it encodes interactions, haplotypes, or per-genotype weights. |
| Inheritance-model columns | is_dominant / is_recessive. | The dosage → contribution mapping is non-additive (dominant/recessive), not the standard 0/1/2 × weight. |
Conservative-by-presence. For the complex-format and inheritance columns the classifier flags on the presence of the column, not on confirming every row uses it. This can over-defer a score that carries an unused column, but that is the safe direction — we would rather review a deferred score than silently compute a complex one. Row-level refinement is a planned hardening step.
4. How a class is chosen
The classifier assigns exactly one class, checked in this precedence (first match wins):
- D — if no variant in the file is position-resolvable.
- C — else if any dedicated-calculator column is present.
- B — else if any Class-B signal is present (indel, multiallelic, palindromic, X, inferred-other, missing-other, inheritance model, or some unplaceable rows).
- A — otherwise: a clean additive, biallelic, autosomal, position-resolvable SNV score.
Every result records the specific reasons behind it, so a score is never a bare label — it is a label plus the evidence. The assignment is a pure function of the scoring file, so it is deterministic and reproducible from the file’s checksum.
5. Class-B promotion (which “computable-with-care” scores we actually compute)
Class B is the interesting middle. Its scores can be computed, but each complication needs handling that must be validated before we trust it. So Class B carries a second decision — promotion — that is a signed-off policy, not an automatic judgement.
A Class-B score is production-eligible only if every one of its complications is on the promoted list. One un-promoted signal defers the whole score (the strict-AND rule keeps a mostly-simple score from sneaking through on the strength of its simple parts).
Current promoted set (approved 2026-07-14; ambiguous_other added deferred 2026-09-22):
| Class-B signal | Promoted? | Rationale |
|---|---|---|
| Indels | ✅ | The genotype builder emits normalized, reference-verified indel alleles; the reference scorer matches them. |
| X chromosome | ✅ | The current patient has two X chromosomes, so X dosage behaves like an autosome (0/1/2). A single-X patient would need hemizygous handling. |
| Inferred other-allele | ✅ | Every allele is verified against the reference genome, so an inferred allele that matches the genome is safe to use. |
| Palindromic SNV | ✅ (resolvable subset) | Standard tooling resolves strand by allele frequency; genuinely ambiguous ones stay deferred at compute time. |
Unresolvable allele (ambiguous_other) | ❌ defer | The allele names no single sequence — a harmonizer candidate set (A/T, C/G/T), an unknown base (N), presence/absence notation (-, P), or a star-allele name. Added in classifier v3 (GH #273): these rows used to be counted as indels, which are promoted, so they could qualify a score for production on an allele we cannot even orient. |
| Multiallelic site | ❌ defer | Duplicate-position handling and allele matching need golden tests first. |
| Missing other-allele | ❌ defer | Cannot orient a wild-type dosage with no other allele. |
| Inheritance model (dom/rec) | ❌ defer | Non-additive dosage mapping, out of scope for the additive scorer. |
| Unplaceable rows | ❌ defer | Partial coverage — handled by the coverage gate later, not promoted here. |
Promotion sets policy; nothing is actually computed until a later release step, and only after golden tests.
6. Theoretical bounds — a free sanity check
For any additive score, the raw total S = Σ dosageᵢ · weightᵢ with each dosage in {0,1,2}
is mathematically bounded:
L = Σ min(0, 2·weightᵢ) (all effect alleles absent or present, whichever is lower)
U = Σ max(0, 2·weightᵢ)
Every computed score must satisfy L ≤ S ≤ U. A value outside that interval is not a
biological finding — it is a bug in the scorer, and downstream code asserts the bound per
score. It costs nothing and catches whole classes of arithmetic and allele-orientation
errors. (Note: a score whose weights are odds ratios
rather than log-effects must be transformed before summing — a separate biological check, not
this bound.)
7. Worked examples (real PGS Catalog scores)
Run on four actual scoring files (harmonized to GRCh38):
| Score | Variants | Class | Eligible | Why |
|---|---|---|---|---|
| PGS000027 | 2,100,302 | A | ✅ | Entirely clean additive SNVs — and the classifier handled a 2.1-million-variant file without trouble. |
| PGS000004 | 313 | B | ✅ | Has indels and palindromic SNVs — both promoted — so eligible. |
| PGS000118 | 29 | B | ✅ | Indels + palindromic + inferred-other — all promoted. |
| PGS000019 | 192 | B | ❌ | 168 of 192 rows rely on an inferred other-allele, plus it has missing-other and unplaceable rows. Two of those (missing-other, unplaceable) are not promoted, so the whole score is correctly deferred. |
PGS000019 is instructive: it was the score used in the original audit that exposed the underlying dosage bug, and here the classifier independently flags it as not-yet-trustworthy for exactly the right reasons.
8. The fleet run (2026-07-21)
The classifier was run across the entire cached GRCh38 catalog: 5,377 scoring files, ~59 GB, 3.45 billion variant rows examined. Every score is now classified, on one classifier version, with reasons and a scoring-file checksum.
| Class | Scores | Share | Production-eligible | Variant rows |
|---|---|---|---|---|
| A | 1,997 | 37.1% | 1,997 | 1,424,367,610 |
| B | 2,950 | 54.9% | 1,866 | 2,007,436,633 |
| C | 426 | 7.9% | 0 | 20,736,700 |
| D | 4 | 0.1% | 0 | 17 |
| Total | 5,377 | 3,863 (71.8%) | 3,452,540,960 |
So roughly seven in ten published scores are computable under the current promotion policy, and the eligible set carries ~1.88 billion variant rows — the real size of the job facing the later gates.
What blocks the other 1,084
Of the deferred Class-B scores, the table below counts how many carry each not-yet-promoted category, and — more usefully — how many would become eligible if that category alone were promoted (a score with two deferred categories needs both, so it counts toward neither).
| Deferred category | Appears in | Sole blocker |
|---|---|---|
| Multiallelic site | 588 | 588 |
| Missing other-allele | 477 | 94 |
| Unplaceable rows | 399 | 18 |
| Inheritance model | 3 | 1 |
Multiallelic handling is by far the highest-leverage open decision: it is the only
thing standing between 588 scores and production, and promoting it alone would lift
eligibility from 3,863 to 4,451 (+15%). By contrast unplaceable_rows appears in 399
deferred scores but is almost never the lone blocker — those scores have other problems
too, so work there buys little.
Notes from the run
- Five cached scoring files were truncated (interrupted downloads from an earlier sweep). Every one failed loudly on its gzip checksum rather than being parsed as far as the truncation and classified on partial data. All five were re-downloaded — each 25–62% larger than the truncated copy — and classified cleanly.
- Fifteen scores parse to zero variant rows, because they carry their weights in
dosage_0/1/2_weightinstead ofeffect_weight. These are correctly Class C (dedicated calculator), not Class D — the zero-row result is a fact about this parser, not about the file. §4’s ordering exists to preserve that distinction. weight_typeis not clean catalog-wide and will need attention at the biological checks: 3,250beta, 1,390NR(not reported), plusBETA/Betacase variants, 199log(OR), and a long tail of free-text values. A score whose weights are odds ratios must not be summed as if they were log-effects.- Classifying at this scale needs the streaming reader (
open_scoring_file), notparse_scoring_file: the largest single score carries 10,318,272 variants (PGS003162), and the largest file is PGS005197 at 9,503,677 variants and 185.8 MB compressed. Five scores exceed 10 million variants.
Reproduce with backend/scripts/prs/build_prs_manifest.py; verify with
check_manifest_invariants.py; summarize with manifest_report.py.
9. Where this sits in the pipeline
The classifier populates a score manifest (mods_prs_score_manifest, one row per PGS
score) with the class, eligibility, reasons, feature counts, and theoretical bounds. Nothing
downstream computes a score that the manifest has not marked production-eligible. In the full
PRS rebuild this is Gate B: it decides which scores to compute; a later gate makes the
computation itself correct, and a final gate applies coverage and biological checks before any
score is released.
Source: backend/mods_prs_compat.py · Manifest schema: migration 104
(mods_prs_score_manifest) · Tests: backend/tests/test_mods_prs_compat.py.