Chapter 4 Human PBMC with surface proteins (10X Genomics) | Single-Cell Analysis Workflows with Bioconductor

# Chapter 4 Human PBMC with surface proteins (10X Genomics)

## 4.1 Introduction

Here, we describe a brief analysis of yet another peripheral blood mononuclear cell (PBMC) dataset from 10X Genomics (Zheng et al. 2017). Data are publicly available from the 10X Genomics website, from which we download the filtered gene/barcode count matrices for gene expression and cell surface proteins.

## 4.2 Data loading

    library(BiocFileCache)
    bfc <- BiocFileCache(ask=FALSE)
    exprs.data <- bfcrpath(bfc, file.path(
        "http://cf.10xgenomics.com/samples/cell-vdj/3.1.0",
        "vdj_v1_hs_pbmc3",
        "vdj_v1_hs_pbmc3_filtered_feature_bc_matrix.tar.gz"))
    untar(exprs.data, exdir=tempdir())

    library(DropletUtils)
    sce.pbmc <- read10xCounts(file.path(tempdir(), "filtered_feature_bc_matrix"))
    sce.pbmc <- splitAltExps(sce.pbmc, rowData(sce.pbmc)$Type)

## 4.3 Quality control

    unfiltered <- sce.pbmc

We discard cells with high mitochondrial proportions and few detectable ADT counts.

    library(scater)
    is.mito <- grep("^MT-", rowData(sce.pbmc)$Symbol)
    stats <- perCellQCMetrics(sce.pbmc, subsets=list(Mito=is.mito))

    high.mito <- isOutlier(stats$subsets_Mito_percent, type="higher")
    low.adt <- stats$`altexps_Antibody Capture_detected` < nrow(altExp(sce.pbmc))/2

    discard <- high.mito | low.adt
    sce.pbmc <- sce.pbmc[,!discard]

We examine some of the statistics:

    summary(high.mito)

    ##    Mode   FALSE    TRUE
    ## logical    6660     571

    summary(low.adt)

    ##    Mode   FALSE
    ## logical    7231

    summary(discard)

    ##    Mode   FALSE    TRUE
    ## logical    6660     571

We examine the distribution of each QC metric (Figure 4.1).

    colData(unfiltered) <- cbind(colData(unfiltered), stats)
    unfiltered$discard <- discard

    gridExtra::grid.arrange(
        plotColData(unfiltered, y="sum", colour_by="discard") +
            scale_y_log10() + ggtitle("Total count"),
        plotColData(unfiltered, y="detected", colour_by="discard") +
            scale_y_log10() + ggtitle("Detected features"),
        plotColData(unfiltered, y="subsets_Mito_percent",
            colour_by="discard") + ggtitle("Mito percent"),
        plotColData(unfiltered, y="altexps_Antibody Capture_detected",
            colour_by="discard") + ggtitle("ADT detected"),
        ncol=2
    )

Figure 4.1: Distribution of each QC metric in the PBMC dataset, where each point is a cell and is colored by whether or not it was discarded by the outlier-based QC approach.

We also plot the mitochondrial proportion against the total count for each cell, as one does (Figure 4.2).

    plotColData(unfiltered, x="sum", y="subsets_Mito_percent",
        colour_by="discard") + scale_x_log10()

Figure 4.2: Percentage of UMIs mapped to mitochondrial genes against the totalcount for each cell.

## 4.4 Normalization

Computing size factors for the gene expression and ADT counts.

    library(scran)

    set.seed(1000)
    clusters <- quickCluster(sce.pbmc)
    sce.pbmc <- computeSumFactors(sce.pbmc, cluster=clusters)
    altExp(sce.pbmc) <- computeMedianFactors(altExp(sce.pbmc))
    sce.pbmc <- applySCE(sce.pbmc, logNormCounts)

We generate some summary statistics for both sets of size factors:

    summary(sizeFactors(sce.pbmc))

    ##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
    ##   0.074   0.717   0.909   1.000   1.128   9.100

    summary(sizeFactors(altExp(sce.pbmc)))

    ##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
    ##   0.101   0.698   0.828   1.000   1.028 227.356

We also look at the distribution of size factors compared to the library size for each set of features (Figure 4.3).

    par(mfrow=c(1,2))
    plot(librarySizeFactors(sce.pbmc), sizeFactors(sce.pbmc), pch=16,
        xlab="Library size factors", ylab="Deconvolution factors",
        main="Gene expression", log="xy")
    plot(librarySizeFactors(altExp(sce.pbmc)), sizeFactors(altExp(sce.pbmc)), pch=16,
        xlab="Library size factors", ylab="Median-based factors",
        main="Antibody capture", log="xy")

