Flow cytometry and the more recently introduced CyTOF (cytometry by time-of-flight mass spectrometry or mass cytometry) are high-throughput technologies that measure protein abundance on the surface or within cells. In flow cytometry, antibodies are labeled with fluorescent dyes and fluorescence intensity is measured using lasers and photodetectors. CyTOF utilizes antibodies tagged with metal isotopes from the lanthanide series, which have favorable chemistry and do not occur in biological systems; abundances per cell are recorded with a time-of-flight mass spectrometer. In either case, fluorescence intensities (flow cytometry) or ion counts (mass cytometry) are assumed to be proportional to the expression level of the antibody-targeted antigens of interest.
Due to the differences in acquisition, further distinct characteristics should be noted. Conventional fluorophore-based flow cytometry is non-destructive and can be used to sort cells for further analysis. However, because of the spectral overlap between fluorophores, compensation of the data needs to be performed [1], which also limits the number of parameters that can be measured simultaneously. Thus, standard flow cytometry experiments measure 6-12 parameters with modern systems measuring up to 20 channels [2], while new developments (e.g., BD FACSymphony) promise to increase this capacity towards 50. Moreover, flow cytometry offers the highest throughput with tens of thousands of cells measured per second at relatively low operating costs per sample.
By using rare metal isotopes in CyTOF, cell autofluorescence can be avoided and spectral overlap is drastically reduced. However, the sensitivity of mass spectrometry results in the measurement of metal impurities and oxide formations, which need to be carefully considered in antibody panel design (e.g., through antibody concentrations and coupling of antibodies to neighboring metals). Leipold et al. recently commented that minimal spillover does not equal no spillover [3]. Nonetheless, CyTOF offers a high dimension of parameters measured per cell, with current panels using ~40 parameters and the promise of up to 100. Throughput of CyTOF is lower, at the rate of hundreds of cells per second, and cells are destroyed during ionization.
The ability of flow cytometry and mass cytometry to analyze individual cells at high-throughput scales has resulted in a wide range of biological and medical applications. For example, immunophenotyping assays are used to detect and quantify cell populations of interest, to uncover new cell populations and compare abundance of cell populations between different conditions, for example between patient groups [4]. Thus, it can be used as a biomarker discovery tool.
Various methodological approaches aim for biomarker discovery [5]. A common strategy, which we will refer to throughout this workflow as the “classic” approach, is to first identify cell populations of interest by manual gating or automated clustering [6, 7]. Second, using statistical tests, one can determine which of the cell subpopulations or protein markers are associated with a phenotype (e.g., clinical outcome) of interest. Typically, cell subpopulation abundance expressed as cluster cell counts or median marker expression would be used in the statistical model to relate to the sample-level phenotype.
Importantly, there are many alternatives to what we propose below, and several methods have emerged. For instance, Citrus [8] tackles the differential discovery problem by strong over-clustering of the cells, and by building a hierarchy of clusters from very specific to general ones. Using model selection and regularization techniques, clusters and markers that associate with the outcome are identified. A further machine learning approach, CellCnn [9], learns the representation of clusters that are associated with the considered phenotype by means of convolutional neural networks, which makes it particularly applicable to detecting discriminating rare cell populations. Another approach, cydar [10] performs differential abundance analysis on “hypersphere” counts, where hyperspheres are defined using all markers, and calculates differential tests using the the generalized linear modeling capabilities of edgeR [11].
However, there are tradeoffs to consider. Citrus performs feature selection but does not provide significance levels, such as p-values, for the strength of associations. Due to its computational requirements, Citrus cannot be run on entire mass cytometry datasets and one typically must analyze a subset of the data. The “filters” from CellCnn may identify one or more cell subsets that distinguish experimental groups, while these groups may not necessarily correspond to any of the canonical cell types, since they are learned with a data-driven approach. Since the hyperspheres from cydar are defined using all markers, interpretation of differential expression of specific markers (e.g., functional markers) within cell populations is difficult.
A noticeable distinction between the machine-learning approaches and our classical regression approach is the configuration of the model. Citrus and CellCnn model the patient response as a function of the measured HDCyto values, whereas the classical approach models the HDCyto data itself as the response, thus putting the distributional assumptions on the experimental HDCyto data. This carries the distinct advantage that covariates (e.g., age, gender, batch) can be included, together with finding associations of the phenotype to the predictors of interest (e.g., cell type abundance). Specifically, neither Citrus nor CellCnn are able to directly account for covariates, such as paired experiments or presence of batches. Another recent approach, mixed-effects association testing for single cells (MASC) uses the same “reverse” association approach that we illustrate below [12]. Recently, we have formalized and compared various regression approaches, resulting in the diffcyt package [13].
Within the classical approach, hybrid methods are certainly possible, where discovery of interesting cell populations is done with one algorithm, and quantifications or signal aggregations are modeled in standard regression frameworks. For instance, CellCnn provides p-values from a t-test or Mann-Whitney U-test conducted on the frequencies of previously detected cell populations. Some caution is warranted here, in terms of using data twice – so-called double dipping or circular analysis – and making claims about the statistical evidence of a change in abundance where initial analyses of the same data were used to discover subpopulations. This topic has been discussed with respect to clustering other types of single cell data and then inferring the markers of such populations [14]; however, it is less clear how much clustering affects cross-sample inferences.
Step by step, this workflow presents differential discovery analyses assembled from a suite of tools and methods that, in our view, lead to a higher level of flexibility and robust, statistically-supported and interpretable results. Cell population identification is conducted by means of unsupervised clustering using the FlowSOM and ConsensusClusterPlus packages, which together were among the best performing clustering approaches for high-dimensional cytometry data [15]. Notably, FlowSOM scales easily to millions of cells and thus no subsetting of the data is required.
To be able to analyze arbitrary experimental designs (e.g., batch effects, paired experiments, etc.), we show how to conduct differential analysis of cell population abundances using generalized linear mixed models (GLMM) and of marker intensities using linear models (LM) and linear mixed models (LMM). For both differential abundance and expression analysis, we use methods implemented in the diffcyt package [13]. Internally, model fitting is performed with packages lme4 and stats, and hypothesis testing with the multcomp package.
For visualization, we use new plotting functions from the CATALYST package that employ ggplot2 as their graphical engine. Notably, CATALYST delivers a suite of useful visual representations of HDCyto data characteristics, such as an MDS (multidimensional scaling) plot of aggregated signal for exploring sample similarities. The obtained cell populations are visualized using dimension reduction techniques (e.g., UMAP via the umap package) and heatmaps (via the ComplexHeatmap package [16]) to represent characteristics of the annotated cell populations and identified biomarkers. (Note that an alternative R implementation of the UMAP algorithm with additional functionality is also available in the uwot package.)
The workflow is intentionally not fully automatic. First, we strongly advocate for exploratory data analysis to get an understanding of data characteristics before formal statistical modeling. Second, the workflow involves an optional step where the user can manually merge and annotate clusters (see Cluster merging and annotation section) but in a way that is easily reproducible. The CyTOF data used here (see Data description section) is already preprocessed; i.e., the normalization and de-barcoding, as well as removal of doublets, debris and dead cells, were already performed; further details are available in the Data preprocessing section.
Notably, this workflow is equally applicable to flow or mass cytometry datasets, for which the preprocessing steps have already been performed. In addition, the workflow is modular and can be adapted as new algorithms or new knowledge about how to best use existing tools comes to light. Alternative clustering algorithms such as the popular PhenoGraph algorithm [17] (e.g., via the Rphenograph package), dimensionality reduction techniques, such as diffusion maps [18] via the destiny package [19], t-SNE via the Rtsne and SIMLR [20] via the SIMLR package could be inserted into the workflow.
Note: To cite this workflow, please refer to this F1000 article https://f1000research.com/articles/6-748/v3 .
To generate reproducible results, we set random seeds in several steps of the workflow. However, the default methods for random number generation in R were updated in R version 3.6.0 (released in April 2019; see R News for details). Therefore, for consistency with earlier versions of the workflow, we use the function RNGversion()
to use the random number generation methods from the previous version of R. Note that this step is not required when running a standard analysis on a new dataset; it is included here for reproducibility and backward compatibility only.
RNGversion("3.5.3")
We use a subset of CyTOF data originating from Bodenmiller et al. [21] that was also used in the Citrus paper [8]. In the original study, peripheral blood mononuclear cells (PBMCs) in unstimulated and after 11 different stimulation conditions were measured for 8 healthy donors. For each sample, expression of 10 cell surface markers and 14 signaling markers was recorded. We perform our analysis on samples from the reference and one stimulated condition where cells were crosslinked for 30 minutes with B cell receptor/Fc receptor known as BCR/FcR-XL, resulting in 16 samples in total (8 patients, unstimulated and stimulated for each).
The original data is available from the Cytobank report. The subset used here can be downloaded from the Citrus Cytobank repository (files with _BCR-XL.fcs
or _Reference.fcs
endings) or from the HDCytoData [22] package via Bodenmiller_BCR_XL_flowSet()
(see Data import section).
In both the Bodenmiller et al. and Citrus manuscripts, the 10 lineage markers were used to identify cell subpopulations. These were then investigated for differences between reference and stimulated cell subpopulations separately for each of the 14 functional markers. The same strategy is used in this workflow; 10 lineage markers are used for cell clustering and 14 functional markers are tested for differential expression between the reference and BCR/FcR-XL stimulation. Even though differential analysis of cell abundance was not in the scope of the Bodenmiller et al. experiment, we present it here to highlight the generality of the discovery.
Conventional flow cytometers and mass cytometers produce .fcs files that can be manually analyzed using programs such as FlowJo [TriStar] or Cytobank [23], or using R/Bioconductor packages, such as flowWorkspace [24] and openCyto [25]. During this initial analysis step, dead cells are removed, compensation is checked and with simple two dimensional scatter plots (e.g., marker intensity versus time), marker expression patterns are checked. Often, modern experiments are barcoded in order to remove analytical biases due to individual sample variation or acquisition time. Preprocessing steps including normalization using bead standards [26], de-barcoding [27] and compensation can be completed with the CATALYST package [28], which also provides a Shiny app for interactive analysis. Of course, preprocessing steps can occur using custom scripts within R or outside of R (e.g., Normalizer [26]).
We recommend as standard practice to keep an independent record of all samples collected, with additional information about the experimental condition, including sample or patient identifiers, processing batch and so on. That is, we recommend having a trail of metadata for each experiment. In our example, the metadata file, PBMC8_metadata.xlsx
, can be downloaded from the Robinson Lab server with the download.file()
function. For the workflow, the user should place it in the current working directory (getwd()
). Here, we load it into R with the read_excel()
function from the readxl package and save it into a variable called md
, but other file types and interfaces to read them in are also possible.
The data frame md
contains the following columns:
file_name
with names of the .fcs files corresponding to the reference (suffix “Reference”) and BCR/FcR-XL stimulation (suffix “BCR-XL”) samples,
sample_id
with shorter unique names for each sample containing information about conditions and patient IDs. These will be used to label samples throughout the entire workflow.
condition
describes whether samples originate from the reference (Ref
) or stimulated (BCRXL
) condition,
patient_id
defines the IDs of patients.
library(readxl)
url <- "http://imlspenticton.uzh.ch/robinson_lab/cytofWorkflow"
md <- "PBMC8_metadata.xlsx"
download.file(file.path(url, md), destfile = md, mode = "wb")
md <- read_excel(md)
head(data.frame(md))
## file_name sample_id condition patient_id
## 1 PBMC8_30min_patient1_BCR-XL.fcs BCRXL1 BCRXL Patient1
## 2 PBMC8_30min_patient1_Reference.fcs Ref1 Ref Patient1
## 3 PBMC8_30min_patient2_BCR-XL.fcs BCRXL2 BCRXL Patient2
## 4 PBMC8_30min_patient2_Reference.fcs Ref2 Ref Patient2
## 5 PBMC8_30min_patient3_BCR-XL.fcs BCRXL3 BCRXL Patient3
## 6 PBMC8_30min_patient3_Reference.fcs Ref3 Ref Patient3
In our example, the data from the .fcs files listed in the metadata can be loaded from the HDCytoData package [22].
library(HDCytoData)
fs <- Bodenmiller_BCR_XL_flowSet()
Alternatively, the files can be downloaded manually from the Citrus Cytobank repository and loaded into R as a flowSet
using read.flowSet()
from the flowCore package [29]. Importantly, read.flowSet()
and the underlying read.FCS()
functions, by default, may transform the marker intensities and remove cells with extreme positive values. This behavior can be controlled with arguments transformation
and truncate_max_range
, respectively.
In our example, information about the panel is also available in a file called PBMC8_panel.xlsx
, and can be downloaded from the Robinson Lab server and loaded into a variable called panel
. It contains columns for Isotope
and Metal
that define the atomic mass number and the symbol of the chemical element conjugated to the antibody, respectively, and Antigen
, which specifies the protein marker that was targeted; two additional columns specify whether a channel belongs to the lineage or functional type of marker.
The isotope, metal and antigen information that the instrument receives is also stored in the flowFrame
(container for one sample) or flowSet
(container for multiple samples) objects. One can type fs[[1]]
to see the first flowFrame
, which contains a table with columns name
and desc
. Their content can be retrieved with accessors pData(parameters(fs[[1]]))
. The variable name
corresponds to the column names in the flowSet
object, and can be viewed in R via colnames(fs)
.
It should be checked that elements from panel
can be matched to their corresponding entries in the flowSet
object. Specifically, the entries in panel$Antigen
must have an equivalent in the desc
columns of the flowFrame
objects.
In the following analysis, we will often use marker IDs as column names in the tables containing expression values. As a cautionary note, during object conversion from one type to another (e.g., in the creation of data.frame from a matrix), some characters (e.g., dashes) in the dimension names are replaced with dots, which may cause problems in matching. To avoid this problem, we will replace problematic characters (dashes with underscores; colons with dots) when organizing all data (measurement data, panel, and experimental metadata) into a SingleCellExperiment
(SCE) object (see below).
panel <- "PBMC8_panel_v3.xlsx"
download.file(file.path(url, panel), destfile = panel, mode = "wb")
panel <- read_excel(panel)
head(data.frame(panel))
## fcs_colname antigen marker_class
## 1 CD3(110:114)Dd CD3 type
## 2 CD45(In115)Dd CD45 type
## 3 pNFkB(Nd142)Dd pNFkB state
## 4 pp38(Nd144)Dd pp38 state
## 5 CD4(Nd145)Dd CD4 type
## 6 CD20(Sm147)Dd CD20 type
# spot check that all panel columns are in the flowSet object
all(panel$fcs_colname %in% colnames(fs))
## [1] TRUE
Usually, the raw marker intensities read by a cytometer have strongly skewed distributions with varying ranges of expression, thus making it difficult to distinguish between the negative and positive cell populations. It is common practice to transform CyTOF marker intensities using, for example, arcsinh (inverse hyperbolic sine) with cofactor 5 [8, 30] to make the distributions more symmetric and to map them to a comparable range of expression, which is important for clustering. A cofactor of 150 has .been promoted for flow cytometry, but users are free to implement alternative transformations, some of which are available from the transform()
function of the flowCore package. By default, the prepData()
SCE constructor (see next section) arcsinh transforms marker expressions with a cofactor of 5.
As the ranges of marker intensities can vary substantially, for visualization, we apply another transformation that scales the expression of all markers to values between 0 and 1 using low (e.g., 1%) and high (e.g., 99%) percentiles as the boundary. This additional transformation of the arcsinh-transformed data can sometimes give better visual representation of relative differences in marker expression between annotated cell populations. However, all computations (differential testing, hierarchical clustering etc.) are still performed on arcsinh-transformed not scaled expressions. Whether scaled expression values should be plotted is specified with argument scale = TRUE
or FALSE
in the respective visualizations (e.g., plotExprHeatmap()
and plotClusterHeatmap()
).
We will store all data used and returned throughout differential analysis in an object of the SingleCellExperiment (SCE) class. For this, CATALYST provides the wrapper prepData()
to construct a SCE object from the following inputs:
x
: a flowSet
containing the raw measurement data, or a character string that specifies the path to a set of .fcs files.panel
: a data.frame
containing, for each marker, i) its column name in the input raw data, ii) its targeted protein markers, and, optionally, iii) its class (type, state, or none).md
: a data.frame
with columns describing the experimental design.Argument features
specifies which columns (channels) to retain from the input data. By default, all measurement parameters will be kept (features = NULL
). Here, we only keep the channels listed in panel
.
It is important to carefully check whether variables are of the desired type (factor, numeric, character), since input methods may convert columns into different data types. This is taken care of by the prepData()
SCE constructor. For the statistical modeling, we want to make the condition variable a factor with the reference (Ref
) being the reference level. The order of factor levels can be defined with the levels
parameter of the factor
function or via relevel()
.
As a final note, prepData()
requires the filenames listed in the md$file_name
column to match those in the flowSet
.
# specify levels for conditions & sample IDs to assure desired ordering
md$condition <- factor(md$condition, levels = c("Ref", "BCRXL"))
md$sample_id <- factor(md$sample_id,
levels = md$sample_id[order(md$condition)])
# construct SingleCellExperiment
sce <- prepData(fs, panel, md, features = panel$fcs_colname)
We propose some quick checks to verify whether the data we analyze globally represents what we expect; for example, whether samples that are replicates of one condition are more similar and are distinct from samples from another condition. Another important check is to verify that marker expression distributions do not have any abnormalities such as having different ranges or distinct distributions for a subset of the samples. This could highlight problems with the sample collection or data acquisition, or batch effects that were unexpected. Depending on the situation, one can then consider removing problematic markers or samples from further analysis; in the case of batch effects, a covariate column could be added to the metadata table and used below in the statistical analyses.
The step below generates a plot with per-sample marker expression distributions, colored by condition (Figure 1). Here, we can already see distinguishing markers, such as pNFkB and CD20, between stimulated and unstimulated conditions.
p <- plotExprs(sce, color_by = "condition")
p$facet$params$ncol <- 6
p
Another spot check is the number of cells per sample (Figure 2). This plot can be used as a guide together with other readouts to identify samples where not enough cells were assayed. The number of cells measured in each sample is also stored in the experiment_info
slot of the SingleCellExperiment
’s metadata
, and can be accessed directly via n_cells()
.
n_cells(sce)
##
## Ref1 Ref2 Ref3 Ref4 Ref5 Ref6 Ref7 Ref8 BCRXL1 BCRXL2
## 2739 16725 9434 6906 11962 11038 15974 13670 2838 16675
## BCRXL3 BCRXL4 BCRXL5 BCRXL6 BCRXL7 BCRXL8
## 12252 8990 8543 8622 14770 11653
plotCounts(sce, group_by = "sample_id", color_by = "condition")