suppressPackageStartupMessages({
library(scater)
library(scran)
library(patchwork)
library(ggplot2)
library(umap)
})
Code chunks run R commands unless otherwise specified.
1 Data preparation
First, let’s load all necessary libraries and the QC-filtered dataset from the previous step.
# download pre-computed data if missing or long compute
<- TRUE
fetch_data
# url for source and intermediate data
<- "https://export.uppmax.uu.se/naiss2023-23-3/workshops/workshop-scrnaseq"
path_data <- "data/covid/results/bioc_covid_qc.rds"
path_file if (!dir.exists(dirname(path_file))) dir.create(dirname(path_file), recursive = TRUE)
if (fetch_data && !file.exists(path_file)) download.file(url = file.path(path_data, "covid/results/bioc_covid_qc.rds"), destfile = path_file)
<- readRDS(path_file) sce
2 Feature selection
We first need to define which features/genes are important in our dataset to distinguish cell types. For this purpose, we need to find genes that are highly variable across cells, which in turn will also provide a good separation of the cell clusters.
With modelGeneVar
we can specify a blocking parameter, so the trend fitting and variance decomposition is done separately for each batch.
<- computeSumFactors(sce, sizes = c(20, 40, 60, 80))
sce <- logNormCounts(sce)
sce <- modelGeneVar(sce, block = sce$sample)
var.out <- getTopHVGs(var.out, n = 2000) hvgs
We can plot the total variance and the biological variance vs mean expressioni for one of the samples.
par(mfrow = c(1, 2))
# plot mean over TOTAL variance
# Visualizing the fit:
<- metadata(var.out$per.block[[1]])
fit.var
{plot(fit.var$mean, fit.var$var,
xlab = "Mean of log-expression",
ylab = "Variance of log-expression",
main = "Total variance"
)curve(fit.var$trend(x), col = "dodgerblue", add = TRUE, lwd = 2)
# Select 1000 top variable genes
<- getTopHVGs(var.out, n = 1000)
hvg.out
# highligt those cells in the plot
<- rownames(var.out) %in% hvg.out
cutoff points(fit.var$mean[cutoff], fit.var$var[cutoff], col = "red", pch = 16, cex = .6)
}
{# plot mean over BIOLOGICAL variance for same sample
plot(var.out$per.block[[1]]$mean, var.out$per.block[[1]]$bio, pch = 16, cex = 0.4,
xlab = "Mean log-expression",
ylab = "Variance of log-expression",
main = "Biological variance")
lines(c(min(var.out$per.block[[1]]$mean), max(var.out$per.block[[1]]$mean)), c(0, 0), col = "dodgerblue", lwd = 2)
points(var.out$per.block[[1]]$mean[cutoff], var.out$per.block[[1]]$bio[cutoff], col = "red", pch = 16, cex = .6)
}
We can plot the same for all the samples, also showing the technical variance.
<- rownames(var.out) %in% hvgs
cutoff
par(mfrow = c(1, 3))
plot(var.out$mean, var.out$total, pch = 16, cex = 0.4,
xlab = "Mean log-expression",
ylab = "Variance of log-expression",
main = "Total variance")
points(var.out$mean[cutoff], var.out$total[cutoff], col = "red", pch = 16, cex = .6)
plot(var.out$mean, var.out$bio, pch = 16, cex = 0.4,
xlab = "Mean log-expression",
ylab = "Variance of log-expression",
main = "Biological variance")
points(var.out$mean[cutoff], var.out$bio[cutoff], col = "red", pch = 16, cex = .6)
plot(var.out$mean, var.out$tech, pch = 16, cex = 0.4,
xlab = "Mean log-expression",
ylab = "Variance of log-expression",
main = "Technical variance")
points(var.out$mean[cutoff], var.out$tech[cutoff], col = "red", pch = 16, cex = .6)
3 Z-score transformation
Now that the genes have been selected, we now proceed with PCA. Since each gene has a different expression level, it means that genes with higher expression values will naturally have higher variation that will be captured by PCA. This means that we need to somehow give each gene a similar weight when performing PCA (see below). The common practice is to center and scale each gene before performing PCA. This exact scaling called Z-score normalization is very useful for PCA, clustering and plotting heatmaps. Additionally, we can use regression to remove any unwanted sources of variation from the dataset, such as cell cycle
, sequencing depth
, percent mitochondria
etc. This is achieved by doing a generalized linear regression using these parameters as co-variates in the model. Then the residuals of the model are taken as the regressed data. Although perhaps not in the best way, batch effect regression can also be done here. By default, variables are scaled in the PCA step and is not done separately. But it could be achieved by running the commands below:
However, unlike the Seurat, this step is implemented inside the PCA function below. Here we will show you how to add the scaledData back to the object.
# sce@assays$data@listData$scaled.data <- apply(exprs(sce)[rownames(hvg.out),,drop=FALSE],2,function(x) scale(x,T,T))
# rownames(sce@assays$data@listData$scaled.data) <- rownames(hvg.out)
4 PCA
Performing PCA has many useful applications and interpretations, which much depends on the data used. In the case of single-cell data, we want to segregate samples based on gene expression patterns in the data.
We use the logcounts
and then set scale_features
to TRUE in order to scale each gene.
# runPCA and specify the variable genes to use for dim reduction with subset_row
<- runPCA(sce, exprs_values = "logcounts", ncomponents = 50, subset_row = hvgs, scale = TRUE) sce
We then plot the first principal components.
wrap_plots(
plotReducedDim(sce, dimred = "PCA", colour_by = "sample", ncomponents = 1:2, point_size = 0.6),
plotReducedDim(sce, dimred = "PCA", colour_by = "sample", ncomponents = 3:4, point_size = 0.6),
plotReducedDim(sce, dimred = "PCA", colour_by = "sample", ncomponents = 5:6, point_size = 0.6),
ncol = 3
+ plot_layout(guides = "collect") )
To identify which genes (Seurat) or metadata parameters (Scater/Scran) contribute the most to each PC, one can retrieve the loading matrix information. Unfortunately, this is not implemented in Scater/Scran, so you will need to compute PCA using logcounts
.
Here, we can check how the different metadata variables contributes to each PC. This can be important to look at to understand different biases you may have in your data.
plotExplanatoryPCs(sce,nvars_to_plot = 15)
Clearly, the sample id contributes to many of the PCs and PC7 but you can also see that many PCs are effected by different QC parameters.
We can also plot the amount of variance explained by each PC.
plot(attr(reducedDim(sce, "PCA"), "percentVar")[1:50] * 100, type = "l", ylab = "% variance", xlab = "Principal component #")
points(attr(reducedDim(sce, "PCA"), "percentVar")[1:50] * 100, pch = 21, bg = "grey", cex = .5)
Based on this plot, we can see that the top 8 PCs retain a lot of information, while other PCs contain progressively less. However, it is still advisable to use more PCs since they might contain information about rare cell types (such as platelets and DCs in this dataset)
5 tSNE
We will now run BH-tSNE.
set.seed(42)
<- runTSNE(sce, dimred = "PCA", n_dimred = 30, perplexity = 30, name = "tSNE_on_PCA") sce
We plot the tSNE scatterplot colored by dataset. We can clearly see the effect of batches present in the dataset.
plotReducedDim(sce, dimred = "tSNE_on_PCA", colour_by = "sample")
6 UMAP
We can now run UMAP for cell embeddings.
<- runUMAP(sce, dimred = "PCA", n_dimred = 30, ncomponents = 2, name = "UMAP_on_PCA")
sce # see ?umap and ?runUMAP for more info
UMAP is plotted colored per dataset. Although less distinct as in the tSNE, we still see quite an effect of the different batches in the data.
<- runUMAP(sce, dimred = "PCA", n_dimred = 30, ncomponents = 10, name = "UMAP10_on_PCA")
sce # see ?umap and ?runUMAP for more info
We can now plot PCA, UMAP and tSNE side by side for comparison. Have a look at the UMAP and tSNE. What similarities/differences do you see? Can you explain the differences based on what you learned during the lecture? Also, we can conclude from the dimensionality reductions that our dataset contains a batch effect that needs to be corrected before proceeding to clustering and differential gene expression analysis.
wrap_plots(
plotReducedDim(sce, dimred = "UMAP_on_PCA", colour_by = "sample") +
::ggtitle(label = "UMAP_on_PCA"),
ggplot2plotReducedDim(sce, dimred = "UMAP10_on_PCA", colour_by = "sample", ncomponents = 1:2) +
::ggtitle(label = "UMAP10_on_PCA"),
ggplot2plotReducedDim(sce, dimred = "UMAP10_on_PCA", colour_by = "sample", ncomponents = 3:4) +
::ggtitle(label = "UMAP10_on_PCA"),
ggplot2ncol = 3
+ plot_layout(guides = "collect") )
We have now done Variable gene selection, PCA and UMAP with the settings we selected for you. Test a few different ways of selecting variable genes, number of PCs for UMAP and check how it influences your embedding.
7 Z-scores & DR graphs
Although running a second dimensionality reduction (i.e tSNE or UMAP) on PCA would be a standard approach (because it allows higher computation efficiency), the options are actually limitless. Below we will show a couple of other common options such as running directly on the scaled data (z-scores) (which was used for PCA) or on a graph built from scaled data. We will only work with UMAPs, but the same applies for tSNE.
7.1 UMAP from z-scores
To run tSNE or UMAP on the scaled data, one first needs to select the number of variables to use. This is because including dimensions that do contribute to the separation of your cell types will in the end mask those differences. Another reason for it is because running with all genes/features also will take longer or might be computationally unfeasible. Therefore we will use the scaled data of the highly variable genes.
<- runUMAP(sce, exprs_values = "logcounts", name = "UMAP_on_ScaleData") sce
7.2 UMAP from graph
To run tSNE or UMAP on the a graph, we first need to build a graph from the data. In fact, both tSNE and UMAP first build a graph from the data using a specified distance matrix and then optimize the embedding. Since a graph is just a matrix containing distances from cell to cell and as such, you can run either UMAP or tSNE using any other distance metric desired. Euclidean and Correlation are usually the most commonly used.
# Build Graph
<- RANN::nn2(reducedDim(sce, "PCA"), k = 30)
nn names(nn) <- c("idx", "dist")
<- buildKNNGraph(sce, k = 30, use.dimred = "PCA")
g reducedDim(sce, "KNN") <- igraph::as_adjacency_matrix(g)
# Run UMAP and rename it for comparisson
# temp <- umap::umap.defaults
try(reducedDim(sce, "UMAP_on_Graph") <- NULL)
reducedDim(sce, "UMAP_on_Graph") <- uwot::umap(X = NULL, n_components = 2, nn_method = nn)
We can now plot the UMAP comparing both on PCA vs ScaledSata vs Graph.
wrap_plots(
plotReducedDim(sce, dimred = "UMAP_on_PCA", colour_by = "sample") +
::ggtitle(label = "UMAP_on_PCA"),
ggplot2plotReducedDim(sce, dimred = "UMAP_on_ScaleData", colour_by = "sample") +
::ggtitle(label = "UMAP_on_ScaleData"),
ggplot2plotReducedDim(sce, dimred = "UMAP_on_Graph", colour_by = "sample") +
::ggtitle(label = "UMAP_on_Graph"),
ggplot2ncol = 3
+ plot_layout(guides = "collect") )
8 Genes of interest
Let’s plot some marker genes for different cell types onto the embedding.
Markers | Cell Type |
---|---|
CD3E | T cells |
CD3E CD4 | CD4+ T cells |
CD3E CD8A | CD8+ T cells |
GNLY, NKG7 | NK cells |
MS4A1 | B cells |
CD14, LYZ, CST3, MS4A7 | CD14+ Monocytes |
FCGR3A, LYZ, CST3, MS4A7 | FCGR3A+ Monocytes |
FCER1A, CST3 | DCs |
<- list()
plotlist for (i in c("CD3E", "CD4", "CD8A", "NKG7", "GNLY", "MS4A1", "CD14", "LYZ", "MS4A7", "FCGR3A", "CST3", "FCER1A")) {
<- plotReducedDim(sce, dimred = "UMAP_on_PCA", colour_by = i, by_exprs_values = "logcounts") +
plotlist[[i]] scale_fill_gradientn(colours = colorRampPalette(c("grey90", "orange3", "firebrick", "firebrick", "red", "red"))(10)) +
ggtitle(label = i) + theme(plot.title = element_text(size = 20))
}wrap_plots(plotlist, ncol = 3)
Select some of your dimensionality reductions and plot some of the QC stats that were calculated in the previous lab. Can you see if some of the separation in your data is driven by quality of the cells?
<- list()
plotlist for (i in c("detected", "total", "subsets_mt_percent","subsets_ribo_percent","subsets_hb_percent")) {
<- plotReducedDim(sce, dimred = "UMAP_on_PCA", colour_by = i, by_exprs_values = "logcounts") +
plotlist[[i]] scale_fill_gradientn(colours = colorRampPalette(c("grey90", "orange3", "firebrick", "firebrick", "red", "red"))(10)) +
ggtitle(label = i) + theme(plot.title = element_text(size = 20))
}wrap_plots(plotlist, ncol = 3)
9 Save data
We can finally save the object for use in future steps.
saveRDS(sce, "data/covid/results/bioc_covid_qc_dr.rds")
10 Session info
Click here
sessionInfo()
R version 4.3.3 (2024-02-29)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: macOS Big Sur ... 10.16
Matrix products: default
BLAS/LAPACK: /Users/asabjor/miniconda3/envs/seurat5/lib/libopenblasp-r0.3.27.dylib; LAPACK version 3.12.0
locale:
[1] sv_SE.UTF-8/sv_SE.UTF-8/sv_SE.UTF-8/C/sv_SE.UTF-8/sv_SE.UTF-8
time zone: Europe/Stockholm
tzcode source: system (macOS)
attached base packages:
[1] stats4 stats graphics grDevices utils datasets methods
[8] base
other attached packages:
[1] umap_0.2.10.0 patchwork_1.2.0
[3] scran_1.30.0 scater_1.30.1
[5] ggplot2_3.5.1 scuttle_1.12.0
[7] SingleCellExperiment_1.24.0 SummarizedExperiment_1.32.0
[9] Biobase_2.62.0 GenomicRanges_1.54.1
[11] GenomeInfoDb_1.38.1 IRanges_2.36.0
[13] S4Vectors_0.40.2 BiocGenerics_0.48.1
[15] MatrixGenerics_1.14.0 matrixStats_1.4.1
loaded via a namespace (and not attached):
[1] bitops_1.0-8 gridExtra_2.3
[3] rlang_1.1.4 magrittr_2.0.3
[5] RcppAnnoy_0.0.22 compiler_4.3.3
[7] DelayedMatrixStats_1.24.0 png_0.1-8
[9] vctrs_0.6.5 pkgconfig_2.0.3
[11] crayon_1.5.3 fastmap_1.2.0
[13] XVector_0.42.0 labeling_0.4.3
[15] utf8_1.2.4 rmarkdown_2.28
[17] ggbeeswarm_0.7.2 xfun_0.47
[19] bluster_1.12.0 zlibbioc_1.48.0
[21] beachmat_2.18.0 jsonlite_1.8.8
[23] DelayedArray_0.28.0 BiocParallel_1.36.0
[25] irlba_2.3.5.1 parallel_4.3.3
[27] cluster_2.1.6 R6_2.5.1
[29] limma_3.58.1 reticulate_1.39.0
[31] Rcpp_1.0.13 knitr_1.48
[33] Matrix_1.6-5 igraph_2.0.3
[35] tidyselect_1.2.1 abind_1.4-5
[37] yaml_2.3.10 viridis_0.6.5
[39] codetools_0.2-20 lattice_0.22-6
[41] tibble_3.2.1 withr_3.0.1
[43] askpass_1.2.0 evaluate_0.24.0
[45] Rtsne_0.17 pillar_1.9.0
[47] generics_0.1.3 RCurl_1.98-1.16
[49] sparseMatrixStats_1.14.0 munsell_0.5.1
[51] scales_1.3.0 glue_1.7.0
[53] metapod_1.10.0 tools_4.3.3
[55] BiocNeighbors_1.20.0 ScaledMatrix_1.10.0
[57] RSpectra_0.16-2 locfit_1.5-9.9
[59] RANN_2.6.2 cowplot_1.1.3
[61] grid_4.3.3 edgeR_4.0.16
[63] colorspace_2.1-1 GenomeInfoDbData_1.2.11
[65] beeswarm_0.4.0 BiocSingular_1.18.0
[67] vipor_0.4.7 cli_3.6.3
[69] rsvd_1.0.5 fansi_1.0.6
[71] S4Arrays_1.2.0 viridisLite_0.4.2
[73] dplyr_1.1.4 uwot_0.1.16
[75] gtable_0.3.5 digest_0.6.37
[77] SparseArray_1.2.2 ggrepel_0.9.6
[79] dqrng_0.3.2 htmlwidgets_1.6.4
[81] farver_2.1.2 htmltools_0.5.8.1
[83] lifecycle_1.0.4 statmod_1.5.0
[85] openssl_2.2.1