multistateQTL 2.4.1
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)
library(QTLExperiment)
library(multistateQTL)
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
head(betas(gtex))
## 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:
params <- qtleEstimate(gtex, threshSig=0.05, threshNull=0.5)
params
## $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.
plotSimulationParams(params=params)
(#fig:plot-estimated params)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.
sim <- qtleSimulate(nTests=1000, nStates=6, global=0.5)
sim
## 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
head(rowData(sim))
## 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:
head(rowData(subset(sim, QTL == "unique")))
## 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:
head(rowData(subset(sim, QTL == "multistate")))
## 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:
sim_na <- getComplete(sim_na, n=0.5, verbose=TRUE)
Then for the remaining QTL, we can fill in the missing values using the following scheme
sim_na <- replaceNAs(sim_na, verbose=TRUE)
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 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)
p1 <- plotPairwiseSharing(sim_top, annotateColsBy=c("nSignificant", "multistateGroup"))
p1
These plots show the set of tests that are significant, but not necessarily shared, by groups of states.
plotUpSet(sim_top, annotateColsBy=c("nSignificant", "multistateGroup"))
Once 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”.
sim_top <- runTestMetrics(sim_top)
plotCompareStates(sim_top, x="S01", y="S02")
## $plot
##
## $counts
##
## S01 S02 both_diverging both_shared
## 5 6 153 283
table(rowData(sim_top)$qtl_type)
##
## global_diverging global_shared multistate_diverging
## 270 114 51
## multistate_shared
## 12
hist(rowData(sim_top)$nSignificant)
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"))
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.5 LTS
##
## Matrix products: default
## BLAS: /home/biocbuild/bbs-3.23-bioc/R/lib/libRblas.so
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/liblapack.so.3; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_GB 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: America/New_York
## tzcode source: system (glibc)
##
## attached base packages:
## [1] grid stats4 stats graphics grDevices utils datasets
## [8] methods base
##
## other attached packages:
## [1] multistateQTL_2.4.1 collapse_2.1.8
## [3] ComplexHeatmap_2.28.0 QTLExperiment_2.4.0
## [5] SummarizedExperiment_1.42.0 Biobase_2.72.0
## [7] GenomicRanges_1.64.0 Seqinfo_1.2.0
## [9] IRanges_2.46.0 S4Vectors_0.50.3
## [11] BiocGenerics_0.58.1 generics_0.1.4
## [13] MatrixGenerics_1.24.0 matrixStats_1.5.0
## [15] BiocStyle_2.40.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 magick_2.9.1 backports_1.5.1
## [16] XVector_0.52.0 labeling_0.4.3 rmarkdown_2.32
## [19] tzdb_0.5.0 tinytex_0.61 purrr_1.2.2
## [22] bit_4.6.0 xfun_0.61 cachem_1.1.0
## [25] jsonlite_2.0.0 DelayedArray_0.38.2 irlba_2.4.1
## [28] parallel_4.6.1 cluster_2.1.8.3 R6_2.6.1
## [31] bslib_0.12.0 RColorBrewer_1.1-3 SQUAREM_2026.1
## [34] jquerylib_0.1.4 Rcpp_1.1.2 bookdown_0.48
## [37] assertthat_0.2.1 iterators_1.0.14 knitr_1.52
## [40] Matrix_1.7-6 splines_4.6.1 tidyselect_1.2.1
## [43] dichromat_2.0-1 abind_1.4-8 yaml_2.3.12
## [46] viridis_0.6.5 doParallel_1.0.17 codetools_0.2-20
## [49] lattice_0.23-1 tibble_3.3.1 plyr_1.8.9
## [52] withr_3.0.3 S7_0.2.2 evaluate_1.0.5
## [55] survival_3.8-12 fitdistrplus_1.2-6 circlize_0.4.18
## [58] pillar_1.11.1 BiocManager_1.30.27 checkmate_2.3.4
## [61] foreach_1.5.2 vroom_1.7.1 invgamma_1.2
## [64] truncnorm_1.0-9 ggplot2_4.0.3 scales_1.4.0
## [67] ashr_2.2-63 glue_1.8.1 tools_4.6.1
## [70] data.table_1.18.6.1 mvtnorm_1.4-2 Cairo_1.7-0
## [73] tidyr_1.3.2 colorspace_2.1-4 mashr_0.2.79
## [76] cli_3.6.6 mixsqp_0.3-54 S4Arrays_1.12.1
## [79] viridisLite_0.4.3 dplyr_1.2.1 gtable_0.3.6
## [82] sass_0.4.10 digest_0.6.39 SparseArray_1.12.3
## [85] rjson_0.2.23 farver_2.1.2 htmltools_0.5.9
## [88] lifecycle_1.0.5 rmeta_3.0 GlobalOptions_0.1.4
## [91] bit64_4.8.6 MASS_7.3-66