Chatomics Field GuideWhat They Don't Teach You

Comparison · single cell differential expression

Seurat vs DESeq2: Which One Should You Use?

The real fight isn't Seurat vs DESeq2, it's cells vs samples as your unit of replication, and getting that wrong is how you publish a false positive.

By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read

The verdict

For a condition-vs-condition comparison across donors or samples, the default should be pseudobulk DESeq2, not cell-level FindMarkers. The reason is not statistical sophistication, it's correctness: your experiment has donors as the replicate unit, and only pseudobulk DESeq2 tests at that unit. Running FindMarkers across pooled cells from multiple donors treats thousands of correlated cells as independent replicates, which manufactures significance that isn't there. If you're going to report a treatment effect, aggregate first with AggregateExpression(), then run DESeq2 on the resulting sample-by-gene matrix.

That said, FindMarkers with its default Wilcoxon test is the right tool, not a compromise, for marker gene discovery and cluster annotation, where the question is "what distinguishes this cluster from the rest of this same dataset" rather than "did this condition change expression across replicates." It's also the better choice at population scale: once you're comparing hundreds of samples (TCGA-sized cohorts), DESeq2's negative binomial assumptions can produce FDR inflation that Wilcoxon avoids. And for raw, sparse single-cell counts specifically, neither tool's default is the strongest option; MAST outperforms both on AUROC because it models detection-rate sparsity directly, so reach for it before forcing DESeq2 onto unaggregated data.

Seurat's FindMarkers() and DESeq2 answer two different questions even when you point them at the same dataset. FindMarkers compares expression between groups of cells, cell by cell. DESeq2 compares expression between groups of samples, sample by sample. That difference in what counts as one data point is the whole story here, not which package has better code.

Under the hood, FindMarkers defaults to a Wilcoxon rank-sum test: a nonparametric method that ranks expression values and asks whether one group's ranks are systematically higher, with no assumption about the shape of the expression distribution. DESeq2 instead fits a negative binomial generalized linear model to raw counts, using median-of-ratios normalization and empirical Bayes shrinkage of fold changes, an approach built for the small, carefully controlled replicate structure of classic bulk RNA-seq experiments (2 vs 2, 3 vs 3 donors).

The collision happens when someone wants to test a condition effect, say treatment vs control, in single-cell data. Run it as FindMarkers on all cells pooled from both conditions and you're testing cells as if they were independent replicates, when in fact cells from the same donor are correlated by shared genetics, batch, and technical depth. Aggregate cells into per-sample pseudobulk counts first, then hand that matrix to DESeq2, and you're testing donors as replicates, which is what your experimental design actually supports.

Head to head

