Skip to main content
STAR Protocols logoLink to STAR Protocols
. 2024 Aug 2;5(3):103209. doi: 10.1016/j.xpro.2024.103209

Protocol for analysis of single-cell sequencing data by Seqtometry

Robert Kousnetsov 1,2, Daniel Hawiger 1,2,3,
PMCID: PMC11342777  PMID: 39096493

Summary

Seqtometry (sequencing-to-measurement) is an analytical platform for single-cell analysis based on direct profiling of gene expression and accessibility achieved by advanced scoring with gene signatures. Here, we present a protocol for single-cell RNA sequencing (scRNA-seq) and single-cell assay for transposase-accessible chromatin using sequencing (scATAC-seq) analysis using Seqtometry. We describe steps for preprocessing, imputation, scoring, and plotting, with extensions to large datasets and integration of multiple datasets. This protocol yields results in the form of biologically interpretable dimensions for direct identification and comprehensive characterization of specific cells.

For complete details on the use and execution of this protocol, please refer to Kousnetsov et al.1

Subject areas: Bioinformatics, Sequence analysis, Cell Biology, Immunology, Systems biology

Graphical abstract

graphic file with name fx1.jpg

Highlights

  • Steps for processing and scoring of single-cell sequencing data by Seqtometry

  • Visualization of signature scores as biologically interpretable results

  • Workflow for analysis of different kinds of scRNA-seq and scATAC-seq data

  • Steps for hierarchical Seqtometry analysis by gating and rescoring


Publisher’s note: Undertaking any experimental protocol requires adherence to local institutional guidelines for laboratory safety and ethics.


Seqtometry (sequencing-to-measurement) is an analytical platform for single-cell analysis based on direct profiling of gene expression and accessibility achieved by advanced scoring with gene signatures. Here, we present a protocol for single-cell RNA sequencing (scRNA-seq) and single-cell assay for transposase-accessible chromatin using sequencing (scATAC-seq) analysis using Seqtometry. We describe steps for preprocessing, imputation, scoring, and plotting, with extensions to large datasets and integration of multiple datasets. This protocol yields results in the form of biologically interpretable dimensions for direct identification and comprehensive characterization of specific cells.

Before you begin

Common operations in scRNA-seq and scATAC-seq analysis include clustering cells based on similarities of their global gene expression or accessibility, and a subsequent characterization of such clusters according to specifically measured expression/accessibility of individual marker genes or defined sets (signatures) consisting of multiple genes. In contrast, Seqtometry seamlessly combines cell grouping and specific characterization into a single step based on advanced scoring with multiple gene signatures. That is, gene signature enrichment values (scores) that are obtained using the advanced scoring algorithm included in this protocol can be plotted to generate biologically interpretable and informative plots. Therein, populations can be further scrutinized and progressively defined by their differing scores.

Installation and environment setup

Inline graphicTiming: 30 min

  • 1.

    Install the R programming language and a compatible integrated development environment (such as RStudio) or extensible text editor (such as VS Code).

Note: Refer to the installation instructions specific to your operating system (Windows, macOS, or Linux) and CPU architecture. It may be convenient to install R using a package manager. The latest version of R (≥ 4.3.0) is required, given its support for new language features (the native pipe operator and its extensions). If you are using Windows, you will also need to install RTools.

  • 2.

    Within an R session, install the following software, including Seqtometry and multiple additional packages required for the workflow.

Note: the Seqtometry package (≥ 0.2.0) only contains functions for scoring (and imputation). The graphical user interface described in the original publication is a separate package and will not be used here.

# For installing from Github and Bioconductor

install.packages(c("remotes", "BiocManager"))

# General purpose utilities for R

# ggplot2, dplyr, magrittr, purrr, stringr, readxl

install.packages("tidyverse")

# Utilities for scRNA-seq analysis

install.packages(c("Seurat", "harmony"))

BiocManager::install("batchelor")

# Utilities for scATAC-seq analysis

remotes::install_github("GreenleafLab/ArchR")

BiocManager::install(c(

 "BSgenome.Hsapiens.UCSC.hg38",

 "TxDb.Hsapiens.UCSC.hg38.knownGene",

 "org.Hs.eg.db"))

# For signature scoring (and imputation)

remotes::install_github("HawigerLab/Seqtometry")

Optional: The originally published version of Seqtometry (v0.1.0) used a Python environment with MAGIC for imputation. For interoperation with Python, install reticulate and then use it to setup a Python environment with MAGIC as described below. However, this step can be skipped when using the latest version of Seqtometry (v0.2.0), which provides an implementation of MAGIC entirely within the R ecosystem.

# For managing Python in R

install.packages("reticulate")

# Python environment creation and configuration

reticulate::install_miniconda()

reticulate::use_miniconda(reticulate::miniconda_path())

# For MAGIC imputation

reticulate::conda_install("pandas") # To avoid build issues

reticulate::conda_install("magic-impute", pip = TRUE, pip_ignore_installed = TRUE)

  • 3.

    Define the following utility functions for easy reuse.

# Utility function for performing quality control

