7 Niche annotation using KNN
Load the psuedobulked cell type references
# A function to get the niche counts out of the Seurat objects
grab_nichecounts <- function(xenium_object, k_param, cell_types, fov ="fov"){
# Steal from the Seurat BuildNicheAssay function to get nearest neighbours
coords <- GetTissueCoordinates(xenium_object[[fov]], which = "centroids")
cells <- coords$cell
rownames(coords) <- cells
coords <- as.matrix(coords[, c("x", "y")])
neighbors <- FindNeighbors(coords, k.param = k_param)
ct.mtx <- matrix(data = 0, nrow = length(cells), ncol = length(unlist(unique(xenium_object[[cell_types]]))))
rownames(ct.mtx) <- cells
colnames(ct.mtx) <- unique(unlist(xenium_object[[cell_types]]))
cts <- xenium_object[[cell_types]]
for (i in 1:length(cells)) {
ct <- as.character(cts[cells[[i]], ])
ct.mtx[cells[[i]], ct] <- 1
}
niche_counts <- as.matrix(neighbors$nn %*% ct.mtx)%>%t()
niche_counts[1:5,1:5]
return(niche_counts)
}
Get each cell’s 50 neareast neighbours
p1_nichecounts_detailed <- grab_nichecounts(cropped.obj, 50,cell_types = "MMR_atlas_lowlevel_pred", fov = "zoom")
p1_nichecounts_detailed[1:5,1:5]
#> bggahoon-1 bggaimpb-1 bggalkpc-1
#> cE01 (Stem/TA-like) 17 18 17
#> cE03 (Stem/TA-like prolif) 30 31 29
#> cE11 (Enteroendocrine) 0 0 0
#> cM06 (DC IL22RA2) 0 0 0
#> cTNI24 (NK GZMK+) 0 0 0
#> bggbeool-1 bggbnono-1
#> cE01 (Stem/TA-like) 18 16
#> cE03 (Stem/TA-like prolif) 30 28
#> cE11 (Enteroendocrine) 0 0
#> cM06 (DC IL22RA2) 0 0
#> cTNI24 (NK GZMK+) 0 0
Variable features (cell types) in the data
combined_niche_seu <- CreateSeuratObject(p1_nichecounts_detailed)
combined_niche_seu <- NormalizeData(combined_niche_seu, scale.factor =1)
combined_niche_seu <- FindVariableFeatures(combined_niche_seu)
top20 <- head(VariableFeatures(combined_niche_seu), 20)
plot1 <- VariableFeaturePlot(combined_niche_seu)
plot2 <- LabelPoints(plot = plot1, points = top20, repel = TRUE)
plot2

Elbow plot of niche-level PCA
combined_niche_seu <- ScaleData(combined_niche_seu)
combined_niche_seu <- RunPCA(combined_niche_seu, features = VariableFeatures(object = combined_niche_seu))
DimPlot(combined_niche_seu, reduction = "pca") + NoLegend()

ElbowPlot(combined_niche_seu,ndims = 30)+
geom_vline(xintercept = 5, colour="blue", linetype = "longdash")

UMAP of KNN matrix
# Even running this with 10 PCs look ok, but maybe a bit complicated
combined_niche_seu <- FindNeighbors(combined_niche_seu, dims = 1:5)
# Setting a low resolution so we don't end up with an unmanageable number of niches
combined_niche_seu <- FindClusters(combined_niche_seu)
#> Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck
#>
#> Number of nodes: 16537
#> Number of edges: 449088
#>
#> Running Louvain algorithm...
#> Maximum modularity in 10 random starts: 0.9618
#> Number of communities: 64
#> Elapsed time: 1 seconds
combined_niche_seu <- FindClusters(combined_niche_seu, resolution = 0.1)
#> Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck
#>
#> Number of nodes: 16537
#> Number of edges: 449088
#>
#> Running Louvain algorithm...
#> Maximum modularity in 10 random starts: 0.9869
#> Number of communities: 36
#> Elapsed time: 1 seconds
combined_niche_seu <- RunUMAP(combined_niche_seu, dims = 1:5)
DimPlot(combined_niche_seu, reduction = "umap", group.by = "seurat_clusters", label = T)+NoLegend()

marks <- FindAllMarkers(combined_niche_seu)
Plot the niches on the slide
# Check everything lines up
all.equal(colnames(combined_niche_seu), colnames(cropped.obj))
#> [1] TRUE
cropped.obj$Niche <- combined_niche_seu$seurat_clusters
# Plot the 42 predicted niches
plot <- ImageDimPlot(cropped.obj,fov = "zoom", group.by = "Niche",
size = 0.3, cols = "polychrome",
dark.background = T, boundaries = "segmentation",border.size = 0.1,
nmols = 20000) +
ggtitle("Niches")
plot

7.1 Plot the niche composition in terms of cell type makeup
niche_makeup_gut <- cropped.obj@meta.data %>%
group_by(Niche) %>%
mutate(Total = n()) %>%
group_by(MMR_atlas_midlevel_pred, Niche, Total) %>%
summarise(Count = n(), .groups = "drop") %>%
mutate(Niche_pct = Count / Total * 100) %>%
arrange(Niche) %>%
mutate(Niche = factor(Niche, levels = unique(Niche)))
# Auto name the niches
niche_names <- niche_makeup_gut %>%
group_by(Niche) %>%
arrange(desc(Niche_pct), .by_group = TRUE) %>%
slice_head(n = 3) %>%
mutate(label = paste0(MMR_atlas_midlevel_pred, " (", round(Niche_pct), "%)")) %>%
summarise(Niche_name = paste(label, collapse = " – "), .groups = "drop")
niche_makeup_gut <- niche_makeup_gut %>%
left_join(niche_names, by = "Niche")
# Create a mapping vector from cluster to name
niche_name_vec <- setNames(niche_names$Niche_name, niche_names$Niche)
# Assign to metadata
cropped.obj$Niche_named <-as.character(niche_name_vec[as.character(cropped.obj$Niche)])
ggplot(data = niche_makeup_gut, aes(x = Niche, y = Niche_pct, fill = MMR_atlas_midlevel_pred))+
geom_bar(stat = "identity")+
labs(x = "Niche", y = "Celltype % of niche", fill = "Cell type")+
blank_theme+
theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1))