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("../vignette_data/multiomic/pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5")
fragpath <- "../vignette_data/multiomic/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 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)

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 > 1000 &
    nCount_RNA > 1000 &
    nucleosome_signal < 2 &
    TSS.enrichment > 1
)
pbmc
## An object of class Seurat 
## 144978 features across 11331 samples within 2 assays 
## Active assay: ATAC (108377 features, 0 variable features)
##  1 other assay present: RNA

Peak calling

The set of peaks identified using Cellranger often merges distinct peaks that are close together. This can create a problem for certain analyses, particularly motif enrichment analysis and peak-to-gene linkage. To identify a more accurate set of peaks, we can call peaks using MACS2 with the CallPeaks() function. Here we call peaks on all cells together, but we could identify peaks for each group of cells separately by setting the group.by parameter, and this can help identify peaks specific to rare cell populations.

# call peaks using MACS2
peaks <- CallPeaks(pbmc)

# remove peaks on nonstandard chromosomes and in genomic blacklist regions
peaks <- keepStandardChromosomes(peaks, pruning.mode = "coarse")
peaks <- subsetByOverlaps(x = peaks, ranges = blacklist_hg38_unified, invert = TRUE)
# quantify counts in each peak
macs2_counts <- FeatureMatrix(
  fragments = Fragments(pbmc),
  features = peaks,
  cells = colnames(pbmc)
)
# create a new assay using the MACS2 peak set and add it to the Seurat object
pbmc[["peaks"]] <- CreateChromatinAssay(
  counts = macs2_counts,
  fragments = fragpath,
  annotation = annotation
)
## Computing hash

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) <- "peaks"
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("../vignette_data/multiomic/pbmc_multimodal.h5seurat", assays = list("SCT" = "counts"), reductions = 'spca')

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"

# set a reasonable order for cell types to be displayed when plotting
levels(pbmc) <- c("CD4 Naive", "CD4 TCM", "CD4 CTL", "CD4 TEM", "CD4 Proliferating",
                  "CD8 Naive", "dnT",
                 "CD8 TEM", "CD8 TCM", "CD8 Proliferating", "MAIT", "NK", "NK_CD56bright",
                 "NK Proliferating", "gdT",
                 "Treg", "B naive", "B intermediate", "B memory", "Plasmablast",
                 "CD14 Mono", "CD16 Mono",
                 "cDC1", "cDC2", "pDC", "HSPC", "Eryth", "ASDC", "ILC", "Platelet")

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) <- "peaks"

