This tutorial describes how to use harmony in Seurat v5 single-cell
analysis workflows. RunHarmony()
is a generic function is
designed to interact with Seurat objects. This vignette will walkthrough
basic workflow of Harmony with Seurat objects. Also, it will provide
some basic downstream analyses demonstrating the properties of
harmonized cell embeddings and a brief explanation of the exposed
algorithm parameters.
Install Harmony from CRAN with standard commands.
For this demo, we will be aligning two groups of PBMCs Kang et al., 2017. In this experiment, PBMCs are in stimulated and control conditions. The stimulated PBMC group was treated with interferon beta.
## Generate SeuratObject
``` r
## Source required data
data("pbmc_stim")
pbmc <- CreateSeuratObject(counts = cbind(pbmc.stim, pbmc.ctrl), project = "PBMC", min.cells = 5)
## Separate conditions
[email protected]$stim <- c(rep("STIM", ncol(pbmc.stim)), rep("CTRL", ncol(pbmc.ctrl)))
The example above contains only two thousand cells. The full Kang et al., 2017 dataset is deposited in the GEO. This analysis uses GSM2560248 and GSM2560249 samples from GSE96583_RAW.tar file and the GSE96583_batch2.genes.tsv.gz gene file.
library(Matrix)
## Download and extract files from GEO
##setwd("/path/to/downloaded/files")
genes = read.table("GSE96583_batch2.genes.tsv.gz", header = FALSE, sep = "\t")
pbmc.ctrl.full = as.readMM("GSM2560248_2.1.mtx.gz")
colnames(pbmc.ctrl.full) = paste0(read.table("GSM2560248_barcodes.tsv.gz", header = FALSE, sep = "\t")[,1], "-1")
rownames(pbmc.ctrl.full) = genes$V1
pbmc.stim.full = readMM("GSM2560249_2.2.mtx.gz")
colnames(pbmc.stim.full) = paste0(read.table("GSM2560249_barcodes.tsv.gz", header = FALSE, sep = "\t")[,1], "-2")
rownames(pbmc.stim.full) = genes$V1
library(Seurat)
pbmc <- CreateSeuratObject(counts = cbind(pbmc.stim.full, pbmc.ctrl.full), project = "PBMC", min.cells = 5)
pbmc@meta.data$stim <- c(rep("STIM", ncol(pbmc.stim.full)), rep("CTRL", ncol(pbmc.ctrl.full)))
# Running Harmony
Harmony works on an existing matrix with cell embeddings and outputs its transformed version with the datasets aligned according to some user-defined experimental conditions. By default, harmony will look up the `pca` cell embeddings and use these to run harmony. Therefore, it assumes that the Seurat object has these embeddings already precomputed.
## Calculate PCA cell embeddings
Here, using `Seurat::NormalizeData()`, we will be generating a union of highly variable genes using each condition (the control and stimulated cells). These features are going to be subsequently used to generate the 20 PCs with `Seurat::RunPCA()`.
pbmc <- pbmc %>%
NormalizeData(verbose = FALSE)
VariableFeatures(pbmc) <- split(row.names(pbmc@meta.data), pbmc@meta.data$stim) %>% lapply(function(cells_use) {
pbmc[,cells_use] %>%
FindVariableFeatures(selection.method = "vst", nfeatures = 2000) %>%
VariableFeatures()
}) %>% unlist %>% unique
#> Finding variable features for layer counts
#> Finding variable features for layer counts
pbmc <- pbmc %>%
ScaleData(verbose = FALSE) %>%
RunPCA(features = VariableFeatures(pbmc), npcs = 20, verbose = FALSE)
To run harmony on Seurat object after it has been normalized, only one argument needs to be specified which contains the batch covariate located in the metadata. For this vignette, further parameters are specified to align the dataset but the minimum parameters are shown in the snippet below:
## run harmony with default parameters
pbmc <- pbmc %>% RunHarmony("stim")
## is equivalent to:
pbmc <- RunHarmony(pbmc, "stim")
Here, we will be running harmony with some indicative parameters and plotting the convergence plot to illustrate some of the under the hood functionality.
pbmc <- pbmc %>%
RunHarmony("stim", plot_convergence = TRUE, nclust = 50, max_iter = 10, early_stop = T)
#> Transposing data matrix
#> Initializing state using k-means centroids initialization
#> Harmony 1/10
#> Harmony 2/10
#> Harmony 3/10
#> Harmony 4/10
#> Harmony 5/10
#> Harmony converged after 5 iterations
RunHarmony
has several parameters accessible to users
which are outlined below.
object
(required)The Seurat object. This vignette assumes Seurat objects are version 5.
group.by.vars
(required)A character vector that specifies all the experimental covariates to be corrected/harmonized by the algorithm.
When using RunHarmony()
with Seurat, harmony will look
up the group.by.vars
metadata fields in the Seurat Object
metadata.
For example, given the pbmc[["stim"]]
exists as the stim
condition, setting group.by.vars="stim"
will perform
integration of these samples accordingly. If you want to integrate on
another variable, it needs to be present in Seurat object’s
meta.data.
To correct for several covariates, specify them in a vector:
group.by.vars = c("stim", "new_covariate")
.
reduction.use
The cell embeddings to be used for the batch alignment. This
parameter assumes that a reduced dimension already exists in the
reduction slot of the Seurat object. By default, the pca
reduction is used.
dims.use
Optional parameter which can use a name vector to select specific dimensions to be harmonized.
nclust
is a positive integer. Under the hood, harmony applies k-means
soft-clustering. For this task, k
needs to be determined.
nclust
corresponds to k
. The harmonization
results and performance are not particularly sensitive for a reasonable
range of this parameter value. If this parameter is not set, harmony
will autodetermine this based on the dataset size with a maximum cap of
200. For dataset with a vast amount of different cell types and batches
this pamameter may need to be determined manually.
sigma
a positive scalar that controls the soft clustering probability
assignment of single-cells to different clusters. Larger values will
assign a larger probability to distant clusters of cells resulting in a
different correction profile. Single-cells are assigned to clusters by
their euclidean distance d to
some cluster center Y after
cosine normalization which is defined in the range [0,4]. The clustering
probability of each cell is calculated as $e^{-\frac{d}{\sigma}}$ where σ is controlled by the
sigma
parameter. Default value of sigma
is 0.1
and it generally works well since it defines probability assignment of a
cell in the range [e−40, e0].
Larger values of sigma
restrict the dynamic range of
probabilities that can be assigned to cells. For example,
sigma=1
will yield a probabilities in the range of [e−4, e0].
theta
theta
is a positive scalar vector that determines the
coefficient of harmony’s diversity penalty for each corrected
experimental covariate. In challenging experimental conditions,
increasing theta may result in better integration results. Theta is an
expontential parameter of the diversity penalty, thus setting
theta=0
disables this penalty while increasing it to
greater values than 1 will perform more aggressive corrections in an
expontential manner. By default, it will set theta=2
for
each experimental covariate.
max_iter
The number of correction steps harmony will perform before completing
the data set integration. In general, more iterations than necessary
increases computational runtime especially which becomes evident in
bigger datasets. Setting early_stop=TRUE
may reduce the
actual number of correction steps which will be smaller than
max_iter
.
early_stop
Under the hood, harmony minimizes its objective function through a
series of clustering and integration tests. By setting
early_stop=TRUE
, when the objective function is less than
1e-4
after a correction step harmony exits before reaching
the max_iter
correction steps. This parameter can
drastically reduce run-time in bigger datasets.
.options
A set of internal algorithm parameters that can be overriden. For advanced users only.
These parameters are Seurat-specific and do not affect the flow of the algorithm.
project_dim
Toggle-like parameter, by default project_dim=TRUE
. When
enabled, RunHarmony()
calculates genomic feature loadings
using Seurat’s ProjectDim()
that correspond to the
harmonized cell embeddings.
reduction.save
The new Reduced Dimension slot identifier. By default,
reduction.save=TRUE
. This option allows several independent
runs of harmony to be retained in the appropriate slots in the
SeuratObjects. It is useful if you want to try Harmony with multiple
parameters and save them as e.g. ‘harmony_theta0’, ‘harmony_theta1’,
‘harmony_theta2’.
These parameters help users troubleshoot harmony.
plot_convergence
Option that plots the convergence plot after the execution of the
algorithm. By default FALSE
. Setting it to
TRUE
will collect harmony’s objective value and plot it
allowing the user to troubleshoot the flow of the algorithm and
fine-tune the parameters of the dataset integration procedure.
RunHarmony()
returns the Seurat object which contains
the harmonized cell embeddings in a slot named harmony.
This entry can be accessed via pbmc@reductions$harmony
. To
access the values of the cell embeddings we can also use:
After Harmony integration, we should inspect the quality of the harmonization and contrast it with the unharmonized algorithm input. Ideally, cells from different conditions will align along the Harmonized PCs. If they are not, you could increase the theta value above to force a more aggressive fit of the dataset and rerun the workflow.
p1 <- DimPlot(object = pbmc, reduction = "harmony", pt.size = .1, group.by = "stim")
p2 <- VlnPlot(object = pbmc, features = "harmony_1", group.by = "stim", pt.size = .1)
plot_grid(p1,p2)
Plot Genes correlated with the Harmonized PCs
The harmonized cell embeddings generated by harmony can be used for
further integrated analyses. In this workflow, the Seurat object
contains the harmony reduction
modality name in the method
that requires it.
pbmc <- pbmc %>%
FindNeighbors(reduction = "harmony") %>%
FindClusters(resolution = 0.5)
#> Computing nearest neighbor graph
#> Computing SNN
#> Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck
#>
#> Number of nodes: 2000
#> Number of edges: 71968
#>
#> Running Louvain algorithm...
#> Maximum modularity in 10 random starts: 0.8721
#> Number of communities: 9
#> Elapsed time: 0 seconds
pbmc <- pbmc %>%
RunTSNE(reduction = "harmony")
p1 <- DimPlot(pbmc, reduction = "tsne", group.by = "stim", pt.size = .1)
p2 <- DimPlot(pbmc, reduction = "tsne", label = TRUE, pt.size = .1)
plot_grid(p1, p2)
One important observation is to assess that the harmonized data contain biological states of the cells. Therefore by checking the following genes we can see that biological cell states are preserved after harmonization.
FeaturePlot(object = pbmc, features= c("CD3D", "SELL", "CREM", "CD8A", "GNLY", "CD79A", "FCGR3A", "CCL2", "PPBP"),
min.cutoff = "q9", cols = c("lightgrey", "blue"), pt.size = 0.5)
Very similarly with TSNE we can run UMAP by passing the harmony reduction in the function.
pbmc <- pbmc %>%
RunUMAP(reduction = "harmony", dims = 1:20)
#> Warning: The default method for RunUMAP has changed from calling Python UMAP via reticulate to the R-native UWOT using the cosine metric
#> To use Python UMAP via reticulate, set umap.method to 'umap-learn' and metric to 'correlation'
#> This message will be shown once per session
#> 22:55:25 UMAP embedding parameters a = 0.9922 b = 1.112
#> 22:55:25 Read 2000 rows and found 20 numeric columns
#> 22:55:25 Using Annoy for neighbor search, n_neighbors = 30
#> 22:55:25 Building Annoy index with metric = cosine, n_trees = 50
#> 0% 10 20 30 40 50 60 70 80 90 100%
#> [----|----|----|----|----|----|----|----|----|----|
#> **************************************************|
#> 22:55:25 Writing NN index file to temp file /tmp/RtmpQjz2Fd/file248078323c1c
#> 22:55:25 Searching Annoy index using 1 thread, search_k = 3000
#> 22:55:25 Annoy recall = 100%
#> 22:55:26 Commencing smooth kNN distance calibration using 1 thread with target n_neighbors = 30
#> 22:55:26 Initializing from normalized Laplacian + noise (using RSpectra)
#> 22:55:26 Commencing optimization for 500 epochs, with 83250 positive edges
#> 22:55:28 Optimization finished
p1 <- DimPlot(pbmc, reduction = "umap", group.by = "stim", pt.size = .1)
p2 <- DimPlot(pbmc, reduction = "umap", label = TRUE, pt.size = .1)
plot_grid(p1, p2)
sessionInfo()
#> R version 4.4.2 (2024-10-31)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=C
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] cowplot_1.1.3 dplyr_1.1.4 Seurat_5.1.0 SeuratObject_5.0.2
#> [5] sp_2.1-4 harmony_1.2.3 Rcpp_1.0.13-1 rmarkdown_2.29
#>
#> loaded via a namespace (and not attached):
#> [1] deldir_2.0-4 pbapply_1.7-2 gridExtra_2.3
#> [4] rlang_1.1.4 magrittr_2.0.3 RcppAnnoy_0.0.22
#> [7] spatstat.geom_3.3-3 matrixStats_1.4.1 ggridges_0.5.6
#> [10] compiler_4.4.2 png_0.1-8 vctrs_0.6.5
#> [13] reshape2_1.4.4 stringr_1.5.1 pkgconfig_2.0.3
#> [16] fastmap_1.2.0 labeling_0.4.3 utf8_1.2.4
#> [19] promises_1.3.0 purrr_1.0.2 xfun_0.49
#> [22] cachem_1.1.0 jsonlite_1.8.9 goftest_1.2-3
#> [25] later_1.3.2 spatstat.utils_3.1-1 irlba_2.3.5.1
#> [28] parallel_4.4.2 cluster_2.1.6 R6_2.5.1
#> [31] ica_1.0-3 spatstat.data_3.1-2 stringi_1.8.4
#> [34] bslib_0.8.0 RColorBrewer_1.1-3 reticulate_1.39.0
#> [37] spatstat.univar_3.1-1 parallelly_1.39.0 lmtest_0.9-40
#> [40] jquerylib_0.1.4 scattermore_1.2 knitr_1.49
#> [43] tensor_1.5 future.apply_1.11.3 zoo_1.8-12
#> [46] sctransform_0.4.1 httpuv_1.6.15 Matrix_1.7-1
#> [49] splines_4.4.2 igraph_2.1.1 tidyselect_1.2.1
#> [52] abind_1.4-8 yaml_2.3.10 spatstat.random_3.3-2
#> [55] spatstat.explore_3.3-3 codetools_0.2-20 miniUI_0.1.1.1
#> [58] listenv_0.9.1 lattice_0.22-6 tibble_3.2.1
#> [61] plyr_1.8.9 withr_3.0.2 shiny_1.9.1
#> [64] ROCR_1.0-11 evaluate_1.0.1 Rtsne_0.17
#> [67] future_1.34.0 fastDummies_1.7.4 survival_3.7-0
#> [70] polyclip_1.10-7 fitdistrplus_1.2-1 pillar_1.9.0
#> [73] KernSmooth_2.23-24 plotly_4.10.4 generics_0.1.3
#> [76] RcppHNSW_0.6.0 ggplot2_3.5.1 munsell_0.5.1
#> [79] scales_1.3.0 globals_0.16.3 xtable_1.8-4
#> [82] RhpcBLASctl_0.23-42 glue_1.8.0 lazyeval_0.2.2
#> [85] maketools_1.3.1 tools_4.4.2 sys_3.4.3
#> [88] data.table_1.16.2 RSpectra_0.16-2 RANN_2.6.2
#> [91] buildtools_1.0.0 leiden_0.4.3.1 dotCall64_1.2
#> [94] grid_4.4.2 tidyr_1.3.1 colorspace_2.1-1
#> [97] nlme_3.1-166 patchwork_1.3.0 cli_3.6.3
#> [100] spatstat.sparse_3.1-0 spam_2.11-0 fansi_1.0.6
#> [103] viridisLite_0.4.2 uwot_0.2.2 gtable_0.3.6
#> [106] sass_0.4.9 digest_0.6.37 progressr_0.15.0
#> [109] ggrepel_0.9.6 htmlwidgets_1.6.4 farver_2.1.2
#> [112] htmltools_0.5.8.1 lifecycle_1.0.4 httr_1.4.7
#> [115] mime_0.12 MASS_7.3-61