Introduction

Methrix provides set of function which allows easy importing of various flavors of bedgraphs generated by methylation callers, and many downstream analysis to be performed on large matrices.

This vignette describes basic usage of the package intended to process several large bedgraph files in R. In addition, a detailed exemplary complete data analysis with steps from reading in to annotation and differential methylation calling can be found in our WGBS best practices workflow

Overview and usage functions of the package

Installation

if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")

#Installing stable version from BioConductor
BiocManager::install("methrix")

#Installing developmental version from GitHub
BiocManager::install("CompEpigen/methrix")

NOTE

Installation from BioConductor requires the BioC and R versions to be the newest. This arises from the restrictions imposed by BioConductor community which might cause package incompatibilities with the earlier versions of R (for e.g; R < 4.0). In that case installing from GitHub might be easier since it is much more merciful with regards to versions.

Reading bedgraph files

read_bedgraphs function is a versatile bedgraph reader intended to import bedgraph files generated virtually by any sort of methylation calling program. It requires user to provide indices for chromosome names, start position and other required fields. There are also presets available to import bedgraphs from most common programs such as Bismark, MethylDackel, and MethylcTools.

#Load library
library(methrix)
#Genome of your preference to work with
if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")

library(BiocManager)

if(!requireNamespace("BSgenome.Hsapiens.UCSC.hg19")) {
BiocManager::install("BSgenome.Hsapiens.UCSC.hg19")
}
library(BSgenome.Hsapiens.UCSC.hg19) 
#Example bedgraph files
bdg_files <- list.files(
  path = system.file('extdata', package = 'methrix'),
  pattern = "*bedGraph\\.gz$",
  full.names = TRUE
)

print(basename(bdg_files))
#> [1] "C1.bedGraph.gz" "C2.bedGraph.gz" "N1.bedGraph.gz" "N2.bedGraph.gz"

#Generate some sample annotation table
sample_anno <- data.frame(
  row.names = gsub(
    pattern = "\\.bedGraph\\.gz$",
    replacement = "",
    x = basename(bdg_files)
  ),
  Condition = c("cancer", 'cancer', "normal", "normal"),
  Pair = c("pair1", "pair2", "pair1", "pair2"),
  stringsAsFactors = FALSE
)

print(sample_anno)
#>    Condition  Pair
#> C1    cancer pair1
#> C2    cancer pair2
#> N1    normal pair1
#> N2    normal pair2

We can import bedgraph files with the function read_bedgraphs which reads in the bedgraphs, adds CpGs missing from the reference set, and creates a methylation/coverage matrices. Once the process is complete - it returns an object of class methrix which in turn inherits SummarizedExperiment class. methrix object contains ‘methylation’ and ‘coverage’ matrices (either in-memory or as on-disk HDF5 arrays) along with pheno-data and other basic info. This object can be passed to all downstream functions for various analysis.

#First extract genome wide CpGs from the desired reference genome
hg19_cpgs <- suppressWarnings(methrix::extract_CPGs(ref_genome = "BSgenome.Hsapiens.UCSC.hg19"))
#> 
#> Attaching package: 'Biostrings'
#> The following object is masked from 'package:base':
#> 
#>     strsplit
#> 
#> Attaching package: 'rtracklayer'
#> The following object is masked from 'package:BiocIO':
#> 
#>     FileForFormat
#> -Extracting CpGs
#> -Done. Extracted 28,217,448 CpGs from 25 contigs.
#Read the files 
meth <- methrix::read_bedgraphs(
  files = bdg_files,
  ref_cpgs = hg19_cpgs,
  chr_idx = 1,
  start_idx = 2,
  M_idx = 3,
  U_idx = 4,
  stranded = FALSE,
  zero_based = FALSE, 
  collapse_strands = FALSE, 
  coldata = sample_anno
)
#> ----------------------------
#> -Preset:        Custom
#> --Missing beta and coverage info. Estimating them from M and U values
#> -CpGs raw:      28,217,448 (total reference CpGs)
#> -CpGs retained: 28,217,448(reference CpGs from contigs of interest)
#> ----------------------------
#> -Processing:    C1.bedGraph.gz
#> --CpGs missing: 28,216,771 (from known reference CpGs)
#> -Processing:    C2.bedGraph.gz
#> --CpGs missing: 28,216,759 (from known reference CpGs)
#> -Processing:    N1.bedGraph.gz
#> --CpGs missing: 28,216,746 (from known reference CpGs)
#> -Processing:    N2.bedGraph.gz
#> --CpGs missing: 28,216,747 (from known reference CpGs)
#> -Finished in:  00:01:15 elapsed (51.3s cpu)

