Comparing totalVI and OSCA book CITE-seq analyses

Overview

This vignette endeavors to put Bioconductor and scvi-tools together to help understand how different data structures and methods relevant to CITE-seq analysis contribute to interpretation of CITE-seq exeperiments.

The scvi-tools tutorial (for version 0.20.0) analyzes a pair of 10x PBMC CITE-seq experiments (5k and 10k cells). Chapter 12 of the OSCA book analyzes only the 10k dataset.

Technical steps to facilitate comparison

The following subsections are essentially “code-only”. We exhibit steps necessary to assemble a SingleCellExperiment instance with the subset of the totalVI quantifications produced for the cells from the “10k” dataset.

Acquire software

library(SingleCellExperiment)
library(scater)
library(scviR)

Obtain key data representations

ch12sce = getCh12Sce(clear_cache=FALSE)
ch12sce
## class: SingleCellExperiment 
## dim: 33538 7472 
## metadata(2): Samples se.averaged
## assays(2): counts logcounts
## rownames(33538): ENSG00000243485 ENSG00000237613 ... ENSG00000277475
##   ENSG00000268674
## rowData names(3): ID Symbol Type
## colnames(7472): AAACCCAAGATTGTGA-1 AAACCCACATCGGTTA-1 ...
##   TTTGTTGTCGAGTGAG-1 TTTGTTGTCGTTCAGA-1
## colData names(3): Sample Barcode sizeFactor
## reducedDimNames(0):
## mainExpName: Gene Expression
## altExpNames(1): Antibody Capture
options(timeout=3600)
fullvi = getTotalVI5k10kAdata()
fullvi
## AnnData object with n_obs × n_vars = 10849 × 4000
##     obs: 'n_genes', 'percent_mito', 'n_counts', 'batch', '_scvi_labels', '_scvi_batch', 'leiden_totalVI'
##     var: 'highly_variable', 'highly_variable_rank', 'means', 'variances', 'variances_norm', 'highly_variable_nbatches'
##     uns: '_scvi_manager_uuid', '_scvi_uuid', 'hvg', 'leiden', 'log1p', 'neighbors', 'umap'
##     obsm: 'X_totalVI', 'X_umap', 'denoised_protein', 'protein_expression', 'protein_foreground_prob'
##     layers: 'counts', 'denoised_rna'
##     obsp: 'connectivities', 'distances'

Assemble a SingleCellExperiment with totalVI outputs

Acquire cell identities and batch labels

totvi_cellids = rownames(fullvi$obs)
totvi_batch = fullvi$obs$batch

Acquire quantifications and latent space positions

totvi_latent = fullvi$obsm$get("X_totalVI")
totvi_umap = fullvi$obsm$get("X_umap")
totvi_denoised_rna = fullvi$layers$get("denoised_rna")
totvi_denoised_protein = fullvi$obsm$get("denoised_protein")
totvi_leiden = fullvi$obs$leiden_totalVI

Drop 5k data from all

is5k = which(totvi_batch == "PBMC5k")
totvi_cellids = totvi_cellids[-is5k]
totvi_latent = totvi_latent[-is5k,]
totvi_umap = totvi_umap[-is5k,]
totvi_denoised_rna = totvi_denoised_rna[-is5k,]
totvi_denoised_protein = totvi_denoised_protein[-is5k,]
totvi_leiden = totvi_leiden[-is5k]

Label the rows of components

rownames(totvi_latent) = totvi_cellids
rownames(totvi_umap) = totvi_cellids
rownames(totvi_denoised_rna) = totvi_cellids
rownames(totvi_denoised_protein) = totvi_cellids
names(totvi_leiden) = totvi_cellids

Find common cell ids

In this section we reduce the cell collections to cells common to the chapter 12 and totalVI datasets.

comm = intersect(totvi_cellids, ch12sce$Barcode)

Build the totalVI SingleCellExperiment

# select and order
totvi_latent = totvi_latent[comm,]
totvi_umap = totvi_umap[comm,]
totvi_denoised_rna = totvi_denoised_rna[comm,]
totvi_denoised_protein = totvi_denoised_protein[comm,]
totvi_leiden = totvi_leiden[comm]
 
# organize the totalVI into SCE with altExp

totsce = SingleCellExperiment(SimpleList(logcounts=t(totvi_denoised_rna))) # FALSE name
rowData(totsce) = S4Vectors::DataFrame(fullvi$var)
rownames(totsce) = rownames(fullvi$var)
rowData(totsce)$Symbol = rownames(totsce)
nn = SingleCellExperiment(SimpleList(logcounts=t(totvi_denoised_protein))) # FALSE name
reducedDims(nn) = list(UMAP=totvi_umap)
altExp(totsce) = nn
altExpNames(totsce) = "denoised_protein"
totsce$leiden = totvi_leiden
altExp(totsce)$leiden = totvi_leiden
altExp(totsce)$ch12.clusters = altExp(ch12sce[,comm])$label

