1 Introduction

ASOC generated this report includes stats and diagnostic plots of the Xenium In Situ data.

2 Prerequisite

2.1 Environment

  • OS: macOS Sequoia 15.0
  • Platform: aarch64-apple-darwin20 (64-bit)
  • Software: R (v4.4.1)
  • Pakcages: arrow (v17.0.0.1), dplyr (v1.1.4), ggplot2 (v3.5.1), ggrepel (v0.9.5), ggridges (v0.5.6), HDF5Array (v1.32.1), outliers (v0.15), presto (v1.0.0), reshape2 (v1.4.4), scMerge (v1.20.0), SingleCellExperiment (v1.26.0), stringr (v1.5.1), yaml (v2.3.10)

2.2 Xenium In Situ

  • Slide ID: 0010348
  • Preparation method: FFPE
  • Sample description: Six female mouse brains
    • 5xFAD transgenic (TG) mice (greens, even numbers), N=3:
      • TG2F (Region_2) â—¼
      • TG3F (Region_4) â—¼
      • TG4F (Region_6) â—¼
    • Wild-type (WT) mice (purples, odd numbers), N=3:
      • WT1F (Region_1) â—¼
      • WT2F (Region_3) â—¼
      • WT3F (Region_5) â—¼
  • Number of panel genes: 247
  • Xenium Ranger analysis summary
  • Files: feature.tsv.gz, cell_feature_matrix.h5, cells.parquet, metrics_summary.csv, and transcripts.parquet (see below)
WORKING_DIRECTORY
└── output-INSTRUMENT__SLIDE__REGION__DATE__TIME
    ├── cell_feature_matrix
    │   └── features.tsv.gz
    ├── cell_feature_matrix.h5
    ├── cells.parquet
    ├── metrics_summary.csv
    └── transcripts.parquet

3 Preprocessing/QC

3.1 Preparation

  • Set working directory to load files
# Do not run
baseDir <- "WORKING_DIRECTORY" # where the output-* folders are located
setwd(baseDir)
outDir <- file.path(baseDir, "Results")
library(arrow)
library(dplyr)
library(ggplot2)
library(ggrepel)
library(ggridges)
library(HDF5Array)
library(outliers)
library(presto)
library(reshape2)
library(scMerge)
library(SingleCellExperiment)
library(stringr)

3.2 Load Xenium data (Step 1)

outputFolders <- list.files(path = baseDir, pattern = "output")
outputFolders
## [1] "output-XETG00116__0010348__TG2F__20240427__002555"
## [2] "output-XETG00116__0010348__TG3F__20240427__002555"
## [3] "output-XETG00116__0010348__TG4F__20240427__002555"
## [4] "output-XETG00116__0010348__WT1F__20240427__002555"
## [5] "output-XETG00116__0010348__WT2F__20240427__002555"
## [6] "output-XETG00116__0010348__WT3F__20240427__002555"
# Summary
summaryL <- lapply(seq_along(outputFolders), function(idx) {
        outputFolder <- outputFolders[idx]
        summary <- read.csv(file.path(baseDir, outputFolder, "metrics_summary.csv"))
        return(summary)
})
summary <- Reduce(rbind, summaryL)

# Cells
cellsL <- lapply(seq_along(outputFolders), function(idx) {
        outputFolder <- outputFolders[idx]
        cells <- arrow::read_parquet(file.path(baseDir, outputFolder, "cells.parquet"), as_data_frame = TRUE)
        cells$region <- sapply(str_split(outputFolder, "__"), "[[", 3)
        return(cells)
})
cells <- Reduce(rbind, cellsL)
cells$region <- factor(cells$region, levels = regionNames)

# Transcripts
txL <- lapply(seq_along(outputFolders), function(idx) {
        outputFolder <- outputFolders[idx]
        tx <- arrow::read_parquet(file.path(baseDir, outputFolder, "transcripts.parquet"), as_data_frame = TRUE)
        return(tx)
})
names(txL) <- sapply(str_split(outputFolders, "__"), "[[", 3)
txL <- txL[regionNames]

