Contents

1 Overview

scCertify provides an explainable framework for evaluating confidence in single-cell RNA-sequencing cell-type annotations.

The package combines several complementary sources of evidence:

scCertify supports both SingleCellExperiment and Seurat objects.

library(scCertify)
library(SingleCellExperiment)
## Loading required package: SummarizedExperiment
## Loading required package: MatrixGenerics
## Loading required package: matrixStats
## 
## Attaching package: 'MatrixGenerics'
## The following objects are masked from 'package:matrixStats':
## 
##     colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
##     colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
##     colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
##     colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
##     colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
##     colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
##     colWeightedMeans, colWeightedMedians, colWeightedSds,
##     colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
##     rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
##     rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
##     rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
##     rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
##     rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
##     rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
##     rowWeightedSds, rowWeightedVars
## Loading required package: GenomicRanges
## Loading required package: stats4
## Loading required package: BiocGenerics
## Loading required package: generics
## 
## Attaching package: 'generics'
## The following objects are masked from 'package:base':
## 
##     as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
##     setequal, union
## 
## Attaching package: 'BiocGenerics'
## The following objects are masked from 'package:stats':
## 
##     IQR, mad, sd, var, xtabs
## The following object is masked from 'package:utils':
## 
##     data
## The following objects are masked from 'package:base':
## 
##     Filter, Find, Map, Position, Reduce, anyDuplicated, aperm, append,
##     as.data.frame, basename, cbind, colnames, dirname, do.call,
##     duplicated, eval, evalq, get, grep, grepl, is.unsorted, lapply,
##     mapply, match, mget, order, paste, pmax, pmax.int, pmin, pmin.int,
##     rank, rbind, rownames, sapply, saveRDS, scale, sequence, table,
##     tapply, transform, unique, unsplit, which.max, which.min
## Loading required package: S4Vectors
## 
## Attaching package: 'S4Vectors'
## The following object is masked from 'package:utils':
## 
##     findMatches
## The following objects are masked from 'package:base':
## 
##     I, expand.grid, unname
## Loading required package: IRanges
## Loading required package: Seqinfo
## Loading required package: Biobase
## Welcome to Bioconductor
## 
##     Vignettes contain introductory material; view with
##     'browseVignettes()'. To cite Bioconductor, see
##     'citation("Biobase")', and for packages 'citation("pkgname")'.
## 
## Attaching package: 'Biobase'
## The following object is masked from 'package:MatrixGenerics':
## 
##     rowMedians
## The following objects are masked from 'package:matrixStats':
## 
##     anyMissing, rowMedians
library(SummarizedExperiment)
library(Seurat)
## Loading required package: SeuratObject
## Loading required package: sp
## 
## Attaching package: 'sp'
## The following object is masked from 'package:IRanges':
## 
##     %over%
## 'SeuratObject' was built with package 'Matrix' 1.7.5 but the current
## version is 1.7.6; it is recomended that you reinstall 'SeuratObject' as
## the ABI for 'Matrix' may have changed
## 
## Attaching package: 'SeuratObject'
## The following object is masked from 'package:SummarizedExperiment':
## 
##     Assays
## The following object is masked from 'package:GenomicRanges':
## 
##     intersect
## The following object is masked from 'package:Seqinfo':
## 
##     intersect
## The following object is masked from 'package:IRanges':
## 
##     intersect
## The following object is masked from 'package:S4Vectors':
## 
##     intersect
## The following object is masked from 'package:BiocGenerics':
## 
##     intersect
## The following objects are masked from 'package:base':
## 
##     intersect, t
## 
## Attaching package: 'Seurat'
## The following object is masked from 'package:SummarizedExperiment':
## 
##     Assays
set.seed(42)

2 Input Data

The following example creates a small simulated SingleCellExperiment containing count data and predicted cell-type labels.

n_genes <- 50
n_cells <- 40

