Single-Cell RNA-seq Analysis with Bioconductor

A hands-on workflow from quality control to cell-type annotation

Author

Dario Righelli

Introduction

This hands-on workshop walks through a compact but complete single-cell RNA-seq analysis using the Bioconductor ecosystem. The emphasis is on how the pieces fit together in practice: we start from a SingleCellExperiment, progressively add analysis results to the same object, and finish with biologically interpretable cell-type labels.

The statistical and biological foundations of the individual methods are covered in the accompanying theoretical session. Here, the goal is to build and inspect a working analysis pipeline.

TipWorkflow at a glance

Data → quality control → normalization → feature selection → PCA → UMAP → clustering → marker genes → cell-type annotation

By the end of the session, you should be able to:

  • navigate a SingleCellExperiment;
  • compute and inspect basic QC metrics;
  • normalize counts and select highly variable genes;
  • generate PCA and UMAP representations;
  • identify graph-based clusters and their marker genes;
  • annotate cells using an external reference with SingleR.

1. Loading the data

We use the wild-type chimera dataset distributed with MouseGastrulationData. It contains mouse gastrulation cells with count data and rich cell-level metadata, while remaining small enough to run interactively during a workshop.

library(MouseGastrulationData)

sce <- WTChimeraData(
  samples = 5,
  type = "processed"
)

The object contains 2,411 cells and 29,453 features. The original count matrix is available in the counts assay, and the supplied metadata include sample information and mapped cell-type annotations.

sce
class: SingleCellExperiment 
dim: 29453 2411 
metadata(0):
assays(1): counts
rownames(29453): ENSMUSG00000051951 ENSMUSG00000089699 ...
  ENSMUSG00000095742 tomato-td
rowData names(2): ENSEMBL SYMBOL
colnames(2411): cell_9769 cell_9770 ... cell_12178 cell_12179
colData names(11): cell barcode ... doub.density sizeFactor
reducedDimNames(2): pca.corrected.E7.5 pca.corrected.E8.5
mainExpName: NULL
altExpNames(0):
NoteWhat should we notice?

At this stage the object already contains the raw count matrix and metadata, but it does not yet contain the logcounts, PCA, UMAP, clustering or annotation results that we will generate below. The analysis will progressively enrich this same object rather than creating disconnected data structures.

2. The SingleCellExperiment object

SingleCellExperiment is the central container used by many Bioconductor single-cell packages. It keeps expression matrices, feature annotations, cell-level metadata and low-dimensional representations synchronized.

Figure 1: Structure of a SingleCellExperiment object, showing the main assays, row- and column-level metadata, and reduced-dimensional representations.

The expression matrix forms the core of the object, while feature and cell annotations are stored alongside it. Results generated during the analysis, such as PCA or UMAP coordinates, can be added to the same object without breaking the correspondence between genes and cells.

Assays and annotations

The most important components for this workshop are:

  • assays: matrices such as raw counts and normalized expression;
  • rowData: feature-level annotations;
  • colData: cell-level annotations and QC metrics;
  • reducedDims: PCA, UMAP and other low-dimensional representations.
assayNames(sce)
[1] "counts"
counts(sce)[1:5, 1:5]
5 x 5 sparse Matrix of class "dgCMatrix"
                   cell_9769 cell_9770 cell_9771 cell_9772 cell_9773
ENSMUSG00000051951         .         .         .         .         .
ENSMUSG00000089699         .         .         .         .         .
ENSMUSG00000102343         .         .         .         .         .
ENSMUSG00000025900         .         .         .         .         .
ENSMUSG00000025902         .         .         .         .         .
colData(sce)
DataFrame with 2411 rows and 11 columns
                  cell          barcode    sample       stage    tomato
           <character>      <character> <integer> <character> <logical>
cell_9769    cell_9769 AAACCTGAGACTGTAA         5        E8.5      TRUE
cell_9770    cell_9770 AAACCTGAGATGCCTT         5        E8.5      TRUE
cell_9771    cell_9771 AAACCTGAGCAGCCTC         5        E8.5      TRUE
cell_9772    cell_9772 AAACCTGCATACTCTT         5        E8.5      TRUE
cell_9773    cell_9773 AAACGGGTCAACACCA         5        E8.5      TRUE
...                ...              ...       ...         ...       ...
cell_12175  cell_12175 TTTGGTTAGTCCGTAT         5        E8.5      TRUE
cell_12176  cell_12176 TTTGGTTAGTGTTGAA         5        E8.5      TRUE
cell_12177  cell_12177 TTTGGTTGTTAAAGAC         5        E8.5      TRUE
cell_12178  cell_12178 TTTGGTTTCAGTCAGT         5        E8.5      TRUE
cell_12179  cell_12179 TTTGGTTTCGCCATAA         5        E8.5      TRUE
                pool stage.mapped        celltype.mapped closest.cell
           <integer>  <character>            <character>  <character>
