| Title: | Whitfield Convergence Score and Seurat Metadata Association |
|---|---|
| Description: | Calculates the convergence score defined by Whitfield et al. 2026 <doi:10.1158/0008-5472.CAN-25-4403>. It allows visualization of this score alongside qualitative and quantitative Seurat object metadata via barplots and density curves, and runs appropriate statistical tests for associations. The code was built, corrected and adapted to SingleCellExpriment with the help of Gemini. |
| Authors: | Paul de Boissier [aut, cre] (ORCID: <https://orcid.org/0000-0003-2246-1580>) |
| Maintainer: | Paul de Boissier <paul.deboissier04@gmail.com> |
| License: | GPL-3 |
| Version: | 0.99.2 |
| Built: | 2026-10-11 10:50:38 UTC |
| Source: | https://github.com/pauldeboissier1/converger |
Computes a "convergence score" comparing two groups of module scores. Each group can be made of a single score or a combination of several scores; the underlying methodology is identical either way.
CalculateConvergedScore( object, principal_score, other_scores, principal_name = "Principal", other_name = "Other", output_colname = "Converged_Score", direction_colname = NULL, color_colname = NULL, principal_color = "#2ecc71", other_color = "#e74c3c" )CalculateConvergedScore( object, principal_score, other_scores, principal_name = "Principal", other_name = "Other", output_colname = "Converged_Score", direction_colname = NULL, color_colname = NULL, principal_color = "#2ecc71", other_color = "#e74c3c" )
object |
A SingleCellExperiment (or any SummarizedExperiment) or a Seurat object. Its cell metadata must already contain the score columns. |
principal_score |
Character. Name of one score, OR a character vector naming several scores. |
other_scores |
Character. Same as principal_score, but for the group compared against. |
principal_name |
Character. Human-readable label (default: "Principal"). |
other_name |
Character. Human-readable label (default: "Other"). |
output_colname |
Character. Name of the column for the score (default: "Converged_Score"). |
direction_colname |
Character or NULL. Name of the interpretation column. |
color_colname |
Character or NULL. Name of the hex color column. |
principal_color |
Character. Hex color for principal group (default: "#2ecc71"). |
other_color |
Character. Hex color for other group (default: "#e74c3c"). |
The function works on the cell-level metadata only (colData for a
SingleCellExperiment, meta.data for a Seurat object).
The object is never converted: it is returned with its original class and
all its content (assays, reduced dimensions, ...) untouched, with the new
columns added to the cell metadata.
The same object, with the convergence score columns added to its cell metadata.
Whitfield HJ, Anderson ND, Burke C, Groot Koerkamp MJ, Parks C, Ogbonnah T, Wood Y, Piapi A, Robertson E, Watt E, White A, De Noon S, Kennedy J, Nagrecha R, Meister MT, Aladowicz E, Man YKS, Laspidea V, Trinh MK, Hodder A, Porter T, Lawrence JE, Tuck E, Nguyen T, Kelsey A, Flanagan AM, Hewitt R, Smeulders N, Slater O, Hutchinson JC, Sebire N, Shipley JM, Drost J, Straathof K, Behjati S. High-Risk Rhabdomyosarcomas Feature a Convergent Cell State. Cancer Res. 2026 Jul 14. doi: 10.1158/0008-5472.CAN-25-4403. PMID: 42446905.
## --- SingleCellExperiment --- set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) # Simulated module scores (fast) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", principal_name = "Interferon", other_name = "Inflammatory", output_colname = "Converged_Immune", principal_color = "#9b59b6", other_color = "#e67e22" ) head(SummarizedExperiment::colData(sce)[, c("Converged_Immune", "Converged_Immune_Direction")]) ## --- Seurat (same function, same arguments) --- if (requireNamespace("SeuratObject", quietly = TRUE)) { seu <- SeuratObject::CreateSeuratObject(counts = counts) seu$Score_IFN <- rnorm(ncol(seu)) seu$Score_Inflam <- rnorm(ncol(seu)) seu <- CalculateConvergedScore( object = seu, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) head(seu[[]][, c("Converged_Immune", "Converged_Immune_Direction")]) } # Full workflow with MSigDB signatures on a SingleCellExperiment # (slower, requires scuttle and msigdbr) if (requireNamespace("scuttle", quietly = TRUE) && requireNamespace("msigdbr", quietly = TRUE)) { sce_path <- system.file("extdata", "pbmc3k_subset_sce.rds", package = "ConvergeR") sce <- readRDS(sce_path) sce <- scuttle::logNormCounts(sce) hs <- msigdbr::msigdbr(species = "Homo sapiens", category = "H") raw_group1 <- hs[hs$gs_name == "HALLMARK_INTERFERON_ALPHA_RESPONSE", ]$gene_symbol raw_group2 <- hs[hs$gs_name == "HALLMARK_INFLAMMATORY_RESPONSE", ]$gene_symbol group1_genes <- intersect(raw_group1, rownames(sce)) group2_genes <- intersect(raw_group2, rownames(sce)) # Mean log-normalized expression of each signature lc <- SingleCellExperiment::logcounts(sce) sce$Score_IFN <- colMeans(as.matrix(lc[group1_genes, , drop = FALSE])) sce$Score_Inflam <- colMeans(as.matrix(lc[group2_genes, , drop = FALSE])) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune_MSigDB" ) }## --- SingleCellExperiment --- set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) # Simulated module scores (fast) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", principal_name = "Interferon", other_name = "Inflammatory", output_colname = "Converged_Immune", principal_color = "#9b59b6", other_color = "#e67e22" ) head(SummarizedExperiment::colData(sce)[, c("Converged_Immune", "Converged_Immune_Direction")]) ## --- Seurat (same function, same arguments) --- if (requireNamespace("SeuratObject", quietly = TRUE)) { seu <- SeuratObject::CreateSeuratObject(counts = counts) seu$Score_IFN <- rnorm(ncol(seu)) seu$Score_Inflam <- rnorm(ncol(seu)) seu <- CalculateConvergedScore( object = seu, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) head(seu[[]][, c("Converged_Immune", "Converged_Immune_Direction")]) } # Full workflow with MSigDB signatures on a SingleCellExperiment # (slower, requires scuttle and msigdbr) if (requireNamespace("scuttle", quietly = TRUE) && requireNamespace("msigdbr", quietly = TRUE)) { sce_path <- system.file("extdata", "pbmc3k_subset_sce.rds", package = "ConvergeR") sce <- readRDS(sce_path) sce <- scuttle::logNormCounts(sce) hs <- msigdbr::msigdbr(species = "Homo sapiens", category = "H") raw_group1 <- hs[hs$gs_name == "HALLMARK_INTERFERON_ALPHA_RESPONSE", ]$gene_symbol raw_group2 <- hs[hs$gs_name == "HALLMARK_INFLAMMATORY_RESPONSE", ]$gene_symbol group1_genes <- intersect(raw_group1, rownames(sce)) group2_genes <- intersect(raw_group2, rownames(sce)) # Mean log-normalized expression of each signature lc <- SingleCellExperiment::logcounts(sce) sce$Score_IFN <- colMeans(as.matrix(lc[group1_genes, , drop = FALSE])) sce$Score_Inflam <- colMeans(as.matrix(lc[group2_genes, , drop = FALSE])) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune_MSigDB" ) }
Draws a 100%-stacked barplot of cell proportions across categories of x_var, colored by a convergence-direction column, and separated (faceted) by a secondary categorical variable (facet_var).
PlotConvergenceCrossedProportion( object, x_var, fill_var, facet_var, color_var = NULL, title = NULL, x_label = NULL )PlotConvergenceCrossedProportion( object, x_var, fill_var, facet_var, color_var = NULL, title = NULL, x_label = NULL )
object |
A SingleCellExperiment or Seurat object whose cell metadata holds x_var, fill_var, facet_var, and the color column. |
x_var |
Character. Name of the metadata column used on the x-axis. |
fill_var |
Character. Name of the metadata column used to fill the bars. |
facet_var |
Character. Name of the metadata column used to facet the plot (e.g., "tumor_status"). |
color_var |
Character or NULL. Name of the metadata column holding the hex color. |
title |
Character or NULL. Plot title. |
x_label |
Character or NULL. X-axis label. |
A ggplot object.
set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$SS <- paste0("Patient_", sample(1:5, ncol(sce), replace = TRUE)) sce$tumor_status <- sample(c("Primary", "Metastatic"), ncol(sce), replace = TRUE) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) b_cross <- PlotConvergenceCrossedProportion( object = sce, x_var = "SS", fill_var = "Converged_Immune_Direction", facet_var = "tumor_status", title = "Immune Convergence split by Tumor Status", x_label = "Patient ID" ) print(b_cross)set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$SS <- paste0("Patient_", sample(1:5, ncol(sce), replace = TRUE)) sce$tumor_status <- sample(c("Primary", "Metastatic"), ncol(sce), replace = TRUE) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) b_cross <- PlotConvergenceCrossedProportion( object = sce, x_var = "SS", fill_var = "Converged_Immune_Direction", facet_var = "tumor_status", title = "Immune Convergence split by Tumor Status", x_label = "Patient ID" ) print(b_cross)
Draws overlapping density curves of a continuous variable (x_var), split by a convergence-direction column (fill_var).
PlotConvergenceDensity( object, x_var, fill_var, color_var = NULL, title = NULL, x_label = NULL, alpha = 0.6 )PlotConvergenceDensity( object, x_var, fill_var, color_var = NULL, title = NULL, x_label = NULL, alpha = 0.6 )
object |
A SingleCellExperiment or Seurat object whose cell metadata holds x_var, fill_var, and its matching color column. |
x_var |
Character. Name of the metadata column holding the continuous variable (e.g. "age"). |
fill_var |
Character. Name of the metadata column used to fill the density curves. |
color_var |
Character or NULL. Name of the metadata column holding the hex color. |
title |
Character or NULL. Plot title. |
x_label |
Character or NULL. X-axis label. Defaults to x_var. |
alpha |
Numeric. Transparency of the density fill, between 0 and 1 (default: 0.6). |
A ggplot object.
set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$age <- sample(20:70, ncol(sce), replace = TRUE) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) PlotConvergenceDensity( object = sce, x_var = "age", fill_var = "Converged_Immune_Direction", x_label = "Patient Age" )set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$age <- sample(20:70, ncol(sce), replace = TRUE) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) PlotConvergenceDensity( object = sce, x_var = "age", fill_var = "Converged_Immune_Direction", x_label = "Patient Age" )
Draws a 100%-stacked barplot of cell proportions across categories of
x_var, colored by a convergence-direction column (fill_var). The color
mapping is extracted automatically from the cell metadata based on the
color column generated by CalculateConvergedScore.
PlotConvergenceProportion( object, x_var, fill_var, color_var = NULL, title = NULL )PlotConvergenceProportion( object, x_var, fill_var, color_var = NULL, title = NULL )
object |
A SingleCellExperiment or Seurat object whose cell metadata holds fill_var and its matching color column. |
x_var |
Character. Name of the metadata column used on the x-axis (e.g. a patient identifier). |
fill_var |
Character. Name of the metadata column used to fill the bars. |
color_var |
Character or NULL. Name of the metadata column holding the hex color. Defaults to paste0(fill_var, "_Color"). |
title |
Character or NULL. Plot title. Defaults to x_var. |
A ggplot object.
set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$SS <- paste0("Patient_", sample(1:5, ncol(sce), replace = TRUE)) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) p <- PlotConvergenceProportion( object = sce, x_var = "SS", fill_var = "Converged_Immune_Direction", title = "Immune Convergence by Patient" ) print(p)set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) sce$SS <- paste0("Patient_", sample(1:5, ncol(sce), replace = TRUE)) sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) p <- PlotConvergenceProportion( object = sce, x_var = "SS", fill_var = "Converged_Immune_Direction", title = "Immune Convergence by Patient" ) print(p)
Runs a statistical test between a numeric convergence score and another variable, automatically choosing the appropriate test (Spearman, Wilcoxon, or Kruskal-Wallis).
TestConvergenceScore( object, score_var, test_var, level = c("cell", "patient"), patient_id_var = NULL )TestConvergenceScore( object, score_var, test_var, level = c("cell", "patient"), patient_id_var = NULL )
object |
A SingleCellExperiment or Seurat object whose cell metadata holds score_var and test_var. |
score_var |
Character. Name of the metadata column holding the numeric score. |
test_var |
Character. Name of the metadata column to test the score against. |
level |
Character. Either "cell" (default) or "patient" (pseudobulk aggregation). |
patient_id_var |
Character. Name of the metadata column identifying patients (required if level = "patient"). |
The underlying htest object (invisibly). A message is printed summarizing the results.
set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) pat <- paste0("Patient_", 1:5) sce$SS <- sample(pat, ncol(sce), replace = TRUE) # age is a patient-level covariate (constant within a patient) sce$age <- c(25, 33, 47, 52, 68)[match(sce$SS, pat)] sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) TestConvergenceScore( object = sce, score_var = "Converged_Immune", test_var = "age", level = "patient", patient_id_var = "SS" )set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) pat <- paste0("Patient_", 1:5) sce$SS <- sample(pat, ncol(sce), replace = TRUE) # age is a patient-level covariate (constant within a patient) sce$age <- c(25, 33, 47, 52, 68)[match(sce$SS, pat)] sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) TestConvergenceScore( object = sce, score_var = "Converged_Immune", test_var = "age", level = "patient", patient_id_var = "SS" )
Fits a multivariate linear model (lm) to test a numeric convergence score against multiple covariates simultaneously, correcting for confounding factors.
TestMultivariateConvergence( object, score_var, test_vars, level = c("cell", "patient"), patient_id_var = NULL )TestMultivariateConvergence( object, score_var, test_vars, level = c("cell", "patient"), patient_id_var = NULL )
object |
A SingleCellExperiment or Seurat object whose cell metadata holds the variables. |
score_var |
Character. Name of the metadata column holding the numeric score. |
test_vars |
Character vector. Names of the metadata columns to include in the model. |
level |
Character. Either "cell" (default) or "patient" (pseudobulk aggregation). |
patient_id_var |
Character. Name of the metadata column identifying patients (required if level = "patient"). |
The summary.lm() object (invisibly). A detailed message is printed summarizing the model.
set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) pat <- paste0("Patient_", 1:8) sce$SS <- sample(pat, ncol(sce), replace = TRUE) # patient-level covariates (constant within a patient) sce$age <- c(25, 33, 47, 52, 68, 41, 59, 36)[match(sce$SS, pat)] sce$tumor_status <- rep(c("Primary", "Metastatic"), 4)[match(sce$SS, pat)] sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) TestMultivariateConvergence( object = sce, score_var = "Converged_Immune", test_vars = c("age", "tumor_status"), level = "patient", patient_id_var = "SS" )set.seed(42) counts <- matrix(rpois(20000, 5), nrow = 200, dimnames = list(paste0("gene", 1:200), paste0("cell", 1:100))) sce <- SingleCellExperiment::SingleCellExperiment(assays = list(counts = counts)) pat <- paste0("Patient_", 1:8) sce$SS <- sample(pat, ncol(sce), replace = TRUE) # patient-level covariates (constant within a patient) sce$age <- c(25, 33, 47, 52, 68, 41, 59, 36)[match(sce$SS, pat)] sce$tumor_status <- rep(c("Primary", "Metastatic"), 4)[match(sce$SS, pat)] sce$Score_IFN <- rnorm(ncol(sce)) sce$Score_Inflam <- rnorm(ncol(sce)) sce <- CalculateConvergedScore( object = sce, principal_score = "Score_IFN", other_scores = "Score_Inflam", output_colname = "Converged_Immune" ) TestMultivariateConvergence( object = sce, score_var = "Converged_Immune", test_vars = c("age", "tumor_status"), level = "patient", patient_id_var = "SS" )