scp 1.2.0
scp
packageThe scp
package is used to process and analyse mass spectrometry
(MS)-based single cell proteomics (SCP) data. The functions rely on a
specific data structure that wraps
QFeatures
objects (Gatto (2020)) around
SingleCellExperiment
objects (Amezquita et al. (2019)). This data structure could be seen as
Matryoshka dolls were the SingleCellExperiment
objects are small
dolls contained in the bigger QFeatures
doll.
The SingleCellExperiment
class provides a dedicated framework for
single-cell data. The SingleCellExperiment
serves as an interface to
many cutting-edge methods for processing, visualizing and analysis
single-cell data. More information about the SingleCellExperiment
class and associated methods can be found in the
OSCA book.
The QFeatures
class is a data framework dedicated to manipulate and
process MS-based quantitative data. It preserves the relationship
between the different levels of information: peptide to spectrum match
(PSM) data, peptide data and protein data. The QFeatures
package
also provides an interface to many utility functions to streamline the
processing MS data. More information about MS data analysis tools can
be found in the
RforMassSpectrometry project.
(#fig:scp_framework)scp
relies on SingleCellExperiment
and QFeatures
objects.
Before running the vignette we need to load the scp
package.
library("scp")
We also load ggplot2
, magrittr
and dplyr
for convenient data
manipulation and plotting.
library("ggplot2")
library("magrittr")
library("dplyr")
This vignette will guide you through some common steps of mass
spectrometry-based single-cell proteomics (SCP) data analysis. SCP is
an emerging field and further research is required to develop a
principled analysis workflow. Therefore, we do not guarantee that the
steps presented here are the best steps for this type of data analysis.
This vignette performs the steps that were described in the SCoPE2
landmark paper (Specht et al. (2021)). We hope to convince the reader that,
although the workflow is probably not optimal, scp
has the full
potential to perform standardized and principled data analysis. All
functions presented here are comprehensively documented, highly modular,
can easily be extended with new algorithms. Suggestions, feature
requests or bug reports are warmly welcome. Feel free to open an issue
in the
GitHub repository.
The first step is to read in the PSM quantification table
generated by, for example, MaxQuant (Tyanova, Temu, and Cox (2016)). We created a
small example data by subsetting the MaxQuant evidence.txt
table
provided in the SCoPE2 landmark paper (Specht et al. (2021)). The
mqScpData
table is a typical example of what you would get after
reading in a CSV file using read.csv
or read.table
. See
?mqScpData
for more information about the table content.
data("mqScpData")
dim(mqScpData)
#> [1] 1361 149
mqScpData[1:10, 1:5]
#> uid
#> 1 _(Acetyl (Protein N-term))ATNFLAHEK_ 2 0.00052636 190321S_LCA10_X_FP97AG
#> 2 _(Acetyl (Protein N-term))ATNFLAHEK_ 2 0.00058789 190222S_LCA9_X_FP94BM
#> 3 _(Acetyl (Protein N-term))SHTILLVQPTK_ 2 4.0315e-24 190914S_LCB3_X_16plex_Set_21
#> 4 _(Acetyl (Protein N-term))SHTILLVQPTK_ 2 4.7622e-06 190222S_LCA9_X_FP94BM
#> 5 _(Acetyl (Protein N-term))SHTILLVQPTK_ 2 6.8709e-09 190321S_LCA10_X_FP97AG
#> 6 _(Acetyl (Protein N-term))SLVIPEK_ 2 0.053705 190222S_LCA9_X_FP94BM
#> 7 _(Acetyl (Protein N-term))SLVIPEK_ 2 0.0595 190914S_LCB3_X_16plex_Set_21
#> 8 _(Acetyl (Protein N-term))VDREQLVQK_ 2 0.004872 190321S_LCA10_X_FP97AG
#> 9 _AAGLALK_ 2 0.020042 190222S_LCA9_X_FP94BM
#> 10 _AAGLALK_ 2 0.038752 190321S_LCA10_X_FP97AG
#> Sequence Length Modifications
#> 1 ATNFLAHEK 9 Acetyl (Protein N-term)
#> 2 ATNFLAHEK 9 Acetyl (Protein N-term)
#> 3 SHTILLVQPTK 11 Acetyl (Protein N-term)
#> 4 SHTILLVQPTK 11 Acetyl (Protein N-term)
#> 5 SHTILLVQPTK 11 Acetyl (Protein N-term)
#> 6 SLVIPEK 7 Acetyl (Protein N-term)
#> 7 SLVIPEK 7 Acetyl (Protein N-term)
#> 8 VDREQLVQK 9 Acetyl (Protein N-term)
#> 9 AAGLALK 7 Unmodified
#> 10 AAGLALK 7 Unmodified
#> Modified.sequence
#> 1 _(Acetyl (Protein N-term))ATNFLAHEK_
#> 2 _(Acetyl (Protein N-term))ATNFLAHEK_
#> 3 _(Acetyl (Protein N-term))SHTILLVQPTK_
#> 4 _(Acetyl (Protein N-term))SHTILLVQPTK_
#> 5 _(Acetyl (Protein N-term))SHTILLVQPTK_
#> 6 _(Acetyl (Protein N-term))SLVIPEK_
#> 7 _(Acetyl (Protein N-term))SLVIPEK_
#> 8 _(Acetyl (Protein N-term))VDREQLVQK_
#> 9 _AAGLALK_
#> 10 _AAGLALK_
In order to convert this tabular data to a scp
-compatible
QFeatures
object, we need to provide a metadata table where rows
contain sample information and columns must contain at least:
Any additional information about the samples will be stored in the
colData
.
We provide an example of such a data frame. It was formatted from the
annotation table provided in the SCoPE2 article. See
?sampleAnnotation
for more information about the table content.
data("sampleAnnotation")
sampleAnnotation
#> Raw.file Channel SampleType lcbatch
#> 1 190222S_LCA9_X_FP94BM Reporter.intensity.1 Carrier LCA9
#> 2 190222S_LCA9_X_FP94BM Reporter.intensity.2 Reference LCA9
#> 3 190222S_LCA9_X_FP94BM Reporter.intensity.3 Unused LCA9
#> 4 190222S_LCA9_X_FP94BM Reporter.intensity.4 Monocyte LCA9
#> 5 190222S_LCA9_X_FP94BM Reporter.intensity.5 Blank LCA9
#> 6 190222S_LCA9_X_FP94BM Reporter.intensity.6 Monocyte LCA9
#> 7 190222S_LCA9_X_FP94BM Reporter.intensity.7 Macrophage LCA9
#> 8 190222S_LCA9_X_FP94BM Reporter.intensity.8 Macrophage LCA9
#> 9 190222S_LCA9_X_FP94BM Reporter.intensity.9 Macrophage LCA9
#> 10 190222S_LCA9_X_FP94BM Reporter.intensity.10 Macrophage LCA9
#> 11 190222S_LCA9_X_FP94BM Reporter.intensity.11 Macrophage LCA9
#> 12 190222S_LCA9_X_FP94BM Reporter.intensity.12 Unused LCA9
#> 13 190222S_LCA9_X_FP94BM Reporter.intensity.13 Unused LCA9
#> 14 190222S_LCA9_X_FP94BM Reporter.intensity.14 Unused LCA9
#> 15 190222S_LCA9_X_FP94BM Reporter.intensity.15 Unused LCA9
#> 16 190222S_LCA9_X_FP94BM Reporter.intensity.16 Unused LCA9
#> 17 190321S_LCA10_X_FP97AG Reporter.intensity.1 Carrier LCA10
#> 18 190321S_LCA10_X_FP97AG Reporter.intensity.2 Reference LCA10
#> 19 190321S_LCA10_X_FP97AG Reporter.intensity.3 Unused LCA10
#> 20 190321S_LCA10_X_FP97AG Reporter.intensity.4 Macrophage LCA10
#> 21 190321S_LCA10_X_FP97AG Reporter.intensity.5 Monocyte LCA10
#> 22 190321S_LCA10_X_FP97AG Reporter.intensity.6 Macrophage LCA10
#> 23 190321S_LCA10_X_FP97AG Reporter.intensity.7 Macrophage LCA10
#> 24 190321S_LCA10_X_FP97AG Reporter.intensity.8 Macrophage LCA10
#> 25 190321S_LCA10_X_FP97AG Reporter.intensity.9 Macrophage LCA10
#> 26 190321S_LCA10_X_FP97AG Reporter.intensity.10 Macrophage LCA10
#> 27 190321S_LCA10_X_FP97AG Reporter.intensity.11 Macrophage LCA10
#> 28 190321S_LCA10_X_FP97AG Reporter.intensity.12 Unused LCA10
#> 29 190321S_LCA10_X_FP97AG Reporter.intensity.13 Unused LCA10
#> 30 190321S_LCA10_X_FP97AG Reporter.intensity.14 Unused LCA10
#> 31 190321S_LCA10_X_FP97AG Reporter.intensity.15 Unused LCA10
#> 32 190321S_LCA10_X_FP97AG Reporter.intensity.16 Unused LCA10
#> 33 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.1 Carrier LCB3
#> 34 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.2 Reference LCB3
#> 35 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.3 Unused LCB3
#> 36 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.4 Unused LCB3
#> 37 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.5 Macrophage LCB3
#> 38 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.6 Macrophage LCB3
#> 39 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.7 Blank LCB3
#> 40 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.8 Monocyte LCB3
#> 41 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.9 Macrophage LCB3
#> 42 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.10 Monocyte LCB3
#> 43 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.11 Blank LCB3
#> 44 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.12 Macrophage LCB3
#> 45 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.13 Macrophage LCB3
#> 46 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.14 Macrophage LCB3
#> 47 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.15 Macrophage LCB3
#> 48 190914S_LCB3_X_16plex_Set_21 Reporter.intensity.16 Macrophage LCB3
#> 49 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.1 Blank LCA10
#> 50 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.2 Blank LCA10
#> 51 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.3 Blank LCA10
#> 52 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.4 Blank LCA10
#> 53 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.5 Blank LCA10
#> 54 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.6 Blank LCA10
#> 55 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.7 Blank LCA10
#> 56 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.8 Blank LCA10
#> 57 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.9 Blank LCA10
#> 58 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.10 Blank LCA10
#> 59 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.11 Blank LCA10
#> 60 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.12 Blank LCA10
#> 61 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.13 Blank LCA10
#> 62 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.14 Blank LCA10
#> 63 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.15 Blank LCA10
#> 64 190321S_LCA10_X_FP97_blank_01 Reporter.intensity.16 Blank LCA10
#> sortday digest
#> 1 s8 N
#> 2 s8 N
#> 3 s8 N
#> 4 s8 N
#> 5 s8 N
#> 6 s8 N
#> 7 s8 N
#> 8 s8 N
#> 9 s8 N
#> 10 s8 N
#> 11 s8 N
#> 12 s8 N
#> 13 s8 N
#> 14 s8 N
#> 15 s8 N
#> 16 s8 N
#> 17 s8 Q
#> 18 s8 Q
#> 19 s8 Q
#> 20 s8 Q
#> 21 s8 Q
#> 22 s8 Q
#> 23 s8 Q
#> 24 s8 Q
#> 25 s8 Q
#> 26 s8 Q
#> 27 s8 Q
#> 28 s8 Q
#> 29 s8 Q
#> 30 s8 Q
#> 31 s8 Q
#> 32 s8 Q
#> 33 s9 R
#> 34 s9 R
#> 35 s9 R
#> 36 s9 R
#> 37 s9 R
#> 38 s9 R
#> 39 s9 R
#> 40 s9 R
#> 41 s9 R
#> 42 s9 R
#> 43 s9 R
#> 44 s9 R
#> 45 s9 R
#> 46 s9 R
#> 47 s9 R
#> 48 s9 R
#> 49 s8 <NA>
#> 50 s8 <NA>
#> 51 s8 <NA>
#> 52 s8 <NA>
#> 53 s8 <NA>
#> 54 s8 <NA>
#> 55 s8 <NA>
#> 56 s8 <NA>
#> 57 s8 <NA>
#> 58 s8 <NA>
#> 59 s8 <NA>
#> 60 s8 <NA>
#> 61 s8 <NA>
#> 62 s8 <NA>
#> 63 s8 <NA>
#> 64 s8 <NA>
The two tables are supplied to the readSCP
function.
scp <- readSCP(featureData = mqScpData,
colData = sampleAnnotation,
channelCol = "Channel",
batchCol = "Raw.file")
#> Loading data as a 'SingleCellExperiment' object
#> Splitting data based on 'Raw.file'
#> Formatting sample metadata (colData)
#> Formatting data as a 'QFeatures' object
As indicated by the output on the console, readSCP
proceeds as follows:
If featureData
is the path to a CSV file, it reads the file using
read.csv
. The table is converted to a SingleCellExperiment
object. readSCP
needs to know in which field(s) the quantitative
data is stored. Those field name(s) is/are provided by the
channelCol
field in the metaData
table. So in this example, the
column names holding the quantitative data in mqScpData
are
stored in the column named Channel
in sampleAnnotation
.
The SingleCellExperiment
object is then split according to
batch. The split is performed depending on the batchCol
field in
featureData
, in this case the data will be split according to the
Raw.file
column in mqScpData
. Raw.file
contains the names of the
acquisition runs that was used by MaxQuant to retrieve the raw data
files.
The sample metadata is generated from the supplied colData
. Note
that in order for readSCP
to correctly match the feature data
with the metadata, colData
must contain the same batchCol
field
with batch names.
Finally, the split feature data and the sample metadata are stored
in a single QFeatures
object.
Here is a compact overview of the data:
scp
#> An instance of class QFeatures containing 4 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 395 rows and 16 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 487 rows and 16 columns
#> [3] 190321S_LCA10_X_FP97_blank_01: SingleCellExperiment with 109 rows and 16 columns
#> [4] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 370 rows and 16 columns
We can see that the scp
object we created is a QFeatures
object
containing 4 assays. Each assay has an associated name, this is the
batch name that was used for splitting. We can also see that each
assay is a SingleCellExperiment
object. The rows represent the
peptide to spectrum matches (PSMs), the number vary depending on the
batch. Finally, all three assays contains 16 columns that correspond
to the 16 TMT channels recorded during the 4 MS runs.
The SCoPE2 data we provide as an example contains runs that were
acquired with two TMT protocols. The 3 first assays were acquired
using the TMT-11 protocol and the last assay was acquired
using a TMT-16 protocol. When exporting the table, the authors
combined the data in a single table, were missing channels in the
TMT-11 data are filled with NA
. This is essential when working in
table format, but since scp
keeps the runs separated we can allow for
different number of channels per run. readSCP
can automatically
detect and remove columns that contain only NA
s, by supplying the
argument removeEmptyCols = TRUE
.
scp <- readSCP(featureData = mqScpData,
colData = sampleAnnotation,
channelCol = "Channel",
batchCol = "Raw.file",
removeEmptyCols = TRUE)
#> Loading data as a 'SingleCellExperiment' object
#> Splitting data based on 'Raw.file'
#> Formatting sample metadata (colData)
#> Formatting data as a 'QFeatures' object
scp
#> An instance of class QFeatures containing 4 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 395 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 487 rows and 11 columns
#> [3] 190321S_LCA10_X_FP97_blank_01: SingleCellExperiment with 109 rows and 11 columns
#> [4] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 370 rows and 16 columns
See here that the 3 first assays contain 11 columns that correspond to the TMT-11 labels and the last assay contains 16 columns that correspond to the TMT-16 labels.
All single-cell data contain many zeros. The zeros can be biological
zeros or technical zeros and differentiating between the two types is
not a trivial task. To avoid artefacts in downstream steps, we replace
the zeros by the missing value NA
. The zeroIsNA
function takes the
QFeatures
object and the name(s) or index/indices of the assay(s) to
clean and automatically replaces any zero in the selected quantitative
data by NA
.
scp <- zeroIsNA(scp, i = 1:4)
A common steps in SCP is to filter out low-confidence PSMs. Each PSM
assay contains feature meta-information that are stored in the
rowData
of the assays. The QFeatures
package allows to quickly
filter the rows of an assay by using these information. The available
variables in the rowData
are listed below for each assay.
rowDataNames(scp)
#> CharacterList of length 4
#> [["190222S_LCA9_X_FP94BM"]] uid Sequence Length ... residual participated
#> [["190321S_LCA10_X_FP97AG"]] uid Sequence Length ... residual participated
#> [["190321S_LCA10_X_FP97_blank_01"]] uid Sequence ... residual participated
#> [["190914S_LCB3_X_16plex_Set_21"]] uid Sequence ... residual participated
Below are some examples of criteria that are used to identify low-confidence. The information is readily available since this was computed by MaxQuant:
We can perform this filtering using the filterFeatures
function from
QFeatures
. filterFeatures
automatically accesses the feature
metadata and selects the rows that meet the provided condition(s). For
instance, Reverse != "+"
keeps the rows for which the Reverse
variable in the rowData
is not "+"
(i.e. the PSM is not matched
to the decoy database).
scp <- filterFeatures(scp,
~ Reverse != "+" &
Potential.contaminant != "+" &
!is.na(PIF) & PIF > 0.8)
To avoid proceeding with failed runs, another interesting filter is to
remove assays with too few features. If a batch contains less than,
for example, 150 features we can then suspect something wrong happened
in that batch and it should be removed. Using dims
, we can query the
dimensions (hence the number of features and the number of samples) of
all assays contained in the dataset.
dims(scp)
#> 190222S_LCA9_X_FP94BM 190321S_LCA10_X_FP97AG 190321S_LCA10_X_FP97_blank_01
#> [1,] 283 318 60
#> [2,] 11 11 11
#> 190914S_LCB3_X_16plex_Set_21
#> [1,] 200
#> [2,] 16
Actually, a QFeatures
object can be seen as a three-order array:
\(features \times samples \times assay\). Hence, QFeatures
supports
three-order subsetting x[rows, columns, assays]
. We first select the
assays that have sufficient PSMs (the number of rows is greater than
150), and then subset the scp
object for the assays that meet the
criterion.
keepAssay <- dims(scp)[1, ] > 150
scp <- scp[, , keepAssay]
#> Warning: 'experiments' dropped; see 'metadata'
#> harmonizing input:
#> removing 11 sampleMap rows not in names(experiments)
#> removing 11 colData rownames not in sampleMap 'primary'
scp
#> An instance of class QFeatures containing 3 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 283 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 318 rows and 11 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 200 rows and 16 columns
Notice the 190321S_LCA10_X_FP97_blank_01
sample was removed because
it did not contain sufficient features, as expected from a blank
run. This could also have been the case for failed runs.
Another type of filtering is specific to SCP. In the SCoPE2 analysis, the authors suggest a filters based on the sample to carrier ratio (SCR), that is the reporter ion intensity of a single-cell sample divided by the reporter ion intensity of the carrier channel (200 cells) from the same batch. It is expected that the carrier intensities are much higher than the single-cell intensities.
The SCR can be computed using the computeSCR
function from
scp
. The function must be told which channels are the samples that
must be divided and which channel contains the carrier. This
information is provided in the sample metadata and is accessed using
the colData
, under the SampleType
field.
table(colData(scp)[, "SampleType"])
#>
#> Blank Carrier Macrophage Monocyte Reference Unused
#> 3 3 20 5 3 4
In this dataset, SampleType
gives the type of sample that is present
in each TMT channel. The SCoPE2 protocole includes 5 types of samples:
Carrier
) contain 200 cell equivalents and
are meant to boost the peptide identification rate.Reference
) contain 5 cell equivalents
and are used to partially correct for between-run variation.Unused
) are channels that are left empty due
to isotopic cross-contamination by the carrier channel.Blank
) contain samples that do not contain any
cell but are processed as single-cell samples.Macrophage
) or monocyte
(Monocyte
).The computeSCR
function expects the following input:
QFeatures
datasetThe function creates a new field in the rowData
of the assays. We
store the computed SCR in the rowData
and call it MeanSCR
.
scp <- computeSCR(scp,
i = 1:3,
colDataCol = "SampleType",
carrierPattern = "Carrier",
samplePattern = "Macrophage|Monocyte",
rowDataName = "MeanSCR")
Before applying the filter, we plot the distribution of the average
SCR. We can extract the MeanSCR
variable from the rowData
of
several assays using the rowDataToDF
. It takes the rowData
field(s) of interest and returns a DataFrame
table.
scp %>%
rowDataToDF(i = 1:3,
vars = "MeanSCR") %>%
data.frame %>%
ggplot(aes(x = MeanSCR)) +
geom_histogram() +
geom_vline(xintercept = c(1/200, 0.1),
lty = c(2, 1)) +
scale_x_log10()
The expected ratio between single cells and the carrier is 1/200
(dashed line). We can see that the distribution mode is slightly
shifted towards higher ratios with a mode around 0.01. However, there
are a few PSMs that stand out of the distribution and have a much
higher signal than expected, indicating something wrong happened
during the quantification of those PSMs. We therefore filter out PSMs
with an average SCR higher than 0.1 (solide line). This is again easily
performed using the filterFeatures
functions.
scp <- filterFeatures(scp,
~ !is.na(MeanSCR) &
MeanSCR < 0.1)
Finally, we might also want to control for false discovery rate (FDR).
MaxQuant already computes posterior error probabilities (PEP), but
filtering on PEPs is too conservative (Käll et al. (2008)) so we provide the
pep2qvalue
function to convert PEPs to q-values that are directly
related to FDR. We here compute the q-values from the PEP (dart_PEP
)
across all 3 assays. dart_PEP
contains the PEP values that have been
updated using the DART-ID algorithm (Chen, Franks, and Slavov (2019)). The function will
store the results in the rowData
, we here asked to name the new
column qvalue_PSMs
.
scp <- pep2qvalue(scp,
i = 1:3,
PEP = "dart_PEP",
rowDataName = "qvalue_PSMs")
We also allow to compute q-values at peptide or protein level rather
than PSM. In this case, you need to supply the groupBy
argument.
Suppose we want to compute the q-values at protein level, we can fetch
the protein information stored under protein
in the rowData
. This
time, we store the q-values in a new field called qvalue_proteins
.
scp <- pep2qvalue(scp,
i = 1:3,
PEP = "dart_PEP",
groupBy = "protein",
rowDataName = "qvalue_proteins")
We can now filter the PSM to control, let’s say, the protein FDR at
1%. This can be performed using filterFeatures
because the q-values
were stored in the rowData
.
scp <- filterFeatures(scp,
~ qvalue_proteins < 0.01)
In order to partialy correct for between-run variation, SCoPE2
suggests computing relative reporter ion intensities. This means that
intensities measured for single-cells are divided by the reference
channel containing 5-cell equivalents. We use the divideByReference
function that divides channels of interest by the reference
channel. Similarly to computeSCR
, we can point to the samples and
the reference columns in each assay using the annotation contained in
the colData
.
We here divide all columns (using the regular expression wildcard .
)
by the reference channel (Reference
).
scp <- divideByReference(scp,
i = 1:3,
colDataCol = "SampleType",
samplePattern = ".",
refPattern = "Reference")
Now that the PSM assays are processed, we can aggregate them to
peptides. This is performed using the aggregateFeaturesOverAssays
function. For each assay, the function aggregates several PSMs into a
unique peptide. This is best illustrated by the figure below.
(#fig:features_aggregation)Conceptual illustration of feature aggregation.
Remember there currently are three assays containing the PSM data.
scp
#> An instance of class QFeatures containing 3 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 186 rows and 16 columns
The PSMs are aggregated over the fcol
feature variable, here
peptides. We also need to supply an aggregating function that will
tell how to combine the quantitative data of the PSMs to aggregate. We
here aggregate the PSM data using the median value per sample thanks
to the matrixStats:colMedians
function. Other functions can be used
and we refer to ?aggregateFeatures
for more information about
available aggregation functions. The aggregateFeaturesOverAssays
function will create a new assay for each aggregated assay. We name
the aggregated assays using the original names and appending
peptides_
at the start.
scp <- aggregateFeaturesOverAssays(scp,
i = 1:3,
fcol = "peptide",
name = paste0("peptides_", names(scp)),
fun = matrixStats::colMedians, na.rm = TRUE)
Notice that 3 new assays were created in the scp
object. Those new
assays contain the aggregated features while the three first assays
are unchanged. This allows to keep track of the data processing.
scp
#> An instance of class QFeatures containing 6 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 186 rows and 16 columns
#> [4] peptides_190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [5] peptides_190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [6] peptides_190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 185 rows and 16 columns
Under the hood, the QFeatures
architecture preserves the
relationship between the aggregated assays. See ?AssayLinks
for more
information on relationships between assays.
Up to now, we kept the data belonging to each MS run in separate
assays. We now combine all batches into a single assay. This is done
using the joinAssays
function from the QFeatures
package. Note
that we now use the aggregated assays, so assay 4 to 6.
scp <- joinAssays(scp,
i = 4:6,
name = "peptides")
In this case, one new assay is created in the scp
object that
combines the data from assay 4 to 6. The samples are always distinct
so the number of column in the new assay (here \(48\)) will always
equals the sum of the columns in the assays to join (here \(16 + 16 + 16\)). The feature in the joined assay might contain less features than
the sum of the rows of the assays to join since common features
between assays are joined in a single row.
scp
#> An instance of class QFeatures containing 7 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 186 rows and 16 columns
#> [4] peptides_190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [5] peptides_190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [6] peptides_190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 185 rows and 16 columns
#> [7] peptides: SingleCellExperiment with 432 rows and 38 columns
Another common step in single-cell data analysis pipelines is to remove low-quality cells. After subsetting for the samples of interest, we will use 2 metrics: the median relative intensities per cell and the median coefficient of variation (CV) per cell.
We can subset the cells of interest, that is the blank samples
(sc_0
), the macrophages (sc_m0
) and the monocytes (sc_u
). This
can easily be done by taking advantage of the colData
and the
subsetting operators. Recall that QFeatures
objects support
three-order subsetting, x[rows, columns, assays]
, where columns are
the samples of interest.
scp
#> An instance of class QFeatures containing 7 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 186 rows and 16 columns
#> [4] peptides_190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 11 columns
#> [5] peptides_190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 11 columns
#> [6] peptides_190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 185 rows and 16 columns
#> [7] peptides: SingleCellExperiment with 432 rows and 38 columns
scp <- scp[, scp$SampleType %in% c("Blank", "Macrophage", "Monocyte"), ]
The subsetting removes unwanted samples from all assays. The filtered data set contains the same number of assays with the same number of features, but the number of columns (hence sampled) decreased.
scp
#> An instance of class QFeatures containing 7 assays:
#> [1] 190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 8 columns
#> [2] 190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 8 columns
#> [3] 190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 186 rows and 12 columns
#> [4] peptides_190222S_LCA9_X_FP94BM: SingleCellExperiment with 239 rows and 8 columns
#> [5] peptides_190321S_LCA10_X_FP97AG: SingleCellExperiment with 277 rows and 8 columns
#> [6] peptides_190914S_LCB3_X_16plex_Set_21: SingleCellExperiment with 185 rows and 12 columns
#> [7] peptides: SingleCellExperiment with 432 rows and 28 columns
We compute the median relative reporter ion intensity for each cell
separately and apply a filter based on this statistic. This procedure
recalls that of library size filtering commonly performed in scRNA-Seq
data analysis, where the library size is the sum of the counts in each
single cell. We will store the median intensity in the colData
. The
medians are computed for every sample from the quantitative data using
the apply
function. The data matrix can be extracted from a
SingleCellExperiment
using the assay
function. Setting
MARGIN = 2
means that we compute the median by column.
scp[["peptides"]] %>%
assay %>%
apply(MARGIN = 2,
FUN = median,
na.rm = TRUE) ->
medians
head(medians)
#> 190222S_LCA9_X_FP94BMReporter.intensity.4
#> 0.7572816
#> 190222S_LCA9_X_FP94BMReporter.intensity.5
#> 0.4766553
#> 190222S_LCA9_X_FP94BMReporter.intensity.6
#> 0.8314786
#> 190222S_LCA9_X_FP94BMReporter.intensity.7
#> 0.4127720
#> 190222S_LCA9_X_FP94BMReporter.intensity.8
#> 1.0302978
#> 190222S_LCA9_X_FP94BMReporter.intensity.9
#> 0.8830915
The computed medians are then inserted in the colData
.
colData(scp)[names(medians), "MedianRI"] <- medians
Looking at the distribution of the median per cell can highlight low-quality cells.
scp %>%
colData %>%
data.frame %>%
ggplot() +
aes(x = MedianRI,
y = SampleType,
fill = SampleType) +
geom_boxplot() +
scale_x_log10()