cell_9769          3        E8.25             Mesenchyme   cell_24159
cell_9770          3         E8.5            Endothelium   cell_96660
cell_9771          3         E8.5              Allantois  cell_134982
cell_9772          3         E8.5             Erythroid3  cell_133892
cell_9773          3        E8.25             Erythroid1   cell_76296
...              ...          ...                    ...          ...
cell_12175         3         E8.5             Erythroid3  cell_138060
cell_12176         3         E8.5 Forebrain/Midbrain/H..   cell_72709
cell_12177         3        E8.25       Surface ectoderm  cell_100275
cell_12178         3        E8.25             Erythroid2   cell_70906
cell_12179         3         E8.5            Spinal cord  cell_102334
           doub.density sizeFactor
              <numeric>  <numeric>
cell_9769    0.02985045    1.41243
cell_9770    0.00172753    1.22757
cell_9771    0.01338013    1.15439
cell_9772    0.00218402    1.28676
cell_9773    0.00211723    1.78719
...                 ...        ...
cell_12175   0.00129403   1.219506
cell_12176   0.01833074   1.095753
cell_12177   0.03104037   0.910728
cell_12178   0.00169483   2.061701
cell_12179   0.03767894   1.798687
rowData(sce)
DataFrame with 29453 rows and 2 columns
                              ENSEMBL         SYMBOL
                          <character>    <character>
ENSMUSG00000051951 ENSMUSG00000051951           Xkr4
ENSMUSG00000089699 ENSMUSG00000089699         Gm1992
ENSMUSG00000102343 ENSMUSG00000102343        Gm37381
ENSMUSG00000025900 ENSMUSG00000025900            Rp1
ENSMUSG00000025902 ENSMUSG00000025902          Sox17
...                               ...            ...
ENSMUSG00000095041 ENSMUSG00000095041     AC149090.1
ENSMUSG00000063897 ENSMUSG00000063897          DHRSX
ENSMUSG00000096730 ENSMUSG00000096730       Vmn2r122
ENSMUSG00000095742 ENSMUSG00000095742 CAAA01147332.1
tomato-td                   tomato-td      tomato-td
reducedDims(sce)
List of length 2
names(2): pca.corrected.E7.5 pca.corrected.E8.5
NoteInterpretation

The count matrix is sparse, which is expected for scRNA-seq data. Feature names are Ensembl gene identifiers, while rowData(sce)$SYMBOL stores gene symbols. The cell metadata already contain experimental information and previously mapped annotations. We will leave those existing annotations and PCAs untouched and create our own downstream results.

3. Quality control

Quality control aims to identify cells whose expression profiles are dominated by technical problems rather than meaningful biology. We focus on three standard per-cell summaries:

  • library size: total number of counts per cell;
  • detected features: number of genes with non-zero counts;
  • mitochondrial percentage: fraction of counts assigned to mitochondrial genes.

Identifying mitochondrial genes

The matrix uses Ensembl gene IDs, so we map them to chromosome names using EnsDb.Mmusculus.v79. Genes annotated on chromosome MT are flagged as mitochondrial.

library(EnsDb.Mmusculus.v79)
library(AnnotationDbi)

chr.loc <- mapIds(
  EnsDb.Mmusculus.v79,
  keys = rownames(sce),
  keytype = "GENEID",
  column = "SEQNAME",
  multiVals = "first"
)

is.mito <- chr.loc == "MT"
is.mito[is.na(is.mito)] <- FALSE

table(is.mito)
is.mito
FALSE  TRUE 
29440    13 

In this dataset, 13 features are identified as mitochondrial genes.

Computing QC metrics

qc <- perCellQCMetrics(
  sce,
  subsets = list(Mito = is.mito)
)

colData(sce) <- cbind(colData(sce), qc)

We first inspect each metric separately before deciding which cells to remove.

(
  plotColData(sce, y = "sum") |
  plotColData(sce, y = "detected") |
  plotColData(sce, y = "subsets_Mito_percent")
) +
  plot_layout(guides = "collect")

Distributions of library size, detected features and mitochondrial percentage across cells.
NoteHow to read these plots

