In this vignette, we’ll demonstrate how to jointly analyze a single-cell dataset measuring both DNA accessibility and gene expression in the same cells using Signac and Seurat. In this vignette we’ll be using a publicly available 10x Genomic Multiome dataset for human PBMCs.

View data download code

You can download the required data by running the following lines in a shell:

wget https://cf.10xgenomics.com/samples/cell-arc/1.0.0/pbmc_granulocyte_sorted_10k/pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5
wget https://cf.10xgenomics.com/samples/cell-arc/1.0.0/pbmc_granulocyte_sorted_10k/pbmc_granulocyte_sorted_10k_atac_fragments.tsv.gz
wget https://cf.10xgenomics.com/samples/cell-arc/1.0.0/pbmc_granulocyte_sorted_10k/pbmc_granulocyte_sorted_10k_atac_fragments.tsv.gz.tbi
library(Signac)
library(Seurat)
library(EnsDb.Hsapiens.v86)
library(BSgenome.Hsapiens.UCSC.hg38)
# load the RNA and ATAC data
counts <- Read10X_h5("pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5")
fragpath <- "pbmc_granulocyte_sorted_10k_atac_fragments.tsv.gz"
# get gene annotations for hg38
annotation <- GetGRangesFromEnsDb(ensdb = EnsDb.Hsapiens.v86)
seqlevels(annotation) <- paste0('chr', seqlevels(annotation))

# create a Seurat object containing the RNA adata
pbmc <- CreateSeuratObject(
  counts = counts$`Gene Expression`,
  assay = "RNA"
)

# create ATAC assay and add it to the object
pbmc[["ATAC"]] <- CreateChromatinAssay(
  counts = counts$Peaks,
  sep = c(":", "-"),
  fragments = fragpath,
  annotation = annotation
)
pbmc
## An object of class Seurat 
## 144978 features across 11909 samples within 2 assays 
## Active assay: RNA (36601 features, 0 variable features)
##  1 layer present: counts
##  1 other assay present: ATAC

Quality control

We can compute per-cell quality control metrics using the DNA accessibility data and remove cells that are outliers for these metrics, as well as cells with low or unusually high counts for either the RNA or ATAC assay.

DefaultAssay(pbmc) <- "ATAC"

pbmc <- NucleosomeSignal(pbmc)
pbmc <- TSSEnrichment(pbmc)

The relationship between variables stored in the object metadata can be visualized using the DensityScatter() function. This can also be used to quickly find suitable cutoff values for different QC metrics by setting quantiles=TRUE:

DensityScatter(pbmc, x = 'nCount_ATAC', y = 'TSS.enrichment', log_x = TRUE, quantiles = TRUE)

VlnPlot(
  object = pbmc,
  features = c("nCount_RNA", "nCount_ATAC", "TSS.enrichment", "nucleosome_signal"),
  ncol = 4,
  pt.size = 0
)

# filter out low quality cells
pbmc <- subset(
  x = pbmc,
  subset = nCount_ATAC < 100000 &
    nCount_RNA < 25000 &
    nCount_ATAC > 1800 &
    nCount_RNA > 1000 &
    nucleosome_signal < 2 &
    TSS.enrichment > 1
)
pbmc
## An object of class Seurat 
## 144978 features across 11070 samples within 2 assays 
## Active assay: ATAC (108377 features, 0 variable features)
##  2 layers present: counts, data
##  1 other assay present: RNA

Gene expression data processing

We can normalize the gene expression data using SCTransform, and reduce the dimensionality using PCA.

DefaultAssay(pbmc) <- "RNA"
pbmc <- SCTransform(pbmc)
pbmc <- RunPCA(pbmc)

DNA accessibility data processing

Here we process the DNA accessibility assay the same way we would process a scATAC-seq dataset, by performing latent semantic indexing (LSI).

DefaultAssay(pbmc) <- "ATAC"
pbmc <- FindTopFeatures(pbmc, min.cutoff = 5)
pbmc <- RunTFIDF(pbmc)
pbmc <- RunSVD(pbmc)

