Code and released tables behind the AD Loci Explorer, a browsable view of the ADSP FunGen-xQTL release across 196 candidate Alzheimer's disease loci.
Live app: https://xqtl.niagads.org/AD-loci-explorer/ (direct link: https://jenny-empawi.shinyapps.io/xQTL-AD-loci-explorer/)
scripts/
build_AD_locus_table.R locus-level build: evidence integration and tier assignment
gene_prio_utils.R helper functions, including the T1-T6 tier rules
convert_issac_coloc.R keys single-nucleus sQTL (ISSAC) x AD colocalization exports to genes
add_evidence_source.R register a new evidence source (see 'Adding data')
check_inputs.R checks every file in a build input list exists and matches its md5
build_record.R writes the build record, <out>/_build_record/ (see Running it)
compare_builds.R compares two builds table by table
make_processed_inputs.sh regenerates the processed inputs from the raw exports
convert_*.R, drop_*.R, dedup_against_export.R the processing steps make_processed_inputs.sh runs
validate_outputs.R check a release against the expectations of this build
run_pipeline.qsub end-to-end job script for the SCC scheduler
config/ metadata tables the build reads
data/ released tables, one directory per build date
app/ Shiny application, runnable from a clone
DEPENDENCIES.md R version and package versions
R 4.5.3 with the packages listed in DEPENDENCIES.md:
install.packages(c("data.table","stringr","openxlsx","tidyverse","ggplot2",
"scales","circlize","shiny","bslib","DT","plotly",
"shinycssloaders"))
remotes::install_github("StatFunGen/pecotmr") # not on CRANThere are two ways in, depending on whether you want to reproduce the published release or add your own data to it.
Rebuilds the locus table from the registered inputs. This needs access to the
tables listed in config/metadata_analysis.csv, which are controlled-access; see
Inputs for how to obtain them.
export AD_LOCI_ROOT=/path/to/AD_loci_xQTL # where the input trees live
export AD_LOCI_STAGING=/path/to/staging # three precomputed tables, see Inputs
export AD_LOCI_OUT=$AD_LOCI_ROOT/out_$(date +%Y%m%d) # optional
export AD_LOCI_CONFIG=/path/to/private/config # optional, see Inputs
Rscript scripts/build_AD_locus_table.R # -> $AD_LOCI_OUT
Rscript scripts/validate_outputs.R $AD_LOCI_OUT # optional count check: AD_LOCI_EXPECT_LOCI, AD_LOCI_EXPECT_GENES
Rscript app/build_shiny_data.R $AD_LOCI_OUT app/data.csvOn the SCC the same steps run as one job. From the root of a clean checkout of the commit you want to build:
qsub -v AD_LOCI_INPUTS=data/build_inputs/YYYYMMDD.inputs.csv,AD_LOCI_OUT=/path/to/out_YYYYMMDD scripts/run_pipeline.qsubThe job:
- refuses to write into an output folder that already has files (override with
AD_LOCI_ALLOW_EXISTING_OUT=1); - runs
check_inputs.R, which stops the job if any file the build will read is missing, has changed, or is not in the input list; - builds and validates the tables;
- writes the build record (below);
- writes
_COMPLETEonly if the build, the validation and the build record all succeeded.
Before anything runs, the job copies the input list and the config (config/, or AD_LOCI_CONFIG) into the build record; the input check, the build and the record all use those copies, so editing the originals during a run cannot change it. The three precomputed tables are read in place from $AD_LOCI_ROOT/staging_gene_prio (or AD_LOCI_STAGING) and are never written to. Scratch files go to <out>/_scratch.
Each build has one record, <out>/_build_record/:
| file | contents |
|---|---|
run.csv |
run id, code commit and whether the checkout had local changes, input list and its sha256, config sha256, R and package versions, host, user, time |
inputs.csv |
the input list the build used (copied before the build) |
config/ |
the config files the build read (copied before the build), with MD5SUMS |
registry_runtime.csv |
the registry as the build filled it in (run-time paths of intermediate tables) |
check_inputs.txt, validation.txt |
output of the input check and of the validation |
outputs_md5.csv, summary.csv |
checksum of every output file; headline counts |
changes_since_previous.txt |
inputs, counts and outputs that differ from the build in AD_LOCI_PREV, when set |
environment.txt |
R session information |
The build uses NSLOTS threads. The per-file overlap step runs in parallel; set AD_LOCI_OVERLAP_WORKERS to change the number of workers. Set AD_LOCI_RSCRIPT to choose which Rscript runs the steps (default: ~/.pixi/bin/Rscript, otherwise the one on PATH).
The build checks every required input before starting and stops with a list of whatever is missing, rather than failing part-way through a long run.
Register new results as an evidence source and rebuild with them included.
scripts/add_evidence_source.R converts a ColocBoost-style colocalization
summary and adds its rows to the registry; see Adding data for the input format,
and for what a source in any other format needs.
Rscript scripts/add_evidence_source.R --name mysource --bed coloc_summary.bed
Rscript scripts/build_AD_locus_table.RAdding a source is a configuration change, not a code change. Be aware that the rebuild still reads the whole registry, so this route needs the same input access as option 1; it saves you from modifying the pipeline, not from having the data.
If all you want is the Explorer, neither option is needed: app/data.csv ships
with the repository. See Running the app.
Three things are versioned separately:
| what | how |
|---|---|
| code, config, processing recipe | git commit (each build records its commit in _build_record/run.csv) |
| accepted inputs | the dated input list in data/build_inputs/ (path, md5, source, parents and recipe); files on project storage are not modified once listed |
| published releases | git tag release-YYYYMMDD on the commit that added the release's tables to data/; corrections get a new tag |
Work happens on short-lived branches that are merged and then deleted.
config/metadata_analysis.csv is the registry of every input the build reads.
One row per exported analysis table:
| column | meaning |
|---|---|
| context | dataset label, joined to config/contexts_metadata.csv |
| Data Type | eQTL, sQTL, pQTL, ... |
| Cohort | ROSMAP, MSBB, KNIGHT, ... |
| Modality | assay or analysis modality |
| Method | how the table was produced; selects the loader in the build |
| Path | location of the table, relative to $AD_LOCI_ROOT |
| variant_level_method | whether the method contributes variant-level evidence |
Paths under $AD_LOCI_ROOT refer to the FunGen-xQTL release, Synapse
syn68872650. The layout on Synapse does not necessarily mirror these relative
paths; email jaempawi@bu.edu if you need help locating a particular table. Some
rows carry <user> and <collaborator> placeholders where
tables were exported from per-user analysis directories; replace these with the
directory names in your own copy.
Three precomputed tables are too large to distribute with the code and are read
from $AD_LOCI_STAGING:
| file | size | source |
|---|---|---|
| gwas_variants_cor0.5.csv.gz | 295 MB | Synapse syn75082260 |
| res_APOE_interaction_summ.csv.gz | 45 MB | on request |
| res_msex_interaction_summ.csv.gz | 1.6 MB | on request |
The two interaction summaries are not redistributed publicly. To request them, open an issue on this repository or email jaempawi@bu.edu.
The correlation table drives the LD-based credible-set extension, so the released variant sets cannot be reproduced without it.
Each build has an input list in data/build_inputs/, named by build date (YYYYMMDD.inputs.csv). One row per file the build reads, with its path, md5, size, who provided it and where it came from. Files that were processed before use (for example, with signals removed or duplicates dropped) also list their parents (path and md5) and the recipe that produced them; scripts/make_processed_inputs.sh reruns every recipe and reproduces those files exactly. Rows with role = reference are lookup files rather than evidence.
A few delivered files are changed before the build reads them. The original file is kept and listed as the parent; every step below is run by scripts/make_processed_inputs.sh.
| input | what is done | why | script |
|---|---|---|---|
| ROSMAP AD x xQTL colocalization | remove the two TMEM106B glycoprotein (DLPFC_Klein gpQTL) signals | they are not in Ru's Oct 16 glycoQTL rerun | drop_events.R |
| tpQTL x AD colocalization | remove ROSMAP eQTL outcomes that are already in the main ROSMAP export | avoid counting the same colocalization twice | dedup_against_export.R |
| TransMap x AD colocalization | remove the CoS at the TMEM106B locus, group rows into colocalization sets, then write the trans part as one file per context and the AD + cis part as one ColocBoost-style file | the build reads trans evidence one context at a time, and reads cis colocalization with the other AD x xQTL files; trans is shown as annotation only and does not set tiers | drop_cos.R, transmap_export_to_sets.R, convert_transmap_AD_target_trans.R, convert_transmap_AD_target.R |
| single-nucleus sQTL (ISSAC) x AD colocalization | map each splice site to its gene (GENCODE v32 genes and the sn-sQTL association table) | the build links colocalization to genes, and these exports carry splice sites | convert_issac_coloc.R |
| glycoQTL x AD colocalization | reshape to one row per variant per colocalization set | match the format of the other AD x xQTL files | convert_glyco_AD_coloc.R |
| tpQTL fine-mapping (genome-wide table) | split by context (AC, DLPFC, PCC); rows unchanged | the build reads fine-mapping one context at a time | in make_processed_inputs.sh |
To check a set of inputs before a run:
Rscript scripts/check_inputs.R data/build_inputs/YYYYMMDD.inputs.csv $AD_LOCI_ROOTTo see what changed between two builds:
Rscript scripts/compare_builds.R /path/to/out_A /path/to/out_BTo add a new evidence source you register it in config/metadata_analysis.csv.
The build auto-detects any Method there that points at per-context toploci files
and has no dedicated loader, so registration is the only step.
For a ColocBoost-style colocalization summary, scripts/add_evidence_source.R does the
conversion and registration for you:
Rscript scripts/add_evidence_source.R --name transmap \
--bed trans_xQTL_only_colocalization_summary_table.bed
Rscript scripts/build_AD_locus_table.RIt is safe to re-run: conversion is skipped when outputs already exist, and rows
for the same --name are replaced rather than duplicated.
The --bed file is tab-delimited with a header, one row per variant, and event
lists delimited by ;:
| # | column | notes |
|---|---|---|
| 1 | #chr | |
| 2 | start | |
| 3 | end | |
| 4 | a1 | |
| 5 | a2 | |
| 6 | variant_ID | chr:pos:ref:alt |
| 7 | region_ID | |
| 8 | event_ID | semicolon-delimited list; context is the part before _cis_ or _trans_ |
| 9 | cos_ID | must contain cosN; rows with cos0 are skipped |
| 10 | vcp | becomes PIP in the emitted file |
| 11 | cos_npc | becomes purity |
| 14 | zscore | semicolon-delimited, positionally matched to event_ID |
It emits one toploci file per context with the columns the build expects:
#chr start end a1 a2 variant_ID region_ID event_ID cs_coverage_0.95 purity PIP z.
A source in any other format needs its own loader in scripts/build_AD_locus_table.R;
follow one of the existing Method == blocks.
To change the code and test the change against a baseline build, see CONTRIBUTING.md.
The build reads every input through one project folder (AD_LOCI_ROOT). On a cluster
where the inputs sit in different places, make that folder out of links, using the
location column of the input list (columbia_path for the Columbia cluster):
pixi install # R and packages at the pinned versions (pixi.toml, pixi.lock)
Rscript scripts/make_input_root.R data/build_inputs/YYYYMMDD.inputs.csv /path/to/input_root
AD_LOCI_STAGING=/path/to/input_root/staging_gene_prio \
Rscript scripts/check_inputs.R data/build_inputs/YYYYMMDD.inputs.csv /path/to/input_root config scripts/build_AD_locus_table.R
sbatch --export=ALL,AD_LOCI_ROOT=/path/to/input_root,AD_LOCI_INPUTS=data/build_inputs/YYYYMMDD.inputs.csv,AD_LOCI_OUT=/path/to/out_YYYYMMDD,AD_LOCI_RSCRIPT=$PWD/.pixi/envs/default/bin/Rscript scripts/run_pipeline.slurmmake_input_root.R only makes links; it stops if any input is missing on that cluster.
The input check then confirms every file is byte-identical to the input list, and
scripts/compare_builds.R <release> <new build> shows whether the result matches a
release exactly. run_pipeline.slurm runs the same steps as run_pipeline.qsub and
needs about 64 GB of memory.
This section describes what the build does, step by step, and the thresholds it applies. Two terms recur. A credible set is the small group of variants that fine-mapping says most likely contains the causal one; 95% coverage (cs95) means the set is built to have a 95% chance of containing it, and cs70 and cs50 are weaker versions of the same idea. Colocalization asks whether an AD signal and a molecular signal in the same place are driven by the same variant rather than by two neighbouring ones.
The locus set is imported, not derived here: GWAS fine-mapping is run upstream
and is not repeated by this pipeline. The credible sets behind the current
build come from eight AD GWAS studies fine-mapped with SuSiE-RSS EB-mix (the
final R4_L10 run); the earlier 20260514 release used SuSiE-RSS version 1 on the same studies, which is
why the two are not nested (see data/README.md).
At startup the build scans the registry
for the analysis methods that point at per-context top-loci files and reports
what it found, so the source list is discovered from the configuration rather
than hardcoded. The job log records it as [toploci] auto-detected sources.
Nothing is merged until the identifiers agree.
Contexts. Dataset and assay labels are mapped to one harmonized context name
through config/contexts_metadata.csv. Single-nucleus tables that carry a cell
type instead of a context are mapped by cell type.
Variants. Identifiers arrive in several spellings. Underscores are converted
to colons and a chr prefix is added where it is missing, but the merge itself
is done on chromosome and position rather than on the identifier string, and the
AD locus table's spelling is then adopted for the merged row. This is what
absorbs allele-order differences: where two sources name the same position with
the alleles written the other way round, the merge still succeeds, and the build
counts the disagreeing spellings instead of dropping those variants (213 of
5,318 in this build).
Events and genes. Event identifiers have trailing _chr, _ENSG and _gp
fragments stripped. Genes are matched on the Ensembl ID with the version suffix
removed, so ENSG00000141510.16 and ENSG00000141510 are treated as the same
gene.
GWAS studies. Source and study names are harmonized so that one GWAS is not counted twice under two labels.
| evidence | what makes it count |
|---|---|
| fine-mapping | membership in a credible set; the coverage level is recorded on each row as cs95, cs70 or cs50, and the strongest level available is kept per locus |
| colocalization | a variant-level colocalization probability (VCP), or the SNP-level PPH4 where that is what the method reports |
| TWAS | gene-level p < 2.5e-6, either in the selected best method or in at least half of the methods run, evaluated per gene, block, context and GWAS source |
| MR | TWAS-significant, and cPIP > 0.5, and at least two credible sets, and I2 < 0.5 |
| cTWAS | a non-empty credible set and SuSiE PIP > 0.75 |
A row that names a method but lacks that method's statistic is dropped, so a colocalization row with no VCP and a fine-mapping row with no PIP never enter the table.
Each surviving row is reduced to a single variant_inclusion_probability by
precedence rather than by averaging: the colocalization VCP when present,
otherwise the fine-mapping PIP, otherwise the SNP-level PPH4. The maximum across
every method and source is then taken per variant, and the method that produced
that maximum is recorded beside it. Because it is a maximum, adding a source can
only raise a variant's score and never lower it; a score that falls between two
builds therefore means the inputs or the filtering changed, not merely that
something was added. The GWAS methods, sources and effect directions behind each
variant are collapsed into parallel |-separated fields on the same row.
A locus identifier is built from its chromosome and the first and last positions of its member variants, and the loci are then numbered in order. That number is an ordinal within a single build, so the same number does not denote the same locus in another build and cross-build comparisons have to be made on variant IDs.
Only sets that qualify on their own form loci: AD GWAS credible sets at 95% coverage and colocalization sets. cs70 and cs50 sets are dropped as whole sets before sets are merged into loci, so a weaker set cannot join a locus by overlapping a qualifying one.
Sets are then merged by linkage disequilibrium. There are no fixed LD blocks:
AD variants with no gap larger than 1 Mb form one window (2 Mb, ±1 Mb). Within
each window, LD between AD variants up to 1 Mb apart is computed during the build
from the ADSP R4 EUR LD sketch with pecotmr (scripts/ld_sketch.R); only the AD
variants are read from the sketch, and no precomputed LD table is used. Two sets
are merged when some variant pair between them has |r| > 0.8 and every pair in
their union has |r| > 0.5, and merging repeats until no further sets join. Sets
that share a variant end up in the same locus.
There is no GWAS p-value filter: every locus built from a qualifying set is kept, because a locus with a marginal p-value above 1e-5 can still have a small joint p-value. Each locus is represented by up to five variants whose GWAS PIP or VCP exceeds 0.1; where no variant clears 0.1, the single best one is shown, ranked on the GWAS probability first and the xQTL probability second. GWAS significance is reported in bands: p < 5e-8, p < 1e-6, and p ≥ 1e-6.
Two flags are carried per locus: whether it is supported only by proxy-based GWAS, and whether it falls in the APOE region (chr19:43,905,790-45,905,791), which is reported separately because the extreme linkage disequilibrium there makes fine-mapping and colocalization hard to read.
Tiers are assigned per variant, gene and context group by the rules in the next
section, and each gene is reported at the strongest tier it reaches anywhere.
The build writes the locus-level summary, the variant-level membership table and
the unified workbook to $AD_LOCI_OUT; what the run used (code commit, inputs,
config, environment) goes into the build record, _build_record/. scripts/validate_outputs.R then
checks that release: the expected tables are present, and the locus and gene
counts match AD_LOCI_EXPECT_LOCI / AD_LOCI_EXPECT_GENES when those are set.
Two further steps run after the build. scripts/locus_pair_ld.R reports, for
every pair of loci, the strongest and weakest |r| between their variants
(locus_pair_min_abs_ld.csv.gz), so loci that the merge rule could have joined
are visible. scripts/annotate_transmap_xqtl_only.R overlaps the genome-wide
TransMap trans-xQTL-only colocalization with the AD loci and records, per locus,
whether xQTL support is cis only, trans only, or both.
scripts/locus_gene_report.R summarizes how many genes and cell types have
tiered support at each AD locus (T1-T4, and with T5 added; cell types counted with
and without bulk brain tissue; myeloid support from microglia, monocytes or
macrophages), and at how many loci each gene has tiered support, with and
without the APOE region (locus_gene_report.txt, locus_gene_support.csv.gz).
The output directory is deliberately created empty. The GWAS credible-set harmonization recomputes from its inputs rather than reusing a previous release's artifacts, so a rerun cannot silently inherit stale intermediate files.
Tiers T1–T5 need localized cis-xQTL support; genes implicated only by trans-xQTL are reported for reference. TransMap trans colocalization (TransMap × AD, and the genome-wide TransMap xQTL-only results overlapped with the AD loci) counts as trans xQTL support for a locus (discovery) but not toward gene tiers. AD GWAS credible sets (CS) come from SuSiE-rss. Gene-level xQTL credible sets come from single-context SuSiE and multi-context or multi-gene mvSuSiE. A 95% CS overlap is at least one variant shared by an AD GWAS 95% CS and an xQTL 95% CS. CoS support is AD and a cis molecular trait of the gene in the same ColocBoost event with a 95% CoS. Epigenomic evidence (regional fSuSiE credible sets, and fSuSiE colocalization of AD with methylation, histone and snATAC QTL) supports a locus but does not count toward gene tiers, until epigenomic targets are linked to genes in a principled way. Gene-level TWAS, MR and cTWAS evidence follows the regulome-wide association companion study. Rules are checked in order.
| tier | evidence |
|---|---|
| T1 | 95% CS overlap and CoS support, with MR or cTWAS |
| T2 | 95% CS overlap or CoS support, with MR or cTWAS |
| T3 | 95% CS overlap or CoS support, with TWAS |
| T4 | CoS support and an xQTL 95% CS of the same gene and context sharing at least one variant with the CoS; no separate AD GWAS CS needed |
| T5 | other localized evidence: CoS alone, or an AD GWAS 95% CS overlapping an xQTL CS at 95% or 70% |
| T6 | gene-level evidence only: TWAS, MR, or cis quantile/interaction QTL at variants in an AD GWAS 95% CS |
scripts/gene_prio_utils.R assigns top_confidence per row during the build.
Evidence outside these rules gives no tier: 50% credible sets, AD credible sets
below 95%, single-nucleus sQTL association without colocalization, and trans
evidence. Genes with TWAS, MR or cTWAS results but no row in the xQTL overlap
table are given T6 from the gene-level tables produced earlier in the same run.
A gene is reported at its strongest tier. The app takes its tiers from the release, so the table and the Explorer cannot diverge.
shiny::runApp("app")app/data.csv ships with the repository, so the Explorer runs without the
pipeline. Note that app/build_shiny_data.R carries a number of gene-level
columns forward from the existing app/data.csv, so that file is an input as
well as an output.
Derived from ADSP/NIAGADS study data. Use of the underlying controlled-access resources is governed by their own data use terms.