A phenome-wide association study (PheWAS) is a powerful tool for the discovery and replication of genetic associations across many phenotypes. Large biobanks provide deep genomic and phenotypic data, but testing thousands of traits against whole-genome variants remains computationally demanding. SAIGE (scalable and accurate implementation of generalized mixed models [2]) addresses case-control imbalance and sample relatedness in association studies.
SAIGEgds is a high-performance implementation of the SAIGE framework using Genomic Data Structure (GDS) files [1,3]. Its optimized C++ code takes advantage of sparse genotype dosages and supports integer genotypes and numeric imputed dosages. SAIGEgds supports binary, quantitative and time-to-event outcomes, as well as single-variant and set-based association tests. This vignette focuses on the binary-trait, single-variant workflow; time-to-event outcomes are covered in the companion survival vignette.
Benchmarks using UK Biobank White British genotype data (\(N=430\mathrm{K}\)) with coronary heart disease and simulated case-control outcomes showed that SAIGEgds was 5 to 6 times faster than the SAIGE R package for null-model fitting and p-value calculation. Together with high-performance computing (HPC) clusters or cloud resources, it provides an efficient pipeline for biobank-scale PheWAS.
if (!requireNamespace("BiocManager", quietly=TRUE))
install.packages("BiocManager")
BiocManager::install("SAIGEgds")
The BiocManager::install() approach may build the package from source. In that case, make and a suitable compiler toolchain must be installed; see the R FAQ for platform-specific instructions.
library(SeqArray)
library(SAIGEgds)
# the genotype file in SeqArray GDS format (1000 Genomes Phase1, chromosome 22)
(geno_fn <- seqExampleFileName("KG_Phase1"))
## [1] "/Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/library/SeqArray/extdata/1KG_phase1_release_v3_chr22.gds"
# open a SeqArray file in the package (1000 Genomes Phase1, chromosome 22)
gds <- seqOpen(geno_fn)
The LD pruning is provided by snpgdsLDpruning() in the package SNPRelate:
library(SNPRelate)
## SNPRelate -- supported by Streaming SIMD Extensions 2 (SSE2)
set.seed(1000)
snpset <- snpgdsLDpruning(gds)
## SNV pruning based on LD:
## Calculating allele counts/frequencies (19773 variants) ...
##
[..................................................] 0%, ETC: --- (1/1)
[==================================================] 100%, used 1s (1/1)
[==================================================] 100%, complete, 1s
## # of selected variants: 9,043
## Excluding 10,730 SNVs (monomorphic: TRUE, MAF: 0.005, missing rate: 0.01)
## # of samples: 1,092
## # of SNVs: 9,043
## using 1 thread/core
## sliding window: 500,000 basepairs, Inf SNPs
## |LD| threshold: 0.2
## method: composite
## Chrom 22: |====================|====================|
## 14.58%, 2,883 / 19,773 (Thu Sep 24 20:16:22 2026)
## 2,883 markers are selected in total.
str(snpset)
## List of 1
## $ chr22: int [1:2883] 1 8 9 10 12 16 17 18 20 23 ...
snpset.id <- unlist(snpset, use.names=FALSE) # get the variant IDs of a LD-pruned set
head(snpset.id)
## [1] 1 8 9 10 12 16
Create a genotype file for the genetic relationship matrix (GRM) using the LD-pruned SNP set:
grm_fn <- "grm_geno.gds"
seqSetFilter(gds, variant.id=snpset.id)
## # of selected variants: 2,883
# export to a GDS genotype file without annotation data
seqExport(gds, grm_fn, info.var=character(), fmt.var=character(), samp.var=character())
## ##< 2026-09-24 20:16:22
## Export to 'grm_geno.gds':
## sample.id (1,092) [md5: bd2a93b49750ae227793ed23c575b620]
## variant.id (2,883) [md5: 37e34ffd0328a6bb5bcedb53dddad2e3]
## position [md5: 9f2c5f81ad3709aa4e8c4549a82d2175]
## chromosome [md5: 7b4b80eaf4724d87d0d9b73aab237580]
## allele [md5: 20bd0aa343398d59112bd4b58f562f68]
## genotype [md5: be854ad6089ef332349ff25a99674949]
## phase [md5: 94c1e11249a7e2a4426b02bf852af5cb]
## annotation/id [md5: 2020922f9380eddb5e5f8e271ade54e2]
## annotation/qual [md5: 5d14e0a74e09172e9192c42b646c1559]
## annotation/filter [md5: 9bcb8f5aa38515d50d47129d3c57a542]
## Done. # 2026-09-24 20:16:27
## Optimize the access efficiency ...
## Clean up the fragments of GDS file:
## open the file 'grm_geno.gds' (343.6K)
## # of fragments: 101
## save to 'grm_geno.gds.tmp'
## rename 'grm_geno.gds.tmp' (343.2K, reduced: 444B)
## # of fragments: 64
## ##> 2026-09-24 20:16:27
If genotypes are split by chromosomes, seqMerge() in the SeqArray package can be used to combine the LD-pruned SNP sets.
# close the file
seqClose(gds)
A simulated phenotype data set is used to demonstrate model fitting. sample.col identifies the phenotype column that is matched to sample IDs in the GDS file; samples without complete model variables or without matching genotypes are excluded.
set.seed(1000)
sampid <- seqGetData(grm_fn, "sample.id") # sample IDs in the genotype file
pheno <- data.frame(
sample.id = sampid,
y = sample(c(0, 1), length(sampid), replace=TRUE, prob=c(0.95, 0.05)),
x1 = rnorm(length(sampid)),
x2 = rnorm(length(sampid)),
stringsAsFactors = FALSE)
head(pheno)
## sample.id y x1 x2
## 1 HG00096 0 0.02329127 -0.1700062
## 2 HG00097 0 -1.38629108 -1.0058530
## 3 HG00099 0 0.56339700 -0.6989284
## 4 HG00100 0 -0.25703246 -1.5884806
## 5 HG00101 0 0.02926429 0.7645377
## 6 HG00102 0 -0.44480750 1.9964501
grm_fn
## [1] "grm_geno.gds"
Null-model fitting is the computationally intensive step. The call is shown below but is not run while building this vignette:
# null model fitting using GRM from grm_fn
glmm <- seqFitNullGLMM_SPA(y ~ x1 + x2, pheno, grm_fn, trait.type="binary", sample.col="sample.id")
For a fast and reproducible vignette build, the package includes a model precomputed with the same workflow:
Check the sample count, convergence status and estimated variance components before running association tests:
length(glmm$sample.id)
## [1] 1092
glmm$converged
## [1] TRUE
glmm$tau
## Sigma_E Sigma_G
## 1 0
Note what the components of a binary null model mean:
| Component | Meaning for trait.type="binary" |
|---|---|
coefficients |
fixed effects on the log-odds scale, including the intercept |
tau |
Sigma_E is fixed at 1, and Sigma_G is the variance of the genetic random effect on the log-odds scale |
fitted.values |
\(\mu_i = \Pr(y_i=1)\), the fitted probability of the outcome |
residuals |
\(y_i - \mu_i\), the raw residuals |
var.ratio |
variance ratio(s) used to approximate the score-statistic variance in association tests |
# genetic variants stored in the file geno_fn
geno_fn
## [1] "/Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/library/SeqArray/extdata/1KG_phase1_release_v3_chr22.gds"
# use parallel > 1 in production to distribute variants across processes
assoc <- seqAssocGLMM_SPA(geno_fn, glmm, mac=10, parallel=1)
## SAIGE association analysis:
## 2026-09-24 20:16:30
## open '1KG_phase1_release_v3_chr22.gds'
## trait type: binary
## # of samples: 1,092
## # of variants: 19,773
## MAF threshold: no
## MAC threshold: >= 10
## missing proportion threshold: <= 0.05
## variance ratio for approximation: 1
## genetic model: additive
## Warning: For more accurate model building, the null model should be built using
## SAIGEgds>=v1.9.1!
## # of processes: 1
##
[..................................................] 0%, ETC: --- (1/1)
[==================================================] 100%, used 1s (1/1)
[==================================================] 100%, complete, 1s
## # of variants after filtering by MAF, MAC and missing thresholds: 9,371
## P-value:
## [0,5e-10] (5e-10,5e-08] (5e-08,5e-06] (5e-06,0.0005] (0.0005,1]
## 0 0 0 3 9368
## 2026-09-24 20:16:31
## Done.
head(assoc)
## id chr pos rs.id ref alt AF.alt mac num
## 1 1 22 16051497 rs141578542 A G 0.30494505 666 1092
## 2 2 22 16059752 rs139717388 G A 0.05677656 124 1092
## 3 5 22 16060995 rs2843244 G A 0.06135531 134 1092
## 4 8 22 16166919 ATATTTTCTGCACATATT A 0.01190476 26 1092
## 5 9 22 16173887 GT G 0.03067766 67 1092
## 6 10 22 16205515 rs144309057 G A 0.01098901 24 1092
## beta SE pval method p.norm converged
## 1 0.2214607 0.2194430 0.3128813 Normal 0.3128813 TRUE
## 2 0.4227084 0.4240575 0.3188526 Normal 0.3188526 TRUE
## 3 0.3044670 0.4100805 0.4578107 Normal 0.4578107 TRUE
## 4 -1.0851502 0.8804521 0.2177653 Normal 0.2177653 TRUE
## 5 -0.8195449 0.5587294 0.1424302 Normal 0.1424302 TRUE
## 6 -1.0826898 0.9340256 0.2463890 Normal 0.2463890 TRUE
# basic result checks
c(n.variant=nrow(assoc), n.finite.p=sum(is.finite(assoc$pval)))
## n.variant n.finite.p
## 9371 9371
# filtering based on p-value
assoc[assoc$pval < 5e-4, ]
## id chr pos rs.id ref alt AF.alt mac num beta
## 8422 17856 22 48507315 rs57751251 T C 0.1666667 364 1092 0.971123
## 8423 17857 22 48509092 rs7292083 C T 0.1625458 355 1092 1.008351
## 8630 18282 22 49060987 rs4925399 A G 0.5929487 889 1092 0.708757
## SE pval method p.norm converged
## 8422 0.2555021 1.442051e-04 SPA 5.999175e-05 TRUE
## 8423 0.2585365 9.610252e-05 SPA 3.358045e-05 TRUE
## 8630 0.1956659 2.920160e-04 SPA 2.363433e-04 TRUE
The main result columns report the variant location and allele frequency, effect estimate (beta), standard error (SE) and p-value (pval). The method and converged columns indicate which p-value approximation was used and whether its numerical calculation converged.
The output can also be saved directly to an R object file or a GDS file:
# save to 'assoc.gds'
seqAssocGLMM_SPA(geno_fn, glmm, mac=10, parallel=1, res.savefn="assoc.gds")
## SAIGE association analysis:
## 2026-09-24 20:16:32
## open '1KG_phase1_release_v3_chr22.gds'
## trait type: binary
## # of samples: 1,092
## # of variants: 19,773
## MAF threshold: no
## MAC threshold: >= 10
## missing proportion threshold: <= 0.05
## variance ratio for approximation: 1
## genetic model: additive
## Warning: For more accurate model building, the null model should be built using
## SAIGEgds>=v1.9.1!
## # of processes: 1
## Save to 'assoc.gds' ...
##
[..................................................] 0%, ETC: --- (1/1)
[==================================================] 100%, used 1s (1/1)
[==================================================] 100%, complete, 1s
## # of variants after filtering by MAF, MAC and missing thresholds: 9,371
## P-value:
## [0,5e-10] (5e-10,5e-08] (5e-08,5e-06] (5e-06,0.0005] (0.0005,1]
## 0 0 0 3 9368
## 2026-09-24 20:16:33
## Done.
Open the output GDS file using the functions in the gdsfmt package:
# open the GDS file
(f <- openfn.gds("assoc.gds"))
## File: /private/tmp/Rtmpxt1SL1/Rbuild26dc58fadfe6/SAIGEgds/vignettes/assoc.gds (506.6K)
## + [ ] *
## |--+ sample.id { Str8 1092 ZIP(26.0%), 2.2K }
## |--+ id { Int32 9371 ZIP(35.1%), 12.8K }
## |--+ chr { Str8 9371 ZIP(0.18%), 52B }
## |--+ pos { Int32 9371 ZIP(83.8%), 30.7K }
## |--+ rs.id { Str8 9371 ZIP(40.7%), 38.8K }
## |--+ ref { Str8 9371 ZIP(28.0%), 105.4K }
## |--+ alt { Str8 9371 ZIP(22.4%), 4.2K }
## |--+ AF.alt { Float64 9371 ZIP(27.4%), 20.1K }
## |--+ mac { Int32 9371 ZIP(41.6%), 15.2K }
## |--+ num { Int32 9371 ZIP(0.17%), 64B }
## |--+ beta { Float64 9371 ZIP(94.3%), 69.1K }
## |--+ SE { Float64 9371 ZIP(92.8%), 67.9K }
## |--+ pval { Float64 9371 ZIP(92.9%), 68.0K }
## |--+ method { Int32,factor 9371 ZIP(1.62%), 606B } *
## |--+ p.norm { Float64 9371 ZIP(92.9%), 68.0K }
## \--+ converged { Int32,logical 9371 ZIP(0.17%), 62B } *
# get p-values
pval <- read.gdsn(index.gdsn(f, "pval"))
summary(pval)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.0000961 0.2862278 0.4639086 0.4980417 0.7256674 0.9999449
closefn.gds(f)
Load association results using the function seqSAIGE_LoadPval() in SAIGEgds:
res <- seqSAIGE_LoadPval("assoc.gds")
## Loading 'assoc.gds' ...
head(res)
## id chr pos rs.id ref alt AF.alt mac num
## 1 1 22 16051497 rs141578542 A G 0.30494505 666 1092
## 2 2 22 16059752 rs139717388 G A 0.05677656 124 1092
## 3 5 22 16060995 rs2843244 G A 0.06135531 134 1092
## 4 8 22 16166919 ATATTTTCTGCACATATT A 0.01190476 26 1092
## 5 9 22 16173887 GT G 0.03067766 67 1092
## 6 10 22 16205515 rs144309057 G A 0.01098901 24 1092
## beta SE pval method p.norm converged
## 1 0.2214607 0.2194430 0.3128813 Normal 0.3128813 TRUE
## 2 0.4227084 0.4240575 0.3188526 Normal 0.3188526 TRUE
## 3 0.3044670 0.4100805 0.4578107 Normal 0.4578107 TRUE
## 4 -1.0851502 0.8804521 0.2177653 Normal 0.2177653 TRUE
## 5 -0.8195449 0.5587294 0.1424302 Normal 0.1424302 TRUE
## 6 -1.0826898 0.9340256 0.2463890 Normal 0.2463890 TRUE
library(ggmanh)
## Loading required package: ggplot2
g <- manhattan_plot(assoc, pval.colname="pval", chr.colname="chr",
pos.colname="pos", x.label="Chromosome 22")
g
# QQ plot
qqunif(assoc$pval)
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the ggmanh package.
## Please report the issue to the authors.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
SAIGEgds supports optional OpenCL-based GPU acceleration for the GRM cross-product computation during null-model fitting. This can significantly speed up seqFitNullGLMM_SPA() for large sample sizes with dense genotypes.
Requirements:
To enable GPU acceleration, set use.gpu=TRUE:
glmm <- seqFitNullGLMM_SPA(y ~ x1 + x2, pheno, grm_fn,
trait.type="binary", sample.col="sample.id", use.gpu=TRUE)
If no compatible GPU or OpenCL runtime is found, the function automatically falls back to CPU computation. When GPU initialization succeeds, use.gpu=TRUE automatically selects dense 2-bit packed genotype storage; users do not need to set geno.sparse=FALSE separately. The default sparse genotype path remains CPU-only and may be preferable when genotypes are sparse.
GPU Memory Usage:
The GPU memory required depends on the operation and dataset size. SAIGEgds checks that the total allocation does not exceed 80% of device global memory, and falls back to CPU if it does.
nsnp.sub.random subset), the packed genotypes require approximately 200 MB.GPU compute buffers use single-precision (32-bit) floating point. Results are converted back to double precision on the host.
sessionInfo()
## R version 4.6.1 Patched (2026-06-24 r90190)
## Platform: x86_64-apple-darwin20
## Running under: macOS Ventura 13.7.8
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/New_York
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] ggmanh_1.17.0 ggplot2_4.0.3 SNPRelate_1.47.0 SAIGEgds_2.13.3
## [5] Rcpp_1.1.2 SeqArray_1.53.3 gdsfmt_1.49.8 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] tidyr_1.3.2 sass_0.4.10 generics_0.1.4
## [4] lattice_0.23-1 digest_0.6.39 magrittr_2.0.5
## [7] evaluate_1.0.5 grid_4.6.1 RColorBrewer_1.1-3
## [10] bookdown_0.48 CompQuadForm_1.4.4 fastmap_1.2.0
## [13] jsonlite_2.0.0 Matrix_1.7-6 tinytex_0.61
## [16] BiocManager_1.30.27 purrr_1.2.2 scales_1.4.0
## [19] RhpcBLASctl_0.23-42 Biostrings_2.81.9 jquerylib_0.1.4
## [22] cli_3.6.6 rlang_1.3.0 crayon_1.5.3
## [25] XVector_0.53.0 withr_3.0.3 cachem_1.1.0
## [28] yaml_2.3.12 otel_0.2.0 tools_4.6.1
## [31] parallel_4.6.1 dplyr_1.2.1 BiocGenerics_0.59.12
## [34] vctrs_0.7.3 R6_2.6.1 magick_2.9.1
## [37] stats4_4.6.1 lifecycle_1.0.5 Seqinfo_1.3.2
## [40] S4Vectors_0.51.10 IRanges_2.47.5 pkgconfig_2.0.3
## [43] pillar_1.11.1 RcppParallel_6.2.1 bslib_0.12.0
## [46] gtable_0.3.6 glue_1.8.1 tidyselect_1.2.1
## [49] tibble_3.3.1 xfun_0.61 GenomicRanges_1.65.4
## [52] knitr_1.52 dichromat_2.0-1 farver_2.1.2
## [55] htmltools_0.5.9 labeling_0.4.3 rmarkdown_2.32
## [58] compiler_4.6.1 S7_0.2.2
Time-to-event outcomes are covered in Time-to-event (survival) analysis in SAIGEgds (vignette("SAIGEgds_surv", package="SAIGEgds")). Rare-variant set-based tests using full and sparse GRMs are covered in Set-based Rare-Variant Association Analysis in SAIGEgds (vignette("SAIGEgds_set-aggr", package="SAIGEgds")).
SeqArray: Data Management of Large-scale Whole-genome Sequence Variant Calls
SNPRelate: Parallel Computing Toolset for Relatedness and Principal Component Analysis of SNP Data