Annotating cell types

To annotate cell types in the dataset we can transfer cell labels from an existing PBMC reference dataset using tools in the Seurat package. See the Seurat reference mapping vignette for more information.

We’ll use an annotated PBMC reference dataset from Hao et al. (2020), available for download here: https://atlas.fredhutch.org/data/nygc/multimodal/pbmc_multimodal.h5seurat

Note that the SeuratDisk package is required to load the reference dataset. Installation instructions for SeuratDisk can be found here.

library(SeuratDisk)

# load PBMC reference
reference <- LoadH5Seurat("pbmc_multimodal.h5seurat", assays = list("SCT" = "counts"), reductions = 'spca')
reference <- UpdateSeuratObject(reference)

DefaultAssay(pbmc) <- "SCT"

# transfer cell type labels from reference to query
transfer_anchors <- FindTransferAnchors(
  reference = reference,
  query = pbmc,
  normalization.method = "SCT",
  reference.reduction = "spca",
  recompute.residuals = FALSE,
  dims = 1:50
)

predictions <- TransferData(
  anchorset = transfer_anchors, 
  refdata = reference$celltype.l2,
  weight.reduction = pbmc[['pca']],
  dims = 1:50
)

pbmc <- AddMetaData(
  object = pbmc,
  metadata = predictions
)

# set the cell identities to the cell type predictions
Idents(pbmc) <- "predicted.id"

# remove low-quality predictions
pbmc <- pbmc[, pbmc$prediction.score.max > 0.5]

Joint UMAP visualization

Using the weighted nearest neighbor methods in Seurat v4, we can compute a joint neighbor graph that represent both the gene expression and DNA accessibility measurements.

# build a joint neighbor graph using both assays
pbmc <- FindMultiModalNeighbors(
  object = pbmc,
  reduction.list = list("pca", "lsi"), 
  dims.list = list(1:50, 2:40),
  modality.weight.name = "RNA.weight",
  verbose = TRUE
)

# build a joint UMAP visualization
pbmc <- RunUMAP(
  object = pbmc,
  nn.name = "weighted.nn",
  assay = "RNA",
  verbose = TRUE
)

DimPlot(pbmc, label = TRUE, repel = TRUE, reduction = "umap") + NoLegend()

Linking peaks to genes

For each gene, we can find the set of peaks that may regulate the gene by by computing the correlation between gene expression and accessibility at nearby peaks, and correcting for bias due to GC content, overall accessibility, and peak size. See the Signac paper for a full description of the method we use to link peaks to genes.

Running this step on the whole genome can be time consuming, so here we demonstrate peak-gene links for a subset of genes as an example. The same function can be used to find links for all genes by omitting the genes.use parameter:

DefaultAssay(pbmc) <- "ATAC"

# first compute the GC content for each peak
pbmc <- RegionStats(pbmc, genome = BSgenome.Hsapiens.UCSC.hg38)

# link peaks to genes
pbmc <- LinkPeaks(
  object = pbmc,
  peak.assay = "ATAC",
  expression.assay = "SCT",
  genes.use = c("LYZ", "MS4A1")
)

We can visualize these links using the CoveragePlot() function, or alternatively we could use the CoverageBrowser() function in an interactive analysis:

idents.plot <- c("B naive", "B intermediate", "B memory",
                 "CD14 Mono", "CD16 Mono", "CD8 TEM", "CD8 Naive")

pbmc <- SortIdents(pbmc)

p1 <- CoveragePlot(
  object = pbmc,
  region = "MS4A1",
  features = "MS4A1",
  expression.assay = "SCT",
  idents = idents.plot,
  extend.upstream = 500,
  extend.downstream = 10000
)

p2 <- CoveragePlot(
  object = pbmc,
  region = "LYZ",
  features = "LYZ",
  expression.assay = "SCT",
  idents = idents.plot,
  extend.upstream = 8000,
  extend.downstream = 5000
)

