A lightweight Shiny app for building a curated gene list for a disease: "GDA (gene–disease association) for gene lists". It replaces the manual, API-by-hand curation workflow with a reproducible pipeline:
- Resolve a disease name to an ontology term (EFO/MONDO) via Open Targets.
- Query multiple gene–disease-association sources.
- Aggregate + dedupe the results into a common schema (one row per gene).
- Rank the genes with transparent, tunable, source-weighted scoring.
- Curate the final list with an AI agent (Google Gemini), on demand.
- Export the gene list (CSV) and a run report (Markdown).
Current app version: v0.2.1
Evidence sources add candidate genes and count toward the multi-source coverage bonus:
| Source | Driven by | Key | Notes |
|---|---|---|---|
| Open Targets | disease | no | Overall association score (0–1); also resolves the disease name to an ontology id. |
| PanelApp | disease | no | Genomics England curated diagnostic panels; green/amber confidence → score. |
| DISEASES | disease | no | Text-mined + curated disease–gene associations (keyed by DOID via OT xrefs). |
| ClinVar | disease | no¹ | Genes with pathogenic/likely-pathogenic variants (NCBI E-utilities). |
| DGIdb | gene | no | Drug–gene interaction count (druggability). |
Annotation sources (prioritizers) score genes to nudge ranking but do not inflate coverage:
| Source | Driven by | Key | Notes |
|---|---|---|---|
| gnomAD | gene | no | Loss-of-function constraint (LOEUF); more constrained → higher score. |
| Pharos | gene | no | Target Development Level (Tclin → Tdark). |
¹ ClinVar needs no key; an optional ENTREZ_KEY raises the E-utilities rate limit.
Sources are registered through an extensible registry (R/source_registry.R).
Each entry declares needs (disease or genes) and role (evidence or
annotation). Adding DisGeNET, OMIM, GWAS Catalog, or others is a matter of
writing one adapter that returns the canonical schema and calling
register_source(). No pipeline changes are required.
- R (>= 4.3)
- Packages:
shiny,bslib,brand.yml,dplyr,tibble,tidyr,rlang,DT,shinycssloaders,ellmer,shinyvalidate,cicerone,shinyjs, plusbiohttp,bioclients, andbiobouncer(HTTP transport, database clients, and identifier validation).
renv.lock pins every dependency, including the exact biobouncer version.
That matters for reproducibility: biobouncer bundles the HGNC snapshot used
for the symbol_status column, so the package version determines the result.
if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv")
renv::restore()biohttp, bioclients, and biobouncer are not on CRAN; they are served from
an r-universe repo, so a single install.packages() call covers everything:
install.packages(
c(
"shiny", "bslib", "brand.yml", "dplyr", "tibble", "tidyr",
"rlang", "DT", "shinycssloaders", "ellmer", "shinyvalidate",
"cicerone", "shinyjs",
"biohttp", "bioclients", "biobouncer"
),
repos = c(
"https://samuelbharti.r-universe.dev",
"https://cloud.r-project.org"
)
)The app runs fully without any keys (AI curation degrades to a top-N-by-rank
fallback). To enable Gemini curation, copy .Renviron.example to .Renviron
(git-ignored) and set:
GEMINI_API_KEY=your-key-here
# optional, defaults to gemini-flash-lite-latest
GLB_GEMINI_MODEL=gemini-flash-lite-latestOpen Targets and DGIdb are public and need no credentials.
Caching happens in two tiers. Finished per-source gene tables are stored under
data/cache/ (git-ignored, override with GLB_CACHE_DIR), and the sidebar
"Rebuild from source" toggle bypasses that tier. Underneath, biohttp also
caches the HTTP responses themselves in memory for a short TTL, so that toggle
does not guarantee a fresh network call; set BIOHTTP_CACHE_TTL to shorten it.
shiny::runApp()Or open the project in RStudio and click Run App. Enter a disease (e.g. "lung cancer"), resolve and select the matching term, choose sources, click Build gene list, tune the ranking weights, then optionally Curate with AI and export.
docker build -t gene-list-builder .
docker run --rm -p 3838:3838 -e GEMINI_API_KEY=your-key gene-list-builderThen open http://localhost:3838.
For each gene:
weighted_evidence = sum over present sources of weight * normalized_score
coverage_factor = 1 + bonus * (n_sources - 1) / (n_total - 1)
combined_score = weighted_evidence * coverage_factorOpen Targets/PanelApp scores pass through (already 0–1); count-based scores
(DISEASES, ClinVar, DGIdb) are rank-normalized within the source; gnomAD is
rank-normalized so lower LOEUF scores higher. Annotation sources (gnomAD,
Pharos) contribute to the weighted score but n_sources/coverage count evidence
sources only, so a constraint metric doesn't inflate the multi-source bonus.
Genes present in only some sources are not zero-imputed; breadth of evidence is
rewarded explicitly via the coverage factor. Weights and the multi-source bonus
are tunable live in the sidebar and re-rank instantly without re-querying.
The aggregated table carries a symbol_status column from biobouncer's
bundled (offline) HGNC snapshot: valid, alias of <X>, microRNA (not HGNC),
or unknown. It is diagnostic and never affects scoring.
Sources sometimes name the same gene differently (KMT2A vs the legacy MLL),
which splits one gene into two rows, halves its evidence and understates the
multi-source coverage bonus. Setting GLB_REPAIR_SYMBOLS=1 merges an alias
onto its modern symbol, but only when that modern symbol was already reported
by another source in the same run. That corroboration gate matters:
biobouncer::repair_id() also suggests via a fuzzy match, and would otherwise
turn ORF1AB into the unrelated OR11A1.
It is off by default. On a real lung-cancer run (449 genes) the corpus was 427
valid, 21 microRNA, and 1 alias with no corroborating source, so enabling it
merged nothing. Open Targets and PanelApp already return HGNC-approved symbols.
Check symbol_status on your own diseases before turning it on.
.
├── _brand.yml # Brand colors, fonts, logo (theming)
├── global.R # Libraries + component loader
├── ui.R / server.R # App entry points
├── R/ # Pure logic: schema, sources, ranking, curator
├── modules/ # Shiny modules (disease search, pipeline, etc.)
├── userInterface/ # Page layouts (builder, about)
├── data/cache/ # Cached API responses (git-ignored)
├── tests/testthat/ # Unit, testServer, and shinytest2 smoke tests
└── docs/ # Project documentationshiny::runTests(".") # or: testthat::test_dir("tests/testthat")Pure logic and source adapters are tested without network access (the GraphQL
client and the AI chat are injected as stubs). The shinytest2 smoke test runs
the pipeline against offline demo data via the GLB_TEST_MODE environment
variable and skips when Chrome is unavailable.
- Posit Publisher or Posit Connect for direct app publishing.
- Docker image deployment for a containerized release.
Branding lives in _brand.yml: colors, fonts, and logo in one
place, applied by bslib via bs_theme(brand = TRUE) in ui.R. See
docs/theming.md.
Issues and pull requests are welcome. Read CONTRIBUTING.md first, and please follow the Code of Conduct.
For a security problem, do not open a public issue: SECURITY.md explains how to report it privately.
Samuel Bharti
- Email: [email protected]
- Web: samuelbharti.com
- ORCID: 0000-0003-4190-7058
- GitHub: @samuelbharti
Zenodo archives each release. The badge at the top of this file resolves to the latest version; to cite one specific version, use that version's DOI from the Zenodo record.
CITATION.cff holds the full metadata, and CITATION.md gives a ready-made text and BibTeX entry.
MIT. Copyright (c) 2024-2026 Samuel Bharti.
The app queries public gene-disease databases. Each one sets its own terms for the data it returns, and this license does not change them. The Data sources table links to every one of them.