Library size and the number of detected genes are related measures of sequencing depth and complexity. Cells at the extreme low end of either distribution are potentially low quality. A high mitochondrial percentage can indicate damaged cells in which cytoplasmic RNA has been preferentially lost. The aim is not to maximize filtering, but to identify clear outliers.

Defining low-quality cells

We use data-adaptive thresholds rather than arbitrary fixed cut-offs.

qc.filter <- perCellQCFilters(
  qc,
  sub.fields = "subsets_Mito_percent"
)

table(qc.filter$discard)

FALSE  TRUE 
 2324    87 
sce$discard <- qc.filter$discard

The automatic filtering marks 87 of 2,411 cells for removal, leaving 2,324 cells for downstream analysis.

We can now highlight the discarded cells on the individual QC distributions.

(
  plotColData(sce, y = "sum", colour_by = "discard") |
  plotColData(sce, y = "detected", colour_by = "discard") |
  plotColData(sce, y = "subsets_Mito_percent", colour_by = "discard")
) +
  plot_layout(guides = "collect")

QC distributions with cells flagged for removal highlighted.
NoteInterpretation

The highlighted cells let us check whether the automatic rule is targeting extreme observations rather than removing a large central portion of the data. This visual check is important: QC thresholds should always be treated as a diagnostic decision, not as an unquestioned preprocessing recipe.

A bivariate view is especially useful because it shows whether cells with high mitochondrial content also occupy unusual regions of the library-size distribution.

plotColData(
  sce,
  x = "sum",
  y = "subsets_Mito_percent",
  colour_by = "discard"
)

Library size versus mitochondrial percentage, highlighting cells selected for removal.
NoteInterpretation

The joint plot helps detect problematic cells that may not look extreme when each QC metric is considered in isolation. Once we are satisfied that the flagged cells represent QC outliers, we remove them.

sce <- sce[, !sce$discard]

4. Normalization

Cells can differ substantially in sequencing depth and capture efficiency. Normalization places expression values on a comparable scale before we measure transcriptional similarity.

Here we use logNormCounts(), which computes normalized log-expression values and stores them directly in a new logcounts assay.

sce <- logNormCounts(sce)

assayNames(sce)
[1] "counts"    "logcounts"

The SingleCellExperiment now contains both the original counts and normalized expression values.

logcounts(sce)[10:15, 10:15]
6 x 6 sparse Matrix of class "dgCMatrix"
                   cell_9778 cell_9779 cell_9780 cell_9781 cell_9782 cell_9783
ENSMUSG00000033813   2.22095  2.443967   2.18185  2.380348  2.984191  2.659692
ENSMUSG00000002459   .        .          .        .         .         .       
ENSMUSG00000085623   .        .          .        .         .         .       
ENSMUSG00000033793   .        .          .        .         .         1.220173
ENSMUSG00000025905   .        .          .        .         .         .       
ENSMUSG00000033774   .        .          .        .         .         .       
NoteWhat changed?

Normalization does not replace the raw counts. Instead, logcounts is added as a second assay, so downstream methods can use normalized expression while the original data remain available in the same object.

5. Feature selection

Not every detected gene is informative for distinguishing cell states. Many genes show little biological variation or mostly contribute technical noise.

We model the mean–variance relationship and quantify the biological component of variability for each gene.

dec <- modelGeneVar(sce)

dec
DataFrame with 29453 rows and 6 columns
                          mean       total        tech          bio     p.value
                     <numeric>   <numeric>   <numeric>    <numeric>   <numeric>
ENSMUSG00000051951 0.002954686 0.003756409 0.003000574  7.55835e-04 1.01938e-01
ENSMUSG00000089699 0.000000000 0.000000000 0.000000000  0.00000e+00         NaN
ENSMUSG00000102343 0.000000000 0.000000000 0.000000000  0.00000e+00         NaN
ENSMUSG00000025900 0.000839533 0.000927307 0.000852572  7.47351e-05 3.29188e-01
ENSMUSG00000025902 0.170769849 0.386818391 0.170162868  2.16656e-01 6.71410e-11
...                        ...         ...         ...          ...         ...
ENSMUSG00000095041  0.36452490   0.3502868  0.34216706  0.008119788    0.452361
ENSMUSG00000063897  0.50329012   0.4227755  0.44746303 -0.024687477    0.609606
ENSMUSG00000096730  0.00000000   0.0000000  0.00000000  0.000000000         NaN
ENSMUSG00000095742  0.00186983   0.0022693  0.00189887  0.000370437    0.162554
tomato-td           0.58882456   0.4762199  0.50365749 -0.027437598    0.608259
                           FDR
                     <numeric>
