---
title: "ClusterFoldSimilarity: comparing cell-groups from independent single-cell experiments"
author: 
- name: Óscar González-Velasco
  affiliation: Division of Applied Bioinformatics, German Cancer Research Center DKFZ 
package: ClusterFoldSimilarity
output:
  BiocStyle::html_document:
    toc_float: true
vignette: >
  %\VignetteIndexEntry{ClusterFoldSimilarity: }
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r options, include=FALSE, echo=FALSE}
library(BiocStyle)
knitr::opts_chunk$set(eval=TRUE, warning=FALSE, error=FALSE, message=FALSE)
```

# Installation

The package can be installed using bioconductor install manager:

```{r eval=FALSE}
if (!require("BiocManager", quietly = TRUE))
    install.packages("BiocManager")

BiocManager::install("ClusterFoldSimilarity")
```

```{r setup}
library(ClusterFoldSimilarity)
```

# Introduction

Comparing single-cell data across different datasets, samples and batches has demonstrated to be challenging. `ClusterFoldSimilarity` aims to solve the complexity of comparing different single-cell datasets by computing similarity scores between clusters (or user-defined groups) from any number of independent single-cell experiments, including different species and sequencing technologies. It accomplishes this by identifying analogous fold-change patterns across cell groups that share a common set of features (such as genes). Additionally, it selects and reports the top important features that have contributed to the observed similarity, serving as a tool for feature selection.

The output is a table that contains the similarity values for all the combinations of cluster-pairs from the independent datasets. `ClusterFoldSimilarity` also includes various plotting utilities to enhance the interpretability of the similarity scores.

### Cross-species analysis and sequencing technologies (e.g.: Human vs Mouse, ATAC-Seq vs RNA-Seq)

`ClusterFoldSimilarity` is able to compare **any number** of independent experiments, including **different organisms**, making it useful for matching cell populations across different organisms, and thus, useful for inter-species analysis. Additionally, it can be used with **single-cell RNA-Seq data, single-cell ATAC-Seq data**, or more broadly, with continuous numerical data that shows changes in feature abundance across a set of common features between different groups.

### Compatibility

It can be easily integrated on any existing single-cell analysis pipeline, and it is compatible with the most used single-cell objects: `Seurat` and `SingleCellExperiment`.

Parallel computing is available through the option parallel=TRUE which make use of BiocParallel.

# Using ClusterFoldSimilarity to find similar clusters/cell-groups across datasets

Typically, `ClusterFoldSimilarity` will receive as input either a list of two or more `Seurat` or `SingleCellExperiment` objects.

`ClusterFoldSimilarity` will obtain the **raw count data** from these objects ( `GetAssayData(assay, layer = "counts")` in the case of `Seurat`, or `counts()` for `SingleCellExperiment` object), and **group or cluster label information** (using `Idents()` function from `Seurat`, or `colLabels()` for `SingleCellExperiment` ).

For the sake of illustration, we will employ the scRNAseq package, which contains numerous individual-cell datasets ready for download and encompassing samples from both human and mouse origins. In this example, we specifically utilize 2 human single-cell datasets obtained from the pancreas.

```{r construct }
library(Seurat)
library(scRNAseq)
library(dplyr)
# Human pancreatic single cell data 1
pancreasMuraro <- scRNAseq::MuraroPancreasData(ensembl=FALSE)
pancreasMuraro <- pancreasMuraro[,rownames(colData(pancreasMuraro)[!is.na(colData(pancreasMuraro)$label),])]
colData(pancreasMuraro)$cell.type <- colData(pancreasMuraro)$label
rownames(pancreasMuraro) <- make.names(unlist(lapply(strsplit(rownames(pancreasMuraro), split="__"), function(x)x[[1]])), unique = TRUE)
singlecell1Seurat <- CreateSeuratObject(counts=counts(pancreasMuraro), meta.data=as.data.frame(colData(pancreasMuraro)))
```

```{r ,  echo=FALSE}
knitr::kable(
  table(colData(pancreasMuraro)$label),
  caption = 'Cell-types on pancreas dataset from Muraro et al.', align = "c"
)
```

```{r }
# Human pancreatic single cell data 2
pancreasBaron <- scRNAseq::BaronPancreasData(which="human", ensembl=FALSE)
colData(pancreasBaron)$cell.type <- colData(pancreasBaron)$label
rownames(pancreasBaron) <- make.names(rownames(pancreasBaron), unique = TRUE)