Note: Use the argument pipeline if your bedgraphs are generated with “Bismark”, “MethylDeckal”, or “MethylcTools”. This will automatically figure out the file formats for you, and you dont have to use the arguments chr_idx start_idx and so..

#Typing meth shows basic summary.
meth
#> An object of class methrix
#>    n_CpGs: 28,217,448
#> n_samples: 4
#>     is_h5: FALSE
#> Reference: hg19

HTML QC report

Get basic summary statistics of the methrix object with methrix_report function which produces an interactive html report

methrix::methrix_report(meth = meth, output_dir = tempdir())

Click here for an example report.

Filtering

Remove uncovered loci

Usual task in analysis involves removing uncovered CpGs. i.e, those loci which are not covered across all sample (in other words covered only in subset of samples resulting NA for rest of the samples ).

meth = methrix::remove_uncovered(m = meth)
#> -Removed 28,216,705 [100%] uncovered loci of 28,217,448 sites
#> -Finished in:  3.133s elapsed (2.064s cpu)
meth
#> An object of class methrix
#>    n_CpGs: 743
#> n_samples: 4
#>     is_h5: FALSE
#> Reference: hg19

Remove SNPs

One can also remove CpG sites overlaping with common SNPs based on minor allele frequencies.

if(!require(MafDb.1Kgenomes.phase3.hs37d5)) {
BiocManager::install("MafDb.1Kgenomes.phase3.hs37d5")} 
if(!require(GenomicScores)) {
BiocManager::install("GenomicScores")} 
library(MafDb.1Kgenomes.phase3.hs37d5)
#> Loading required package: GenomicScores
#> 
#> Attaching package: 'GenomicScores'
#> The following object is masked from 'package:utils':
#> 
#>     citation
#> Warning: replacing previous import 'utils::findMatches' by
#> 'S4Vectors::findMatches' when loading 'MafDb.1Kgenomes.phase3.hs37d5'
library(GenomicScores)

meth_snps_filtered <- methrix::remove_snps(m = meth)
#> Used SNP database: MafDb.1Kgenomes.phase3.hs37d5.
#> Number of SNPs removed:
#>       chr     N
#>    <char> <int>
#> 1:  chr21    42
#> 2:  chr22    39
#> Sum:
#> [1] 81
#> -Finished in:  4.430s elapsed (3.142s cpu)

Basic operations

Extract methylation/coverage matrices

#Example data bundled, same as the previously generated meth 
data("methrix_data")

