GSE244696 (doi: https://doi.org/10.1016/j.trsl.2024.05.005) is a PDAC MethylationEPIC study comparing tumor-cell enriched (“Tumor”) and stroma-cell enriched (“Stroma”) compartments. This notebook uses CMEnt in a meta-analysis mode: the published DMP list (Supplementary table 1) is treated as the external evidence, and CMEnt uses the available beta matrix from GEO to assemble coherent DMRs around those known signals. The only external DMR comparison retained here is the Bumphunter DMR table (Supplementary table 2) provided by the original researchers.
ensure_cran_packages <- function(pkgs) {
repos <- getOption("repos")
if (is.null(repos) || is.na(repos["CRAN"]) || repos["CRAN"] %in% c("@CRAN@", "")) {
options(repos = c(CRAN = "https://cloud.r-project.org"))
}
missing <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing) > 0L) {
install.packages(missing)
}
}
ensure_bioc_packages <- function(pkgs) {
if (!requireNamespace("BiocManager", quietly = TRUE)) {
ensure_cran_packages("BiocManager")
}
missing <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing) > 0L) {
BiocManager::install(missing, update = FALSE, ask = FALSE)
}
}
suppressWarnings(
{
ensure_cran_packages(c("readxl", "data.table", "devtools", "dplyr", "ggplot2", "tidyr", "scales", "svglite"))
ensure_bioc_packages(c(
"BiocStyle", "minfi", "GenomicRanges", "annotatr", "missMethyl",
"org.Hs.eg.db", "TxDb.Hsapiens.UCSC.hg19.knownGene",
"IlluminaHumanMethylationEPICanno.ilm10b4.hg19", "clusterProfiler",
"ReactomePA", "pathview", "KEGGREST"
))
}
)
.loadCMEnt <- function() {
root <- if (file.exists("NAMESPACE")) "." else if (file.exists(file.path("..", "NAMESPACE"))) ".." else NULL
if (!is.null(root) && requireNamespace("pkgload", quietly = TRUE)) {
ok <- tryCatch({
pkgload::load_all(root, quiet = TRUE)
TRUE
}, error = function(e) {
message("Could not load local CMEnt source: ", conditionMessage(e))
FALSE
})
if (ok) return(invisible(TRUE))
}
suppressPackageStartupMessages(library(CMEnt))
}
.loadCMEnt()
suppressPackageStartupMessages({
library(data.table)
library(dplyr)
library(ggplot2)
library(GenomicRanges)
library(minfi)
})
getBenchmarkFile <- function(filename) {
folder <- "metastudyEpic"
if (getwd() == "notebooks") {
benchmark_file <- file.path("cached_results", folder, filename)
} else if ("NAMESPACE" %in% dir()) {
benchmark_file <- file.path("notebooks", "cached_results", folder, filename)
} else {
benchmark_file <- file.path("cached_results", folder, filename)
}
dir.create(dirname(benchmark_file), showWarnings = FALSE, recursive = TRUE)
benchmark_file
}
short_label <- function(x, width = 65L) {
x <- as.character(x)
ifelse(nchar(x) > width, paste0(substr(x, 1L, width - 3L), "..."), x)
}
has_text <- function(x) {
!is.na(x) & nzchar(as.character(x))
}
theme_meta <- function() {
theme_minimal(base_size = 11) +
theme(
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 35, hjust = 1),
legend.position = "bottom"
)
}
method_palette <- c(CMEnt = "#0072B2", `Published Bumphunter` = "#D55E00")
array_type <- "EPIC"
genome <- "hg19"
beta_file <- getBenchmarkFile("GSE244696_beta_values.tsv.gz")
samplesheet <- getBenchmarkFile("GSE244696_samplesheet.tsv")
dmp_file <- getBenchmarkFile("GSE244696_DMPs.xlsx")
published_dmr_file <- getBenchmarkFile("GSE244696_DMRs.xlsx")
zenodo_base_url <- "https://zenodo.org/records/20592944/files/"
if (!file.exists(beta_file)) {
curl::curl_download(
url = paste0(zenodo_base_url, basename(beta_file), "?download=1"),
destfile = beta_file
)
}
if (!file.exists(samplesheet)) {
curl::curl_download(
url = paste0(zenodo_base_url, basename(samplesheet), "?download=1"),
destfile = samplesheet
)
}
if (!file.exists(dmp_file)) {
curl::curl_download(
url = paste0(zenodo_base_url, basename(dmp_file), "?download=1"),
destfile = dmp_file
)
}
if (!file.exists(published_dmr_file)) {
curl::curl_download(
url = paste0(zenodo_base_url, basename(published_dmr_file), "?download=1"),
destfile = published_dmr_file
)
}
pheno <- utils::read.table(samplesheet, header = TRUE, stringsAsFactors = FALSE, sep = "\t", check.names = FALSE)
if (all(rownames(pheno) == as.character(seq_len(nrow(pheno)))) && nzchar(names(pheno)[1])) {
rownames(pheno) <- pheno[[1]]
}
if (all(rownames(pheno) == as.character(seq_len(nrow(pheno)))) && !nzchar(names(pheno)[1])) {
rownames(pheno) <- pheno[[1]]
}
pheno$casecontrol <- pheno$tissue.ch1 == "Tumor"
beta_handler <- CMEnt::getBetaHandler(beta_file, array = array_type, genome = genome)
beta_mat <- as.matrix(beta_handler$getBeta())
locs <- beta_handler$getBetaLocs()
beta_row_names <- beta_handler$getBetaRowNames()
common_samples <- intersect(colnames(beta_mat), rownames(pheno))
beta_mat <- beta_mat[, common_samples, drop = FALSE]
pheno <- pheno[common_samples, , drop = FALSE]
case_samples <- rownames(pheno)[pheno$tissue.ch1 == "Tumor"]
control_samples <- rownames(pheno)[pheno$tissue.ch1 == "Stroma"]
reported_dmps_raw <- suppressWarnings(suppressMessages(readxl::read_xlsx(
dmp_file,
sheet = "Suppl_Table 1",
range = readxl::cell_cols(1:21),
.name_repair = "unique"
)))
reported_dmps <- data.frame(
site_id = as.character(reported_dmps_raw[[1]]),
pval = reported_dmps_raw[["Stroma_to_Tumor.P.Value"]],
qval = reported_dmps_raw[["Stroma_to_Tumor.adj.P.Val"]],
logFC_reported = reported_dmps_raw[["Stroma_to_Tumor.logFC"]],
delta_beta_reported = reported_dmps_raw[["Stroma_to_Tumor.deltaBeta"]],
stroma_avg_reported = reported_dmps_raw[["Stroma_to_Tumor.Stroma_AVG"]],
tumor_avg_reported = reported_dmps_raw[["Stroma_to_Tumor.Tumor_AVG"]],
chr_reported = paste0("chr", reported_dmps_raw[["Stroma_to_Tumor.CHR"]]),
mapinfo_reported = reported_dmps_raw[["Stroma_to_Tumor.MAPINFO"]],
gene_reported = reported_dmps_raw[["Stroma_to_Tumor.gene"]],
feature_reported = reported_dmps_raw[["Stroma_to_Tumor.feature"]],
cgi_reported = reported_dmps_raw[["Stroma_to_Tumor.cgi"]],
stringsAsFactors = FALSE
)
reported_dmps <- reported_dmps[!is.na(reported_dmps$site_id) & nzchar(reported_dmps$site_id), ]
reported_dmps <- reported_dmps[!duplicated(reported_dmps$site_id), ]
sig_dmps <- reported_dmps[reported_dmps$site_id %in% beta_row_names, , drop = FALSE]
sig_dmps$delta_beta_available <- rowMeans(beta_mat[sig_dmps$site_id, case_samples, drop = FALSE], na.rm = TRUE) -
rowMeans(beta_mat[sig_dmps$site_id, control_samples, drop = FALSE], na.rm = TRUE)
dmp_locs <- locs[sig_dmps$site_id, , drop = FALSE]
dmp_gr <- makeGRangesFromDataFrame(
data.frame(
chr = dmp_locs$chr,
start = dmp_locs$start,
end = dmp_locs$start + 1L,
site_id = sig_dmps$site_id,
pval = sig_dmps$pval,
qval = sig_dmps$qval
),
keep.extra.columns = TRUE
)
names(dmp_gr) <- sig_dmps$site_id
published_dmr_raw <- suppressWarnings(suppressMessages(readxl::read_xlsx(
published_dmr_file,
sheet = "Suppl_DMR",
range = readxl::cell_cols(1:32),
.name_repair = "unique"
)))
published_bumphunter <- makeGRangesFromDataFrame(
as.data.frame(published_dmr_raw, check.names = FALSE),
seqnames.field = "seqnames",
start.field = "start",
end.field = "end",
keep.extra.columns = TRUE
)
names(published_bumphunter) <- make.unique(as.character(published_dmr_raw[[1]]))
genome(published_bumphunter) <- genome
mcols(published_bumphunter)$dmr_id <- names(published_bumphunter)
input_summary <- data.frame(
Metric = c(
"Samples",
"Tumor samples",
"Stroma samples",
"Published DMPs",
"Published DMPs present in beta matrix",
"Published Bumphunter DMRs"
),
Value = c(
nrow(pheno),
length(case_samples),
length(control_samples),
nrow(reported_dmps),
nrow(sig_dmps),
length(published_bumphunter)
)
)
knitr::kable(input_summary, row.names = FALSE, caption = "Meta-study input summary")
| Metric | Value |
|---|---|
| Samples | 26 |
| Tumor samples | 13 |
| Stroma samples | 13 |
| Published DMPs | 25410 |
| Published DMPs present in beta matrix | 22667 |
| Published Bumphunter DMRs | 29 |
dmp_direction <- data.frame(
Direction = c("Higher in Tumor", "Higher in Stroma"),
Count = c(sum(sig_dmps$delta_beta_available > 0, na.rm = TRUE), sum(sig_dmps$delta_beta_available < 0, na.rm = TRUE))
)
knitr::kable(dmp_direction, row.names = FALSE, caption = "Direction of published DMPs")
| Direction | Count |
|---|---|
| Higher in Tumor | 17566 |
| Higher in Stroma | 5101 |
sig_dmps_delta_beta_medians <- sig_dmps |>
group_by(Direction = ifelse(delta_beta_available > 0, "Higher in Tumor", "Higher in Stroma")) |>
summarise(
Median = median(delta_beta_available, na.rm = TRUE),
y = 1.1 * max(hist(delta_beta_available, breaks = 60, plot = FALSE)$counts, na.rm = TRUE)
)
ggplot(sig_dmps, aes(x = delta_beta_available)) +
geom_histogram(bins = 60, fill = "#0072B2", color = "white", linewidth = 0.15) +
geom_vline(xintercept = 0, linetype = "dashed", color = "grey35") +
geom_text(
data = sig_dmps_delta_beta_medians,
aes(x = Median, y = y, label = paste0("Median: ", round(Median, 3))),
inherit.aes = FALSE,
color = "black",
fontface = "bold",
size = 3.5
) +
theme_meta() +
labs(
title = "Effect Size for Published DMPs",
x = "Delta beta", y = "Published DMPs"
)
Figure 1: Distribution of available Tumor-minus-Stroma delta beta values for the published GSE244696 DMPs
cment_file <- getBenchmarkFile("dmrs.CMEnt.meta_reported_dmps.rds")
if (!file.exists(cment_file)) {
cment_njobs <- max(1L, min(8L, parallel::detectCores(logical = TRUE) - 1L))
options("CMEnt.verbose" = 1)
options("CMEnt.njobs" = cment_njobs)
cat(sprintf("CMEnt parallel jobs: %d\n", cment_njobs))
start_time <- Sys.time()
dmrs_cment <- CMEnt::buildDMRs(
beta = beta_handler,
seeds = sig_dmps,
pheno = pheno,
seeds_id_col = "site_id",
sample_group_col = "tissue.ch1",
covariates = "predictedSex",
max_bridge_seeds_gaps = 1,
max_bridge_extension_gaps = 3,
min_seeds = 2,
min_sites = 3,
ext_site_delta_beta = 0.2,
max_lookup_dist = 1000,
max_pval = 0.05,
entanglement = "weak",
score_dmrs = TRUE,
annotate_with_genes = TRUE,
extract_motifs = TRUE,
njobs = cment_njobs,
verbose = 1
)
time_cment <- Sys.time() - start_time
saveRDS(list(dmrs = dmrs_cment, time = time_cment), cment_file)
} else {
ret <- readRDS(cment_file)
dmrs_cment <- ret$dmrs
time_cment <- ret$time
}
cat(sprintf("CMEnt found %d DMRs in %d seconds.\n", length(dmrs_cment), round(as.numeric(time_cment, units = "secs"))))
CMEnt found 2104 DMRs in 177 seconds.
if (is.null(dmrs_cment)) {
dmrs_cment <- GRanges()
}
if (length(dmrs_cment) > 0L && !"in_promoter_of" %in% colnames(mcols(dmrs_cment))) {
dmrs_cment <- CMEnt::annotateDMRsWithGenes(dmrs_cment, genome = genome)
}
published_bumphunter <- CMEnt::annotateDMRsWithGenes(published_bumphunter, genome = genome)
methods_list <- list(CMEnt = dmrs_cment, `BH` = published_bumphunter)
dmr_effect_df <- bind_rows(
data.frame(
Region = "Promoter-overlapping DMRs",
Width = width(dmrs_cment),
Delta_beta = as.numeric(mcols(dmrs_cment)$delta_beta_promoter)
),
data.frame(
Region = "Gene-body-overlapping DMRs",
Width = width(dmrs_cment),
Delta_beta = as.numeric(mcols(dmrs_cment)$delta_beta_gene_body)
)
) |>
filter(is.finite(Delta_beta)) |>
mutate(
Region = factor(Region, levels = unique(Region)),
Direction = factor(
ifelse(Delta_beta > 0, "Hyper", "Hypo"),
levels = c("Hyper", "Hypo")
)
)
library(dplyr)
library(ggplot2)
library(scales)
ignore_abs_delta <- !is.finite(dmr_effect_df$Delta_beta) | abs(dmr_effect_df$Delta_beta) < 0.05
dmr_effect_df <- dmr_effect_df[!ignore_abs_delta, ]
# Summary per Region and Direction
dmr_summary <- dmr_effect_df |>
group_by(Region, Direction) |>
summarise(
median_delta_beta = median(Delta_beta, na.rm = TRUE),
median_width_bp = median(Width, na.rm = TRUE),
n_dmrs = n(),
.groups = "drop"
)
mood_median_p_value <- function(width, direction) {
widths <- split(width, direction)
if (!all(c("Hyper", "Hypo") %in% names(widths)) || any(lengths(widths[c("Hyper", "Hypo")]) < 2L)) {
return(NA_real_)
}
median_width <- median(width, na.rm = TRUE)
keep <- is.finite(width) & width != median_width
tab <- table(
factor(direction[keep], levels = c("Hypo", "Hyper")),
factor(width[keep] > median_width, levels = c(FALSE, TRUE))
)
if (any(rowSums(tab) == 0L) || any(colSums(tab) == 0L)) {
return(NA_real_)
}
suppressWarnings(stats::chisq.test(tab, correct = FALSE)$p.value)
}
p_to_stars <- function(p) {
case_when(
is.na(p) ~ "",
p < 0.001 ~ "***",
p < 0.01 ~ "**",
p < 0.05 ~ "*",
TRUE ~ ""
)
}
x_range <- range(dmr_effect_df$Delta_beta, na.rm = TRUE)
x_min <- x_range[[1]]
region_levels <- unique(dmr_effect_df$Region)
dmr_mood <- dmr_summary |>
group_by(Region) |>
summarise(
star_y = exp(mean(log(median_width_bp))), .groups = "drop",
segy_min = min(median_width_bp), segy_max = max(median_width_bp)
) |>
left_join(
dmr_effect_df |>
group_by(Region) |>
summarise(mood_p = mood_median_p_value(Width, Direction), .groups = "drop"),
by = "Region"
) |>
mutate(
stars = p_to_stars(mood_p),
star_x = x_min - 0.05 * abs(x_min),
hjust = 0.5
)
# Define two y-levels for the two summary bars
y_max <- max(dmr_effect_df$Width, na.rm = TRUE)
dmr_summary <- dmr_summary |>
mutate(
bar_y = case_when(
Direction == "Hyper" ~ y_max * 2.0,
Direction == "Hypo" ~ y_max * 3.2
),
label_y = bar_y * 1.12
)
# Expand y-range so the bars and labels fit
y_upper <- max(dmr_summary$label_y) * 1.15
ggplot(dmr_effect_df, aes(x = Delta_beta, y = Width, color = Direction)) +
geom_hline(
data = dmr_summary,
aes(yintercept = median_width_bp, color = Direction),
inherit.aes = FALSE,
linewidth = 0.2
) +
geom_vline(xintercept = 0, linewidth = 0.2, color = "grey70") +
geom_point(alpha = 0.55, size = 1.2) +
geom_segment(
data = dmr_mood,
aes(x = star_x + 0.05 * abs(x_min), xend = star_x + 0.05 * abs(x_min), y = segy_min, yend = segy_max),
inherit.aes = FALSE,
linewidth = 0.4,
arrow = arrow(length = unit(0.15, "cm"), type = "closed", ends = "both"),
color = "black",
linetype = "dashed"
) +
geom_text(
data = dmr_mood,
aes(x = star_x, y = star_y, label = stars, hjust = hjust),
inherit.aes = FALSE,
angle = 90,
color = "black",
fontface = "bold",
size = 5
) +
# Separate horizontal bars for hypo and hyper
geom_segment(
data = dmr_summary,
aes(
x = 0,
xend = median_delta_beta,
y = bar_y,
yend = bar_y,
color = Direction
),
inherit.aes = FALSE,
linewidth = 5,
lineend = "butt"
) +
# Labels for each bar
geom_text(
data = dmr_summary,
aes(
x = median_delta_beta,
y = label_y,
label = paste0(
"median Δβ = ", round(median_delta_beta, 3),
"\nmedian width = ", round(median_width_bp, 1), " bp",
"\nDMRs = ", n_dmrs
)
),
inherit.aes = FALSE,
color = "black",
size = 4,
hjust = ifelse(dmr_summary$median_delta_beta >= 0, -0.05, 1.05)
) +
facet_wrap(~Region, nrow = 1) +
scale_y_log10(
limits = c(min(dmr_effect_df$Width[dmr_effect_df$Width > 0], na.rm = TRUE), y_upper),
labels = comma
) +
scale_color_manual(
values = c(
"Hyper" = "#B8405E",
"Hypo" = "#247BA0"
)
) +
coord_cartesian(clip = "off") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom", strip.text.x = element_text(size = 12)) +
labs(
title = "DMR Effect Size and Width by Gene Context",
x = "Context-specific delta beta (Tumor - Stroma)",
y = "DMR width (bp, log scale)",
color = ""
)
(#fig:cment_dmr_scatter_plot)DMR width versus Tumor-minus-Stroma delta beta in promoter and gene-body contexts, restricted to absolute delta beta of at least 0.05.
metadata_numeric_summary <- function(gr, fields) {
fields <- intersect(fields, colnames(mcols(gr)))
if (length(fields) == 0L || length(gr) == 0L) {
return(data.frame())
}
base::do.call(rbind, lapply(fields, function(field) {
x <- suppressWarnings(as.numeric(mcols(gr)[[field]]))
data.frame(
Field = field,
Min = min(x, na.rm = TRUE),
Median = stats::median(x, na.rm = TRUE),
Mean = mean(x, na.rm = TRUE),
Max = max(x, na.rm = TRUE)
)
}))
}
dmr_summary <- data.frame(
Set = names(methods_list),
DMRs = vapply(methods_list, length, integer(1)),
Median_width_bp = vapply(methods_list, function(gr) stats::median(width(gr)), numeric(1)),
Total_covered_bp = vapply(methods_list, function(gr) sum(width(reduce(gr))), numeric(1)),
stringsAsFactors = FALSE
)
knitr::kable(dmr_summary, row.names = FALSE, caption = "DMR set summary")
| Set | DMRs | Median_width_bp | Total_covered_bp |
|---|---|---|---|
| CMEnt | 2104 | 1095.5 | 3097452 |
| BH | 29 | 654.0 | 19371 |
cment_fields <- c("n_sites", "n_seeds", "pval", "qval", "score", "cv_accuracy", "mean_delta_beta", "median_delta_beta")
cment_meta_summary <- metadata_numeric_summary(dmrs_cment, cment_fields)
if (nrow(cment_meta_summary) > 0L) {
knitr::kable(cment_meta_summary, digits = 3, row.names = FALSE, caption = "CMEnt numeric metadata summary")
}
| Field | Min | Median | Mean | Max |
|---|---|---|---|---|
| pval | 0.000 | 0.000 | 0.000 | 0.000 |
| qval | 0.000 | 0.000 | 0.000 | 0.000 |
| score | 0.357 | 0.475 | 0.475 | 0.612 |
| cv_accuracy | 0.077 | 0.500 | 0.494 | 0.846 |
width_df <- base::do.call(rbind, lapply(names(methods_list), function(method) {
data.frame(Method = method, Width = width(methods_list[[method]]))
}))
ggplot(width_df, aes(x = Method, y = Width, fill = Method)) +
geom_boxplot(outlier.alpha = 0.35, width = 0.55) +
scale_y_log10(labels = scales::comma) +
scale_fill_manual(values = method_palette, drop = FALSE) +
theme_meta() +
labs(title = "DMR Width Distribution", x = "", y = "Width (bp, log10 scale)")
Figure 2: Width distributions of CMEnt and supplied Bumphunter DMRs in GSE244696
gene_context_df <- data.frame(
Context = c("Promoter", "Gene body", "Promoter and gene body", "No annotated gene overlap"),
DMRs = c(
sum(has_text(mcols(dmrs_cment)$in_promoter_of)),
sum(has_text(mcols(dmrs_cment)$in_gene_body_of)),
sum(has_text(mcols(dmrs_cment)$in_promoter_of) & has_text(mcols(dmrs_cment)$in_gene_body_of)),
sum(!has_text(mcols(dmrs_cment)$in_promoter_of) & !has_text(mcols(dmrs_cment)$in_gene_body_of))
)
) |>
mutate(Percentage = round(100 * DMRs / length(dmrs_cment), 1))
knitr::kable(gene_context_df, caption = "Gene context of CMEnt DMRs")
| Context | DMRs | Percentage |
|---|---|---|
| Promoter | 1455 | 69.2 |
| Gene body | 1044 | 49.6 |
| Promoter and gene body | 620 | 29.5 |
| No annotated gene overlap | 225 | 10.7 |
ggplot(gene_context_df, aes(x = reorder(Context, DMRs), y = DMRs, fill = Context)) +
geom_col(width = 0.7) +
coord_flip() +
scale_y_continuous(labels = scales::comma) +
theme_minimal() +
theme(legend.position = "none") +
labs(title = "DMR Gene Context", x = "", y = "DMRs")
(#fig:gene_context_plot)Gene context of CMEnt DMRs. Promoter and gene-body overlap categories can include the same DMR.
split_ids <- function(x) {
ids <- unlist(strsplit(stats::na.omit(as.character(x)), ","), use.names = FALSE)
unique(ids[nzchar(ids)])
}
if (requireNamespace("IlluminaHumanMethylationEPICanno.ilm10b4.hg19", quietly = TRUE)) {
library(IlluminaHumanMethylationEPICanno.ilm10b4.hg19)
anno <- as.data.frame(minfi::getAnnotation(IlluminaHumanMethylationEPICanno.ilm10b4.hg19))
anno$probe_id <- rownames(anno)
dmr_site_ids <- split_ids(mcols(dmrs_cment)$sites)
dmr_site_context <- anno[intersect(dmr_site_ids, rownames(anno)), c("probe_id", "Relation_to_Island")]
dmp_site_context <- anno[intersect(sig_dmps$site_id, rownames(anno)), c("probe_id", "Relation_to_Island")]
site_context_df <- bind_rows(
dmp_site_context |> mutate(Source = "Significant DMPs"),
dmr_site_context |> mutate(Source = "Sites inside DMRs")
) |>
dplyr::count(Source, Relation_to_Island, name = "Sites") |>
group_by(Source) |>
mutate(Percentage = round(100 * Sites / sum(Sites), 1)) |>
ungroup()
knitr::kable(site_context_df, caption = "CpG island context of significant DMPs and sites inside DMRs")
}
| Source | Relation_to_Island | Sites | Percentage |
|---|---|---|---|
| Significant DMPs | Island | 3101 | 13.7 |
| Significant DMPs | N_Shelf | 684 | 3.0 |
| Significant DMPs | N_Shore | 1934 | 8.5 |
| Significant DMPs | OpenSea | 14907 | 65.8 |
| Significant DMPs | S_Shelf | 558 | 2.5 |
| Significant DMPs | S_Shore | 1483 | 6.5 |
| Sites inside DMRs | Island | 5917 | 37.4 |
| Sites inside DMRs | N_Shelf | 358 | 2.3 |
| Sites inside DMRs | N_Shore | 2884 | 18.2 |
| Sites inside DMRs | OpenSea | 4355 | 27.5 |
| Sites inside DMRs | S_Shelf | 298 | 1.9 |
| Sites inside DMRs | S_Shore | 2014 | 12.7 |
if (exists("site_context_df")) {
context_levels <- site_context_df |>
group_by(Relation_to_Island) |>
summarise(Max_sites = max(Sites), .groups = "drop") |>
arrange(Max_sites) |>
pull(Relation_to_Island)
plot_context_df <- site_context_df |>
mutate(
Relation_to_Island = factor(Relation_to_Island, levels = context_levels),
Source = factor(Source, levels = c("Significant DMPs", "Sites inside DMRs"))
)
diff_df <- plot_context_df |>
dplyr::select(Source, Relation_to_Island, Sites) |>
tidyr::pivot_wider(names_from = Source, values_from = Sites, values_fill = 0) |>
dplyr::mutate(
Difference = `Sites inside DMRs` - `Significant DMPs`,
Label = paste0("Delta=", scales::comma(Difference)),
Label_x = pmax(`Significant DMPs`, `Sites inside DMRs`)
)
ggplot(plot_context_df, aes(x = Sites, y = Relation_to_Island, fill = Source)) +
geom_col(data = dplyr::filter(plot_context_df, Source == "Significant DMPs"), width = 0.78, alpha = 0.45) +
geom_col(data = dplyr::filter(plot_context_df, Source == "Sites inside DMRs"), width = 0.48, alpha = 0.9) +
geom_text(data = diff_df, aes(x = Label_x, y = Relation_to_Island, label = Label), inherit.aes = FALSE, hjust = -0.05, size = 3) +
scale_x_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.2))) +
scale_fill_manual(values = c("Significant DMPs" = "grey65", "Sites inside DMRs" = "#0072B2")) +
theme_minimal() +
theme(legend.position = "bottom") +
labs(title = "CpG Island Context", x = "Sites", y = "", fill = "")
}
(#fig:cpg_island_context_plot)CpG island context of significant DMPs and sites inside CMEnt DMRs.
overlap_hits <- findOverlaps(dmrs_cment, published_bumphunter, ignore.strand = TRUE)
cment_overlapping <- unique(queryHits(overlap_hits))
published_overlapping <- unique(subjectHits(overlap_hits))
intersection_bp <- sum(width(intersect(reduce(dmrs_cment), reduce(published_bumphunter), ignore.strand = TRUE)))
union_bp <- sum(width(union(reduce(dmrs_cment), reduce(published_bumphunter), ignore.strand = TRUE)))
overlap_summary <- data.frame(
Metric = c(
"CMEnt DMRs overlapping supplied Bumphunter",
"Supplied Bumphunter DMRs overlapping CMEnt",
"Fraction of CMEnt DMRs overlapping supplied Bumphunter",
"Fraction of supplied Bumphunter DMRs overlapping CMEnt",
"Base-pair Jaccard index"
),
Value = c(
length(cment_overlapping),
length(published_overlapping),
length(cment_overlapping) / max(1, length(dmrs_cment)),
length(published_overlapping) / max(1, length(published_bumphunter)),
intersection_bp / max(1, union_bp)
)
)
knitr::kable(overlap_summary, digits = 3, row.names = FALSE, caption = "CMEnt vs supplied Bumphunter overlap")
| Metric | Value |
|---|---|
| CMEnt DMRs overlapping supplied Bumphunter | 29.000 |
| Supplied Bumphunter DMRs overlapping CMEnt | 29.000 |
| Fraction of CMEnt DMRs overlapping supplied Bumphunter | 0.014 |
| Fraction of supplied Bumphunter DMRs overlapping CMEnt | 1.000 |
| Base-pair Jaccard index | 0.006 |
capture_by_set <- data.frame(
Set = names(methods_list),
Captured_DMPs = vapply(methods_list, function(gr) sum(overlapsAny(dmp_gr, gr, ignore.strand = TRUE)), integer(1)),
Total_DMPs = length(dmp_gr),
stringsAsFactors = FALSE
)
capture_by_set$Captured_Pct <- 100 * capture_by_set$Captured_DMPs / capture_by_set$Total_DMPs
knitr::kable(capture_by_set, digits = 1, row.names = FALSE, caption = "Published DMPs captured by each DMR set")
| Set | Captured_DMPs | Total_DMPs | Captured_Pct |
|---|---|---|---|
| CMEnt | 6739 | 22667 | 29.7 |
| BH | 238 | 22667 | 1.0 |
ggplot(capture_by_set, aes(x = Set, y = Captured_Pct, fill = Set)) +
geom_col(width = 0.6) +
geom_text(aes(label = paste0(scales::comma(Captured_DMPs), " probes")), vjust = -0.4, size = 3.3) +
scale_fill_manual(values = method_palette, drop = FALSE) +
scale_y_continuous(labels = scales::percent_format(scale = 1), limits = c(0, max(capture_by_set$Captured_Pct) * 1.18)) +
theme_meta() +
labs(title = "Capture of Published DMPs", x = "", y = "Published DMPs captured (%)")
Figure 3: Percentage of published GSE244696 DMPs captured by each DMR set
best_overlap_table <- data.frame()
if (length(overlap_hits) > 0L) {
qi <- queryHits(overlap_hits)
si <- subjectHits(overlap_hits)
inter_width <- pmax(
0,
pmin(end(dmrs_cment)[qi], end(published_bumphunter)[si]) -
pmax(start(dmrs_cment)[qi], start(published_bumphunter)[si]) + 1
)
pair_jaccard <- inter_width / (width(dmrs_cment)[qi] + width(published_bumphunter)[si] - inter_width)
best_overlap_table <- data.frame(
CMEnt_region = paste0(as.character(seqnames(dmrs_cment))[qi], ":", start(dmrs_cment)[qi], "-", end(dmrs_cment)[qi]),
Bumphunter_region = paste0(as.character(seqnames(published_bumphunter))[si], ":", start(published_bumphunter)[si], "-", end(published_bumphunter)[si]),
Bumphunter_id = names(published_bumphunter)[si],
Overlap_bp = inter_width,
Pair_Jaccard = pair_jaccard,
Bumphunter_fwer = suppressWarnings(as.numeric(mcols(published_bumphunter)$fwer[si])),
Bumphunter_gene = as.character(mcols(published_bumphunter)$genename[si]),
stringsAsFactors = FALSE
)
best_overlap_table <- best_overlap_table |>
arrange(CMEnt_region, desc(Pair_Jaccard)) |>
group_by(CMEnt_region) |>
slice_head(n = 1) |>
ungroup() |>
arrange(desc(Pair_Jaccard))
knitr::kable(
head(best_overlap_table, 15),
digits = 3,
row.names = FALSE,
caption = "Best supplied Bumphunter match for each overlapping CMEnt DMR"
)
} else {
cat("No direct genomic overlap was found between CMEnt and the supplied Bumphunter DMRs.\n")
}
| CMEnt_region | Bumphunter_region | Bumphunter_id | Overlap_bp | Pair_Jaccard | Bumphunter_fwer | Bumphunter_gene |
|---|---|---|---|---|---|---|
| chr6:30139478-30140325 | chr6:30139478-30140231 | DMR_19 | 754 | 0.889 | 0 | TRIM15 |
| chr8:17270347-17271215 | chr8:17270604-17271215 | DMR_22 | 612 | 0.704 | 0 | MTMR7 |
| chr4:4859935-4860655 | chr4:4859772-4860547 | DMR_6 | 613 | 0.693 | 0 | MSX1 |
| chr19:57183016-57183342 | chr19:57182816-57183342 | DMR_14 | 327 | 0.620 | 0 | ZNF835 |
| chr19:58545001-58546307 | chr19:58545122-58545837 | DMR_13 | 716 | 0.548 | 0 | ZSCAN1 |
| chr16:809476-811347 | chr16:810365-811347 | DMR_23 | 983 | 0.525 | 0 | MSLN |
| chr5:127873228-127874587 | chr5:127873283-127873980 | DMR_3 | 698 | 0.513 | 0 | FBN2 |
| chr5:140810051-140811642 | chr5:140810106-140810920 | DMR_5 | 815 | 0.512 | 0 | PCDHGA8 |
| chr7:27280585-27282112 | chr7:27280914-27281687 | DMR_20 | 774 | 0.507 | 0 | EVX1 |
| chr7:70596308-70598282 | chr7:70597058-70597921 | DMR_12 | 864 | 0.437 | 0 | GALNT17 |
| chr16:51182904-51186266 | chr16:51184355-51185772 | DMR_1 | 1418 | 0.422 | 0 | SALL1 |
| chr19:35605533-35607221 | chr19:35606534-35607209 | DMR_25 | 676 | 0.400 | 0 | FXYD3 |
| chr22:46480891-46482023 | chr22:46481603-46482023 | DMR_21 | 421 | 0.372 | 0 | MIRLET7BHG |
| chr6:28602513-28603779 | chr6:28602705-28603173 | DMR_17 | 469 | 0.370 | 0 | SCAND3 |
| chr6:30130109-30132715 | chr6:30131283-30132133 | DMR_4 | 851 | 0.326 | 0 | TRIM15 |
dmr_rank_table <- as.data.frame(dmrs_cment)
CMEnt::plotDMRs(
dmrs_cment,
top_n = 1,
score_by = "score",
plot_motif = FALSE,
beta = beta_handler,
pheno = pheno,
sample_group_col = "tissue.ch1",
array = "EPIC",
genome = "hg19"
)
Figure 4: Structure, gene context, and sample methylation of the top CMEnt DMR ranked by score in GSE244696
cat("CMEnt DMR Manhattan plot:\n")
CMEnt DMR Manhattan plot:
CMEnt::plotDMRsManhattan(
dmrs_cment,
genome = "hg19"
)
Figure 5: Genome-wide CMEnt DMR scores in GSE244696, colored by primary gene annotation class
cat("CMEnt DMR Circos interactions plot:\n")
CMEnt DMR Circos interactions plot:
CMEnt::plotDMRsCircos(
dmrs_cment,
genome = "hg19",
array = "EPIC"
)
Figure 6: Circos summary of CMEnt DMR locations, methylation direction, and detected interactions in GSE244696
annotatr is used for CpG and gene-region context,
while missMethyl is applied to promoter-overlapping DMRs for array-aware GO/KEGG enrichment because it accounts
for probe-per-gene bias in methylation arrays, and it was also included in the original publication.
top_10_cment_dmrs <- as.data.frame(dmrs_cment) |>
arrange(by = -score) |>
head(10) |>
dplyr::select(id, score, cv_accuracy, in_promoter_of, in_gene_body_of)
knitr::kable(
top_10_cment_dmrs,
digits = 3,
row.names = FALSE,
caption = "Top 10 CMEnt DMRs with gene annotations"
)
| id | score | cv_accuracy | in_promoter_of | in_gene_body_of |
|---|---|---|---|---|
| chr5:cg06715097-cg27197785 | 0.612 | 0.731 | CAMK2A | NA |
| chr15:cg11681321-cg21398489 | 0.597 | 0.846 | ANXA2 | ANXA2 |
| chr22:cg04329382-cg07546360 | 0.590 | 0.808 | MIRLET7BHG | NA |
| chr13:cg11970797-cg04595004 | 0.584 | 0.692 | NA | CRYL1 |
| chr8:cg16163338-cg09920804 | 0.579 | 0.769 | PLAT | NA |
| chr12:cg04796818-cg06992831 | 0.578 | 0.769 | NA | LOC283387 |
| chr8:cg04311582-cg07188077 | 0.576 | 0.769 | NA | PEBP4 |
| chr3:cg11268077-cg07552538 | 0.575 | 0.731 | NA | CMSS1,LOC105374010,FILIP1L |
| chr22:cg22887880-cg23349242 | 0.573 | 0.769 | SUSD2 | NA |
| chr1:cg17079374-cg20800117 | 0.571 | 0.808 | NA | CAMTA1 |
normalize_missmethyl_result <- function(x, method, collection) {
if (is.null(x) || nrow(x) == 0L) {
return(NULL)
}
x <- as.data.frame(x)
id_col <- intersect(c("ID", "PathwayID", "Pathway.ID", "KEGGID"), colnames(x))[1]
term_col <- intersect(c("Term", "TERM", "Description", "Pathway"), colnames(x))[1]
fdr_col <- intersect(c("FDR", "adj.P.Val", "P.DE", "P.Value"), colnames(x))[1]
p_col <- intersect(c("P.DE", "P.Value", "P.DE.weighted"), colnames(x))[1]
de_col <- intersect(c("DE", "N_DE", "Count"), colnames(x))[1]
n_col <- intersect(c("N", "Total", "Size"), colnames(x))[1]
ont_col <- intersect(c("Ont", "Ontology", "Category"), colnames(x))[1]
if (is.na(term_col) || is.na(fdr_col)) {
return(NULL)
}
pathway_id <- if (!is.na(id_col)) as.character(x[[id_col]]) else rownames(x)
pathway_id <- ifelse(collection == "KEGG" & grepl("^(path:)?hsa[0-9]{5}$|^[0-9]{5}$", pathway_id), pathway_id, NA_character_)
data.frame(
Method = method,
Collection = collection,
PathwayID = pathway_id,
Term = as.character(x[[term_col]]),
Ontology = if (!is.na(ont_col)) as.character(x[[ont_col]]) else collection,
N = if (!is.na(n_col)) suppressWarnings(as.numeric(x[[n_col]])) else NA_real_,
DE = if (!is.na(de_col)) suppressWarnings(as.numeric(x[[de_col]])) else NA_real_,
PValue = if (!is.na(p_col)) suppressWarnings(as.numeric(x[[p_col]])) else NA_real_,
FDR = suppressWarnings(as.numeric(x[[fdr_col]])),
stringsAsFactors = FALSE
)
}
library(IlluminaHumanMethylationEPICanno.ilm10b4.hg19)
missmethyl_long <- NULL
missmethyl_array <- if (toupper(array_type) == "EPICV2") "EPIC_V2" else toupper(array_type)
missmethyl_regions <- lapply(names(methods_list), function(method) {
gr <- methods_list[[method]]
if (!"in_promoter_of" %in% colnames(mcols(gr))) {
return(gr[integer()])
}
gr[has_text(mcols(gr)$in_promoter_of)]
})
names(missmethyl_regions) <- names(methods_list)
missmethyl_region_summary <- data.frame(
Method = names(methods_list),
Promoter_overlapping_DMRs = vapply(missmethyl_regions, length, integer(1)),
Total_DMRs = vapply(methods_list, length, integer(1)),
stringsAsFactors = FALSE
)
missmethyl_region_summary$Percentage <- 100 * missmethyl_region_summary$Promoter_overlapping_DMRs / pmax(1L, missmethyl_region_summary$Total_DMRs)
knitr::kable(
missmethyl_region_summary,
digits = 1,
row.names = FALSE,
caption = "CMEnt-annotated promoter-overlapping DMRs supplied to missMethyl"
)
| Method | Promoter_overlapping_DMRs | Total_DMRs | Percentage |
|---|---|---|---|
| CMEnt | 1455 | 2104 | 69.2 |
| BH | 25 | 29 | 86.2 |
cached <- getBenchmarkFile("missmethyl_cment_gene_annotation_promoter_enrichment_results.rds")
if (file.exists(cached)) {
missmethyl_long <- readRDS(cached)
if (!all(c("PathwayID", "RegionScope") %in% colnames(missmethyl_long))) {
unlink(cached)
missmethyl_long <- NULL
}
}
if (is.null(missmethyl_long)) {
missmethyl_results <- Filter(Negate(is.null), unlist(lapply(names(missmethyl_regions), function(method) {
regions <- missmethyl_regions[[method]]
if (length(regions) == 0L) {
cat(sprintf("Skipping missMethyl enrichment for %s: no promoter-overlapping DMRs.\n", method))
return(NULL)
}
lapply(c("GO", "KEGG"), function(collection) {
cat(sprintf("Running missMethyl %s enrichment for %s CMEnt-annotated promoter-overlapping DMRs (n = %d)...\n", collection, method, length(regions)))
ret <- tryCatch(
missMethyl::goregion(
regions = regions,
all.cpg = beta_row_names,
collection = collection,
array.type = missmethyl_array,
plot.bias = FALSE,
prior.prob = TRUE
),
error = function(e) {
warning(sprintf("missMethyl %s enrichment failed for %s: %s", collection, method, e$message))
NULL
}
)
ret <- normalize_missmethyl_result(ret, method, collection)
if (!is.null(ret)) {
ret$RegionScope <- "CMEnt-annotated promoter-overlapping DMRs"
}
ret
})
}), recursive = FALSE))
missmethyl_long <- if (length(missmethyl_results) > 0L) {
base::do.call(rbind, missmethyl_results)
} else {
NULL
}
saveRDS(missmethyl_long, cached)
}
if (!is.null(missmethyl_long) && nrow(missmethyl_long) > 0L) {
missmethyl_long <- missmethyl_long[is.finite(missmethyl_long$FDR), , drop = FALSE]
missmethyl_long$NegLog10FDR <- -log10(pmax(missmethyl_long$FDR, .Machine$double.xmin))
missmethyl_top <- missmethyl_long |>
group_by(Method, Collection) |>
arrange(FDR, .by_group = TRUE) |>
slice_head(n = 10) |>
ungroup()
missmethyl_top$Term_Label <- short_label(missmethyl_top$Term, 70)
missmethyl_top$Facet <- paste(missmethyl_top$Method, missmethyl_top$Collection, sep = " - ")
shared_missmethyl_terms <- missmethyl_top |>
distinct(Collection, Term, Method) |>
dplyr::count(Collection, Term, name = "Methods") |>
filter(Methods > 1L) |>
transmute(Term_Key = paste(Collection, Term, sep = "___")) |>
pull(Term_Key)
missmethyl_top$Shared_Term <- paste(missmethyl_top$Collection, missmethyl_top$Term, sep = "___") %in% shared_missmethyl_terms
missmethyl_top$Term_Facet <- factor(
paste(missmethyl_top$Term_Label, missmethyl_top$Facet, sep = "___"),
levels = rev(unique(paste(missmethyl_top$Term_Label, missmethyl_top$Facet, sep = "___")))
)
missmethyl_axis_term_labels <- gsub("\\\\", "\\\\\\\\", missmethyl_top$Term_Label)
missmethyl_axis_term_labels <- gsub('"', '\\"', missmethyl_axis_term_labels, fixed = TRUE)
missmethyl_axis_term_labels <- paste0('"', missmethyl_axis_term_labels, '"')
missmethyl_axis_labels <- setNames(
ifelse(missmethyl_top$Shared_Term, paste0("bold(", missmethyl_axis_term_labels, ")"), missmethyl_axis_term_labels),
as.character(missmethyl_top$Term_Facet)
)
knitr::kable(
missmethyl_top |>
dplyr::select(Method, Collection, PathwayID, Ontology, Term, N, DE, PValue, FDR) |>
arrange(Collection, Method, FDR),
digits = 3,
row.names = FALSE,
caption = "Top missMethyl GO/KEGG enrichments from promoter-overlapping DMRs"
)
}
| Method | Collection | PathwayID | Ontology | Term | N | DE | PValue | FDR |
|---|---|---|---|---|---|---|---|---|
| BH | GO | NA | GO | homophilic cell-cell adhesion | 170 | 17 | 0.000 | 0.000 |
| BH | GO | NA | GO | calcium ion binding | 677 | 18 | 0.000 | 0.000 |
| BH | GO | NA | GO | cell adhesion molecule binding | 624 | 17 | 0.000 | 0.000 |
| BH | GO | NA | GO | cell-cell adhesion | 889 | 17 | 0.000 | 0.000 |
| BH | GO | NA | GO | cell adhesion | 1393 | 19 | 0.000 | 0.000 |
| BH | GO | NA | GO | metal ion binding | 4170 | 27 | 0.000 | 0.000 |
| BH | GO | NA | GO | cation binding | 4261 | 27 | 0.000 | 0.000 |
| BH | GO | NA | GO | nervous system development | 2087 | 18 | 0.000 | 0.001 |
| BH | GO | NA | GO | multicellular organism development | 3798 | 23 | 0.000 | 0.005 |
| BH | GO | NA | GO | system development | 3278 | 21 | 0.000 | 0.007 |
| CMEnt | GO | NA | GO | DNA-binding transcription factor activity, RNA polymerase II-specific | 1254 | 182 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | DNA-binding transcription factor activity | 1351 | 191 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | developmental process | 5516 | 596 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | anatomical structure development | 4968 | 551 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | RNA polymerase II transcription regulatory region sequence-specific DNA binding | 1265 | 178 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | homophilic cell-cell adhesion | 170 | 49 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | system development | 3278 | 395 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | DNA-binding transcription activator activity | 437 | 87 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | RNA polymerase II cis-regulatory region sequence-specific DNA binding | 1072 | 153 | 0.000 | 0.000 |
| CMEnt | GO | NA | GO | cis-regulatory region sequence-specific DNA binding | 1095 | 156 | 0.000 | 0.000 |
| BH | KEGG | hsa04519 | KEGG | Cadherin signaling | 394 | 17 | 0.000 | 0.000 |
| BH | KEGG | hsa00010 | KEGG | Glycolysis / Gluconeogenesis | 64 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00020 | KEGG | Citrate cycle (TCA cycle) | 28 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00030 | KEGG | Pentose phosphate pathway | 26 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00040 | KEGG | Pentose and glucuronate interconversions | 36 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00051 | KEGG | Fructose and mannose metabolism | 35 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00052 | KEGG | Galactose metabolism | 30 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00053 | KEGG | Ascorbate and aldarate metabolism | 29 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00061 | KEGG | Fatty acid biosynthesis | 16 | 0 | 1.000 | 1.000 |
| BH | KEGG | hsa00062 | KEGG | Fatty acid elongation | 28 | 0 | 1.000 | 1.000 |
| CMEnt | KEGG | hsa04519 | KEGG | Cadherin signaling | 394 | 67 | 0.000 | 0.000 |
| CMEnt | KEGG | hsa00053 | KEGG | Ascorbate and aldarate metabolism | 29 | 6 | 0.002 | 0.294 |
| CMEnt | KEGG | hsa04382 | KEGG | Cornified envelope formation | 206 | 25 | 0.002 | 0.294 |
| CMEnt | KEGG | hsa00920 | KEGG | Sulfur metabolism | 10 | 4 | 0.003 | 0.303 |
| CMEnt | KEGG | hsa00140 | KEGG | Steroid hormone biosynthesis | 63 | 9 | 0.008 | 0.501 |
| CMEnt | KEGG | hsa00980 | KEGG | Metabolism of xenobiotics by cytochrome P450 | 71 | 9 | 0.008 | 0.501 |
| CMEnt | KEGG | hsa04950 | KEGG | Maturity onset diabetes of the young | 26 | 6 | 0.011 | 0.591 |
| CMEnt | KEGG | hsa00565 | KEGG | Ether lipid metabolism | 51 | 8 | 0.028 | 0.758 |
| CMEnt | KEGG | hsa00591 | KEGG | Linoleic acid metabolism | 30 | 5 | 0.033 | 0.758 |
| CMEnt | KEGG | hsa00592 | KEGG | alpha-Linolenic acid metabolism | 26 | 5 | 0.031 | 0.758 |
if (!is.null(missmethyl_long) && nrow(missmethyl_long) > 0L) {
ggplot(missmethyl_top, aes(x = NegLog10FDR, y = Term_Facet, size = DE)) +
geom_point(alpha = 0.9) +
facet_wrap(~Facet, scales = "free_y", ncol = 2) +
scale_y_discrete(labels = function(x) parse(text = unname(missmethyl_axis_labels[x]))) +
theme_minimal(base_size = 17) +
labs(
x = expression(-log[10]("FDR")), y = "", color = "", size = "DM genes"
)
} else {
cat("missMethyl returned no enrichment results.\n")
}
Figure 7: Top missMethyl GO and KEGG enrichments from promoter-overlapping DMRs by method
Bold term labels indicate terms shared across methods.
if (!is.null(missmethyl_long) && nrow(missmethyl_long) > 0L &&
requireNamespace("pathview", quietly = TRUE) &&
requireNamespace("KEGGREST", quietly = TRUE)) {
library(pathview)
kegg_candidates <- missmethyl_long |>
filter(Method == "CMEnt", Collection == "KEGG", !is.na(PathwayID), is.finite(PValue)) |>
arrange(FDR, PValue) |>
mutate(
PathwayID = sub("^path:", "", PathwayID),
PathwayID = ifelse(grepl("^hsa", PathwayID), PathwayID, paste0("hsa", PathwayID))
)
promoter_gene_delta <- as.data.frame(dmrs_cment) |>
arrange(desc(score)) |>
filter(is.finite(as.numeric(delta_beta_promoter)), !is.na(in_promoter_of), nzchar(in_promoter_of)) |>
mutate(delta_beta_promoter = as.numeric(delta_beta_promoter)) |>
dplyr::select(id, score, delta_beta_promoter, in_promoter_of) |>
tidyr::separate_rows(in_promoter_of, sep = "\\s*,\\s*") |>
mutate(Gene = trimws(in_promoter_of)) |>
filter(nzchar(Gene)) |>
arrange(desc(abs(delta_beta_promoter))) |>
distinct(Gene, .keep_all = TRUE)
if (nrow(kegg_candidates) > 0L && nrow(promoter_gene_delta) > 0L) {
gene_map <- suppressMessages(AnnotationDbi::select(
org.Hs.eg.db::org.Hs.eg.db,
keys = promoter_gene_delta$Gene,
keytype = "SYMBOL",
columns = c("SYMBOL", "ENTREZID")
)) |>
filter(!is.na(ENTREZID)) |>
distinct(SYMBOL, ENTREZID)
promoter_gene_delta <- promoter_gene_delta |>
inner_join(gene_map, by = c("Gene" = "SYMBOL")) |>
arrange(desc(abs(delta_beta_promoter))) |>
distinct(ENTREZID, .keep_all = TRUE)
if (nrow(promoter_gene_delta) == 0L) {
cat("No promoter genes could be mapped to Entrez IDs for pathview.\n")
} else {
kegg_gene_map_file <- getBenchmarkFile("kegg_hsa_pathway_gene_map.rds")
kegg_gene_map <- if (file.exists(kegg_gene_map_file)) {
readRDS(kegg_gene_map_file)
} else {
links <- tryCatch(
KEGGREST::keggLink("hsa", "pathway"),
error = function(e) {
warning(sprintf("Could not retrieve KEGG pathway-gene links: %s", e$message))
NULL
}
)
if (is.null(links)) {
data.frame(PathwayID = character(), ENTREZID = character())
} else {
data.frame(
PathwayID = sub("^path:", "", names(links)),
ENTREZID = sub("^hsa:", "", unname(links)),
stringsAsFactors = FALSE
) |>
distinct()
}
}
if (!file.exists(kegg_gene_map_file)) {
saveRDS(kegg_gene_map, kegg_gene_map_file)
}
candidate_overlaps <- kegg_candidates |>
left_join(kegg_gene_map, by = "PathwayID") |>
filter(ENTREZID %in% promoter_gene_delta$ENTREZID) |>
group_by(PathwayID) |>
summarise(OverlapEntrez = list(unique(ENTREZID)), .groups = "drop")
matched_pathway <- match(kegg_candidates$PathwayID, candidate_overlaps$PathwayID)
matched_pathway <- which(!is.na(matched_pathway))[1]
if (is.na(matched_pathway)) {
cat("No missMethyl KEGG pathway contained the top promoter-overlapping DMR genes for pathview coloring.\n")
} else {
top_kegg <- kegg_candidates |> dplyr::slice(matched_pathway)
matched_genes <- candidate_overlaps$OverlapEntrez[[match(top_kegg$PathwayID[1], candidate_overlaps$PathwayID)]]
promoter_pathway_delta <- promoter_gene_delta |>
filter(ENTREZID %in% matched_genes)
gene_delta <- promoter_pathway_delta$delta_beta_promoter
names(gene_delta) <- promoter_pathway_delta$ENTREZID
kegg_id <- sub("^hsa", "", top_kegg$PathwayID[1])
pathview_dir <- getBenchmarkFile("pathview")
dir.create(pathview_dir, showWarnings = FALSE, recursive = TRUE)
pathview_suffix <- "promoter_delta_beta"
pathview_png <- file.path(pathview_dir, paste0("hsa", kegg_id, ".", pathview_suffix, ".png"))
gene_limit <- max(0.001, max(abs(gene_delta), na.rm = TRUE))
unlink(c(pathview_png, basename(pathview_png)))
pathview_result <- tryCatch(
pathview::pathview(
gene.data = gene_delta,
gene.idtype = "entrez",
pathway.id = kegg_id,
species = "hsa",
out.suffix = pathview_suffix,
kegg.dir = pathview_dir,
limit = list(gene = gene_limit, cpd = 1),
low = list(gene = "#2F80ED", cpd = "#2F80ED"),
mid = list(gene = "#FFFFFF", cpd = "#FFFFFF"),
high = list(gene = "#FF3B30", cpd = "#FF3B30")
),
error = function(e) {
warning(sprintf("pathview failed for KEGG pathway %s: %s", kegg_id, e$message))
NULL
}
)
if (file.exists(basename(pathview_png))) {
file.rename(basename(pathview_png), pathview_png)
}
pathview_mapped <- if (!is.null(pathview_result$plot.data.gene)) {
sum(!is.na(pathview_result$plot.data.gene[["mol.data"]]))
} else {
NA_integer_
}
knitr::kable(
promoter_pathway_delta |>
dplyr::select(Gene, ENTREZID, delta_beta_promoter, score, id) |>
arrange(desc(abs(delta_beta_promoter))) |>
head(20),
digits = 3,
row.names = FALSE,
caption = paste0(
"Promoter delta beta values supplied to pathview for the top missMethyl KEGG pathway with promoter-gene overlap: ",
top_kegg$Term[1],
". Genes overlapping this KEGG pathway: ", nrow(promoter_pathway_delta),
". Genes mapped by pathview: ", pathview_mapped,
". Red indicates higher methylation in Tumor; blue indicates lower methylation in Tumor."
)
)
if (file.exists(pathview_png)) {
knitr::include_graphics(pathview_png)
} else {
cat("pathview did not produce a KEGG pathway image.\n")
}
}
}
} else {
cat("No CMEnt KEGG pathway with ID or promoter gene delta-beta values were available for pathview.\n")
}
} else {
cat("pathview and KEGGREST are required for the KEGG pathway diagram.\n")
}
Figure 8: Top missMethyl KEGG pathway with promoter-gene overlap, colored by CMEnt promoter delta beta in GSE244696
In the GSE244696 PDAC tumor-stroma comparison, CMEnt was applied in meta-analysis mode by using the published DMPs as external evidence and the available beta matrix to assemble spatially coherent DMRs around those signals. The resulting DMRs recovered biological themes that are consistent with the original study and with PDAC tumor microenvironment biology. Study-context terms highlighted extracellular matrix, myeloid leukocyte differentiation, steroid-hormone response and hormone-regulatory processes. These signals are plausible because PDAC is characterized by a dense desmoplastic extracellular matrix and because the original study reported compartment-specific immune and estrogen receptor-related epigenetic dysregulation, with tumor-associated macrophages enriched in tumor-cell regions and associated with patient outcome. The annotation profile further supports a regulatory interpretation: CMEnt DMRs overlap promoters, exons, introns and 5′ UTRs, indicating that the method organizes CpG-level evidence into regions connected to gene-regulatory architecture rather than producing isolated CpG hits. The genome-wide circos view shows both hypermethylated and hypomethylated DMRs, consistent with a compartment-specific remodeling process. A motif-supported DMR interaction involving AHR::ARNT provides an additional hypothesis-generating link to xenobiotic and cytochrome P450-related metabolism, although this motif result should not be overinterpreted given the small number of motif-supported interactions. Overall, these results suggest that CMEnt preserves the biological context of the published DMP evidence while emphasizing coherent regulatory, extracellular-matrix, myeloid and hormone-associated tumor-stroma programs.
sessionInfo()
R version 4.6.1 (2026-06-24) Platform: x86_64-pc-linux-gnu Running under: Ubuntu 24.04.4 LTS
Matrix products: default BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 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: Europe/Brussels tzcode source: system (glibc)
attached base packages:
[1] parallel stats4 stats graphics grDevices datasets utils
[8] methods base
other attached packages:
[1] pathview_1.52.0
[2] IlluminaHumanMethylationEPICanno.ilm10b4.hg19_0.6.0
[3] scales_1.4.0
[4] minfi_1.58.0
[5] bumphunter_1.54.0
[6] locfit_1.5-9.12
[7] iterators_1.0.14
[8] foreach_1.5.2
[9] Biostrings_2.80.2
[10] XVector_0.52.0
[11] CMEnt_0.99.14
[12] DSS_2.60.0
[13] BiocParallel_1.46.0
[14] bsseq_1.48.0
[15] SummarizedExperiment_1.42.0
[16] Biobase_2.72.0
[17] MatrixGenerics_1.24.0
[18] matrixStats_1.5.0
[19] ggplot2_4.0.3
[20] GenomicRanges_1.64.0
[21] Seqinfo_1.2.0
[22] IRanges_2.46.0
[23] S4Vectors_0.50.3
[24] BiocGenerics_0.58.1
[25] generics_0.1.4
[26] dplyr_1.2.1
[27] data.table_1.18.6.1
[28] testthat_3.3.2
[29] BiocStyle_2.40.0
loaded via a namespace (and not attached):
[1] graph_1.90.0
[2] igraph_2.3.4
[3] plotly_4.12.1
[4] Formula_1.2-6
[5] devtools_2.5.2
[6] tidyselect_1.2.1
[7] bit_4.6.0
[8] doParallel_1.0.17
[9] clue_0.3-68
[10] lattice_0.23-1
[11] rjson_0.2.23
[12] nor1mix_1.3-3
[13] blob_1.3.0
[14] stringr_1.6.0
[15] rngtools_1.5.2
[16] S4Arrays_1.12.1
[17] base64_2.0.2
[18] dichromat_2.0-1
[19] scrime_1.3.7
[20] seqLogo_1.78.0
[21] png_0.1-9
[22] tinytex_0.61
[23] cli_3.6.6
[24] ggplotify_0.1.4
[25] bedr_1.1.5
[26] ProtGenerics_1.44.0
[27] askpass_1.2.1
[28] outliers_0.15
[29] multtest_2.68.0
[30] openssl_2.4.2
[31] pkgdown_2.2.1
[32] textshaping_1.0.5
[33] BiocIO_1.22.0
[34] purrr_1.2.2
[35] curl_8.1.0
[36] mime_0.13
[37] evaluate_1.0.5
[38] tidytree_0.4.8
[39] ComplexHeatmap_2.28.0
[40] stringi_1.8.9
[41] backports_1.5.1
[42] desc_1.4.3
[43] XML_3.99-0.25
[44] httpuv_1.6.17
[45] AnnotationDbi_1.74.0
[46] magrittr_2.0.5
[47] clusterProfiler_4.20.2
[48] rappdirs_0.3.4
[49] splines_4.6.1
[50] mclust_6.1.3
[51] DelayedDataFrame_1.28.0
[52] jpeg_0.1-11
[53] doRNG_1.8.6.3
[54] ggraph_2.2.2
[55] rentrez_1.2.4
[56] org.Hs.eg.db_3.23.1
[57] DT_0.34.0
[58] sessioninfo_1.2.4
[59] DBI_1.3.0
[60] HDF5Array_1.40.0
[61] reactome.db_1.96.0
[62] jquerylib_0.1.4
[63] genefilter_1.94.0
[64] withr_3.0.3
[65] class_7.3-24
[66] systemfonts_1.3.2
[67] rprojroot_2.1.1
[68] enrichplot_1.32.1
[69] ggnewscale_0.5.2
[70] benchmarkme_1.0.8
[71] brio_1.1.5
[72] tidygraph_1.3.1
[73] formatR_1.14
[74] rtracklayer_1.72.0
[75] BiocManager_1.30.27
[76] htmlwidgets_1.6.4
[77] fs_2.1.0
[78] biomaRt_2.68.0
[79] missMethyl_1.46.0
[80] ggrepel_0.9.8
[81] labeling_0.4.3
[82] SparseArray_1.12.3
[83] cellranger_1.1.0
[84] shinycssloaders_1.1.0
[85] h5mread_1.4.1
[86] annotate_1.90.0
[87] VariantAnnotation_1.58.0
[88] knitr_1.52
[89] TFBSTools_1.50.0
[90] UCSC.utils_1.8.0
[91] beanplot_1.3.1
[92] TFMPvalue_1.0.0
[93] annotatr_1.38.3
[94] patchwork_1.3.2
[95] caTools_1.18.4
[96] grid_4.6.1
[97] ggtree_4.2.0
[98] rhdf5_2.56.1
[99] pwalign_1.8.0
[100] R.oo_1.27.1
[101] ggiraph_0.9.6
[102] regioneR_1.44.0
[103] gridGraphics_0.5-1
[104] ellipsis_0.3.3
[105] lazyeval_0.2.3
[106] yaml_2.3.12
[107] survival_3.8-12
[108] BiocVersion_3.23.1
[109] crayon_1.5.3
[110] Rgraphviz_2.56.0
[111] tweenr_2.0.3
[112] RColorBrewer_1.1-3
[113] tidyr_1.3.2
[114] later_1.4.8
[115] codetools_0.2-20
[116] base64enc_0.1-6
[117] GlobalOptions_0.1.4
[118] KEGGREST_1.52.2
[119] shape_1.4.6.1
[120] ReactomePA_1.56.0
[121] limma_3.68.5
[122] gdtools_0.5.2
[123] Rsamtools_2.28.0
[124] filelock_1.0.3
[125] showtext_0.9-8
[126] foreign_0.8-91
[127] pkgconfig_2.0.3
[128] KEGGgraph_1.72.0
[129] IlluminaHumanMethylation450kanno.ilmn12.hg19_0.6.1
[130] xml2_1.6.0
[131] GenomicAlignments_1.48.0
[132] aplot_0.3.2
[133] hummingbird_1.22.0
[134] BSgenome_1.80.0
[135] ape_5.8-1
[136] viridisLite_0.4.3
[137] biovizBase_1.60.0
[138] TxDb.Hsapiens.UCSC.hg19.knownGene_3.22.1
[139] gridBase_0.4-7
[140] xtable_1.8-8
[141] interp_1.1-6
[142] plyr_1.8.9
[143] httr_1.4.9
[144] tools_4.6.1
[145] pkgbuild_1.4.8
[146] htmlTable_2.5.0
[147] checkmate_2.3.4
[148] nlme_3.1-170
[149] futile.logger_1.4.9
[150] lambda.r_1.2.6
[151] dbplyr_2.6.0
[152] ExperimentHub_3.2.2
[153] digest_0.6.39
[154] permute_0.9-10
[155] bookdown_0.48
[156] Matrix_1.7-6
[157] farver_2.1.2
[158] tzdb_0.5.0
[159] AnnotationFilter_1.36.0
[160] reshape2_1.4.5
[161] viridis_0.6.5
[162] yulab.utils_0.2.5
[163] DirichletMultinomial_1.54.1
[164] rpart_4.1.27
[165] glue_1.8.1
[166] cachem_1.1.0
[167] bspm_0.5.8
[168] VennDiagram_1.8.2
[169] BiocFileCache_3.2.0
[170] polyclip_1.10-7
[171] Hmisc_5.3-0
[172] sysfonts_0.8.9
[173] pkgload_1.5.3
[174] statmod_1.5.2
[175] fontBitstreamVera_0.1.1
[176] GEOquery_2.80.0
[177] httr2_1.3.0
[178] showtextdb_3.0
[179] gson_0.2.2
[180] dmrseq_1.32.0
[181] graphlayouts_1.2.5
[182] siggenes_1.86.0
[183] gtools_3.9.5
[184] readxl_1.6.0
[185] preprocessCore_1.74.0
[186] gridExtra_2.3.1
[187] shiny_1.14.0
[188] tidydr_0.0.7
[189] R.utils_2.13.0
[190] rhdf5filters_1.24.1
[191] RCurl_1.98-1.20
[192] memoise_2.0.1
[193] rmarkdown_2.32
[194] R.methodsS3_1.8.2
[195] svglite_2.2.2
[196] reshape_0.8.10
[197] fontLiberation_0.1.0
[198] illuminaio_0.54.0
[199] rstudioapi_0.19.0
[200] cluster_2.1.8.3
[201] hms_1.1.4
[202] DMRcate_3.8.0
[203] colorspace_2.1-4
[204] FNN_1.1.4.1
[205] rlang_1.3.0
[206] quadprog_1.5-8
[207] GenomeInfoDb_1.48.0
[208] DelayedMatrixStats_1.34.0
[209] sparseMatrixStats_1.24.0
[210] ggforce_0.5.0
[211] circlize_0.4.18
[212] ggtangle_0.1.4
[213] xfun_0.62
[214] ggseqlogo_0.2.2
[215] e1071_1.7-17
[216] aisdk_1.4.12
[217] abind_1.4-8
[218] GOSemSim_2.38.3
[219] tibble_3.3.1
[220] treeio_1.36.1
[221] Rhdf5lib_2.0.0
[222] readr_2.2.0
[223] futile.options_1.0.1
[224] bitops_1.1-0
[225] ps_1.9.3
[226] promises_1.5.0
[227] qvalue_2.44.0
[228] scatterpie_0.2.6
[229] inline_0.3.21
[230] RSQLite_3.53.3
[231] DelayedArray_0.38.2
[232] proxy_0.4-29
[233] GO.db_3.23.1
[234] compiler_4.6.1
[235] prettyunits_1.2.0
[236] beachmat_2.28.0
[237] graphite_1.58.0
[238] Rcpp_1.1.2
[239] benchmarkmeData_2.0.0
[240] fontquiver_0.2.1
[241] edgeR_4.10.6
[242] AnnotationHub_4.2.2
[243] usethis_3.2.2
[244] MASS_7.3-66
[245] progress_1.2.3
[246] R6_2.6.1
[247] fastmap_1.2.0
[248] ensembldb_2.36.1
[249] enrichit_0.2.6
[250] nnet_7.3-21
[251] gtable_0.3.6
[252] strex_2.1.0
[253] latticeExtra_0.6-31
[254] deldir_2.0-4
[255] htmltools_0.5.9
[256] bit64_4.8.6
[257] lifecycle_1.0.5
[258] S7_0.2.2
[259] processx_3.9.0
[260] callr_3.8.0
[261] Gviz_1.56.0
[262] restfulr_0.0.17
[263] sass_0.4.10
[264] vctrs_0.7.3
[265] DOSE_4.6.0
[266] ggfun_0.2.2
[267] bslib_0.12.0
[268] pillar_1.11.1
[269] GenomicFeatures_1.64.0
[270] magick_2.9.1
[271] otel_0.2.0
[272] jsonlite_2.0.0
[273] cigarillo_1.2.1
[274] GetoptLong_1.1.1