Skip to content

Repository files navigation

just-prs: The Polygenic Risk Score (PRS) Toolbox

PyPI version PyPI version Python 3.13+ Research use only Not medical advice MCP ready Web UI

just-prs scores a genome against 5,000+ published PGS Catalog models (or your own scoring file), places the result on a reference-population bell curve, and estimates absolute risk when the evidence supports it. It is research software, not medical advice.

Most PRS tools either hide the catalog behind one curated score or dump raw sheets with no guidance. just-prs shows every available model for a trait, plus the quality, match-rate, ancestry, and consensus metrics you need to decide which ones to trust.

Three ways in:

  • Browseruv run ui, upload a VCF, browse traits, inspect bell curves.
  • Agentsjust-prs-mcp for Claude, Cursor, Codex, and other MCP clients.
  • CLI / Python — scripts, notebooks, and pipelines.

Quick start

pip install just-prs          # HTML/JSON charts included
pip install "just-prs[viz]"   # optional PNG/SVG export

prs compute --vcf anton --pgs-id PGS000001
prs plot trait BMI --vcf anton -o bmi.html --show-table

Built-in aliases anton and livia auto-download public test genomes from Zenodo on first use. From this workspace: uv sync --all-packages then uv run ui or uv run prs ….

Never used this repo? Start with the beginner's guide (install, web UI, MCP, troubleshooting).

Contents

Features — what the toolbox covers
  • PRS from VCF or consumer arrays — normalize, score one or many PGS IDs (catalog or a local .txt.gz / .parquet), inspect match rates, quality labels, percentiles, and absolute-risk context.
  • Trait-first analysis — pick a trait such as type 2 diabetes; compute all associated models and summarize agreement, outliers, and quality.
  • PGS Catalog metadata — search cleaned score, trait, performance, publication, prevalence, and scoring-file sheets.
  • Fast data engine — Polars and DuckDB scoring, zstd Parquet caches, HuggingFace sync for cleaned metadata and reference distributions.
  • Reference and pgen workflows — optional Linux/WSL .pgen / 1000G / HGDP+1kGP scoring and PLINK2 cross-validation.
  • Reusable UI components — embed the workbench in another Reflex app via PRSComputeStateMixin and load_genotypes(path).

Web UI

PRS Compute UI — upload VCF, select scores, compute PRS

uv sync --all-packages
uv run ui    # http://localhost:3000

Upload a VCF once (or drop several for a family comparison). The app detects build, normalizes to Parquet, and feeds both Select by PRS and Select by Trait. Ancestry is inferred after upload and used as the default reference population.

Workbench tabs and compute flow

Tabs: Compute PRS (default), Metadata Sheets, Scoring File.

  1. Upload a VCF — build detection, normalization, cached Parquet, shared across both selection modes.
  2. Select by PRS or by Trait — individual PGS IDs, or a whole trait aggregated into a consensus summary.
  3. Download CSV from the results table.

The metadata and scoring-file tabs browse PGS Catalog sheets and stream harmonized scoring files by PGS ID.

Agents and slash skill

The MCP server is just-prs-mcp. Ask in plain language: "Download Anton's sample genome, normalize it, and compute PRS for type 2 diabetes." Step-by-step Cursor / Claude / Codex setup and MCP troubleshooting: beginner's guide — MCP.

Claude Code, Cursor, Codex, Antigravity

Claude Code:

claude mcp add just-prs -- uvx just-prs-mcp@latest stdio

Cursor (.cursor/mcp.json or user MCP config):

{
  "mcpServers": {
    "just-prs": {
      "command": "uvx",
      "args": ["just-prs-mcp@latest", "stdio"],
      "env": {
        "PRS_MCP_MODE": "essentials"
      }
    }
  }
}

Codex:

[mcp_servers.just-prs]
command = "uvx"
args = ["just-prs-mcp@latest", "stdio"]

Antigravity (and other MCP clients): uvx just-prs-mcp@latest stdio.

Claude Code / Antigravity /prs skill (no MCP required)

The skill at docs/skills/prs/SKILL.md teaches the assistant to search the catalog, compute scores, generate Altair charts, and interpret results through the CLI.

Workspace (this repo):

mkdir -p .claude/skills/prs
ln -sf ../../../docs/skills/prs/SKILL.md .claude/skills/prs/SKILL.md

Personal (every project):