counts <- matrix(
    rpois(n_genes * n_cells, lambda = 5),
    nrow = n_genes,
    ncol = n_cells,
    dimnames = list(
        paste0("Gene", seq_len(n_genes)),
        paste0("Cell", seq_len(n_cells))
    )
)

sce <- SingleCellExperiment(
    assays = list(
        counts = counts
    )
)

colData(sce)$predicted_label <- rep(
    c("T cell", "B cell"),
    each = n_cells / 2
)

sce
## class: SingleCellExperiment 
## dim: 50 40 
## metadata(0):
## assays(1): counts
## rownames(50): Gene1 Gene2 ... Gene49 Gene50
## rowData names(0):
## colnames(40): Cell1 Cell2 ... Cell39 Cell40
## colData names(1): predicted_label
## reducedDimNames(0):
## mainExpName: NULL
## altExpNames(0):

The predicted cell labels are stored in colData().

head(colData(sce))
## DataFrame with 6 rows and 1 column
##       predicted_label
##           <character>
## Cell1          T cell
## Cell2          T cell
## Cell3          T cell
## Cell4          T cell
## Cell5          T cell
## Cell6          T cell

3 Marker Gene Definitions

A marker database is represented as a named list, with one element for each predicted cell type.

markers <- list(
    "T cell" = c("Gene1", "Gene2", "Gene3"),
    "B cell" = c("Gene4", "Gene5", "Gene6")
)

markers
## $`T cell`
## [1] "Gene1" "Gene2" "Gene3"
## 
## $`B cell`
## [1] "Gene4" "Gene5" "Gene6"

4 Neighborhood Representation

neighbor_score() evaluates agreement among nearby cells. For a SingleCellExperiment, a suitable low-dimensional representation is required.

For this simulated example, a logcounts assay is created from a log-transformed count matrix. marker_score() requires this assay to be present, and the same log-transformed matrix is also used to calculate PCA.

log_counts <- log1p(
    assay(sce, "counts")
)

assay(sce, "logcounts") <- log_counts

pca <- stats::prcomp(
    t(log_counts),
    center = TRUE,
    scale. = TRUE
)$x[, seq_len(5), drop = FALSE]

reducedDim(sce, "PCA") <- pca

dim(
    reducedDim(sce, "PCA")
)
## [1] 40  5

5 Marker Consistency

Marker consistency evaluates whether cells express genes expected for their predicted identity.

scCertify uses rank-based enrichment scoring through UCell.

marker_scores <- marker_score(
    sce,
    markers,
    label_column = "predicted_label"
)

head(marker_scores)
## [1] 0.6423611 0.4270833 0.3055556 0.6701389 0.7951389 0.3784722

The resulting scores provide a cell-level measure of marker consistency.

6 Neighborhood Agreement

The neighborhood score measures whether nearby cells tend to share the same predicted annotation.

neighbor_scores <- neighbor_score(
    sce,
    label_column = "predicted_label"
)

head(neighbor_scores)
## [1] 0.4 0.7 0.7 0.2 0.5 0.5

7 Entropy-Based Uncertainty

entropy_score() provides an uncertainty measure based on the distribution of expression values.

entropy_values <- entropy_score(
    t(assay(sce, "counts"))
)

head(entropy_values)
##    Cell1    Cell2    Cell3    Cell4    Cell5    Cell6 
## 3.799701 3.783424 3.811471 3.831461 3.809691 3.796189

The values can be normalized and stored in the cell metadata.

entropy_norm <- entropy_values / max(
    entropy_values,
    na.rm = TRUE
)

colData(sce)$entropy_norm <- entropy_norm

head(colData(sce)$entropy_norm)
##     Cell1     Cell2     Cell3     Cell4     Cell5     Cell6 
## 0.9849849 0.9807655 0.9880360 0.9932180 0.9875746 0.9840745

8 Doublet Information

For this simulated example, doublet scores are generated as demonstration metadata.

In a real analysis, these values would normally come from a dedicated doublet detection method.

colData(sce)$doublet_score <- runif(
    ncol(sce),
    min = 0,
    max = 1
)

