--- title: "Getting Started with conformeR" author: "Justine Leclerc and Christof Seiler" output: BiocStyle::html_document: toc: true toc_float: true number_sections: true vignette: > %\VignetteIndexEntry{Getting Started with conformeR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r options, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", dev = "png", fig.width = 12, fig.height = 7, out.width = "100%", warning = FALSE, message = FALSE ) ``` ```{css, echo = FALSE} .callout { border-left: 4px solid #4472A8; background-color: #F2F6FA; padding: 0.6em 1em; margin: 1em 0; border-radius: 2px; } .callout p { margin: 0.3em 0; } ``` # Introduction ## Overview LEMUR fits a low-dimensional embedding and assigns cells to neighborhoods based on similar responses to a given contrast and gene. Because these neighborhood assignments are themselves derived from a fitted model, they are subject to sampling variability: refitting the model on a different sample of cells can shift the neighborhood boundaries. conformeR quantifies this uncertainty by wrapping the neighborhood construction step in a conformal prediction framework. It is designed to integrate naturally into existing Bioconductor workflows for single-cell data analysis, operating downstream of SingleCellExperiment-based preprocessing and LEMUR analyses to provide uncertainty-aware inference for neighborhood assignments. It provides two complementary views of that uncertainty: - **Conformal selection** asks, for a given gene, *which cells can we confidently say belong to the affected neighborhood?* It returns a subset of neighborhood cells with a guaranteed bound on the proportion of false inclusions. - **Conformal clustering** asks, *for each cell, how uncertain are we about its neighborhood assignment?* It returns a prediction *set* of plausible neighborhoods per cell, which can contain zero, one, or several labels.

**Why this matters.** A single LEMUR fit gives you one particular neighborhood assignment, with no way to tell how much of it would survive resampling. `conformeR` turns that single assignment into a statement with a formal, finite-sample coverage guarantee.

# Installation Install conformeR running the following commands. ```{r installation, eval=FALSE} if (!requireNamespace("BiocManager", quietly = TRUE)) { install.packages("BiocManager") } BiocManager::install("juslecl/conformeR") ``` ```{r setup and theme} library(conformeR) library(SingleCellExperiment) library(dplyr) library(ggplot2) library(tibble) library(knitr) theme_set( theme_minimal(base_size = 13) + theme( panel.grid.minor = element_blank(), plot.title = element_text(face = "bold"), legend.position = "bottom" ) ) conformer_palette <- c( "inside" = "#4472A8", "outside" = "#D9822B", "none" = "#B0B0B0" ) ``` # Use-case of conformeR ## Example dataset This vignette runs `conformeR` on a reduced version of the single-cell RNA-seq dataset from Zhao, Dovas, Spinazzi, et al. (2021), *"Deconvolution of cell type-specific drug responses in human tumor tissue with single-cell RNA-seq,"* published in *Genome Medicine* (GEO accession `GSE148842`). As with any input to `conformeR`, the data have already been preprocessed and quality-checked upstream. ```{r load-data} data("sce_reduced") sce_reduced rownames(sce_reduced) <- rowData(sce_reduced)$gene head(colData(sce_reduced)) ``` We restrict the analysis to three genes with distinct expected behavior, to illustrate a range of neighborhood sizes and uncertainty patterns: `HIST3H2A`, `PTPRZ1`, and `HBEGF`. ```{r genes-of-interest} pan_affect_genes <- rowData(sce_reduced) |> as_tibble() |> dplyr::select(gid, gene) |> filter(gene %in% c("HIST3H2A", "PTPRZ1", "HBEGF")) kable(pan_affect_genes, caption = "Genes of interest for this vignette") ``` ## Running conformeR `conformeR()` fits `LEMUR` and layers both conformal procedures on top of it in one call. The key arguments are: - `design_lemur` — the LEMUR design formula; here we adjust for `patient_id` and estimate the effect of `condition`. - `contrast_column` — which column of the design defines the contrast of interest. - `genes_of_interest` — restrict the (more expensive) conformal layers to a gene subset; LEMUR itself is still fit genome-wide. - `what` — which conformal procedure(s) to run: `"conf_selection"`, `"conf_clustering"`, or both. By default the target coverage is `cp_alpha`, and the data are split 50% training / 50% test, with the calibration set taking 25% of the training set (`cp_train_frac` and `cp_cal_frac` set these proportions). ```{r run-conformer} set.seed(2002) res <- conformeR( sce_reduced, design_lemur = ~ patient_id + condition, contrast_column = "condition", genes_of_interest = pan_affect_genes$gene, what = c("conf_selection", "conf_clustering"), n_cores = 1, verbose = TRUE ) ``` ## Conformal selection For each gene, conformal selection returns the set of cells flagged as belonging to the neighborhood affected by `condition`, at the target coverage level. The table below reports both the raw neighborhood size and its share of all cells in the test set. ```{r selection-summary} fit <- res$fit_lemur cp_out <- res$conf_results |> dplyr::mutate( inside_cs = purrr::map(inside_cs, ~ colnames(fit) %in% .x), inside_cc = purrr::map(inside_cc, ~ colnames(fit) %in% .x), outside_cc = purrr::map(outside_cc, ~ colnames(fit) %in% .x) ) |> tidyr::unnest(c(inside_cs, inside_cc, outside_cc)) |> dplyr::mutate(cell = rep(colnames(fit), nrow(fit))) selection_summary <- cp_out |> group_by(gene) |> summarize( `Neighborhood size` = sum(inside_cs), `% of cells` = round(sum(inside_cs) / ncol(res$fit_lemur), 2) * 100, .groups = "drop" ) kable(selection_summary, caption = "Conformal selection: neighborhood size per gene" ) ``` ## Conformal clustering Conformal clustering instead reports, per cell, a *prediction set*: the neighborhoods that cell could plausibly belong to at the target coverage. A prediction set of size zero or two means the procedure could not confidently assign that cell to any neighborhood. It is ambiguous. ```{r clustering-summary} clustering_summary <- cp_out |> group_by(gene) |> summarize( `Prediction set = inside` = sum(inside_cc + outside_cc == 1 & inside_cc == 1), `Prediction set = outside` = sum(inside_cc + outside_cc == 1 & outside_cc == 1), `Prediction set size 2 or empty` = sum(inside_cc + outside_cc != 1), .groups = "drop" ) kable(clustering_summary, caption = "Conformal clustering: prediction set composition per gene" ) ``` ## Visualizing results on the LEMUR embedding ### Conformal selection ```{r plot-selection, fig.cap = "Conformal selection overlaid on LEMUR"} plotter_conformal_selection(res, pan_affect_genes$gene) ``` Cells shaded as part of the selected neighborhood are those the procedure can confidently attribute to the condition effect for that gene; unshaded cells fall outside the guaranteed selection at the chosen coverage level. ### Conformal clustering ```{r plot-clustering, fig.cap = "Conformal prediction sets overlaid on LEMUR"} plotter_conformal_clustering(res, pan_affect_genes$gene) ``` Here color reflects the prediction set itself rather than a single hard assignment, making it easy to spot the boundary regions where neighborhood membership is genuinely uncertain versus the core regions where every resampling agrees. # Session info ```{r session-info} sessionInfo() ```