Pharmacogenomics PGx at population scale

Case study · Pharmacogenomics

78% of 2,503 people carry a gene variant that would change their prescription.

Some people clear a drug from the body too fast for it to work. Others clear it so slowly that a normal dose becomes an overdose. A DNA test can tell you which, before the prescription is written rather than after it goes wrong. I ran that test on 2,503 people to find out how often it would have changed what the doctor prescribed.

For the specialist Star alleles called across six genes from phased 1000 Genomes Illumina short-read NGS data on GRCh38, diplotypes resolved from haplotypes rather than genotypes, CPIC phenotype assignment, validated against published population frequencies. Public data, real code, every step below.

2,503
Individuals, 1000 Genomes GRCh38
6 · 12
Genes · phased biallelic markers
78.3%
Carry an actionable phenotype
<5 MB
Downloaded of ~800 GB available

The result

Drug response is not evenly distributed.

Nearly four in five people carry at least one variant that changes a prescribing decision. Which variant, and how often, depends heavily on ancestry — which is exactly why a pipeline validated on one population cannot be assumed to work for another.

How many actionable phenotypes does one person carry?

22%
54%
22%
3%
0123+
Actionable phenotypes carried (of 5 CPIC genes)
Across CYP2C19, CYP2C9, TPMT, DPYD and SLCO1B1. VKORC1 is excluded from this count because it modifies warfarin dose rather than defining a metaboliser phenotype.

Predicted phenotype by ancestry group

Poor function / high warfarin sensitivityIntermediate / decreasedNormalRapidUltrarapid

CYP2C19 clopidogrel

African
283528
American
196117
East Asian
125037
European
273830
South Asian
16412416

CYP2C9 warfarin, NSAIDs

African
98
American
2574
East Asian
93
European
3365
South Asian
2573

TPMT thiopurines

African
1288
American
1288
East Asian
96
European
93
South Asian
97

DPYD fluoropyrimidines

African
100
American
99
East Asian
100
European
98
South Asian
98

SLCO1B1 statins

African
97
American
2375
East Asian
2177
European
2870
South Asian
892

VKORC1 warfarin dose

African
1090
American
174835
East Asian
7919
European
164638
South Asian
2473

Each bar is 100% of that ancestry group. Numbers are % of the group; segments under 7% are unlabelled (exact values in the table below). Source: 1000 Genomes, 2,503 individuals.

Two results stand out. CYP2C19 poor metabolisers reach 11.7% in East Asian and 16.4% in South Asian groups against 1.2% in European — clopidogrel is substantially less effective in those individuals. VKORC1 is starker still: 78.8% of the East Asian group carries the high-sensitivity genotype requiring a lower warfarin dose, against 0.5% of the African group. Both patterns match the published literature, which is the only validation available without a truth set.

From genotype to prescription: how many people carry a variant that changes a drug decision?

Poor function / high sensitivityIntermediate / decreased

Warfarinanticoagulant · gene VKORC1
49%

More sensitive to the drug: lower warfarin dose needed.

Clopidogrelantiplatelet, after stent / heart attack · gene CYP2C19
40%

Reduced activation of the drug: guidelines suggest considering another antiplatelet.

Warfarin, NSAIDsanticoagulant / pain · gene CYP2C9
18%

Slower clearance: lower warfarin dose, caution with some NSAIDs.

Statinscholesterol (simvastatin) · gene SLCO1B1
16%

Higher risk of muscle side effects: lower dose or a different statin.

Thiopurinesimmunosuppressant / leukaemia · gene TPMT
8%

Risk of bone-marrow toxicity: reduced dose, and much lower in poor metabolisers.

Fluoropyrimidineschemotherapy (5-FU, capecitabine) · gene DPYD
1%

Risk of severe toxicity: lower starting dose, or avoid in poor metabolisers.

Share of all 2,503 individuals, every ancestry group pooled, with a reduced-function or higher-sensitivity result for that gene. The one-line actions summarise the direction of CPIC guidance and are an illustration of the method, not clinical advice. Rapid and ultrarapid CYP2C19 results (22% of people) matter for other drugs and are not counted here.

