# About this analysis
This file runs the usual TF activity analysis from
Decoupler analysis.Rmd, then keeps only the transcription
factors that regulate MYC, meaning MYC is one of their
targets in CollecTRI. It lists them ranked by activity score.
stat from DESeq2_mraz.tsv (set with
deseq_file in the YAML params), where positive
= higher in tFL. Every TF is scored on the full CollecTRI network with
decoupleR ULM and MLM (minsize = 5),
and on bcellViper with msVIPER with shadow for
support.exclude_myc_dropouts), and so are their patients for the
within-patient changes, because a zero in a shallow library is more
likely dropout than absent MYC.params.suppressPackageStartupMessages({
library(decoupleR)
library(viper)
library(bcellViper)
library(dplyr)
library(tidyr)
library(ggplot2)
library(patchwork)
library(openxlsx)
})
proj_dir <- "C:/Users/User/Desktop/MM Work/Main Work/fl_tFL analysis R"
deseq_file <- if (grepl("^([A-Za-z]:)?[/\\\\]", params$deseq_file)) params$deseq_file else
file.path(proj_dir, params$deseq_file)
collectri_file <- "C:/Users/User/Desktop/MM Work/collectri_human.csv"
out_xlsx <- if (grepl("^([A-Za-z]:)?[/\\\\]", params$out_xlsx)) params$out_xlsx else
file.path(proj_dir, params$out_xlsx)
if (!file.exists(deseq_file))
stop(deseq_file, " not found. Knit 'Deseq analysis.Rmd' first, or set deseq_file: \"DESeq2_mraz.tsv\".")
# plot tokens
col_act <- "#2a78d6" # activates MYC
col_rep <- "#eb6834" # represses MYC
col_tfl <- "#1baf7a" # tFL samples
col_fl <- "#898781" # FL samples
col_context <- "#c3c2b7"
col_grid <- "#e1e0d9"
col_surface <- "#fcfcfb"
ink_primary <- "#0b0b0b"
ink_second <- "#52514e"
ink_muted <- "#898781"
edge_cols <- c("activates MYC" = col_act, "represses MYC" = col_rep)
# outcome colours for the cross-method tiles (the same slots as in Decoupler proliferation sensitivity.Rmd)
col_survive <- "#2a78d6"
col_atten <- "#eb6834"
col_lost <- "#1baf7a"
theme_sens <- function(base_size = 11) {
theme_minimal(base_size = base_size) +
theme(plot.background = element_rect(fill = col_surface, colour = NA),
panel.grid.major = element_line(colour = col_grid, linewidth = 0.3),
panel.grid.minor = element_blank(),
axis.text = element_text(colour = ink_muted),
axis.title = element_text(colour = ink_second),
plot.title = element_text(colour = ink_primary, face = "bold"),
plot.subtitle = element_text(colour = ink_second),
plot.caption = element_text(colour = ink_muted, hjust = 0),
plot.title.position = "plot",
plot.caption.position = "plot",
strip.text = element_text(colour = ink_primary, face = "bold", hjust = 0),
legend.position = "top",
legend.justification = "left",
legend.title = element_blank(),
legend.text = element_text(colour = ink_second))
}
show_table <- function(df, caption = NULL) {
num <- which(vapply(df, is.double, logical(1)))
DT::datatable(df, rownames = FALSE, caption = caption, filter = "top",
options = list(pageLength = 15, scrollX = TRUE)) |>
DT::formatSignif(columns = num, digits = 3)
}
is_sig <- function(p) {
q <- if (params$p_adjust == "BH") stats::p.adjust(p, "BH") else p
!is.na(q) & q < params$p_cut
}
Data prep is identical to sections 1 and 3 of
Decoupler analysis.Rmd.
deseq_unfiltered <- read.table(deseq_file, sep = "\t", header = TRUE)
deseq <- deseq_unfiltered |> dplyr::filter(baseMean > 4.99)
# decoupleR signature: stat as a genes x 1 matrix (first row per gene_name).
# Labelled "FL_vs_tFL" in Decoupler analysis.Rmd; the label does not affect any result.
deseq_stats <- deseq[!duplicated(deseq$gene_name), c("gene_name", "stat")]
deseq_stats <- deseq_stats[!is.na(deseq_stats$stat), ]
mat_stat <- matrix(deseq_stats$stat, ncol = 1,
dimnames = list(deseq_stats$gene_name, "tFL_vs_FL"))
# msVIPER signature: baseMean > 10, one row per gene (largest |stat|)
deg <- deseq_unfiltered |>
dplyr::filter(!is.na(gene_name) & !is.na(stat) & baseMean > 10) |>
dplyr::arrange(dplyr::desc(abs(stat))) |>
dplyr::distinct(gene_name, .keep_all = TRUE)
sign_C <- setNames(deg$stat, deg$gene_name)
# each TF's own DESeq2 results, for context
deseq_lookup <- deseq_unfiltered |>
dplyr::filter(!duplicated(gene_name)) |>
dplyr::select(gene_name, deseq_baseMean = baseMean,
deseq_log2FoldChange = log2FoldChange, deseq_stat = stat,
deseq_pvalue = pvalue, deseq_padj = padj)
deseq_lookup |> dplyr::filter(gene_name == "MYC") |>
dplyr::mutate(dplyr::across(where(is.numeric), ~ signif(.x, 3))) |>
knitr::kable(caption = "MYC itself (positive stat = higher in tFL)")
| gene_name | deseq_baseMean | deseq_log2FoldChange | deseq_stat | deseq_pvalue | deseq_padj |
|---|---|---|---|---|---|
| MYC | 54.3 | 1.28 | 3.67 | 0.000243 | 0.00633 |
net_collectri <- read.csv(collectri_file)
net_collectri <- net_collectri |> dplyr::select(source, target, mor = weight)
targets_in_sig <- net_collectri |>
dplyr::filter(target %in% rownames(mat_stat)) |>
dplyr::distinct(source, target) |>
dplyr::count(source, name = "n_targets")
myc_targets <- net_collectri |>
dplyr::filter(source == "MYC", target %in% rownames(mat_stat)) |>
dplyr::pull(target)
shared_with_myc <- net_collectri |>
dplyr::filter(target %in% rownames(mat_stat)) |>
dplyr::distinct(source, target) |>
dplyr::group_by(source) |>
dplyr::summarise(shared_with_MYC_targets = mean(target %in% myc_targets), .groups = "drop")
myc_regulators <- net_collectri |>
dplyr::filter(target == "MYC") |>
dplyr::distinct(source, .keep_all = TRUE) |>
dplyr::transmute(source, mor_to_MYC = mor,
edge_to_MYC = ifelse(mor > 0, "activates MYC", "represses MYC")) |>
dplyr::left_join(targets_in_sig, by = "source") |>
dplyr::mutate(n_targets = dplyr::coalesce(n_targets, 0L),
scored = n_targets >= params$minsize)
myc_regulators |>
dplyr::count(edge_to_MYC, scored) |>
tidyr::pivot_wider(names_from = scored, values_from = n, names_prefix = "scored_") |>
knitr::kable(caption = sprintf("%d CollecTRI TFs have MYC as a target; scored = at least %d targets in the signature",
nrow(myc_regulators), params$minsize))
| edge_to_MYC | scored_FALSE | scored_TRUE |
|---|---|---|
| activates MYC | 6 | 87 |
| represses MYC | 2 | 60 |
cat("Not scored (fewer than", params$minsize, "targets in the signature):",
paste(myc_regulators$source[!myc_regulators$scored], collapse = ", "), "\n")
## Not scored (fewer than 5 targets in the signature): NME2, AHRR, ZBTB32, IKZF3, L3MBTL1, TBL1X, NDN, ZMIZ1
cat("MYC is in the list because CollecTRI has a MYC -> MYC autoregulation edge.\n")
## MYC is in the list because CollecTRI has a MYC -> MYC autoregulation edge.
The decouple() call is identical to section 2a of
Decoupler analysis.Rmd. It runs on the full signature, and
again with the MYC gene removed, which drops the MYC edge from every
regulon.
run_collectri <- function(mat) {
decoupleR::decouple(
mat = mat, network = net_collectri,
.source = "source", .target = "target",
statistics = c("mlm", "ulm"),
args = list(mlm = list(.mor = "mor"),
ulm = list(.mor = "mor")),
consensus_score = FALSE,
minsize = params$minsize
)
}
# one row per TF: <method>_score, <method>_p, <method>_fdr (FDR across ALL scored TFs)
to_wide <- function(res, suffix = "") {
res |>
dplyr::select(source, statistic, score, p_value) |>
dplyr::group_by(statistic) |>
dplyr::mutate(fdr = stats::p.adjust(p_value, "BH")) |>
dplyr::ungroup() |>
tidyr::pivot_wider(names_from = statistic, values_from = c(score, p_value, fdr),
names_glue = paste0("{statistic}_{.value}", suffix)) |>
dplyr::rename_with(~ sub("_p_value", "_p", .x))
}
res_all <- run_collectri(mat_stat)
res_nomyc <- run_collectri(mat_stat[rownames(mat_stat) != "MYC", , drop = FALSE])
scores_all <- to_wide(res_all)
scores_nomyc <- to_wide(res_nomyc, "_without_MYC") |>
dplyr::select(source, dplyr::matches("_(score|p)_without_MYC$"))
tibble::tibble(run = c("all genes", "without MYC"),
TFs_scored = c(dplyr::n_distinct(res_all$source), dplyr::n_distinct(res_nomyc$source))) |>
knitr::kable()
| run | TFs_scored |
|---|---|
| all genes | 700 |
| without MYC | 698 |
data(bcellViper, package = "bcellViper", envir = environment())
regulon_filtered <- regulon[intersect(names(regulon), deg$gene_name)]
mrs <- viper::msviper(sign_C, regulon_filtered, pleiotropy = TRUE, verbose = FALSE)
viper_tab <- tibble::tibble(source = names(mrs$es$nes),
msviper_nes = as.numeric(mrs$es$nes),
msviper_p = as.numeric(mrs$es$p.value)) |>
dplyr::mutate(msviper_fdr = stats::p.adjust(msviper_p, "BH"),
MYC_in_bcellviper_regulon = source %in%
names(regulon)[vapply(regulon, function(r) "MYC" %in% names(r$tfmode), logical(1))])
For each sample: log2(normalized count + 1), each gene centred on its mean across the 22 samples, and the MYC gene removed. TF activity is then scored per sample with ULM, so no TF’s activity contains MYC’s own expression.
For each MYC regulator this section reports: - the Spearman
correlation of its activity level with MYC mRNA level, across all
samples and across tFL samples only; - the Spearman correlation of the
tFL − FL change in its activity with the change in MYC mRNA, within
patients; - its paired FL→tFL change
(activity ~ patient + FL/tFL), before and after adding MYC
mRNA to the model.
MYC has only tens of reads per sample. When
exclude_myc_dropouts is true, samples with 0 MYC reads are
left out of the level correlations. Patients with such a sample are left
out of the paired statistics, because their change in MYC is not
reliable; the all-patient within-patient correlation is still reported
for comparison.
nc <- grep("normCounts$", colnames(deseq_unfiltered), value = TRUE)
expr_rows <- deseq[!duplicated(deseq$gene_name), ]
expr_rows <- expr_rows[!is.na(expr_rows$stat), ]
X <- log2(as.matrix(expr_rows[, nc]) + 1)
rownames(X) <- expr_rows$gene_name
X <- X[apply(X, 1, sd) > 0 & rowSums(X > 0) >= 3, ]
samples <- tibble::tibble(sample = colnames(X),
group = ifelse(grepl("^tFL_", colnames(X)), "tFL", "FL"),
patient = sub("^t?FL_(\\d+)_normCounts$", "\\1", colnames(X)),
MYC_expr = as.numeric(X["MYC", ]))
Xc <- X - rowMeans(X)
act <- decoupleR::run_ulm(Xc[rownames(Xc) != "MYC", ], network = net_collectri,
.source = "source", .target = "target", .mor = "mor",
minsize = params$minsize)
per_sample <- act |>
dplyr::select(source, sample = condition, activity = score) |>
dplyr::filter(source %in% myc_regulators$source) |>
dplyr::left_join(samples, by = "sample")
# patients with a MYC dropout (0 reads) in either sample
myc_zero <- samples$sample[samples$MYC_expr == 0]
drop_patients <- if (isTRUE(params$exclude_myc_dropouts)) unique(samples$patient[samples$MYC_expr == 0]) else character(0)
n_pairs_used <- dplyr::n_distinct(samples$patient) - length(drop_patients)
cat("Samples with 0 MYC reads:", if (length(myc_zero)) paste(sub("_normCounts$", "", myc_zero), collapse = ", ") else "none",
"| patients left out of the per-sample link:", if (length(drop_patients)) paste(drop_patients, collapse = ", ") else "none",
"| pairs used:", n_pairs_used, "\n")
## Samples with 0 MYC reads: FL_17, tFL_4 | patients left out of the per-sample link: 17, 4 | pairs used: 9
pair_rho <- function(d) {
w <- tidyr::pivot_wider(d, id_cols = patient, names_from = group, values_from = c(activity, MYC_expr))
suppressWarnings(stats::cor.test(w$activity_tFL - w$activity_FL, w$MYC_expr_tFL - w$MYC_expr_FL,
method = "spearman", exact = FALSE))
}
spearman <- function(x, y) suppressWarnings(stats::cor.test(x, y, method = "spearman", exact = FALSE))
link_one <- function(d) {
rho_all <- unname(pair_rho(d)$estimate)
# level correlations need no pairs: only the samples with 0 MYC reads are left out
lv <- d[!(isTRUE(params$exclude_myc_dropouts) & d$MYC_expr == 0), ]
ct_s <- spearman(lv$activity, lv$MYC_expr)
ct_t <- spearman(lv$activity[lv$group == "tFL"], lv$MYC_expr[lv$group == "tFL"])
d <- d[!d$patient %in% drop_patients, ]
ct <- pair_rho(d)
f0 <- stats::lm(activity ~ factor(patient) + group, data = d)
f1 <- stats::lm(activity ~ factor(patient) + group + MYC_expr, data = d)
tibble::tibble(rho_MYC_samples = unname(ct_s$estimate),
p_rho_samples = ct_s$p.value,
rho_MYC_tFL = unname(ct_t$estimate),
p_rho_tFL = ct_t$p.value,
rho_MYC_within_patients = unname(ct$estimate),
p_rho_within = ct$p.value,
rho_MYC_within_all_patients = rho_all,
change_tFL_vs_FL = stats::coef(f0)[["grouptFL"]],
p_change = summary(f0)$coefficients["grouptFL", 4],
change_adj_for_MYC = stats::coef(f1)[["grouptFL"]],
p_change_adj = summary(f1)$coefficients["grouptFL", 4],
p_MYC_term = summary(f1)$coefficients["MYC_expr", 4])
}
link <- per_sample |>
dplyr::group_by(source) |>
dplyr::group_modify(~ link_one(.x)) |>
dplyr::ungroup()
myc_change <- samples |>
dplyr::filter(!patient %in% drop_patients) |>
tidyr::pivot_wider(id_cols = patient, names_from = group, values_from = MYC_expr) |>
dplyr::mutate(d = tFL - FL)
cat(sprintf("In the pairs used, MYC mRNA is higher in tFL in %d of %d patients (median log2 change %+.2f).\n",
sum(myc_change$d > 0), nrow(myc_change), median(myc_change$d)))
## In the pairs used, MYC mRNA is higher in tFL in 9 of 9 patients (median log2 change +1.54).
# |rho| needed for p < 0.05 with n pairs or samples (t approximation)
crit_rho <- function(n) { t <- stats::qt(0.975, n - 2); t / sqrt(n - 2 + t^2) }
rho_crit <- crit_rho(n_pairs_used)
# samples behind the level correlation shown in the ranked figure (params$level_rho_samples)
lvl_tfl <- !identical(params$level_rho_samples, "all")
n_lvl <- sum((!isTRUE(params$exclude_myc_dropouts) | samples$MYC_expr > 0) & (!lvl_tfl | samples$group == "tFL"))
myc_table <- myc_regulators |>
dplyr::filter(scored) |>
dplyr::left_join(scores_all, by = "source") |>
dplyr::left_join(scores_nomyc, by = "source") |>
dplyr::left_join(shared_with_myc, by = "source") |>
dplyr::left_join(viper_tab, by = "source") |>
dplyr::left_join(link, by = "source") |>
dplyr::left_join(deseq_lookup, by = c("source" = "gene_name")) |>
dplyr::mutate(
ulm_sig = is_sig(ulm_p), mlm_sig = is_sig(mlm_p),
consistent_with_MYC_up = sign(ulm_score) * mor_to_MYC > 0,
per_sample_support = dplyr::case_when(
source == "MYC" ~ "not applicable (autoregulation)",
is.na(rho_MYC_within_patients) ~ NA_character_,
sign(rho_MYC_within_patients) == mor_to_MYC &
p_rho_within < 0.05 ~ "tracks MYC as expected",
sign(rho_MYC_within_patients) == mor_to_MYC ~ "expected direction, not significant",
TRUE ~ "opposite direction")) |>
dplyr::arrange(dplyr::desc(ulm_score)) |>
dplyr::mutate(rank = dplyr::row_number()) |>
dplyr::select(rank, TF = source, edge_to_MYC, consistent_with_MYC_up, n_targets, shared_with_MYC_targets,
ulm_score, ulm_p, ulm_fdr, ulm_sig, mlm_score, mlm_p, mlm_fdr, mlm_sig,
ulm_score_without_MYC, ulm_p_without_MYC, mlm_score_without_MYC, mlm_p_without_MYC,
msviper_nes, msviper_p, msviper_fdr, MYC_in_bcellviper_regulon,
rho_MYC_samples, p_rho_samples, rho_MYC_tFL, p_rho_tFL,
rho_MYC_within_patients, p_rho_within, per_sample_support,
rho_MYC_within_all_patients,
change_tFL_vs_FL, p_change, change_adj_for_MYC, p_change_adj, p_MYC_term,
mor_to_MYC, dplyr::starts_with("deseq_"))
myc_table |>
dplyr::select(rank, TF, edge_to_MYC, consistent_with_MYC_up, n_targets, ulm_score, ulm_p,
mlm_score, mlm_p, ulm_score_without_MYC, msviper_nes,
rho_MYC_within_patients, per_sample_support) |>
show_table("All scored MYC regulators, sorted by ULM activity score (positive = more active in tFL)")
sig_tab <- myc_table |> dplyr::filter(ulm_sig)
both_tab <- myc_table |> dplyr::filter(ulm_sig, mlm_sig)
push_up <- sig_tab |> dplyr::filter(consistent_with_MYC_up, TF != "MYC")
tracks <- sig_tab |> dplyr::filter(per_sample_support == "tracks MYC as expected")
max_shift <- max(abs(myc_table$ulm_score - myc_table$ulm_score_without_MYC), na.rm = TRUE)
fmt <- function(d) if (nrow(d)) paste0(d$TF, ifelse(d$ulm_score > 0, " (up)", " (down)"), collapse = ", ") else "none"
At a glance
plot_df <- myc_table |>
dplyr::mutate(msviper_sig = is_sig(msviper_p)) |>
dplyr::filter(ulm_sig | mlm_sig) |>
dplyr::mutate(TF = factor(TF, levels = TF[order(ulm_score)]))
# one method's score as a lollipop; filled = significant in that method
score_lollipop <- function(score, sig, title, xlab, first = FALSE) {
d <- plot_df |>
dplyr::filter(!is.na(.data[[score]])) |>
dplyr::mutate(x = .data[[score]], s = .data[[sig]])
p <- ggplot(d, aes(y = TF)) +
geom_vline(xintercept = 0, colour = col_context, linewidth = 0.3) +
geom_segment(aes(x = 0, xend = x, yend = TF), colour = col_context, linewidth = 0.4) +
geom_point(data = ~ dplyr::filter(.x, s), aes(x = x, fill = edge_to_MYC),
shape = 21, size = 2.6, colour = col_surface, stroke = 0.5) +
geom_point(data = ~ dplyr::filter(.x, !s), aes(x = x, colour = edge_to_MYC),
shape = 21, size = 2.4, fill = col_surface, stroke = 1) +
scale_y_discrete(limits = levels(plot_df$TF)) + # the same rows in every panel
scale_fill_manual(values = edge_cols, guide = if (first) "legend" else "none") +
scale_colour_manual(values = edge_cols, guide = "none") +
labs(title = title, x = xlab, y = NULL) +
theme_sens()
if (first) p else p + theme(axis.text.y = element_blank())
}
p_ulm <- score_lollipop("ulm_score", "ulm_sig", "ULM", "ULM score", first = TRUE)
p_mlm <- score_lollipop("mlm_score", "mlm_sig", "MLM", "MLM score")
p_vip <- score_lollipop("msviper_nes", "msviper_sig", "msVIPER", "msVIPER NES (bcellViper)")
# a Spearman rho as a lollipop; filled = p < 0.05
rho_lollipop <- function(rho, p, title, xlab) {
d <- plot_df |>
dplyr::filter(!is.na(.data[[rho]])) |>
dplyr::mutate(x = .data[[rho]], s = .data[[p]] < 0.05)
ggplot(d, aes(y = TF)) +
geom_vline(xintercept = 0, colour = col_context, linewidth = 0.3) +
geom_segment(aes(x = 0, xend = x, yend = TF), colour = col_context, linewidth = 0.4) +
geom_point(data = ~ dplyr::filter(.x, s), aes(x = x, fill = edge_to_MYC),
shape = 21, size = 2.6, colour = col_surface, stroke = 0.5) +
geom_point(data = ~ dplyr::filter(.x, !s), aes(x = x, colour = edge_to_MYC),
shape = 21, size = 2.4, fill = col_surface, stroke = 1) +
scale_y_discrete(limits = levels(plot_df$TF)) +
scale_fill_manual(values = edge_cols, guide = "none") +
scale_colour_manual(values = edge_cols, guide = "none") +
scale_x_continuous(limits = c(-1, 1)) +
labs(title = title, x = xlab, y = NULL) +
theme_sens() +
theme(axis.text.y = element_blank())
}
p_chg <- rho_lollipop("rho_MYC_within_patients", "p_rho_within", "Tracks MYC change",
sprintf("Spearman rho, tFL - FL change (%d pairs)", n_pairs_used))
p_lvl <- rho_lollipop(if (lvl_tfl) "rho_MYC_tFL" else "rho_MYC_samples",
if (lvl_tfl) "p_rho_tFL" else "p_rho_samples",
"Tracks MYC level",
sprintf("Spearman rho, %s samples (n = %d)", if (lvl_tfl) "tFL" else "all", n_lvl))
p_ulm + p_mlm + p_vip + p_chg + p_lvl +
patchwork::plot_layout(widths = c(1.3, 1, 1, 1, 1)) +
patchwork::plot_annotation(
title = "MYC regulators significant in ULM or MLM",
subtitle = paste0("Activity, tFL vs FL (positive = more active in tFL); its within-patient link to the change in MYC mRNA; ",
"and its link to the MYC mRNA level across ", if (lvl_tfl) "tFL" else "all", " samples"),
caption = paste0("Filled points: significant in that method (rho: p < 0.05); hollow: not. ",
"No msVIPER point: no bcellViper regulon. Rows sorted by ULM score.\n",
"Activators of MYC should sit right of zero and repressors left of zero if their change is consistent with MYC going up in tFL. ",
sprintf("|rho| above about %.2f is significant with %d pairs (change) and %.2f with %d samples (level).\n",
rho_crit, n_pairs_used, crit_rho(n_lvl), n_lvl),
"Samples with 0 MYC reads are left out of the level correlation. MYC's own row reflects autoregulation."),
theme = theme_sens())
Per-sample activity vs MYC mRNA. One figure per TF. Activity is the ULM t-value in each sample, with the MYC gene left out, and lines join each patient’s FL and tFL samples:
Points at 0 MYC are dropouts (patients 17 and 4), left out of the
within-patient correlation. Choose the TFs with
per_sample_tfs: in the YAML header: a number such as
15 (the top N regulators above by |ULM score|),
"all" with the quotes, or a list of names such as
["GATA3", "RUNX1"].
# which TFs to plot: a number = top N regulators above by |ULM score|, "all" = every one of them,
# or TF names (any TF scored per sample)
sel <- params$per_sample_tfs
ranked <- plot_df |> dplyr::arrange(dplyr::desc(abs(ulm_score))) |> dplyr::pull(TF) |> as.character()
top_tf <- if (is.numeric(sel)) head(ranked, sel) else if (identical(sel, "all")) ranked else
intersect(sel, unique(act$source))
ps_all <- act |>
dplyr::select(source, sample = condition, activity = score) |>
dplyr::left_join(samples, by = "sample")
# points per sample, lines per patient, on MYC mRNA
patient_panel <- function(df, y, title) {
ggplot(df, aes(MYC_expr, .data[[y]])) +
geom_line(aes(group = patient), colour = col_context, linewidth = 0.3) +
geom_point(aes(fill = group), shape = 21, size = 2.4, colour = col_surface, stroke = 0.4) +
scale_fill_manual(values = c(FL = col_fl, tFL = col_tfl)) +
labs(title = title, x = "MYC mRNA, log2(normalized count + 1)", y = NULL) +
theme_sens()
}
plot_per_sample <- function(tf) {
d <- ps_all |> dplyr::filter(source == tf)
rho <- pair_rho(d[!d$patient %in% drop_patients, ])
edge <- myc_regulators$edge_to_MYC[match(tf, myc_regulators$source)]
row <- match(tf, deseq_unfiltered$gene_name) # first row per gene, as in deseq_lookup
de <- deseq_lookup[match(tf, deseq_lookup$gene_name), ]
# paired activity, FL to tFL
chg <- tidyr::pivot_wider(d, id_cols = patient, names_from = group, values_from = activity)
p_pair <- ggplot(d, aes(group, activity)) +
geom_line(aes(group = patient), colour = col_context, linewidth = 0.3) +
geom_point(aes(fill = group), shape = 21, size = 2.4, colour = col_surface, stroke = 0.4) +
scale_fill_manual(values = c(FL = col_fl, tFL = col_tfl)) +
scale_x_discrete(expand = expansion(add = 0.4)) +
labs(title = "Activity, FL to tFL (ULM t-value)",
subtitle = sprintf("Higher in tFL in %d of %d patients", sum(chg$tFL > chg$FL, na.rm = TRUE),
sum(!is.na(chg$tFL - chg$FL))),
x = NULL, y = NULL) +
theme_sens()
p_act <- patient_panel(d, "activity", "Activity vs MYC mRNA") + guides(fill = "none")
# across tFL samples: activity vs MYC mRNA (a level-to-level link, not a change)
tfl <- d |>
dplyr::filter(group == "tFL") |>
dplyr::mutate(used = !(isTRUE(params$exclude_myc_dropouts) & MYC_expr == 0))
rho_tfl <- suppressWarnings(stats::cor.test(tfl$activity[tfl$used], tfl$MYC_expr[tfl$used],
method = "spearman", exact = FALSE))
p_tfl <- ggplot(tfl, aes(MYC_expr, activity)) +
geom_smooth(data = ~ dplyr::filter(.x, used), method = "lm", formula = y ~ x, se = FALSE,
colour = col_context, linewidth = 0.5) +
geom_point(data = ~ dplyr::filter(.x, used), shape = 21, size = 2.4,
fill = col_tfl, colour = col_surface, stroke = 0.4) +
geom_point(data = ~ dplyr::filter(.x, !used), shape = 21, size = 2.2,
fill = col_surface, colour = col_tfl, stroke = 0.8) +
labs(title = sprintf("tFL only: rho %+.2f, p = %.2g (n = %d)",
unname(rho_tfl$estimate), rho_tfl$p.value, sum(tfl$used)),
x = "MYC mRNA, log2(normalized count + 1)", y = NULL) +
theme_sens()
p_mrna <- if (is.na(row)) {
ggplot() +
annotate("text", x = 0, y = 0, label = paste(tf, "is a CollecTRI complex: no single gene"),
colour = ink_muted, size = 3.2) +
theme_void() +
theme(plot.background = element_rect(fill = col_surface, colour = NA))
} else {
tibble::tibble(sample = nc, tf_mrna = log2(as.numeric(unlist(deseq_unfiltered[row, nc])) + 1)) |>
dplyr::left_join(samples, by = "sample") |>
patient_panel("tf_mrna", paste(tf, "mRNA, log2(normalized count + 1)")) +
guides(fill = "none")
}
mrna_txt <- if (is.na(row) || is.na(de$deseq_log2FoldChange)) "n/a" else
sprintf("log2FC %+.2f (DESeq2, shrunken), padj %s", de$deseq_log2FoldChange,
if (is.na(de$deseq_padj)) "n/a" else sprintf("%.2g", de$deseq_padj))
(p_pair | p_act) / (p_tfl | p_mrna) +
patchwork::plot_annotation(
title = if (is.na(edge)) tf else sprintf("%s (%s)", tf, edge),
subtitle = sprintf("Activity: ULM %+.1f; within-patient rho with MYC %+.2f, p = %.2g. mRNA: %s",
scores_all$ulm_score[match(tf, scores_all$source)],
unname(rho$estimate), rho$p.value, mrna_txt),
theme = theme_sens())
}
for (tf in top_tf) print(plot_per_sample(tf))
Many TFs share targets with MYC’s own program. When that program goes up in tFL, the shared targets go up with it, and a TF can look active only because of them. This section asks which TFs are still active once MYC’s target genes are taken out. It covers all TFs, with MYC’s regulators highlighted, and uses two definitions of MYC target genes:
Each set is tested the same way:
Decoupler proliferation sensitivity.Rmd, with the MYC
target genes in place of the E2F/G2M genes.The Hallmark lists below are the MSigDB gene sets
MYC_TARGETS_V1 (200 genes) and
MYC_TARGETS_V2 (58 genes), human, copied from
gsea-msigdb.org on 2026-09-29. Gene names are current HGNC symbols;
renamed genes are also matched on the original symbol MSigDB gives for
them. The CollecTRI set is every target of MYC in
collectri_human.csv. MYC itself is in both sets, so it is
removed too. The E2F_TARGETS and G2M_CHECKPOINT lists are the ones in
Decoupler proliferation sensitivity.Rmd, used here only to
report the overlap.
words <- function(x) strsplit(trimws(x), "\\s+")[[1]]
hallmark_myc_v1 <- words("
ABCE1 ACP1 AIMP2 AP3S1 APEX1 BUB3 C1QBP CAD CANX CBX3 CCNA2 CCT2 CCT3 CCT4 CCT5
CCT7 CDC20 CDC45 CDK2 CDK4 CLNS1A CNBP COPS5 COX5A CSTF2 CTPS1 CUL1 CYC1 DDX18 DDX21
DEK DHX15 DUT EEF1B2 EIF1AX EIF2S1 EIF2S2 EIF3B EIF3D EIF3J EIF4A1 EIF4E EIF4G2 EIF4H EPRS1
ERH ETF1 EXOSC7 FAM120A FBL G3BP1 GLO1 GNL3 GOT2 GSPT1 H2AZ1 HDAC2 HDDC2 HDGF HNRNPA1
HNRNPA2B1 HNRNPA3 HNRNPC HNRNPD HNRNPR HNRNPU HPRT1 HSP90AB1 HSPD1 HSPE1 IARS1 IFRD1 ILF2 IMPDH2 KARS1
KPNA2 KPNB1 LDHA LSM2 LSM7 MAD2L1 MCM2 MCM4 MCM5 MCM6 MCM7 MRPL23 MRPL9 MRPS18B MYC
NAP1L1 NCBP1 NCBP2 NDUFAB1 NHP2 NME1 NOLC1 NOP16 NOP56 NPM1 ODC1 ORC2 PA2G4 PABPC1 PABPC4
PCBP1 PCNA PGK1 PHB1 PHB2 POLD2 POLE3 PPIA PPM1G PRDX3 PRDX4 PRPF31 PRPS2 PSMA1 PSMA2
PSMA4 PSMA6 PSMA7 PSMB2 PSMB3 PSMC4 PSMC6 PSMD1 PSMD14 PSMD3 PSMD7 PSMD8 PTGES3 PWP1 RACK1
RAD23B RAN RANBP1 RFC4 RNPS1 RPL14 RPL18 RPL22 RPL34 RPL6 RPLP0 RPS10 RPS2 RPS3 RPS5
RPS6 RRM1 RRP9 RSL1D1 RUVBL2 SERBP1 SET SF3A1 SF3B3 SLC25A3 SMARCC1 SNRPA SNRPA1 SNRPB2 SNRPD1
SNRPD2 SNRPD3 SNRPG SRM SRPK1 SRSF1 SRSF2 SRSF3 SRSF7 SSB SSBP1 STARD7 SYNCRIP TARDBP TCP1
TFDP1 TOMM70 TRA2B TRIM28 TUFM TXNL4A TYMS U2AF1 UBA2 UBE2E1 UBE2L3 USP1 VBP1 VDAC1 VDAC3
XPO1 XPOT XRCC6 YWHAE YWHAQ")
hallmark_myc_v2 <- words("
AIMP2 BYSL CBX3 CDK4 DCTPP1 DDX18 DUSP2 EXOSC5 FARSA GNL3 GRWD1 HK2 HSPD1 HSPE1 IMP4
IPO4 LAS1L MAP3K6 MCM4 MCM5 MPHOSPH10 MRTO4 MYBBP1A MYC NDUFAF4 NIP7 NOC4L NOLC1 NOP16 NOP2
NOP56 NPM1 PA2G4 PES1 PHB1 PLK1 PLK4 PPAN PPRC1 PRMT3 PUS1 RABEPK RCL1 RRP12 RRP9
SLC19A1 SLC29A2 SORD SRM SUPV3L1 TBRG4 TCOF1 TFB2M TMEM97 UNG UTP20 WDR43 WDR74")
hallmark_e2f <- words("
AK2 ANP32E ASF1A ASF1B ATAD2 AURKA AURKB BARD1 BIRC5 BRCA1 BRCA2 BRMS1L BUB1B CBX5 CCNB2
CCNE1 CCP110 CDC20 CDC25A CDC25B CDCA3 CDCA8 CDK1 CDK4 CDKN1A CDKN1B CDKN2A CDKN2C CDKN3
CENPE CENPM CHEK1 CHEK2 CIT CKS1B CKS2 CNOT9 CSE1L CTCF CTPS1 DCK DCLRE1B DCTPP1 DDX39A
DEK DEPDC1 DIAPH3 DLGAP5 DNMT1 DONSON DSCC1 DUT E2F8 EED EIF2S1 ESPL1 EXOSC8 EZH2 GINS1
GINS3 GINS4 GSPT1 H2AX H2AZ1 HELLS HMGA1 HMGB2 HMGB3 HMMR HNRNPD HUS1 ILF3 ING3 IPO7
JPT1 KIF18B KIF22 KIF2C KIF4A KPNA2 LBR LIG1 LMNB1 LUC7L3 LYAR MAD2L1 MCM2 MCM3 MCM4
MCM5 MCM6 MCM7 MELK MKI67 MLH1 MMS22L MRE11 MSH2 MTHFD2 MXD3 MYBL2 MYC NAA38 NAP1L1 NASP
NBN NCAPD2 NME1 NOLC1 NOP56 NUDT21 NUP107 NUP153 NUP205 ORC2 ORC6 PA2G4 PAICS PAN2 PCNA
PDS5B PHF5A PLK1 PLK4 PMS2 PNN POLA2 POLD1 POLD2 POLD3 POLE POLE4 POP7 PPM1D PPP1R8
PRDX4 PRIM2 PRKDC PRPS1 PSIP1 PSMC3IP PTTG1 RACGAP1 RAD1 RAD21 RAD50 RAD51AP1 RAD51C RAN
RANBP1 RBBP7 RFC1 RFC2 RFC3 RNASEH2A RPA1 RPA2 RPA3 RRM2 SHMT1 SLBP SMC1A SMC3 SMC4 SMC6
SNRPB SPAG5 SPC24 SPC25 SRSF1 SRSF2 SSRP1 STAG1 STMN1 SUV39H1 SYNCRIP TACC3 TBRG4 TCF19
TFRC TIMELESS TIPIN TK1 TMPO TOP2A TP53 TRA2B TRIP13 TUBB TUBG1 UBE2S UBE2T UBR7 UNG
USP1 WDR90 WEE1 XPO1 XRCC6 ZW10")
hallmark_g2m <- words("
ABL1 AMD1 ARID4A ATF5 ATRX AURKA AURKB BARD1 BCL3 BIRC5 BRCA2 BUB1 BUB3 CASP8AP2 CBX1
CCNA2 CCNB2 CCND1 CCNF CCNT1 CDC20 CDC25A CDC25B CDC27 CDC45 CDC6 CDC7 CDK1 CDK4 CDKN1B
CDKN2C CDKN3 CENPA CENPE CENPF CHAF1A CHEK1 CHMP1A CKS1B CKS2 CTCF CUL1 CUL3 CUL4A CUL5
DBF4 DDX39A DKC1 DMD DR1 DTYMK E2F1 E2F2 E2F3 E2F4 EFNA5 EGF ESPL1 EWSR1 EXO1 EZH2 FANCC
FBXO5 FOXN3 G3BP1 GINS2 GSPT1 H2AX H2AZ1 H2AZ2 H2BC12 HIF1A HIRA HMGA1 HMGB3 HMGN2 HMMR
HNRNPD HNRNPU HOXC10 HSPA8 HUS1 ILF3 INCENP JPT1 KATNA1 KIF11 KIF15 KIF20B KIF22 KIF23
KIF2C KIF4A KIF5B KMT5A KNL1 KPNA2 KPNB1 LBR LIG3 LMNB1 MAD2L1 MAP3K20 MAPK14 MARCKS
MCM2 MCM3 MCM5 MCM6 MEIS1 MEIS2 MKI67 MNAT1 MT2A MTF2 MYBL2 MYC NASP NCL NDC80 NEK2
NOLC1 NOTCH2 NSD2 NUMA1 NUP50 NUP98 NUSAP1 ODC1 ODF2 ORC5 ORC6 PAFAH1B1 PBK PDS5B PLK1
PLK4 PML POLA2 POLE POLQ PRC1 PRIM2 PRMT5 PRP4K PTTG1 PTTG3P PURA RACGAP1 RAD21 RAD23B
RAD54L RASAL2 RBL1 RBM14 RPA2 RPS6KA5 SAP30 SFPQ SLC12A2 SLC38A1 SLC7A1 SLC7A5 SMAD3
SMARCC1 SMC1A SMC2 SMC4 SNRPD1 SQLE SRSF1 SRSF10 SRSF2 SS18 STAG1 STIL STMN1 SUV39H1
SYNCRIP TACC3 TENT4A TFDP1 TGFB1 TLE3 TMPO TNPO2 TOP1 TOP2A TPX2 TRA2B TRAIP TROAP TTK
UBE2C UBE2S UCK2 UPF1 WRN XPO1 YTHDC1")
stopifnot(length(unique(hallmark_myc_v1)) == 200, length(unique(hallmark_myc_v2)) == 58,
length(unique(hallmark_e2f)) == 200, length(unique(hallmark_g2m)) == 200)
# current HGNC symbol -> original MSigDB source symbol, for renamed genes
msigdb_alias <- c(EPRS1 = "EPRS", IARS1 = "IARS", KARS1 = "KARS", PHB1 = "PHB",
CTPS1 = "CTPS", H2AX = "H2AFX", H2AZ1 = "H2AFZ", H2AZ2 = "H2AFV",
H2BC12 = "HIST1H2BK", JPT1 = "HN1", MRE11 = "MRE11A", CNOT9 = "RQCD1",
KNL1 = "CASC5", TENT4A = "PAPD7", PRP4K = "PRPF4B", KMT5A = "SETD8",
NSD2 = "WHSC1", MAP3K20 = "ZAK")
to_data_symbol <- function(g, pool) {
hit <- ifelse(g %in% pool, g, NA_character_)
alt <- unname(msigdb_alias[g])
ifelse(is.na(hit) & !is.na(alt) & alt %in% pool, alt, hit)
}
e2f_g2m <- union(hallmark_e2f, hallmark_g2m)
prolif_genes <- sort(unique(stats::na.omit(to_data_symbol(e2f_g2m, deseq_unfiltered$gene_name))))
n_prolif_sig <- sum(prolif_genes %in% rownames(mat_stat))
myc_set_tbl <- tibble::tibble(msigdb_symbol = sort(union(hallmark_myc_v1, hallmark_myc_v2))) |>
dplyr::mutate(in_MYC_TARGETS_V1 = msigdb_symbol %in% hallmark_myc_v1,
in_MYC_TARGETS_V2 = msigdb_symbol %in% hallmark_myc_v2,
in_E2F_TARGETS = msigdb_symbol %in% hallmark_e2f,
in_G2M_CHECKPOINT = msigdb_symbol %in% hallmark_g2m,
data_symbol = to_data_symbol(msigdb_symbol, deseq_unfiltered$gene_name))
myc_set_genes <- sort(unique(stats::na.omit(myc_set_tbl$data_symbol)))
# CollecTRI: every target of MYC, activated or repressed (the MYC -> MYC edge included)
collectri_myc <- net_collectri |>
dplyr::filter(source == "MYC") |>
dplyr::distinct(target, .keep_all = TRUE) |>
dplyr::select(gene = target, collectri_MYC_mor = mor)
myc_sets <- list(hallmark = myc_set_genes, collectri = collectri_myc$gene)
set_labels <- c(hallmark = "Hallmark MYC_TARGETS_V1 + V2", collectri = "CollecTRI MYC targets")
set_keys <- setNames(names(set_labels), set_labels)
# one row per MYC target gene of either set, for the workbook
e2f_data <- to_data_symbol(hallmark_e2f, deseq_unfiltered$gene_name)
g2m_data <- to_data_symbol(hallmark_g2m, deseq_unfiltered$gene_name)
myc_gene_tbl <- myc_set_tbl |>
dplyr::transmute(gene = dplyr::coalesce(data_symbol, msigdb_symbol), msigdb_symbol,
in_MYC_TARGETS_V1, in_MYC_TARGETS_V2) |>
dplyr::full_join(collectri_myc, by = "gene") |>
dplyr::mutate(in_MYC_TARGETS_V1 = dplyr::coalesce(in_MYC_TARGETS_V1, FALSE),
in_MYC_TARGETS_V2 = dplyr::coalesce(in_MYC_TARGETS_V2, FALSE),
collectri_MYC_target = !is.na(collectri_MYC_mor),
in_E2F_TARGETS = gene %in% e2f_data,
in_G2M_CHECKPOINT = gene %in% g2m_data,
in_decoupler_signature = gene %in% rownames(mat_stat),
in_msviper_signature = gene %in% names(sign_C)) |>
dplyr::left_join(deseq_lookup |> dplyr::select(gene_name, deseq_baseMean, deseq_stat),
by = c("gene" = "gene_name")) |>
dplyr::arrange(gene)
overlap_row <- function(set_name, s) {
tibble::tibble(MYC_gene_set = set_name,
genes = length(s),
also_E2F_TARGETS = sum(s %in% hallmark_e2f),
also_G2M_CHECKPOINT = sum(s %in% hallmark_g2m),
also_E2F_or_G2M = sum(s %in% e2f_g2m),
pct_E2F_or_G2M = 100 * mean(s %in% e2f_g2m))
}
dplyr::bind_rows(overlap_row("MYC_TARGETS_V1", hallmark_myc_v1),
overlap_row("MYC_TARGETS_V2", hallmark_myc_v2),
overlap_row("V1 + V2 (used here)", union(hallmark_myc_v1, hallmark_myc_v2))) |>
knitr::kable(digits = 1, caption = "Overlap of the Hallmark MYC sets with E2F_TARGETS and G2M_CHECKPOINT (all MSigDB genes)")
| MYC_gene_set | genes | also_E2F_TARGETS | also_G2M_CHECKPOINT | also_E2F_or_G2M | pct_E2F_or_G2M |
|---|---|---|---|---|---|
| MYC_TARGETS_V1 | 200 | 35 | 29 | 47 | 23.5 |
| MYC_TARGETS_V2 | 58 | 12 | 6 | 12 | 20.7 |
| V1 + V2 (used here) | 240 | 40 | 31 | 52 | 21.7 |
shared_genes <- myc_set_tbl |> dplyr::filter(in_E2F_TARGETS | in_G2M_CHECKPOINT)
cat("Hallmark MYC target genes that are also E2F/G2M genes (", nrow(shared_genes), "): ",
paste(shared_genes$msigdb_symbol, collapse = ", "), "\n", sep = "")
## Hallmark MYC target genes that are also E2F/G2M genes (52): BUB3, CCNA2, CDC20, CDC45, CDK4, CTPS1, CUL1, DCTPP1, DEK, DUT, EIF2S1, G3BP1, GSPT1, H2AZ1, HNRNPD, HNRNPU, KPNA2, KPNB1, MAD2L1, MCM2, MCM4, MCM5, MCM6, MCM7, MYC, NAP1L1, NME1, NOLC1, NOP56, ODC1, ORC2, PA2G4, PCNA, PLK1, PLK4, POLD2, PRDX4, RAD23B, RAN, RANBP1, SMARCC1, SNRPD1, SRSF1, SRSF2, SYNCRIP, TBRG4, TFDP1, TRA2B, UNG, USP1, XPO1, XRCC6
renamed <- myc_set_tbl |> dplyr::filter(!is.na(data_symbol), data_symbol != msigdb_symbol)
cat("Hallmark genes matched on the older symbol:",
paste(renamed$msigdb_symbol, renamed$data_symbol, sep = " -> ", collapse = ", "), "\n")
## Hallmark genes matched on the older symbol: EPRS1 -> EPRS, H2AZ1 -> H2AFZ, IARS1 -> IARS, KARS1 -> KARS, PHB1 -> PHB
cat("Hallmark genes not in the DESeq2 file:",
paste(myc_set_tbl$msigdb_symbol[is.na(myc_set_tbl$data_symbol)], collapse = ", "), "\n")
## Hallmark genes not in the DESeq2 file: EIF4A1, HNRNPA1, HSPE1, NME1, PPAN, PSMA2, PSMA6, RPS10, TARDBP
sets_tbl <- dplyr::bind_rows(lapply(names(myc_sets), function(key) {
in_sig <- intersect(myc_sets[[key]], rownames(mat_stat))
other <- myc_sets[[setdiff(names(myc_sets), key)]]
tibble::tibble(MYC_gene_set = set_labels[[key]],
in_decoupleR_signature = length(in_sig),
in_msVIPER_signature = sum(names(sign_C) %in% myc_sets[[key]]),
also_E2F_G2M = sum(in_sig %in% prolif_genes),
pct_of_E2F_G2M_removed = 100 * sum(in_sig %in% prolif_genes) / n_prolif_sig,
also_in_the_other_set = sum(in_sig %in% other))
}))
knitr::kable(sets_tbl, digits = 1,
caption = sprintf("The two MYC target gene sets, genes in the signatures (the decoupleR signature has %d E2F/G2M genes)",
n_prolif_sig))
| MYC_gene_set | in_decoupleR_signature | in_msVIPER_signature | also_E2F_G2M | pct_of_E2F_G2M_removed | also_in_the_other_set |
|---|---|---|---|---|---|
| Hallmark MYC_TARGETS_V1 + V2 | 229 | 228 | 50 | 15.7 | 56 |
| CollecTRI MYC targets | 755 | 720 | 80 | 25.1 | 56 |
How the gene groups move between FL and tFL:
group_levels <- c("MYC target gene, also E2F/G2M", "MYC target gene, not E2F/G2M",
"E2F/G2M gene, not MYC target", "All other genes")
shift_tbl <- function(key) {
s <- myc_sets[[key]]
tibble::tibble(gene = rownames(mat_stat), stat = mat_stat[, 1]) |>
dplyr::mutate(group = dplyr::case_when(
gene %in% s & gene %in% prolif_genes ~ group_levels[1],
gene %in% s ~ group_levels[2],
gene %in% prolif_genes ~ group_levels[3],
TRUE ~ group_levels[4]),
group = factor(group, levels = group_levels)) |>
dplyr::group_by(group) |>
dplyr::summarise(genes = dplyr::n(), mean_stat = mean(stat), median_stat = stats::median(stat),
pct_higher_in_tFL = 100 * mean(stat > 0), .groups = "drop") |>
dplyr::mutate(MYC_gene_set = set_labels[[key]], .before = 1)
}
dplyr::bind_rows(lapply(names(myc_sets), shift_tbl)) |>
knitr::kable(digits = 2, caption = "DESeq2 stat by gene group, decoupleR signature (positive = higher in tFL)")
| MYC_gene_set | group | genes | mean_stat | median_stat | pct_higher_in_tFL |
|---|---|---|---|---|---|
| Hallmark MYC_TARGETS_V1 + V2 | MYC target gene, also E2F/G2M | 50 | 3.43 | 3.44 | 98.00 |
| Hallmark MYC_TARGETS_V1 + V2 | MYC target gene, not E2F/G2M | 179 | 2.60 | 2.63 | 96.65 |
| Hallmark MYC_TARGETS_V1 + V2 | E2F/G2M gene, not MYC target | 269 | 2.73 | 2.83 | 88.48 |
| Hallmark MYC_TARGETS_V1 + V2 | All other genes | 19129 | 0.00 | -0.07 | 48.09 |
| CollecTRI MYC targets | MYC target gene, also E2F/G2M | 80 | 2.96 | 3.08 | 86.25 |
| CollecTRI MYC targets | MYC target gene, not E2F/G2M | 675 | 0.71 | 0.67 | 63.56 |
| CollecTRI MYC targets | E2F/G2M gene, not MYC target | 239 | 2.80 | 2.86 | 91.21 |
| CollecTRI MYC targets | All other genes | 18633 | 0.00 | -0.07 | 48.00 |
The same decouple() and msviper() calls as
above, run once for each gene set on the signatures without it.
The random-target null uses a closed-form ULM, copied from
Decoupler proliferation sensitivity.Rmd. It computes the
same t-value as decoupleR::run_ulm, which is checked on all
genes and after each exclusion. MYC target genes that are not targets of
the TF are removed in every permutation, as in the real exclusion, so
the only difference is which targets are removed.
y <- mat_stat[, 1]
genes <- rownames(mat_stat)
net_u <- net_collectri |>
dplyr::filter(target %in% genes) |>
dplyr::distinct(source, target, .keep_all = TRUE)
tf_keep <- net_u |> dplyr::count(source) |> dplyr::filter(n >= params$minsize) |> dplyr::pull(source)
net_u <- net_u |> dplyr::filter(source %in% tf_keep)
tfs <- sort(unique(net_u$source))
W <- Matrix::sparseMatrix(i = match(net_u$target, genes), j = match(net_u$source, tfs),
x = net_u$mor, dims = c(length(genes), length(tfs)),
dimnames = list(genes, tfs))
# ULM t-value of the slope in y ~ w (with intercept), from sufficient statistics
ulm_t <- function(n, sy, syy, sw, sww, swy) {
sxx <- sww - sw^2 / n
sxy <- swy - sw * sy / n
syc <- syy - sy^2 / n
r <- sxy / sqrt(sxx * syc)
r * sqrt((n - 2) / (1 - r^2))
}
ulm_closed <- function(y, W) {
ulm_t(length(y), sum(y), sum(y^2), Matrix::colSums(W), Matrix::colSums(W^2),
as.numeric(Matrix::crossprod(W, y)))
}
ulm_before <- res_all |> dplyr::filter(statistic == "ulm")
t_cf_before <- setNames(ulm_closed(y, W), tfs)
if (!setequal(ulm_before$source, tfs) ||
max(abs(ulm_before$score - t_cf_before[ulm_before$source])) > 1e-6)
stop("Closed-form ULM does not reproduce decoupleR; the random-target null would not be valid.")
# size-matched random-target null for every TF; is_x marks the excluded genes
ulm_null <- function(is_x) {
set.seed(params$seed)
n_all <- length(y); sy_all <- sum(y); syy_all <- sum(y^2)
n_x <- sum(is_x); sy_x <- sum(y[is_x]); syy_x <- sum(y[is_x]^2)
null_cols <- c("k", "m", "t_before", "t_after", "retention",
"null_lo", "null_med", "null_hi", "p_null")
null_one <- function(j) {
out <- setNames(rep(NA_real_, length(null_cols)), null_cols)
rng <- if (W@p[j + 1] > W@p[j]) (W@p[j] + 1):W@p[j + 1] else integer(0)
idx <- W@i[rng] + 1L
w <- W@x[rng]
yj <- y[idx]
xj <- is_x[idx]
k <- length(idx); m <- sum(xj)
sw <- sum(w); sww <- sum(w^2); swy <- sum(w * yj)
out[c("k", "m")] <- c(k, m)
out["t_before"] <- ulm_t(n_all, sy_all, syy_all, sw, sww, swy)
if (k - m < params$minsize) return(out)
# universe with every excluded gene that is NOT a target of this TF removed
nA <- n_all - (n_x - m)
syA <- sy_all - (sy_x - sum(yj[xj]))
syyA <- syy_all - (syy_x - sum(yj[xj]^2))
t_drop <- function(s_y, s_yy, s_w, s_ww, s_wy)
ulm_t(nA - m, syA - s_y, syyA - s_yy, sw - s_w, sww - s_ww, swy - s_wy)
out["t_after"] <- t_drop(sum(yj[xj]), sum(yj[xj]^2), sum(w[xj]), sum(w[xj]^2), sum(w[xj] * yj[xj]))
out["retention"] <- out[["t_after"]] / out[["t_before"]]
if (m == 0) return(out)
R <- matrix(replicate(params$n_perm, sample.int(k, m)), nrow = m) # m x n_perm
cs <- function(v) colSums(matrix(v[R], nrow = m))
ret_null <- t_drop(cs(yj), cs(yj^2), cs(w), cs(w^2), cs(w * yj)) / out[["t_before"]]
out[c("null_lo", "null_med", "null_hi")] <- stats::quantile(ret_null, c(0.025, 0.5, 0.975),
names = FALSE)
out["p_null"] <- (1 + sum(ret_null <= out[["retention"]])) / (params$n_perm + 1)
out
}
do.call(rbind, lapply(seq_along(tfs), null_one)) |>
tibble::as_tibble() |>
dplyr::mutate(source = tfs, .before = 1)
}
classify <- function(score_b, sig_b, score_a, sig_a) {
testable_a <- !is.na(score_a)
dplyr::case_when(
is.na(score_b) ~ NA_character_, # TF not in this network
sig_b & !testable_a ~ "Untestable after exclusion",
sig_b & sig_a & sign(score_a) == sign(score_b) ~ "Survives",
sig_b & sig_a ~ "Lost (direction reversed)",
sig_b ~ "Lost",
testable_a & sig_a ~ "Emerged after exclusion",
TRUE ~ "Not significant")
}
run_without <- function(key) {
excl <- myc_sets[[key]]
is_x <- genes %in% excl
res <- run_collectri(mat_stat[!is_x, , drop = FALSE])
mrs_x <- viper::msviper(sign_C[!names(sign_C) %in% excl], regulon_filtered, pleiotropy = TRUE, verbose = FALSE)
nul <- ulm_null(is_x)
# the closed-form exclusion inside the null must equal decoupleR's run without these genes
chk <- nul |>
dplyr::inner_join(res |> dplyr::filter(statistic == "ulm") |> dplyr::select(source, score), by = "source")
if (nrow(chk) != sum(!is.na(nul$t_after)) || max(abs(chk$t_after - chk$score)) > 1e-6)
stop("Closed-form ULM does not reproduce decoupleR without the ", set_labels[[key]], ".")
list(scores = to_wide(res),
viper = tibble::tibble(source = names(mrs_x$es$nes),
msviper_nes = as.numeric(mrs_x$es$nes),
msviper_p = as.numeric(mrs_x$es$p.value),
msviper_fdr = stats::p.adjust(msviper_p, "BH")),
null = nul,
size =tibble::tibble(MYC_gene_set = set_labels[[key]],
decoupleR_signature = sum(!is_x),
msVIPER_signature = sum(!names(sign_C) %in% excl),
TFs_scored_CollecTRI = dplyr::n_distinct(res$source),
regulons_scored_msVIPER = length(mrs_x$es$nes)))
}
runs <- lapply(setNames(names(myc_sets), names(myc_sets)), run_without)
n_untest <- vapply(runs, function(r) sum(is.na(r$null$t_after)), integer(1))
dplyr::bind_rows(
tibble::tibble(MYC_gene_set = "none (all genes)", decoupleR_signature = nrow(mat_stat),
msVIPER_signature = length(sign_C), TFs_scored_CollecTRI = dplyr::n_distinct(res_all$source),
regulons_scored_msVIPER = length(mrs$es$nes)),
dplyr::bind_rows(lapply(runs, `[[`, "size"))) |>
knitr::kable(caption = "Signatures and TFs scored with and without each MYC target gene set")
| MYC_gene_set | decoupleR_signature | msVIPER_signature | TFs_scored_CollecTRI | regulons_scored_msVIPER |
|---|---|---|---|---|
| none (all genes) | 19627 | 17093 | 700 | 529 |
| Hallmark MYC_TARGETS_V1 + V2 | 19398 | 16865 | 694 | 529 |
| CollecTRI MYC targets | 18872 | 16373 | 574 | 529 |
# one row per TF on all genes, CollecTRI and bcellViper, with its overall direction
tf_base <- scores_all |>
dplyr::full_join(viper_tab |> dplyr::select(source, msviper_nes, msviper_p, msviper_fdr), by = "source") |>
dplyr::left_join(targets_in_sig, by = "source") |>
dplyr::left_join(myc_regulators |> dplyr::select(source, edge_to_MYC), by = "source") |>
dplyr::left_join(myc_table |> dplyr::select(source = TF, consistent_with_MYC_up), by = "source") |>
dplyr::mutate(MYC_regulator = source %in% myc_regulators$source,
ulm_sig = is_sig(ulm_p),
mlm_sig = is_sig(mlm_p),
msviper_sig = is_sig(msviper_p),
direction = dplyr::case_when(ulm_sig ~ sign(ulm_score),
mlm_sig ~ sign(mlm_score),
msviper_sig ~ sign(msviper_nes)),
direction = dplyr::case_when(direction > 0 ~ "up in tFL", direction < 0 ~ "down in tFL")) |>
dplyr::rename(TF = source)
verdicts <- function(key) {
r <- runs[[key]]
tf_base |>
dplyr::select(TF, ulm_score, ulm_sig, mlm_score, mlm_sig, msviper_nes, msviper_sig) |>
dplyr::left_join(r$scores |> dplyr::select(TF = source, ulm_score_after = ulm_score, ulm_p_after = ulm_p,
ulm_fdr_after = ulm_fdr, mlm_score_after = mlm_score,
mlm_p_after = mlm_p, mlm_fdr_after = mlm_fdr), by = "TF") |>
dplyr::left_join(r$null |> dplyr::select(TF = source, n_targets_removed = m, retention,
null_lo, null_med, null_hi, p_null), by = "TF") |>
dplyr::left_join(r$viper |> dplyr::select(TF = source, msviper_nes_after = msviper_nes,
msviper_p_after = msviper_p, msviper_fdr_after = msviper_fdr),
by = "TF") |>
dplyr::mutate(
MYC_gene_set = set_labels[[key]],
ulm_call = classify(ulm_score, ulm_sig, ulm_score_after, is_sig(ulm_p_after)),
ulm_verdict = dplyr::case_when(
ulm_call == "Survives" & n_targets_removed == 0 ~ "Survives (no MYC target genes among its targets)",
ulm_call == "Survives" & retention >= null_lo ~ "Survives: robust",
ulm_call == "Survives" ~ "Attenuated: still significant, MYC-target-weighted",
ulm_call == "Lost (direction reversed)" ~ "Reversed: significant in the opposite direction",
ulm_call == "Lost" & n_targets_removed == 0 ~ "Lost: background shift only (no MYC target genes among its targets)",
ulm_call == "Lost" & retention < null_lo ~ "Lost: MYC-target-driven",
ulm_call == "Lost" ~ "Lost: no worse than random target loss",
TRUE ~ ulm_call),
still_active = dplyr::if_else(ulm_sig, ulm_call == "Survives", NA), # ULM, Attenuated included
mlm_call = classify(mlm_score, mlm_sig, mlm_score_after, is_sig(mlm_p_after)),
msviper_call = classify(msviper_nes, msviper_sig, msviper_nes_after, is_sig(msviper_p_after))) |>
dplyr::select(TF, MYC_gene_set, n_targets_removed, ulm_score_after, ulm_p_after, ulm_fdr_after,
retention, null_lo, null_med, null_hi, p_null, ulm_verdict, still_active,
mlm_score_after, mlm_p_after, mlm_fdr_after, mlm_call,
msviper_nes_after, msviper_p_after, msviper_fdr_after, msviper_call)
}
# one row per TF and gene set
ex_long <- dplyr::bind_rows(lapply(names(myc_sets), verdicts)) |>
dplyr::left_join(tf_base, by = "TF") |>
dplyr::relocate(TF, MYC_gene_set, MYC_regulator, edge_to_MYC, consistent_with_MYC_up, direction, n_targets,
ulm_score, ulm_p, ulm_fdr, ulm_sig, mlm_score, mlm_p, mlm_fdr, mlm_sig,
msviper_nes, msviper_p, msviper_fdr, msviper_sig)
# one row per TF, gene set and method; a cell counts if still active in the TF's overall direction
method_levels <- c("ULM", "MLM", "msVIPER")
cells <- dplyr::bind_rows(
ex_long |> dplyr::transmute(TF, MYC_gene_set, method = "ULM", outcome = ulm_verdict,
sig_before = ulm_sig, sign_before = sign(ulm_score)),
ex_long |> dplyr::transmute(TF, MYC_gene_set, method = "MLM", outcome = mlm_call,
sig_before = mlm_sig, sign_before = sign(mlm_score)),
ex_long |> dplyr::transmute(TF, MYC_gene_set, method = "msVIPER", outcome = msviper_call,
sig_before = msviper_sig, sign_before = sign(msviper_nes))) |>
dplyr::left_join(tf_base |> dplyr::select(TF, direction), by = "TF") |>
dplyr::mutate(
dir_sign = dplyr::case_when(direction == "up in tFL" ~ 1, direction == "down in tFL" ~ -1),
agrees = dplyr::coalesce(sign_before == dir_sign, FALSE),
code = dplyr::case_when(
is.na(outcome) ~ "Not in network",
sig_before & !agrees ~ "Opposite direction",
grepl("^Survives", outcome) ~ "Active",
grepl("^Attenuated", outcome) ~ "Active, MYC-target-weighted",
grepl("^Lost|^Reversed", outcome) ~ "Lost",
outcome == "Untestable after exclusion" ~ "Untestable",
grepl("^Emerged", outcome) ~ "Emerged",
TRUE ~ "Not significant on all genes"),
active = code %in% c("Active", "Active, MYC-target-weighted"),
eligible = sig_before & agrees,
cell = paste0(set_keys[MYC_gene_set], "_", method))
cell_cols <- as.vector(outer(method_levels, names(myc_sets),
function(m, s) paste0(s, "_", m)))
summary_all <- cells |>
dplyr::group_by(TF) |>
dplyr::summarise(n_active = sum(active), n_eligible = sum(eligible),
direction_conflict = any(code == "Opposite direction"), .groups = "drop") |>
dplyr::left_join(cells |>
dplyr::select(TF, cell, code) |>
tidyr::pivot_wider(names_from = cell, values_from = code), by = "TF") |>
dplyr::left_join(tf_base, by = "TF") |>
dplyr::arrange(dplyr::desc(n_active), ulm_p, msviper_p)
# TFs significant on all genes in at least one method, most active cells first (ties: ULM p, then msVIPER p)
summary_tf <- summary_all |>
dplyr::filter(ulm_sig | mlm_sig | msviper_sig) |>
dplyr::mutate(rank = dplyr::row_number()) |>
dplyr::select(rank, TF, MYC_regulator, edge_to_MYC, consistent_with_MYC_up, direction,
n_active, n_eligible, direction_conflict, dplyr::all_of(cell_cols),
n_targets, ulm_score, ulm_p, mlm_score, mlm_p, msviper_nes, msviper_p)
cells |>
dplyr::filter(!is.na(outcome)) |>
dplyr::mutate(method = factor(method, levels = method_levels)) |>
dplyr::group_by(MYC_gene_set, method) |>
dplyr::summarise(significant_on_all_genes = sum(sig_before),
still_active = sum(sig_before & grepl("^Survives|^Attenuated", outcome)),
of_which_MYC_target_weighted = sum(grepl("^Attenuated", outcome)),
lost = sum(sig_before & grepl("^Lost|^Reversed", outcome)),
too_few_targets_left = sum(outcome == "Untestable after exclusion"),
emerged = sum(grepl("^Emerged", outcome)), .groups = "drop") |>
knitr::kable(caption = "All TFs: outcome in each method, with each method's own direction")
| MYC_gene_set | method | significant_on_all_genes | still_active | of_which_MYC_target_weighted | lost | too_few_targets_left | emerged |
|---|---|---|---|---|---|---|---|
| CollecTRI MYC targets | ULM | 109 | 41 | 3 | 53 | 15 | 21 |
| CollecTRI MYC targets | MLM | 68 | 30 | 0 | 27 | 11 | 15 |
| CollecTRI MYC targets | msVIPER | 309 | 285 | 0 | 24 | 0 | 0 |
| Hallmark MYC_TARGETS_V1 + V2 | ULM | 109 | 85 | 17 | 22 | 2 | 9 |
| Hallmark MYC_TARGETS_V1 + V2 | MLM | 68 | 54 | 0 | 14 | 0 | 8 |
| Hallmark MYC_TARGETS_V1 + V2 | msVIPER | 309 | 289 | 0 | 20 | 0 | 1 |
summary_tf |>
dplyr::count(n_active, name = "TFs") |>
dplyr::left_join(summary_tf |> dplyr::filter(MYC_regulator) |> dplyr::count(n_active, name = "MYC_regulators"),
by = "n_active") |>
dplyr::mutate(MYC_regulators = dplyr::coalesce(MYC_regulators, 0L)) |>
dplyr::arrange(dplyr::desc(n_active)) |>
knitr::kable(col.names = c(sprintf("Cells still active (of %d)", length(cell_cols)), "TFs", "of which MYC regulators"),
caption = sprintf("%d TFs significant on all genes in at least one method", nrow(summary_tf)))
| Cells still active (of 6) | TFs | of which MYC regulators |
|---|---|---|
| 6 | 1 | 1 |
| 5 | 4 | 1 |
| 4 | 18 | 6 |
| 3 | 14 | 3 |
| 2 | 283 | 42 |
| 1 | 59 | 15 |
| 0 | 37 | 10 |
lab_dir <- function(d) if (nrow(d)) paste0(d$TF, ifelse(d$direction == "up in tFL", " (up)", " (down)"),
collapse = ", ") else "none"
ulm_active <- vapply(names(myc_sets), function(key)
sum(ex_long$MYC_gene_set == set_labels[[key]] & ex_long$ulm_sig &
grepl("^Survives|^Attenuated", ex_long$ulm_verdict)), integer(1))
untest_sig <- vapply(names(myc_sets), function(key)
sum(ex_long$MYC_gene_set == set_labels[[key]] & ex_long$ulm_verdict %in% "Untestable after exclusion"),
integer(1))
top_tf <- summary_tf |> dplyr::filter(n_active == max(n_active))
reg_top <- head(summary_tf, params$n_top) |> dplyr::filter(MYC_regulator)
n_conflict <- sum(summary_tf$direction_conflict)
share_removed <- ex_long |>
dplyr::filter(!is.na(n_targets_removed)) |>
dplyr::mutate(share = n_targets_removed / n_targets) |>
dplyr::group_by(MYC_gene_set) |>
dplyr::summarise(all_TFs = stats::median(share), MYC_regulators = stats::median(share[MYC_regulator]),
.groups = "drop")
med_removed <- setNames(share_removed$all_TFs, set_keys[share_removed$MYC_gene_set])
med_removed_reg <- setNames(share_removed$MYC_regulators, set_keys[share_removed$MYC_gene_set])
At a glance
tile_cols <- c("Active" = col_survive, "Active, MYC-target-weighted" = col_atten, "Lost" = col_lost,
"Untestable" = ink_muted, "Opposite direction" = col_grid, "Not significant on all genes" = col_grid,
"Emerged" = col_grid, "Not in network" = col_surface)
tile_txt <- c("Active" = "S", "Active, MYC-target-weighted" = "A", "Lost" = "L", "Untestable" = "U",
"Opposite direction" = "O", "Not significant on all genes" = "", "Emerged" = "E",
"Not in network" = "-")
plot_tiles <- function(tf_rows, title, subtitle) {
mark <- !all(tf_rows$MYC_regulator) # star MYC regulators unless every row is one
lab <- tf_rows |>
dplyr::mutate(label = sprintf("%s (%s) %d/%d%s", TF, sub(" in tFL", "", direction), n_active, n_eligible,
ifelse(mark & MYC_regulator, " *", "")))
df <- cells |>
dplyr::inner_join(lab |> dplyr::select(TF, label), by = "TF") |>
dplyr::mutate(label = factor(label, levels = rev(lab$label)),
method = factor(method, levels = method_levels),
MYC_gene_set = factor(paste("Without", MYC_gene_set), levels = paste("Without", set_labels)),
txt = unname(tile_txt[code]),
txt_col = ifelse(code %in% c("Active", "Active, MYC-target-weighted", "Lost", "Untestable"),
"white", ink_second))
ggplot(df, aes(method, label)) +
geom_tile(aes(fill = code), colour = col_surface, linewidth = 0.8) +
geom_text(aes(label = txt, colour = txt_col), size = 2.8) +
facet_grid(~ MYC_gene_set) +
scale_fill_manual(values = tile_cols,
breaks = c("Active", "Active, MYC-target-weighted", "Lost", "Untestable",
"Not significant on all genes")) +
scale_colour_identity() +
scale_x_discrete(position = "top") +
guides(fill = guide_legend(nrow = 2, byrow = TRUE)) +
labs(title = title, subtitle = subtitle, x = NULL, y = NULL,
caption = paste0("S still active (significant, same sign); A still active but leaning on MYC target genes (ULM only); ",
"L lost; U too few targets left.\nO significant on all genes in the other direction (does not count); ",
"E significant only after the change; - not in that network.\n",
"Label: TF (overall direction) cells that count / cells where it was significant on all genes ",
"in that direction.", if (mark) " * regulates MYC in CollecTRI." else "")) +
theme_sens() +
theme(panel.grid.major = element_blank(), strip.placement = "outside")
}
plot_tiles(head(summary_tf, params$n_top),
"Which TFs are still active without MYC target genes?",
sprintf("Top %d of %d TFs significant on all genes in at least one method, most cells first",
params$n_top, nrow(summary_tf)))
reg_rows <- summary_tf |> dplyr::filter(MYC_regulator)
plot_tiles(head(reg_rows, params$n_top),
"Which MYC regulators are still active without MYC target genes?",
sprintf("Top %d of %d MYC regulators significant on all genes in at least one method, most cells first",
min(params$n_top, nrow(reg_rows)), nrow(reg_rows)))
summary_tf |>
dplyr::select(rank, TF, MYC_regulator, direction, n_active, n_eligible, direction_conflict,
dplyr::all_of(cell_cols), ulm_score, ulm_p, mlm_score, mlm_p, msviper_nes, msviper_p) |>
show_table("TFs significant on all genes in at least one method, ranked by cells still active without MYC target genes")
The same results for MYC’s regulators, for each gene set, drawn like the ranked figure under MYC regulators ranked by activity: the same regulators (significant in ULM or MLM on all genes), in the same order. Each panel shows one method’s score without the MYC target genes, filled if the TF is still active, with a grey dot at its score on all genes. The ULM verdicts against the random-target null are in the table below the figures.
myc_rows <- ex_long |> dplyr::filter(TF == "MYC")
ulm_groups <- c("Still active", "Still active, MYC-target-weighted", "Lost", "Too few targets left")
ex_long |>
dplyr::filter(MYC_regulator, ulm_sig, TF != "MYC") |>
dplyr::mutate(ulm_group = dplyr::case_when(
grepl("^Survives", ulm_verdict) ~ ulm_groups[1],
grepl("^Attenuated", ulm_verdict) ~ ulm_groups[2],
ulm_verdict == "Untestable after exclusion" ~ ulm_groups[4],
TRUE ~ ulm_groups[3]),
ulm_group = factor(ulm_group, levels = ulm_groups),
MYC_gene_set = factor(MYC_gene_set, levels = set_labels)) |>
dplyr::arrange(MYC_gene_set, ulm_group, ulm_p) |>
dplyr::group_by(MYC_gene_set, ulm_group, .drop = FALSE) |>
dplyr::summarise(n = dplyr::n(),
TFs = paste0(TF, ifelse(ulm_score > 0, " (up)", " (down)"), collapse = ", "),
.groups = "drop") |>
knitr::kable(col.names = c("MYC target genes removed", "ULM", "n", "Regulators"),
caption = "MYC regulators significant in ULM (MYC itself not counted)")
| MYC target genes removed | ULM | n | Regulators |
|---|---|---|---|
| Hallmark MYC_TARGETS_V1 + V2 | Still active | 12 | E2F4 (up), IKZF1 (down), GATA3 (down), NFE2L2 (up), KLF5 (up), EWSR1 (up), ETV3 (down), RUNX1 (down), NFATC2 (down), SPI1 (down), SP3 (up), E2F5 (up) |
| Hallmark MYC_TARGETS_V1 + V2 | Still active, MYC-target-weighted | 11 | E2F1 (up), TFDP1 (up), FOXM1 (up), MYCN (up), TBP (up), SP1 (up), STAT3 (up), JUN (up), ESR1 (up), AR (up), HIF1A (up) |
| Hallmark MYC_TARGETS_V1 + V2 | Lost | 12 | RB1 (down), EZH2 (up), MYBL2 (up), KAT5 (up), CREBBP (up), AHR (up), FOSB (up), MYB (up), NFKB (up), PATZ1 (up), GLI2 (up), CTCFL (up) |
| Hallmark MYC_TARGETS_V1 + V2 | Too few targets left | 1 | TFDP2 (up) |
| CollecTRI MYC targets | Still active | 12 | E2F4 (up), FOXM1 (up), MYCN (up), IKZF1 (down), GATA3 (down), NFE2L2 (up), AR (up), RUNX1 (down), RB1 (down), NFATC2 (down), SPI1 (down), E2F5 (up) |
| CollecTRI MYC targets | Still active, MYC-target-weighted | 2 | E2F1 (up), STAT3 (up) |
| CollecTRI MYC targets | Lost | 20 | TFDP1 (up), TBP (up), SP1 (up), KLF5 (up), JUN (up), EWSR1 (up), ESR1 (up), ETV3 (down), HIF1A (up), EZH2 (up), MYBL2 (up), KAT5 (up), SP3 (up), CREBBP (up), AHR (up), FOSB (up), MYB (up), NFKB (up), GLI2 (up), CTCFL (up) |
| CollecTRI MYC targets | Too few targets left | 2 | TFDP2 (up), PATZ1 (up) |
MYC’s own CollecTRI score is 15.4 on all genes and 12.1 without the Hallmark genes. Without its own CollecTRI targets it has nothing left to score.
tf_cols <- c(edge_cols, "not a MYC regulator" = ink_second)
# one method, drawn like the ranked figure: a lollipop from zero to the score without the MYC target
# genes (filled = still active: significant on all genes and without them, same sign), plus a grey
# dot at the score on all genes (hollow if not significant there)
without_panel <- function(d, before, after, sig_before, active, title, xlab, first = FALSE) {
d <- d |>
dplyr::mutate(b = .data[[before]], a = .data[[after]], sb = .data[[sig_before]], act = .data[[active]])
p <- ggplot(d, aes(y = label)) +
geom_vline(xintercept = 0, colour = col_context, linewidth = 0.3) +
geom_segment(data = ~ dplyr::filter(.x, !is.na(a)), aes(x = 0, xend = a, yend = label),
colour = col_context, linewidth = 0.4) +
geom_point(data = ~ dplyr::filter(.x, !is.na(b), sb), aes(x = b),
shape = 21, size = 1.6, fill = col_context, colour = col_surface, stroke = 0.3) +
geom_point(data = ~ dplyr::filter(.x, !is.na(b), !sb), aes(x = b),
shape = 21, size = 1.4, fill = col_surface, colour = col_context, stroke = 0.6) +
geom_point(data = ~ dplyr::filter(.x, !is.na(a), act), aes(x = a, fill = colour_group),
shape = 21, size = 2.6, colour = col_surface, stroke = 0.5) +
geom_point(data = ~ dplyr::filter(.x, !is.na(a), !act), aes(x = a, colour = colour_group),
shape = 21, size = 2.4, fill = col_surface, stroke = 1) +
scale_y_discrete(limits = levels(d$label)) + # the same rows in every panel
scale_fill_manual(values = tf_cols, breaks = names(tf_cols), guide = if (first) "legend" else "none") +
scale_colour_manual(values = tf_cols, guide = "none") +
labs(title = title, x = xlab, y = NULL) +
theme_sens()
if (first) p else p + theme(axis.text.y = element_blank())
}
plot_without <- function(rows, key, title) {
fig <- ex_long |>
dplyr::filter(MYC_gene_set == set_labels[[key]], TF %in% rows) |>
dplyr::mutate(colour_group = dplyr::coalesce(edge_to_MYC, "not a MYC regulator"),
ulm_active = still_active %in% TRUE,
mlm_active = mlm_call %in% "Survives",
vip_active = msviper_call %in% "Survives",
label = sprintf("%s (%d/%d)", TF, as.integer(n_targets_removed), as.integer(n_targets)))
fig$label <- factor(fig$label, levels = fig$label[order(fig$ulm_score)])
without_panel(fig, "ulm_score", "ulm_score_after", "ulm_sig", "ulm_active", "ULM", "ULM score", first = TRUE) +
without_panel(fig, "mlm_score", "mlm_score_after", "mlm_sig", "mlm_active", "MLM", "MLM score") +
without_panel(fig, "msviper_nes", "msviper_nes_after", "msviper_sig", "vip_active",
"msVIPER", "msVIPER NES (bcellViper)") +
patchwork::plot_layout(widths = c(1.3, 1, 1)) +
patchwork::plot_annotation(
title = paste0(title, ", without the ", set_labels[[key]]),
subtitle = "Score without the MYC target genes (positive = more active in tFL); grey dot: score on all genes",
caption = paste0("Filled points: still active (significant on all genes and without the MYC target genes, same sign); ",
"hollow: not. Hollow grey dot: not significant on all genes.\n",
"No coloured point: too few targets left without the MYC target genes, or (msVIPER) no bcellViper regulon. ",
"Rows sorted by ULM score on all genes.\n",
"Label: TF (MYC target genes among its CollecTRI targets / targets). ",
"ULM verdicts against the random-target null are in the tables and the without_MYC_targets sheet."),
theme = theme_sens())
}
ranked_regs <- as.character(plot_df$TF) # the regulators of the ranked figure
plot_without(ranked_regs, "hallmark", "MYC regulators significant in ULM or MLM")
plot_without(ranked_regs, "collectri", "MYC regulators significant in ULM or MLM")
ex_long |>
dplyr::filter(MYC_regulator, ulm_sig) |>
dplyr::select(TF, MYC_gene_set, edge_to_MYC, consistent_with_MYC_up, still_active, n_targets, n_targets_removed,
ulm_score, ulm_score_after, ulm_p_after, retention, null_lo, null_hi, ulm_verdict,
mlm_call, msviper_call) |>
show_table("MYC regulators significant in ULM on all genes, for each MYC target gene set")
The same figures for all TFs: the 40 TFs with the largest |ULM score| among those significant in ULM or MLM on all genes, sorted by ULM score. MYC regulators are coloured by their edge to MYC, other TFs dark grey.
top_all <- tf_base |>
dplyr::filter(ulm_sig | mlm_sig) |>
dplyr::slice_max(abs(ulm_score), n = params$n_top, with_ties = FALSE) |>
dplyr::pull(TF)
plot_without(top_all, "hallmark", sprintf("Top %d TFs significant in ULM or MLM", params$n_top))
plot_without(top_all, "collectri", sprintf("Top %d TFs significant in ULM or MLM", params$n_top))
readme <- tibble::tribble(
~column, ~meaning,
"rank", "Position when sorted by ULM score, highest first (positive = more active in tFL).",
"edge_to_MYC / mor_to_MYC", "Whether CollecTRI lists the TF as an activator (+1) or repressor (-1) of MYC.",
"consistent_with_MYC_up", "TRUE if the TF's change would push MYC up in tFL: an activator more active in tFL, or a repressor less active. Based on the ULM score sign.",
"n_targets", "The TF's CollecTRI targets present in the signature (baseMean > 4.99).",
"shared_with_MYC_targets", "Fraction of the TF's targets that are also MYC targets in CollecTRI. High values mean its activity partly reflects MYC's own downstream program.",
"ulm_* / mlm_*", "decoupleR scores (t-values), nominal p and BH-FDR across all scored TFs, on the full network and signature.",
"ulm_sig / mlm_sig", paste0(if (params$p_adjust == "BH") "BH-FDR" else "nominal p", " < ", params$p_cut, "."),
"*_without_MYC", "The same scores with the MYC gene removed from the signature, so no TF uses MYC as a target.",
"n_hallmark_MYC_targets", "How many of the TF's targets in the signature are Hallmark MYC target genes (MYC_TARGETS_V1 + V2).",
"still_active_without_hallmark_MYC / still_active_without_collectri_MYC", "TFs significant in ULM only: TRUE if still significant in ULM with the same sign after removing that MYC target gene set (Attenuated included). Details in the without_MYC_targets sheet.",
"n_cells_active_without_MYC_targets / n_cells_eligible", "Of the 6 summary cells (ULM, MLM and msVIPER, each without the Hallmark and without the CollecTRI MYC target genes): how many the TF is still active in, in its overall direction, and in how many it was significant on all genes in that direction. See the MYC_target_summary sheet.",
"msviper_*", "msVIPER with shadow on bcellViper (NES, p, BH-FDR), if the TF has a bcellViper regulon. MYC_in_bcellviper_regulon: MYC is among its bcellViper targets.",
"rho_MYC_samples / p_rho_samples", "Spearman correlation of per-sample TF activity level (MYC gene excluded) with MYC mRNA level, across all samples (samples with 0 MYC reads left out if exclude_myc_dropouts = TRUE).",
"rho_MYC_tFL / p_rho_tFL", "The same level correlation across tFL samples only.",
"rho_MYC_within_patients / p_rho_within", paste0("Spearman correlation of the tFL - FL change in activity with the change in MYC mRNA (",
n_pairs_used, " patients; those with a MYC dropout left out if exclude_myc_dropouts = TRUE)."),
"rho_MYC_within_all_patients", "The same correlation using all 11 patients, including MYC dropouts.",
"per_sample_support", "'tracks MYC as expected': within-patient rho has the sign of the edge (activator +, repressor -) and p < 0.05.",
"change_tFL_vs_FL / p_change", "Paired FL -> tFL change in per-sample activity (activity ~ patient + FL/tFL).",
"change_adj_for_MYC / p_change_adj / p_MYC_term", "The same change with MYC mRNA added to the model, and the p-value of the MYC term.",
"deseq_*", "The TF's own DESeq2 result (tFL vs FL).",
"MYC_target_summary sheet", "One row per TF significant on all genes in at least one method, ranked by n_active (ties: ULM p, then msVIPER p). direction: from ULM if significant there, otherwise MLM, otherwise msVIPER. direction_conflict: significant in the opposite direction in another method.",
"hallmark_* / collectri_* (summary sheet)", "Outcome per cell: Active (still significant, same sign) / Active, MYC-target-weighted (ULM, below the random-target null) / Lost / Untestable (too few targets left) / Opposite direction (significant on all genes against the TF's overall direction; does not count) / Emerged (significant only after the change) / Not significant on all genes / Not in network.",
"without_MYC_targets sheet", "One row per TF and MYC target gene set, for all TFs. *_after: scores without that gene set (FDR across all TFs in that run). The *_sig columns in this sheet and the summary sheet use all TFs.",
"n_targets_removed", "How many of the TF's CollecTRI targets in the signature are in that MYC target gene set.",
"retention / null_lo / null_med / null_hi / p_null", "ULM only: score kept after / before the exclusion, and the 2.5th / 50th / 97.5th percentile of the score kept when the same number of random targets is removed. p_null: share of random removals that keep as little or less.",
"ulm_verdict", "Survives: robust / Survives (no MYC target genes among its targets) / Attenuated (still significant, but its MYC target genes carried more signal than random targets) / Lost: MYC-target-driven / Lost: no worse than random target loss / Lost: background shift only / Reversed / Untestable after exclusion / Emerged / Not significant.",
"still_active", "TFs significant in ULM only: TRUE if still significant in ULM with the same sign after the exclusion (Attenuated included).",
"mlm_call / msviper_call", "Survives (same sign, still significant) / Lost / Lost (direction reversed) / Untestable after exclusion / Emerged / Not significant; empty if the TF is not in that network.",
"MYC_target_genes sheet", "Every gene in either MYC target set: Hallmark V1 / V2 membership, CollecTRI MYC target and its sign (collectri_MYC_mor), E2F_TARGETS / G2M_CHECKPOINT membership, whether it is in each signature, and its DESeq2 result."
)
run_info <- tibble::tibble(
item = c("date", "DESeq2 input", "p_cut", "p_adjust", "minsize", "CollecTRI TFs with MYC as a target", "scored",
"signature genes", "samples with 0 MYC reads", "patients left out of the per-sample link",
"Hallmark MYC target genes", "Hallmark MYC target genes: total / in DESeq2 file / in decoupleR signature / in msVIPER signature",
"CollecTRI MYC targets: total / in decoupleR signature / in msVIPER signature",
"n_perm / seed / n_top", "decoupleR", "viper", "R"),
value = c(format(Sys.time()), deseq_file, params$p_cut, params$p_adjust, params$minsize,
nrow(myc_regulators), sum(myc_regulators$scored), nrow(mat_stat),
paste(sub("_normCounts$", "", myc_zero), collapse = ", "), paste(drop_patients, collapse = ", "),
"MSigDB Hallmark MYC_TARGETS_V1 + MYC_TARGETS_V2 (human), gsea-msigdb.org, copied 2026-09-29",
paste(nrow(myc_set_tbl), length(myc_set_genes), sets_tbl$in_decoupleR_signature[1],
sets_tbl$in_msVIPER_signature[1], sep = " / "),
paste(nrow(collectri_myc), sets_tbl$in_decoupleR_signature[2], sets_tbl$in_msVIPER_signature[2], sep = " / "),
paste(params$n_perm, params$seed, params$n_top, sep = " / "),
as.character(packageVersion("decoupleR")), as.character(packageVersion("viper")), R.version.string))
# main sheet: the headline MYC-target results sit next to the MYC-gene check
mt_cols <- ex_long |>
dplyr::filter(MYC_gene_set == set_labels[["hallmark"]]) |>
dplyr::select(TF, n_hallmark_MYC_targets = n_targets_removed, still_active_without_hallmark_MYC = still_active) |>
dplyr::left_join(ex_long |>
dplyr::filter(MYC_gene_set == set_labels[["collectri"]]) |>
dplyr::select(TF, still_active_without_collectri_MYC = still_active), by = "TF") |>
dplyr::left_join(summary_all |> dplyr::select(TF, n_cells_active_without_MYC_targets = n_active,
n_cells_eligible = n_eligible), by = "TF")
myc_sheet <- myc_table |>
dplyr::left_join(mt_cols, by = "TF") |>
dplyr::relocate(dplyr::all_of(setdiff(names(mt_cols), "TF")), .after = mlm_p_without_MYC)
openxlsx::write.xlsx(
list(README = readme,
MYC_regulators = myc_sheet,
MYC_target_summary = summary_tf,
without_MYC_targets = ex_long,
MYC_target_genes = myc_gene_tbl,
per_sample_activity = per_sample,
not_scored = myc_regulators |> dplyr::filter(!scored),
run_info = run_info),
file = out_xlsx, overwrite = TRUE, firstRow = TRUE
)
cat("Wrote", out_xlsx, "\n")
## Wrote C:/Users/User/Desktop/MM Work/Main Work/fl_tFL analysis R/FL_TFL_MYC_regulators.xlsx
shared_with_MYC_targets), or
because both follow proliferation or tumour content.exclude_myc_dropouts: false to
keep them.n_active next to
n_eligible.sessionInfo()
## R version 4.4.0 (2024-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 22000)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: Europe/Budapest
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] openxlsx_4.2.9 patchwork_1.3.2 ggplot2_4.0.3
## [4] tidyr_1.3.2 dplyr_1.2.1 bcellViper_1.42.0
## [7] viper_1.40.0 Biobase_2.66.0 BiocGenerics_0.52.0
## [10] decoupleR_2.12.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.61 bslib_0.12.0
## [4] htmlwidgets_1.6.4 lattice_0.22-6 crosstalk_1.2.2
## [7] vctrs_0.7.3 tools_4.4.0 generics_0.1.4
## [10] parallel_4.4.0 tibble_3.3.1 proxy_0.4-29
## [13] pkgconfig_2.0.3 Matrix_1.7-6 KernSmooth_2.23-22
## [16] data.table_1.17.4 RColorBrewer_1.1-3 S7_0.2.1
## [19] lifecycle_1.0.5 compiler_4.4.0 farver_2.1.2
## [22] stringr_1.6.0 mixtools_2.0.0.1 codetools_0.2-20
## [25] htmltools_0.5.8.1 class_7.3-22 sass_0.4.10
## [28] yaml_2.3.12 plotly_4.12.1 pillar_1.11.1
## [31] jquerylib_0.1.4 MASS_7.3-60.2 DT_0.34.0
## [34] BiocParallel_1.40.2 cachem_1.1.0 nlme_3.1-164
## [37] parallelly_1.48.0 zip_2.3.3 tidyselect_1.2.1
## [40] digest_0.6.35 stringi_1.8.9 purrr_1.2.2
## [43] kernlab_0.9-33 labeling_0.4.3 splines_4.4.0
## [46] fastmap_1.2.0 grid_4.4.0 cli_3.6.6
## [49] magrittr_2.0.5 survival_3.5-8 e1071_1.7-17
## [52] withr_3.0.3 scales_1.4.0 segmented_2.2-2
## [55] rmarkdown_2.32 httr_1.4.9 otel_0.2.0
## [58] evaluate_1.0.5 knitr_1.52 viridisLite_0.4.3
## [61] mgcv_1.9-1 rlang_1.3.0 Rcpp_1.0.12
## [64] glue_1.8.0 rstudioapi_0.19.0 jsonlite_2.0.0
## [67] R6_2.6.1