Figure 4.3: Plot of the deconvolution size factors for the gene expression values (left) or the median-based size factors for the ADT expression values (right) compared to the library size-derived factors for the corresponding set of features. Each point represents a cell.

## 4.5 Dimensionality reduction

We omit the PCA step for the ADT expression matrix, given that it is already so low-dimensional, and progress directly to \(t\)-SNE and UMAP visualizations.

    set.seed(100000)
    altExp(sce.pbmc) <- runTSNE(altExp(sce.pbmc))

    set.seed(1000000)
    altExp(sce.pbmc) <- runUMAP(altExp(sce.pbmc))

## 4.6 Clustering

We perform graph-based clustering on the ADT data and use the assignments as the column labels of the alternative Experiment.

    g.adt <- buildSNNGraph(altExp(sce.pbmc), k=10, d=NA)
    clust.adt <- igraph::cluster_walktrap(g.adt)$membership
    colLabels(altExp(sce.pbmc)) <- factor(clust.adt)

We examine some basic statistics about the size of each cluster, their separation (Figure 4.4) and their distribution in our \(t\)-SNE plot (Figure 4.5).

    table(colLabels(altExp(sce.pbmc)))

    ##
    ##    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16
    ##  160  507  662   39  691 1415   32  650   76 1037  121   47   68   25   15  562
    ##   17   18   19   20   21   22   23   24
    ##  139   32   44  120   84   65   52   17

    library(bluster)
    mod <- pairwiseModularity(g.adt, clust.adt, as.ratio=TRUE)

    library(pheatmap)
    pheatmap::pheatmap(log10(mod + 10), cluster_row=FALSE, cluster_col=FALSE,
        color=colorRampPalette(c("white", "blue"))(101))

Figure 4.4: Heatmap of the pairwise cluster modularity scores in the PBMC dataset, computed based on the shared nearest neighbor graph derived from the ADT expression values.

    plotTSNE(altExp(sce.pbmc), colour_by="label", text_by="label", text_colour="red")

Figure 4.5: Obligatory \(t\)-SNE plot of PBMC dataset based on its ADT expression values, where each point is a cell and is colored by the cluster of origin. Cluster labels are also overlaid at the median coordinates across all cells in the cluster.

We perform some additional subclustering using the expression data to mimic an in silico FACS experiment.

    set.seed(1010010)
    subclusters <- quickSubCluster(sce.pbmc, clust.adt,
        prepFUN=function(x) {
            dec <- modelGeneVarByPoisson(x)
            top <- getTopHVGs(dec, prop=0.1)
            denoisePCA(x, dec, subset.row=top)
        },
        clusterFUN=function(x) {
            g.gene <- buildSNNGraph(x, k=10, use.dimred = 'PCA')
            igraph::cluster_walktrap(g.gene)$membership
        }
    )

We counting the number of gene expression-derived subclusters in each ADT-derived parent cluster.

    data.frame(
        Cluster=names(subclusters),
        Ncells=vapply(subclusters, ncol, 0L),
        Nsub=vapply(subclusters, function(x) length(unique(x$subcluster)), 0L)
    )

    ##    Cluster Ncells Nsub
    ## 1        1    160    3
    ## 2        2    507    4
    ## 3        3    662    5
    ## 4        4     39    1
    ## 5        5    691    5
    ## 6        6   1415    8
    ## 7        7     32    1
    ## 8        8    650    8
    ## 9        9     76    2
    ## 10      10   1037    9
    ## 11      11    121    3
    ## 12      12     47    1
    ## 13      13     68    3
    ## 14      14     25    1
    ## 15      15     15    1
    ## 16      16    562    6
    ## 17      17    139    3
    ## 18      18     32    1
    ## 19      19     44    1
    ## 20      20    120    4
    ## 21      21     84    2
    ## 22      22     65    2
    ## 23      23     52    2
    ## 24      24     17    1