qc <- function(mat) {

 # Indices of mitochondrial genes (starting with "MT-" for HUGO nomenclature)

 mito_idx <- grep("ˆMT-", rownames(mat))

 # Calculate QC metrics and determine which cells are outliers based on them

 res <- mat |>

  scuttle::perCellQCMetrics(subsets = list("Mito" = mito_idx)) |>

  scuttle::perCellQCFilters(sub.fields = "subsets_Mito_percent")

 # Remove low quality cells

 mat <- mat[, !res$discard]

 # Remove unexpressed genes

 mat <- mat[Matrix::rowSums(mat) > 0, ]

 mat

}

# Avoid having to prefix common plot elements with ggplot2::

library(ggplot2)

# A flow cytometry style theme for plots

theme_custom <- theme_bw() + theme(

 aspect.ratio = 1,

 panel.grid = element_blank(),

 axis.title = element_text(size = 32),

 axis.text = element_text(size = 32),

 legend.text = element_text(size = 32),

 legend.title = element_text(size = 32))

# Utility function for generating flow cytometry style plots

biaxial_plot <- function(d, x, y, ...) {

 lim <- c(-0.15, 1.15)

 brk <- seq(0, 1, 0.25)

 lbl <- c("0", "0.25", "0.5", "0.75", "1")

 ggplot(d, aes(x = .data[[x]], y = .data[[y]])) +

  geom_density_2d(...) +

  scale_x_continuous(limits = lim, breaks = brk, labels = lbl) +

  scale_y_continuous(limits = lim, breaks = brk, labels = lbl) +

  theme_custom + theme(axis.title = element_text(face = "italic"))

}

# Utility function to create a rectangular region

# lims is a 4 element named list containing xmin, xmax, ymin, ymax

# These 4 numbers define the bottom left (xmin, ymin) and top right

# (xmax, ymax) corners of the rectangle

rect_region <- function(lims) {

 f <- purrr::partial(annotate, geom = "rect",

  color = "blue", fill = NA, linewidth = 2)

 do.call(f, lims)

}

  • 4.

    Adjust the global settings for parallel execution.

# Register a parallel backend for future, so it is used across all

# eligible R functions (in this case, Seqtometry::score).

# Potential speedups can be acheived from parallel execution.

# A good default is shared memory parallelism (using forked processes)

# and alloting half of the available processor cores.

future::plan(future::multicore, workers = future::availableCores() / 2)

# Grant a max of 4 GiB to exported globals for future expressions

options(future.globals.maxSize = 4 ∗ 1024ˆ3)

  • 5.

    Set up path variables.

Note: ensure that read and write permissions exist in the directories that will be used.

# Any new folders will be created in the current working directory,

# where the user must have read and write permissions

# Folder that will contain all downloaded data

DATA_DIR <- getwd() |> file.path("Data")

dir.create(DATA_DIR)

# Folder that will contain all generated plots

PLOT_DIR <- getwd() |> file.path("Plots")

dir.create(PLOT_DIR)

Key resources table

REAGENT or RESOURCE SOURCE IDENTIFIER
Deposited data

