---
title: "CRMetrics - Cell Ranger Filtering and Metrics Visualization"
output: 
  html_document:
    toc: true
    toc_float: true
---

# Introduction

## Preparations

We have selected a [publicly available dataset](https://www.ncbi.nlm.nih.gov/geo/) from GEO with accession number GSE179590 which can be downloaded [here](http://kkh.bric.ku.dk/fabienne/crmetrics_testdata.tar.gz).
You can download the zipped data using wget or curl, e.g. `wget http://kkh.bric.ku.dk/fabienne/crmetrics_testdata.tar.gz`, and then unpack using `tar -xvf crmetrics_testdata.tar.gz`

## Using Python modules

CRMetrics utilizes several Python packages.
Of these, the packages for doublet detection, `scrublet` and `DoubletDetection`, can be run from R using `reticulate`.
However, CRMetrics can provide a Python script to run the analyses directly in Python instead through the `export` parameter.

In order to run the analyses in R, first you should install `reticulate`:

```{r, eval=FALSE}
install.packages("reticulate")
library(reticulate)
```

Also, you need to install Conda.
Then, you are ready to create a Conda virtual environment.
In this example, we're on a server and we load `miniconda` using modules.
You may need to include the `conda` parameter to point to wherever your Conda binary is located (in terminal, try `whereis conda`).
In this example, we install a virtual environment for `scrublet`.

```{r, eval=FALSE}
conda_create("scrublet", 
             conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
             python_version = 3.8)
```

There is a known problem with openBLAS which may be different between R and Python.
If this is the case, you will receive the error `floating point exception` and R will crash when you try to run a Python script using `reticulate`.
In Python, the problem lies within `numpy`.
`numba` requires `numpy` \< 1.23, so force reinstall from scratch with no binaries in the `scrublet` Conda environment from terminal `module load miniconda/4.12.0` `conda activate scrublet` `python -m pip install numpy==1.22.0 --force-reinstall --no-binary numpy`

Then, follow the instructions to install [`scrublet`](https://github.com/swolock/scrublet).
Finally, restart your R session.

Please note, if at any point you receive an error that you can't change the current Python instance, please remove any Python-dependent object in your environment and restart your R session.

# Main use

Load the library

```{r setup, message=FALSE}
library(CRMetrics)
library(magrittr)
library(dplyr)
```

There are two ways to initialize a new object of class `CRMetrics`, either by providing `data.path` or `cms`.
`data.path` is the path to a directory containing sample-wise directories with the Cell Ranger count outputs.
Optionally, it can be a vector of multiple paths.
`cms` is a (named, optional) list of (sparse, optional) count matrices.

Please note, if `data.path` is not provided, some functionality is lost, e.g. ambient RNA removal.

Optionally, metadata can be provided, either as a file or as a data.frame.
For a file, the separator can be set with the parameter `sep.meta` (most often, either `,` (comma) or `\t` (tab) is used).
In either format, the columns must be named and one column must be named `sample` and contain sample names.
In combination with `data.path`, the sample names must match the sample directory names.
Unmatched directory names are dropped.

If `cms` is provided, it is recommended to add summary metrics afterwards:

```{r cms, eval=FALSE}
crm <- CRMetrics$new(cms = cms, 
                     n.cores = 10, 
                     pal = grDevices::rainbow(8),
                     theme = ggplot2::theme_bw())
crm$addSummaryFromCms()
```

Please note, some functionality depends on aggregation of sample and cell IDs using the `sep.cell` parameter.
The default is `!!` which creates cell names in the format of `<sampleID>!!<cellID>`.
If another separator is used, this needs to be provided in relevant function calls.

Here, the folder with our test data is stored in `/data/ExtData/CRMetrics_testdata/` and we provide metadata in a comma-separated file.

```{r init, eval=FALSE}
crm <- CRMetrics$new(data.path = "/data/ExtData/CRMetrics_testdata/", 
                     metadata = "/data/ExtData/CRMetrics_testdata/metadata.csv", 
                     sep.meta = ",",
                     n.cores = 10,
                     verbose = FALSE,
                     pal = grDevices::rainbow(8),
                     theme = ggplot2::theme_bw())
```

```{r load-obj, echo = F}
crm <- qs::qread("/data/ExtData/CRMetrics_testdata/crm.qs", nthreads = 10)
```

We can review our metadata

```{r meta}
crm$metadata
```

## Plot summary statistics

We can investigate which metrics are available and choose the ones we would like to plot

```{r select-metrics}
crm$selectMetrics()
```

### Samples per condition

First, we can plot the number of samples per condition.
Here, we investigate how the distribution of the sex differs between the type of MS of the samples where RRMS is short for relapsing remitting MS, and SPMS is short for secondary progressive MS.

```{r plot-summary-metrics, eval=FALSE}
crm$plotSummaryMetrics(comp.group = "sex", 
                       metrics = "samples per group", 
                       second.comp.group = "type",
                       plot.geom = "bar")
```
![](img/fig1.png)

### Metrics per sample

In one plot, we can illustrate selected metric summary stats.
If no comparison group is set, it defaults to `sample`.

```{r plot-sum-metrics-selected, fig.width=12, fig.height=12, eval=FALSE}
metrics.to.plot <- crm$selectMetrics(ids = c(1:4,6,18,19))
crm$plotSummaryMetrics(comp.group = "sample",
                       metrics = metrics.to.plot, 
                       plot.geom = "bar")
```
![](img/fig2.png)

### Metrics per condition

We can do the same, but set the comparison group to `type`.
This will add statistics to the plots.
Additionally, we can add a second comparison group for coloring.

```{r plot-sum-metrics-comp, fig.width=12, fig.height=10, eval=FALSE}
crm$plotSummaryMetrics(comp.group = "type",
                       metrics = metrics.to.plot, 
                       plot.geom = "point", 
                       stat.test = "non-parametric",
                       second.comp.group = "sex")
```
![](img/fig3.png)

### Metrics per condition with >2 levels

For the sake of the example, we change the `RIN` values to `low` (RIN\<6), `medium` (6\<RIN\<7), and `high` (RIN\>7).
This will provide us with three comparisons groups to exemplify how to use automated statistics for such situations.

```{r plot-sum-metrics-multilevel, fig.width=12, fig.height=10, eval=FALSE}
crm$metadata$RIN %<>% 
  as.character() %>% 
  {c("medium","high","high","medium","high","high","low","high")} %>% 
  factor(., levels = c("low", "medium", "high"))

crm$plotSummaryMetrics(comp.group = "RIN",
                       metrics = metrics.to.plot, 
                       plot.geom = "point", 
                       stat.test = "non-parametric",
                       second.comp.group = "type", 
                       secondary.testing = TRUE)
```
![](img/fig4.png)

### Metrics per condition with numeric covariate

We can choose a numeric comparison group, in this case `age`, which will add regression lines to the plots.

```{r plot-sum-metrics-num-cov, fig.height=10, fig.width=12, eval=FALSE}
crm$plotSummaryMetrics(comp.group = "age",
                       metrics = metrics.to.plot, 
                       plot.geom = "point",
                       second.comp.group = "type",
                       se = FALSE)
```
![](img/fig5.png)

If the numeric vector has a significant effect on one of the metrics we can investigate it closer by performing regression analyses for both conditions of `type`.

```{r plot-sum-metrics-sec-comp, eval=FALSE}
crm$plotSummaryMetrics(comp.group = "age",
                       metrics = "mean reads per cell", 
                       plot.geom = "point",
                       second.comp.group = "type", 
                       group.reg.lines = TRUE)
```
![](img/fig6.png)

We see that there is no significant effect of the numeric vector on neither of the MS types.

## Add detailed metrics

We can read in count matrices to assess detailed metrics.
Otherwise, if count matrices have already been added earlier, this step prepares data for plotting UMI and gene counts.

```{r add-detailed-metrics, eval=FALSE}
crm$addCms(add.metadata = FALSE)
crm$addDetailedMetrics()
```

We plot the detailed metrics.
The horizontal lines indicates the median values for all samples.

```{r plot-detailed-metrics, eval=FALSE}
metrics.to.plot <- crm$detailed.metrics$metric %>%
  unique()
crm$plotDetailedMetrics(comp.group = "type",
                        metrics = metrics.to.plot, 
                        plot.geom = "violin")
```
![](img/fig7.png)

## Embed cells using Conos

In order to plot our cells in our embedding, we need to perform preprocessing of the raw count matrices.
To do this, either `pagoda2` (default) or `Seurat` can be used.

```{r preprocessing, eval=FALSE}
crm$doPreprocessing()
```

Then, we create the embedding using `conos`.

```{r create-embedding, eval=FALSE}
crm$createEmbedding()
```

We can now plot our cells.

```{r plot-embedding, eval=FALSE}
crm$plotEmbedding()
```
![](img/fig8.png)

## Cell depth

We can plot cell depth, both in the embedding or as histograms per sample.

```{r plot-embedding-depth, eval=FALSE}
crm$plotEmbedding(depth = TRUE, 
             depth.cutoff = 1e3)
```
![](img/fig9.png)

```{r plot-depth, eval=FALSE}
crm$plotDepth()
```
![](img/fig10.png)

We can see that the depth distribution varies between samples. We can create a cutoff vector specifying the depth cutoff per sample. It should be a named vector containing sample names.

```{r plot-sw-depth}
depth_cutoff_vec <- c(2.5e3, 2e3, 1e3, 1.5e3, 1.5e3, 2e3, 2.5e3, 2e3) %>% 
  setNames(crm$detailed.metrics$sample %>% 
             unique() %>% 
             sort())

depth_cutoff_vec
```

Let's plot the updated cutoffs:

```{r plot-upd-depth, eval=FALSE}
crm$plotDepth(cutoff = depth_cutoff_vec)
```
![](img/fig11.png)

Also, we can do this in the embedding:

```{r plot-embedding-depth-upd, eval=FALSE}
crm$plotEmbedding(depth = TRUE, 
             depth.cutoff = depth_cutoff_vec)
```
![](img/fig12.png)

## Mitochondrial fraction

We can also investigate the mitochondrial fraction in our cells

```{r plot-emb-mf, eval=FALSE}
crm$plotEmbedding(mito.frac = TRUE, 
             mito.cutoff = 0.05, 
             species = "human")
```
![](img/fig13.png)

Similar as for depth, we can plot the distribution of the mitochondrial fraction per sample and include sample-wise cutoffs (not shown here).

```{r plot-mf, eval=FALSE}
crm$plotMitoFraction(cutoff = 0.05)
```
![](img/fig14.png)

# Remove ambient RNA

We have added functionality to remove ambient RNA from our samples.
This approach should be used with caution since it induces changes to the UMI counts (NB: it does not overwrite the outputs from Cell Ranger).
We have included preparative steps for [CellBender](https://github.com/broadinstitute/CellBender/) as well as incorporated [SoupX](https://github.com/constantAmateur/SoupX) into CRMetrics.

## CellBender

### Installation

To install, follow [these instructions](https://cellbender.readthedocs.io/en/latest/installation/index.html#manual-installation).
It is highly recommended to run `CellBender` using GPU acceleration.
If you are more comfortable installing through `reticulate` in R, these lines should be run:

```{r cellbender-install, eval=FALSE}
library(reticulate)
conda_create("cellbender", 
             conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
             python_version = 3.7)
conda_install("cellbender", 
              conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
              forge = FALSE, 
              channel = "anaconda", 
              packages = "pytables")
conda_install("cellbender", 
              conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
              packages = c("pytorch","torchvision","torchaudio"),
              channel = "pytorch")
```

Then, clone the `CellBender` repository as instructed in the manual.
Here, we clone to `/apps/` through `cd /apps/; git clone https://github.com/broadinstitute/CellBender.git` and then `CellBender` can be installed:

```{r cellbender-install-2, eval=FALSE}
conda_install("cellbender", 
              conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
              pip = TRUE, 
              pip_options = "-e", 
              packages = "/apps/CellBender/")
```

### Analysis

For `CellBender`, we need to specify expected number of cells and total droplets included (please see the [manual](https://cellbender.readthedocs.io/en/latest/usage/index.html) for additional information).
As hinted in the manual, the number of total droplets included could be expected number of cells multiplied by 3 (which we set as default).
First, we plot these measures:

```{r cbprep, eval=FALSE}
crm$prepareCellbender()
```
![](img/fig15.png)

We could change the total droplets included for any sample.
Let us first look at the vector.

```{r totdrops}
droplets <- crm$getTotalDroplets()
droplets
```

Then we change the total droplets for SRR15054424.

```{r change-topdrops}
droplets["SRR15054424"] <- 2e4
```

We plot this change.

```{r cbprep-totdrops, eval=FALSE}
crm$prepareCellbender(shrinkage = 100, 
                      show.expected.cells = TRUE, 
                      show.total.droplets = TRUE, 
                      total.droplets = droplets)
```
![](img/fig16.png)

We could also multiply expected cells by 2.5 for all samples and save this in our CRMetrics object.

```{r cb-totdrops-multiply, eval=FALSE}
crm$cellbender$total.droplets <- crm$getTotalDroplets(multiplier = 2.5)
```

Finally, we save a script for running `CellBender` on all our samples. To only select specific samples, use the `samples` parameter. Here, we use our modified total droplet vector. If `total.droplets` is not specified, it will use the stored vector at `crm$cellbender$total.droplets`.

```{r cb-save, eval=FALSE}
crm$saveCellbenderScript(file = "/apps/cellbender_script.sh", 
                         fpr = 0.01, 
                         epochs = 150, 
                         use.gpu = TRUE,
                         total.droplets = droplets)
```

We can run this script in the terminal. Here, we activate the environment: `conda activate cellbender` and we run the bash script: `sh /apps/cellbender_script.sh`

### Plotting

We can plot the changes in cell numbers following CellBender estimations.

```{r cb-plotcells, eval=FALSE}
crm$plotCbCells()
```
![](img/fig17.png)

We can plot the CellBender training results.

```{r cb-plottraining, fig.width = 12, fig.height = 10, eval=FALSE}
crm$plotCbTraining()
```
![](img/fig18.png)

We can plot the cell probabilities.

```{r cb-plotcellprobs, fig.width = 12, fig.height = 10, eval=FALSE}
crm$plotCbCellProbs()
```
![](img/fig19.png)

We can plot the identified ambient genes per sample.

```{r cb-plotambexp, fig.width = 12, fig.height = 10, eval=FALSE}
crm$plotCbAmbExp(cutoff = 0.005)
```
![](img/fig20.png)

Lastly, we can plot the proportion of samples expressing ambient genes. We see that *MALAT1* is identified as an ambient gene in all samples [which is expected](https://kb.10xgenomics.com/hc/en-us/articles/360004729092-Why-do-I-see-high-levels-of-Malat1-in-my-gene-expression-data-).

```{r cb-plotambgenes, eval=FALSE}
crm$plotCbAmbGenes(cutoff = 0.005, pal = grDevices::rainbow(17))
```
![](img/fig21.png)

Then, we can add the filtered CMs to our object. Additionally, it is recommended to generate new summary metrics from the adjusted CMs which can then be plotted.

```{r, eval = FALSE}
crm$cms <- NULL
crm$addCms(cellbender = T)
crm$addSummaryFromCms()
crm$detailed.metrics <- NULL
crm$addDetailedMetrics()
```

## SoupX

The implementation of SoupX uses the automated estimation of contamination and correction. Please note, SoupX depends on Seurat for import of data. Since this calculation takes several minutes, it is not run in this vignette.

```{r runsoupx, eval=FALSE}
crm$runSoupX()
```

Then, we can plot the corrections.

```{r plotsoupx, eval=FALSE}
crm$plotSoupX()
```
![](img/fig22.png)

Then, we can add the SoupX adjusted CMs to our object.
Additionally, it is recommended to generate new summary metrics from the adjusted CMs which can then be plotted.

```{r add-adj-cms}
crm$cms <- NULL
crm$addCms(cms = crm$soupx$cms.adj, 
           unique.names = TRUE, 
           sep = "!!")
crm$addSummaryFromCms()
crm$detailed.metrics <- NULL
crm$addDetailedMetrics()
```

# Doublet detection

For doublet detection, we included the possibility to do so using the Python modules `scrublet` and `DoubletDetection`.
Here, we use virtual environments where we installed each method.

`scrublet` is the default method, which is fast.
`DoubletDetection` is significantly slower, but performs better according to [this](https://www.sciencedirect.com/science/article/pii/S2405471220304592) review.
Here, we show how to run `scrublet` and `DoubletDetection` to compare in the next section.
Since this takes some time, the results have been precalculated and are not run in this vignette.

```{r run-dd, eval=FALSE}
crm$detectDoublets(env = "scrublet",
                   conda.path = "/opt/software/miniconda/4.12.0/condabin/conda",
                   method = "scrublet")
crm$detectDoublets(env = "doubletdetection",
                   conda.path = "/opt/software/miniconda/4.12.0/condabin/conda",
                   method = "doubletdetection")
```

It is also possible to generate a Python script to run each method from the terminal.
To do this, set `export = TRUE` and run `python <METHOD>.py` inside the virtual environment to generate data.
Then, load data with:

```{r, eval = FALSE}
crm$addDoublets()
```

We can plot the estimated doublets in the embedding.

```{r plot-scrublet-embedding, eval=FALSE}
crm$plotEmbedding(doublet.method = "scrublet")
crm$plotEmbedding(doublet.method = "doubletdetection")
```
![](img/fig23a.png)
![](img/fig23b.png)

And we can plot the scores for the doublet estimations.

```{r plot-scrublet-scores, eval=FALSE}
crm$plotEmbedding(doublet.method = "scrublet", 
                  doublet.scores = TRUE)
crm$plotEmbedding(doublet.method = "doubletdetection", 
                  doublet.scores = TRUE)
```
![](img/fig24a.png)
![](img/fig24b.png)

## Differences between methods

We can compare how much `scrublet` and `DoubletDetection` overlap in their doublets estimates.
First, let us plot a bar plot of the number of doublets per sample.

```{r compare-dd-res, eval=FALSE}
scrub.res <- crm$doublets$scrublet$result %>% 
  select(labels, sample) %>% 
  mutate(method = "scrublet")

dd.res <- crm$doublets$doubletdetection$result %>% 
  select(labels, sample) %>% 
  mutate(labels = as.logical(labels), 
         method = "DoubletDetection")

dd.res[is.na(dd.res)] <- FALSE

plot.df <- rbind(scrub.res,
                 dd.res) %>% 
  filter(labels) %>% 
  group_by(sample, method) %>% 
  summarise(count = n())

ggplot(plot.df, aes(sample, count, fill = method)) +
  geom_bar(stat = "identity", position = position_dodge()) +
  crm$theme +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5)) +
  labs(x = "", y = "No. doublets", fill = "Method", title = "Doublets per sample") +
  scale_fill_manual(values = crm$pal)
```
![](img/fig25.png)

We can also show the total number of doublets detected per method.

```{r plot-dd-per-method, eval=FALSE}
plot.df %>% 
  group_by(method) %>% 
  summarise(count = sum(count)) %>% 
  ggplot(aes(method, count, fill = method)) + 
  geom_bar(stat = "identity") +
  crm$theme +
  guides(fill = "none") +
  labs(x = "", y = "No. doublets", title = "Total doublets per method") +
  scale_fill_manual(values = crm$pal)
```
![](img/fig26.png)

Finally, let's plot an embedding showing the method-wise estimations as well as overlaps.

```{r plot-dd-emb-per-method, eval=FALSE}
plot.vec <- data.frame(scrublet = scrub.res$labels %>% as.numeric(), 
                       doubletdetection = dd.res$labels %>% as.numeric()) %>% 
  apply(1, \(x) if (x[1] == 0 & x[2] == 0) "Kept" else if (x[1] > x[2]) "scrublet" else if (x[1] < x[2]) "DoubletDetection" else "Both") %>% 
  setNames(rownames(scrub.res)) %>% 
  factor(levels = c("Kept","scrublet","DoubletDetection","Both"))

crm$con$plotGraph(groups = plot.vec, 
                  mark.groups = FALSE, 
                  show.legend = TRUE, 
                  shuffle.colors = TRUE, 
                  title = "Doublets", 
                  size = 0.3) +
  scale_color_manual(values = c("grey80","red","blue","black"))
```
![](img/fig27.png)

# Plot filtered cells

We can plot all the cells to be filtered in our embedding

```{r plot-filtered-cells-emb, eval=FALSE}
crm$plotFilteredCells(type = "embedding", 
                      depth = TRUE, 
                      depth.cutoff = depth_cutoff_vec, 
                      doublet.method = "scrublet", 
                      mito.frac = TRUE, 
                      mito.cutoff = 0.05, 
                      species = "human")
```
![](img/fig28.png)

And we can plot the cells to be filtered per sample where `combination` means a cell that has at least two filter labels, e.g. `mito` and `depth`.

```{r plot-filtered-cells-bar, eval=FALSE}
crm$plotFilteredCells(type = "bar", 
                      doublet.method = "scrublet", 
                      depth = TRUE, 
                      depth.cutoff = depth_cutoff_vec, 
                      mito.frac = TRUE, 
                      mito.cutoff = 0.05, 
                      species = "human")
```
![](img/fig29.png)

Finally, we can create a tile plot with an overview of sample quality for the different filters.
NB, this is experimental and has not been validated across datasets.

```{r plot-filtered-cells-tile, eval=FALSE}
crm$plotFilteredCells(type = "tile", 
                      doublet.method = "doubletdetection",
                      depth = TRUE, 
                      depth.cutoff = depth_cutoff_vec,
                      mito.frac = TRUE, 
                      mito.cutoff = 0.05, 
                      species = "human")
```
![](img/fig30.png)

We can also extract the raw numbers for plotting in other ways than those included here

```{r export-filtered-cells, eval=FALSE}
filter.data <- crm$plotFilteredCells(type = "export")
filter.data %>% head()
```

# Filter count matrices

Finally, we can filter the count matrices to create a cleaned list to be used in downstream applications.

```{r filter}
crm$filterCms(depth.cutoff = depth_cutoff_vec, 
              mito.cutoff = 0.05, 
              doublets = "scrublet",
              samples.to.exclude = NULL,
              species = "human",
              verbose = TRUE)
```

The filtered list of count matrices is stored in \$cms.filtered which can be saved on disk afterwards.

```{r save, eval=FALSE}
qs::qsave(crm$cms.filtered, "/data/ExtData/CRMetrics_testdata/cms_filtered.qs", 
      nthreads = 10)
```

```{r session-info}
sessionInfo()
```