mkdir -p ~/.claude/skills/prs
curl -o ~/.claude/skills/prs/SKILL.md \
  https://raw.githubusercontent.com/dna-seq/just-prs/main/docs/skills/prs/SKILL.md

Then /prs BMI or /prs type 2 diabetes. The skill calls uvx just-prs. Use the skill for interactive sessions; use MCP when you need typed tool schemas.

Visualization

Altair charts are built in. HTML (interactive) and JSON (Vega-Lite) work out of the box; just-prs[viz] adds PNG/SVG. plot trait auto-detects each sample's ancestry unless you pass --ancestry. --reference-restoration is the same off / wgs / chip-id flag as prs compute (default off — see reference restoration). Results are cached per VCF × PGS ID × build × ancestry × restoration.

prs plot commands
prs plot trait "type 1 diabetes" --vcf livia -o t1d.html --show-table
prs plot trait BMI --vcf anton -o bmi.html --show-table
prs plot trait thrombosis --vcf anton -o dvt.html --fuzzy --show-table

# Overlay population curves
prs plot trait BMI --vcf anton -o bmi.html --show-table --all-ancestries
prs plot trait BMI --vcf anton -o bmi.html --ancestries EUR,AFR,EAS

# Specific PGS IDs (full trait-style report)
prs plot trait PGS000001 --vcf anton -o pgs1.html
prs plot trait PGS000001,PGS000002 --vcf anton -o two_scores.html

# Minimal single-score charts
prs plot bell-curve PGS000001 --vcf anton -o bell.html
prs plot multi-ancestry PGS000001 --vcf livia -o multi.html

# From pre-computed JSON
prs plot trait "type 2 diabetes" -o t2d.html --results my_results.json
prs plot strip results.json -o strip.html --title "My PRS Report"

prs plot trait BMI --vcf anton -o bmi.html --no-cache   # force recompute
prs plot trait intelligence --vcf anton --reference-restoration wgs -o compare.html

Format follows the file extension (.html, .json, .png, .svg). Trait matching uses the UI EFO label (e.g. intelligence) first, then the catalog reported name; --fuzzy is only needed when a substring hits several traits. The plot trait positional also accepts comma-separated PGS IDs.

AI prompts for agents (prs prompt)

The Compute UI's Ask Claude / ChatGPT / … buttons are also a CLI. Progress goes to stderr; the prompt itself is stdout, so you can pipe it into an agent.

# Multi-sample comparison prompt (same text the HTML Ask-AI buttons prefill)
prs prompt intelligence --vcf Anton=anton --vcf Livia=livia

# Pipe into an agent
prs prompt BMI --vcf anton | claude

# Already-computed JSON ({sample: [rows]} or a list with a sample field)
prs prompt intelligence --results family.json -o prompt.txt

# Prefill URL instead of prompt text
prs prompt PGS000001 --vcf anton --assistant claude --url

--assistant other (the default) uses the full 6000-character budget with no URL encoding. --assistant claude|chatgpt|perplexity|grok matches that assistant's UI character limit. Repeat --vcf with Label=path the same way as plot trait.

Multi-sample / family comparison

Repeat --vcf with optional Label=path (aliases work: Anton=anton). Each sample gets its own color, dots, and median line. Ancestry is detected per genome, so mixed-ancestry families compare correctly. The intelligence screenshot under Research use only is the example family (Mom, Dad, Son1, Son2, Daughter).

prs plot trait intelligence --vcf Anton=anton --vcf Livia=livia -o compare.html

prs plot trait intelligence \
  --vcf Mom=mom.vcf.gz --vcf Dad=dad.vcf.gz \
  --vcf Son1=son1.vcf --vcf Son2=son2.vcf --vcf Daughter=daughter.vcf \
  -o intel_o_family.html

prs plot trait PGS000001 --vcf Anton=anton --vcf Livia=livia -o pgs1_compare.html
prs plot bell-curve PGS000001 --vcf Anton=anton --vcf Livia=livia -o bell_compare.html

The web UI does the same: drop several VCFs, get colored sample chips, and overlay percentiles on every bell curve.

Python API (just_prs.viz)
Function What it shows
plot_prs_bell_curve One model + ancestry, user score marker
plot_prs_multi_ancestry Five population curves + user score
plot_trait_scores Trait-grouped N(0,1) curve + quality-colored model dots
plot_prs_percentile_strip Horizontal risk-band strip
from pathlib import Path
import polars as pl
from just_prs.viz import plot_trait_scores, save_chart