ENSMUSG00000051951 6.42460e-01
ENSMUSG00000089699         NaN
ENSMUSG00000102343         NaN
ENSMUSG00000025900 7.52549e-01
ENSMUSG00000025902 3.52807e-09
...                        ...
ENSMUSG00000095041    0.752549
ENSMUSG00000063897    0.752549
ENSMUSG00000096730         NaN
ENSMUSG00000095742    0.752549
tomato-td             0.752549

We retain the 1,000 most highly variable genes for dimensionality reduction.

hvg <- getTopHVGs(dec, n = 1000)

length(hvg)
[1] 1000
head(hvg)
[1] "ENSMUSG00000055609" "ENSMUSG00000052217" "ENSMUSG00000069919"
[4] "ENSMUSG00000048583" "ENSMUSG00000052187" "ENSMUSG00000051855"
NoteWhy select HVGs?

PCA and clustering work best when the input emphasizes genes that vary meaningfully between cells. Restricting the analysis to highly variable genes also reduces noise and computational cost without discarding the original expression matrix.

6. Principal component analysis

PCA compresses the expression profiles of the selected HVGs into a smaller set of orthogonal components. This denoised representation is used as the basis for neighbour finding and clustering.

sce <- runPCA(
  sce,
  subset_row = hvg
)

reducedDims(sce)
List of length 3
names(3): pca.corrected.E7.5 pca.corrected.E8.5 PCA
plotReducedDim(
  sce,
  dimred = "PCA"
)

Cells projected onto the first two principal components.
NoteInterpretation

The PCA projection already reveals broad transcriptional structure, but the first two components represent only part of the total variation. For downstream analysis we retain the multidimensional PCA representation rather than relying only on what is visible in this two-dimensional plot.

7. UMAP

UMAP provides a two-dimensional visualization that emphasizes local neighbourhood structure. We compute it from the PCA coordinates rather than directly from the full expression matrix.

sce <- runUMAP(
  sce,
  dimred = "PCA"
)

plotReducedDim(
  sce,
  dimred = "UMAP"
)

UMAP representation computed from the PCA space.
NoteInterpretation

The UMAP separates the dataset into several compact regions and trajectories, suggesting substantial cellular heterogeneity. At this point the visualization is deliberately unlabelled: the next step asks whether graph-based clustering recovers coherent groups within this structure.

8. Clustering

We identify groups of transcriptionally similar cells using graph-based clustering. A nearest-neighbour graph is constructed in PCA space and communities are detected with the Louvain algorithm.

colLabels(sce) <- clusterCells(
  sce,
  use.dimred = "PCA",
  BLUSPARAM = NNGraphParam(
    cluster.fun = "louvain"
  )
)

table(colLabels(sce))

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15 
 88 133  75 385 164 257 120 105 136 138 174 329  59 142  19 

The procedure identifies 15 clusters, with sizes ranging from small rare groups to several hundred cells.

plotReducedDim(
  sce,
  dimred = "UMAP",
  colour_by = "label"
)

UMAP coloured by Louvain cluster assignment.
NoteInterpretation

The cluster colouring provides a discrete summary of the continuous UMAP structure. Well-separated UMAP regions should generally correspond to individual or closely related clusters, while neighbouring clusters may represent finer substructure within related developmental states. Clusters are computational groups at this stage; they are not yet biological cell types.

9. Marker genes

Marker detection connects the unsupervised clusters to gene-level biology. We search for genes that are up-regulated in each cluster relative to the others.

markers <- findMarkers(
  sce,
  groups = colLabels(sce),
  direction = "up"
)

markers
List of length 15
names(15): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

markers is a list with one table per cluster. Each table summarizes pairwise comparisons against the remaining clusters and ranks genes according to their ability to distinguish that cluster.

head(markers[[1]])
DataFrame with 6 rows and 18 columns
                         Top      p.value          FDR summary.logFC   logFC.2
                   <integer>    <numeric>    <numeric>     <numeric> <numeric>
ENSMUSG00000049866         1  6.10531e-79  5.28881e-76       3.19375 2.7712217
ENSMUSG00000028023         1  4.91833e-43  9.22673e-41       2.68504 2.6655890
ENSMUSG00000051855         1 1.53028e-140 2.25357e-136       5.65312 0.0596104
ENSMUSG00000002265         1 4.20586e-138 4.05009e-134       4.33073 2.0866778
ENSMUSG00000037335         1 2.11091e-102  3.27225e-99       4.33614 4.3361382
ENSMUSG00000018217         1 4.55417e-113 1.03180e-109       5.09744 3.6598802
                     logFC.3   logFC.4   logFC.5   logFC.6   logFC.7   logFC.8
                   <numeric> <numeric> <numeric> <numeric> <numeric> <numeric>
