Here we demonstrate the integration of multiple single-cell ATAC-seq datasets from the adult mouse brain, profiled using 10x Genomics and sci-ATAC-seq. The dataset from 10x Genomics is the same as used in our mouse brain vignette, and you can find links to download the raw data as well as the code used to produce the processed Seurat object used in this vignette on the mouse brain vignette page.

The sci-ATAC-seq dataset was generated by Cusanovich and Hill et al.. Raw data can be downloaded from the authors’ website:

We will demonstrate the use of Seurat v3 integration methods described here on scATAC-seq data, for both dataset integration and label transfer between datasets, as well as use of the harmony package for dataset integration.

First, we load the required packages, read in the data and create a Seurat object for the sci-ATAC-seq data.



# this object was created following the mouse brain vignette
tenx <- readRDS(file = "../vignette_data/adult_mouse_brain.rds")
tenx$tech <- '10x'
tenx$celltype <- Idents(tenx)

sci.metadata <- read.table(
  file = "../vignette_data/sci/cell_metadata.txt",
  header = TRUE,
  row.names = 1,
  sep = "\t"
# subset to include only the brain data
sci.metadata <- sci.metadata[sci.metadata$tissue == 'PreFrontalCortex', ]
sci.counts <- readRDS(file = "../vignette_data/sci/atac_matrix.binary.qc_filtered.rds")
sci.counts <- sci.counts[, rownames(x = sci.metadata)]

The sci-ATAC-seq data was mapped to the mm9 mouse genome, whereas the 10x data was mapped to mm10, and so the coordinate space of the peaks in each experiment are different. We can lift over the mm9 coordinates to mm10 using the rtracklayer package, and rename the sci-ATAC-seq peaks with mm10 coordinates. Liftover chains can be downloaded from UCSC. See this post on biostars for more information about lifting over genomic coordinates.

sci_peaks_mm9 <- StringToGRanges(regions = rownames(sci.counts), sep = c("_", "_"))
mm9_mm10 <- rtracklayer::import.chain("/home/stuartt/data/liftover/mm9ToMm10.over.chain")
sci_peaks_mm10 <- rtracklayer::liftOver(x = sci_peaks_mm9, chain = mm9_mm10)
names(sci_peaks_mm10) <- rownames(sci.counts)

# discard any peaks that were mapped to >1 region in mm10
correspondence <- S4Vectors::elementNROWS(sci_peaks_mm10)
sci_peaks_mm10 <- sci_peaks_mm10[correspondence == 1]
sci_peaks_mm10 <- unlist(sci_peaks_mm10)
sci.counts <- sci.counts[names(sci_peaks_mm10), ]

# rename peaks with mm10 coordinates
rownames(sci.counts) <- GRangesToString(grange = sci_peaks_mm10)

# create Seurat object and perform some basic QC filtering
sci <- CreateSeuratObject(
  counts = sci.counts, = sci.metadata,
  assay = 'peaks',
  project = 'sci'
sci <- sci[, sci$nFeature_peaks > 2000 & !(sci$cell_label %in% c("Collisions", "Unknown"))]
sci$tech <- 'sciATAC'
sci <- RunTFIDF(sci)
sci <- FindTopFeatures(sci, min.cutoff = 50)
sci <- RunSVD(sci, n = 30, = 'lsi', reduction.key = 'LSI_')
sci <- RunUMAP(sci, reduction = 'lsi', dims = 1:30)

We now have two scATAC-seq objects containing features in a common coordinate space. However, as the peak calling was performed independently in each experiment, it is unlikely that the peak coordinates overlap perfectly. In order to have common features across the datasets we want to integrate, we can count reads in the sci-ATAC-seq peaks in the 10x Genomics dataset and create a new assay with these counts. For efficiency, we will downsample the sci-ATAC-seq peaks used. We have found integration results to be quite robust to the number and set of peaks used.

# find peaks that intersect in both datasets
intersecting.regions <- GetIntersectingFeatures(
  object.1 = sci,
  object.2 = tenx,
  sep.1 = c("-", "-"),
  sep.2 = c(":", "-")

# choose a subset of intersecting peaks
peaks.use <- sample(intersecting.regions[[1]], size = 10000, replace = FALSE)

# count fragments per cell overlapping the set of peaks in the 10x data
sci_peaks_tenx <- FeatureMatrix(
  fragments = GetFragments(object = tenx, assay = 'peaks'),
  features = StringToGRanges(peaks.use),
  cells = colnames(tenx)

# create a new assay and add it to the 10x dataset
tenx[['sciPeaks']] <- CreateAssayObject(counts = sci_peaks_tenx, min.cells = 1)
tenx <- RunTFIDF(object = tenx, assay = 'sciPeaks')


Before performing integration it is always a good idea to check if there are dataset-specific difference present that need to be removed. If not, we could simply merge the objects without performing integration. In this case there is a strong difference between the two datasets due to the different technologies used. This effect can be removed using the integration methods in Seurat v3 described in this paper.

# Look at the data without integration first
unintegrated <- MergeWithRegions(
  object.1 = sci,
  object.2 = tenx,
  sep.1 = c("-", "-"),
  sep.2 = c(":", "-")

unintegrated <- RunTFIDF(unintegrated)
unintegrated <- FindTopFeatures(unintegrated, min.cutoff = 50)
unintegrated <- RunSVD(unintegrated, n = 30, = 'lsi', reduction.key = 'LSI_')
unintegrated <- RunUMAP(unintegrated, reduction = 'lsi', dims = 1:30)
p1 <- DimPlot(unintegrated, = 'tech', pt.size = 0.1) + ggplot2::ggtitle("Unintegrated")

# find integration anchors between 10x and sci-ATAC
anchors <- FindIntegrationAnchors(
  object.list = list(tenx, sci),
  anchor.features = peaks.use,
  assay = c('sciPeaks', 'peaks'),
  k.filter = NA

# integrate data and create a new merged object
integrated <- IntegrateData(
  anchorset = anchors,
  weight.reduction = sci[['lsi']],
  preserve.order = TRUE

# we now have a "corrected" TF-IDF matrix, and can run LSI again on this corrected matrix
integrated <- RunSVD(
  object = integrated,
  n = 30, = 'integratedLSI'

integrated <- RunUMAP(
  object = integrated,
  dims = 1:30,
  reduction = 'integratedLSI'

p2 <- DimPlot(integrated, = 'tech', pt.size = 0.1) + ggplot2::ggtitle("Integrated")
CombinePlots(list(p1, p2), ncol = 2)

Label transfer

We can also use Seurat to transfer data from one dataset to another. We previously demonstrated this between scRNA-seq datasets, and between scRNA-seq and scATAC-seq datasets ( Here, we demonstrate data transfer between two single-cell ATAC-seq datasets by transferring the cell type label from the 10x Genomics scATAC-seq data to the sci-ATAC-seq data.

transfer.anchors <- FindTransferAnchors(
  reference = tenx,
  query = sci,
  reference.assay = 'sciPeaks',
  query.assay = 'peaks',
  reduction = 'cca',
  features = peaks.use,
  k.filter = NA
) <- TransferData(
  anchorset = transfer.anchors,
  refdata = tenx$celltype,
  weight.reduction = sci[['lsi']]

sci <- AddMetaData(
  object = sci,
  metadata =

sci$ <- factor(sci$, levels = levels(tenx$celltype))  # to make the colors match

p3 <- DimPlot(tenx, = 'celltype', label = TRUE) + NoLegend() + ggplot2::ggtitle("Celltype labels 10x scATAC-seq")
p4 <- DimPlot(sci, = '', label = TRUE) + NoLegend() + ggplot2::ggtitle("Predicted labels sci-ATAC-seq")
CombinePlots(list(p3, p4), ncol = 2)

Integration with Harmony

While Seurat adjusts the underlying high-dimensional data in order to correct for differences between the groups, harmony (developed by the Raychaudhuri lab) adjusts the low-dimensional cell embeddings to to reduce the dependence between cluster assignment and dataset of origin. You can read more about Harmony here.

Harmony requires a single object as input, so here we merge the sci-ATAC-seq and scATAC-seq datasets in a coordinate-aware manner using the MergeWithRegions function, and then compute LSI embeddings using the merged object. Once we have computed the LSI embeddings, we can run the RunHarmony function from the harmony package and provide the technology used as a grouping variable to remove the batch difference between the sci-ATAC-seq and 10x Genomics scATAC-seq datasets. This produces a set of “corrected” LSI embeddings that can be used for further dimension reduction with UMAP or tSNE, and for clustering.

brain <- MergeWithRegions(
  object.1 = tenx,
  object.2 = sci,
  sep.1 = c(":", "-"),
  sep.2 = c("-", "-")

brain <- FindTopFeatures(brain, min.cutoff = 0)
brain <- RunTFIDF(brain)
brain <- RunSVD(brain, n = 30, = 'lsi', reduction.key = 'LSI_')
brain <- RunUMAP(brain, dims = 1:30, reduction = 'lsi')
p5 <- DimPlot(brain, = 'tech', pt.size = 0.1) + ggplot2::ggtitle("Unintegrated")


brain <- RunHarmony(
  object = brain, = 'tech',
  reduction = 'lsi',
  assay.use = 'peaks',
  project.dim = FALSE

# re-compute the UMAP using corrected LSI embeddings
brain <- RunUMAP(brain, dims = 1:30, reduction = 'harmony')
p6 <- DimPlot(brain, = 'tech', pt.size = 0.1) + ggplot2::ggtitle("Harmony integration")
CombinePlots(plots = list(p5, p6), ncol = 2)