Read it from the drug side: a clinician prescribing clopidogrel or a statin will meet a relevant variant in roughly 4 patients out of 10 and 1 in 6 respectively, before any of them has a side effect. That is the case for running the test before the first prescription rather than after the first adverse event.
Show the numbers as a table

CYP2C19, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African3.527.635.528.05.5
American0.919.360.817.31.7
East Asian11.750.236.71.40.0
European1.226.638.229.64.4
South Asian16.441.324.315.52.5

CYP2C9, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African0.02.197.90.00.0
American0.925.174.10.00.0
East Asian0.26.593.30.00.0
European2.232.865.00.00.0
South Asian1.824.973.20.00.0

TPMT, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African0.911.587.60.00.0
American0.611.587.90.00.0
East Asian0.04.495.60.00.0
European0.06.893.20.00.0
South Asian0.03.596.50.00.0

DPYD, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African0.00.399.70.00.0
American0.00.999.10.00.0
East Asian0.00.0100.00.00.0
European0.02.497.60.00.0
South Asian0.21.498.40.00.0

SLCO1B1, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African0.22.497.40.00.0
American1.723.374.90.00.0
East Asian2.020.677.40.00.0
European2.028.269.80.00.0
South Asian0.47.891.80.00.0

VKORC1, % of group

GroupPoor / highIntermediateNormalRapidUltrarapid
African0.510.089.50.00.0
American17.347.635.20.00.0
East Asian78.819.41.80.00.0
European15.746.138.20.00.0
South Asian2.524.173.40.00.0

The method

Six steps. Open every one.

The code below is the code that produced the figures above — not a tidied reconstruction. Outputs are the real console output from the run.

Step 01

Resolve coordinates from an authority, never from memory

Every pharmacogenomic variant has a canonical rsID and a build-specific position. Hardcoding positions is how a PGx pipeline silently produces wrong diplotypes after an assembly change. Two of the twelve coordinates I first wrote from memory were wrong, which is the whole argument for this step.

Code
def resolve_coords(rsids):
    """GRCh38 coordinates from Ensembl -- never hardcoded from memory."""
    req = urllib.request.Request(
        "https://rest.ensembl.org/variation/homo_sapiens?content-type=application/json",
        data=json.dumps({"ids": rsids}).encode(),
        headers={"Content-Type": "application/json", "Accept": "application/json"})
    data = json.load(urllib.request.urlopen(req, timeout=120))
    out = {}
    for rs in rsids:
        v = data.get(rs)
        # drop patch/alt scaffolds; keep the primary GRCh38 assembly only
        m = [x for x in v["mappings"]
             if x["assembly_name"] == "GRCh38"
             and "_" not in str(x["seq_region_name"])][0]
        out[rs] = {"chrom": str(m["seq_region_name"]), "pos": int(m["start"])}
    return out

Step 02

Pull twelve positions, not eight hundred gigabytes

The 1000 Genomes release is tens of gigabytes per chromosome. Tabix-indexed random access over HTTPS fetches only the byte ranges covering the requested positions, so the whole analysis runs from a laptop in minutes with nothing staged to disk. This is the same technique that keeps cloud egress costs down on production pipelines.

Code
VCF = ("https://ftp.1000genomes.ebi.ac.uk/vol1/ftp/data_collections/"
       "1000_genomes_project/release/20190312_biallelic_SNV_and_INDEL/"
       "ALL.chr{c}.shapeit2_integrated_snvindels_v2a_27022019.GRCh38.phased.vcf.gz")

vf = pysam.VariantFile(VCF.format(c=chrom))   # reads the remote .tbi index only
for r in vf.fetch(chrom, pos - 1, pos):
    if r.id == rs or r.pos == pos:
        rec = r
        break

# keep the two haplotypes separately -- see step 04 for why
h1, h2 = [], []
for s in samples:
    gt = rec.samples[s]["GT"]
    h1.append(gt[0]); h2.append(gt[1])