## Session Info

    R version 4.5.1 Patched (2025-08-23 r88802)
    Platform: x86_64-pc-linux-gnu
    Running under: Ubuntu 24.04.3 LTS

    Matrix products: default
    BLAS:   /home/biocbuild/bbs-3.22-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] pheatmap_1.0.13             bluster_1.20.0
     [3] scran_1.38.0                scater_1.38.0
     [5] ggplot2_4.0.0               scuttle_1.20.0
     [7] DropletUtils_1.30.0         SingleCellExperiment_1.32.0
     [9] SummarizedExperiment_1.40.0 Biobase_2.70.0
    [11] GenomicRanges_1.62.0        Seqinfo_1.0.0
    [13] IRanges_2.44.0              S4Vectors_0.48.0
    [15] BiocGenerics_0.56.0         generics_0.1.4
    [17] MatrixGenerics_1.22.0       matrixStats_1.5.0
    [19] BiocFileCache_3.0.0         dbplyr_2.5.1
    [21] BiocStyle_2.38.0            rebook_1.20.0

    loaded via a namespace (and not attached):
     [1] DBI_1.2.3                 gridExtra_2.3
     [3] httr2_1.2.1               CodeDepends_0.6.6
     [5] rlang_1.1.6               magrittr_2.0.4
     [7] RcppAnnoy_0.0.22          compiler_4.5.1
     [9] RSQLite_2.4.3             dir.expiry_1.18.0
    [11] DelayedMatrixStats_1.32.0 vctrs_0.6.5
    [13] pkgconfig_2.0.3           fastmap_1.2.0
    [15] XVector_0.50.0            labeling_0.4.3
    [17] rmarkdown_2.30            graph_1.88.0
    [19] ggbeeswarm_0.7.2          purrr_1.1.0
    [21] bit_4.6.0                 xfun_0.54
    [23] cachem_1.1.0              beachmat_2.26.0
    [25] jsonlite_2.0.0            blob_1.2.4
    [27] rhdf5filters_1.22.0       DelayedArray_0.36.0
    [29] Rhdf5lib_1.32.0           BiocParallel_1.44.0
    [31] cluster_2.1.8.1           irlba_2.3.5.1
    [33] parallel_4.5.1            R6_2.6.1
    [35] bslib_0.9.0               RColorBrewer_1.1-3
    [37] limma_3.66.0              jquerylib_0.1.4
    [39] Rcpp_1.1.0                bookdown_0.45
    [41] knitr_1.50                R.utils_2.13.0
    [43] igraph_2.2.1              Matrix_1.7-4
    [45] tidyselect_1.2.1          viridis_0.6.5
    [47] dichromat_2.0-0.1         abind_1.4-8
    [49] yaml_2.3.10               codetools_0.2-20
    [51] curl_7.0.0                lattice_0.22-7
    [53] tibble_3.3.0              withr_3.0.2
    [55] S7_0.2.0                  Rtsne_0.17
    [57] evaluate_1.0.5            pillar_1.11.1
    [59] BiocManager_1.30.26       filelock_1.0.3
    [61] sparseMatrixStats_1.22.0  scales_1.4.0
    [63] glue_1.8.0                metapod_1.18.0
    [65] tools_4.5.1               BiocNeighbors_2.4.0
    [67] ScaledMatrix_1.18.0       locfit_1.5-9.12
    [69] XML_3.99-0.19             cowplot_1.2.0
    [71] rhdf5_2.54.0              grid_4.5.1
    [73] edgeR_4.8.0               beeswarm_0.4.0
    [75] BiocSingular_1.26.0       HDF5Array_1.38.0
    [77] vipor_0.4.7               cli_3.6.5
    [79] rsvd_1.0.5                rappdirs_0.3.3
    [81] viridisLite_0.4.2         S4Arrays_1.10.0
    [83] dplyr_1.1.4               uwot_0.2.3
    [85] gtable_0.3.6              R.methodsS3_1.8.2
    [87] sass_0.4.10               digest_0.6.37
    [89] SparseArray_1.10.1        ggrepel_0.9.6
    [91] dqrng_0.4.1               farver_2.1.2
    [93] memoise_2.0.1             htmltools_0.5.8.1
    [95] R.oo_1.27.1               lifecycle_1.0.4
    [97] h5mread_1.2.0             statmod_1.5.1
    [99] bit64_4.6.0-1

### References

Zheng, G. X., J. M. Terry, P. Belgrader, P. Ryvkin, Z. W. Bent, R. Wilson, S. B. Ziraldo, et al. 2017. “Massively parallel digital transcriptional profiling of single cells.” Nat Commun 8 (January): 14049.