# add average ADT abundance to metadata, for adt_profiles

tot.se.averaged <- sumCountsAcrossCells(altExp(totsce), altExp(totsce)$leiden,
    exprs_values="logcounts", average=TRUE)
rownames(tot.se.averaged) = gsub("_TotalSeqB", "", rownames(tot.se.averaged))
metadata(totsce)$se.averaged = tot.se.averaged

Reduce the chapter 12 dataset to the cells held in common

colnames(ch12sce) = ch12sce$Barcode
ch12sce_matched = ch12sce[, comm]

Key outputs of the chapter 12 analysis

Clustering and projection based on the ADT quantifications

The TSNE projection of the normalized ADT quantifications and the walktrap cluster assignments are produced for the cells common to the two approaches.

plotTSNE(altExp(ch12sce_matched), color_by="label", text_by="label")
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text_repel()`).

Cluster profiles based on averaging ADT quantities within clusters

This heatmap uses precomputed cluster averages that are lodged in the metadata element of the SingleCellExperiment. Colors represent the log2-fold change from the grand average across all clusters.

adtProfiles(ch12sce_matched)

Marker expression patterns in mRNA-based sub-clusters of ADT-based clusters

We enhance the annotation of the list of subclusters retrieved using getCh12AllSce and then drill into mRNA-based subclusters of ADT-based cluster 3 to compare expression levels of three genes.

ch12_allsce = getCh12AllSce() 
## adding rname '/tmp/RtmpqE370F/ch12_allsce.rda88b669996f9d'
ch12_allsce = lapply(ch12_allsce, function(x) {
   colnames(x)= x$Barcode; 
   cn = colnames(x); 
   x = x[,intersect(cn,comm)]; x})
of.interest <- "3"
markers <- c("GZMH", "IL7R", "KLRB1")
plotExpression(ch12_allsce[[of.interest]], x="subcluster",
    features=markers, swap_rownames="Symbol", ncol=3)

There is a suggestion of a boolean basis for subcluster identity, depending on low or high expression of the selected genes.

Graduated relationships between mRNA and surface protein expression

Following the exploration in OSCA chapter 12, cluster 3 is analyzed for a regression association between expression measures of three genes and the ADT-based abundance of CD127.

plotExpression(ch12_allsce[["3"]], x="CD127", show_smooth=TRUE, show_se=FALSE, 
    features=c("IL7R", "TPT1", "KLRB1", "GZMH"), swap_rownames="Symbol")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Analogs to the chapter 12 findings, based on totalVI quantifications

The Leiden clustering in UMAP projection

plotUMAP(altExp(totsce), color_by="leiden", text_by="leiden")

Cluster profiles based on average ADT abundance, using denoised protein quantifications

The approach to profiling the ADT abundances used in the totalVI tutorial employs scaling to (0,1).

tav = S4Vectors::metadata(totsce)$se.averaged
ata = assay(tav)
uscale = function(x) (x-min(x))/max(x)
scmat = t(apply(ata,1,uscale))
pheatmap::pheatmap(scmat, cluster_rows=FALSE)

Concordance in ADT-based clustering between OSCA and totalVI

A quick view of the concordance of the two clustering outcomes is

atot = altExp(totsce)
ach12 = altExp(ch12sce_matched)
tt = table(ch12=ach12$label, VI=atot$leiden)
pheatmap::pheatmap(log(tt+1))

With this we can pick out some clusters with many cells in common:

lit = tt[c("9", "12", "5", "3"), c("0", "1", "2", "8", "6", "5")]
rownames(lit) = sQuote(rownames(lit))
colnames(lit) = sQuote(colnames(lit))
lit
##       VI
## ch12    '0'  '1'  '2'  '8'  '6'  '5'
##   '9'  1334    0    0    0    0    0
##   '12'    0  993    0    0   15    0
##   '5'     0  102  671    8   44    2
##   '3'     0    0    5  322  297   67

Subcluster assessment for OSCA cluster “3”

Let’s examine the distributions of marker mRNAs in the Leiden totalVI clusters corresponding to OSCA cluster “3”:

tsub = totsce[,which(altExp(totsce)$leiden %in% c("5", "6", "8"))]
markers <- c("GZMH", "IL7R", "KLRB1")
altExp(tsub)$leiden = factor(altExp(tsub)$leiden) # squelch unused levels
tsub$leiden = factor(tsub$leiden) # squelch unused levels
plotExpression(tsub, x="leiden",
    features=markers, swap_rownames="Symbol", ncol=3)

Note that the y axis label is incorrect – we are plotting the denoised expression values from totalVI.

The display seems roughly consistent with the “boolean basis” observed above with the mRNA-based subclustering.

Graduated relationships between ADT and mRNA abundance as measured by totalVI

The same approach is taken as above. We don’t have TPT1 in the 4000 genes retained in the totalVI exercise.

rn = rownames(altExp(tsub))
rn = gsub("_TotalSeqB", "", rn)
rownames(altExp(tsub)) = rn
rowData(altExp(tsub)) = DataFrame(Symbol=rn)
plotExpression(tsub, x="CD127", show_smooth=TRUE, show_se=FALSE,
   features=c("IL7R", "KLRB1", "GZMH"), swap_rownames="Symbol")
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

Conclusions

We have shown how rudimentary programming and data organization can be used to make outputs of OSCA and totalVI methods amenable to comparison in the Bioconductor framework.

The scviR package includes a shiny app in the function explore_subcl that should be expanded to facilitate exploration of totalVI subclusters. Much more work remains to be done in the area of exploring

  • additional approaches to integrative interpretation of ADT and mRNA abundance patterns, such as intersection and concatenation methods in the feature selection materials in OSCA ch. 12

  • effects of tuning and architecture details for the totalVI VAE

Session information

sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.1 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] scater_1.34.0               scuttle_1.16.0             
##  [3] reshape2_1.4.4              ggplot2_3.5.1              
##  [5] scviR_1.7.0                 SingleCellExperiment_1.28.0
##  [7] SummarizedExperiment_1.36.0 Biobase_2.67.0             
##  [9] GenomicRanges_1.59.0        GenomeInfoDb_1.43.0        
## [11] IRanges_2.41.0              S4Vectors_0.44.0           
## [13] BiocGenerics_0.53.0         MatrixGenerics_1.19.0      
## [15] matrixStats_1.4.1           shiny_1.9.1                
## [17] basilisk_1.19.0             reticulate_1.39.0          
## [19] BiocStyle_2.35.0           
## 
## loaded via a namespace (and not attached):
##   [1] DBI_1.2.3               gridExtra_2.3           rlang_1.1.4            
##   [4] magrittr_2.0.3          compiler_4.4.1          RSQLite_2.3.7          
##   [7] mgcv_1.9-1              dir.expiry_1.15.0       png_0.1-8              
##  [10] vctrs_0.6.5             stringr_1.5.1           pkgconfig_2.0.3        
##  [13] crayon_1.5.3            fastmap_1.2.0           dbplyr_2.5.0           
##  [16] XVector_0.46.0          labeling_0.4.3          utf8_1.2.4             
##  [19] promises_1.3.0          rmarkdown_2.28          UCSC.utils_1.2.0       
##  [22] ggbeeswarm_0.7.2        purrr_1.0.2             bit_4.5.0              
##  [25] xfun_0.48               zlibbioc_1.52.0         cachem_1.1.0           
##  [28] beachmat_2.23.0         jsonlite_1.8.9          blob_1.2.4             
##  [31] highr_0.11              later_1.3.2             DelayedArray_0.33.1    
##  [34] BiocParallel_1.41.0     irlba_2.3.5.1           parallel_4.4.1         
##  [37] R6_2.5.1                stringi_1.8.4           bslib_0.8.0            
##  [40] RColorBrewer_1.1-3      limma_3.63.0            jquerylib_0.1.4        
##  [43] Rcpp_1.0.13             knitr_1.48              splines_4.4.1          
##  [46] httpuv_1.6.15           Matrix_1.7-1            tidyselect_1.2.1       
##  [49] abind_1.4-8             yaml_2.3.10             viridis_0.6.5          
##  [52] codetools_0.2-20        curl_5.2.3              plyr_1.8.9             
##  [55] lattice_0.22-6          tibble_3.2.1            withr_3.0.2            
##  [58] basilisk.utils_1.19.0   evaluate_1.0.1          BiocFileCache_2.15.0   
##  [61] pillar_1.9.0            BiocManager_1.30.25     filelock_1.0.3         
##  [64] generics_0.1.3          munsell_0.5.1           scales_1.3.0           
##  [67] xtable_1.8-4            glue_1.8.0              pheatmap_1.0.12        
##  [70] maketools_1.3.1         tools_4.4.1             BiocNeighbors_2.1.0    
##  [73] sys_3.4.3               ScaledMatrix_1.14.0     buildtools_1.0.0       
##  [76] grid_4.4.1              colorspace_2.1-1        nlme_3.1-166           
##  [79] GenomeInfoDbData_1.2.13 beeswarm_0.4.0          BiocSingular_1.23.0    
##  [82] vipor_0.4.7             cli_3.6.3               rsvd_1.0.5             
##  [85] fansi_1.0.6             S4Arrays_1.6.0          viridisLite_0.4.2      
##  [88] dplyr_1.1.4             gtable_0.3.6            sass_0.4.9             
##  [91] digest_0.6.37           SparseArray_1.6.0       ggrepel_0.9.6          
##  [94] farver_2.1.2            memoise_2.0.1           htmltools_0.5.8.1      
##  [97] lifecycle_1.0.4         httr_1.4.7              statmod_1.5.0          
## [100] mime_0.12               bit64_4.5.2