# Expression profile
exprL <- lapply(seq_along(outputFolders), function(idx) {
        outputFolder <- outputFolders[idx]
        mat <- HDF5Array::TENxMatrix(file.path(baseDir, outputFolder, "cell_feature_matrix.h5"), "matrix")
        return(mat)
})
names(exprL) <- sapply(str_split(outputFolders, "__"), "[[", 3)
exprL <- exprL[regionNames]

# Feature annotation
featuresAnnot <- read.delim(file.path(baseDir, outputFolders[1], "cell_feature_matrix", "features.tsv.gz"), header = FALSE, stringsAsFactors = FALSE)

3.3 Stats at a glance

Region Region_area Cell_area Cells_detected High_qual_Tx Tx_per_100um2 FOVs
TG2F 28462857 8833587 53020 11305322 127.9811 65
TG3F 23250723 8057483 51515 9050443 112.3235 53
TG4F 21498027 7171907 45791 7632048 106.4159 49
WT1F 28483607 9199148 54878 12407959 134.8816 65
WT2F 28034067 7882022 47444 10679458 135.4914 64
WT3F 23244105 7663646 50510 8058815 105.1564 53

3.4 Exclude unassigned Tx (Step 2)

  • The following capture is an example case to show Tx that are unassigned
txAssignedL <- lapply(seq_along(txL), function(idx) {
        tx <- txL[[idx]]
        return(tx[which(tx$cell_id != "UNASSIGNED"), ])
})
names(txAssignedL) <- regionNames
Model Total AssignedTx UnAssignedTx Proportion
TG2F 13870843 9774591 4096252 0.295
TG3F 11414778 8855622 2559156 0.224
TG4F 9643010 7360260 2282750 0.237
WT1F 14980357 10621294 4359063 0.291
WT2F 13171948 9404299 3767649 0.286
WT3F 10177480 7965262 2212218 0.217
Total 73258416 53981328 19277088 0.263

3.5 QC (Step 3)

3.5.1 Cell area and detected Tx

  • The following violin plots show the distribution of the cell area or detected Tx across regions
    • X-axis: model
    • Y-axis: area in micrometer square or total detected Tx
    • Dot: cell
ggplot(data = cells, aes(x = region, y = log10(cell_area + 1), fill = region)) +
        geom_violin(position = dodge, size = 0) +
        geom_boxplot(width = 0.1, position = dodge, fill = "white") +
        scale_fill_manual(values = regionCols) +
        labs(
                x = "",
                y = "Cell area, log10"
        ) +
        theme_bw() +
        theme(
                axis.line = element_line(colour = "black"),
                panel.grid.major = element_blank(),
                panel.grid.minor = element_blank(),
                panel.border = element_blank(),
                panel.background = element_blank(),
                axis.text.x = element_text(angle = 0, vjust = 0, hjust = 0.5),
                legend.position = "none",
                text = element_text(size = 12)
        )

ggplot(data = cells, aes(x = region, y = log10(transcript_counts + 1), fill = region)) +
        geom_violin(position = dodge, size = 0) +
        geom_boxplot(width = 0.1, position = dodge, fill = "white") +
        scale_fill_manual(values = regionCols) +
        labs(
                x = "",
                y = "Tx counts, log10"
        ) +
        theme_bw() +
        theme(
                axis.line = element_line(colour = "black"),
                panel.grid.major = element_blank(),
                panel.grid.minor = element_blank(),
                panel.border = element_blank(),
                panel.background = element_blank(),
                axis.text.x = element_text(angle = 0, vjust = 0, hjust = 0.5),
                legend.position = "none",
                text = element_text(size = 12)
        )

3.5.2 Detect ouliters in terms of area

  • Detect under segmentated (when more than one cells segmented as one) cells using Grubb’s test
  • A given area of the cell is an outlier when Grubb’s test p-value is less than 0.05 in a region
  • Note that a centroid diameter, or cell diameter, is preset in Xenium per tissue type
Model Total_cells Excluded Remainders
TG2F 53020 11 53009
TG3F 51515 5 51510
TG4F 45791 2 45789
WT1F 54878 5 54873
WT2F 47444 3 47441
WT3F 50510 2 50508
Total 303158 28 303130
for (region in regionNames) {
        subCells <- cells[which(cells$region == region), ]
        subCells$cell_area <- round(subCells$cell_area, 3)
        testRes <- grubbsTestRec(subCells$cell_area)
        areaThreshold <- max(testRes$Area[!testRes$Outlier])
        
        cat("#### ", region, "\n")
        cat("\n")
        plot(density(testRes$Area), sub="Cell area, µm2", main="")
        abline(v = areaThreshold, col="red", lty=2)
        cat("\n\n")
}

3.5.2.1 TG2F

3.5.2.2 TG3F

3.5.2.3 TG4F

3.5.2.4 WT1F

3.5.2.5 WT2F

3.5.2.6 WT3F

3.5.3 Detected Tx across cells

  • Number of detected Tx across cells at the FOV level
  • In other words, some Tx detected only in one cell, while others detected in many cells
  • The following ridge plot shows the distribution of the detected Tx in each FOV
    • X-axis: number of cells that captures a given Tx
    • Y-axis: density
txDfL <- lapply(seq_along(txAssignedL), function(idx) {
        txName <- names(txAssignedL)[idx]
        txAssigned <- txAssignedL[[idx]]
        txDf <- as.data.frame(txAssigned %>% group_by(feature_name, fov_name) %>% dplyr::count() %>% dplyr::rename(cells = n))
        txDf$model <- txName
        return(txDf)
})
txDf <- Reduce(rbind, txDfL)
head(txDf)
##    feature_name fov_name cells model
## 1 2010300C02Rik      E12  3789  TG2F
## 2 2010300C02Rik      E13  5266  TG2F
## 3 2010300C02Rik      E14  6676  TG2F
## 4 2010300C02Rik      E15  4288  TG2F
## 5 2010300C02Rik      E16  2035  TG2F
## 6 2010300C02Rik      E17   339  TG2F
for (region in regionNames) {
        cat("#### ", region, "\n")
        cat("\n")
        regDf <- txDf[which(txDf$model == region), ]
        rp <- ridgePlot(df = regDf, x = "cells", y = "fov_name", title = region, xLbl = "nCells", yLbl = "FOV", scaleTrans = "log10")
        print(rp)
        cat("\n\n")
}

3.5.3.1 TG2F

3.5.3.2 TG3F

3.5.3.3 TG4F

3.5.3.4 WT1F

3.5.3.5 WT2F

3.5.3.6 WT3F

3.5.4 Count and Features per cell

  • Exclude cells with few (<10) Tx detected
  • The following scatter plots show the relationship between nCount and nFeature in linear (L) or log-scale (R)
    • X-axis: nCount (total number of detected Tx within a cell, depth)
    • Y-axis: nFeature (number of detected Tx in each cell, coverage)
    • Dot: cell
Model Total_cells Large_cell_area Low_nCount Remainders
TG2F 53020 11 99 52910
TG3F 51515 5 207 51303
TG4F 45791 2 414 45375
WT1F 54878 5 93 54780
WT2F 47444 3 113 47328
WT3F 50510 2 386 50122
Total 303158 28 1312 301818
for (idx in seq_along(regionNames)) {
        exprName <- names(exprL)[idx]
        expr <- exprL[[idx]]
        panelGenesIdx <- which(str_detect(rownames(expr), "ENS"))
        expr <- expr[panelGenesIdx, ]

        cellDf <- data.frame(
                nCount = apply(as.matrix(expr), 2, sum),
                nFeature = apply(as.matrix(expr), 2, function(x) length(which(x > 0)))
        )
        cellDf <- densityColors(df = cellDf, cols = heatCols)

        cellDf$criteria <- "Include"
        cellDf$criteria[which(cellDf$nCount < nCountThre)] <- "Exclude"

        cellDfInc <- cellDf[which(cellDf$criteria == "Include"), ]
        cellDfExc <- cellDf[which(cellDf$criteria == "Exclude"), ]

        cat("#### ", exprName, "\n")
        cat("\n")
        par(mfrow = c(2, 2))
        plot(cellDf$nCount, cellDf$nFeature, col = cellDf$Col, xlab = "nCount", ylab = "nFeature", pch = 20, xlim = c(0, nCountMax), ylim = c(0, nFeatureMax))
        plot(log10(cellDf$nCount + 1), log10(cellDf$nFeature + 1), col = cellDf$Col, xlab = "nCount, log10", ylab = "nFeature, log10", pch = 20, xlim = c(0, log10(nCountMax)), ylim = c(0, log10(nFeatureMax)))
        plot(cellDfInc$nCount, cellDfInc$nFeature, col = "grey60", xlab = "nCount", ylab = "nFeature", pch = 20, xlim = c(0, nCountMax), ylim = c(0, max(cellDfInc$nFeature)))
        points(cellDfExc$nCount, cellDfExc$nFeature, col = "red", pch = 20)
        legend("topleft", legend = c("Include", "Exclude"), fill = c("grey60", "red"), bty = "n")
        plot(log10(cellDfInc$nCount + 1), log10(cellDfInc$nFeature + 1), col = "grey60", xlab = "nCount, log10", ylab = "nFeature, log10", pch = 20, xlim = c(0, log10(nCountMax + 1)), ylim = c(0, log10(nFeatureMax)))
        points(log10(cellDfExc$nCount + 1), log10(cellDfExc$nFeature + 1), col = "red", pch = 20)
        legend("topleft", legend = c("Include", "Exclude"), fill = c("grey60", "red"), bty = "n")
        cat("\n\n")
}

3.5.4.1 TG2F

3.5.4.2 TG3F

3.5.4.3 TG4F

3.5.4.4 WT1F

3.5.4.5 WT2F

3.5.4.6 WT3F

3.5.5 Background signals

Model Total_cells Large_cell_area Low_nCount High_background Remainders
TG2F 53020 11 99 3 52907
TG3F 51515 5 207 32 51271
TG4F 45791 2 414 88 45287
WT1F 54878 5 93 2 54778
WT2F 47444 3 113 2 47326
WT3F 50510 2 386 94 50028
Total 303158 28 1312 221 301597
for (idx in seq_along(regionNames)) {
        exprName <- names(exprL)[idx]
        expr <- exprL[[idx]]

        panelGenesIdx <- which(str_detect(rownames(expr), "^ENS"))
        negProbesIdx <- which(str_detect(rownames(expr), "^NegControlProbe"))
        # negCodewordsIdx <- which(str_detect(rownames(expr), "^NegControlCodeword"))

        signalDf <- data.frame(
                CellID = colnames(expr),
                Genes = apply(as.matrix(expr[panelGenesIdx, ]), 2, sum),
                negProbes = apply(as.matrix(expr[negProbesIdx, ]), 2, sum)
                # negCodewords = apply(expr[negCodewordsIdx,], 2, sum)
        )
        signalDf <- signalDf[which(signalDf$negProbes != 0), ]
        signalDf$Prop <- signalDf$negProbes / (signalDf$Genes + signalDf$negProbes)

        cat("#### ", exprName, "\n")
        cat("\n")
        hist(signalDf$Prop, breaks = seq(0, max(signalDf$Prop) + 0.01, 0.01), xlab = "# Neg probes / Total detected molecules", sub = paste0(nrow(signalDf), " cells that capture at least one Neg Probe"), main = exprName)
        abline(v = negProbePropThre, col = "red", lty = 2)
        cat("\n\n")
}

3.5.5.1 TG2F

3.5.5.2 TG3F

3.5.5.3 TG4F

3.5.5.4 WT1F

3.5.5.5 WT2F

3.5.5.6 WT3F

3.5.6 Tx Quality and Number of Tx

  • 10X Genomics recommend to include Tx quality that is greater than 20 and the threshold in this study is 20
    • 20 (99%, an error rate of 1 in 100)
    • 30 (99.9%, an error rate of 1 in 1000)
    • 40 (99.99%, an error rate of 1 in 10000)
  • Low qual Tx are likely the negative controls
  • QC purposes across models/runs/cohorts
qvDf <- c()
for (idx in seq_along(regionNames)) {
        txName <- names(txAssignedL)[idx]
        txAssigned <- txAssignedL[[idx]]

        buff <- data.frame(cut(txAssigned$qv, breaks = c(0, 20, 30, 40)) %>% table)
        buff$Model <- txName

        qvDf <- rbind(qvDf, buff)
}
colnames(qvDf) <- c("Quality_score", "Freq", "Model")
qvDf$Model <- factor(qvDf$Model, levels=regionNames)
knitr::kable(reshape2::dcast(qvDf, Model ~ Quality_score, value.var = c("Freq")))
Model (0,20] (20,30] (30,40]
TG2F 1786996 639617 7347978
TG3F 1779629 677997 6397996
TG4F 1475614 1038204 4846442
WT1F 1889126 719979 8012189
WT2F 1811919 668407 6923973
WT3F 1609489 696525 5659248

3.5.7 QC Summary

  • Number of cells after QC for the downstream analysis
  • Analysis ready expression profiles (before and after QC)
exprL <- lapply(seq_along(exprL), function(idx) {
        expr <- exprL[[idx]]
        panelGenesIdx <- which(str_detect(rownames(expr), "ENS"))
        expr <- expr[panelGenesIdx, ]
        return(expr)
})
names(exprL) <- regionNames

arExprL <- lapply(seq_along(exprL), function(idx) { # analysis-ready expression profiles
        exprName <- names(exprL)[idx]
        expr <- exprL[[idx]]
        exclude <- excludeCells[which(excludeCells$Model == exprName),]

        if (any(colnames(expr) %in% exclude$Cells)) {
                expr <- expr[, -which(colnames(expr) %in% exclude$Cells)]
        }
        return(expr)
})
names(arExprL) <- regionNames
saveRDS(arExprL, file.path(outDir, "01_analysisReady_expression.RDS"))
Model Total_cells Excluded_cells Proportion_exc Remainders
TG2F 53020 113 0.002 52907
TG3F 51515 241 0.005 51274
TG4F 45791 494 0.011 45297
WT1F 54878 100 0.002 54778
WT2F 47444 118 0.002 47326
WT3F 50510 471 0.009 50039
Total 303158 1537 0.005 301621

4 Research reproducibility

  • Parameters used in this report
Step Parameter Threshold Description
QC nCount 10 Minimum number of detected Tx within a cell
negProbe proportion 0.05 Maximum proportion of the background signal
quality score 20 Minimum Tx Phred-like quality score
sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: aarch64-apple-darwin20
## Running under: macOS 15.0
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
## 
## locale:
## [1] en_CA.UTF-8/en_CA.UTF-8/en_CA.UTF-8/C/en_CA.UTF-8/en_CA.UTF-8
## 
## time zone: America/Edmonton
## tzcode source: internal
## 
## attached base packages:
## [1] stats4    stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] stringr_1.5.1               SingleCellExperiment_1.26.0
##  [3] SummarizedExperiment_1.34.0 Biobase_2.64.0             
##  [5] GenomicRanges_1.56.1        GenomeInfoDb_1.40.1        
##  [7] scMerge_1.20.0              reshape2_1.4.4             
##  [9] presto_1.0.0                data.table_1.16.0          
## [11] Rcpp_1.0.13                 outliers_0.15              
## [13] HDF5Array_1.32.1            rhdf5_2.48.0               
## [15] DelayedArray_0.30.1         SparseArray_1.4.8          
## [17] S4Arrays_1.4.1              abind_1.4-8                
## [19] IRanges_2.38.1              S4Vectors_0.42.1           
## [21] MatrixGenerics_1.16.0       matrixStats_1.4.1          
## [23] BiocGenerics_0.50.0         Matrix_1.7-0               
## [25] ggridges_0.5.6              ggrepel_0.9.6              
## [27] ggplot2_3.5.1               dplyr_1.1.4                
## [29] arrow_17.0.0.1             
## 
## loaded via a namespace (and not attached):
##   [1] RColorBrewer_1.1-3        rstudioapi_0.16.0        
##   [3] jsonlite_1.8.9            magrittr_2.0.3           
##   [5] ggbeeswarm_0.7.2          farver_2.1.2             
##   [7] rmarkdown_2.28            zlibbioc_1.50.0          
##   [9] vctrs_0.6.5               DelayedMatrixStats_1.26.0
##  [11] base64enc_0.1-3           htmltools_0.5.8.1        
##  [13] BiocNeighbors_1.22.0      Rhdf5lib_1.26.0          
##  [15] Formula_1.2-5             sass_0.4.9               
##  [17] StanHeaders_2.32.10       reldist_1.7-2            
##  [19] KernSmooth_2.23-24        bslib_0.8.0              
##  [21] htmlwidgets_1.6.4         plyr_1.8.9               
##  [23] cachem_1.1.0              ResidualMatrix_1.14.1    
##  [25] sfsmisc_1.1-19            igraph_2.0.3             
##  [27] startupmsg_0.9.7          lifecycle_1.0.4          
##  [29] pkgconfig_2.0.3           M3Drop_1.30.0            
##  [31] rsvd_1.0.5                R6_2.5.1                 
##  [33] fastmap_1.2.0             GenomeInfoDbData_1.2.12  
##  [35] digest_0.6.37             numDeriv_2016.8-1.1      
##  [37] colorspace_2.1-1          scater_1.32.1            
##  [39] dqrng_0.4.1               irlba_2.3.5.1            
##  [41] Hmisc_5.1-3               beachmat_2.20.0          
##  [43] labeling_0.4.3            fansi_1.0.6              
##  [45] httr_1.4.7                mgcv_1.9-1               
##  [47] compiler_4.4.1            bit64_4.0.5              
##  [49] withr_3.0.1               htmlTable_2.4.3          
##  [51] backports_1.5.0           inline_0.3.19            
##  [53] BiocParallel_1.38.0       viridis_0.6.5            
##  [55] highr_0.11                QuickJSR_1.3.1           
##  [57] pkgbuild_1.4.4            gplots_3.1.3.1           
##  [59] MASS_7.3-61               proxyC_0.4.1             
##  [61] bluster_1.14.0            gtools_3.9.5             
##  [63] caTools_1.18.3            loo_2.8.0                
##  [65] distr_2.9.5               tools_4.4.1              
##  [67] vipor_0.4.7               foreign_0.8-87           
##  [69] beeswarm_0.4.0            nnet_7.3-19              
##  [71] glue_1.7.0                batchelor_1.20.0         
##  [73] cvTools_0.3.3             nlme_3.1-166             
##  [75] rhdf5filters_1.16.0       grid_4.4.1               
##  [77] checkmate_2.3.2           cluster_2.1.6            
##  [79] generics_0.1.3            gtable_0.3.5             
##  [81] metapod_1.12.0            BiocSingular_1.20.0      
##  [83] ScaledMatrix_1.12.0       utf8_1.2.4               
##  [85] XVector_0.44.0            pillar_1.9.0             
##  [87] limma_3.60.4              robustbase_0.99-4        
##  [89] splines_4.4.1             lattice_0.22-6           
##  [91] densEstBayes_1.0-2.2      bit_4.0.5                
##  [93] ruv_0.9.7.1               tidyselect_1.2.1         
##  [95] locfit_1.5-9.10           scuttle_1.14.0           
##  [97] knitr_1.48                gridExtra_2.3            
##  [99] edgeR_4.2.1               xfun_0.47                
## [101] statmod_1.5.0             DEoptimR_1.1-3           
## [103] rstan_2.32.6              stringi_1.8.4            
## [105] UCSC.utils_1.0.0          yaml_2.3.10              
## [107] evaluate_1.0.0            codetools_0.2-20         
## [109] bbmle_1.0.25.1            tibble_3.2.1             
## [111] cli_3.6.3                 RcppParallel_5.1.9       
## [113] rpart_4.1.23              munsell_0.5.1            
## [115] jquerylib_0.1.4           bdsmatrix_1.3-7          
## [117] parallel_4.4.1            rstantools_2.4.0         
## [119] assertthat_0.2.1          scran_1.32.0             
## [121] sparseMatrixStats_1.16.0  bitops_1.0-8             
## [123] viridisLite_0.4.2         mvtnorm_1.3-1            
## [125] scales_1.3.0              purrr_1.0.2              
## [127] crayon_1.5.3              rlang_1.1.4