singlecell2Seurat <- CreateSeuratObject(counts=counts(pancreasBaron), meta.data=as.data.frame(colData(pancreasBaron)))
```

```{r ,  echo=FALSE}
knitr::kable(
  table(colData(pancreasBaron)$label),
  caption = 'Cell-types on pancreas dataset from Baron et al.', align = "c"
)
```

As we want to perform clustering analysis for later comparison of these cluster groups using `ClusterFoldSimilarity`, we first need to normalize and identify variable features for each dataset independently.

*Note: these steps should be done tailored to each independent dataset, here we apply the same parameters for the sake of simplicity:*

```{r,  results='hide'}
# Create a list with the unprocessed single-cell datasets
singlecellObjectList <- list(singlecell1Seurat, singlecell2Seurat)
# Apply the same processing to each dataset and return a list of single-cell analysis
singlecellObjectList <- lapply(X=singlecellObjectList, FUN=function(scObject){
scObject <- NormalizeData(scObject)
scObject <- FindVariableFeatures(scObject, selection.method="vst", nfeatures=2000)
scObject <- ScaleData(scObject, features=VariableFeatures(scObject))
scObject <- RunPCA(scObject, features=VariableFeatures(object=scObject))
scObject <- FindNeighbors(scObject, dims=seq(16))
scObject <- FindClusters(scObject, resolution=0.4)
})
```

Once we have all of our single-cell datasets analyzed independently, we can compute the similarity values. `clusterFoldSimilarity()` takes as arguments:

-   `scList`: a list of single-cell objects (mandatory) either of class `Seurat` or of class `SingleCellExperiment`.
-   `sampleNames`: vector with names for each of the datasets. If not set the datasets will be named in the given order as: *1, 2, ..., N*.
-   `topN`: the top n most similar clusters/groups to report for each cluster/group (default: `1`, the top most similar cluster). If set to `Inf` it will return the values from all the possible cluster-pairs.
-   `topNFeatures`: the top *n* features (e.g.: genes) that contribute to the observed similarity between the pair of clusters (default: `1`, the top contributing gene). If a negative number, the tool will report the *n* most dissimilar features.
-   `nSubsampling`: number of subsamplings (1/3 of cells on each iteration) at group level for calculating the fold-changes (default: `15`). At start, the tool will report a message with the recommended number of subsamplings for the given data (average n of subsamplings needed to observe all cells).
-   `parallel`: whether to use parallel computing with multiple threads or not (default: `FALSE`). If we want to use a specific single-cell experiment for annotation (from which we know a ground-truth label, e.g. cell type, cell cycle, treatment... etc.), we can use that label to directly compare the single-cell datasets.

Here we will use the annotated pancreas cell-type labels from the dataset 1 to illustrate how to match clusters to cell-types using a reference dataset:

```{r , fig.wide = TRUE}
# Assign cell-type annotated from the original study to the cell labels:
Idents(singlecellObjectList[[1]]) <- factor(singlecellObjectList[[1]][[]][, "cell.type"])

library(ClusterFoldSimilarity)
similarityTable <- clusterFoldSimilarity(scList=singlecellObjectList, 
                                         sampleNames=c("human", "humanNA"),
                                         topN=1, 
                                         nSubsampling=24)

```

```{r ,  echo=FALSE}
knitr::kable(
  head(similarityTable), caption = 'A table of the first 10 rows of the similarity results.', align = "c", 
  format = "html", table.attr = "style='width:70%;'") %>% kableExtra::kable_styling(font_size=10)
```

A data.frame with the results is returned containing:

-   `similarityValue`: The top similarity value calculated between datasetL:clusterL and datasetR.
-   `w`: Weight associated with the similarity score value.
-   `datasetL`: Dataset left, the dataset/sample which has been used to be compared. 
-   `clusterL`: Cluster left, the cluster source from datasetL which has been compared.
-   `datasetR`: Dataset right, the dataset/sample used for comparison against datasetL.
-   `clusterR`: Cluster right, the cluster target from datasetR which is being compared with the clusterL from datasetL.
-   `topFeatureConserved`: The features (e.g.: genes, peaks...) that most contributed to the similarity between clusterL & clusterR.
-   `featureScore`: The similarity score contribution for the specific topFeatureConserved (e.g.: genes, peaks...).

By default, `clusterFoldSimilarity()` will plot a graph network that visualizes the connections between the clusters from the different datasets using the similarity table that has been obtained. The arrows point in the direction of the similarity (datasetL:clusterL -\> datasetR:clusterR); it can be useful for identifying relationships between groups of clusters and cell-populations that tend to be more similar. The graph plot can also be obtained by using the function `plotClustersGraph()` from this package, using as input the similarity table.

In this example, as we have information regarding cell-type labels, we can check how the cell types match by calculating the most abundant cell type on each of the similar clusters:

```{r}
typeCount <- singlecellObjectList[[2]][[]] %>% 
  group_by(seurat_clusters) %>% 
  count(cell.type) %>%
  arrange(desc(n), .by_group = TRUE) %>% 
  filter(row_number()==1)