ENSMUSG00000049866  3.244595   3.50600   2.56889   2.31187   2.58754  2.382022
ENSMUSG00000028023  1.900518   2.72152   1.97717   2.70612   2.71271  0.654973
ENSMUSG00000051855  0.354282   5.65312   1.90749   1.08866   3.30373  0.121485
ENSMUSG00000002265  0.773699   4.89576   1.14814   4.33073   4.41609  0.989117
ENSMUSG00000037335  1.620026   4.66726   4.09930   4.71761   4.69330  0.207842
ENSMUSG00000018217  1.043548   5.42612   4.18262   5.20494   5.25020  0.358036
                     logFC.9  logFC.10  logFC.11  logFC.12  logFC.13  logFC.14
                   <numeric> <numeric> <numeric> <numeric> <numeric> <numeric>
ENSMUSG00000049866   3.19375   2.66964   2.81275   1.52971  2.979505   1.84787
ENSMUSG00000028023   2.64760   2.72109   2.39715   2.07519  2.685043   2.38217
ENSMUSG00000051855   4.77004   3.52721   1.95034   1.27569  0.705496   2.82082
ENSMUSG00000002265   3.63977   4.29893   2.38821   1.39028  3.446944   2.15516
ENSMUSG00000037335   4.22999   4.66830   4.70533   2.87727  4.644707   4.63394
ENSMUSG00000018217   5.17777   4.66260   5.28814   3.99988  4.419181   5.09744
                     logFC.15
                    <numeric>
ENSMUSG00000049866 -0.2214644
ENSMUSG00000028023  2.6309222
ENSMUSG00000051855  0.5684350
ENSMUSG00000002265  0.4252704
ENSMUSG00000037335  0.1701581
ENSMUSG00000018217 -0.0221686
NoteReading the marker table

Top summarizes the best rank achieved across pairwise comparisons, while p.value and FDR quantify statistical evidence. The logFC.* columns show the direction and magnitude of expression differences against individual clusters. In practice, we would combine these statistics with known biological markers rather than assigning cell identities from significance alone.

10. Cell-type annotation

Manual annotation based on marker genes is valuable but can be time-consuming and subjective. As a complementary approach, we compare each query cell with an independently annotated embryonic reference using SingleR.

Preparing the reference

We use EmbryoAtlasData() from the same data package. Cells without a reference label are removed, as are cell types represented by fewer than 10 cells.

library(MouseGastrulationData)

ref <- EmbryoAtlasData(samples = 29)
ref <- ref[, !is.na(ref$celltype)]

tab <- table(ref$celltype)
keep.types <- names(tab)[tab >= 10]
ref <- ref[, ref$celltype %in% keep.types]

ref <- logNormCounts(ref)

ref
class: SingleCellExperiment 
dim: 29452 6453 
metadata(0):
assays(2): counts logcounts
rownames(29452): ENSMUSG00000051951 ENSMUSG00000089699 ...
  ENSMUSG00000096730 ENSMUSG00000095742
rowData names(2): ENSEMBL SYMBOL
colnames(6453): cell_95728 cell_95730 ... cell_103293 cell_103294
colData names(17): cell barcode ... colour sizeFactor
reducedDimNames(2): pca.corrected umap
mainExpName: NULL
altExpNames(0):
table(ref$celltype)

                     Allantois            Blood progenitors 1 
                           322                             12 
           Blood progenitors 2                 Cardiomyocytes 
                            33                            241 
               Caudal Mesoderm                  Def. endoderm 
                            21                             30 
                   Endothelium                     Erythroid1 
                           169                             23 
                    Erythroid2                     Erythroid3 
                            71                            550 
                  ExE endoderm                   ExE mesoderm 
                           294                            332 
  Forebrain/Midbrain/Hindbrain                            Gut 
                           991                            214 
Haematoendothelial progenitors          Intermediate mesoderm 
                           197                            198 
                    Mesenchyme                   Neural crest 
                           356                            241 
                           NMP              Paraxial mesoderm 
                           342                            507 
                           PGC            Pharyngeal mesoderm 
                            10                            354 
              Somitic mesoderm                    Spinal cord 
                           225                            346 
              Surface ectoderm 
                           374 

The filtered reference contains 6,453 cells spanning 25 embryonic cell types, providing a biologically appropriate label space for the query cells.

Running SingleR

SingleR compares each query profile with the labelled reference. We aggregate reference cells within labels to keep the workshop computation fast.

library(SingleR)

