library(knitr) knitr::opts_chunk$set( cache=TRUE, warning=FALSE, message=FALSE, cache.lazy=FALSE )
tidySingleCellExperiment provides a bridge between Bioconductor single-cell packages [@amezquita2019orchestrating] and the tidyverse [@wickham2019welcome]. It creates an invisible layer that enables viewing the Bioconductor SingleCellExperiment object as a tidyverse tibble, and provides SingleCellExperiment-compatible dplyr, tidyr, ggplot and plotly functions. This allows users to get the best of both Bioconductor and tidyverse worlds.
SingleCellExperiment-compatible Functions | Description
------------ | -------------
all
| After all tidySingleCellExperiment
is a SingleCellExperiment object, just better
tidyverse Packages | Description
------------ | -------------
dplyr
| All dplyr
tibble functions (e.g. tidySingleCellExperiment::select
)
tidyr
| All tidyr
tibble functions (e.g. tidySingleCellExperiment::pivot_longer
)
ggplot2
| ggplot
(tidySingleCellExperiment::ggplot
)
plotly
| plot_ly
(tidySingleCellExperiment::plot_ly
)
Utilities | Description
------------ | -------------
tidy
| Add tidySingleCellExperiment
invisible layer over a SingleCellExperiment object
as_tibble
| Convert cell-wise information to a tbl_df
join_transcripts
| Add transcript-wise information, returns a tbl_df
if (!requireNamespace("BiocManager", quietly=TRUE)) install.packages("BiocManager") BiocManager::install("tidySingleCellExperiment")
Load libraries used in this vignette.
# Bioconductor single-cell packages library(scater) library(scran) library(SingleR) library(SingleCellSignalR) # Tidyverse-compatible packages library(ggplot2) library(purrr) library(tidyHeatmap) # Both library(tidySingleCellExperiment)
tidySingleCellExperiment
, the best of both worlds!This is a SingleCellExperiment object but it is evaluated as a tibble. So it is compatible both with SingleCellExperiment and tidyverse.
pbmc_small_tidy <- tidySingleCellExperiment::pbmc_small %>% tidy()
It looks like a tibble
pbmc_small_tidy
But it is a SingleCellExperiment object after all
assay(pbmc_small_tidy)
We may have a column that contains the directory each run was taken from, such as the "file" column in pbmc_small_tidy
.
pbmc_small_tidy$file[1:5]
We may want to extract the run/sample name out of it into a separate column. Tidyverse extract
can be used to convert a character column into multiple columns using regular expression groups.
# Create sample column pbmc_small_polished <- pbmc_small_tidy %>% extract(file, "sample", "../data/([a-z0-9]+)/outs.+", remove=FALSE) # Reorder to have sample column up front pbmc_small_polished %>% select(sample, everything())
Set colours and theme for plots.
# Use colourblind-friendly colours friendly_cols <- dittoSeq::dittoColors() # Set theme custom_theme <- list( scale_fill_manual(values=friendly_cols), scale_color_manual(values=friendly_cols), theme_bw() + theme( panel.border=element_blank(), axis.line=element_line(), panel.grid.major=element_line(size=0.2), panel.grid.minor=element_line(size=0.1), text=element_text(size=12), legend.position="bottom", aspect.ratio=1, strip.background=element_blank(), axis.title.x=element_text(margin=margin(t=10, r=10, b=10, l=10)), axis.title.y=element_text(margin=margin(t=10, r=10, b=10, l=10)) ) )
We can treat pbmc_small_polished
as a tibble for plotting.
Here we plot number of transcripts per cell.
pbmc_small_polished %>% tidySingleCellExperiment::ggplot(aes(nFeature_RNA, fill=groups)) + geom_histogram() + custom_theme
Here we plot total transcripts per cell.
pbmc_small_polished %>% tidySingleCellExperiment::ggplot(aes(groups, nCount_RNA, fill=groups)) + geom_boxplot(outlier.shape=NA) + geom_jitter(width=0.1) + custom_theme
Here we plot abundance of two transcripts for each group.
pbmc_small_polished %>% join_transcripts(transcripts=c("HLA-DRA", "LYZ")) %>% ggplot(aes(groups, abundance_counts + 1, fill=groups)) + geom_boxplot(outlier.shape=NA) + geom_jitter(aes(size=nCount_RNA), alpha=0.5, width=0.2) + scale_y_log10() + custom_theme
We can also treat pbmc_small_polished
as a SingleCellExperiment object and proceed with data processing with Bioconductor packages, such as scran [@lun2016pooling] and scater [@mccarthy2017scater].
# Identify variable genes with scran variable_genes <- pbmc_small_polished %>% modelGeneVar() %>% getTopHVGs(prop=0.1) # Perform PCA with scater pbmc_small_pca <- pbmc_small_polished %>% runPCA(subset_row=variable_genes) pbmc_small_pca
If a tidyverse-compatible package is not included in the tidySingleCellExperiment collection, we can use as_tibble
to permanently convert tidySingleCellExperiment
into a tibble.
# Create pairs plot with GGally pbmc_small_pca %>% as_tibble() %>% select(contains("PC"), everything()) %>% GGally::ggpairs(columns=1:5, ggplot2::aes(colour=groups)) + custom_theme
We can proceed with cluster identification with scran.
pbmc_small_cluster <- pbmc_small_pca # Assign clusters to the 'colLabels' of the SingleCellExperiment object colLabels(pbmc_small_cluster) <- pbmc_small_pca %>% buildSNNGraph(use.dimred="PCA") %>% igraph::cluster_walktrap() %$% membership %>% as.factor() # Reorder columns pbmc_small_cluster %>% select(label, everything())
And interrogate the output as if it was a regular tibble.
# Count number of cells for each cluster per group pbmc_small_cluster %>% tidySingleCellExperiment::count(groups, label)
We can identify and visualise cluster markers combining SingleCellExperiment, tidyverse functions and tidyHeatmap [@mangiola2020tidyheatmap]
# Identify top 10 markers per cluster marker_genes <- pbmc_small_cluster %>% findMarkers(groups=pbmc_small_cluster$label) %>% as.list() %>% map(~ .x %>% head(10) %>% rownames()) %>% unlist() # Plot heatmap pbmc_small_cluster %>% join_transcripts(transcripts=marker_genes) %>% group_by(label) %>% heatmap(transcript, cell, abundance_counts, .scale="column")
We can calculate the first 3 UMAP dimensions using the SingleCellExperiment framework and scater.
pbmc_small_UMAP <- pbmc_small_cluster %>% runUMAP(ncomponents=3)
And we can plot the result in 3D using plotly.
pbmc_small_UMAP %>% plot_ly( x=~`UMAP1`, y=~`UMAP2`, z=~`UMAP3`, color=~label, colors=friendly_cols[1:4] )
We can infer cell type identities using SingleR [@aran2019reference] and manipulate the output using tidyverse.
# Get cell type reference data blueprint <- celldex::BlueprintEncodeData() # Infer cell identities cell_type_df <- assays(pbmc_small_UMAP)$logcounts %>% Matrix::Matrix(sparse = TRUE) %>% SingleR::SingleR( ref = blueprint, labels = blueprint$label.main, method = "single" ) %>% as.data.frame() %>% as_tibble(rownames="cell") %>% select(cell, first.labels)
# Join UMAP and cell type info pbmc_small_cell_type <- pbmc_small_UMAP %>% left_join(cell_type_df, by="cell") # Reorder columns pbmc_small_cell_type %>% tidySingleCellExperiment::select(cell, first.labels, everything())
We can easily summarise the results. For example, we can see how cell type classification overlaps with cluster classification.
# Count number of cells for each cell type per cluster pbmc_small_cell_type %>% count(label, first.labels)
We can easily reshape the data for building information-rich faceted plots.
pbmc_small_cell_type %>% # Reshape and add classifier column pivot_longer( cols=c(label, first.labels), names_to="classifier", values_to="label" ) %>% # UMAP plots for cell type and cluster ggplot(aes(UMAP1, UMAP2, color=label)) + geom_point() + facet_wrap(~classifier) + custom_theme
We can easily plot gene correlation per cell category, adding multi-layer annotations.
pbmc_small_cell_type %>% # Add some mitochondrial abundance values mutate(mitochondrial=rnorm(dplyr::n())) %>% # Plot correlation join_transcripts(transcripts=c("CST3", "LYZ"), shape="wide") %>% ggplot(aes(CST3 + 1, LYZ + 1, color=groups, size=mitochondrial)) + geom_point() + facet_wrap(~first.labels, scales="free") + scale_x_log10() + scale_y_log10() + custom_theme
A powerful tool we can use with tidySingleCellExperiment is tidyverse nest
. We can easily perform independent analyses on subsets of the dataset. First we classify cell types into lymphoid and myeloid, and then nest based on the new classification.
pbmc_small_nested <- pbmc_small_cell_type %>% filter(first.labels != "Erythrocytes") %>% mutate(cell_class=dplyr::if_else(`first.labels` %in% c("Macrophages", "Monocytes"), "myeloid", "lymphoid")) %>% nest(data=-cell_class) pbmc_small_nested
Now we can independently for the lymphoid and myeloid subsets (i) find variable features, (ii) reduce dimensions, and (iii) cluster using both tidyverse and SingleCellExperiment seamlessly.
pbmc_small_nested_reanalysed <- pbmc_small_nested %>% mutate(data=map( data, ~ { .x <- runPCA(.x, subset_row=variable_genes) variable_genes <- .x %>% modelGeneVar() %>% getTopHVGs(prop=0.3) colLabels(.x) <- .x %>% buildSNNGraph(use.dimred="PCA") %>% igraph::cluster_walktrap() %$% membership %>% as.factor() .x %>% runUMAP(ncomponents=3) } )) pbmc_small_nested_reanalysed
We can then unnest and plot the new classification.
pbmc_small_nested_reanalysed %>% # Convert to tibble otherwise SingleCellExperiment drops reduced dimensions when unifying data sets. mutate(data=map(data, ~ .x %>% as_tibble())) %>% unnest(data) %>% # Define unique clusters unite("cluster", c(cell_class, label), remove=FALSE) %>% # Plotting ggplot(aes(UMAP1, UMAP2, color=cluster)) + geom_point() + facet_wrap(~cell_class) + custom_theme
We can perform a large number of functional analyses on data subsets. For example, we can identify intra-sample cell-cell interactions using SingleCellSignalR [@cabello2020singlecellsignalr], and then compare whether interactions are stronger or weaker across conditions. The code below demonstrates how this analysis could be performed. It won't work with this small example dataset as we have just two samples (one for each condition). But some example output is shown below and you can imagine how you can use tidyverse on the output to perform t-tests and visualisation.
pbmc_small_nested_interactions <- pbmc_small_nested_reanalysed %>% # Unnest based on cell category unnest(data) %>% # Create unambiguous clusters mutate(integrated_clusters=first.labels %>% as.factor() %>% as.integer()) %>% # Nest based on sample tidySingleCellExperiment::nest(data=-sample) %>% tidySingleCellExperiment::mutate(interactions=map(data, ~ { # Produce variables. Yuck! cluster <- colData(.x)$integrated_clusters data <- data.frame(assays(.x) %>% as.list() %>% .[[1]] %>% as.matrix()) # Ligand/Receptor analysis using SingleCellSignalR data %>% cell_signaling(genes=rownames(data), cluster=cluster) %>% inter_network(data=data, signal=., genes=rownames(data), cluster=cluster) %$% `individual-networks` %>% map_dfr(~ bind_rows(as_tibble(.x))) })) pbmc_small_nested_interactions %>% select(-data) %>% unnest(interactions)
If the dataset was not so small, and interactions could be identified, you would see something like below.
tidySingleCellExperiment::pbmc_small_nested_interactions
sessionInfo()
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.