CriterionSeuratDESeq2Edge
Statistical modelFindMarkers defaults to a Wilcoxon rank-sum test (test.use='wilcox'), a nonparametric rank-based test with 10 other options including MAST and t-test.DESeq2 fits a negative binomial GLM per gene, designed for count data with overdispersion, and is available inside FindMarkers as test.use='DESeq2'.Tie
Different tools for different questions, not a strict better/worse.
Unit of replicationFindMarkers at cell level treats each cell as an independent observation, so a 10,000-cell cluster gives you 10,000 'replicates' from however many donors contributed them.Pseudobulk DESeq2 aggregates counts per sample first, so the biological replicate is the donor or sample, matching how the experiment was actually replicated.DESeq2
Handling of donor correlationCells from the same donor share technical and biological effects, so FindMarkers p-values from cell-level comparisons are calculated as if those cells were independent, which they are not.Because DESeq2 pseudobulk operates on one value per sample, donor-to-donor correlation is absorbed into the model's per-gene dispersion estimate instead of masquerading as significance.DESeq2
This is the core reason the hbctraining and Bioconductor Support sources warn against cell-level DE for condition comparisons.
Minimum sample sizeFindMarkers needs cells, not samples, so it runs even with a single sample per group; it just cannot detect donor variability because it never saw more than one donor.Pseudobulk DESeq2 needs at least 2 biological replicates per condition as a hard technical floor, with 3 or more recommended for a usable dispersion estimate.DESeq2
Speed at hundreds to thousands of cellsPresto-accelerated Wilcoxon in FindMarkers is fast and scales well to the thousands-of-cells comparisons typical of marker gene discovery.DESeq2 was never built for cell-level scale; on pseudobulk data it is fast because the matrix has been collapsed to one row per sample, not one row per cell.Seurat
Fair comparison only holds at the scale each tool is meant to run at.
FDR behavior at large sample countsWilcoxon rank-sum keeps FDR control even with hundreds of samples, as shown in population-scale comparisons like TCGA LUAD vs LUSC.DESeq2's negative binomial assumptions can break down with hundreds of samples, inflating the false discovery rate.Seurat
Applies to large-cohort bulk-style comparisons, not typical 3-vs-3 pseudobulk designs.
Performance on sparse, zero-heavy dataSeurat offers MAST as an alternative to Wilcoxon specifically because it models the cellular detection rate as a covariate, which helps with sparsity.DESeq2 run directly on sparse single-cell counts underperforms MAST on AUROC; it performs relatively better once zeros are filtered or counts are aggregated into pseudobulk.Tie
Neither tool's default handles raw single-cell sparsity as well as MAST; pseudobulk aggregation is the real fix for DESeq2's side.
Normalization and aggregation defaultsAggregateExpression() with return.seurat=TRUE optionally runs NormalizeData for you, but the raw summed counts are also available if you want to normalize elsewhere.DESeq2 expects raw counts and applies its own median-of-ratios normalization internally; feeding it pre-normalized values breaks the model.DESeq2
The bite: normalizing pseudobulk counts before handing them to DESeq2 is a common mistake.
Fold-change reportingavg_log2FC in FindMarkers is not a simple log2(mean1/mean2); it is computed on expm1-transformed normalized data, which has caused documented discrepancies against Scanpy's equivalent.DESeq2's log2FoldChange comes from the GLM coefficient and can be run through lfcShrink for empirical Bayes shrinkage, giving fold changes that are more stable for low-count genes.DESeq2
Documentation and community supportSeurat's own vignettes cover FindMarkers thoroughly, including all 11 test options, directly on satijalab.org.DESeq2's Bioconductor documentation covers the bulk workflow well, but the pseudobulk-from-single-cell workflow lives across separate tutorials (hbctraining, Bioconductor Support threads) rather than one canonical doc.Seurat
Interoperability between the twoAggregateExpression() is Seurat's built-in bridge to pseudobulk, producing a matrix or Seurat object you can hand to DESeq2.DESeqDataSetFromMatrix() accepts that aggregated matrix directly with colData describing sample-level condition and replicate, so the two tools chain together for a proper pseudobulk pipeline.Tie
This pairing, not a head-to-head substitution, is how most labs actually use both tools together.

Use Seurat when

  • You're identifying marker genes to annotate clusters or cell types and want a fast, ranked list rather than a formally powered hypothesis test.
  • You only have one sample or one 10x run, so there's no donor-level replication to model in the first place.
  • You need results on thousands of cells per group quickly, where presto-accelerated Wilcoxon is built for that scale.
  • You want a test that's insensitive to outlier cells and skewed expression distributions rather than one that assumes a specific count distribution.
  • You're doing a quick within-dataset cluster-vs-cluster comparison before deciding what, if anything, deserves a formal condition-level test.

Use DESeq2 when

  • You're comparing a condition (treatment vs control, disease vs healthy) across multiple donors or samples and need p-values that account for donor-to-donor variability.
  • You have at least 2, and ideally 3 or more, biological replicates per condition after aggregating to pseudobulk.
  • You're preparing results for publication or a report where reviewers will check for pseudoreplication in the DE testing.
  • Your comparison is a small, classic design (2 vs 2 or 3 vs 3 samples) where DESeq2's shrinkage estimators were specifically built to perform well.
  • You want single-cell condition-level DE results to be directly comparable to an existing bulk RNA-seq DESeq2 pipeline in the same project.

Switching between them

Switching from cell-level FindMarkers to pseudobulk DESeq2 changes more than the function call. First, the object format: instead of a Seurat object with per-cell counts, you need a sample-by-gene count matrix, built with AggregateExpression(seurat_obj, group.by = c('cell_type', 'sample')) or Bioconductor's aggregateAcrossCells(), plus a colData table describing each sample's condition and any covariates.

Second, normalization: FindMarkers works on Seurat's log-normalized per-cell data, while DESeq2 wants raw, unnormalized counts and computes its own median-of-ratios size factors internally. If you feed DESeq2 already-normalized pseudobulk values, you break its variance model; AggregateExpression() and aggregateAcrossCells() both default to returning raw summed counts for exactly this reason.

