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.

1 Setup

#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)

2 Read in data

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

3 Embed cells in a joint UMAP graph using conos

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

4 Metadata table

Generate one table/tibble with sample, condition, depth, mito.fraction, doublet.scores ordered by barcodes/cell names of conos object.

4.1 Metadata from conos

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)

4.2 Metadata from crm object

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)

5 UMAPs coloured by metadata

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.

6 Stacked barplots

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

7 Refine clustering

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

8 Cluster marker genes

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

8.1 Plot marker genes on UMAP

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

9 Annotation

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

10 Useful functions for refining and annotating clusters and subsetting cms

11 Final plots

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

11.1 UMAPs

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.

11.2 Dotplot of marker genes

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

11.3 Barplot of number of cells per cluster

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

11.4 Stacked barplots

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