multistateQTL is a Bioconductor package for applying
basic statistical tests (e.g., feature-wise FDR correction, calculating
pairwise sharing), summarizing, and visualizing QTL summary statistics
from multiple states (e.g., tissues, celltypes, environmental
conditions). It works on the QTLExperiment
(QTLE) object class, where rows represent features (e.g.,
genes, transcripts, genomic regions), columns represent states, and
assays are the various summary statistics. It also provides wrapper
implementations of a number of multi-test correction methods (e.g., mashr, meta-soft,
etc), which result in a set of multi-test corrected summary
statistics.
QTLExperiment and multistateQTL can be installed from GitHub:
if (!require("BiocManager", quietly=TRUE))
install.packages("BiocManager")
BiocManager::install(c("QTLExperiment", "multistateQTL"))
They are also available on GitHub:
devtools::install_git("https://github.com/dunstone-a/QTLExperiment", build_vignettes=TRUE)
devtools::install_git(https://github.com/dunstone-a/multistateQTL", build_vignettes=TRUE)
Provided with real QTL summary statistics as either a
QTLE object or a named list with betas and error (and
optionally pval or lfsr), key parameters are estimated that are used to
simulate realistic multi-state QTL data. We demonstrate the parameter
estimation function on publicly available summary statistics from GTEx
(v8). Note that this data only contains tests that were called as
significant by GTEx and vroom only loads the first chunk of results as
it does not read .gz compressed objects well. This truncated dataset is
used in the vignette for convenience, however, to estimate the default
parameters in qtleParams() we downloaded QTL summary
statistics for all associations tested for the 10 GTEx tissues with the
largest sample sizes from Google
Cloud). To speed up calculations we filtered this to include only
associations on chromosome 1 and considered significant tests with pval
< 0.05 and null tests with a pval > 0.1.
See QTLExperiment for
more info on the sumstats2qtle function and for other
approaches for reading in QTL summary statistics.
The parameters estimated here include:
rnorm to sample an effect
size for each QTL for each state. The variance parameter for
rnorm is user defined (default = 0.1).input_path <- system.file("extdata", package="multistateQTL")
state <- c("lung", "thyroid", "spleen", "blood")
input <- data.frame(
state=state,
path=paste0(input_path, "/GTEx_tx_", state, ".tsv"))
gtex <- sumstats2qtle(
input,
feature_id="molecular_trait_id",
variant_id="rsid",
betas="beta",
errors="se",
pvalues="pvalue",
verbose=TRUE)
gtex## class: QTLExperiment
## dim: 1163 4
## metadata(0):
## assays(3): betas errors pvalues
## rownames(1163): ENST00000428771|rs554008981 ENST00000477976|rs554008981
## ... ENST00000445118|rs368254419 ENST00000483767|rs368254419
## rowData names(2): feature_id variant_id
## colnames(4): lung thyroid spleen blood
## colData names(1): state_id
## lung thyroid spleen blood
## ENST00000428771|rs554008981 -0.1733690 NA 0.134913 NA
## ENST00000477976|rs554008981 0.1616170 0.3173110 NA NA
## ENST00000483767|rs554008981 -0.4161480 -0.0483018 NA -0.204647
## ENST00000623070|rs554008981 -0.1137930 NA NA NA
## ENST00000669922|rs554008981 -0.1921680 -0.1067540 0.724622 -0.117424
## ENST00000428771|rs201055865 -0.0630909 NA NA NA
Estimating parameters:
## $cv.sig.shape
## [1] 7.070354
##
## $cv.sig.rate
## [1] 7.717703
##
## $cv.null.shape
## [1] 1.903592
##
## $cv.null.rate
## [1] 0.8979238
##
## $betas.sig.shape
## [1] 3.470245
##
## $betas.sig.rate
## [1] 16.27222
##
## $betas.null.shape
## [1] 1.360183
##
## $betas.null.rate
## [1] 11.03394
Looking at the distributions defined by these estimated parameters, the simulated effect sizes for significant QTL will tend to be larger, while the simulated coefficient of variation values will be smaller than for the non-significant QTL.
Gamma distributions defined by the parameters estimated by qtleEstimate.
The default parameters available through qtleParams()
were estimated from the GTEx v8 tissue-level eQTL summary statistics
from chromosome 1 using the 10 tissues with the largest sample sizes.
From these data, significant QTL parameters were estimated from tests in
the lowest p-value quantile, while null parameters were estimated from
tests in the highest p-value quantile. Data for tests on chromosome 1
were included in all four tissues (n=32613).
The simulation tool allows for the simulation of four types of associations: (1) Global, where the simulated effect size is approximately equal across all states; (2) Unique, where the association is only significant in one state; (3) Multi-state, where the association is significant in a subset of states (i.e., state-groups), and (4) Null, where the association has no significant effects in any state. First each test is randomly assigned as one of the above types according to the proportions specified by the user. For multi-state QTL, each state is assigned to a state-group, either randomly or according to user defined groups, then each multi-state QTL is assigned randomly to one of the state-groups. For unique QTL, the QTL is randomly assigned to a single state.
Simulated mean effect sizes for all non-null QTL are sampled from gamma(beta.sig.shape, beta.sig.rate) and are randomly assigned a positive or negative effect direction. Then for each state where that QTL is significant, an effect size is sampled from N(mean effect size, σ), where σ is user defined (default=0.1). Effect sizes for null QTL are sampled from gamma(beta.null.shape, beta.null.rate) and are randomly assigned a positive or negative effect direction. Standard errors for each QTL for each state are simulated by sampling from gamma(cv.sig.shape, cv.sig.rate) or gamma(cv.null.shape, cv.null.rate) for significant and null QTL, respectively, and multiplying the sampled cv by the absolute value of the simulated beta for that QTL in that state.
Here is an example of a simple simulation with half of the simulated QTL tests having globally significant effects. This example uses the default parameters.
## class: QTLExperiment
## dim: 1000 6
## metadata(0):
## assays(3): betas errors lfsrs
## rownames(1000): F0867|v86973 F0107|v67504 ... F0874|v42722 F0190|v78203
## rowData names(11): feature_id variant_id ... S5 S6
## colnames(6): S1 S2 ... S5 S6
## colData names(1): state_id
## DataFrame with 6 rows and 11 columns
## feature_id variant_id QTL id mean_beta
## <character> <character> <character> <character> <numeric>
## F0867|v86973 F0867 v86973 global F0867|v86973 0.274222
## F0107|v67504 F0107 v67504 global F0107|v67504 0.740930
## F0239|v64205 F0239 v64205 global F0239|v64205 0.211987
## F0531|v90951 F0531 v90951 global F0531|v90951 -0.650529
## F0950|v93596 F0950 v93596 null F0950|v93596 0.000000
## F0851|v25798 F0851 v25798 null F0851|v25798 0.000000
## S1 S2 S3 S4 S5 S6
## <logical> <logical> <logical> <logical> <logical> <logical>
## F0867|v86973 TRUE TRUE TRUE TRUE TRUE TRUE
## F0107|v67504 TRUE TRUE TRUE TRUE TRUE TRUE
## F0239|v64205 TRUE TRUE TRUE TRUE TRUE TRUE
## F0531|v90951 TRUE TRUE TRUE TRUE TRUE TRUE
## F0950|v93596 FALSE FALSE FALSE FALSE FALSE FALSE
## F0851|v25798 FALSE FALSE FALSE FALSE FALSE FALSE
We can also generate more complex simulations, for example this simulation has 20% global, 40% multi-state, 20% unique, and 20% null QTL effects, where multi-state effects are assigned to one of two state-groups.
sim <- qtleSimulate(
nStates=10, nFeatures=100, nTests=1000,
global=0.2, multi=0.4, unique=0.2, k=2)Here is a snapshot of the simulation key for QTL simulated as unique to a single state:
## DataFrame with 6 rows and 16 columns
## feature_id variant_id QTL id mean_beta S01
## <character> <character> <character> <character> <numeric> <logical>
## F048|v14842 F048 v14842 unique F048|v14842 -0.159967 FALSE
## F087|v59649 F087 v59649 unique F087|v59649 -0.924853 FALSE
## F094|v88099 F094 v88099 unique F094|v88099 0.887211 TRUE
## F002|v41283 F002 v41283 unique F002|v41283 0.418866 TRUE
## F028|v17965 F028 v17965 unique F028|v17965 0.562209 FALSE
## F011|v73807 F011 v73807 unique F011|v73807 -0.578273 TRUE
## S02 S03 S04 S05 S06 S07
## <logical> <logical> <logical> <logical> <logical> <logical>
## F048|v14842 FALSE TRUE FALSE FALSE FALSE FALSE
## F087|v59649 FALSE FALSE TRUE FALSE FALSE FALSE
## F094|v88099 FALSE FALSE FALSE FALSE FALSE FALSE
## F002|v41283 FALSE FALSE FALSE FALSE FALSE FALSE
## F028|v17965 FALSE TRUE FALSE FALSE FALSE FALSE
## F011|v73807 FALSE FALSE FALSE FALSE FALSE FALSE
## S08 S09 S10 multistateGroup
## <logical> <logical> <logical> <character>
## F048|v14842 FALSE FALSE FALSE NA
## F087|v59649 FALSE FALSE FALSE NA
## F094|v88099 FALSE FALSE FALSE NA
## F002|v41283 FALSE FALSE FALSE NA
## F028|v17965 FALSE FALSE FALSE NA
## F011|v73807 FALSE FALSE FALSE NA
Here is a snapshot of the simulation key for QTL simulated as multi-state:
## DataFrame with 6 rows and 16 columns
## feature_id variant_id QTL id mean_beta S01
## <character> <character> <character> <character> <numeric> <logical>
## F018|v19728 F018 v19728 multistate F018|v19728 -0.398207 TRUE
## F084|v98536 F084 v98536 multistate F084|v98536 0.524651 TRUE
## F053|v82198 F053 v82198 multistate F053|v82198 0.526131 TRUE
## F060|v42323 F060 v42323 multistate F060|v42323 0.344318 FALSE
## F059|v53070 F059 v53070 multistate F059|v53070 -0.566041 TRUE
## F091|v47866 F091 v47866 multistate F091|v47866 -0.312867 FALSE
## S02 S03 S04 S05 S06 S07
## <logical> <logical> <logical> <logical> <logical> <logical>
## F018|v19728 FALSE TRUE FALSE TRUE TRUE TRUE
## F084|v98536 FALSE TRUE FALSE TRUE TRUE TRUE
## F053|v82198 FALSE TRUE FALSE TRUE TRUE TRUE
## F060|v42323 TRUE FALSE TRUE FALSE FALSE FALSE
## F059|v53070 FALSE TRUE FALSE TRUE TRUE TRUE
## F091|v47866 TRUE FALSE TRUE FALSE FALSE FALSE
## S08 S09 S10 multistateGroup
## <logical> <logical> <logical> <character>
## F018|v19728 FALSE TRUE FALSE Group1
## F084|v98536 FALSE TRUE FALSE Group1
## F053|v82198 FALSE TRUE FALSE Group1
## F060|v42323 TRUE FALSE TRUE Group2
## F059|v53070 FALSE TRUE FALSE Group1
## F091|v47866 TRUE FALSE TRUE Group2
message("Number of QTL specific to each state-group:")
table(rowData(subset(sim, QTL == "multistate"))$multistateGroup)##
## Group1 Group2
## 218 182
The multistateQTL toolkit provides two functions to help deal with
missing data, getComplete and replaceNAs. The
getComplete function is a smart subsetting function that
remove QTL associations (rows) with more than an allowed amount of
missing data. The replaceNAs function allows for NAs in
each assay to be replaced with a constant or with the row mean or row
median. For example, here is a snapshot of our simulated data from above
with added NAs:
na_pattern <- sample(seq(1, ncol(sim)*nrow(sim)), 1000)
sim_na <- sim
assay(sim_na, "betas")[na_pattern] <- NA
assay(sim_na, "errors")[na_pattern] <- NA
assay(sim_na, "lfsrs")[na_pattern] <- NA
message("Number of simulated tests: ", nrow(sim_na))
head(betas(sim_na))## S01 S02 S03 S04 S05 S06
## F018|v19728 -0.5057508 -0.3351966 -0.2898633 -0.1051673 -0.3629587 -0.4188840
## F084|v98536 0.6601750 0.1856120 0.5397380 NA 0.6795253 0.5699258
## F011|v93756 -0.5691937 -0.4729110 -0.6127229 NA -0.5868161 -0.5970767
## F009|v4113 -0.2526991 -0.3455134 0.3641551 0.3442019 0.4030663 0.4945627
## F053|v82198 0.4877209 0.3886733 0.6344544 -0.7110317 0.5356872 0.3866470
## F100|v3910 0.3837357 0.3255985 0.3406026 0.2911573 0.4224596 0.1828234
## S07 S08 S09 S10
## F018|v19728 NA -0.27212303 -0.5049474 0.2664509
## F084|v98536 0.5173080 -0.52960143 0.5569916 -0.1343244
## F011|v93756 -0.3869528 -0.28290290 -0.7443570 -0.4633451
## F009|v4113 -0.4581777 0.06575733 0.2263776 -0.1147040
## F053|v82198 0.5911729 -0.15968805 0.5895809 0.3476646
## F100|v3910 0.3152780 0.19877817 0.4485532 0.4523443
First we can use getComplete to keep only the tests that
have data for at least half of the states:
Then for the remaining QTL, we can fill in the missing values using the following scheme
## S01 S02 S03 S04 S05 S06
## F018|v19728 -0.5057508 -0.3351966 -0.2898633 -0.1051673 -0.3629587 -0.4188840
## F084|v98536 0.6601750 0.1856120 0.5397380 0.0000000 0.6795253 0.5699258
## F011|v93756 -0.5691937 -0.4729110 -0.6127229 0.0000000 -0.5868161 -0.5970767
## F009|v4113 -0.2526991 -0.3455134 0.3641551 0.3442019 0.4030663 0.4945627
## F053|v82198 0.4877209 0.3886733 0.6344544 -0.7110317 0.5356872 0.3866470
## F100|v3910 0.3837357 0.3255985 0.3406026 0.2911573 0.4224596 0.1828234
## S07 S08 S09 S10
## F018|v19728 0.0000000 -0.27212303 -0.5049474 0.2664509
## F084|v98536 0.5173080 -0.52960143 0.5569916 -0.1343244
## F011|v93756 -0.3869528 -0.28290290 -0.7443570 -0.4633451
## F009|v4113 -0.4581777 0.06575733 0.2263776 -0.1147040
## F053|v82198 0.5911729 -0.15968805 0.5895809 0.3476646
## F100|v3910 0.3152780 0.19877817 0.4485532 0.4523443
The multistateQTL toolkit also provides the
callSignificance function, which calls QTL tests
significant in each state using either a single or two-step threshold
approach. For example, we can set a single lfsr threshold of 0.1 to call
significance of our simulate QTL:
sim <- callSignificance(sim, assay="lfsrs", thresh=0.05)
message("Median number of significant tests per state: ",
median(colData(sim)$nSignificant))Because we have the simulated ground-truth, we can compare these
significance calls to what was simulated using the
simPerformance function, which provides the following
global (i.e. across all state) performance metrics:
sim <- callSignificance(sim, assay="lfsrs", thresh=0.001)
perf_metrics <- simPerformance(sim)
lapply(perf_metrics, FUN=function(x) {round(x, 2)})## $accuracy
## [1] 0.42
##
## $precision
## [1] 0.42
##
## $recall
## [1] 1
##
## $f1
## [1] 0.59
##
## $cm
## called
## simulated FALSE TRUE
## FALSE 33 5731
## TRUE 21 4215
As you can see the recall of TRUE significant QTL is quite low. However if we change our significance calling approach to be more flexible.
sim <- callSignificance(
sim, mode="simple", assay="lfsrs",
thresh=0.0001, secondThresh=0.0002)
simPerformance(sim)$recall## [1] 0.9907932
The multistateQTL package contains five functions for
visualising multi-state eQTL data. These functions are based on the
ggplot2 and
ComplexHeatmap
R packages.
plotPairwiseSharing: based on
ComplexHeatmap::HeatmapplotQTLClusters: based on
ComplexHeatmap::HeatmapplotUpSet: based on
ComplexHeatmap::UpSetplotCompareStates: based on ggplot2plotSimulationParams: based on
ggplot2The functions built on ggplot2 are compatible with
ggplot2 syntax such as the + operator.
The function plotPairwiseSharing shows the degree of
pairwise sharing of significant hits for each combination of two states.
Column annotations can be added by specifying a valid column name from
the colData of the object.
In the plot below, columns are ordered by the broad cell type
(multistateGroup) of the states. We expect states belonging
to the same multi-state group to be more related and have a greater
degree of sharing of significant eQTLs. Column annotations are used to
show the number of significant eQTLs for each state.
sim_sig <- getSignificant(sim)
sim_top <- getTopHits(sim_sig, assay="lfsrs", mode="state")
sim_top <- runPairwiseSharing(sim_top, factor = 0.9)
p1 <- plotPairwiseSharing(sim_top, annotateColsBy=c("nSignificant", "multistateGroup"))
p1Once multi-state test correction is performed, you will want to identify global, multi-state, and unique QTL.
Note that plotCompareStates returns a list with a
ggplot2 object as the first element and a
table as the second element. These can be accessed using
the names “plot” or “counts”.
## $plot
##
## $counts
##
## both_diverging both_shared S01 S02
## 157 288 5 6
##
## global_diverging global_shared multistate_diverging
## 279 113 53
## multistate_shared
## 11
The function plotQTLClusters can be used to produce a
heatmap of the eQTL betas values for each state. Each row is a SNP-gene
pair, and columns are states. Row and column annotations can be added by
naming column names from the rowData or
colData of the input QTLExperiment object.
sim_top_ms <- subset(sim_top, qtl_type_simple == "multistate")
plotQTLClusters(
sim_top_ms,
annotateColsBy=c("multistateGroup"),
annotateRowsBy=c("qtl_type", "mean_beta", "QTL"))## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.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.32.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=en_US.UTF-8
## [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] grid stats4 stats graphics grDevices utils datasets
## [8] methods base
##
## other attached packages:
## [1] multistateQTL_2.5.1 collapse_2.1.8
## [3] ComplexHeatmap_2.29.0 QTLExperiment_2.5.0
## [5] SummarizedExperiment_1.43.0 Biobase_2.73.2
## [7] GenomicRanges_1.65.4 Seqinfo_1.3.2
## [9] IRanges_2.47.5 S4Vectors_0.51.10
## [11] BiocGenerics_0.59.12 generics_0.1.4
## [13] MatrixGenerics_1.25.0 matrixStats_1.5.0
## [15] BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] gridExtra_2.3.1 rlang_1.3.0 magrittr_2.0.5
## [4] clue_0.3-68 GetoptLong_1.1.1 otel_0.2.0
## [7] compiler_4.6.1 png_0.1-9 vctrs_0.7.3
## [10] pkgconfig_2.0.3 shape_1.4.6.1 crayon_1.5.3
## [13] fastmap_1.2.0 backports_1.5.1 XVector_0.53.0
## [16] labeling_0.4.3 rmarkdown_2.32 tzdb_0.5.0
## [19] purrr_1.2.2 bit_4.6.0 xfun_0.61
## [22] cachem_1.1.0 jsonlite_2.0.0 DelayedArray_0.39.8
## [25] irlba_2.4.1 parallel_4.6.1 cluster_2.1.8.3
## [28] R6_2.6.1 bslib_0.12.0 RColorBrewer_1.1-3
## [31] SQUAREM_2026.1 jquerylib_0.1.4 Rcpp_1.1.2
## [34] assertthat_0.2.1 iterators_1.0.14 knitr_1.52
## [37] Matrix_1.7-6 splines_4.6.1 tidyselect_1.2.1
## [40] abind_1.4-8 yaml_2.3.12 viridis_0.6.5
## [43] doParallel_1.0.17 codetools_0.2-20 lattice_0.23-1
## [46] tibble_3.3.1 plyr_1.8.9 withr_3.0.3
## [49] S7_0.2.2 evaluate_1.0.5 survival_3.8-12
## [52] fitdistrplus_1.2-6 circlize_0.4.18 pillar_1.11.1
## [55] BiocManager_1.30.27 checkmate_2.3.4 foreach_1.5.2
## [58] vroom_1.7.1 invgamma_1.2 truncnorm_1.0-9
## [61] ggplot2_4.0.3 scales_1.4.0 ashr_2.2-63
## [64] glue_1.8.1 maketools_1.3.2 tools_4.6.1
## [67] sys_3.4.3 data.table_1.18.6.1 buildtools_1.0.0
## [70] mvtnorm_1.4-2 tidyr_1.3.2 colorspace_2.1-3
## [73] mashr_0.2.79 cli_3.6.6 mixsqp_0.3-54
## [76] S4Arrays_1.13.2 viridisLite_0.4.3 dplyr_1.2.1
## [79] gtable_0.3.6 sass_0.4.10 digest_0.6.39
## [82] SparseArray_1.13.4 rjson_0.2.23 farver_2.1.2
## [85] htmltools_0.5.9 lifecycle_1.0.5 rmeta_3.0
## [88] GlobalOptions_0.1.4 bit64_4.8.6 MASS_7.3-66