Third, the numbers themselves stop being comparable. FindMarkers' p-values come from thousands of cell-level "observations" and will look far more significant than DESeq2's p-values from a handful of true sample replicates, for the same underlying biology, because the effective sample size is completely different. Fold changes shift too: avg_log2FC from FindMarkers is not the same calculation as DESeq2's log2FoldChange, and DESeq2's version can be additionally shrunk via lfcShrink. Finally, check your design formula and replicate count before you migrate: DESeq2 needs design = ~ condition (or more, with covariates) and a minimum of 2 replicates per condition to even run, 3+ to trust.

Pitfalls with either

  • Running FindMarkers or FindMarkers(test.use='DESeq2') across conditions by pooling all cells from all donors treats correlated cells as independent replicates and inflates significance; aggregate to pseudobulk per sample first if you're testing a condition effect across donors.
  • Feeding DESeq2 a pseudobulk matrix that has already been normalized (e.g. by NormalizeData or CPM) breaks its internal median-of-ratios normalization; always give DESeq2 raw summed counts and let it normalize.
  • Running standard DESeq2 on a cohort of hundreds of samples without checking FDR behavior can produce inflated false discoveries; switch to Wilcoxon rank-sum for population-scale comparisons like TCGA subtype contrasts.
  • Comparing FindMarkers' avg_log2FC directly against DESeq2's log2FoldChange as if they were the same statistic ignores that they're computed differently, one from expm1-transformed normalized data, the other from a GLM coefficient with optional shrinkage.
  • Attempting pseudobulk DESeq2 with only one sample per condition will not run correctly; the design matrix needs at least 2 biological replicates per condition as a technical minimum, 3+ for a dispersion estimate you can trust.
  • Trusting DESeq2's default negative binomial fit on raw, unaggregated single-cell counts full of zeros; either filter genes and cells first or switch to MAST, which was built to model detection-rate sparsity directly.

Questions people ask

Can I run DESeq2 directly on single-cell count matrices without pseudobulk aggregation?

Seurat exposes this via FindMarkers(test.use='DESeq2'), but it treats individual cells as replicates. Cells from the same donor are correlated, so this setup underestimates variance and produces artificially small p-values. Use it for exploratory cluster comparisons at most, never for a condition-vs-condition claim you plan to defend.

How many biological replicates do I need for pseudobulk DESeq2?

Two per condition is the bare statistical minimum DESeq2 needs to function, but that gives you almost no power to estimate dispersion reliably. Three or more replicates per condition is the practical floor for results you'd trust enough to report.

Why do Seurat and DESeq2 report different fold changes for the same gene?

Seurat's avg_log2FC is computed differently from a plain log2(mean1/mean2), and this has produced documented mismatches against tools like Scanpy. DESeq2's log2FoldChange is a GLM coefficient that can additionally be shrunk with lfcShrink. Treat them as related but non-identical statistics, not interchangeable numbers.

Is Wilcoxon or DESeq2 better for a large cohort with hundreds of samples?

Wilcoxon rank-sum holds up better at that scale; DESeq2's negative binomial GLM can develop FDR inflation once you're comparing hundreds of samples, as seen in TCGA-scale comparisons like LUAD vs LUSC. For population-level bulk comparisons, default to Wilcoxon and treat DESeq2 as tuned for the small-n regime it was built for.

What should I use for sparse single-cell data with a lot of zeros?

Neither Seurat's default Wilcoxon nor raw DESeq2 is the strongest choice here; MAST shows the best AUROC on sparse data because it models detection rate directly. If you want to keep using DESeq2, aggregate to pseudobulk first, since that's what actually resolves the sparsity problem, not the test you pick.

Related pages

Related reading on the blog

Sources

  1. Differential expression testing • Seurat — FindMarkers test.use options, Wilcoxon default, avg_log2FC and DESeq2 mode
  2. Single-cell RNA-seq: Pseudobulk differential expression analysis — Pseudobulk aggregation workflow and the biological replicate argument
  3. An evaluation of statistical differential analysis methods in single-cell RNA-seq data — DESeq2 vs MAST performance on sparse vs filtered data
  4. Using DESeq2 on single cell RNA Seq data (Bioconductor Support) — Minimum replicate requirement and correct design matrix construction
  5. Do you really understand log2Fold change in single-cell RNAseq data? — avg_log2FC calculation details and discrepancy with other tools