Skip to contents

Overview

A QtlDataset is an individual-level container for a single study: one genotype source plus a per-context set of molecular phenotypes. MultiStudyQtlDataset bundles several QtlDatasets (optionally alongside a summary-statistics-only QtlSumStats) so a molecular trait measured across multiple studies can be analyzed with downstream pipelines together.

Slot Holds
study study identifier (length-1 character)
genotypes a GenotypeHandle (lazy PLINK1/PLINK2/VCF/GDS handle)
phenotypes a named list of SummarizedExperiments, one per context; assay = traits x samples, rowRanges = trait positions, colData = per-context covariates
genotypeCovariates numeric matrix (samples x covariates), e.g. genotype PCs, applied to every context
QC thresholds mafCutoff, macCutoff, xvarCutoff, imissCutoff, keepIndel, keepSamples, keepVariants, scaleResiduals — stored as lazy filters, applied at genotype-extraction time

There are two ways to build a QtlDataset:

  • From a manifestloadQtlDatasetFromManifest() reads a table (data.frame or file) that points at the phenotype / covariate / genotype files and assembles the object for you. This is the recommended route for real data.
  • Manually — build the per-context SummarizedExperiments and the GenotypeHandle yourself and call the QtlDataset() constructor. Useful when your data is already in memory.

Toy data for this vignette

