This page demonstrates the QTL cascade pipeline for tracing variant effects from DNA to RNA (eQTL) to protein (pQTL).
The QTL cascade pipeline:
- Builds eQTL and pQTL Hail Tables from summary statistics
- Outer-joins them on
(locus, alleles, gene_id)to classify variant–gene pairs - Optionally runs colocalization ABF to distinguish true cascades from LD artifacts
- Generates gene-level summaries with constraint and disease overlays
- Produces plots and an HTML report
# Step 1: Build eQTL table from GTEx v11 significant pairs.
# Place Liver.v11.signif_pairs.parquet under /data/gtex_v11/signif_pairs/.
hvantk reprocess gtex-eqtl:eqtls \
--raw-dir /data/gtex_v11/signif_pairs/ \
--output /data/tables/eqtl_liver.ht \
--skip-download \
--plugin-arg source=gtex_v11 \
--plugin-arg tissue=Liver
# Step 2: Build pQTL table from Fang et al. allpairs.
# Place Liver_allpairs.txt.gz under /data/fang_pqtl/.
hvantk reprocess pqtl:metrics \
--raw-dir /data/fang_pqtl/ \
--output /data/tables/pqtl_liver.ht \
--skip-download \
--plugin-arg source=gtex_fang \
--plugin-arg tissue=Liver \
--plugin-arg hgnc_ht=/data/tables/ensembl_gene.ht \
--plugin-arg p_threshold=5e-8
# Step 3: Run the cascade pipeline
hvantk qtlcascade run \
--eqtl-ht /data/tables/eqtl_liver.ht \
--pqtl-ht /data/tables/pqtl_liver.ht \
--constraint-ht /data/tables/gnomad_metrics.ht \
-o /results/cascade_liverfrom hvantk.algorithms.qtlcascade import (
build_cascade,
build_cascade_gene_summary,
CascadeConfig,
CascadePipeline,
)
# Option A: Use individual functions
ht = build_cascade(
eqtl_ht_path="/data/tables/eqtl_liver.ht",
pqtl_ht_path="/data/tables/pqtl_liver.ht",
output_path="/results/cascade.ht",
tissue="Liver",
)
print(f"Cascade pairs: {ht.count()}")
gene_ht = build_cascade_gene_summary(
cascade_ht_path="/results/cascade.ht",
output_path="/results/gene_summary.ht",
constraint_ht_path="/data/tables/gnomad_metrics.ht",
)
print(f"Cascade genes: {gene_ht.count()}")
# Option B: Use the pipeline
config = CascadeConfig(
eqtl_ht="/data/tables/eqtl_liver.ht",
pqtl_ht="/data/tables/pqtl_liver.ht",
constraint_ht="/data/tables/gnomad_metrics.ht",
output_dir="/results/cascade_liver",
)
pipeline = CascadePipeline(config)
result = pipeline.run()
print(f"Class counts: {result.class_counts}")To distinguish true signal propagation from LD artifacts, provide allpairs tables for coloc:
# Build allpairs tables (set p_threshold to 0 to keep all variants)
hvantk reprocess gtex-eqtl:eqtls \
--raw-dir /data/gtex_v11/allpairs/Liver/ \
--output /data/tables/eqtl_allpairs_liver.ht \
--skip-download \
--plugin-arg source=gtex_v11 \
--plugin-arg tissue=Liver \
--plugin-arg p_threshold=0
hvantk reprocess pqtl:metrics \
--raw-dir /data/fang_pqtl/ \
--output /data/tables/pqtl_allpairs_liver.ht \
--skip-download \
--plugin-arg source=gtex_fang \
--plugin-arg tissue=Liver \
--plugin-arg hgnc_ht=/data/tables/ensembl_gene.ht
# Run pipeline with coloc
hvantk qtlcascade run \
--eqtl-ht /data/tables/eqtl_liver.ht \
--pqtl-ht /data/tables/pqtl_liver.ht \
--eqtl-allpairs /data/tables/eqtl_allpairs_liver.ht \
--pqtl-allpairs /data/tables/pqtl_allpairs_liver.ht \
--constraint-ht /data/tables/gnomad_metrics.ht \
--disease-genes-ht /data/tables/clingen.ht \
-o /results/cascade_liver_colocRun across Fang et al. (2025) tissues with cross-tissue comparison:
hvantk qtlcascade run \
--eqtl-ht /data/tables/eqtl.ht \
--pqtl-ht /data/tables/pqtl.ht \
--eqtl-allpairs /data/tables/eqtl_allpairs.ht \
--pqtl-allpairs /data/tables/pqtl_allpairs.ht \
--tissues "Liver,Heart,Lung,Colon,Thyroid" \
--constraint-ht /data/tables/gnomad_metrics.ht \
-o /results/cascade_multi# Python API for multi-tissue
config = CascadeConfig(
eqtl_ht="/data/tables/eqtl.ht",
pqtl_ht="/data/tables/pqtl.ht",
eqtl_allpairs_ht="/data/tables/eqtl_allpairs.ht",
pqtl_allpairs_ht="/data/tables/pqtl_allpairs.ht",
tissues=["Liver", "Heart", "Lung", "Colon", "Thyroid"],
constraint_ht="/data/tables/gnomad_metrics.ht",
output_dir="/results/cascade_multi",
)
pipeline = CascadePipeline(config)
results = pipeline.run_collection()
for tissue, res in results.items():
n_coloc = 0
if res.coloc_df is not None:
n_coloc = (res.coloc_df["H4"] > 0.8).sum()
print(f"{tissue}: {res.n_cascade_genes} genes, {n_coloc} colocalised")results/cascade_liver/
├── per_tissue/
│ └── liver/
│ ├── cascade.ht/ # Variant-gene cascade pairs
│ ├── gene_summary.ht/ # Gene-level aggregation
│ ├── gene_summary.tsv # TSV export
│ └── coloc_results.tsv # H0-H4 posteriors (if coloc run)
├── plots/
│ ├── liver_cascade_classes.png
│ └── liver_coloc_posteriors.png
└── qtlcascade_report.html
Run colocalization independently on a set of genes:
# Extract cascade genes with both eQTL + pQTL evidence
# (e.g., from gene_summary.tsv where has_complete_cascade = true)
awk -F'\t' 'NR>1 && $7=="true" {print $1}' gene_summary.tsv > cascade_genes.txt
# Run coloc
hvantk qtlcascade coloc \
--eqtl-allpairs /data/tables/eqtl_allpairs_liver.ht \
--pqtl-allpairs /data/tables/pqtl_allpairs_liver.ht \
--cascade-genes cascade_genes.txt \
--tissue Liver \
--window-kb 500 \
-o coloc_results.tsvhvantk qtlcascade gwas-coloc colocalizes a GWAS locus against cis-eQTL to rank the likely
effector gene(s), then (by default) confirms the lead effector with SuSiE-RSS + coloc.susie
fine-mapping. All summary statistics stream via remote tabix — no bulk downloads.
The fine-mapping step is what separates a genuine colocalization (CONFIRMED) from a
single-variant-ABF artifact (REFUTED). The example below shows both, anchored on a
literature-supported positive control.
MYOZ1 (10q22) is a known cardiac effector for atrial fibrillation. Confirm it end-to-end
(FinnGen I9_AF × GTEx heart atrial-appendage eQTL QTD000251):
hvantk qtlcascade gwas-coloc \
--endpoint I9_AF --chrom 10 --lead 73600000 \
--eqtl QTD000251 \
--gene ENSG00000177791 \
--gwas-n 261395 --eqtl-n 372 \
-o results/myoz1Expected output (values are illustrative; remote data may shift slightly between releases):
Region chr10:73100000-74100000 | GWAS min-p 2.7e-14 | 51 genes tested
Top effector: ENSG00000177791 (PP4=0.812)
Fine-map: credible sets GWAS=1 eQTL=1; coloc.susie PP4=0.649
VERDICT: CONFIRMED (ABF + fine-mapping agree)
Report: results/myoz1/report_I9_AF_10_73600000.json
ENSG00000177791isMYOZ1. The tool reports Ensembl gene IDs; HGNC symbol mapping is a planned enhancement.
The ABF ranking is pure-Python (only the core deps). Skip fine-mapping with --no-fine-map
(then --gwas-n/--eqtl-n are not required):
hvantk qtlcascade gwas-coloc \
--endpoint I9_AF --chrom 10 --lead 73600000 \
--eqtl QTD000251 --no-fine-map \
-o results/myoz1_abf
# Top effector: ENSG00000177791 (PP4=0.812)
# VERDICT: SUGGESTIVE (ABF only; fine-mapping not run)The septal-defect 17q21 locus has an identical-looking ABF signal (PP4 ≈ 0.81 for NSF) that
fails fine-mapping — neither trait yields a credible set, so the ABF hit is a single-variant
artifact:
hvantk qtlcascade gwas-coloc \
--endpoint Q17_SEPTA_DEFEC --chrom 17 --lead 46890164 \
--eqtl QTD000136 \
--gene ENSG00000073969 \
--gwas-n 412181 --eqtl-n 213 \
-o results/nsf_17q21
# Top effector: ENSG00000073969 (PP4=0.81)
# Fine-map: credible sets GWAS=0 eQTL=0; coloc.susie PP4=0.0
# VERDICT: REFUTED (no fine-mappable signal — single-variant-ABF artifact)This CONFIRMED-vs-REFUTED contrast is why the fine-mapping layer matters: single-variant ABF
over-calls when a strong GWAS meets a weak eQTL.
| Tier | Requirements |
|---|---|
ABF only (--no-fine-map) |
core hvantk deps (pysam, numpy, pandas) + network |
| Fine-mapping (default) | same core deps — fine-mapping is pure-Python SuSiE-RSS + coloc.susie (no R / bcftools / curl). Streams a 1000G GRCh38 LD reference for the chosen --superpop (default EUR), cached under --ld-cache-dir (default: $HVANTK_LD_CACHE, else ~/.cache/hvantk/1kg); or pass a local --ld-vcf for an offline LD reference (the GWAS/eQTL summary statistics are still streamed) |
Data sources (all remote, no downloads): FinnGen R10 GWAS, eQTL Catalogue (GTEx) cis-eQTL, and 1000 Genomes high-coverage GRCh38 for the LD reference.
from hvantk.algorithms.qtlcascade.gwas_pipeline import (
GwasColocConfig, run_gwas_coloc_pipeline,
)
config = GwasColocConfig(
endpoint="I9_AF", chrom="10", lead=73600000,
eqtl_dataset="QTD000251", gene_of_interest="ENSG00000177791",
gwas_N=261395, eqtl_N=372, fine_map=True,
output_dir="results/myoz1",
)
report = run_gwas_coloc_pipeline(config)
print(report["verdict"]) # CONFIRMED (ABF + fine-mapping agree)
print(report["results"]["top_PP4"]) # ~0.81| Source | Type | Format | Reference |
|---|---|---|---|
| GTEx v11 | eQTL | Parquet | GTEx Consortium |
| GTEx v8 | eQTL | TSV | GTEx Consortium |
| eQTLGen | eQTL | TSV | Vosa et al. (2021) |
| Fang et al. 2025 | pQTL | Space-delimited TSV | Fang et al. (2025) |
| FinnGen R10 | GWAS | Remote-tabix (bgzip+tbi) | FinnGen (2023) |
| eQTL Catalogue (GTEx) | cis-eQTL | Remote-tabix (bgzip+tbi) | Kerimov et al. (2021) |
| 1000 Genomes (GRCh38) | LD reference | VCF (remote-tabix) | 1000G / NYGC (2020) |
See Data Sources for download instructions.
Documentation: QTL Cascade Docs