Single-Cell RNA-Seq for Beginners: Raw Data and Count Matrix
Single-cell RNA-seq for beginners: how tissue becomes a count matrix, what UMIs and barcodes do, the 3 QC metrics that flag bad cells, plus Seurat code.
Single-Cell RNA-Seq, Explained Like You're Paying For It
From a piece of tissue to a count matrix, and why that matrix can't be trusted yet
What you get out of this
Read this once and you'll be able to:
- Explain in one sentence why bulk RNA-seq hides the cells that matter.
- Walk someone from a tissue sample to a spreadsheet of counts, step by step.
- Say what a cell barcode does, what a UMI does, and why 500 reads can turn into 50 counts.
- Name the three impostors in every raw matrix and the three numbers that expose them.
- Build the same dataset as a Seurat object and as a SingleCellExperiment, and move between the two.
That's the foundation. Every QC decision, every clustering result, every figure you'll ever make sits on top of these five things. Most people skip them and go straight to the UMAP. Then they can't explain their own plot.
No single-cell experience needed. If you can read a spreadsheet and paste R code, you're good.
Part 1: The problem bulk can't solve
Say you own a store with 1,000 customers. At the end of the month you get one number: total revenue.
Useful number. It also hides everything interesting. You can't tell if 10 customers spent a fortune and 990 spent nothing, or if everyone spent about the same.
Bulk RNA-seq is total revenue. You grind up millions of cells, pull out the RNA, and get one averaged value per gene for the whole sample.
Single-cell RNA-seq is every receipt. Each cell gets measured on its own.
Why the average lies to you
Picture a tumor. 90% healthy stromal cells. 10% rare, drug-resistant cancer stem cells.
Run bulk on it and the answer is "stroma." The 10% that decides whether the patient relapses gets averaged into the background.
Now picture a tumor biopsy with cancer cells, T cells, macrophages, and blood-vessel cells all mixed together. Bulk tells you an inflammatory gene went up. That could mean:
- every cell is a little inflamed, or
- a small group of T cells is highly activated and everything else is quiet.
Two different biological stories. One bulk number. No way to tell them apart.
Single-cell measures thousands of cells one at a time. So you see which cells carry the signal, and how many of them there are.
What you get instead
Bulk gives you one column of numbers per sample.
Single-cell gives you a matrix:
- one row per gene
- one column per cell
- each entry is how many molecules of that gene were caught in that cell
That matrix is what lets you find cell types, discover subtypes nobody has named yet, order cells along a developmental path, and track which clones expand over time.
Memorize this: bulk gives you the average, single-cell gives you the distribution.
Part 2: Three places single-cell earns its price
Single-cell costs more per sample than bulk. Here's where the resolution pays for itself.
1. Tumors and immunotherapy
A solid tumor is an ecosystem: malignant clones, exhausted immune cells, cancer-associated fibroblasts, blood vessels. Single-cell maps who is in there and who is interacting with whom.
Concrete case. A lab compares biopsies from before and after immunotherapy.
- Bulk result: a cytokine gene went down.
- Single-cell result: the CD8+ T cells shifted from an active effector state into an exhausted state marked by inhibitory receptors like PD-1 and TIM-3, and a specific myeloid subset is expressing the matching ligands.
One result says "something changed." The other says what to target.
2. Immune repertoires
Adaptive immunity runs on rare clones of B and T cells. Older assays could tell you what a cell was doing, or what its receptor sequence was. Getting both from the same cell was the hard part.
Single-cell profiles the whole transcriptome of an immune cell and reconstructs its V(D)J receptor sequence at the same time. So you can tie a specific T-cell receptor clonotype to its transcriptional state during an infection, an autoimmune flare, or a vaccine response, and watch naive T cells turn into memory or effector cells.
3. Development and lineage
Progenitor cells change continuously as they mature. A single-cell experiment catches cells at every point along that path at once, and algorithms sort them into a "pseudotime" order.
In bone marrow, this showed blood-cell differentiation as a continuous, branching landscape of probabilistic states, and it exposed the transcription-factor switches that send a progenitor toward red blood cells versus myeloid cells.
Part 3: How tissue becomes a spreadsheet
Every single-cell dataset goes through the same six steps. Every QC problem you'll ever face traces back to one of them. Learn the steps and the problems stop being mysterious.
Step 1: Dissociation
Tissue is cells glued together. Enzymes plus gentle mechanical force break the glue and leave a suspension of single, living cells.
Both words matter: single and living. Push too hard and cells burst, spilling their RNA into the buffer. That spilled RNA shows up again in Step 6.
Step 2: Capture
The most common platform is droplet microfluidics. 10x Genomics Chromium is the one you'll see most often.
Each cell gets pushed into its own tiny oil droplet along with a gel bead. The bead is coated with primers, and every primer carries two tags:
- Cell barcode. The same sequence on every primer on that bead. Every molecule from this droplet ends up carrying it. This is the customer ID on the receipt.
- UMI (unique molecular identifier). A different random sequence on every primer. Each mRNA molecule that binds gets its own. This is the serial number on each item.
Step 3: Lysis and reverse transcription
Inside the droplet the cell gets popped open. Its mRNA (the poly-A-tailed kind) sticks to the bead's primers. Reverse transcription copies each mRNA into cDNA, and now every cDNA molecule carries the cell barcode plus its own UMI.
That's the whole trick of the technology. The physical separation happens once, in the droplet. After this step everything gets pooled and processed together, because the tags remember which cell each molecule came from.
Step 4: Library prep and sequencing
The cDNA gets amplified by PCR, built into a sequencing library, and run on an Illumina sequencer.
Notice the word amplified. PCR makes copies, and it makes them unevenly. Some molecules get copied 3 times, some 300. Hold that thought.
Step 5: Alignment and counting
Software takes over. Raw reads get split by sample, aligned to a reference genome, and counted per gene per cell. The output is the raw feature-barcode matrix: features (genes) as rows, barcodes (cells) as columns.
Step 6: Hand-off to QC
The matrix lands on your desk. It is not clean. Part 5 covers what's wrong with it.
Part 4: Three questions every read has to answer
For every read off the sequencer, the counting software asks three things:
| Question | Answered by | In plain terms |
|---|---|---|
| Which cell did this come from? | Cell barcode | Which droplet? |
| Which original molecule was this? | UMI | Which physical mRNA, before PCR copied it? |
| Which gene is this? | Alignment | Which spot in the genome does it map to? |
Every count in the matrix is those three answers, tallied.
The 500 → 50 example
This is the example that separates people who ran a tutorial from people who understand the data.
500 reads map to the gene CD3D. All 500 carry the cell barcode AAAC.
Do you write down 500?
No. You look at the UMIs. Across those 500 reads there are only 50 unique UMI sequences. The other 450 reads are PCR copies of molecules already on the list.
The count for CD3D in cell AAAC is 50.
Reads are photocopies. UMIs are serial numbers. You count serial numbers.
Skip this step and your expression values mostly measure how hard PCR worked that day, instead of how much RNA the cell had.
Part 5: Three impostors in your raw matrix
The raw matrix contains columns that were never a healthy cell. Three kinds.
1. Empty droplets
Some droplets caught a bead and no cell. They still soak up the loose RNA floating in the buffer from cells that burst during dissociation. That loose RNA is called ambient RNA. An empty droplet looks like a cell with very few molecules and very few genes.
2. Dying cells
A cell with a ruptured membrane leaked its cytoplasmic mRNA before it was captured. What's left is skewed toward what stayed inside, and mitochondrial transcripts are a big share of that. Dying cells show up with a high mitochondrial fraction and a low gene count.
3. Doublets
Two (or more) cells landed in one droplet and share one barcode. Their profiles get merged into one fake cell with an unusually high molecule count, an unusually high gene count, and often the markers of two different cell types at once.
The three numbers that expose them
All three impostors get caught with three per-cell metrics computed straight from the matrix:
| Metric | What it is | What it catches |
|---|---|---|
| Library size | Total UMI count in the cell | Empty droplets (low). Doublets (high). |
| Genes detected | Genes with at least one count | Empty droplets and dying cells (low). Doublets (high). |
| Mitochondrial fraction | Share of counts from mitochondrial genes | Dying cells (high). |
Every strange number in the matrix maps to a physical event upstream. Low counts: the droplet was empty or the cell was damaged. High mito: the membrane broke. Double the counts plus two sets of markers: two cells in one droplet.
Part 6: Two toolboxes, one dataset
You'll do this work in R, in one of two ecosystems. They talk to each other.
- Seurat is the all-in-one toolkit: load, QC, normalize, cluster, plot, all through one object. Built for interactive exploration.
- Bioconductor is modular. Hundreds of packages (scater, scran, batchelor, and more) all read and write one standard container, the SingleCellExperiment. Built for plugging in specialized statistics.
Most people learn one and avoid the other. Learn both. The conversion is one line each way.
The Seurat object
A Seurat object holds one analysis in one container: raw counts, normalized values, scaled values, dimensional reductions, clusters, and metadata.
When you build one from a count matrix:
- it creates an assay called
RNA - inside that assay, a
countslayer holds the raw UMI counts NormalizeData()later adds adatalayer, andScaleData()addsscale.data- per-cell information (counts per cell, genes per cell, sample, condition, mito %) lives in a data frame at
@meta.data
Build one with simulated data so you can see the parts move:
library(Seurat)
library(Matrix)
# Simulated data: 2,000 genes x 500 cells.
# Poisson noise with a low mean, so most entries are zero,
# which is the same shape as a true count matrix.
set.seed(42)
counts_matrix <- matrix(rpois(2000 * 500, lambda = 0.3), nrow = 2000, ncol = 500)
rownames(counts_matrix) <- paste0("Gene", 1:2000)
colnames(counts_matrix) <- paste0("Cell", 1:500)
counts_matrix <- Matrix(counts_matrix, sparse = TRUE)
# Build the Seurat object.
# min.cells = 3 -> drop genes seen in fewer than 3 cells
# min.features = 200 -> drop cells with fewer than 200 genes detected
pbmc_obj <- CreateSeuratObject(
counts = counts_matrix,
project = "SingleCellIntro",
min.cells = 3,
min.features = 200
)
pbmc_obj
head(pbmc_obj@meta.data, 3)
Output (your exact per-cell numbers will differ a little):
An object of class Seurat
2000 features across 500 samples within 1 assay
Active assay: RNA (2000 features, 0 variable features)
1 layer present: counts
orig.ident nCount_RNA nFeature_RNA
Cell1 SingleCellIntro 603 521
Cell2 SingleCellIntro 588 509
Cell3 SingleCellIntro 612 527
Three things to notice:
- "features" means genes and "samples" means cells. Old naming. Everyone lives with it.
- Only one layer exists so far.
dataandscale.datashow up after you normalize and scale. nCount_RNA(library size) andnFeature_RNA(genes detected) were computed for free. Two of your three QC numbers are already in the metadata.
The third one takes one line:
# Share of each cell's counts that come from mitochondrial genes.
# Human gene names start with "MT-". For mouse use "^mt-".
# On the simulated data this is 0 everywhere, since no gene is named MT-anything.
pbmc_obj[["percent.mt"]] <- PercentageFeatureSet(pbmc_obj, pattern = "^MT-")
head(pbmc_obj@meta.data, 3)
The SingleCellExperiment object
Same data, different box. An SCE separates things into named slots:
assays: the matrices (countsnow,logcountsafter normalization)colData: per-cell metadata (the equivalent of Seurat'smeta.data)rowData/rowRanges: per-gene annotationsreducedDim: PCA, UMAP, and other low-dimensional coordinates
library(SingleCellExperiment)
sce_obj <- SingleCellExperiment(
assays = list(counts = counts_matrix),
colData = data.frame(CellID = colnames(counts_matrix), Batch = "Batch1"),
rowData = data.frame(GeneID = rownames(counts_matrix))
)
sce_obj
dim(counts(sce_obj))
head(colData(sce_obj), 3)
Output:
class: SingleCellExperiment
dim: 2000 500
metadata(0):
assays(1): counts
rownames(2000): Gene1 Gene2 ... Gene1999 Gene2000
rowData names(1): GeneID
colnames(500): Cell1 Cell2 ... Cell499 Cell500
colData names(2): CellID Batch
reducedDimNames(0):
mainExpName: NULL
altExpNames(0):
[1] 2000 500
DataFrame with 3 rows and 2 columns
CellID Batch
<character> <character>
Cell1 Cell1 Batch1
Cell2 Cell2 Batch1
Cell3 Cell3 Batch1
Same 2,000 genes, same 500 cells. The only difference is the address of each piece. Seurat keeps the count matrix under an assay with layers. SCE keeps it under assays with names.
Moving between them
Trajectory tools tend to live in Bioconductor. Multimodal integration tends to live in Seurat. You will need both, sometimes in the same afternoon.
library(Seurat)
library(SingleCellExperiment)
# Seurat -> SingleCellExperiment
# Normalize first so the "data" layer exists. It becomes "logcounts" on the other side.
pbmc_obj <- NormalizeData(pbmc_obj)
sce_converted <- as.SingleCellExperiment(pbmc_obj)
class(sce_converted)
assayNames(sce_converted)
# SingleCellExperiment -> Seurat
seurat_converted <- as.Seurat(sce_converted, assay = "RNA")
class(seurat_converted)
# Converting an SCE that only has raw counts (no logcounts yet)?
# Tell Seurat not to look for a normalized assay.
seurat_from_raw <- as.Seurat(sce_obj, counts = "counts", data = NULL, assay = "RNA")
Output:
[1] "SingleCellExperiment"
attr(,"package")
[1] "SingleCellExperiment"
[1] "counts" "logcounts"
[1] "Seurat"
attr(,"package")
[1] "SeuratObject"
Metadata and reduction coordinates travel with the object in both directions. You are never locked into one ecosystem.
Part 7: The vocabulary, on one page
| Term | Plain meaning |
|---|---|
| Bulk RNA-seq | One averaged expression value per gene for the whole sample |
| scRNA-seq | One expression profile per individual cell |
| Dissociation | Breaking tissue into single, living cells |
| Droplet | A tiny oil bubble holding one cell and one bead |
| Gel bead | Carries the primers that hold the cell barcode and the UMIs |
| Cell barcode | Tag shared by every molecule from one droplet. Answers "which cell?" |
| UMI | Tag unique to each original mRNA molecule. Answers "which molecule?" |
| cDNA | The DNA copy of an mRNA, made by reverse transcription |
| Feature-barcode matrix | The genes × cells count table. Features = genes, barcodes = cells |
| Ambient RNA | Loose RNA in the buffer, from cells that burst |
| Empty droplet | Bead, no cell, filled with ambient RNA |
| Doublet | Two cells in one droplet, sharing one barcode |
| Library size | Total UMIs in a cell (nCount_RNA) |
| Genes detected | Number of genes with at least one count in a cell (nFeature_RNA) |
| Mitochondrial fraction | Share of a cell's counts that come from mitochondrial genes (percent.mt) |
| Seurat object | Container with an RNA assay (layers counts, data, scale.data) and meta.data |
| SingleCellExperiment | Container with assays, colData, rowData, reducedDim |
Part 8: What to do this week
- Get a raw 10x matrix. Any public PBMC dataset works.
- Build the Seurat object. Look at
@meta.databefore you do anything else. - Compute
percent.mt. - Plot all three numbers:
VlnPlot(pbmc_obj, features = c("nCount_RNA", "nFeature_RNA", "percent.mt"), ncol = 3)
- Stare at the tails. Low counts and low genes: empties and broken cells. High mito: dying cells. Far right on counts and genes: doublets.
Do that once and Part 5 stops being a paragraph you read and becomes a picture you've seen.
The next skill is picking thresholds for those three numbers without throwing away rare cells. That's a separate guide.
Want this applied to your own code?
Free guides get you started. The 4-week 1:1 Intensive works through your actual pipelines until they hold up in production.
See the 1:1 Intensive