We will reuse the toy_ref PLINK1 panel shipped with the package as the genotype source, take its first few samples as our phenotyped individuals, and write two small per-context phenotype BED files (TensorQTL-style layout: #chr, start, end, gene_id, then one column per sample) plus a covariate table to a temporary directory.

genoPrefix <- sub("\\.bed$", "",
                  system.file("extdata", "toy_ref.bed", package = "pecotmr"))
samples <- head(getSampleIds(GenotypeHandle(plink1Prefix = genoPrefix)), 40)

workdir <- file.path(tempdir(), "qtl-vignette")
dir.create(workdir, showWarnings = FALSE)

# Two genes on chr22, positioned inside the toy_ref variant span so a cis
# window captures nearby variants.
writePhenoBed <- function(path, seed) {
  set.seed(seed)
  bed <- data.frame(
    `#chr`  = c("chr22", "chr22"),
    start   = c(14600000L, 14900000L),
    end     = c(14600001L, 14900001L),
    gene_id = c("GENE1", "GENE2"),
    check.names = FALSE)
  for (s in samples) bed[[s]] <- round(rnorm(2), 3)
  readr::write_tsv(bed, path)
  path
}
muscleBed <- writePhenoBed(file.path(workdir, "Muscle.bed"), 1)
bloodBed  <- writePhenoBed(file.path(workdir, "Blood.bed"),  2)

# Per-context covariate table: samples as rows, covariate columns after an ID
# column (here two genotype PCs).
covPath <- file.path(workdir, "covariates.tsv")
set.seed(3)
readr::write_tsv(
  data.frame(sample = samples,
             PC1 = round(rnorm(length(samples)), 3),
             PC2 = round(rnorm(length(samples)), 3),
             check.names = FALSE),
  covPath)

Constructing a QtlDataset from a manifest

The QtlDataset manifest has one row per context. Canonical column names are camelCase; snake_case aliases such as cond, cov_path, genotype_prefix are also accepted and normalized on read.

Column Required Meaning
context (alias cond) yes context / condition label
phenotypePath (alias path) yes bgzipped or plain BED phenotype file
covariatePath (alias cov_path) no per-context covariate table
study single-valued study identifier (may also be an argument)
genotypePath (alias genotype_prefix) single-valued genotype file or PLINK prefix (may also be an argument)
genotypeCovariatePath no, single-valued genotype-derived covariates applied to all contexts

study and genotypePath are single-valued for the whole dataset, so you can supply them either as a constant manifest column or as an argument; if you give both, they must agree.

manifest <- data.frame(
  context       = c("Muscle", "Blood"),
  phenotypePath = c(muscleBed, bloodBed),
  covariatePath = c(covPath, covPath),
  study         = "GTEx_toy",
  genotypePath  = genoPrefix,
  stringsAsFactors = FALSE)

# The path columns hold absolute paths; show basenames for readability
# (the real `manifest` keeps the full paths the loader needs).
transform(manifest,
          phenotypePath = basename(phenotypePath),
          covariatePath = basename(covariatePath),
          genotypePath  = basename(genotypePath))
##   context phenotypePath  covariatePath    study genotypePath
## 1  Muscle    Muscle.bed covariates.tsv GTEx_toy      toy_ref
## 2   Blood     Blood.bed covariates.tsv GTEx_toy      toy_ref
qd <- loadQtlDatasetFromManifest(manifest)
qd
## QtlDataset for study 'GTEx_toy'
##   2 context(s): Muscle, Blood
##   2 unique traits across contexts
##   Genotypes: plink1 @ /tmp/Rtmpi0K25n/temp_libpatha6b6057489c/pecotmr/extdata/toy_ref
##   Genotype covariates: 0 cols
##   Scale residuals: TRUE

The manifest can equally be a path to a .csv (read as CSV) or any other extension (read as TSV):

manifestPath <- file.path(workdir, "qtl_manifest.tsv")
readr::write_tsv(manifest, manifestPath)
qd <- loadQtlDatasetFromManifest(manifestPath)

The QC thresholds are ordinary arguments (they are stored as lazy filters, not applied at load), for example:

qdFiltered <- loadQtlDatasetFromManifest(
  manifest, mafCutoff = 0.01, imissCutoff = 0.05, keepIndel = FALSE)

Constructing a QtlDataset manually

The manifest loader is a thin wrapper over the QtlDataset() constructor. Doing it by hand shows exactly what the object contains: a named list of SummarizedExperiments and a GenotypeHandle.

# One SummarizedExperiment per context. Rows are traits, columns are samples;
# rowRanges carries each trait's position, colData carries per-context
# covariates.
buildSE <- function(bedPath) {
  bed <- as.data.frame(readr::read_tsv(bedPath, show_col_types = FALSE),
                       check.names = FALSE)
  sampleCols <- setdiff(names(bed), c("#chr", "start", "end", "gene_id"))
  expr <- as.matrix(bed[, sampleCols, drop = FALSE])
  rownames(expr) <- bed$gene_id
  rr <- GenomicRanges::GRanges(
    seqnames = bed$`#chr`,
    ranges   = IRanges::IRanges(start = bed$start + 1L, end = bed$end))
  names(rr) <- bed$gene_id
  cov <- as.data.frame(readr::read_tsv(covPath, show_col_types = FALSE),
                       check.names = FALSE)
  cd <- S4Vectors::DataFrame(cov[match(sampleCols, cov$sample),
                                 c("PC1", "PC2"), drop = FALSE],
                             row.names = sampleCols)
  SummarizedExperiment::SummarizedExperiment(
    assays = list(expression = expr), rowRanges = rr, colData = cd)
}

phenotypes <- list(Muscle = buildSE(muscleBed), Blood = buildSE(bloodBed))
genotypes  <- GenotypeHandle(plink1Prefix = genoPrefix)

qdManual <- QtlDataset(
  study      = "GTEx_toy",
  genotypes  = genotypes,
  phenotypes = phenotypes)
qdManual
## QtlDataset for study 'GTEx_toy'
##   2 context(s): Muscle, Blood
##   2 unique traits across contexts
##   Genotypes: plink1 @ /tmp/Rtmpi0K25n/temp_libpatha6b6057489c/pecotmr/extdata/toy_ref
##   Genotype covariates: 0 cols
##   Scale residuals: TRUE

This approach gives users more flexibility if they need to make more modifications to the molecular phenotype data after loading it from a file but before making a QtlDataset

MultiStudyQtlDataset

A MultiStudyQtlDataset needs at least two studies in total - at least two individual-level studies or at least one individual-level study and one summary-only study. When loading from a manifest, a qtlDatasetsManifest with study and genotypePath must be provided where rows are grouped by study and one QtlDataset is built per group. A sumStatsManifest describing summary-only studies can also be provided (see the Constructing QtlSumStats and GwasSumStats objects vignette).

multiManifest <- data.frame(
  study         = c("StudyA", "StudyB"),
  context       = c("Muscle", "Muscle"),
  phenotypePath = c(muscleBed, bloodBed),
  genotypePath  = genoPrefix,
  stringsAsFactors = FALSE)

msd <- loadMultiStudyQtlDatasetFromManifest(multiManifest)
msd
## MultiStudyQtlDataset: 2 individual-level + 0 sumstats studies
##   Individual-level studies: StudyA, StudyB

A MultiStudyQtlDataset can be manually constructed by building a named list of QtlDatasets and passing it to the constructor:

msdManual <- MultiStudyQtlDataset(
  qtlDatasets = list(StudyA = qd, StudyB = qdManual))
names(msdManual@qtlDatasets)
## [1] "StudyA" "StudyB"

To attach a summary-only study, pass a QtlSumStats object to the sumStats argument. manifest form, supply sumStatsManifest, genome, and ldSketch).