head(colData(sce)$doublet_score)
## [1] 0.9899656 0.4384936 0.6999032 0.8890770 0.8341595 0.7344215

9 Cell Certification

cell_certify() combines marker consistency, neighborhood agreement, entropy-based uncertainty, and doublet information into a calibrated confidence score.

sce <- cell_certify(
    sce,
    markers,
    label_column = "predicted_label"
)

head(
    colData(sce)[,
        c(
            "predicted_label",
            "marker_score",
            "neighbor_score",
            "entropy_norm",
            "doublet_score",
            "confidence_score",
            "confidence_class"
        )
    ]
)
## DataFrame with 6 rows and 7 columns
##       predicted_label marker_score neighbor_score entropy_norm doublet_score
##           <character>    <numeric>      <numeric>    <numeric>     <numeric>
## Cell1          T cell     0.642361            0.4     0.984985      0.989966
## Cell2          T cell     0.427083            0.7     0.980766      0.438494
## Cell3          T cell     0.305556            0.7     0.988036      0.699903
## Cell4          T cell     0.670139            0.2     0.993218      0.889077
## Cell5          T cell     0.795139            0.5     0.987575      0.834159
## Cell6          T cell     0.378472            0.5     0.984074      0.734421
##       confidence_score confidence_class
##              <numeric>      <character>
## Cell1            0.450              Low
## Cell2            0.850             High
## Cell3            0.575         Moderate
## Cell4            0.075              Low
## Cell5            0.725         Moderate
## Cell6            0.325              Low

10 Custom Confidence Thresholds

Confidence classes can be customized using the moderate_threshold and high_threshold arguments.

sce_custom <- cell_certify(
    sce,
    markers,
    label_column = "predicted_label",
    moderate_threshold = 0.4,
    high_threshold = 0.75
)

head(
    colData(sce_custom)[,
        c(
            "confidence_score",
            "confidence_class"
        )
    ]
)
## DataFrame with 6 rows and 2 columns
##       confidence_score confidence_class
##              <numeric>      <character>
## Cell1            0.450         Moderate
## Cell2            0.850             High
## Cell3            0.575         Moderate
## Cell4            0.075              Low
## Cell5            0.725         Moderate
## Cell6            0.325              Low

11 Explaining Confidence

explain_confidence() identifies factors that contributed to lower confidence for an individual cell.

explain_confidence(
    sce,
    cell_id = "Cell1"
)
## [1] "Neighborhood disagreement"      "High annotation uncertainty"   
## [3] "Possible doublet contamination"

explain_cell() provides a concise summary containing the predicted label, confidence score, individual evidence components, and interpretation.

explain_cell(
    sce,
    cell_id = "Cell1"
)
## Cell: Cell1
## Predicted Label: T cell
## Confidence Score: 0.45
## Marker Score: 0.642
## Neighbor Agreement: 0.4
## Entropy: 0.985
## Interpretation:
## - Moderate confidence annotation
## - High uncertainty detected
## - Local neighborhood disagreement
## [1] "Moderate confidence annotation"  "High uncertainty detected"      
## [3] "Local neighborhood disagreement"

12 Seurat Interoperability

scCertify also supports Seurat objects. The following example demonstrates a complete evaluated Seurat workflow.

seurat_counts <- matrix(
    rpois(2000, lambda = 5),
    nrow = 100,
    ncol = 20,
    dimnames = list(
        paste0("Gene", seq_len(100)),
        paste0("Cell", seq_len(20))
    )
)

seurat_object <- Seurat::CreateSeuratObject(
    counts = seurat_counts
)
## Warning: Data is of class matrix. Coercing to dgCMatrix.
seurat_object$predicted_label <- rep(
    c("T cell", "B cell"),
    each = 10
)

seurat_object$entropy_norm <- runif(
    ncol(seurat_object)
)

seurat_object$doublet_score <- runif(
    ncol(seurat_object)
)