#Coverage matrix
coverage_mat <- methrix::get_matrix(m = methrix_data, type = "C")
head(coverage_mat)
#>      C1 C2 N1 N2
#> [1,] 13  7  9 10
#> [2,] NA  2  3 NA
#> [3,]  9 10  3  5
#> [4,] 11  8 12  8
#> [5,]  6  7 17  8
#> [6,] 13  6  6 14
#Methylation matrix
meth_mat <- methrix::get_matrix(m = methrix_data, type = "M")
head(meth_mat)
#>             C1        C2        N1        N2
#> [1,] 0.1538462 0.2857143 0.5555556 0.3000000
#> [2,]        NA 0.5000000 0.0000000        NA
#> [3,] 0.5555556 0.7000000 0.3333333 0.8000000
#> [4,] 0.1818182 0.2500000 0.5833333 0.2500000
#> [5,] 0.6666667 1.0000000 0.8823529 0.8750000
#> [6,] 0.8461538 1.0000000 0.8333333 0.9285714
#If you prefer you can attach loci info to the matrix and output in GRanges format
meth_mat_with_loci <- methrix::get_matrix(m = methrix_data, type = "M", add_loci = TRUE, in_granges = TRUE)
meth_mat_with_loci
#> GRanges object with 743 ranges and 4 metadata columns:
#>         seqnames            ranges strand |        C1        C2        N1
#>            <Rle>         <IRanges>  <Rle> | <numeric> <numeric> <numeric>
#>     [1]    chr21 27866423-27866424      * |  0.153846  0.285714  0.555556
#>     [2]    chr21 27866575-27866576      * |        NA  0.500000  0.000000
#>     [3]    chr21 27866921-27866922      * |  0.555556  0.700000  0.333333
#>     [4]    chr21 27867197-27867198      * |  0.181818  0.250000  0.583333
#>     [5]    chr21 27867248-27867249      * |  0.666667  1.000000  0.882353
#>     ...      ...               ...    ... .       ...       ...       ...
#>   [739]    chr22 49007313-49007314      * |  1.000000  0.714286  0.857143
#>   [740]    chr22 49007329-49007330      * |  1.000000  0.428571  1.000000
#>   [741]    chr22 49007347-49007348      * |  0.666667  0.166667  0.875000
#>   [742]    chr22 49007375-49007376      * |  0.333333  0.125000  1.000000
#>   [743]    chr22 49007398-49007399      * |  1.000000  0.600000  1.000000
#>                N2
#>         <numeric>
#>     [1]     0.300
#>     [2]        NA
#>     [3]     0.800
#>     [4]     0.250
#>     [5]     0.875
#>     ...       ...
#>   [739]       1.0
#>   [740]       1.0
#>   [741]       1.0
#>   [742]       0.6
#>   [743]       1.0
#>   -------
#>   seqinfo: 2 sequences from an unspecified genome; no seqlengths

Coverage filter

Furthermore if you prefer you can filter sites based on coverage conditions.

#e.g; Retain all loci which are covered at-least in two sample by 3 or more reads
methrix::coverage_filter(m = methrix_data, cov_thr = 3, min_samples = 2)
#> -Retained 600 of 743 sites
#> -Finished in:  1.479s elapsed (1.216s cpu)
#> An object of class methrix
#>    n_CpGs: 600
#> n_samples: 4
#>     is_h5: FALSE
#> Reference: hg19

Subset operations

Subset operations in methrix make use of data.tables fast binary search which is several orders faster than bsseq or other similar packages.

Subset by chromosome

#Retain sites only from chromosme chr21
methrix::subset_methrix(m = methrix_data, contigs = "chr21")
#> -Subsetting by contigs
#> An object of class methrix
#>    n_CpGs: 540
#> n_samples: 4
#>     is_h5: FALSE
#> Reference: hg19

Subset by genomic regions

Regions can be data.table or GRanges format.

#e.g; Retain sites only in TP53 loci 
target_loci <- GenomicRanges::GRanges("chr21:27867971-27868103")

print(target_loci)
#> GRanges object with 1 range and 0 metadata columns:
#>       seqnames            ranges strand
#>          <Rle>         <IRanges>  <Rle>
#>   [1]    chr21 27867971-27868103      *
#>   -------
#>   seqinfo: 1 sequence from an unspecified genome; no seqlengths

methrix::subset_methrix(m = methrix_data, regions = target_loci)
#> -Subsetting by genomic regions
#> An object of class methrix
#>    n_CpGs: 4
#> n_samples: 4
#>     is_h5: FALSE
#> Reference: hg19