```

```{r ,  echo=FALSE}
knitr::kable(
  cbind.data.frame(typeCount, 
                 matched.type=rep(table(typeCount$seurat_clusters), x=similarityTable[similarityTable$datasetL == "humanNA",]$clusterR)),
  caption = 'Cell-type label matching on pancreas single-cell data.', align = "c"
)
```

## Analyzing graph communities to identify super-groups of similar cell populations

To easily analyze and identify the similarities between the different datasets and cell-groups, we can find the communities that constitute the directed graph (cluster the nodes based on the graph´s closely-related elements).

We can make so by using the function `findCommunitiesSimmilarity()` from the `ClusterFoldSimilarity` package. It uses the InfoMap algorithm to find the best fitting communities. We just need the similarity table obtained from `clusterFoldSimilarity()` as explained on the previous section, the function will plot the graph with the communities and return a data frame containing the community that each sample & cluster/group belongs to.

```{r, fig.wide = TRUE}
cellCommunities <- findCommunitiesSimmilarity(similarityTable = similarityTable)
```

```{r,  echo=FALSE}
knitr::kable(
  head(cellCommunities)
)
```

## Retrieving the top-n similarities

If we suspect that clusters could be related with more than one cluster of other datasets, we can retrieve the top n similarities for each cluster:

```{r, fig.wide = TRUE}
# Retrieve the top 3 similar cluster for each of the clusters:
similarityTable3Top <- clusterFoldSimilarity(scList=singlecellObjectList, 
                                             topN=3,
                                             sampleNames=c("human", "humanNA"), 
                                             nSubsampling=24)
```

```{r , echo=FALSE}
knitr::kable(
  head(similarityTable3Top),
  caption = 'Similarity results showing the top 3 most similar clusters.', align = "c", 
  format = "html", table.attr = "style='width:70%;'") %>% kableExtra::kable_styling(font_size=10)
```

## Obtaining the top-n feature markers

If we are interested on the features that contribute the most to the similarity, we can retrieve the top n features:

```{r, fig.wide = TRUE}
# Retrieve the top 5 features that contribute the most to the similarity between each pair of clusters:
similarityTable5TopFeatures <- clusterFoldSimilarity(scList=singlecellObjectList, 
                                                     topNFeatures=5, 
                                                     nSubsampling=24)
```

```{r ,  echo=FALSE}
knitr::kable(
  head(similarityTable5TopFeatures, n=10),
  caption = 'Similarity results showing the top 5 features that most contributed to the similarity.', align = "c", 
  format = "html", table.attr = "style='width:70%;'") %>% kableExtra::kable_styling(font_size=10)
```

## Retrieving all the similarity values and plotting a similarity heatmap

Sometimes it is useful to retrieve all the similarity values for downstream analysis (e.g. identify more than one cluster that is similar to a cluster of interest, finding the most dissimilar clusters, etc). To obtain all the values, we need to specify `topN=Inf`.

By default, `clusterFoldSimilarity` creates a heatmap plot with the computed similarity values (from the perspective of the first dataset found on `scList`; to modify this plot see the following section). The top 2 similarities for each group within dataset 1 (heatmap row-wise) are highlighted with colored borders.

```{r , fig.wide = TRUE,  out.height="400px" }
similarityTableAllValues <- clusterFoldSimilarity(scList=singlecellObjectList, 
                                                  sampleNames=c("human", "humanNA"), 
                                                  topN=Inf)
dim(similarityTableAllValues)
```

For downstream analysis of the similarities, it can be convenient to create a matrix with all the scores from the comparison of two datasets:

```{r}
library(dplyr)
dataset1 <- "human"
dataset2 <- "humanNA"
similarityTable2 <- similarityTableAllValues %>% 
  filter(datasetL == dataset1 & datasetR == dataset2) %>% 
  arrange(desc(as.numeric(clusterL)), as.numeric(clusterR))
cls <- unique(similarityTable2$clusterL)
cls2 <- unique(similarityTable2$clusterR)
similarityMatrixAll <- t(matrix(similarityTable2$similarityValue, ncol=length(unique(similarityTable2$clusterL))))
rownames(similarityMatrixAll) <- cls
colnames(similarityMatrixAll) <- cls2
```

```{r, echo=FALSE}
knitr::kable(
  similarityMatrixAll,
  caption = 'A 2 datasets Similarity matrix.', digits = 2, align = "c", 
  format = "html", table.attr = "style='width:60%;'") %>% kableExtra::kable_styling(font_size=8)
