Introduction to scRNA-seq & Seurat
Understand what single-cell RNA sequencing measures, why it revolutionised cell biology, and how to install and configure Seurat — the most widely used R package for scRNA-seq analysis.
What is single-cell RNA sequencing?
Every cell in your body carries the same genome — the same DNA sequence. But cells are wildly different: a root cell in Sorghum bicolor behaves nothing like a leaf mesophyll cell, even though they share identical DNA. The reason is gene expression: different cells switch different genes on and off.
Traditional bulk RNA sequencing (RNA-seq) measures the average gene expression across millions of cells in a tissue sample. That average destroys the identity of individual cells. Rare cell types — a stress-activated immune cell, a newly differentiating stem cell — are completely masked by the majority.
Single-cell RNA sequencing (scRNA-seq) solves this by measuring gene expression in each cell individually. You end up with a matrix where each row is a cell and each column is a gene — giving you a transcriptomic fingerprint for thousands of cells at once.
In sorghum research, bulk RNA-seq would tell you that drought stress changes expression across the whole root. scRNA-seq tells you which specific root cell types respond — epidermis, endodermis, stele, quiescent centre — and by how much. This resolution is essential for understanding where and how tolerance mechanisms activate.
What scRNA-seq actually measures
In a typical scRNA-seq experiment:
- Cells are dissociated from tissue into a single-cell suspension
- Each cell is captured in a tiny droplet (e.g. 10x Genomics Chromium) containing a bead with a unique cell barcode
- RNA from the cell is reverse-transcribed and labelled with the barcode plus a Unique Molecular Identifier (UMI) per transcript
- Libraries are sequenced; reads are demultiplexed by barcode → each barcode = one cell
- UMI counts per gene per cell form the raw count matrix
UMIs are critical. Without UMIs, PCR amplification bias would make highly-amplified transcripts look more abundant than they are. UMIs mark each original mRNA molecule uniquely, so duplicates can be collapsed before counting. Seurat always works with UMI counts, never raw read counts.
Bulk RNA-seq vs Single-cell RNA-seq
Understanding the differences shapes every decision you make in a Seurat workflow.
| Feature | Bulk RNA-seq | scRNA-seq |
|---|---|---|
| Resolution | Tissue average (millions of cells) | Per-cell (thousands of individual cells) |
| Input | ~1 µg total RNA | Single-cell suspension |
| Sparsity | Most genes detected | Very sparse — most entries are zero |
| Cost | Lower per sample | Higher (£300–£1000+ per sample) |
| Cell type discovery | Requires prior marker knowledge | Unbiased clustering reveals new types |
| Rare cells | Signal drowned out | Detected if sampled |
| Analysis tool | DESeq2, edgeR, limma | Seurat, Scanpy, Monocle |
Sparsity is the defining challenge. A typical scRNA-seq matrix is 80–95 % zeros. Not because those genes are absent — but because mRNA capture is inefficient and not every transcript is sequenced. This is called dropout. Every step in the Seurat workflow is designed to handle or mitigate this sparsity.
Why Seurat?
Seurat is an R package developed at the Satija Lab (New York Genome Center). It provides a complete end-to-end workflow for scRNA-seq analysis: from raw count matrices all the way to annotated cell type clusters and publication-quality figures. It is the most cited scRNA-seq tool in the world, with over 20,000 citations as of 2025.
Seurat is not just a collection of functions — it introduces a dedicated data structure called the Seurat Object that stores everything in one place:
- The raw count matrix (stored as a sparse matrix to save memory)
- Normalised and scaled data
- Dimensionality reduction results (PCA, UMAP)
- Cluster identities (called "idents")
- Cell-level metadata (QC metrics, sample origin, annotations)
- Gene-level metadata
By keeping everything in one object, you never lose track of which cells belong to which analysis state. This is fundamentally different from the DESeq2 workflow, where you manage separate data frames for counts, metadata, and results.
Seurat versions
| Version | Key addition | Current? |
|---|---|---|
| Seurat v2 | Initial Canonical Correlation Analysis (CCA) integration | Legacy |
| Seurat v3 | Anchor-based integration, SCTransform normalisation | Widely used |
| Seurat v4 | Weighted nearest neighbour (WNN) multimodal analysis | Stable |
| Seurat v5 | Sketch-based analysis, split layers, improved scalability | ✅ Latest (2024+) |
This module uses Seurat v5. If you find older tutorials online (2019–2022), some function names and argument names differ. Always check the version with packageVersion("Seurat") before following any tutorial.
Understanding 10x Genomics data formats
The most common scRNA-seq platform is the 10x Genomics Chromium system. After sequencing and alignment (using Cell Ranger), 10x produces a standard output folder containing three files:
| File | Contents | Size |
|---|---|---|
matrix.mtx.gz |
Sparse count matrix in Matrix Market Exchange format — row/col indices + non-zero values | Typically 10–500 MB |
barcodes.tsv.gz |
Cell barcodes — one per line; the row labels of the matrix | Kilobytes |
features.tsv.gz |
Gene identifiers and names — the column labels of the matrix | Kilobytes |
A full 10x count matrix might have 30,000 genes × 10,000 cells = 300 million entries. Storing all of these as a regular dense matrix would require ~2.4 GB just for integers. Since 90 % of entries are zero, the MEX format stores only the non-zero values along with their row and column positions — typically reducing the file to a few megabytes. R loads this into a dgCMatrix (dense-generic-compressed sparse matrix), which Seurat uses internally throughout.
The three files together are called the filtered feature-barcode matrix and are typically located at:
sample_output/
└── outs/
└── filtered_feature_bc_matrix/
├── matrix.mtx.gz
├── barcodes.tsv.gz
└── features.tsv.gz
Seurat's Read10X() function reads all three files at once and combines them into a single sparse matrix — you just point it at the folder and it does the rest.
Installing Seurat
Seurat v5 requires R ≥ 4.1.0 and depends on several Bioconductor packages. The cleanest approach is to install from CRAN (which handles most dependencies automatically) and then install Bioconductor packages separately. On Ubuntu, you may also need system libraries.
Step 1 — Check your R version
# Check current R version — must be ≥ 4.1.0
R.version.string
# Check if Seurat is already installed
packageVersion("Seurat")
Step 2 — Install system libraries (Ubuntu)
# Required C libraries for R packages that Seurat depends on
sudo apt-get install -y \
libhdf5-dev \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libgeos-dev \
libgdal-dev \
libproj-dev
# libhdf5-dev → needed for reading HDF5 (.h5) 10x files
# libcurl4-openssl-dev → needed for internet downloads inside R
Step 3 — Install Seurat from CRAN
# Install Seurat v5 from CRAN
install.packages("Seurat")
# Install SeuratObject (data structure package — sometimes installed automatically)
install.packages("SeuratObject")
# Install supporting packages
install.packages(c(
"dplyr", # data manipulation
"ggplot2", # plotting (Seurat uses ggplot2 internally)
"patchwork", # arrange multiple plots
"Matrix", # sparse matrix support
"scales" # colour scales for plots
))
Step 4 — Install Bioconductor dependencies
# Install BiocManager if not already present
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
# Install Bioconductor packages used by Seurat
BiocManager::install(c(
"limma", # differential expression testing backend
"glmGamPoi", # fast GLM fitting for SCTransform
"DESeq2" # used for pseudobulk DE in later lessons
))
Installation takes 10–20 minutes on first run because Seurat has ~100 dependencies. If any package fails with a compilation error, the missing system library is usually the cause — re-read Step 2 and check the error message for which C header is missing.
First look in R — verifying your installation
Once installed, let's verify everything works and take a first look at the PBMC dataset we will use throughout this module. PBMC stands for Peripheral Blood Mononuclear Cells — a mixture of immune cells (T cells, B cells, NK cells, monocytes) that is the standard benchmark dataset for Seurat tutorials.
The 10x Genomics PBMC 3k dataset (2,700 cells from a healthy donor) has well-characterised cell types with known markers. This makes it ideal for learning — you can immediately verify that your clustering is biologically correct. Every major scRNA-seq tool publishes a tutorial using this dataset. We will use it across all 10 lessons of this module.
Load Seurat and check version
# Load Seurat
library(Seurat)
library(SeuratObject)
library(dplyr)
library(ggplot2)
# Confirm version — should be ≥ 5.0.0
packageVersion("Seurat")
# [1] '5.1.0'
# See all exported functions (over 300!)
ls("package:Seurat") |> head(20)
Download the PBMC 3k dataset
# Create a working directory for this module
mkdir -p ~/scrna-seurat-pbmc/data/raw
cd ~/scrna-seurat-pbmc/data/raw
# Download the PBMC 3k filtered feature-barcode matrix (22 MB)
wget -q \
https://cf.10xgenomics.com/samples/cell/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz
# Extract — creates pbmc3k_filtered_gene_bc_matrices/hg19/
tar -xzf pbmc3k_filtered_gene_bc_matrices.tar.gz
# Inspect the three files Seurat will read
ls -lh pbmc3k_filtered_gene_bc_matrices/filtered_gene_bc_matrices/hg19/
# -rw-r--r-- barcodes.tsv 70K
# -rw-r--r-- genes.tsv 502K
# -rw-r--r-- matrix.mtx 17M
The PBMC 3k data uses the older genes.tsv filename (not features.tsv.gz). This is fine — Read10X() handles both naming conventions automatically.
Peek at the raw files in R
# Point to the data directory
data_dir <- "data/raw/pbmc3k_filtered_gene_bc_matrices/filtered_gene_bc_matrices/hg19"
# Read the sparse count matrix — returns a dgCMatrix
counts <- Read10X(data.dir = data_dir)
# What class is this?
class(counts)
# [1] "dgCMatrix"
attr(class(counts), "package")
# [1] "Matrix"
# Dimensions: genes × cells
dim(counts)
# [1] 32738 2700
# 32,738 genes measured in 2,700 cells
# First 5 genes, first 4 cells — most values are zero
counts[1:5, 1:4]
# 4 x 4 sparse Matrix of class "dgCMatrix"
# AAACATACAACCAC AAACATTGAGCTAC AAACATTGATCAGC AAACCGTGCTTCCG
# AL627309.1 . . . .
# AP006222.2 . . . .
# RP11-206L10.2 . . . .
# RP11-206L10.9 . . . .
# LINC00115 . . . .
# Dots (.) represent zero — the matrix is very sparse
# Calculate sparsity: % of entries that are zero
sparsity <- 1 - nnzero(counts) / prod(dim(counts))
cat(sprintf("Sparsity: %.1f%%\n", sparsity * 100))
# Sparsity: 94.3%
94.3 % of all entries in this matrix are zero. That single number explains why every decision in the Seurat workflow — normalisation method, feature selection, dimensionality reduction — is specifically designed for sparse count data.
Exercises
Load Seurat in R and run packageVersion("Seurat"). If your version is below 5.0.0, update it using install.packages("Seurat"). Then run ?CreateSeuratObject to open the help page and read what the min.cells and min.features arguments do. We will use these in Lesson 2.
▶ Show answer
min.cells — include a gene only if it is detected in at least this many cells. Setting min.cells = 3 removes genes detected in fewer than 3 cells, which are unlikely to be informative.
min.features — include a cell only if it has at least this many genes detected. Setting min.features = 200 removes empty droplets and debris that have very few transcripts.
After reading the PBMC data with Read10X(), answer these questions using R commands:
- How many genes are in the dataset?
- How many cells are in the dataset?
- What is the maximum UMI count for any single gene in any single cell? (Hint:
max(counts)) - What are the first 5 cell barcodes? (Hint:
colnames(counts)[1:5])
▶ Show answer
dim(counts) → 32,738 genes × 2,700 cells
max(counts) → approximately 1,000+ (varies; one highly expressed gene in one cell)
colnames(counts)[1:5] → the first 5 are cell barcodes like AAACATACAACCAC-1
Calculate the sparsity of the PBMC count matrix using the formula shown in the lesson. Then look up one gene you know — for example PTPRC (encodes CD45, a pan-leukocyte marker) — and find how many cells it is detected in:
sum(counts["PTPRC", ] > 0)
Is it detected in most cells or only a minority? Does this match your expectation for a marker gene expressed in all blood cells?
▶ Show answer
Sparsity ≈ 94.3 % — nearly 19 out of every 20 values is zero.
PTPRC is a pan-leukocyte marker, so it should be detected in most cells. Running sum(counts["PTPRC", ] > 0) shows it in roughly 1,900–2,200 cells (out of 2,700). That is about 70–80 % — high for a single gene, which confirms it is a good pan-marker. Most genes are detected in far fewer cells (often < 10 %).