Skip to contents

Overview

A QTL measured in several contexts — tissues, cell types, conditions — is usually not a set of independent experiments. Effects are shared, and an effect that looks null in one context is often a real effect that context was underpowered to see.

mash learns the covariance structure of effect sharing from the data and shrinks each variant’s per-context estimate toward it. mashPipeline() drives the whole fit; the chainable steps behind it let you swap out any stage.

The input is a named list of summary-statistic collections split into partitions, each with a distinct job:

Partition Role
strong discovery variants; the data-driven covariances are learned here
random unbiased background; the mixture weights are fit here
null null variants; the residual correlation is estimated here

strong and random are required. null is optional and used only by the residual-correlation estimators that need it.

Fitting chunks below are run with results = "hide": mashr and flashier narrate their progress on stdout, which knitr cannot filter, so each fit is split into a compute chunk and a display chunk. The code is otherwise exactly what you would type.

library(pecotmr)
data(qtlSumStatsMulticontextExample)
ss <- qtlSumStatsMulticontextExample
ss
## QtlSumStats: 3 entries, genome build hg19
##   1 studies, 3 contexts, 1 traits
##   LD reference: plink1 @ pecotmr://extdata/toy_canonical

The one-shot fit

sumStatsList <- list(strong = ss, random = ss, null = ss)
fit <- mashPipeline(
    sumStatsList,
    alpha = 0,
    prior = MashPriorParam(nPcs = 2L)
)
length(fit$U)
## [1] 17
round(head(sort(fit$w, decreasing = TRUE), 5), 4)
##          null equal_effects        muscle      ED_PCA_1      ED_PCA_2 
##        0.6708        0.3145        0.0147        0.0000        0.0000

fit$U is the prior covariance list and fit$w the estimated mixture weights over it. A large weight on null is normal and healthy: most variants are not QTLs.

alpha must match the scale you are on: alpha = 0 for effect sizes (BETA/SE), alpha = 1 for Z-scores. inputScale controls which pair is extracted from the sumstats and defaults to "auto" — BETA + SE when every entry carries both, otherwise Z with unit standard errors.

Residual correlation

Conditions measured on overlapping samples have correlated residuals, and mash needs an estimate of that correlation (Vhat) to avoid reading shared noise as shared signal. residualCorrelationMethod chooses the estimator:

Value Estimator Needs
"identity" (default) no residual correlation —
"simple" mashr::estimate_null_correlation_simple() null
"simpleSpecific" nearest PD correlation of cov(nullZ) null
"corshrink" adaptive shrinkage of the null correlation null, CorShrink
"mle" EM refinement against the prior random, priorCovariances

The default is "identity" because it always works, not because it is the best choice. Multi-context QTL data is the overlapping-sample case par excellence — the same donors appear in every context — so the identity assumption is usually wrong. Prefer a data-driven estimate whenever you have a null set:

fitSimple <- mashPipeline(
    sumStatsList,
    alpha = 0,
    residualCorrelationMethod = "simple",
    prior = MashPriorParam(priorCovariances = fit$U)
)
round(head(sort(fitSimple$w, decreasing = TRUE), 5), 4)
##          null equal_effects         blood        muscle      ED_PCA_1 
##        0.7091        0.2350        0.0315        0.0244        0.0000

Passing fit$U back in holds the prior fixed, so Vhat is the only thing that differs from the fit above and the shift in the weights is attributable to it alone. Re-estimating the prior here would confound the comparison – and cost a second data-driven estimate for a prior you already have.

Supplying a null partition while leaving the default in place reports that the partition is going unused, since that combination is more often an oversight than an intent.

You can also skip estimation entirely and pass a Vhat computed elsewhere — useful when it was estimated on a larger reference and shipped as a static artefact:

mashPipeline(sumStatsList, alpha = 0, residualCorrelation = myVhat)

The chain, step by step

mashPipeline() is a wrapper over four steps. Take them apart when you want to reuse an intermediate, try several priors against one Vhat, or compute posteriors for a set other than the one you fit on.

vhat <- mashResidualCorrelation(sumStatsList, alpha = 0, method = "simple")
prior <- mashPriorCovariances(sumStatsList, alpha = 0, vhat = vhat, nPcs = 2L)
model <- mashModelFit(
    sumStatsList,
    alpha = 0,
    priorCovariances = prior,
    vhat = vhat
)
class(model)
## [1] "mash"

mashPriorCovariances() returns list(U, w, loglik), while mashCovarianceComponents() returns a bare covariance list. Either can be handed to any consumer — the wrapper is unwrapped for you, so there is no need to reach for $U.

mashModelFit() returns a mashr model object. That is not the same thing as the MashPrior class, which packages a prior for use as a TWAS weight method; see TWAS weights.

Posteriors

mashPosterior() applies the fitted model to a target set. Fitting on random and computing posteriors on strong is the standard split — the weights should not be learned from the same variants they are applied to.

