This vignette describes how to implement the E-GADGETS method using the epistasisGA package. The E-GADGETS method is used to mine genetic data from case-parent triads for higher-order gene-by-environment interactions, in which the joint effect of a set of single nucleotide polymorphisms (SNPs) depends on candidate non-genetic factors. We here refer to non-genetic factors as ‘environmental exposures’, or just ‘exposures’, regardless of whether those factors are actually agents encountered in the environment. For example, disease severity (i.e., high, medium, low) could be considered an ‘exposure’. Additionally, we here refer to gene-by-environment interactions that involve multiple SNPs as ‘GxGxE’ interactions, and those that also involve multiple exposures as ‘GxGxExE’ interactions. E-GADGETS is an extension of the previously described GADGETS method (Nodzenski* et al. 2022), and we advise users to consult the ‘GADGETS’ vignette prior to this one for a more detailed explanation of how GADGETS, and by extension, E-GADGETS, works.
We begin our demonstration of E-GADGETS by loading a simulated example of case-parent triad data. Note that E-GADGETS requires case-parent triads and, unlike GADGETS, does not accommodate disease-discordant siblings. In the example data, we load simulated maternal, paternal, and case genotypes, as well as the exposures. These data represent 24 SNPs from 1,000 families. Rows correspond to families, and columns represent SNP genotypes. Genotypes must be coded as 0, 1, or 2. The exposure is a binary factor with two levels (0, 1). In the input genotype data, SNPs 6, 12, and 18 are simulated to jointly interact with the exposure.
The second step is to pre-process the data. Below, we default to the assumption that SNPs located on the same biological chromosome are in linkage, but users can more carefully tailor this argument based on individual circumstances if desired. For the example data, the SNPs are drawn from chromosomes 10-13, with the columns sorted by chromosome, and 6 SNPs per chromosome. We therefore construct a vector as follows indicating this assumed linkage structure:
This vector indicates that the input genetic data has 4 distinct linkage blocks, with SNPs 1-6 in the first block, 7-12 in the second block, 13-18 in the third block, and 19-24 in the fourth block. Note the ordering of the columns in the input data must be consistent with the specified structure.
Now, we can execute pre-processing:
pp.list <- preprocess.genetic.data(case, father.genetic.data = dad,
mother.genetic.data = mom,
ld.block.vec = ld.block.vec,
categorical.exposures = exposure)
Note that, above, the exposure data were input for the argument
categorical.exposures
. In doing so, the software will treat
each input column as a factor variable and, ultimately, create dummy
variables for each level. On the other hand, if the exposure of interest
were continuous, users would need to specify the argument
continuous.exposures
, which will not dummy code the input.
Strictly speaking, since the example exposure is binary, dummy coding
would not be different from the existing coding, so we could have
specified either argument.
Note that E-GADGETS can, in principle, accept multiple exposures, but
we have not fully tested that idea; specifically, if there are multiple
exposures and at least one is continuous. If users nevertheless wish to
input multiple exposures in which one or more exposures are continuous,
they should be mindful that we have not tested the software in that
context and interpret results with caution. With that said, if users are
interested in simultaneous effects of both continuous and categorical
exposures, the continuous data should be included for
continuous.exposures
and the categorical data should be
specified for categorical.exposures
.
We now run E-GADGETS to nominate SNP-sets whose joint effects appear
to depend on the exposure using the run.gadgets
function. A
more detailed discussion of the run.gadgets
function and
its arguments is available in the ‘GADGETS’ vignette. Like GADGETS,
E-GADGETS requires the user to pre-specify the number of SNPs that may
jointly interact with the exposure, controlled by the
chromosome.size
argument. We recommend running the
algorithm for a range of sizes (2-5 or 2-6), but for this small example,
we will only consider 3-4.
set.seed(100)
run.gadgets(pp.list, n.chromosomes = 5, chromosome.size = 3,
results.dir = "size3_res", cluster.type = "interactive",
registryargs = list(file.dir = "size3_reg", seed = 1300),
n.islands = 8, island.cluster.size = 4,
n.migrations = 2)
run.gadgets(pp.list, n.chromosomes = 5, chromosome.size = 4,
results.dir = "size4_res", cluster.type = "interactive",
registryargs = list(file.dir = "size4_reg", seed = 1400),
n.islands = 8, island.cluster.size = 4,
n.migrations = 2)
Next, we condense the sets of results using the function
combine.islands
. Note that in addition to the results
directory path, the function requires as input a data.frame indicating
the rsIDs (or a placeholder name), reference, and alternate alleles for
each SNP in the data passed to preprocess.genetic.data
as
well as the list output by preprocess.genetic.data
.
data(snp.annotations.mci)
size3.combined.res <- combine.islands("size3_res", snp.annotations.mci,
pp.list)
size4.combined.res <- combine.islands("size4_res", snp.annotations.mci,
pp.list)
The E-GADGETS fitness score involves a standardization step, where
the standardization is based on the fitness scores from 10,000 random
SNP-sets initiated at algorithm outset. The relevant means and standard
deviations are stored in the results.dir
directory, in the
null.mean.sd.info.rds
file. That information will be
required to carry out permutation-based inferential procedures,
demonstrated later.
We now examine the results:
library(magrittr)
library(knitr)
library(kableExtra)
kable(head(size3.combined.res)) %>%
kable_styling() %>%
scroll_box(width = "750px")
snp1 | snp2 | snp3 | snp1.rsid | snp2.rsid | snp3.rsid | snp1.risk.allele | snp2.risk.allele | snp3.risk.allele | snp1.diff.vec | snp2.diff.vec | snp3.diff.vec | snp1.allele.copies | snp2.allele.copies | snp3.allele.copies | intercept_p_disease_coef | exposure1_p_disease_coef | fitness.score | chromosome | n.islands.found |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6 | 12 | 18 | rs2296831 | rs10834232 | rs4766312 | T | C | C | 3 | 45 | 35 | 1+ | 1+ | 1+ | 0.4214377 | 2.090245 | 87.29061 | 6.12.18 | 8 |
We see that we have identified the correct SNPs with the simulated GxGxE effect. In this example, with just few input SNPs, E-GADGETS was able to identify only the correct SNP-set as risk-associated and no others. In real applications, we anticipate E-GADGETS will nominate multiple distinct SNP-sets for further follow-up study.
The elements of the output are similar to those from GADGETS. The
most useful components are simply the identities of the SNPs in the
nominated set(s). Similar to GADGETS, E-GADGETS suggests a risk
associated allele for each SNP in the set (the allele that is
over-transmitted to cases). However, unlike GADGETS, E-GADGETS does not
try to determine whether risk associated alleles follow a recessive
pattern of inheritance, and instead automatically assumes that carrying
one or more copies of the nominated allele is risk associated. (That is
why the “allele.copies” columns are always “1+” for E-GADGETS, while
they may be “1+” or “2” for GADGETS.) The fitness.score
column does not have a straightforward interpretation except that
SNP-sets with higher fitness presumably are more likely to have GxGxE
effects.
A difference from GADGETS output is that E-GADGETS also outputs columns that end in “p_disease_coef”. Those columns indicate levels of the exposure that, in combination with the nominated risk alleles, appear to be more strongly risk-associated. Namely, the values in the “p_disease_coef” columns correspond to the coefficients from the ‘parental’ fitness score component described in the E-GADGETS paper, in which the probability of parents having a ‘genetically high risk’ child are modeled based on the exposure. We expect the exposure levels that are strongly associated with higher probabilities to be more strongly disease-associated. In this example, our exposure has two levels, so we have two coefficients: “intercept_p_disease_coef” and “exposure1_p_disease_coef”. The “intercept” column represents the model coefficient corresponding to exposure level ‘0’, i.e., the reference level. The “exposure1_p_disease_coef” is the model coefficient corresponding to exposure level ‘1’. That coefficient is positive here, suggesting (correctly) that exposure level ‘1’ is more strongly disease associated (in combination with the nominated risk alleles) than exposure level ‘0’.
Like GADGETS, E-GADGETS has an associated global test of association. The test assesses the null hypothesis that, among top-scoring SNP-sets returned by E-GADGETS, none contain any GxE or GxGxE effects. The test is similar to that for GADGETS, except here, we shuffle the exposure, rather than randomizing case/complement-sibling labels, and then re-run E-GADGETS to generate a null distribution of fitness scores. Note that this test assumes the input SNPs are independent of the candidate exposure under the null. We begin by creating 4 data sets with the observed exposure randomly re-assigned:
set.seed(1400)
permute.dataset(pp.list,
permutation.data.file.path = "perm_data",
n.permutations = 4)
Now we re-run E-GADGETS on each permuted data set. Note that we pass
the same standardization information to run.gadgets
as we
used for the observed data.
#pre-process permuted data
preprocessed.lists <- lapply(seq_len(4), function(permutation){
exp.perm.file <- file.path("perm_data",
paste0("exposure.permute", permutation, ".rds"))
exp.perm <- readRDS(exp.perm.file)
preprocess.genetic.data(case, father.genetic.data = dad,
mother.genetic.data = mom,
ld.block.vec = ld.block.vec,
categorical.exposures = exp.perm)
})
#specify chromosome sizes
chrom.sizes <- 3:4
#specify a different seed for each permutation
seeds <- 4:7
#run GADGETS for each permutation and size
perm.res <- lapply(chrom.sizes, function(chrom.size){
# grab standardization info
orig.res.dir <- paste0("size", chrom.size, "_res")
st.info <- readRDS(file.path(orig.res.dir, "null.mean.sd.info.rds"))
st.means <- st.info$null.mean
st.sd <- st.info$null.se
lapply(seq_len(4), function(permutation){
perm.data.list <- preprocessed.lists[[permutation]]
seed.val <- chrom.size*seeds[permutation]
res.dir <- paste0("perm", permutation, "_size", chrom.size, "_res")
reg.dir <- paste0("perm", permutation, "_size", chrom.size, "_reg")
run.gadgets(perm.data.list, n.chromosomes = 5,
chromosome.size = chrom.size,
results.dir = res.dir, cluster.type = "interactive",
registryargs = list(file.dir = reg.dir, seed = seed.val),
n.islands = 8, island.cluster.size = 4,
n.migrations = 2, null.mean.vec = st.means,
null.sd.vec = st.sd)
})
})
#condense the results
perm.res.list <- lapply(chrom.sizes, function(chrom.size){
lapply(seq_len(4), function(permutation){
perm.data.list <- preprocessed.lists[[permutation]]
res.dir <- paste0("perm", permutation, "_size", chrom.size, "_res")
combine.islands(res.dir, snp.annotations.mci, perm.data.list)
})
})
After the null distribution of fitness scores has been generated, the
global test of association can be run with exactly the same commands as
GADGETS. The only difference is that, for E-GADGETS, the null hypothesis
being tested is that none of the input SNPs interact with the exposure
(either via GxE or GxGxE interactions). Note here that we base the test
on the top chromosome of each size (n.top.scores = 1
), but
we recommend the default (n.top.scores = 10
) for real
applications.
# chromosome size 3 results
# function requires a list of vectors of
# permutation based fitness scores
chrom3.perm.fs <- lapply(perm.res.list[[1]],
function(x) x$fitness.score)
chrom3.list <- list(observed.data = size3.combined.res$fitness.score,
permutation.list = chrom3.perm.fs)
# chromosome size 4 results
chrom4.perm.fs <- lapply(perm.res.list[[2]],
function(x) x$fitness.score)
chrom4.list <- list(observed.data = size4.combined.res$fitness.score,
permutation.list = chrom4.perm.fs)
# list of results across chromosome sizes, with each list
# element corresponding to a chromosome size
final.results <- list(chrom3.list, chrom4.list)
# run global test
global.test.res <- global.test(final.results, n.top.scores = 1)
global.test.res$pval
## [1] 0.2
Aside from the global test, for a candidate SNP-set, we can also compute a p-value corresponding to the null hypothesis that none of the component SNPs interact with the exposure (via either GxE or GxGxE interactions). In this example, we are choosing the set to be tested as the highest ranking E-GADGETS chromosome and thereafter running the test on the same data. Therefore, the ‘p-value’ generated by the software will not have uniform distribution under the null, and should not be used for hypothesis tests. We instead refer to it as an ‘h-value’. If the test were instead run on data independent from that used by E-GADGETS, the p-value could allow for valid inference. Regardless, smaller values indicate less consistency with the null.
We carry out the test as follows:
top.snps <- as.vector(t(size3.combined.res[1, 1:3]))
d3.st.info <- readRDS(file.path("size3_res/null.mean.sd.info.rds"))
d3.st.means <- d3.st.info$null.mean
d3.st.sd <- d3.st.info$null.se
set.seed(10)
GxE.test.res <- GxE.test(top.snps, pp.list,
null.mean.vec = d3.st.means,
null.sd.vec = d3.st.sd)
GxE.test.res$pval
## [1] 9.999e-05
The test indicates the SNP-set’s fitness score is inconsistent with what would be expected under the null.
We visualize results from E-GADGETS using the same network-plotting approach as GADGETS, except larger SNP and SNP-pair scores now indicate evidence for involvement in GxE or GxGxE interactions for the implicated SNPs or pairs, rather than epistasis.
# vector of 95th percentile of null fitness scores max
chrom.size.thresholds <- global.test.res$max.perm.95th.pctl
# chromosome size 3 threshold
d3.t <- chrom.size.thresholds[1]
# chromosome size 4 threshold
d4.t <- chrom.size.thresholds[2]
# create results list
obs.res.list <- list(size3.combined.res[size3.combined.res$fitness.score >=
d3.t, ],
size4.combined.res[size4.combined.res$fitness.score >=
d4.t, ])
# list of standardization information for each chromosome size
d4.st.info <- readRDS(file.path("size4_res/null.mean.sd.info.rds"))
d4.st.means <- d4.st.info$null.mean
d4.st.sd <- d4.st.info$null.se
null.means.list <- list(d3.st.means, d4.st.means)
null.sds.list <- list(d3.st.sd, d4.st.sd)
set.seed(10)
graphical.scores <- compute.graphical.scores(obs.res.list, pp.list,
null.mean.vec.list = null.means.list,
null.sd.vec.list = null.sds.list)
network.plot(graphical.scores, pp.list, graph.area = 200,
node.size = 40, vertex.label.cex = 2)
#remove all example directories
chrom.sizes <- 3:4
perm.reg.dirs <- as.vector(outer(paste0("perm", 1:4),
paste0("_size", chrom.sizes, "_reg"),
paste0))
perm.res.dirs <- as.vector(outer(paste0("perm", 1:4),
paste0("_size", chrom.sizes, "_res"),
paste0))
lapply(c("size3_res", "size3_reg", "size4_res", "size4_reg",
perm.reg.dirs, perm.res.dirs,
"perm_data"), unlink, recursive = TRUE)
## R version 4.4.2 (2024-10-31)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.1 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=C
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] kableExtra_1.4.0 knitr_1.49 magrittr_2.0.3 epistasisGA_1.9.0
## [5] BiocStyle_2.35.0
##
## loaded via a namespace (and not attached):
## [1] psych_2.4.6.26 viridisLite_0.4.2 farver_2.1.2
## [4] fastmap_1.2.0 digest_0.6.37 rpart_4.1.23
## [7] base64url_1.4 lifecycle_1.0.4 cluster_2.1.6
## [10] survival_3.7-0 compiler_4.4.2 rlang_1.1.4
## [13] Hmisc_5.2-0 sass_0.4.9 progress_1.2.3
## [16] tools_4.4.2 igraph_2.1.1 utf8_1.2.4
## [19] yaml_2.3.10 data.table_1.16.2 labeling_0.4.3
## [22] prettyunits_1.2.0 brew_1.0-10 htmlwidgets_1.6.4
## [25] mnormt_2.1.1 xml2_1.3.6 plyr_1.8.9
## [28] batchtools_0.9.17 abind_1.4-8 BiocParallel_1.41.0
## [31] withr_3.0.2 foreign_0.8-87 sys_3.4.3
## [34] nnet_7.3-19 grid_4.4.2 stats4_4.4.2
## [37] fansi_1.0.6 lavaan_0.6-19 colorspace_2.1-1
## [40] ggplot2_3.5.1 gtools_3.9.5 scales_1.3.0
## [43] cli_3.6.3 rmarkdown_2.29 crayon_1.5.3
## [46] rstudioapi_0.17.1 bigmemory.sri_0.1.8 reshape2_1.4.4
## [49] pbapply_1.7-2 cachem_1.1.0 stringr_1.5.1
## [52] splines_4.4.2 parallel_4.4.2 BiocManager_1.30.25
## [55] matrixStats_1.4.1 base64enc_0.1-3 vctrs_0.6.5
## [58] Matrix_1.7-1 jsonlite_1.8.9 hms_1.1.3
## [61] glasso_1.11 Formula_1.2-5 htmlTable_2.4.3
## [64] systemfonts_1.1.0 jpeg_0.1-10 maketools_1.3.1
## [67] jquerylib_0.1.4 qgraph_1.9.8 glue_1.8.0
## [70] bigmemory_4.6.4 codetools_0.2-20 stringi_1.8.4
## [73] gtable_0.3.6 quadprog_1.5-8 munsell_0.5.1
## [76] tibble_3.2.1 pillar_1.9.0 rappdirs_0.3.3
## [79] htmltools_0.5.8.1 R6_2.5.1 evaluate_1.0.1
## [82] pbivnorm_0.6.0 lattice_0.22-6 png_0.1-8
## [85] backports_1.5.0 corpcor_1.6.10 bslib_0.8.0
## [88] Rcpp_1.0.13-1 fdrtool_1.2.18 uuid_1.2-1
## [91] svglite_2.1.3 gridExtra_2.3 nlme_3.1-166
## [94] checkmate_2.3.2 xfun_0.49 fs_1.6.5
## [97] buildtools_1.0.0 pkgconfig_2.0.3