pbmc3k 10× Genomics2 3k PBMCs from a Healthy Donor (https://www.10xgenomics.com/datasets/3-k-pbm-cs-from-a-healthy-donor-1-standard-1-1-0)
pbmc4k 10× Genomics3 4k PBMCs from a Healthy Donor (https://www.10xgenomics.com/datasets/4-k-pbm-cs-from-a-healthy-donor-2-standard-2-1-0)
pbmc5k 10× Genomics4 5k Peripheral Blood Mononuclear Cells (PBMCs) from a Healthy Donor (Next GEM v1.1) (https://www.10xgenomics.com/datasets/5-k-peripheral-blood-mononuclear-cells-pbm-cs-from-a-healthy-donor-next-gem-v-1-1-1-1-standard-2-0-0)
pbmc66k 10× Genomics5 Aggregate of 8 Chromium Connect channels and 8 manual channels (https://www.10xgenomics.com/datasets/aggregate-of-8-chromium-connect-channels-and-8-manual-channels-3-1-standard-3-1-0)
PBMC signatures Kousnetsov et al.1 https://doi.org/10.1016/j.cels.2023.12.005

Software and algorithms

R (v4.3.3) R Core Team6 https://www.r-project.org/
remotes (v2.4.2.1) Csárdi et al.7 https://cran.r-project.org/web/packages/remotes/index.html
Seqtometry (v0.2.0) Kousnetsov et al.1 https://github.com/HawigerLab/Seqtometry
reticulate (v1.34.0) Ushey et al.8 https://cran.r-project.org/web/packages/reticulate/index.html
Python (v3.10.13) Python Core Team9 https://www.python.org/
MAGIC (v3.0.0) van Dijk et al.10 https://pypi.org/project/magic-impute
tidyverse (v2.0.0) Wickham et al.11 https://cran.r-project.org/web/packages/tidyverse/index.html
ggplot2 (v3.4.4) Wickham12 https://cran.r-project.org/web/packages/ggplot2/index.html
future (v1.33.1) Bengtsson13 https://cran.r-project.org/web/packages/future/index.html
Seurat (v5.0.1) Hao et al.14 https://cran.r-project.org/web/packages/Seurat/index.html
harmony (v1.2.0) Korsunsky et al.15 https://cran.r-project.org/web/packages/harmony/index.html
uwot (v0.1.16) Melville16 https://cran.r-project.org/web/packages/uwot/index.html
BiocManager (v1.30.22) Morgan and Ramos17 https://cran.r-project.org/web/packages/BiocManager/index.html
batchelor (v1.18.1) Haghverdi et al.18 https://bioconductor.org/packages/release/bioc/html/batchelor.html
scuttle (v1.12.0) McCarthy et al.19 https://bioconductor.org/packages/release/bioc/html/scuttle.html
BiocSingular (v1.18.0) Lun20 https://bioconductor.org/packages/release/bioc/html/BiocSingular.html
ArchR (v1.0.2) Granja and Corces21 https://github.com/GreenleafLab/ArchR
BioRender BioRender https://www.biorender.com/

Other

Personal computer Apple, Inc. Macbook Air (M1, 2020)

Materials and equipment

All analyses performed here (and the associated timing estimates) were conducted on a personal computer with an Apple M1 processor with 8 cores and 16 GB of RAM (running on macOS Sonoma).

Step-by-step method details

The Seqtometry protocol consists of three main components: data import and preprocessing (read data into memory, subject it to quality control, and normalize it); imputation (fill in missing data due to technical limitations of single cell sequencing); scoring and plotting (generate tables of signatures scores, visualize them using flow-style plots, gate on them, and repeat as necessary).

Different types of dataset(s) require specific variations of instructions within these steps. For clarity, the four main variants of the Seqtometry protocol (for small scRNA-seq datasets, large scRNA-seq datasets, integrated scRNA-seq datasets, and scATAC-seq datasets) are addressed below as individual workflows. For the basic workflow see steps 1-3; for the large data workflow see steps 4-6; for the integration workflow see steps 7-9; for the scATAC-seq workflow see steps 10-12.

Single small to medium scRNA-seq dataset (on the order of 103 to 104 cells) workflow (steps 1-3 only)

Inline graphicTiming: <1 min (for step1)

Inline graphicTiming: <1 min (for step2)

Inline graphicTiming: <1 min (for step3)

  • 1.
    Data import and preprocessing.
    • a.
      Read in the pbmc3k dataset and signatures.
      # Gene expression matrix
      gex <- file.path(DATA_DIR, "pbmc3k", "filtered_gene_bc_matrices", "hg19") |>
       Seurat::Read10X()
      # Signatures
      sig <- file.path(DATA_DIR, "pbmc_signatures.rds") |> readRDS()
    • b.
      Perform quality control.
      Note: Filter out low quality cells according to total transcript count, total number of unique genes expressed, or proportion of counts attributable to mitochondrial genes: outliers for these criteria may indicate dead or dying cells, multiple cells stuck together, or no cells at all (just ambient RNA).
      # qc function defined in the utilities section above
      gex <- qc(gex)
    • c.
      Normalize the data (using a LogCP10K transform).
      Note: Control for differences in sequencing depth across cells (by scaling all library sizes to 10,000). Reduce the downstream impact of the mean-variance relationship for genes (by applying log1p as a variance stabilizing transform).
      gex <- Seurat::LogNormalize(gex)
  • 2.
    Imputation.
    • a.
      Perform imputation (MAGIC) on the normalized gene expression matrix.
      Inline graphicCRITICAL: scRNA-seq data suffers from dropouts and imputation is required to compensate in this workflow.
      Note: Use either the original Python version of MAGIC (via reticulate) or the R version (via Seqtometry - recommended).
      Python version
      # Use reticulate to call functions from Python modules in R
      # Determine diffusion time automatically (t = "auto")
      magic <- reticulate::import("magic")$MAGIC(t = "auto")
      # Peform imputation
      # 1. Python and R have different conventions for gene expression matrices:
      # a. Python: cells x genes; R: genes x cells.
      # b. Transpositions orient the matrix into the correct orientation.
      # 2. Imputation has two main phases (performed by fit_transform):
      # a. fit: computes diffusion operator
      # b. transform: applies diffusion operator to data
      imputed_gex <- gex |> Matrix::t() |> magic$fit_transform() |> t()
      # Restore gene names and cell barcodes
      # They are not preserved fter passing through the Python function
      dimnames(imputed_gex) <- dimnames(gex)
      R version
      # Do not scale genes prior to PCA to match default behavior of Python MAGIC
      imputed_gex <- Seqtometry::impute(gex, scale = FALSE)
  • 3.
    Scoring and plotting.
    • a.
      Score the imputed gene expression data for the T cell and B cell signatures.
      Inline graphicCRITICAL: the imputed gene expression data must be scored, not the regular (just normalized) gene expression data.
      Note: scores are values describing how strongly the genes in a signature are collectively expressed in a cell relative to other cells in the dataset.
      scores <- sig[c("T cell", "B cell")] |>
       Seqtometry::score(imputed_gex, signatures = _)
      Note: By default, scores are minmax transformed to have uniform limits for plotting. Also, the latest version of Seqtometry (0.2.0) automatically returns scores as a data frame (data.table), which can be used immediately for plotting in ggplot2. In contrast, the original version of Seqtometry (0.1.0) returned scores as a numeric matrix, which must be explicitly converted into a data frame by the user prior to use in ggplot2.
    • b.
      Create a biaxial plot from the signature scores (Figure 1).
      Note: Contour plots are usually the best type of plot for delineating populations (many cells with similar scores that are closely packed in regions of high density).
      # Limits defining a rectangular region for double negative cells
      dn_lims <- list(xmin = -0.075, xmax = 0.3, ymin = 0, ymax = 0.375)
      # Draw the biaxial plot with the rectangular region
      # biaxial_plot and rect_region functions defined in utilities section above
      biaxial_plot(scores, "T cell", "B cell", color = "black", h = c(0.2, 0.2)) +
      rect_region(dn_lims)
      # Save the above plot (in PDF format to preserve vector graphics)
      file.path(PLOT_DIR, "Basic_Workflow_B_and_T_Cell_Plot.pdf") |>
       ggsave(width = 8, height = 8)
      Note: T cell and B cell populations are visible as densities positive for their respective signatures. There is also a density of cells negative for both signatures.
    • c.
      Extract the cells negative for both signatures.
      Note: This step is analogous to a “gating” operation from flow cytometry.
      # Subset the imputed data, keeping only the cells in the blue region (whose
      # xy coordinates are less than the upper right corner of the rectangle)
      gated_imputed_gex <- with(scores,
       imputed_gex[, `T cell` < dn_lims$xmax & `B cell` < dn_lims$ymax])
    • d.
      Score the gated cells with the Monocyte and NK cell signatures.
      Note: It is possible to score all signatures at once for the entire dataset and then just show the results for the gated cells by subsetting the scores themselves. However, since scoring is relative in nature, rescoring using only the imputed gene expression data corresponding to the gated cells can enhance resolution. See Figure S1G of the original publication1 for demonstration of the benefits of rescoring.
      gated_scores <- sig[c("NK cell", "Monocyte")] |>
       Seqtometry::score(gated_imputed_gex, signatures = _)
    • e.
      Plot the scores for the gated cells (Figure 2).
      biaxial_plot(gated_scores, "Monocyte", "NK cell",
       color = "black", h = c(0.2, 0.2), binwidth = 2)
      file.path(PLOT_DIR, "Basic_Workflow_NK_and_Monocyte_Plot.pdf") |>
       ggsave(width = 8, height = 8)
      Note: Monocyte and NK cell populations are visible as densities positive for their respective signatures. Gating, rescoring, and plotting can be performed as many times as needed to digitally sort the desired cells.
      Note: In this case, the monocytes could be subdivided into additional subpopulations. This is not a technical problem, rather it reflects biological differences between different types of monocytes.

Figure 1.

Figure 1

Biaxial plot generated from step 3b

Figure 2.

Figure 2

Biaxial plot generated from step 3e

Large scRNA-seq dataset (on the order of 105 or more cells) workflow (steps 4-6 only)

Inline graphicTiming: <1 min (for step4)

Inline graphicTiming: <10 min (for step5)

Inline graphicTiming: <10 min (for step6)

  • 4.
    Data import and preprocessing.
    • a.
      Read in the pbmc66k dataset and the signatures.
      # Gene expression matrix
      gex <- file.path(DATA_DIR, "pbmc66k", "filtered_feature_bc_matrix") |>
       Seurat::Read10X()
      # Signatures
      sig <- file.path(DATA_DIR, "pbmc_signatures.rds") |> readRDS()
    • b.
      Perform quality control.
      gex <- qc(gex)
    • c.
      Normalize the data (using a LogCP10K transform).
      gex <- Seurat::LogNormalize(gex)
  • 5.

    Imputation

    The dataset is too large to generate a dense imputed matrix (a typical computer will run out of memory). Therefore, imputation is instead performed directly on principal components and the results are stored as a low rank approximation (LRA).
    • a.
      Perform MAGIC imputation in principle component space.
      Note: Use either the original Python version of MAGIC (via reticulate) or the R version (via Seqtometry).
      Python version
      # Calculate 100 principal components to match the default setting for MAGIC
      # Do scale genes to make the LRA more faithful to lowly expressed genes
      pca <- gex |>
       Matrix::t() |>
       BiocSingular::runPCA(100, scale = TRUE, BSPARAM = BiocSingular::IrlbaParam())
      # Perform imputation in principal component space
      magic <- reticulate::import("magic")$MAGIC(t = "auto", n_jobs = -1L)
      imputed_pcs <- magic$fit_transform(pca$x)
      # Do not immediately generate the entire imputed matrix as a dense matrix by
      # backrotating the imputed PCs (MAGIC does this in its approximate solver)
      # Instead, wrap the rotation and imputed PC matrices in a delayed matrix, so
      # individual rows or columns can be produced without exhausting memory
      imputed_gex <- BiocSingular::LowRankMatrix(pca$rotation, imputed_pcs)
      dimnames(imputed_gex) <- dimnames(gex)
      R version
      # exact_solver = FALSE: perform imputation in PC space instead of gene space
      # conserve_memory = TRUE: keep the imputed matrix as a delayed matrix
      imputed_gex <- Seqtometry::impute(gex,
       exact_solver = FALSE, conserve_memory = TRUE)
  • 6.
    Scoring and plotting
    • a.
      Score the imputed gene expression data for the T cell and B cell signatures.
      Note: The scoring function can handle imputed data in both regular form (as a dense matrix) and compressed form (as an LRA).
      # Matrix-vector products used to generate the columns of the LRA
      # implicitly use multiple processor cores (via BLAS/OMP multithreading).
      # Parallel execution with future does not currently work well with such
      # multithreading, so fallback to non-parallel execution.
      future::plan(future::sequential)
      scores <- sig[c("T cell", "B cell")] |>
       Seqtometry::score(imputed_gex, signatures = _)
    • b.
      Create a biaxial plot using the signature scores and associated metadata (Figure 3).
      dn_lims <- list(xmin = -0.05, xmax = 0.25, ymin = 0, ymax = 0.45)
      biaxial_plot(scores, "T cell", "B cell", color = "black", h = c(0.2, 0.2)) +
       rect_region(dn_lims)
      file.path(PLOT_DIR, "Large_Data_Workflow_B_and_T_Cell_Plot.pdf") |>
       ggsave(width = 8, height = 8)
    • c.
      Extract the signature double negative cells.
      gated_imputed_gex <- with(scores,
       imputed_gex[, `T cell` < dn_lims$xmax & `B cell` < dn_lims$ymax])
    • d.
      Rescore the gated cells with the Monocyte and NK cell signatures.
      gated_scores <- sig[c("NK cell", "Monocyte")] |>
       Seqtometry::score(gated_imputed_gex, signatures = _)
    • e.
      Plot the scores for the gated cells (Figure 4).
      biaxial_plot(gated_scores, "Monocyte", "NK cell",
       color = "black", h = c(0.2, 0.2))
      file.path(PLOT_DIR, "Large_Data_Workflow_NK_and_Monocyte_Plot.pdf") |>
       ggsave(width = 8, height = 8)
      Note: The large dataset makes rare populations now appear (seen in the small double negative densities).

Figure 3.

Figure 3

Biaxial plot generated from step 6b

Figure 4.

Figure 4

Biaxial plot generated from step 6e

Joint analysis of two (or more) scRNA-seq datasets after integration (steps 7-9 only)

Inline graphicTiming: <1 min (for step7)

Inline graphicTiming: <5 min (for step8)

Inline graphicTiming: <1 min (for step9)

  • 7.
    Data import and preprocessing.
    • a.
      Read in the pbmc3k and pbmc4k datasets as well as the signatures.
      # Gene expression matrices
      pbmc3k <- file.path(DATA_DIR, "pbmc3k", "filtered_gene_bc_matrices", "hg19") |>
       Seurat::Read10X()
      pbmc4k <- file.path(DATA_DIR, "pbmc4k", "filtered_gene_bc_matrices", "GRCh38") |>
       Seurat::Read10X()
      # Signatures
      sig <- file.path(DATA_DIR, "pbmc_signatures.rds") |> readRDS()
    • b.
      Perform quality control on each dataset individually.
      pbmc3k <- qc(pbmc3k)
      pbmc4k <- qc(pbmc4k)
    • c.
      Merge the datasets’ gene expression matrices into one matrix.
      Note: It is also necessary to keep track of which cell belongs to which dataset.
      # Merged gene expression matrix
      gex <- Seurat::RowMergeSparseMatrices(pbmc3k, pbmc4k)
      # Use only genes shared by both gene expression matrices,
      # since they were aligned using different reference genome versions
      shared_genes <- intersect(rownames(pbmc3k), rownames(pbmc4k))
      gex <- gex[shared_genes, ]
      # Dataset to which each cell belongs
      dataset <- factor(c(
       rep("pbmc3k", ncol(pbmc3k)),
       rep("pbmc4k", ncol(pbmc4k))))
    • d.
      Normalize the data (using a LogCP10K transform).
      gex <- Seurat::LogNormalize(gex)
  • 8.
    Imputation
    • a.
      Perform batch balanced principal components analysis, where each dataset is a batch.
      Note: Batch balancing is recommended to prevent the PCA from being dominated by the larger datasets.
      # Calculate 100 principal components to match the default setting for MAGIC
      pcs <- batchelor::multiBatchPCA(gex, batch = dataset, d = 100,
       preserve.single = TRUE) |> _[[1]]
    • b.
      Check whether a batch effect exists between the datasets (Figure 5).
      Note: Integration is only necessary if a batch effect (a pronounced technical difference) exists between the datasets. One way to check for the presence of a batch effect is to visually inspect a reduced dimensional embedding of the merged datasets, which can be achieved via Uniform Manifold Approximation and Projection (UMAP).
      # Reduces principal component matrix to 2 dimensions using UMAP algorithm
      # for subsequent plotting (for a global picture of datasets)
      pca_to_umap <- function(pcs) {
       nm <- paste0("UMAP", 1:2)
       pcs |>
       uwot::umap() |>
       as.data.frame() |>
       magrittr::set_colnames(nm)
      }
      # UMAP reduction along with a dataset column
      unharmonized_umap <- pca_to_umap(pcs) |>
       dplyr::mutate(Dataset = dataset)
      # Visualize UMAP reduction as scatter plot, coloring points by dataset
      col_scale <- scale_color_manual(values = c(
       "pbmc3k" = "black", "pbmc4k" = "red"))
      ggplot(unharmonized_umap, aes(x = UMAP1, y = UMAP2, color = Dataset)) +
       geom_point() + theme_custom + col_scale
      file.path(PLOT_DIR, "Integration_Workflow_UMAP_Plot_Before_Harmony.pdf") |>
       ggsave(width = 10, height = 8)
      Note: The two datasets, which should be almost identical since they each consist of PBMCs from healthy donors, are globally different (points do not overlap at all). Therefore, a batch effect is present (likely due to technical differences between the 10X V1 and V2 chemistries used by the pbmc3k and pbmc4k datasets, respectively).
    • c.
      Perform integration (via Harmony) to remove batch effects from the principal components.
      Inline graphicCRITICAL: The default parameters for an integration algorithm may not always yield a successful mixing of datasets. Adjustment of the parameters controlling the extent of correction performed during integration (theta and lambda, in the case of harmony::RunHarmony) may be necessary.
      harmonized_pcs <- harmony::RunHarmony(pcs, dataset)
    • d.
      Check that the integration was successful (Figure 6).
      # Redo UMAP using harmonized principal components
      harmonized_umap <- pca_to_umap(harmonized_pcs) |>
       dplyr::mutate(Dataset = dataset)
      # Visualize new UMAP reduction, coloring again by dataset
      ggplot(harmonized_umap, aes(x = UMAP1, y = UMAP2, color = Dataset)) +
       geom_point() + theme_custom + col_scale
      file.path(PLOT_DIR, "Integration_Workflow_UMAP_Plot_After_Harmony.pdf") |>
       ggsave(width = 10, height = 8)
      Note: The batch effect has been removed from the principal components (points from both datasets are now well mixed).
    • e.
      Perform imputation, guiding diffusion via the harmonized principal components.
      Note: The integration performed above only affected the principal components derived from the gene expression data. It is necessary to carry over these corrections to the gene expression values themselves during the imputation step.
      Note: Use either the original Python version of MAGIC (via reticulate) or the R version (via Seqtometry).

Figure 5.

Figure 5

UMAP plot generated from step 8b

Figure 6.

Figure 6

UMAP plot generated from step 8d

Python version.

# Compute diffusion operator using harmonized principal components

magic <- reticulate::import("magic")$MAGIC(t = "auto")$fit(harmonized_pcs)

# Apply diffusion operator to original gene expression data

imputed_gex <- gex |> Matrix::t() |> magic$transform() |> t()

dimnames(imputed_gex) <- dimnames(gex)

R version.

# Skip internal PCA procedure, using parameter PC matrix

imputed_gex <- Seqtometry::impute(gex, pca = harmonized_pcs)

Note: the large data approximation (described in the large data workflow) may be used if necessary.

  • 9.
    Scoring and plotting.
    • a.
      Score the imputed gene expression data for the T cell and B cell signatures.
      scores <- sig[c("T cell", "B cell")] |>
       Seqtometry::score(imputed_gex, signatures = _)
    • b.
      Create a biaxial plot using the signature scores and associated metadata (Figure 7).
      # Add dataset info for plotting
      data.table::set(scores, j = "Dataset", value = dataset)
      dn_lims <- list(xmin = -0.1, xmax = 0.275, ymin = -0.05, ymax = 0.45)
      # Visualize the scores, coloring contours by dataset
      biaxial_plot(scores, "T cell", "B cell",
       mapping = aes(color = Dataset), h = c(0.2, 0.2)) +
       rect_region(dn_lims) + col_scale
      file.path(PLOT_DIR, "Integration_Workflow_B_and_T_Cell_Plot.pdf") |>
       ggsave(width = 10, height = 8)
      Note: The imputation guided by the harmonized principal components removes the batch effect in gene expression space, such that the ensuing signature scores are also highly similar between the datasets (their contour lines are overlapping). However, biological differences are preserved: there are still distinct T cell, B cell, and double negative populations.
      Note: In this case, the T cell population has visible subpopulations. This is not a consequence of integration, but rather of biological differences between different types of T cells.
    • c.
      Extract the signature double negative cells.
      gate_idx <- with(scores,
       which(`T cell` < dn_lims$xmax & `B cell` < dn_lims$ymax))
      gated_imputed_gex <- imputed_gex[, gate_idx]
    • d.
      Rescore the gated cells with the Monocyte and NK cell signatures.
      gated_scores <- sig[c("NK cell", "Monocyte")] |>
       Seqtometry::score(gated_imputed_gex, signatures = _)
    • e.
      Plot the scores for the gated cells (Figure 8).
      # Add dataset info to the scores
      data.table::set(gated_scores, j = "Dataset", value = dataset[gate_idx])
      # Plot the scores, showing contours for each dataset
      biaxial_plot(gated_scores, "Monocyte", "NK cell",
       mapping = aes(color = Dataset), h = c(0.2, 0.2), binwidth = 1.5) +
       col_scale
      file.path(PLOT_DIR, "Integration_Workflow_NK_and_Monocyte_Plot.pdf") |>
       ggsave(width = 10, height = 8)
      Note: Monocyte and NK cell populations are visible as densities positive for their respective signatures. Meanwhile, there is no notable difference within each population for the two datasets. By virtue of having more cells, the integrated dataset also reveals a rare double negative population.

Figure 7.

Figure 7

Biaxial plot generated from step 9b

Figure 8.

Figure 8

Biaxial plot generated from step 9e

scATAC-seq dataset workflow (steps 10-12 only)

Inline graphicTiming: <30 min (for step10)

Inline graphicTiming: <10 min (for step11)

Inline graphicTiming: <1 min (for step12)

  • 10.
    Data import and preprocessing
    • a.
      Read in the pbmc5k dataset and the signatures.

Note: in the case of the ArchR package, quality control and normalization are performed when the data is read into an arrow file (they are not separate steps to be explicitly performed by the user with the exception of doublet filtering, which will be skipped for the purposes of this demonstration).

# Signatures

sig <- file.path(DATA_DIR, "pbmc_signatures.rds") |> readRDS()

# Change working directory (since intermediary files will be saved)

file.path(DATA_DIR, "pbmc5k") |> setwd()

# Prepare settings for ArchR; use annotations from GRCh38

library(rhdf5)

library(magrittr)

library(SummarizedExperiment)

ArchR::addArchRThreads(threads = 4)

gene_annot <- ArchR::createGeneAnnotation(

 TxDb = TxDb.Hsapiens.UCSC.hg38.knownGene::TxDb.Hsapiens.UCSC.hg38.knownGene,

 OrgDb = org.Hs.eg.db::org.Hs.eg.db)

genome_annot <- ArchR::createGenomeAnnotation("hg38")

# Read in data, perform quality control, and generate genomic tile matrix (for

# dimensionality reduction) as well as gene activity matrix (for scoring)

arrow_files <- ArchR::createArrowFiles(

 inputFiles = "atac_fragments.tsv.gz",

 sampleNames = "pbmc5k",

 geneAnnotation = gene_annot,

 genomeAnnotation = genome_annot)

# Create ArchR project to perform additional operations on arrow file

prj <- ArchR::ArchRProject(

 ArrowFiles = "pbmc5k.arrow",

 geneAnnotation = gene_annot,

 genomeAnnotation = genome_annot)

  • 11.

    Imputation.

    Imputation for scATAC-seq data is similar in principle to what is done for scRNA-seq data, but certain modifications are necessary. Herein, gene activity scores are used as a substitute for gene expression data, while latent semantic indexing (LSI) is used in place of PCA.
    • a.
      Perform dimensionality reduction via LSI.
      # Generates LSI reduction on the tile matrix
      prj <- ArchR::addIterativeLSI(prj)
      # Extract LSI embedding matrix
      lsi <- ArchR::getReducedDims(prj)
    • b.
      Extract the gene activity scores from the ArchR project.
      # Retrieve the gene activity (score) matrix as a SummarizedExperiment
      gac <- ArchR::getMatrixFromProject(prj)
      # Convert the SummarizedExperiment into an equivalent matrix
      genes <- SummarizedExperiment::rowData(gac)[["name"]]
      gac <- SummarizedExperiment::assay(gac, "GeneScoreMatrix")
      rownames(gac) <- genes
    • c.
      Perform MAGIC imputation on the gene activity scores, computing the diffusion operator using the LSI reduction.
      Note: Use either the original Python version of MAGIC (via reticulate) or the R version (via Seqtometry).

Python version.

# Compute diffusion operator using the LSI reduction

magic <- reticulate::import("magic")$MAGIC(t = "auto")$fit(lsi)

# Perform imputation on gene activity matrix

imputed_gac <- gac |> Matrix::t() |> magic$transform() |> t()

dimnames(imputed_gac) <- dimnames(gac)

R version.

# Use LSI reduction instead of performing PCA

# Perform imputation on gene activity matrix

imputed_gac <- Seqtometry::impute(gac, pca = lsi)

Note: the large data and integration variants of imputation (respectively described in the large data and integration workflows) may be employed if necessary.

  • 12.
    Scoring and plotting.
    • a.
      Score the imputed gene expression data for the T cell and B cell signatures.
      scores <- sig[c("T cell", "B cell")] |>
       Seqtometry::score(imputed_gac, signatures = _)
    • b.
      Create a biaxial plot from the signature scores (Figure 9).
      dn_lims <- list(xmin = -0.1, xmax = 0.45, ymin = -0.075, ymax = 0.275)
      biaxial_plot(scores, "T cell", "B cell", color = "black",
       h = c(0.2, 0.2), binwidth = 0.75) + rect_region(dn_lims)
      file.path(PLOT_DIR, "ATAC_Workflow_B_and_T_Cell_Plot.pdf") |>
       ggsave(width = 8, height = 8)
      Note: Even though transcriptomic signatures were used to generate scores for epigenomic data, they were sufficient to faithfully identify cells. Please see the original publication for details.1
    • c.
      Extract the signature double negative cells.
      gated_imputed_gac <- with(scores,
       imputed_gac[, `T cell` < dn_lims$xmax & `B cell` < dn_lims$ymax])
    • d.
      Score the gated cells with the Monocyte and NK cell signatures.
      gated_scores <- sig[c("NK cell", "Monocyte")] |>
       Seqtometry::score(gated_imputed_gac, signatures = _)
    • e.
      Plot the scores for the gated cells (Figure 10).
      biaxial_plot(gated_scores, "Monocyte", "NK cell",
       color = "black", h = c(0.2, 0.2))
      file.path(PLOT_DIR, "ATAC_Workflow_NK_and_Monocyte_Plot.pdf") |>
       ggsave(width = 8, height = 8)

Figure 9.

Figure 9

Biaxial plot generated from step 12b

Figure 10.

Figure 10

Biaxial plot generated from step 12e

Expected outcomes

Running the above pipelines will generate single cell signature score plots for scRNA-seq data or scATAC-seq data that progressively separate major cell populations (seen as high density regions) from peripheral blood. Since the axes are biologically interpretable, the identities of these populations are evident by construction. In each workflow, the first plot reveals B and T lymphocyte populations which are only positive for their respective signatures. Cells can be computationally gated (subsetted) for additional rounds of analysis (rescoring and plotting). The double negative population from the first plot was gated and scored for NK cell and monocyte signatures, thereby revealing these corresponding innate immune cell populations in the second plot. While the biologically relevant axes used here corresponded to cell type signatures, any biological characteristics or processes can be described by their corresponding signatures. Finally, variations on the imputation procedure enable Seqtometry workflows to be extended to multiple datasets involving integration or large datasets utilizing memory saving approximations.

Limitations

Seqtometry requires well tested signatures as well as the presence of signature positive and negative cells in the dataset to give meaningful results. Therefore, Seqtometry is not a suitable tool for discovery of cell types based solely on global transcriptomic profiles without any prior domain knowledge.

Troubleshooting

Problem 1

Parallel execution does not reduce execution time when scoring (step 3a).

Potential solution

  • Alloting all available processor cores can, in some situations, cause slowdowns since other programs also running on the computer may compete for CPU time. Therefore, reduce the number of workers (CPU cores) assigned in future::plan (see step 4 of the installation and environment setup section). If the problem still persists (likely due to unresolved conflicts between explicit parallel processing and implicit multithreading from linear algebra subroutines), fall back to the non-parallel backend (future::sequential).

  • Certain environments, such as Windows or RStudio, do not support shared memory parallelism via forked processes, which is used by future::multicore, and therefore fallback to non-parallel execution (future::sequential). In this case, either switch to another parallel backend, such as future::multisession, or, if possible, switch to a non-GUI environment in a Unix-like system that permits forked processes.

Problem 2

There is an error during scoring involving the total size of exported globals in a future expression exceeding the maximum allowed size (step 3a).

Potential solution

  • The default maximum memory size for exported globals in a future expression is 500 MiB. If this is too low, change the maximum allowed size (via the future.globals.maxSize option) to a larger number, such as 4 GiB (see utilities and settings section).

Problem 3

A plot is blank; that is, all the scores used to generate the plot are NA (step 3b).

Potential solution

  • The names of genes in the gene expression matrix and those in the signatures are disjoint. This is most likely due to differing nomenclature systems, such as gene identifiers (Ensembl or Entrez) for the gene expression matrix and gene symbols for the signatures. Another possibility is that the gene expression data could be from one species, such as Homo sapiens, while the signatures are from another species, such as Mus musculus. Adjust the gene names inside the signatures to match those of the gene expression data using an appropriate database for conversion and then perform scoring.

Problem 4

There is an error when importing MAGIC from reticulate (step 2a if using MAGIC from Python via reticulate).

Potential solution

  • When installing MAGIC, make sure it is in the same Python environment as the one used by reticulate. To avoid problems between various different Python versions that may exist on a single workstation, have reticulate create its own dedicated miniconda environment and install the magic-impute package there using reticulate (see installation and environment setup section, optional step involving reticulate).

  • To avoid issues with Python interoperation altogether, use the R implementation provided by the Seqtometry package (Seqtometry::impute).

Problem 5

Equivalent cell types from different datasets are still visibly separate after integration and scoring (step 9b).

Potential solution

  • Integration algorithms may not always successfully mix datasets using their default settings. Adjust any parameters that control how strongly the integration algorithm corrects for differences between datasets and rerun the integration step. In the Integration workflow described above, the Harmony algorithm was used for integration, wherein pertinent parameters include theta and lambda (see harmony::RunHarmony for the associated documentation).

Problem 6

A given cell type has noticeably different enrichments for its corresponding signature due to the presence of multiple cell subtypes (step 3e or step 9b).

Potential solution

  • Due to the compositional nature of scRNA-seq data, cell subtypes may not express certain genes as much as other related cell subtypes. That is, expression of subtype specific transcripts may reduce the fraction of the transcriptional budget alloted to signature genes.

  • Another possibility is that due to differing frequencies of certain cell subtypes, signatures generated by differential expression testing are biased towards the most abundant cell subtype.

  • Ultimately, a wider region may be needed to define an initial gate that captures all such subpopulations. Thereafter, subtype-specific signatures may be employed to fully identify all individual subpopulations after gating.

Resource availability

Lead contact

For further information and requests for resources and code availability, please contact Dr. Daniel Hawiger (daniel.hawiger@health.slu.edu).

Technical contact

Technical questions on executing this protocol can be directed to and will be answered by the technical contact, Robert Kousnetsov (robert.kousnetsov@health.slu.edu).

Materials availability

This study did not generate any new reagents.

Data and code availability

Acknowledgments

This work was supported in part by the National Multiple Sclerosis Society (RFA-2104-37543) to D.H. The graphical abstract was created using BioRender.

Author contributions

Conceptualization, development of the methodology, and formal analysis, R.K. and D.H.; software development, R.K.; visualization, R.K. and D.H.; writing – original draft, R.K.; writing – review and editing, R.K. and D.H.; supervision, D.H.; funding acquisition, D.H.

Declaration of interests

The authors declare no competing interests.

References

Associated Data

This section collects any data citations, data availability statements, or supplementary materials included in this article.

Data Availability Statement


Articles from STAR Protocols are provided here courtesy of Elsevier

RESOURCES