post <- mashPosterior(model, ss, alpha = 0, vhat = vhat)
names(post)
## [1] "PosteriorMean" "PosteriorSD"   "lfdr"          "NegativeProb" 
## [5] "lfsr"          "PosteriorCov"
dim(post$PosteriorMean)
## [1] 200   3
Matrix Meaning
PosteriorMean / PosteriorSD shrunk effect and its uncertainty
lfsr local false sign rate — the quantity to threshold on
lfdr local false discovery rate
NegativeProb posterior probability the effect is negative
PosteriorCov per-variant across-context covariance

lfsr is the usual significance measure here: it asks whether the sign of an effect is confidently known, which is a more honest question than whether it is exactly zero.

# Variants significant in at least one context at lfsr < 0.05
sum(apply(post$lfsr, 1, min) < 0.05)
## [1] 24

Choosing the covariance components

The data-driven part of the prior is built from components and then refined by an engine:

mashPriorCovariances(
    sumStatsList,
    alpha = 0,
    vhat = vhat,
    components = c("canonical", "pca", "flash", "flashNonneg"),
    engine = "covEd", # or "covUdr", or "none"
    nPcs = 2L
)

"covEd" is the default and is mashr’s own extreme deconvolution. "covUdr" is opt-in and has known numerical issues; its TED update additionally assumes i.i.d. (Z-scale) data, so it is requested as CovUdrOptions(fit = UdFitOptions(unconstrained.update = "ted")) rather than as a separate engine name. udr is two functions – ud_init() then ud_fit() – so each one’s arguments travel in their own bundle and are checked against their own source. Its dominant cost is CovUdrOptions(init = UdInitOptions(n_unconstrained = )) (default 50), which is worth cutting sharply when you have few conditions.

The engine refines only the data-driven components. Canonical components are fixed structural hypotheses – identity, singletons, equal effects – and reach the prior unmodified, matching mashr’s own workflow. engine = "none" uses the data-driven components as generated, without refinement.

To bypass the covariance chain entirely and fit weights against a prior you already have:

mashPipeline(sumStatsList, alpha = 0, priorCovariances = myU)

Posterior contrasts and feature scores

Posterior means answer “how big is the effect in each context”. A different question — “which contexts does this feature deviate in, relative to its own average” — is what the contrast layer computes.

data(mashPosteriorExample)
p <- mashPosteriorExample
contrast <- mashPosteriorContrast(
    p$posterior$PosteriorMean,
    p$posterior$PosteriorCov,
    p$orig$bhat
)
scores <- calculateFeatureScores(contrast)
scores
## # A tibble: 8 × 2
##   condition zScore
##   <chr>      <dbl>
## 1 ALL         3.41
## 2 Ast         2.58
## 3 End         2.79
## 4 Exc         3.34
## 5 Inh         3.11
## 6 Mic         3.09
## 7 OPC         1.85
## 8 Oli         2.81

Each row is one condition’s meta-analysed deviation score across features — a per-context summary of how distinctive that context’s effects are. The bundled example is a brain cell-type panel, so the conditions are cell types (Ast, Exc, Mic, …) plus the ALL pooled column.

Three ways to score a feature

calculateFeatureScores() is one of three scoring routes over the same contrast result, and they answer different questions:

# 1. meta-analysed deviation z, across features (shown above)
head(scores, 3)
## # A tibble: 3 × 2
##   condition zScore
##   <chr>      <dbl>
## 1 ALL         3.41
## 2 Ast         2.58
## 3 End         2.79
# 2. the fraction of features whose deviation is significant
nSignificantScore(contrast, pCutoff = 1e-5)
## # A tibble: 8 × 2
##   condition ratio
##   <chr>     <dbl>
## 1 ALL       0.176
## 2 Ast       0.118
## 3 End       0.118
## 4 Exc       0.176
## 5 Inh       0.118
## 6 Mic       0.176
## 7 OPC       0.118
## 8 Oli       0.118

nSignificantScore() asks how often a condition deviates rather than how strongly — a condition can carry a large mean deviation driven by a handful of features, or a modest one that is consistent across many, and only the second is captured here.

The third route brings fine-mapping in. scoreFromCs() restricts to the lead variant of each credible set, so the score reflects deviation at variants that are plausibly causal rather than at whatever tagged them:

scoreFromCs(p$fineMapping, contrast, condition = "Exc")
## [1] 8.347042e-05

It takes the least-significant credible-set lead and reports the largest pairwise |mean|/se there — a deliberately conservative summary, since one strong credible set should not carry a condition on its own.

The contrast design itself is built from makePairwiseContrastCol(), which sets +1 and −1 for a pair of conditions in a zero vector, and fitMashContrast(), which fits one feature’s contrast. mashPosteriorContrast() calls both across all features and pairs, so you only need them directly when building a non-pairwise design.

Utilities

The mash track ships a set of helpers for getting real data into shape:

