# Recovering intra-sample doublets

# tl;dr

See the relevant section of the OSCA book for an example of the `recoverDoublets()` function in action on real data. A toy example is also provided in `?recoverDoublets`.

# Mathematical background

Consider any two cell states \(C_1\) and \(C_2\) forming a doublet population \(D_{12}\). We will focus on the relative frequency of inter-sample to intra-sample doublets in \(D_{12}\). Given a vector \(\vec p_X\) containing the proportion of cells from each sample in state \(X\), and assuming that doublets form randomly between pairs of samples, the expected proportion of intra-sample doublets in \(D_{12}\) is \(\vec p_{C_1} \cdot \vec p_{C_2}\). Subtracting this from 1 gives us the expected proportion of inter-sample doublets \(q_{D_{12}}\). Similarly, the expected proportion of inter-sample doublets in \(C_1\) is just \(q_{C_1} =1 - \| \vec p_{C_1} \|_2^2\).

Now, let’s consider the observed proportion of events \(r_X\) in each state \(X\) that are known doublets. We have \(r_{D_{12}} = q_{D_{12}}\) as there are no other events in \(D_{12}\) beyond actual doublets. On the other hand, we expect that \(r_{C_1} \ll q_{C_1}\) due to presence of a large majority of non-doublet cells in \(C_1\) (same for \(C_2\)). If we assume that \(q_{D_{12}} \ge q_{C_1}\) and \(q_{C_2}\), the observed proportion \(r_{D_{12}}\) should be larger than \(r_{C_1}\) and \(r_{C_2}\). (The last assumption is not always true but the \(\ll\) should give us enough wiggle room to be robust to violations.)

The above reasoning motivates the use of the proportion of known doublet neighbors as a “doublet score” to identify events that are most likely to be themselves doublets. `recoverDoublets()` computes the proportion of known doublet neighbors for each cell by performing a \(k\)-nearest neighbor search against all other cells in the dataset. It is then straightforward to calculate the proportion of neighboring cells that are marked as known doublets, representing our estimate of \(r_X\) for each cell.

# Obtaining explicit calls

While the proportions are informative, there comes a time when we need to convert these into explicit doublet calls. This is achieved with \(\vec S\), the vector of the proportion of cells from each sample across the entire dataset (i.e., `samples`). We assume that all cell states contributing to doublet states have proportion vectors equal to \(\vec S\), such that the expected proportion of doublets that occur between cells from the same sample is \(\| \vec S\|_2^2\). We then solve