Console output chr1: 2 marker(s)
rs3918290 -> 1:97450058 C>T (phased: 2503/2503)
rs67376798 -> 1:97082391 T>A (phased: 2503/2503)
chr10: 5 marker(s)
rs4244285 -> 10:94781859 G>A (phased: 2503/2503)
rs4986893 -> 10:94780653 G>A (phased: 2503/2503)
rs12248560 -> 10:94761900 C>T (phased: 2503/2503)
rs1799853 -> 10:94942290 C>T (phased: 2503/2503)
rs1057910 -> 10:94981296 A>C (phased: 2503/2503)

Step 03

Confirm REF and ALT from the file, not from the paper

Which allele is "the variant" is a property of the reference build, and it flips more often than people expect. The run prints the observed REF>ALT for every marker so a reviewer can check the orientation rather than trust it. It also confirms all 2,548 genotypes are phased — which step 04 depends on.

Step 04

Call haplotypes first, then the diplotype

This is the step naive pipelines get wrong. A star allele is a haplotype: a combination of variants on the same physical chromosome. TPMT*3A is rs1800460 and rs1142345 in cis. The same two variants in trans are *3B/*3C — a different diplotype with different clinical consequences. You cannot tell them apart from genotypes alone; you need phase.

Code
def call_haplotype(gene, hap_alleles):
    """Name one haplotype from its marker alleles.
    TPMT*3A = *3B and *3C in cis -- which only phased data can
    distinguish from *3B/*3C in trans."""
    var = {MARKERS[gene][rs] for rs, a in hap_alleles.items() if a == 1}
    if not var:
        return "*1"
    if gene == "TPMT":
        if {"*3B", "*3C"} <= var:
            return "*3A"
        return sorted(var)[0]
    if gene == "CYP2C19":
        # loss-of-function dominates a haplotype that also carries *17
        for a in ("*3", "*2", "*17"):
            if a in var:
                return a
    return sorted(var)[0]
TPMT diplotypes called — the phasing pays off *1/*1    2306
*1/*3C    118
*1/*3A     61
*1/*2      10
*3C/*3C     6
*3A/*3C     1
*3A/*3A     1

Step 05

Map diplotype to phenotype using the published tables

CYP2C19 uses a lookup table of diplotype to metaboliser status; CYP2C9 and DPYD use an activity score that sums per-allele function values. Encoding the difference explicitly — rather than forcing one scheme on both — is what keeps the result defensible against the CPIC guideline it claims to follow.

Code
ACTIVITY = {
    "CYP2C9": {"*1": 1.0, "*2": 0.5, "*3": 0.0},
    "DPYD":   {"*1": 1.0, "c.1905+1G>A": 0.0, "c.2846A>T": 0.5},
}

def phenotype(gene, d1, d2):
    if gene == "CYP2C19":                  # diplotype lookup, not a score
        lof = [a for a in (d1, d2) if a in CYP2C19_LOF]
        if len(lof) == 2: return "Poor metaboliser", None
        if len(lof) == 1: return "Intermediate metaboliser", None
        return CYP2C19_PHENO.get(frozenset([d1, d2]), "Normal metaboliser"), None
    if gene in ACTIVITY:                   # activity score
        score = ACTIVITY[gene].get(d1, 1.0) + ACTIVITY[gene].get(d2, 1.0)
        ...

Step 06

Validate against something you did not fit to

There is no truth set here, so the check is external: do the phenotype frequencies reproduce what the pharmacogenomics literature reports for each population? CYP2C19 poor metabolisers concentrated in East and South Asian groups, VKORC1 high-sensitivity near-fixed in East Asians, DPYD deficiency rare everywhere. They do. A pipeline that cannot state how it was checked is not a pipeline you should put near a prescription.

What this does not do

The limits matter more than the results.

CYP2D6 is deliberately absent

CYP2D6 is the most clinically important pharmacogene of all — codeine, tamoxifen, many antidepressants. It is also defined by whole-gene deletions, duplications and CYP2D6–CYP2D7 hybrids, none of which can be called from biallelic SNVs in a short-read VCF. Including it here would have produced confident, wrong diplotypes. Calling CYP2D6 properly needs copy-number analysis and usually long reads. A pipeline that quietly reports CYP2D6 from SNVs alone is one to be suspicious of.

Star alleles are simplified to their defining SNVs

Each star allele here is called from the variants that define it in CPIC and PharmVar, not from a full haplotype reconstruction. That is standard for panel-based PGx and it is what most clinical pipelines do — but it means rare alleles sharing a defining variant are collapsed, and novel loss-of-function variants are invisible. For a clinical deployment this is where validation against an orthogonal assay belongs.

These are predictions, not clinical results

1000 Genomes is a research resource. Nothing here is a diagnostic result, and none of it was generated in an accredited laboratory. In a real deployment the lab holds the ISO 15189 or CLIA accreditation and the clinical accountability; I build and validate the pipeline that runs inside it.

What comes out the other end

One individual, six genes.

The pipeline's per-sample output, shaped the way a clinician would want to read it. This is sample HG00239 — a British individual from the 1000 Genomes cohort, carrying three findings that would change prescribing.

Pharmacogenomic summary

HG00239 · GBR · research sample
CYP2C19*1/*2 Intermediate metaboliser
TPMT*1/*3A Intermediate metaboliser
SLCO1B1*1/*5 Decreased function
CYP2C9*1/*1 Normal metaboliser
DPYD*1/*1 Normal metaboliser
VKORC1*1/*1 Low warfarin sensitivity

Three actionable findings in one person: reduced clopidogrel activation, reduced thiopurine metabolism, and raised simvastatin myopathy risk. None would be visible without genotyping, and all three have a published CPIC recommendation attached. That is the argument for pre-emptive pharmacogenomics in one card.

Reproduce it

The calling script and the full per-sample results are published alongside this page. It needs Python, pysam and pandas, no local data, and runs in a few minutes.

pgx_starcall.py  ·  per-sample calls (CSV, 2,503 rows)

Beyond ancestry, beyond the public-data demo

Same discipline, different questions: cancer, transplant, population scale.

The pipeline above is framed around ancestry because that is what 1000 Genomes, a public dataset, is built for. The underlying discipline — call variants correctly, validate against something you did not fit to, be honest about what the pipeline can't call — is the same one applied at CSIRO to cancer drug response, transplant genetics, and population-scale association testing.

Cancer: a whole-genome variant study

Separate from the PharmCAT work above: co-author on a whole-genome variant analysis of 6 rats in a breast-cancer study — treated vs. untreated, Wilcoxon rank-sum and t-tests on called variants. Dikeocha, Oliva et al. (2026), J Mammary Gland Biol Neoplasia →

Liver transplant: PrecisionGo

With Sydney University and Westmead Hospital: genetic patterns in liver-transplant donors and recipients, modelled with time-series ML regression. Main technical contact and project manager — clinical genomics work that has nothing to do with ancestry and everything to do with a specific patient outcome.

Population scale: GWAS with VariantSpark

VariantSpark (Random Forest GWAS, built for cohort-scale genome-wide association testing) deployed alongside PharmCAT and sVEP into the same partner AWS accounts — association testing at a scale plain regression-based GWAS tools don't handle well.

Sister project, same doctrine: PathsBeacon

PathsBeacon (COVID-19 and TB surveillance) was built for GSI Lab, Indonesia's second-largest sequencing facility, running in the AWS Jakarta region under the same data-sovereignty principle. “CSIRO has helped us become the first Indonesian health initiative to operate in the cloud,” said the lab's head. Funded by DFAT/AIHSP (A$1.1M) and UNDP (US$1.25M).

Jain & Oliva (2023), public case study →

Got a pipeline that needs to survive an audit?

This is the kind of work a Pipeline & Cloud Sprint produces, built inside your own infrastructure with the validation documents attached. Start with a one-week diagnostic.

Send an enquiry ↗ See how an engagement runs →