Subset by samples

methrix::subset_methrix(m = methrix_data, samples = "C1")
#> Subsetting by samples
#> An object of class methrix
#>    n_CpGs: 743
#> n_samples: 1
#>     is_h5: FALSE
#> Reference: hg19

#Or you could use [] operator to subset by index
methrix_data[,1]
#> An object of class methrix
#>    n_CpGs: 743
#> n_samples: 1
#>     is_h5: FALSE
#> Reference: hg19

Summary statsitcis

Basic summaries

meth_stats <- get_stats(m = methrix_data)
#> -Finished in:  1.379s elapsed (1.247s cpu)
print(meth_stats)
#>    Chromosome Sample_Name    mean_meth  median_meth      sd_meth     mean_cov
#>        <fctr>      <char>       <list>       <list>       <list>       <list>
#> 1:      chr21          C1 0.560004.... 0.651515.... 0.400401.... 4.745967....
#> 2:      chr21          C2 0.493499....          0.5 0.389620.... 5.047524....
#> 3:      chr21          N1 0.524541....       0.6125 0.420522.... 4.978682....
#> 4:      chr21          N2 0.533344.... 0.666666.... 0.422757.... 5.055662....
#> 5:      chr22          C1 0.739242.... 0.857142.... 0.311536.... 4.657458....
#> 6:      chr22          C2 0.577809.... 0.651515.... 0.367876....          5.5
#> 7:      chr22          N1 0.844555....            1 0.227311.... 5.505376....
#> 8:      chr22          N2 0.852069....            1 0.220972.... 5.866666....
#>    median_cov       sd_cov
#>        <list>       <list>
#> 1:          4 2.990217....
#> 2:          4 3.294072....
#> 3:          4 3.214882....
#> 4:          5 3.148071....
#> 5:          4 2.813422....
#> 6:          5 3.046093....
#> 7:          5 3.270254....
#> 8:          5 3.166514....
#Draw mean coverage per sample
plot_stats(plot_dat = meth_stats, what = "C", stat = "mean")

#Draw mean methylation per sample
plot_stats(plot_dat = meth_stats, what = "M", stat = "mean")

PCA

mpca <- methrix_pca(m = methrix_data, do_plot = FALSE)

#Plot PCA results
plot_pca(pca_res = mpca, show_labels = TRUE)


#Color code by an annotation
plot_pca(pca_res = mpca, m = methrix_data, col_anno = "Condition")

Plotting

Methylation

#Violin plots
methrix::plot_violin(m = methrix_data)
#> Randomly selecting 25000 sites
#> Warning: Removed 203 rows containing non-finite outside the scale range
#> (`stat_ydensity()`).

Coverage

methrix::plot_coverage(m = methrix_data, type = "dens")

Converting methrix to BSseq

If you prefer to work with bsseq object, you can generate bsseq object from methrix with the methrix2bsseq.

if(!require(bsseq)) {
BiocManager::install("bsseq")}
library(bsseq)
bs_seq <- methrix::methrix2bsseq(m = methrix_data)

bs_seq
#> An object of type 'BSseq' with
#>   743 methylation loci
#>   4 samples
#> has not been smoothed
#> All assays are in-memory

SessionInfo

