This script integrates the samples into a joint UMAP graph and estimates clusters. Marker genes (= upregulated differentially expressed genes) are calculated for each cluster and provided to the user for annotation of the cell types. The clustering resolution can be adjusted based on the desired level of granularity, allowing either a rough or a more detailed cell-type annotation. Initially 2 different cluster granularities are provided to the user.
#Load helper functions
source("/people/nrq364/NeuSiC/scRNA_helper_new.R")
library(magrittr)
library(dplyr)
library(conos)
library(pagoda2)
library(qs2)
library(ggplot2)
library(ggrastr)
library(openxlsx)
library(tidyverse)
Read in filtered list of count matrices generated in notebook 1) Pre-processing and QC filtering.
cms <- qs_read("cms_filtered.qs2", nthreads = 10)
Table with dimensions of count matrices: genes x cells. As well as total number of cells.
sapply(cms, dim)
## control_1 control_2 control_4 control_5 control_6 control_7 treated_1 treated_2 treated_3 treated_4 treated_6 treated_7
## [1,] 32285 32285 32285 32285 32285 32285 32285 32285 32285 32285 32285 32285
## [2,] 898 2341 1579 790 3468 2505 1080 754 1045 2198 2695 3032
# sum of all cells
sum(sapply(cms, function(x) dim(x)[2]))
## [1] 22385
Create a vector with sample names.
names <- paste0(names(cms), "_")
Use a helper function (defined in the script scRNA_helper_new.R) to embed cells from all samples in a joint UMAP graph and perform first clustering. This helper function performs pagoda2 pre-processing and normalization of each count matrix, then creates a conos object and integrates the samples into a joint UMAP graph. Alignment strength can be adjusted to force stronger integration of samples.
con <- quickConos(cms,
names,
n.cores.p2=10,
n.cores.con=20, get.tsne = F, alignment.strength=0)
con <- con$con
Investigate how cells cluster on the UMAP. Are there any batches visible (clustering by sample, by condition)? If yes, alignment.strength parameter can be increased. The embedding used here was created with alignment.strength = 0.2.
Plot UMAP coloured by sample.
data <- con$plotGraph(groups=con$getDatasetPerCell())
data <- data$data
ggplot(data, aes(x = x, y =y, color= Group))+ geom_point(size=0.2, alpha=0.5)+ theme_light()+ theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("")+ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())+labs(color = "Sample") +guides(color = guide_legend(override.aes = list(size=3, alpha=1)))+scale_color_manual(values=viridisLite::turbo(12))
#ggsave("img/2_clustering/fig0.png")
Plot UMAP coloured by cluster.
con$plotGraph() + theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("")+ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())
#ggsave("img/2_clustering/fig1.png")
Generate one table/tibble with sample, condition, depth, mito.fraction, doublet.scores ordered by barcodes/cell names of conos object.
From conos object we save conos_id, sample, condition, and cell barcode in a table/tibble.
clusters <- con$clusters$leiden$groups
# initiate the data frame
df <- tibble(con_id = names(clusters), cluster = as.integer(as.character(clusters)))
df %<>% separate(con_id, into = c("sample", "barcode"), sep = "!!", remove=F)
df %<>% mutate(condition = sub("_[0-9]$", "", sample)) # generate condition column by replacing the "_x" part with "" (nothing)
#df %<>% mutate(barcode = sub("^[^_]+_[0-9]+_", "", barcode))
# add a "order" column to see in case rows get mixed up. Necessary to keep same order as in conos object.
df %<>% mutate(order = row_number())
head(df)
We add doublet scores, depth (UMI counts per cell), and mito gene fraction from the CRMetrics object to the metadata table.
Get doublet scores and add to metadata table
scrublet <- crm$doublets$scrublet$result$scores
names(scrublet) <- row.names(crm$doublets$scrublet$result)
new_column <- tibble(con_id = row.names(crm$doublets$scrublet$result),scrublet = crm$doublets$scrublet$result$scores)
df %<>% left_join(new_column, by = "con_id")
Get depth and add to metadata table.
depth <- crm$getDepth()
new_column <- tibble(con_id = names(depth), UMI_counts = depth)
df %<>% left_join(new_column, by = "con_id")
Get mito gene fraction and add to metadata table.
mito <- crm$getMitoFraction()
new_column <- tibble(con_id = names(mito), mito_gene_fraction = mito)
df %<>% left_join(new_column, by = "con_id")
head(df)
We colour the UMAP by different variables from the metadata to evaluate the embedding, clustering and QC filtering.
# How to use the metadata to colour UMAP: generate a named vector feed into "groups" parameter
setNames(df$desired_metadata_column, df$con_id)
UMAP coloured by condition
condition <- setNames(df$condition, df$con_id)
data <- con$plotGraph(groups=condition)
data <- data$data
ggplot(data, aes(x = x, y =y, color= Group))+ geom_point(size=0.2, alpha=0.5)+ theme_light()+ theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("") + ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())+labs(color = "Condition") +guides(color = guide_legend(override.aes = list(size=3, alpha=1))) +scale_color_manual(values=rainbow(2))
#ggsave("img/2_clustering/fig2.png")
UMAP coloured by doublet scores
doubletscores <- setNames(df$scrublet, df$con_id)
data <- con$plotGraph()
data <- data$data
ggplot(data, aes(x = x, y =y, color=doubletscores))+ geom_point(size=0.2, alpha=0.5)+ theme_light()+ theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("")+ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())+ scale_color_viridis_b(option = "viridis", direction = -1) # change _b to _c for continuous coloring scale
#ggsave("img/2_clustering/fig3.png")
UMAP coloured by UMI counts.
depth <- setNames(df$UMI_counts, df$con_id)
data <- con$plotGraph()
data <- data$data
ggplot(data, aes(x = x, y =y, color=depth))+ geom_point(size=0.2, alpha=0.5)+ theme_light()+ theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("")+ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())+ scale_color_viridis_c(option = "viridis", direction = -1) # change _b to _c for continuous coloring scale
#ggsave("img/2_clustering/fig4.png")
UMAP can be coloured by various metadata to confirm QC filtering was successful and we do not see clusters of low quality cells. Save conos object if satisfied with sample integration. Otherwise adjust alignment strength parameter.
Plot composition of samples per cluster. We expect that each sample is represented in each cluster and not that one cluster is composed of one/a few samples.
plotClusterBarplots(con, groups = con$clusters$leiden$groups , show.entropy = F, show.size = F) + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + scale_fill_discrete(name = "condition") + theme(plot.margin = margin(0.5,0,0,2, "cm"))
#ggsave("img/2_clustering/fig5.png")
Plot composition of
conditions per cluster.
condition <- setNames(df$condition, df$con_id)
plotClusterBarplots(con, groups = con$clusters$leiden$groups , show.entropy = F, show.size = F, sample.factor = condition) + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + scale_fill_discrete(name = "condition") + theme(plot.margin = margin(0.5,0,0,2, "cm"))
#ggsave("img/2_clustering/fig6.png")
If needed, the leiden clustering algorithm can be rerun to refine the clusters (change resolution parameter). It is not performed here.
con$findCommunities(method = leiden.community, resolution = 4, min.group.size = 50)
Plot new clusters in embedding and also in a table.
con$plotGraph(groups = con$clusters$leiden$groups %>% factor, title="")
table(con$clusters$leiden$groups %>% factor)
Get the clustering factor from conos object.
leiden_number_of_clusters <- con$clusters$leiden$groups %>% factor
Calculate upregulated genes per cluster (= marker genes), use only control cells, because treatment can alter marker gene expression. Use the leiden factor saved above or within the conos object and extract only the ctrl cells.
leiden_xx <- con$clusters$leiden$groups %>% factor
leiden_xx_ctrl <- leiden_xx[grep("control", names(leiden_xx))]
length(leiden_xx_ctrl)
Then, get marker genes per cluster using only control cells. Determine differential expressed genes, comparing each group against all others using Wilcoxon rank sum test.
# uses pagoda2's "getDifferentialGenes()"
de_conos <- con$getDifferentialGenes(groups=leiden_xx_ctrl, append.auc = T, upregulated.only = T)
Ordering the genes by AUC prioritizes genes that distinguish the respective cluster well from the rest.
de_conos %<>% lapply(function(x) {x %>% arrange(desc(AUC)) %>% head()})
head(de_conos$`1`)
The following parameters are reported:
- M = log2 fold change
- Z = adjusted Z score, with positive values indicating higher
expression in a given group compare to the rest
- PValue = significance corresponding to the test
- PAdj = adjusted P-value
- AUC = discrimination between cluster and other cells
- Specificity, Precision
- ExpressionFraction = sum expression of the gene within the cluster
divided by the total expression of this gene
Export top 30 genes for users to explore. Transform the list of marker genes into one big table by adding the column “Clusters”. Order by AUC and save csv file with top 30 genes.
library(data.table)
de_conos <- lapply(de_conos, function(x) {x %>% arrange(desc(AUC)) %>% head(30)})
write.xlsx(de_table, file = "top30_marker_byAUC.csv")
If user gave a list of expected cell types and marker genes those genes can be plotted here.
#plot several genes
genes <- c("Gad1", "Gad2", "Lhx2", "Ascl1")
plots <- lapply(genes, function(gene) {
con$plotGraph(gene = gene, title = gene, size = 0.2) + theme(legend.position = "right", legend.title = element_text("Expression"))+ scale_color_viridis_c(direction = -1)
})
cowplot::plot_grid(plotlist = plots)
#ggsave("img/2_clustering/fig7.png")
Cell type annotation is manually performed by the user who is the
expert in the respective tissue. CyteTypeR, a multi-agent AI system
where specialized agents collaborate on marker analysis, literature
evidence, and Cell Ontology mapping, can be used as guidance. MapMyCells,
which uses the Allen Institutes taxonomies, is currently tested as an
alternative.
The final annotation is stored as a factor “anno” and is also added to
the metadata table
new_column <- tibble(con_id = names(anno), annotation = anno)
df %<>% left_join(new_column, by = "con_id")
Final UMAP coloured by cluster It is possible to define custom colours of clusters.
colours <- c( "#FF0000" ,"#FF6000", "#FFBF00", "#DFFF00" ,"#00FFFF" , "#20FF00", "#00FF40", "#DF00FF","#80FF00","#009FFF", "#0040FF","#FF0060" , "#8000FF", "#00FF9F", "#FF00BF","#2000FF" ) %>% setNames(c("Cajal_Retzius_cells", "CGE_progenitors","Cortical_hem" , "Dorsal_IPC" , "Immature_GABAergic_N","Highly_prolif_RG_S" , "Immature_PN", "Immature_PN_Tbr1","MGE_progenitors","Microglia",
"Dorsal_RG" , "Anteriomedial_RG", "Lateral_RG", "PIC_immature_PN", "Roof_plate","Highly_prolif_RG_G2M"))
UMAP without cell type annotation, so user can add cell type names.
con$plotGraph(groups = anno, color.by="cluster",mark.groups=F, size=0.2) + theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ scale_colour_manual(values=colours)
#ggsave("img/2_clustering/fig8.png")
UMAP with cell type names
con$plotGraph(groups=anno, color.by="cluster",mark.groups=T, size=0.2) + theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ scale_colour_manual(values=colours)
#ggsave("img/2_clustering/fig9.png")
UMAP with cell type names in legend
data <- con$plotGraph()
data <- data$data
ggplot(data, aes(x = x, y =y, color=Group))+ geom_point(size=0.2, alpha=0.5)+ theme_light()+ theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())+ xlab("")+ylab("") + theme(axis.ticks = element_blank(), axis.text =element_blank())+labs(color = "Cluster") +guides(color = guide_legend(override.aes = list(size=3, alpha=1)))+scale_color_manual(values=colours)
#ggsave("img/2_clustering/fig10.png")
UMAP coloured by any metadata can be generated.
Join the list of count matrices into one count matrix.
cm <- con$getJointCountMatrix(raw=F)
Create vector of relevant marker genes.
markers <- c("Trp73", "Reln", 'Eomes','Tbr1','Slc17a6' ,'Dcx','Thsd7b','Neurod1' ,'Neurog2','Btg2','Gad2','Dlx1','Nkx2-1','Shh','Nr2f2','Ptprz1','Wnt3','Rspo3', 'Rspo2', 'Bmp6',"Pax6",'Mcm4','Sox2','Nes','Rplp1','Meis2','Mpped1','Zic1','Zic4','Emx2', 'Creb5','Csf1r', "Spi1")
Order cell types in annotation factor in costume order.
anno_order <- factor(anno, levels=c('Cajal_Retzius_cells', 'Immature_PN_Tbr1' , 'Immature_PN','PIC_immature_PN', 'Dorsal_IPC','Immature_GABAergic_N','MGE_progenitors','CGE_progenitors', 'Roof_plate', 'Cortical_hem','Highly_prolif_RG_S','Highly_prolif_RG_G2M','Lateral_RG', 'Anteriomedial_RG', 'Dorsal_RG', 'Microglia'))
Create dot plot of marker genes.
sccore::dotPlot(markers, cm, cell.groups = anno_order, gene.order = T, xlab = "", ylab = "", cols=c("white","#0E4B99")) +theme_light()+ theme(axis.text.x=element_text(angle=90, hjust=1, vjust=0.5))+ theme(text = element_text(size = 11)) + theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank())
#ggsave("img/2_clustering/fig11.png")
anno_table <- as.data.frame(table(anno_order))
ggplot(anno_table, aes(anno_order, Freq)) + geom_bar(stat = "identity",fill="#2F5496", alpha=0.2, color="#2F5496", width=0.8) + xlab("") + ylab("number of nuclei") + theme_light()+ theme(axis.text.x=element_text(angle=90, hjust = 1, vjust = 0.5)) + theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank()) +
theme(axis.title.y = element_text(size = 9), axis.text.x = element_text(size = 9),axis.text.y = element_text(size = 9))
#ggsave("img/2_clustering/fig12.png")
Stacked barplots of composition of samples per cluster
plotClusterBarplots(con, groups = anno , show.entropy = F, show.size = F) + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + scale_fill_discrete(name = "condition") +
theme(plot.margin = margin(0.5,0,0,2, "cm"))
#ggsave("img/2_clustering/fig13.png")
Stacked barplots of composition of conditions per cluster
condition <- setNames(df$condition, df$con_id)
plotClusterBarplots(con, groups = anno , show.entropy = F, show.size = F, sample.factor = condition) + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + scale_fill_discrete(name = "condition") +
theme(plot.margin = margin(0.5,0,0,2, "cm"))
#ggsave("img/2_clustering/fig14.png")
sessionInfo()
## R version 4.5.3 (2026-03-11)
## Platform: x86_64-redhat-linux-gnu
## Running under: Red Hat Enterprise Linux 8.10 (Ootpa)
##
## Matrix products: default
## BLAS/LAPACK: /usr/lib64/libopenblaso-r0.3.15.so; LAPACK version 3.9.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8 LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 LC_PAPER=en_US.UTF-8
## [8] LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Europe/Copenhagen
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] lubridate_1.9.5 forcats_1.0.1 stringr_1.6.0 purrr_1.2.2 readr_2.2.0 tidyr_1.3.2 tibble_3.3.1 tidyverse_2.0.0 openxlsx_4.2.9 ggrastr_1.0.2 ggplot2_4.0.3 qs2_0.3.1
## [13] pagoda2_1.0.15 conos_1.5.4 igraph_2.3.3 Matrix_1.7-4 dplyr_1.2.1 magrittr_2.0.3
##
## loaded via a namespace (and not attached):
## [1] RcppAnnoy_0.0.23 splines_4.5.3 later_1.4.2 urltools_1.7.3.1 R.oo_1.27.1 triebeard_0.4.1 polyclip_1.10-7 fastDummies_1.7.6
## [9] lifecycle_1.0.5 doParallel_1.0.17 globals_0.19.1 processx_3.9.0 lattice_0.22-9 MASS_7.3-65 plotly_4.12.1 sass_0.4.10
## [17] rmarkdown_2.31 remotes_2.5.0 jquerylib_0.1.4 yaml_2.3.12 httpuv_1.6.16 otel_0.2.0 Seurat_5.5.1 sctransform_0.4.3
## [25] zip_3.0.2 spam_2.11-4 sessioninfo_1.2.4 pkgbuild_1.4.8 sp_2.2-3 spatstat.sparse_3.2-0 reticulate_1.47.0 cowplot_1.2.0
## [33] pbapply_1.7-5 RColorBrewer_1.1-3 pkgload_1.5.3 abind_1.4-8 Rtsne_0.17 R.utils_2.13.0 BiocGenerics_0.56.0 circlize_0.4.18
## [41] IRanges_2.44.0 S4Vectors_0.48.1 ggrepel_0.9.8 RMTstat_0.3.2 irlba_2.3.7 listenv_1.0.0 spatstat.utils_3.2-4 goftest_1.2-3
## [49] RSpectra_0.16-2 spatstat.random_3.5-1 brew_1.0-10 fitdistrplus_1.2-6 parallelly_1.48.0 codetools_0.2-20 tidyselect_1.2.1 shape_1.4.6.1
## [57] farver_2.1.2 matrixStats_1.5.0 stats4_4.5.3 spatstat.explore_3.8-2 jsonlite_2.0.0 GetoptLong_1.1.1 ellipsis_0.3.3 Rook_1.2.1
## [65] progressr_1.0.0 ggridges_0.5.7 survival_3.8-6 iterators_1.0.14 systemfonts_1.3.2 foreach_1.5.2 tools_4.5.3 ica_1.0-3
## [73] Rcpp_1.1.2 glue_1.8.0 gridExtra_2.3.1 xfun_0.60 mgcv_1.9-4 usethis_3.2.1 withr_3.0.2 fastmap_1.2.0
## [81] callr_3.8.0 digest_0.6.37 timechange_0.4.0 R6_2.6.1 mime_0.13 textshaping_1.0.5 colorspace_2.1-3 scattermore_1.2
## [89] N2R_1.0.5 sccore_1.0.7 tensor_1.5.1 spatstat.data_3.1-9 R.methodsS3_1.8.2 utf8_1.2.6 generics_0.1.4 data.table_1.18.6.1
## [97] httr_1.4.9 htmlwidgets_1.6.4 uwot_0.2.5 pkgconfig_2.0.3 gtable_0.3.6 ComplexHeatmap_2.26.1 lmtest_0.9-40 S7_0.2.2
## [105] htmltools_0.5.8.1 dotCall64_1.2 clue_0.3-68 SeuratObject_5.4.0 scales_1.4.0 dendsort_0.3.4 png_0.1-9 spatstat.univar_3.2-0
## [113] knitr_1.51 rstudioapi_0.19.0 tzdb_0.5.0 reshape2_1.4.5 rjson_0.2.23 nlme_3.1-168 curl_8.0.0 cachem_1.1.0
## [121] zoo_1.9-0 GlobalOptions_0.1.4 KernSmooth_2.23-26 vipor_0.4.7 drat_0.2.5 parallel_4.5.3 miniUI_0.1.2 desc_1.4.3
## [129] pillar_1.11.1 grid_4.5.3 vctrs_0.7.3 RANN_2.6.3 promises_1.3.3 stringfish_0.19.2 xtable_1.8-4 cluster_2.1.8.2
## [137] beeswarm_0.4.0 evaluate_1.0.5 cli_3.6.6 compiler_4.5.3 rlang_1.3.0 crayon_1.5.3 future.apply_1.20.2 labeling_0.4.3
## [145] ps_1.9.3 ggbeeswarm_0.7.3 plyr_1.8.9 fs_2.1.0 stringi_1.8.9 viridisLite_0.4.3 deldir_2.0-4 devtools_2.5.2
## [153] spatstat.geom_3.8-2 RcppHNSW_0.7.0 hms_1.1.4 patchwork_1.3.2 future_1.75.0 shiny_1.11.1 ROCR_1.0-12 leidenAlg_1.1.8
## [161] memoise_2.0.1 RcppParallel_6.2.1 bslib_0.9.0