---
title: "Using concordex in place of UMAP in scRNA-seq"
author: "Lambda Moses"
date: "`r format(Sys.Date(), '%b %d, %Y')`"
output:
    BiocStyle::html_document:
        toc: true
        number_sections: true
        toc_depth: 3
        toc_float:
            collapsed: true
bibliography: ref.bib
vignette: >
  %\VignetteIndexEntry{concordex-demo}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

# Introduction

UMAP is commonly used in scRNA-seq data analysis as a visualization tool
projecting high dimensional data onto 2 dimensions to visualize cell clustering.
However, UMAP is prone to showing spurious clustering and distorting distances
[@Chari2021-hb]. Moreover, UMAP shows clustering that seems to correspond to
graph-based clusters from Louvain and Leiden because the k nearest neighbor
graph is used in both clustering and UMAP. We have developed `concordex` as a
quantitative alternative to UMAP cluster visualization without the misleading
problems of UMAP. This package is the R implementation of the original Python
command line tool.

In a nutshell, `concordex` finds the proportion of cells among the k nearest
neighbors of each cell with the same cluster or label as the cell itself. This
is computed across all labels and the average of all labels is returned as a
metric that indicates the quality of clustering. To see if this is significant,
the labels are permuted to estimate a null distribution and the actual observed
value is compared to the simulated values. If the clustering separates cells
well, then the observed value should be much higher than the simulated values,
i.e. the neighborhood of each cell is more dominated by cells of the same label
as the cell of interest than by chance.

``` {r setup, message=FALSE}
library(concordexR)
library(TENxPBMCData)
library(BiocNeighbors)
library(bluster)
library(scater)
library(patchwork)
library(ggplot2)
theme_set(theme_bw())
```

# Preprocessing

In this vignette, we demonstrate the usage of `concordex` on a human peripheral
blood mononuclear cells (PBMC) scRNA-seq dataset from 10X Genomics. The data is
loaded as a `SingleCellExperiment` object.

``` {r}
sce <- TENxPBMCData("pbmc3k")
```

Here we plot the standard QC metrics: total number of UMIs detected per cell
(`nCounts`), number of genes detected (`nGenes`), and percentage of UMIs from
mitochondrially encoded genes (`pct_mito`).

``` {r}
sce$nCounts <- colSums(counts(sce))
sce$nGenes <- colSums(counts(sce) > 0)
mito_inds <- grepl("^MT-", rowData(sce)$Symbol_TENx)
sce$pct_mito <- colSums(counts(sce)[mito_inds,])/sce$nCounts * 100
```

``` {r}
plotColData(sce, "nCounts") +
  plotColData(sce, "nGenes") +
  plotColData(sce, "pct_mito")
```

``` {r}
p1 <- plotColData(sce, x = "nCounts", y = "nGenes") +
  geom_density2d()
p2 <- plotColData(sce, x = "nCounts", y = "pct_mito") +
  geom_density2d()
p1 + p2
```

Remove the outliers and cells with high percentage of mitochondrial counts as
the high percentage is not expected biologically from the cell type:

``` {r}
sce <- sce[, sce$nCounts < 10000 & sce$pct_mito < 8]
sce <- sce[rowSums(counts(sce)) > 0,]
```

Then normalize the data:

``` {r}
sce <- logNormCounts(sce)
```

# Graph based clustering in PCA space

For simplicity, the top 500 highly variable genes are used to perform PCA:

``` {r}
sce <- runPCA(sce, ncomponents = 30, ntop = 500, scale = TRUE)
```

See the number of PCs to use later from the elbow plot:

``` {r}
plot(attr(reducedDim(sce, "PCA"), "percentVar"), ylab = "Percentage of variance explained")
```

Percentage of variance explained drops sharply from PC1 to PC5, and definitely
levels off after PC10, so we use the top 10 PCs for clustering here. The graph
based Leiden clustering uses a k nearest neighbor graph. For demonstration here,
we use `k = 10`.

``` {r}
set.seed(29)
sce$cluster <- clusterRows(reducedDim(sce, "PCA")[,seq_len(10)],
                           NNGraphParam(k = 10, cluster.fun = "leiden",
                                        cluster.args = list(
                                          objective_function = "modularity"
                                        )))
```

See what the clusters look like in PCA space:

``` {r, fig.width=7, fig.height=6}
plotPCA(sce, color_by = "cluster", ncomponents = 4)
```

Some of the clusters seem well-separated along the first 4 PCs.

Since UMAP is commonly used to visualize the clusters, we plot UMAP here
although we don't recommend UMAP because it's prone to showing spurious clusters
and distorting distances. UMAP also uses a k nearest neighbor graph, and we use
the same `k = 10` here:

``` {r}
sce <- runUMAP(sce, dimred = "PCA", n_dimred = 10, n_neighbors = 10)
```

``` {r}
plotUMAP(sce, color_by = "cluster")
```

For the most part, the clusters are clearly separated on UMAP.

# Enter `concordex`

Since UMAP is prone to showing spurious clusters, we'll see what the `concordex`
metric says about the clustering and whether it agrees with UMAP visualization.
Here we explicitly obtain the k nearest neighbor graph, as clustering and UMAP
above did not store the graph itself.

``` {r}
g <- findKNN(reducedDim(sce, "PCA")[,seq_len(10)], k = 10)
```

The result here is a list of two `n` (number of cell) by `k` matrices. The first
is the indices of each cell's neighbors, as in an adjacency list that can be
matrix here due to the fixed number of neighbors, and the second is the
distances between each cell and its neighbors. For `concordex`, only the first
matrix is relevant. An adjacency matrix, either sparse of dense, as stored in
the `Seurat` object, can also be used. Here the cluster labels are permuted 100
times.

``` {r}
res <- calculateConcordex(g$index, labels = sce$cluster, k = 10, n.iter = 100)
```

The results can be visualized in plots, which is implemented in this R package
but not in the Python package. The actual `concordex` value can be compared to
the simulated values where the latter is visualized in a density plot:

``` {r}
plotConcordexSim(res)
```

Here the actual value is much higher than the simulated values, indicating that
the cluster labels do reflect actual clusters well. The value itself is the
proportion of cells with each label in the neighborhood of other cells with the
same label, averaged over all labels.

To aid interpretation, the ratio of the observed value to the average simulated
value is also returned:

```{r}
res$concordex_ratio
```

A number greater than 1 means that cells are more likely to have neighbors with
the same label than expected by chance when the labels are completely randomly
assigned, and the value of 7.7 here means that the clusters are really good.

While the value is an average over all clusters, the matrix with the values for
each cluster is also returned by default, and can be visualized in a heatmap, as
a clustering diagnostic:

``` {r, fig.width=4.5, fig.height=4}
heatConcordex(res, angle_col = 0, cluster_rows = FALSE, cluster_cols = FALSE)
```

This heatmap also indicates good clustering, as almost all neighbors of cells
from each of the cluster labels have the same label, on the diagonal. Off
diagonal entries should be interpreted as such: (i,j) means the proportion of
cells with label i in the neighborhood of cells with label j. Off diagonal
entries means ambiguity between labels as similar cells get different labels. As
overplotting easily happens on the UMAP plot, this heatmap shows clustering
quality more unambiguously than UMAP.

# Session info

``` {r}
sessionInfo()
```

# References