patchwork::wrap_plots(p1, p2, ncol = 1)

Session Info
## R version 4.3.1 (2023-06-16)
## Platform: aarch64-apple-darwin20 (64-bit)
## Running under: macOS Sonoma 14.5
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: Asia/Singapore
## tzcode source: internal
## 
## attached base packages:
## [1] stats4    stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] SeuratDisk_0.0.0.9021             BSgenome.Hsapiens.UCSC.hg38_1.4.5
##  [3] BSgenome_1.70.2                   rtracklayer_1.62.0               
##  [5] BiocIO_1.12.0                     Biostrings_2.70.3                
##  [7] XVector_0.42.0                    EnsDb.Hsapiens.v86_2.99.0        
##  [9] ensembldb_2.26.0                  AnnotationFilter_1.26.0          
## [11] GenomicFeatures_1.54.4            AnnotationDbi_1.64.1             
## [13] Biobase_2.62.0                    GenomicRanges_1.54.1             
## [15] GenomeInfoDb_1.38.8               IRanges_2.36.0                   
## [17] S4Vectors_0.40.2                  BiocGenerics_0.48.1              
## [19] Seurat_5.1.0                      SeuratObject_5.0.2               
## [21] sp_2.1-4                          Signac_1.14.0                    
## 
## loaded via a namespace (and not attached):
##   [1] fs_1.6.4                    ProtGenerics_1.34.0        
##   [3] matrixStats_1.3.0           spatstat.sparse_3.0-3      
##   [5] bitops_1.0-8                httr_1.4.7                 
##   [7] RColorBrewer_1.1-3          backports_1.5.0            
##   [9] tools_4.3.1                 sctransform_0.4.1          
##  [11] utf8_1.2.4                  R6_2.5.1                   
##  [13] lazyeval_0.2.2              uwot_0.1.16                
##  [15] withr_3.0.1                 prettyunits_1.2.0          
##  [17] gridExtra_2.3               progressr_0.14.0           
##  [19] cli_3.6.3                   textshaping_0.4.0          
##  [21] spatstat.explore_3.2-7      fastDummies_1.7.3          
##  [23] labeling_0.4.3              sass_0.4.9                 
##  [25] spatstat.data_3.0-4         ggridges_0.5.6             
##  [27] pbapply_1.7-2               pkgdown_2.0.9              
##  [29] Rsamtools_2.18.0            systemfonts_1.1.0          
##  [31] foreign_0.8-86              dichromat_2.0-0.1          
##  [33] parallelly_1.38.0           rstudioapi_0.16.0          
##  [35] RSQLite_2.3.7               generics_0.1.3             
##  [37] ica_1.0-3                   spatstat.random_3.2-3      
##  [39] dplyr_1.1.4                 Matrix_1.6-5               
##  [41] ggbeeswarm_0.7.2            fansi_1.0.6                
##  [43] abind_1.4-5                 lifecycle_1.0.4            
##  [45] yaml_2.3.10                 SummarizedExperiment_1.32.0
##  [47] SparseArray_1.2.4           BiocFileCache_2.10.2       
##  [49] Rtsne_0.17                  glmGamPoi_1.14.3           
##  [51] grid_4.3.1                  blob_1.2.4                 
##  [53] promises_1.3.0              crayon_1.5.3               
##  [55] miniUI_0.1.1.1              lattice_0.22-6             
##  [57] cowplot_1.1.3               KEGGREST_1.42.0            
##  [59] pillar_1.9.0                knitr_1.48                 
##  [61] rjson_0.2.21                future.apply_1.11.2        
##  [63] codetools_0.2-19            fastmatch_1.1-4            
##  [65] leiden_0.4.3.1              glue_1.7.0                 
##  [67] data.table_1.15.4           remotes_2.5.0              
##  [69] vctrs_0.6.5                 png_0.1-8                  
##  [71] spam_2.10-0                 gtable_0.3.5               
##  [73] cachem_1.1.0                xfun_0.47                  
##  [75] S4Arrays_1.2.1              mime_0.12                  
##  [77] survival_3.6-4              RcppRoll_0.3.1             
##  [79] fitdistrplus_1.1-11         ROCR_1.0-11                
##  [81] nlme_3.1-164                bit64_4.0.5                
##  [83] progress_1.2.3              filelock_1.0.3             
##  [85] RcppAnnoy_0.0.22            bslib_0.8.0                
##  [87] irlba_2.3.5.1               vipor_0.4.7                
##  [89] KernSmooth_2.23-24          rpart_4.1.23               
##  [91] colorspace_2.1-1            DBI_1.2.3                  
##  [93] Hmisc_5.1-3                 nnet_7.3-19                
##  [95] ggrastr_1.0.2               tidyselect_1.2.1           
##  [97] bit_4.0.5                   compiler_4.3.1             
##  [99] curl_5.2.1                  htmlTable_2.4.3            
## [101] hdf5r_1.3.10                xml2_1.3.6                 
## [103] desc_1.4.3                  DelayedArray_0.28.0        
## [105] plotly_4.10.4               checkmate_2.3.2            
## [107] scales_1.3.0                lmtest_0.9-40              
## [109] rappdirs_0.3.3              stringr_1.5.1              
## [111] digest_0.6.37               goftest_1.2-3              
## [113] spatstat.utils_3.0-4        rmarkdown_2.28             
## [115] htmltools_0.5.8.1           pkgconfig_2.0.3            
## [117] base64enc_0.1-3             sparseMatrixStats_1.14.0   
## [119] MatrixGenerics_1.14.0       highr_0.11                 
## [121] dbplyr_2.5.0                fastmap_1.2.0              
## [123] rlang_1.1.4                 htmlwidgets_1.6.4          
## [125] DelayedMatrixStats_1.24.0   shiny_1.9.1                
## [127] farver_2.1.2                jquerylib_0.1.4            
## [129] zoo_1.8-12                  jsonlite_1.8.8             
## [131] BiocParallel_1.36.0         VariantAnnotation_1.48.1   
## [133] RCurl_1.98-1.16             magrittr_2.0.3             
## [135] Formula_1.2-5               GenomeInfoDbData_1.2.11    
## [137] dotCall64_1.1-1             patchwork_1.2.0            
## [139] munsell_0.5.1               Rcpp_1.0.13                
## [141] reticulate_1.37.0           stringi_1.8.4              
## [143] zlibbioc_1.48.2             MASS_7.3-60.0.1            
## [145] plyr_1.8.9                  parallel_4.3.1             
## [147] listenv_0.9.1               ggrepel_0.9.5              
## [149] deldir_2.0-4                splines_4.3.1              
## [151] tensor_1.5                  hms_1.1.3                  
## [153] igraph_2.0.3                spatstat.geom_3.2-9        
## [155] RcppHNSW_0.6.0              reshape2_1.4.4             
## [157] biomaRt_2.58.2              XML_3.99-0.17              
## [159] evaluate_0.24.0             biovizBase_1.50.0          
## [161] BiocManager_1.30.23         tweenr_2.0.3               
## [163] httpuv_1.6.15               RANN_2.6.1                 
## [165] tidyr_1.3.1                 purrr_1.0.2                
## [167] polyclip_1.10-6             future_1.34.0              
## [169] scattermore_1.2             ggplot2_3.5.1              
## [171] ggforce_0.4.2               xtable_1.8-4               
## [173] restfulr_0.0.15             RSpectra_0.16-2            
## [175] later_1.3.2                 viridisLite_0.4.2          
## [177] ragg_1.3.2                  tibble_3.2.1               
## [179] beeswarm_0.4.0              memoise_2.0.1              
## [181] GenomicAlignments_1.38.2    cluster_2.1.6              
## [183] globals_0.16.3