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

ClassMeaningOutcome
AClean: additive, biallelic, autosomal SNVs, position-resolvable, explicit alleles, no complex rules.Computed in production.
BConditionally 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.
CNeeds 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.
DUnavailable: 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.

SignalWhat it isWhy it matters for scoring
Unplaceable rowsVariant has no chromosome/position (hm_chr/hm_pos blank).Our genotype pipeline is position-keyed; an unplaceable variant cannot be looked up.
IndelAn 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 SNVA 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 siteOne 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 variantVariant 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-alleleThe “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-alleleNo 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 columnsColumns 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 columnsis_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):

  1. D — if no variant in the file is position-resolvable.
  2. C — else if any dedicated-calculator column is present.
  3. B — else if any Class-B signal is present (indel, multiallelic, palindromic, X, inferred-other, missing-other, inheritance model, or some unplaceable rows).
  4. 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 signalPromoted?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)❌ deferThe 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❌ deferDuplicate-position handling and allele matching need golden tests first.
Missing other-allele❌ deferCannot orient a wild-type dosage with no other allele.
Inheritance model (dom/rec)❌ deferNon-additive dosage mapping, out of scope for the additive scorer.
Unplaceable rows❌ deferPartial 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):

ScoreVariantsClassEligibleWhy
PGS0000272,100,302A✅Entirely clean additive SNVs — and the classifier handled a 2.1-million-variant file without trouble.
PGS000004313B✅Has indels and palindromic SNVs — both promoted — so eligible.
PGS00011829B✅Indels + palindromic + inferred-other — all promoted.
PGS000019192B❌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.

ClassScoresShareProduction-eligibleVariant rows
A1,99737.1%1,9971,424,367,610
B2,95054.9%1,8662,007,436,633
C4267.9%020,736,700
D40.1%017
Total5,3773,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 categoryAppears inSole blocker
Multiallelic site588588
Missing other-allele47794
Unplaceable rows39918
Inheritance model31

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_weight instead of effect_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_type is not clean catalog-wide and will need attention at the biological checks: 3,250 beta, 1,390 NR (not reported), plus BETA/Beta case variants, 199 log(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), not parse_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.