Next steps

Once built, the accessors extract data lazily, applying the stored QC filters at extraction time:

## [1] "GTEx_toy"
## [1] "Muscle" "Blood"
# Cis genotypes around a trait (+/- a window), QC applied on the fly:
dim(getGenotypes(qd, traitId = "GENE1", cisWindow = 1e6))
## [1] 165  58
# Phenotype matrix for one context:
dim(SummarizedExperiment::assay(getPhenotypes(qd, contexts = "Muscle")))
## [1]  2 40

QtlDataset and MultiStudyQtlDataset are direct inputs to fineMappingPipeline(), twasWeightsPipeline() and colocBoostPipeline()— see the Fine-mapping with pecotmr, Learning TWAS weights with pecotmr and Multi-trait colocalization with ColocBoost vignettes.

## R version 4.5.3 (2026-03-11)
## Platform: x86_64-conda-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
## 
## Matrix products: default
## BLAS/LAPACK: /home/runner/work/pecotmr/pecotmr/.pixi/envs/default/lib/libopenblasp-r0.3.33.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.6.7
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_1.2.1            dplyr_1.2.1                
##  [3] farver_2.1.2                Biostrings_2.78.0          
##  [5] S7_0.2.2                    bitops_1.0-9               
##  [7] fastmap_1.2.0               reshape_0.8.10             
##  [9] mathjaxr_2.0-0              digest_0.6.39              
## [11] lifecycle_1.0.5             survival_3.8-9             
## [13] magrittr_2.0.5              compiler_4.5.3             
## [15] rlang_1.3.0                 sass_0.4.10                
## [17] tools_4.5.3                 yaml_2.3.12                
## [19] knitr_1.51                  S4Arrays_1.10.1            
## [21] htmlwidgets_1.6.4           bit_4.6.0                  
## [23] DelayedArray_0.36.0         plyr_1.8.9                 
## [25] RColorBrewer_1.1-3          abind_1.4-8                
## [27] BiocParallel_1.44.0         purrr_1.2.2                
## [29] numDeriv_2016.8-1.1         BiocGenerics_0.56.0        
## [31] desc_1.4.3                  grid_4.5.3                 
## [33] stats4_4.5.3                susieR_0.16.4              
## [35] ggplot2_4.0.3               scales_1.4.0               
## [37] MASS_7.3-66                 SummarizedExperiment_1.40.0
## [39] cli_3.6.6                   rmarkdown_2.31             
## [41] metafor_5.0-1               crayon_1.5.3               
## [43] ragg_1.5.2                  generics_0.1.4             
## [45] otel_0.2.0                  RcppParallel_5.1.11-2      
## [47] tzdb_0.5.0                  cachem_1.1.0               
## [49] stringr_1.6.0               splines_4.5.3              
## [51] metadat_1.6-0               parallel_4.5.3             
## [53] XVector_0.50.0              matrixStats_1.5.0          
## [55] vctrs_0.7.3                 Matrix_1.7-5               
## [57] jsonlite_2.0.0              IRanges_2.44.0             
## [59] hms_1.1.4                   S4Vectors_0.48.0           
## [61] bit64_4.8.2                 mixsqp_0.3-54              
## [63] irlba_2.3.7                 systemfonts_1.3.2          
## [65] tidyr_1.3.2                 jquerylib_0.1.4            
## [67] snpStats_1.60.0             glue_1.8.1                 
## [69] pkgdown_2.2.1               codetools_0.2-20           
## [71] stringi_1.8.7               gtable_0.3.6               
## [73] GenomicRanges_1.62.1        quadprog_1.5-8             
## [75] tibble_3.3.1                pillar_1.11.1              
## [77] htmltools_0.5.9             Seqinfo_1.0.0              
## [79] R6_2.6.1                    zigg_0.0.2                 
## [81] textshaping_1.0.5           vroom_1.7.1                
## [83] evaluate_1.0.5              lattice_0.22-9             
## [85] Biobase_2.70.0              readr_2.2.0                
## [87] Rsamtools_2.26.0            tictoc_1.2.1               
## [89] Rfast_2.1.5.2               bslib_0.11.0               
## [91] Rcpp_1.1.2                  SparseArray_1.10.8         
## [93] nlme_3.1-170                xfun_0.60                  
## [95] fs_2.1.0                    MatrixGenerics_1.22.0      
## [97] pkgconfig_2.0.3