```

# Using ClusterFoldSimilarity across species and numerous datasets:

`ClusterFoldSimilarity` can compare **any number** of independent studies, including **different organisms**, making it useful for inter-species analysis. Also, it can be used on different sequencing data technologies: e.g.: compare single-cell **ATAC-Seq VS RNA-seq**.

In this example, we are going to add a pancreas single-cell dataset from **Mouse** to the 2 existing ones from **Human** that we have processed in the previous steps.

```{r}
# Mouse pancreatic single cell data
pancreasBaronMM <- scRNAseq::BaronPancreasData(which="mouse", ensembl=FALSE)
```

```{r ,  echo=FALSE}
knitr::kable(
  table(colData(pancreasBaronMM)$label),
  caption = 'Cell-types on pancreas dataset from Baron et al.', align = "c"
)
```

```{r }
colData(pancreasBaronMM)$cell.type <- colData(pancreasBaronMM)$label
# Translate mouse gene ids to human ids
# *for the sake of simplicity we are going to transform to uppercase all mouse gene names
rownames(pancreasBaronMM) <- make.names(toupper(rownames(pancreasBaronMM)), unique=TRUE)
# Create seurat object
singlecell3Seurat <- CreateSeuratObject(counts=counts(pancreasBaronMM), meta.data=as.data.frame(colData(pancreasBaronMM)))

# We append the single-cell object to our list
singlecellObjectList[[3]] <- singlecell3Seurat
```

Now, we process the new single-cell dataset from mouse, and we calculate the similarity scores between the 3 independent datasets.

```{r,  results='hide'}
scObject <- singlecellObjectList[[3]]
scObject <- NormalizeData(scObject)
scObject <- FindVariableFeatures(scObject, selection.method="vst", nfeatures=2000)
scObject <- ScaleData(scObject, features=VariableFeatures(scObject))
scObject <- RunPCA(scObject, features=VariableFeatures(object=scObject))
scObject <- FindNeighbors(scObject, dims=seq(16))
scObject <- FindClusters(scObject, resolution=0.4)
singlecellObjectList[[3]] <- scObject
```

This time we will make use of the option parallel=TRUE. We can set the specific number of CPUs to use using `BiocParallel::register()` 

```{r, fig.wide = TRUE}
# We use the cell labels as a second reference, but we can also use the cluster labels if our interest is to match clusters
Idents(singlecellObjectList[[3]]) <- factor(singlecellObjectList[[3]][[]][,"cell.type"])

# We subset the most variable genes in each experiment
singlecellObjectListVariable <- lapply(singlecellObjectList, function(x){x[VariableFeatures(x),]})

# Setting the number of CPUs with BiocParallel:
BiocParallel::register(BPPARAM =  BiocParallel::MulticoreParam(workers = 6))

similarityTableHumanMouse <- clusterFoldSimilarity(scList=singlecellObjectListVariable,
                                                        sampleNames=c("human", "humanNA", "mouse"),
                                                        topN=1, 
                                                        nSubsampling=24,
                                                        parallel=TRUE)
```

We can compute and visualize with a heatmap all the similarities for each cluster/group of cells from the 3 datasets using `topN=Inf`. Additionally, we can use the function `similarityHeatmap()` from this package to plot the heatmap with the datasets in a different order, or just plot the 2 datasets we are interested in. The top 2 similarities are highlighted to help visualizing the best matching groups.

```{r , fig.wide = TRUE,  out.height="400px" }
similarityTableHumanMouseAll <- clusterFoldSimilarity(scList=singlecellObjectListVariable,
                                                          sampleNames=c("human", "humanNA", "mouse"),
                                                          topN=Inf,
                                                          nSubsampling=24,
                                                          parallel=TRUE)
```

As the similarity values might not be symmetric (e.g. a cluster A from D1 showing the top similarity to B from D2, might not be the top similar cluster to B from D2), we can select which dataset to plot in the Y-axis:

```{r , fig.wide = TRUE,  out.height="400px" }
ClusterFoldSimilarity::similarityHeatmap(similarityTable=similarityTableHumanMouseAll, mainDataset="humanNA")
```

Additionally, we can turn-off the highlight using `highlightTop=FALSE`

```{r , fig.wide = TRUE,  out.height="400px" }
# Turn-off the highlighting:
ClusterFoldSimilarity::similarityHeatmap(similarityTable=similarityTableHumanMouseAll, mainDataset="humanNA", highlightTop=FALSE)
```

# Similarity score calculation

`ClusterFoldSimilarity` does not need to integrate the data, or apply any batch correction techniques across the datasets that we aim to analyze, which makes it less prone to data-loss or noise. The similarity value is based on the fold-changes between clusters/groups of cells defined by the user. These fold-changes from different independent datasets are first computed using a Bayesian approach, we calculate this fold-change distribution using a permutation analysis that shrink the fold-changes with no biological meaning. These differences in abundance are then combined using a pairwise dot product approach, after adding these feature contributions and applying a fold-change concordance weight, a similarity value is obtained for each of the clusters of each of the datasets present.

# Session information {.unnumbered}

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