pred <- SingleR(
  test = sce,
  ref = ref,
  labels = ref$celltype,
  de.method = "wilcox",
  aggr.ref = TRUE
)

table(pred$labels)

                     Allantois            Blood progenitors 1 
                            87                              5 
           Blood progenitors 2                 Cardiomyocytes 
                            19                             79 
               Caudal Mesoderm                  Def. endoderm 
                            44                              7 
                   Endothelium                     Erythroid1 
                            59                             45 
                    Erythroid2                     Erythroid3 
                           118                            201 
                  ExE mesoderm   Forebrain/Midbrain/Hindbrain 
                           153                            261 
                           Gut Haematoendothelial progenitors 
                            46                             69 
         Intermediate mesoderm                     Mesenchyme 
                            73                            270 
                  Neural crest                            NMP 
                            47                            107 
             Paraxial mesoderm                            PGC 
                           164                              3 
           Pharyngeal mesoderm               Somitic mesoderm 
                           143                            103 
                   Spinal cord               Surface ectoderm 
                           139                             82 

The predictions cover a broad range of developmental lineages rather than collapsing most cells into one or two dominant labels, which is a useful first sanity check for this heterogeneous dataset.

sce$cell_type <- pred$labels

Visualizing predicted cell types

With many nominal categories, a continuous colour scale is inappropriate. We therefore generate a qualitative palette designed to keep neighbouring labels visually distinct.

library(Polychrome)

cell_types <- sort(unique(sce$cell_type))

cols <- createPalette(
  length(cell_types),
  seedcolors = c("#E41A1C", "#377EB8", "#4DAF4A")
)

names(cols) <- cell_types

plotReducedDim(
  sce,
  dimred = "UMAP",
  colour_by = "cell_type"
) +
  scale_colour_manual(values = cols) +
  guides(
    colour = guide_legend(
      title = "Cell type",
      override.aes = list(size = 3)
    )
  ) +
  theme(
    legend.position = "right",
    legend.key.height = grid::unit(0.45, "cm")
  )

UMAP coloured by SingleR-predicted cell type.
NoteInterpretation

The predicted labels occupy distinct regions of the UMAP and recover a diverse set of embryonic lineages, including mesodermal, erythroid, endothelial, neural and endodermal populations. Some related labels remain close to one another, which is expected for developmental states connected by gradual transcriptional transitions. Automated labels should therefore be treated as evidence to combine with marker genes and biological knowledge, not as ground truth.

11. Putting everything together

At the end of the workflow, the same SingleCellExperiment contains the raw and normalized expression matrices, QC information, low-dimensional embeddings, cluster assignments and predicted cell types.

sce
class: SingleCellExperiment 
dim: 29453 2324 
metadata(0):
assays(2): counts logcounts
rownames(29453): ENSMUSG00000051951 ENSMUSG00000089699 ...
  ENSMUSG00000095742 tomato-td
rowData names(2): ENSEMBL SYMBOL
colnames(2324): cell_9769 cell_9770 ... cell_12178 cell_12179
colData names(20): cell barcode ... label cell_type
reducedDimNames(4): pca.corrected.E7.5 pca.corrected.E8.5 PCA UMAP
mainExpName: NULL
altExpNames(0):
assayNames(sce)
[1] "counts"    "logcounts"
reducedDims(sce)
List of length 4
names(4): pca.corrected.E7.5 pca.corrected.E8.5 PCA UMAP
colData(sce)
DataFrame with 2324 rows and 20 columns
                  cell          barcode    sample       stage    tomato
           <character>      <character> <integer> <character> <logical>
cell_9769    cell_9769 AAACCTGAGACTGTAA         5        E8.5      TRUE
cell_9770    cell_9770 AAACCTGAGATGCCTT         5        E8.5      TRUE
cell_9771    cell_9771 AAACCTGAGCAGCCTC         5        E8.5      TRUE
cell_9772    cell_9772 AAACCTGCATACTCTT         5        E8.5      TRUE
cell_9773    cell_9773 AAACGGGTCAACACCA         5        E8.5      TRUE
...                ...              ...       ...         ...       ...
cell_12175  cell_12175 TTTGGTTAGTCCGTAT         5        E8.5      TRUE
cell_12176  cell_12176 TTTGGTTAGTGTTGAA         5        E8.5      TRUE
cell_12177  cell_12177 TTTGGTTGTTAAAGAC         5        E8.5      TRUE
cell_12178  cell_12178 TTTGGTTTCAGTCAGT         5        E8.5      TRUE
cell_12179  cell_12179 TTTGGTTTCGCCATAA         5        E8.5      TRUE
                pool stage.mapped        celltype.mapped closest.cell
           <integer>  <character>            <character>  <character>