\[ \frac{N_{intra}}{(N_{intra} + N_{inter}} = \| \vec S\|_2^2 \]

for \(N_{intra}\), where \(N_{inter}\) is the number of observed inter-sample doublets. The top \(N_{intra}\) events with the highest scores (and, obviously, are not already inter-sample doublets) are marked as putative intra-sample doublets.

# Discussion

The rate and manner of doublet formation is (mostly) irrelevant as we condition on the number of events in \(D_{12}\). This means that we do not have to make any assumptions about the relative likelihood of doublets forming between pairs of cell types, especially when cell types have different levels of “stickiness” (or worse, stick specifically to certain other cell types). Such convenience is only possible because of the known doublet calls that allow us to focus on the inter- to intra-sample ratio.

The most problematic assumption is that required to obtain \(N_{intra}\) from \(\vec S\). Obtaining a better estimate would require, at least, the knowledge of the two parent states for each doublet population. This can be determined with some simulation-based heuristics but it is likely to be more trouble than it is worth.

In this theoretical framework, we can easily spot a case where our method fails. If both \(C_1\) and \(C_2\) are unique to a given sample, all events in \(D_{12}\) will be intra-sample doublets. This means that no events in \(D_{12}\) will ever be detected as inter-sample doublets, which precludes their detection as intra-sample doublets by `recoverDoublets`. The computational remedy is to augment the predictions with simulation-based methods (e.g., `scDblFinder()`) while the experimental remedy is to ensure that multiplexed samples include technical or biological replicates.

# Session information

    sessionInfo()

    ## R version 4.5.2 (2025-10-31)
    ## Platform: x86_64-pc-linux-gnu
    ## Running under: Ubuntu 24.04.3 LTS
    ##
    ## Matrix products: default
    ## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
    ## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
    ##
    ## locale:
    ##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
    ##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=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: Etc/UTC
    ## tzcode source: system (glibc)
    ##
    ## attached base packages:
    ## [1] stats4    stats     graphics  grDevices utils     datasets  methods
    ## [8] base
    ##
    ## other attached packages:
    ##  [1] bluster_1.20.0              scDblFinder_1.24.0
    ##  [3] scater_1.38.0               ggplot2_4.0.1
    ##  [5] scran_1.38.0                scuttle_1.20.0
    ##  [7] ensembldb_2.34.0            AnnotationFilter_1.34.0
    ##  [9] GenomicFeatures_1.62.0      AnnotationDbi_1.72.0
    ## [11] scRNAseq_2.24.0             SingleCellExperiment_1.32.0
    ## [13] SummarizedExperiment_1.40.0 Biobase_2.70.0
    ## [15] GenomicRanges_1.62.1        Seqinfo_1.0.0
    ## [17] IRanges_2.44.0              S4Vectors_0.48.0
    ## [19] BiocGenerics_0.56.0         generics_0.1.4
    ## [21] MatrixGenerics_1.22.0       matrixStats_1.5.0
    ## [23] BiocStyle_2.38.0
    ##
    ## loaded via a namespace (and not attached):
    ##   [1] RColorBrewer_1.1-3       sys_3.4.3                jsonlite_2.0.0
    ##   [4] magrittr_2.0.4           ggbeeswarm_0.7.3         gypsum_1.6.0
    ##   [7] farver_2.1.2             rmarkdown_2.30           BiocIO_1.20.0
    ##  [10] vctrs_0.7.1              memoise_2.0.1            Rsamtools_2.26.0
    ##  [13] RCurl_1.98-1.17          htmltools_0.5.9          S4Arrays_1.10.1
    ##  [16] AnnotationHub_4.0.0      curl_7.0.0               BiocNeighbors_2.4.0
    ##  [19] xgboost_3.1.3.1          Rhdf5lib_1.32.0          SparseArray_1.10.8
    ##  [22] rhdf5_2.54.1             sass_0.4.10              alabaster.base_1.10.0
    ##  [25] bslib_0.10.0             alabaster.sce_1.10.0     httr2_1.2.2
    ##  [28] cachem_1.1.0             buildtools_1.0.0         GenomicAlignments_1.46.0
    ##  [31] igraph_2.2.1             lifecycle_1.0.5          pkgconfig_2.0.3
    ##  [34] rsvd_1.0.5               Matrix_1.7-4             R6_2.6.1
    ##  [37] fastmap_1.2.0            digest_0.6.39            dqrng_0.4.1
    ##  [40] irlba_2.3.5.1            ExperimentHub_3.0.0      RSQLite_2.4.5
    ##  [43] beachmat_2.26.0          labeling_0.4.3           filelock_1.0.3
    ##  [46] httr_1.4.7               abind_1.4-8              compiler_4.5.2
    ##  [49] bit64_4.6.0-1            withr_3.0.2              S7_0.2.1
    ##  [52] BiocParallel_1.44.0      viridis_0.6.5            DBI_1.2.3
    ##  [55] HDF5Array_1.38.0         alabaster.ranges_1.10.0  alabaster.schemas_1.10.0
    ##  [58] MASS_7.3-65              rappdirs_0.3.4           DelayedArray_0.36.0
    ##  [61] rjson_0.2.23             tools_4.5.2              vipor_0.4.7
    ##  [64] beeswarm_0.4.0           glue_1.8.0               h5mread_1.2.1
    ##  [67] restfulr_0.0.16          rhdf5filters_1.22.0      grid_4.5.2
    ##  [70] Rtsne_0.17               cluster_2.1.8.1          gtable_0.3.6
    ##  [73] data.table_1.18.0        BiocSingular_1.26.1      ScaledMatrix_1.18.0
    ##  [76] metapod_1.18.0           XVector_0.50.0           ggrepel_0.9.6
    ##  [79] BiocVersion_3.22.0       pillar_1.11.1            limma_3.66.0
    ##  [82] dplyr_1.1.4              BiocFileCache_3.0.0      lattice_0.22-7
    ##  [85] rtracklayer_1.70.1       bit_4.6.0                tidyselect_1.2.1
    ##  [88] locfit_1.5-9.12          maketools_1.3.2          Biostrings_2.78.0
    ##  [91] knitr_1.51               gridExtra_2.3            ProtGenerics_1.42.0
    ##  [94] edgeR_4.8.2              xfun_0.56                statmod_1.5.1
    ##  [97] UCSC.utils_1.6.1         lazyeval_0.2.2           yaml_2.3.12
    ## [100] evaluate_1.0.5           codetools_0.2-20         cigarillo_1.0.0
    ## [103] tibble_3.3.1             alabaster.matrix_1.10.0  BiocManager_1.30.27
    ## [106] cli_3.6.5                jquerylib_0.1.4          Rcpp_1.1.1
    ## [109] GenomeInfoDb_1.46.2      dbplyr_2.5.1             png_0.1-8
    ## [112] XML_3.99-0.20            parallel_4.5.2           blob_1.3.0
    ## [115] bitops_1.0-9             viridisLite_0.4.2        alabaster.se_1.10.0
    ## [118] scales_1.4.0             purrr_1.2.1              crayon_1.5.3
    ## [121] rlang_1.1.7              KEGGREST_1.50.0