seurat_object
## An object of class Seurat 
## 100 features across 20 samples within 1 assay 
## Active assay: RNA (100 features, 0 variable features)
##  1 layer present: counts

The Seurat object is processed using its standard normalization and PCA workflow.

seurat_object <- Seurat::NormalizeData(
    seurat_object,
    verbose = FALSE
)

seurat_object <- Seurat::FindVariableFeatures(
    seurat_object,
    verbose = FALSE
)

seurat_object <- Seurat::ScaleData(
    seurat_object,
    verbose = FALSE
)

seurat_object <- Seurat::RunPCA(
    seurat_object,
    npcs = 10,
    verbose = FALSE
)
## Warning in svd.function(A = t(x = object), nv = npcs, ...): You're computing
## too large a percentage of total singular values, use a standard svd instead.
seurat_object
## An object of class Seurat 
## 100 features across 20 samples within 1 assay 
## Active assay: RNA (100 features, 100 variable features)
##  3 layers present: counts, data, scale.data
##  1 dimensional reduction calculated: pca

The same marker definitions can then be used with scCertify.

seurat_object <- cell_certify(
    seurat_object,
    markers,
    label_column = "predicted_label"
)

head(
    seurat_object[[
        c(
            "predicted_label",
            "marker_score",
            "neighbor_score",
            "entropy_norm",
            "doublet_score",
            "confidence_score",
            "confidence_class"
        )
    ]]
)
##       predicted_label marker_score neighbor_score entropy_norm doublet_score
## Cell1          T cell    0.4812925            0.7    0.6756233  0.4636337464
## Cell2          T cell    0.7159864            0.5    0.3624238  0.9391432542
## Cell3          T cell    0.3622449            0.4    0.3981606  0.3398535226
## Cell4          T cell    0.6683673            0.5    0.2449258  0.0003825743
## Cell5          T cell    0.7993197            0.4    0.1579488  0.2173832541
## Cell6          T cell    0.3945578            0.5    0.5307416  0.0360659512
##       confidence_score confidence_class
## Cell1             0.85             High
## Cell2             0.70         Moderate
## Cell3             0.15              Low
## Cell4             0.95             High
## Cell5             0.80             High
## Cell6             0.40              Low

The resulting confidence information is stored directly in the Seurat metadata.

explain_confidence(
    seurat_object,
    cell_id = "Cell1"
)
## [1] "Strong annotation support"
explain_cell(
    seurat_object,
    cell_id = "Cell1"
)
## Cell: Cell1
## Predicted Label: T cell
## Confidence Score: 0.85
## Marker Score: 0.481
## Neighbor Agreement: 0.7
## Entropy: 0.676
## Interpretation:
## - High confidence annotation
## [1] "High confidence annotation"

13 Summary

This vignette demonstrates the complete scCertify workflow for both SingleCellExperiment and Seurat objects.

The workflow consists of:

  1. creating or loading an annotated single-cell object,
  2. supplying marker genes,
  3. calculating marker consistency,
  4. evaluating neighborhood agreement,
  5. estimating entropy-based uncertainty,
  6. incorporating doublet information,
  7. calculating calibrated confidence scores,
  8. assigning confidence classes, and
  9. explaining confidence for individual cells.

Because all examples in this vignette are evaluated, the document also provides a continuously tested demonstration that the documented workflow remains functional with the package dependencies used by scCertify.

14 Session Information

sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
## 
## Matrix products: default
## BLAS:   /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_GB              LC_COLLATE=C              
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: America/New_York
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats4    stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] Seurat_5.5.1                SeuratObject_5.4.0         
##  [3] sp_2.2-3                    SingleCellExperiment_1.35.2
##  [5] SummarizedExperiment_1.43.0 Biobase_2.73.2             
##  [7] GenomicRanges_1.65.4        Seqinfo_1.3.2              
##  [9] IRanges_2.47.5              S4Vectors_0.51.9           
## [11] BiocGenerics_0.59.12        generics_0.1.4             
## [13] MatrixGenerics_1.25.0       matrixStats_1.5.0          
## [15] scCertify_0.99.7            BiocStyle_2.41.0           
## 
## loaded via a namespace (and not attached):
##   [1] UCell_2.17.0           RColorBrewer_1.1-3     jsonlite_2.0.0        
##   [4] magrittr_2.0.5         spatstat.utils_3.2-4   farver_2.1.2          
##   [7] rmarkdown_2.32         vctrs_0.7.3            ROCR_1.0-12           
##  [10] spatstat.explore_3.8-2 htmltools_0.5.9        S4Arrays_1.13.0       
##  [13] BiocNeighbors_2.7.3    SparseArray_1.13.2     sass_0.4.10           
##  [16] sctransform_0.4.3      parallelly_1.48.0      KernSmooth_2.23-27    
##  [19] bslib_0.12.0           htmlwidgets_1.6.4      ica_1.0-3             
##  [22] plyr_1.8.9             plotly_4.12.1          zoo_1.9-0             
##  [25] cachem_1.1.0           igraph_2.3.3           mime_0.13             
##  [28] lifecycle_1.0.5        pkgconfig_2.0.3        Matrix_1.7-6          
##  [31] R6_2.6.1               fastmap_1.2.0          fitdistrplus_1.2-6    
##  [34] future_1.75.0          shiny_1.14.0           digest_0.6.39         
##  [37] patchwork_1.3.2        tensor_1.5.1           RSpectra_0.16-2       
##  [40] irlba_2.3.7            progressr_1.0.0        spatstat.sparse_3.2-0 
##  [43] httr_1.4.9             polyclip_1.10-7        abind_1.4-8           
##  [46] compiler_4.6.1         S7_0.2.2               BiocParallel_1.47.0   
##  [49] fastDummies_1.7.6      MASS_7.3-66            DelayedArray_0.39.6   
##  [52] tools_4.6.1            lmtest_0.9-40          otel_0.2.0            
##  [55] httpuv_1.6.17          future.apply_1.20.2    goftest_1.2-3         
##  [58] glue_1.8.1             nlme_3.1-171           promises_1.5.0        
##  [61] grid_4.6.1             Rtsne_0.17             cluster_2.1.8.3       
##  [64] reshape2_1.4.5         gtable_0.3.6           spatstat.data_3.1-9   
##  [67] tidyr_1.3.2            data.table_1.18.6.1    XVector_0.53.0        
##  [70] spatstat.geom_3.8-2    RcppAnnoy_0.0.23       ggrepel_0.9.8         
##  [73] RANN_2.6.3             pillar_1.11.1          stringr_1.6.0         
##  [76] spam_2.11-4            RcppHNSW_0.7.0         later_1.4.8           
##  [79] splines_4.6.1          dplyr_1.2.1            lattice_0.23-1        
##  [82] FNN_1.1.4.1            survival_3.8-11        deldir_2.0-4          
##  [85] tidyselect_1.2.1       miniUI_0.1.2           pbapply_1.7-5         
##  [88] knitr_1.52             gridExtra_2.3.1        bookdown_0.48         
##  [91] scattermore_1.2        xfun_0.60              stringi_1.8.9         
##  [94] yaml_2.3.12            evaluate_1.0.5         codetools_0.2-20      
##  [97] entropy_1.3.2          tibble_3.3.1           BiocManager_1.30.27   
## [100] cli_3.6.6              uwot_0.2.5             xtable_1.8-8          
## [103] reticulate_1.47.0      jquerylib_0.1.4        dichromat_2.0-1       
## [106] Rcpp_1.1.2             globals_0.19.1         spatstat.random_3.5-1 
## [109] png_0.1-9              spatstat.univar_3.2-0  parallel_4.6.1        
## [112] ggplot2_4.0.3          dotCall64_1.2          listenv_1.0.0         
## [115] viridisLite_0.4.3      scales_1.4.0           ggridges_0.5.7        
## [118] purrr_1.2.2            rlang_1.3.0            cowplot_1.2.0