Glossary · Single-Cell and Spatial
Pseudobulk
Collapsing cells into sample-level counts turns pseudoreplication into real replication, so a bulk RNA-seq model can actually be trusted on single-cell data.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Updated 2026-09-13 · 3 min read
Also: pseudo-bulk, aggregate expression
Definition
Pseudobulk is the sum (or average) of raw UMI counts across all cells of a given cell type within a single biological sample, collapsing thousands of single-cell measurements into one count vector per sample-celltype combination. The output matrix has the same shape as a bulk RNA-seq count matrix: genes as rows, samples as columns. Because the biological sample becomes the unit of replication instead of the individual cell, pseudobulk data can be fed straight into mature bulk differential expression tools such as DESeq2, edgeR, or limma.
You hit the pseudobulk decision the moment your question changes from "which cluster expresses this gene" to "does this gene change between conditions, across patients." Cluster markers (FindAllMarkers, a per-cell Wilcoxon test) answer the first question fine. They fail the second, because they were never built to handle multiple biological replicates per condition, they treat every cell as an independent observation, which it is not.
Pseudobulk is the fix: aggregate raw UMI counts across all cells of one cell type within one sample, so each sample-celltype combination becomes a single row of counts, the same shape a bulk RNA-seq matrix has. You meet this term in chapter 9 workflows the first time you compare two or more conditions with more than one donor or replicate per condition, which is most real single-cell experiments once you get past a pilot dataset.
Why it matters
Get this wrong and you inherit pseudoreplication: cells from the same donor are correlated with each other, so treating each cell as an independent observation makes your effective sample size look like thousands when it's actually 3, or 5, or however many donors you have. A Wilcoxon test or FindMarkers run cell-by-cell across two conditions with a handful of patients each will hand back p-values like 1e-50 for genes that are really just noisy at the sample level, because the test has no way to see that 2,000 correlated cells came from only 3 people. Aggregate to pseudobulk first and run DESeq2 with n=3 vs n=3, and most of those "significant" genes lose significance, because now the variance estimate correctly reflects donor-to-donor spread instead of cell-to-cell spread. Pseudobulk also happens to fix a second, unrelated problem for free: summing counts across cells kills the sparsity that plagues single-cell data, so lowly expressed genes that were mostly zeros at the single-cell level become measurable at the pseudobulk level.
Where people get it wrong
Practitioners confuse "pseudobulk for statistics" with "average expression for plotting." Averaging expression per cluster to color a dotplot or heatmap is a visualization convenience; it doesn't fix pseudoreplication because you still haven't collapsed to the sample level, only to the cluster level. The second common mistake: people do build a proper sample-celltype pseudobulk matrix, then undo the whole point by running a cell-level test like Wilcoxon on it anyway, or skip filtering out sample-cluster combinations built from only a few cells, which produces pseudosamples that are mostly technical noise rather than signal. Check the cell count backing every pseudobulk column before you trust the differential expression result that comes out of it.
A concrete example
A Seurat object holds cells from 6 patients (3 responders, 3 non-responders) across several annotated cell types. To test whether T cells respond differently between groups, aggregate raw counts per patient per cell type, then hand the result to DESeq2 instead of running a cell-level test. The DE test now has n=3 vs n=3 patients driving the variance estimate, not 2,000+ individual T cells pretending to be independent replicates.
# aggregate raw counts: cell type x patient x condition
pb_counts <- presto::collapse_counts(
counts_mat = seurat_obj@assays$RNA@counts,
meta_data = seurat_obj@meta.data,
varnames = c("cell_type", "patient", "response")
)
# subset to T cells, then run standard bulk DE
tcell_counts <- pb_counts$counts_mat[, pb_counts$meta_data$cell_type == "T_cell"]
tcell_meta <- pb_counts$meta_data[pb_counts$meta_data$cell_type == "T_cell", ]
dds <- DESeq2::DESeqDataSetFromMatrix(
countData = tcell_counts,
colData = tcell_meta,
design = ~ response
)
dds <- DESeq2::DESeq(dds)
res <- DESeq2::results(dds, contrast = c("response", "responder", "non_responder"))Related terms
Questions people ask
- What is pseudobulk in single-cell RNA-seq?
Pseudobulk is the sum (or average) of raw UMI counts across all cells of a given cell type within one biological sample, producing one count vector per sample-celltype combination. The resulting matrix has the same shape as a bulk RNA-seq count matrix: genes by samples, which lets you feed it into DESeq2, edgeR, or limma directly.
- What is pseudobulk differential expression?
It's differential expression run on the aggregated sample-level pseudobulk matrix instead of on individual cells. Because the sample, not the cell, becomes the unit of replication, standard bulk RNA-seq statistical models correctly estimate variance between biological replicates instead of pretending thousands of correlated cells are independent samples.
- Pseudobulk vs Wilcoxon: which should I use?
Use Wilcoxon (or
FindAllMarkers) for cluster marker discovery within one dataset, where you're asking which genes distinguish one cluster from the rest. Use pseudobulk plus DESeq2/edgeR when you're comparing conditions across multiple biological replicates, because Wilcoxon on cells ignores pseudoreplication and inflates significance.- How do I create pseudobulk from a Seurat or AnnData object?
Aggregate the raw counts matrix by grouping on cell type annotation, sample/patient ID, and condition, for example
presto::collapse_counts()in R ormuscat::aggregateData(), which is built for exactly this cluster-sample aggregation step before running a bulk DE tool.- How many cells do I need per sample to build a pseudobulk pseudosample?
There's no universal threshold published for this, but every practical pseudobulk workflow includes a filtering step: check the number of cells contributing to each sample-cluster combination and drop the sparse ones before running differential expression, since a pseudosample built from a handful of cells is mostly noise.
Related pages
Related reading on the blog
Sources
- How to create pseudobulk from single-cell RNAseq data — Source for the presto::collapse_counts() workflow and the three grouping dimensions (cell type, sample, condition)
- muscat detects subpopulation-specific state transitions from multi-sample multi-condition single-cell transcriptomics data — Seminal paper establishing pseudobulk-based differential state analysis for multi-sample scRNA-seq
- Differential state analysis with muscat — Source for aggregateData() and the recommendation to filter sparse sample-cluster combinations before DE
- Pseudobulk and differential expression (glmGamPoi vignette) — Source for the independence-violation argument: cells within one individual are more similar to each other than to cells from another individual
- Benchmarking methods for detecting differential states between conditions from multi-subject single-cell RNA-seq data — Source for the finding that all top-performing DE methods aggregate to pseudobulk first, and for the false-negative tradeoff
- Removal of unwanted variation in pseudobulk analysis of single-cell RNA sequencing data and the leveraging of pseudoreplicates — Source for batch effect handling in pseudobulk and the per-cell-type RUV recommendation