cell_9769          3        E8.25             Mesenchyme   cell_24159
cell_9770          3         E8.5            Endothelium   cell_96660
cell_9771          3         E8.5              Allantois  cell_134982
cell_9772          3         E8.5             Erythroid3  cell_133892
cell_9773          3        E8.25             Erythroid1   cell_76296
...              ...          ...                    ...          ...
cell_12175         3         E8.5             Erythroid3  cell_138060
cell_12176         3         E8.5 Forebrain/Midbrain/H..   cell_72709
cell_12177         3        E8.25       Surface ectoderm  cell_100275
cell_12178         3        E8.25             Erythroid2   cell_70906
cell_12179         3         E8.5            Spinal cord  cell_102334
           doub.density sizeFactor       sum  detected subsets_Mito_sum
              <numeric>  <numeric> <numeric> <integer>        <numeric>
cell_9769    0.02985045   0.932680     27577      5418              471
cell_9770    0.00172753   0.810610     29309      5405              679
cell_9771    0.01338013   0.762288     28795      5218              480
cell_9772    0.00218402   0.849694     34794      4781              496
cell_9773    0.00211723   1.180146     38300      5211              488
...                 ...        ...       ...       ...              ...
cell_12175   0.00129403   0.805283     26680      4308              507
cell_12176   0.01833074   0.723565     19013      4684              174
cell_12177   0.03104037   0.601386     24627      5367              513
cell_12178   0.00169483   1.361415     46162      5312              833
cell_12179   0.03767894   1.187738     38398      6020              252
           subsets_Mito_detected subsets_Mito_percent     total   discard
                       <integer>            <numeric> <numeric> <logical>
cell_9769                     10              1.70795     27577     FALSE
cell_9770                     10              2.31669     29309     FALSE
cell_9771                     12              1.66696     28795     FALSE
cell_9772                     12              1.42553     34794     FALSE
cell_9773                     12              1.27415     38300     FALSE
...                          ...                  ...       ...       ...
cell_12175                    11             1.900300     26680     FALSE
cell_12176                    13             0.915163     19013     FALSE
cell_12177                    10             2.083080     24627     FALSE
cell_12178                    11             1.804515     46162     FALSE
cell_12179                    12             0.656284     38398     FALSE
              label        cell_type
           <factor>      <character>
cell_9769         1       Mesenchyme
cell_9770         2      Endothelium
cell_9771         3        Allantois
cell_9772         4       Erythroid3
cell_9773         4       Erythroid1
...             ...              ...
cell_12175       4        Erythroid3
cell_12176       6       Spinal cord
cell_12177       15 Surface ectoderm
cell_12178       4        Erythroid1
cell_12179       7       Spinal cord
ImportantThe key Bioconductor idea

The object has travelled through the entire analysis:

counts → QC → log-normalization → HVGs → PCA → UMAP → clustering → markers → cell-type annotation

Each step adds information while preserving the existing data and metadata in a common, interoperable container.

Take-home messages

  • SingleCellExperiment provides a common data structure across the workflow.
  • QC decisions should be inspected visually rather than applied blindly.
  • Normalization and feature selection prepare the data for similarity-based analyses.
  • PCA provides a denoised working space; UMAP provides an intuitive visualization.
  • Clusters summarize transcriptional structure but do not automatically define cell types.
  • Marker genes and reference-based annotation provide complementary biological evidence.
  • Bioconductor packages interoperate by adding results back to the same object.

Session information

For reproducibility, the exact R and package versions used to render the workshop are reported below.