sessionInfo()
#> R version 4.4.1 (2024-06-14)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /home/biocbuild/bbs-3.20-bioc/R/lib/libRblas.so 
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.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] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] bsseq_1.42.0                         MafDb.1Kgenomes.phase3.hs37d5_3.10.0
#>  [3] GenomicScores_2.18.0                 BSgenome.Hsapiens.UCSC.hg19_1.4.3   
#>  [5] BSgenome_1.74.0                      rtracklayer_1.66.0                  
#>  [7] BiocIO_1.16.0                        Biostrings_2.74.0                   
#>  [9] XVector_0.46.0                       methrix_1.20.0                      
#> [11] SummarizedExperiment_1.36.0          Biobase_2.66.0                      
#> [13] GenomicRanges_1.58.0                 GenomeInfoDb_1.42.0                 
#> [15] IRanges_2.40.0                       S4Vectors_0.44.0                    
#> [17] BiocGenerics_0.52.0                  MatrixGenerics_1.18.0               
#> [19] matrixStats_1.4.1                    data.table_1.16.2                   
#> 
#> loaded via a namespace (and not attached):
#>  [1] DBI_1.2.3                 bitops_1.0-9             
#>  [3] permute_0.9-7             rlang_1.1.4              
#>  [5] magrittr_2.0.3            compiler_4.4.1           
#>  [7] RSQLite_2.3.7             DelayedMatrixStats_1.28.0
#>  [9] png_0.1-8                 vctrs_0.6.5              
#> [11] pkgconfig_2.0.3           crayon_1.5.3             
#> [13] fastmap_1.2.0             dbplyr_2.5.0             
#> [15] labeling_0.4.3            utf8_1.2.4               
#> [17] Rsamtools_2.22.0          rmarkdown_2.28           
#> [19] UCSC.utils_1.2.0          bit_4.5.0                
#> [21] xfun_0.48                 zlibbioc_1.52.0          
#> [23] cachem_1.1.0              jsonlite_1.8.9           
#> [25] blob_1.2.4                highr_0.11               
#> [27] rhdf5filters_1.18.0       DelayedArray_0.32.0      
#> [29] Rhdf5lib_1.28.0           BiocParallel_1.40.0      
#> [31] parallel_4.4.1            R6_2.5.1                 
#> [33] bslib_0.8.0               RColorBrewer_1.1-3       
#> [35] limma_3.62.0              jquerylib_0.1.4          
#> [37] Rcpp_1.0.13               knitr_1.48               
#> [39] R.utils_2.12.3            Matrix_1.7-1             
#> [41] tidyselect_1.2.1          abind_1.4-8              
#> [43] yaml_2.3.10               codetools_0.2-20         
#> [45] curl_5.2.3                lattice_0.22-6           
#> [47] tibble_3.2.1              withr_3.0.2              
#> [49] KEGGREST_1.46.0           evaluate_1.0.1           
#> [51] BiocFileCache_2.14.0      pillar_1.9.0             
#> [53] BiocManager_1.30.25       filelock_1.0.3           
#> [55] generics_0.1.3            RCurl_1.98-1.16          
#> [57] BiocVersion_3.20.0        ggplot2_3.5.1            
#> [59] sparseMatrixStats_1.18.0  munsell_0.5.1            
#> [61] scales_1.3.0              gtools_3.9.5             
#> [63] glue_1.8.0                tools_4.4.1              
#> [65] AnnotationHub_3.14.0      locfit_1.5-9.10          
#> [67] GenomicAlignments_1.42.0  XML_3.99-0.17            
#> [69] rhdf5_2.50.0              grid_4.4.1               
#> [71] AnnotationDbi_1.68.0      colorspace_2.1-1         
#> [73] GenomeInfoDbData_1.2.13   HDF5Array_1.34.0         
#> [75] restfulr_0.0.15           cli_3.6.3                
#> [77] rappdirs_0.3.3            fansi_1.0.6              
#> [79] S4Arrays_1.6.0            dplyr_1.1.4              
#> [81] gtable_0.3.6              R.methodsS3_1.8.2        
#> [83] sass_0.4.9                digest_0.6.37            
#> [85] SparseArray_1.6.0         rjson_0.2.23             
#> [87] farver_2.1.2              memoise_2.0.1            
#> [89] htmltools_0.5.8.1         R.oo_1.26.0              
#> [91] lifecycle_1.0.4           httr_1.4.7               
#> [93] statmod_1.5.0             bit64_4.5.2