dists = pl.read_parquet("~/.cache/just-prs/percentiles/1000g_distributions.parquet")
quality = pl.read_parquet("~/.cache/just-prs/percentiles/1000g_quality.parquet")

chart = plot_trait_scores(
    "BMI", dists, quality_df=quality,
    user_results=my_results,
    height=150, show_table=True,
)
save_chart(chart, Path("bmi_report.html"))
save_chart(chart, Path("bmi_report.png"))

CLI and Python

prs compute --vcf sample.vcf.gz --pgs-id PGS000001
prs compute --vcf sample.vcf.gz --pgs-id PGS000001,PGS000002,PGS000003
prs compute --vcf anton --pgs-id PGS000001,PGS000002 --ancestry --ancestry-aadr
prs compute --vcf sample.vcf.gz --scoring-file my_custom_score.txt.gz
prs normalize --vcf sample.vcf.gz --pass-filters "PASS,." --min-depth 10
prs catalog scores search --term "breast cancer"
Python scoring snippet
import polars as pl
from pathlib import Path
from just_prs import PRSCatalog, VcfFilterConfig, normalize_vcf
from just_prs.prs import compute_prs

catalog = PRSCatalog()
config = VcfFilterConfig(pass_filters=["PASS", "."], min_depth=10)
parquet_path = normalize_vcf(Path("sample.vcf.gz"), Path("sample.parquet"), config=config)
genotypes_lf = pl.scan_parquet(parquet_path)

result = compute_prs(
    vcf_path="sample.vcf.gz",
    scoring_file="PGS000001",
    genome_build="GRCh38",
    genotypes_lf=genotypes_lf,
)
print(f"Score: {result.score:.6f}, Match rate: {result.match_rate:.1%}")

Full command reference: docs/cli.md. Python API: docs/python-api.md.

VCF aliases and public test genomes

--vcf accepts a path or an alias (compute, plot bell-curve, plot multi-ancestry, plot trait). Built-ins auto-download from Zenodo:

Alias File License Zenodo
anton antonkulaga.vcf (~482 MB) CC0 18370498
livia SIMHIFQTILQ.hard-filtered.vcf.gz (~349 MB) CC-BY-4.0 19487816
prs alias list
prs alias set mygenome /path/to/my/sample.vcf.gz
prs alias remove mygenome
prs compute --vcf anton --pgs-id PGS000001

User aliases live in ~/.cache/just-prs/vcf_aliases.json. MCP agents can fetch either genome with download_sample_genome (sample="anton" or "livia").

Genetic ancestry inference

Runtime is pure Python (no plink2). Models are built offline and pulled from HuggingFace on first use.

prs ancestry infer --vcf anton
prs ancestry infer --vcf anton --mode label --panel 1000g
prs ancestry infer --vcf anton --mode mixture --panel hgdp_1kg
prs ancestry infer --vcf anton --panel hgdp_1kg --resolution population
prs ancestry infer --vcf newton --mode all --prive --aadr --resolution population
prs ancestry check PGS000001 --vcf anton

Modes: label, mixture, prive, consensus, all (default). Panels 1000g and hgdp_1kg are on HuggingFace; prive and aadr_ho are built locally because of data-license terms.

Within-continent calls are best read as soft proportions, not a single hard label — East-Slavic groups form roughly one autosomal cluster. Methodology: docs/sample-ancestry-methodology.md.

Research use only

PRS is a statistical predisposition signal in a studied population — not a diagnosis, not a probability you “will get” a disease, and not a substitute for clinical testing.

Example family comparison for intelligence — Mom, Dad, Son1, Son2, and Daughter

This is the example family from prs plot trait intelligence with five VCFs labeled Mom, Dad, Son1, Son2, and Daughter. Each person has a median card and colored markers on the same bell curve. Relatives can sit far apart, and the models for this trait still disagree — read match rate and quality, not one percentile. A family PRS plot is not a diagnosis and not a relatedness test.

FAQ — quality, ancestry, coverage, absolute risk

What does "research use only" mean? Many people are used to tests that look at a narrow, high-confidence question (a known pathogenic variant). PRS are statistical models from many small associations, often with modest predictive power. Being in the PGS Catalog does not mean a score is clinically ready, ancestry-portable, or useful for an individual decision.

Why do several PRS for the same trait disagree? Different cohorts, ancestries, phenotype definitions, builds, variant sets, and methods. Prefer better published metrics, higher match rates, relevant ancestry, and agreement among high-quality models. The trait summary is built to show consensus and outliers.