sessionInfo()
R version 4.5.1 (2025-06-13 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 10 x64 (build 19045)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=Korean_Korea.utf8  LC_CTYPE=Korean_Korea.utf8   
[3] LC_MONETARY=Korean_Korea.utf8 LC_NUMERIC=C                 
[5] LC_TIME=Korean_Korea.utf8    

time zone: Asia/Seoul
tzcode source: internal

attached base packages:
[1] stats4    stats     graphics  grDevices utils     datasets  methods  
[8] base     

other attached packages:
 [1] Polychrome_1.6.1             SingleR_2.12.0              
 [3] EnsDb.Mmusculus.v79_2.99.0   ensembldb_2.34.0            
 [5] AnnotationFilter_1.34.0      GenomicFeatures_1.62.0      
 [7] AnnotationDbi_1.72.0         patchwork_1.3.2             
 [9] bluster_1.20.0               scran_1.38.1                
[11] scater_1.38.1                ggplot2_4.0.3               
[13] scuttle_1.20.0               BiocStyle_2.38.0            
[15] MouseGastrulationData_1.24.0 SpatialExperiment_1.20.0    
[17] SingleCellExperiment_1.32.0  SummarizedExperiment_1.40.0 
[19] Biobase_2.70.0               GenomicRanges_1.62.1        
[21] Seqinfo_1.0.0                IRanges_2.44.0              
[23] S4Vectors_0.48.1             BiocGenerics_0.56.0         
[25] generics_0.1.4               MatrixGenerics_1.22.0       
[27] matrixStats_1.5.0           

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3        rstudioapi_0.19.0        
  [3] jsonlite_2.0.0            magrittr_2.0.5           
  [5] ggbeeswarm_0.7.3          magick_2.9.1             
  [7] farver_2.1.2              rmarkdown_2.31           
  [9] BiocIO_1.20.0             vctrs_0.7.3              
 [11] DelayedMatrixStats_1.32.0 memoise_2.0.1            
 [13] Rsamtools_2.26.0          RCurl_1.98-1.20          
 [15] htmltools_0.5.9           S4Arrays_1.10.1          
 [17] AnnotationHub_4.0.0       curl_8.0.0               
 [19] BiocNeighbors_2.4.0       SparseArray_1.10.10      
 [21] htmlwidgets_1.6.4         httr2_1.3.0              
 [23] cachem_1.1.0              GenomicAlignments_1.46.0 
 [25] igraph_2.3.3              lifecycle_1.0.5          
 [27] pkgconfig_2.0.3           rsvd_1.0.5               
 [29] Matrix_1.7-6              R6_2.6.1                 
 [31] fastmap_1.2.0             digest_0.6.39            
 [33] colorspace_2.1-3          RSpectra_0.16-2          
 [35] dqrng_0.4.1               irlba_2.3.7              
 [37] ExperimentHub_3.0.0       RSQLite_3.53.3           
 [39] beachmat_2.26.0           labeling_0.4.3           
 [41] filelock_1.0.3            httr_1.4.8               
 [43] abind_1.4-8               compiler_4.5.1           
 [45] bit64_4.8.6               withr_3.0.3              
 [47] S7_0.2.2                  BiocParallel_1.44.0      
 [49] viridis_0.6.5             DBI_1.3.0                
 [51] rappdirs_0.3.4            DelayedArray_0.36.1      
 [53] scatterplot3d_0.3-45      rjson_0.2.23             
 [55] tools_4.5.1               vipor_0.4.7              
 [57] otel_0.2.0                beeswarm_0.4.0           
 [59] glue_1.8.1                restfulr_0.0.17          
 [61] grid_4.5.1                cluster_2.1.8.1          
 [63] gtable_0.3.6              BiocSingular_1.26.1      
 [65] ScaledMatrix_1.18.0       metapod_1.18.0           
 [67] XVector_0.50.0            ggrepel_0.9.8            
 [69] BiocVersion_3.22.0        pillar_1.11.1            
 [71] limma_3.66.0              BumpyMatrix_1.18.0       
 [73] dplyr_1.2.1               BiocFileCache_3.0.0      
 [75] lattice_0.22-7            FNN_1.1.4.1              
 [77] rtracklayer_1.70.1        bit_4.6.0                
 [79] tidyselect_1.2.1          locfit_1.5-9.12          
 [81] Biostrings_2.78.0         knitr_1.51               
 [83] gridExtra_2.3.1           scrapper_1.4.0           
 [85] ProtGenerics_1.42.0       edgeR_4.8.2              
 [87] xfun_0.56                 statmod_1.5.2            
 [89] UCSC.utils_1.6.1          lazyeval_0.2.3           
 [91] yaml_2.3.12               evaluate_1.0.5           
 [93] codetools_0.2-20          cigarillo_1.0.0          
 [95] tibble_3.3.1              BiocManager_1.30.27      
 [97] cli_3.6.6                 uwot_0.2.5               
 [99] dichromat_2.0-1           Rcpp_1.1.2               
[101] GenomeInfoDb_1.46.2       dbplyr_2.6.0             
[103] png_0.1-9                 XML_3.99-0.24            
[105] parallel_4.5.1            blob_1.3.0               
[107] sparseMatrixStats_1.22.0  bitops_1.1-0             
[109] viridisLite_0.4.3         scales_1.4.0             
[111] purrr_1.2.2               crayon_1.5.3             
[113] rlang_1.3.0               cowplot_1.2.0            
[115] KEGGREST_1.50.0