Function Use
mashInput() build the (strong, random, null) matrices from fine-mapping output
sanitizeMashData() replace NaN/Inf in bhat/sbhat with safe values
filterInvalidSummaryStat() drop rows mash cannot use
mergeMashData() / sliceMashData() combine or subset the matrix bundles
mashRandNullSample() draw the random/null partitions
filterMixtureComponents() drop components with negligible weight
metaAnalysisPerCondition() per-condition fixed-effect meta-analysis
updateMashModelCov() swap a fitted model’s covariance list

Next steps

Session info

## R version 4.5.3 (2026-03-11)
## Platform: x86_64-conda-linux-gnu
## Running under: Ubuntu 24.04.5 LTS
## 
## Matrix products: default
## BLAS/LAPACK: /home/runner/work/pecotmr/pecotmr/.pixi/envs/default/lib/libopenblasp-r0.3.34.so;  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
##  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
##  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
## [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.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] pecotmr_0.9.3
## 
## loaded via a namespace (and not attached):
##   [1] bitops_1.1-0                rlang_1.3.0                
##   [3] magrittr_2.0.5              horseshoe_0.2.0            
##   [5] otel_0.2.0                  matrixStats_1.5.0          
##   [7] susieR_0.16.6               compiler_4.5.3             
##   [9] systemfonts_1.3.2           vctrs_0.7.3                
##  [11] quadprog_1.5-8              stringr_1.6.0              
##  [13] pkgconfig_2.0.3             crayon_1.5.3               
##  [15] fastmap_1.2.0               backports_1.5.1            
##  [17] XVector_0.50.0              utf8_1.2.6                 
##  [19] Rsamtools_2.26.0            rmarkdown_2.32             
##  [21] tzdb_0.5.0                  UCSC.utils_1.6.1           
##  [23] ragg_1.5.2                  bit_4.6.0                  
##  [25] purrr_1.2.2                 xfun_0.61                  
##  [27] Rfast_2.1.5.2               MultiAssayExperiment_1.36.2
##  [29] cachem_1.1.0                GenomeInfoDb_1.46.2        
##  [31] trust_0.1-9                 jsonlite_2.0.0             
##  [33] reshape_0.8.10              DelayedArray_0.36.1        
##  [35] tictoc_1.2.1                BiocParallel_1.44.0        
##  [37] irlba_2.4.1                 parallel_4.5.3             
##  [39] R6_2.6.1                    bslib_0.12.0               
##  [41] stringi_1.8.9               RColorBrewer_1.1-3         
##  [43] SQUAREM_2026.1              GenomicRanges_1.62.1       
##  [45] jquerylib_0.1.4             numDeriv_2016.8-1.1        
##  [47] Rcpp_1.1.2                  Seqinfo_1.0.0              
##  [49] assertthat_0.2.1            SummarizedExperiment_1.40.0
##  [51] knitr_1.52                  snow_0.4-4                 
##  [53] readr_2.2.0                 IRanges_2.44.0             
##  [55] BiocBaseUtils_1.12.0        Matrix_1.7-6               
##  [57] splines_4.5.3               tidyselect_1.2.1           
##  [59] abind_1.4-8                 yaml_2.3.12                
##  [61] codetools_0.2-20            metafor_5.0-1              
##  [63] lattice_0.23-1              tibble_3.3.1               
##  [65] plyr_1.8.9                  withr_3.0.3                
##  [67] Biobase_2.70.0              S7_0.2.2                   
##  [69] evaluate_1.0.5              flashier_1.0.21            
##  [71] archive_1.1.13              desc_1.4.3                 
##  [73] RcppParallel_5.1.11-2       Biostrings_2.78.0          
##  [75] pillar_1.11.1               MatrixGenerics_1.22.0      
##  [77] metadat_1.6-0               checkmate_2.3.3            
##  [79] stats4_4.5.3                softImpute_1.4-3           
##  [81] generics_0.1.4              vroom_1.7.1                
##  [83] invgamma_1.2                mathjaxr_2.0-0             
##  [85] truncnorm_1.0-9             S4Vectors_0.48.1           
##  [87] hms_1.1.4                   ggplot2_4.0.3              
##  [89] scales_1.4.0                ashr_2.2-63                
##  [91] glue_1.8.1                  tools_4.5.3                
##  [93] fs_2.1.0                    mvtnorm_1.4-2              
##  [95] grid_4.5.3                  tidyr_1.3.2                
##  [97] nlme_3.1-170                mashr_0.2.79               
##  [99] deconvolveR_1.2-1           cli_3.6.6                  
## [101] zigg_0.0.2                  ebnm_1.0-55                
## [103] textshaping_1.0.5           mixsqp_0.3-54              
## [105] S4Arrays_1.10.1             dplyr_1.2.1                
## [107] gtable_0.3.6                sass_0.4.10                
## [109] digest_0.6.39               BiocGenerics_0.56.0        
## [111] SparseArray_1.10.10         htmlwidgets_1.6.4          
## [113] farver_2.1.2                htmltools_0.5.9            
## [115] pkgdown_2.2.1               lifecycle_1.0.5            
## [117] httr_1.4.9                  rmeta_3.0                  
## [119] bit64_4.8.6                 MASS_7.3-66