Does a high PRS mean I will get a disease? No. Heritability for most common diseases is moderate; current GWAS-based PRS typically capture only a fraction of it. Tag SNPs in LD with causal loci are not a mechanistic readout. Environment, age, sex, lifestyle, and chance often matter as much or more.

Why does ancestry matter? LD patterns and allele frequencies differ across populations, so a score trained mostly in Europeans often transfers poorly. Reference percentiles (“where does this sit vs this panel?”) do not prove the original model works equally well in that population.

Why is my match rate so low? Consumer arrays type ~600–700k SNPs, not a whole genome. Exomes miss non-coding GWAS tags. Build mismatch matches ~0 variants. just-prs can recover some untyped sites via LD-proxy substitution on GSA v3 / GRCh38 (1000g); that lifts coverage but does not replace imputation. A 12% match is a fragment of the model — check matched vs total before trusting a number.

How is quality determined? A synthetic 0–100 score from published discrimination (AUROC / C-index / beta / OR), cohort size, match rate, and a harmonized-liftover penalty, then a combined score after computation on real genomes. Details: docs/prs-quality-score.md.

What does absolute risk mean? A conversion from percentile + prevalence + published performance into a probability. It is only as good as those inputs; weak evidence should show as N/A, not a precise-looking number. See docs/absolute-risk-methodology.md.

Installation

Requires Python >= 3.13. Uses uv. First time on a new machine: beginner's guide.

pip install just-prs

git clone https://github.com/antonkulaga/just-prs
cd just-prs
uv sync --all-packages

CLI names: just-prs and prs. Core library only: cd just-prs/just-prs && uv sync.

Windows

The web UI and VCF-based PRS work on Windows with no C compiler. pgenlib is excluded via sys_platform != 'win32' (no Windows wheels; bundled C fails on MSVC).

cd just-prs
uv sync --all-packages
uv run ui

Reference-panel / .pgen scoring (prs reference, prs pgen, the Dagster pipeline) needs WSL or Linux.

Project structure

uv workspace with three packages:

Package Directory Description
just-prs just-prs/ Core library. Published to PyPI.
prs-ui prs-ui/ Reflex web UI. Published to PyPI.
prs-pipeline prs-pipeline/ Dagster pipeline for reference distributions.

The workspace root is a non-published wrapper (uv run ui, uv run pipeline).

Why not PLINK2?

PLINK2 --score is the gold standard. just-prs matches it (Pearson r = 1.0 across 3,202 samples, relative per-sample differences < 5e-7 — docs/validation.md) and is easier to compose in Python.

PLINK2 just-prs
Install Platform binary pip install just-prs
Integration Subprocess + text I/O Polars DataFrames
Batch One process per PGS ID Reuses parsed .pvar / genotype caches
from pathlib import Path
from just_prs import compute_reference_prs_polars

scores_df = compute_reference_prs_polars(
    pgs_id="PGS000001",
    scoring_file=Path("PGS000001_hmPOS_GRCh38.txt.gz"),
    ref_dir=Path("path/to/pgen_dir"),
    out_dir=Path("data/output/results/pgs000001"),
    genome_build="GRCh38",
)
prs pgen score PGS000001 path/to/pgen_dir/
prs reference score PGS000001
prs reference score-batch
prs reference compare PGS000001
Embed the PRS UI in another Reflex app

Install prs-ui, mix in PRSComputeStateMixin, and push genotypes through load_genotypes(path) — your app supplies the source.

import reflex as rx
from reflex_mui_datagrid import LazyFrameGridMixin
from prs_ui import PRSComputeStateMixin, prs_section


class MyAppState(rx.State):
    genome_build: str = "GRCh38"
    cache_dir: str = ""
    status_message: str = ""


class PRSState(PRSComputeStateMixin, LazyFrameGridMixin, MyAppState):
    """Consumer state — load_genotypes is built in."""


def prs_page() -> rx.Component:
    return prs_section(PRSState)

For the full By PRS / By Trait workbench with your own source, render prs_workbench(...). See AGENTS.md for the embedding contract.

Testing
uv run pytest just-prs/tests/ -v

Integration tests use real genomic data (no mocks). PLINK2 cross-validation, live catalog metadata, and Zenodo test VCFs are documented in docs/validation.md.

Documentation

Data sources: PGS Catalog REST · EBI FTP · HuggingFace pgs-catalog

About

Tool and library with ui to compute polygenic risk scores

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages