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)
QC (Step 3)
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)
)

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
| 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")
}
TG2F

TG3F

TG4F

WT1F

WT2F

WT3F

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")
}
TG2F

TG3F

TG4F

WT1F

WT2F

WT3F

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
| 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")
}
TG2F

TG3F

TG4F

WT1F

WT2F

WT3F

Background signals
| 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")
}
TG2F

TG3F

TG4F

WT1F

WT2F

WT3F

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")))
| TG2F |
1786996 |
639617 |
7347978 |
| TG3F |
1779629 |
677997 |
6397996 |
| TG4F |
1475614 |
1038204 |
4846442 |
| WT1F |
1889126 |
719979 |
8012189 |
| WT2F |
1811919 |
668407 |
6923973 |
| WT3F |
1609489 |
696525 |
5659248 |
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"))
| 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 |