# 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 = "peaks",
  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")

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.0
## 
## 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.9020             BSgenome.Hsapiens.UCSC.hg38_1.4.5
##  [3] BSgenome_1.68.0                   rtracklayer_1.60.1               
##  [5] Biostrings_2.68.1                 XVector_0.40.0                   
##  [7] EnsDb.Hsapiens.v86_2.99.0         ensembldb_2.24.1                 
##  [9] AnnotationFilter_1.24.0           GenomicFeatures_1.52.2           
## [11] AnnotationDbi_1.62.2              Biobase_2.60.0                   
## [13] GenomicRanges_1.52.0              GenomeInfoDb_1.36.3              
## [15] IRanges_2.34.1                    S4Vectors_0.38.2                 
## [17] BiocGenerics_0.46.0               SeuratObject_4.1.4               
## [19] Seurat_4.4.0                      Signac_1.11.0                    
## 
## loaded via a namespace (and not attached):
##   [1] fs_1.6.3                    ProtGenerics_1.32.0        
##   [3] matrixStats_1.0.0           spatstat.sparse_3.0-2      
##   [5] bitops_1.0-7                httr_1.4.7                 
##   [7] RColorBrewer_1.1-3          docopt_0.7.1               
##   [9] tools_4.3.1                 sctransform_0.4.0          
##  [11] backports_1.4.1             utf8_1.2.3                 
##  [13] R6_2.5.1                    lazyeval_0.2.2             
##  [15] uwot_0.1.16                 withr_2.5.1                
##  [17] sp_2.1-0                    prettyunits_1.2.0          
##  [19] gridExtra_2.3               progressr_0.14.0           
##  [21] cli_3.6.1                   textshaping_0.3.7          
##  [23] spatstat.explore_3.2-3      slam_0.1-50                
##  [25] labeling_0.4.3              sass_0.4.7                 
##  [27] spatstat.data_3.0-1         ggridges_0.5.4             
##  [29] pbapply_1.7-2               pkgdown_2.0.7              
##  [31] Rsamtools_2.16.0            systemfonts_1.0.5          
##  [33] foreign_0.8-84              dichromat_2.0-0.1          
##  [35] parallelly_1.36.0           rstudioapi_0.15.0          
##  [37] RSQLite_2.3.1               generics_0.1.3             
##  [39] BiocIO_1.10.0               ica_1.0-3                  
##  [41] spatstat.random_3.1-6       dplyr_1.1.3                
##  [43] Matrix_1.6-1.1              ggbeeswarm_0.7.2           
##  [45] fansi_1.0.5                 abind_1.4-5                
##  [47] lifecycle_1.0.3             yaml_2.3.7                 
##  [49] SummarizedExperiment_1.30.2 BiocFileCache_2.8.0        
##  [51] Rtsne_0.16                  grid_4.3.1                 
##  [53] blob_1.2.4                  promises_1.2.1             
##  [55] crayon_1.5.2                miniUI_0.1.1.1             
##  [57] lattice_0.21-8              cowplot_1.1.1              
##  [59] KEGGREST_1.40.1             pillar_1.9.0               
##  [61] knitr_1.44                  rjson_0.2.21               
##  [63] future.apply_1.11.0         codetools_0.2-19           
##  [65] fastmatch_1.1-4             leiden_0.4.3               
##  [67] glue_1.6.2                  data.table_1.14.8          
##  [69] remotes_2.4.2.1             vctrs_0.6.3                
##  [71] png_0.1-8                   gtable_0.3.4               
##  [73] cachem_1.0.8                xfun_0.40                  
##  [75] S4Arrays_1.0.6              mime_0.12                  
##  [77] qlcMatrix_0.9.7             survival_3.5-5             
##  [79] RcppRoll_0.3.0              ellipsis_0.3.2             
##  [81] fitdistrplus_1.1-11         ROCR_1.0-11                
##  [83] nlme_3.1-162                bit64_4.0.5                
##  [85] progress_1.2.2              filelock_1.0.2             
##  [87] RcppAnnoy_0.0.21            rprojroot_2.0.3            
##  [89] bslib_0.5.1                 irlba_2.3.5.1              
##  [91] vipor_0.4.5                 KernSmooth_2.23-21         
##  [93] rpart_4.1.19                colorspace_2.1-0           
##  [95] DBI_1.1.3                   Hmisc_5.1-1                
##  [97] nnet_7.3-19                 ggrastr_1.0.2              
##  [99] tidyselect_1.2.0            bit_4.0.5                  
## [101] compiler_4.3.1              curl_5.1.0                 
## [103] htmlTable_2.4.1             hdf5r_1.3.8                
## [105] xml2_1.3.5                  desc_1.4.2                 
## [107] DelayedArray_0.26.7         plotly_4.10.2              
## [109] checkmate_2.2.0             scales_1.2.1               
## [111] lmtest_0.9-40               rappdirs_0.3.3             
## [113] stringr_1.5.0               digest_0.6.33              
## [115] goftest_1.2-3               spatstat.utils_3.0-3       
## [117] sparsesvd_0.2-2             rmarkdown_2.25             
## [119] htmltools_0.5.6.1           pkgconfig_2.0.3            
## [121] base64enc_0.1-3             MatrixGenerics_1.12.3      
## [123] dbplyr_2.3.4                fastmap_1.1.1              
## [125] rlang_1.1.1                 htmlwidgets_1.6.2          
## [127] shiny_1.7.5                 farver_2.1.1               
## [129] jquerylib_0.1.4             zoo_1.8-12                 
## [131] jsonlite_1.8.7              BiocParallel_1.34.2        
## [133] VariantAnnotation_1.46.0    RCurl_1.98-1.12            
## [135] magrittr_2.0.3              Formula_1.2-5              
## [137] GenomeInfoDbData_1.2.10     patchwork_1.1.3            
## [139] munsell_0.5.0               Rcpp_1.0.11                
## [141] reticulate_1.32.0           stringi_1.7.12             
## [143] zlibbioc_1.46.0             MASS_7.3-60                
## [145] plyr_1.8.9                  parallel_4.3.1             
## [147] listenv_0.9.0               ggrepel_0.9.3              
## [149] deldir_1.0-9                splines_4.3.1              
## [151] tensor_1.5                  hms_1.1.3                  
## [153] igraph_1.5.1                spatstat.geom_3.2-5        
## [155] reshape2_1.4.4              biomaRt_2.56.1             
## [157] XML_3.99-0.14               evaluate_0.22              
## [159] biovizBase_1.48.0           tweenr_2.0.2               
## [161] httpuv_1.6.11               RANN_2.6.1                 
## [163] tidyr_1.3.0                 purrr_1.0.2                
## [165] polyclip_1.10-6             future_1.33.0              
## [167] scattermore_1.2             ggplot2_3.4.4              
## [169] ggforce_0.4.1               xtable_1.8-4               
## [171] restfulr_0.0.15             later_1.3.1                
## [173] viridisLite_0.4.2           ragg_1.2.6                 
## [175] tibble_3.2.1                memoise_2.0.1              
## [177] beeswarm_0.4.0              GenomicAlignments_1.36.0   
## [179] cluster_2.1.4               globals_0.16.2