Glossary · Single-Cell and Spatial
Marker gene
The gene that supposedly proves your cluster is a real cell type, and the checks that keep FindAllMarkers from lying to you.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 2 min read
Also: FindMarkers, FindAllMarkers
Definition
A marker gene is a gene whose expression is enriched in one cell cluster or cell type relative to all others, identified by running a differential expression test between that group of cells and the rest. In Seurat, FindMarkers() compares two specified groups and FindAllMarkers() runs that comparison for every cluster against the rest, defaulting to the Wilcoxon rank-sum test. A gene only functions as a useful marker when it clears both a fold-change threshold and a detection-rate gap between groups (pct.1 vs. pct.2), not just a significant p-value.
You meet marker genes right after clustering, at the exact moment you need to turn cluster numbers into cell type names. You ran FindClusters() or Leiden, got a UMAP with 15 blobs, and now someone is asking "which one is the NK cells?" Marker genes are the answer mechanism: genes whose expression is high in one cluster and low in the rest, surfaced by a differential expression test run cluster-by-cluster.
This step decides whether every downstream claim in the paper is true. If cluster 7 gets labeled "monocytes" because of a marker gene call that was actually driven by doublets or by an unequal-composition artifact, every fold-change and every pathway enrichment computed "in monocytes" from that point on is built on a mislabeled population.
Why it matters
Getting marker gene calling wrong doesn't throw an error, it produces a confident wrong cell type label that then contaminates every downstream comparison run "within" that cell type. The statistics can look pristine while the biology is broken: cells within a cluster are not independent observations, and Scanpy's own documentation warns that comparing individual cells inflates p-values for exactly this reason, recommending pseudobulk approaches like PyDESeq2 for rigor. Seurat's FindAllMarkers() has an added double-dipping problem, you cluster the cells, then test for differences between the clusters you just defined using the same expression matrix, which will always find "significant" genes even for noisy splits. There's also a quieter failure upstream: restricting to the top 2,000 variable genes before clustering can delete the marker you need before the test ever runs, which is exactly what happened to MS4A1 (CD20) in one documented case.
Where people get it wrong
The most common mistake is sorting FindAllMarkers() output by p_val_adj and reporting the top hit as the marker without checking pct.1 vs. pct.2. A gene expressed in 95% of a cluster and 90% of everything else can post a tiny adjusted p-value from sheer cell count while being nearly useless for telling that cluster apart. A second confusion is trusting Seurat's default "one cluster vs. all others combined" comparison as equivalent to a clean pairwise test, Bioconductor's OSCA book recommends true pairwise cluster comparisons instead, since unbalanced cluster composition biases which genes rank highest in a "vs. rest" setup. A third confusion is equating statistical significance with biological plausibility: a low p-value says the groups differ on that gene, not that the gene means what you assume it means for that tissue, which is why cross-referencing against PanglaoDB or CellMarker matters before the label goes in a figure.
A concrete example
Run marker detection on a clustered PBMC Seurat object, then apply the sanity check that actually validates the call before trusting the p-value: check the pct.1/pct.2 gap and confirm the marker visually.
# 1. Default marker detection (Wilcoxon rank-sum, Seurat defaults)
markers <- FindAllMarkers(
pbmc,
test.use = "wilcox",
logfc.threshold = 0.25, # stricter than the default 0.1
min.pct = 0.1,
only.pos = TRUE
)
# 2. Don't trust p_val_adj alone: require a real detection-rate gap
good_markers <- subset(markers, (pct.1 - pct.2) > 0.3 & avg_log2FC > 1)
# 3. Sanity-check a known marker before annotating the cluster
VlnPlot(pbmc, features = "CD3D", group.by = "seurat_clusters")
FeaturePlot(pbmc, features = c("CD3D", "MS4A1", "CD14"))
# CD3D should be high in exactly the clusters you're about to call T cells
# and near zero everywhere else. If it's diffuse across many clusters,
# the clustering resolution (or the marker call) is wrong, not the biology.Related terms
Questions people ask
- What is a marker gene in single-cell RNA-seq?
A gene whose expression is high in one cell cluster or cell type and low in the rest, identified by a differential expression test comparing that cluster against others. Marker genes are the primary evidence used to assign biological identity to an otherwise anonymous cluster number.
- What's the difference between FindMarkers and FindAllMarkers in Seurat?
FindMarkers() runs one comparison between two groups you specify (ident.1 vs. ident.2, or ident.1 vs. everything else). FindAllMarkers() is a wrapper that calls FindMarkers() once for every cluster against the rest, both defaulting to the Wilcoxon rank-sum test with a 0.1 log fold-change threshold and a 0.01 p-value cutoff.
- Which statistical test should I use for marker gene detection?
Wilcoxon rank-sum is the widely recommended default because it's non-parametric and robust to the outliers common in scRNA-seq data; it's also Seurat's default and one of Bioconductor OSCA's top picks alongside Welch's t-test. Logistic regression with Elastic-Net regularization is a documented alternative that avoids some of the double-dipping problem, but it requires more setup than a one-line FindMarkers call.
- Why do my marker genes have tiny p-values but don't look right on a UMAP or violin plot?
Cells within a cluster aren't independent observations, so testing thousands of cells against each other inflates significance even for genes with a modest real difference, Scanpy's documentation flags this explicitly and recommends pseudobulking for rigorous comparisons. Always check pct.1 vs. pct.2 and look at the gene on a plot before trusting the p-value alone.
- Can I skip differential expression and use machine learning to find marker genes instead?
Yes, logistic regression with Ridge, Lasso, or Elastic-Net regularization has been used as an alternative to FindMarkers, and a comparison of methods on bioRxiv found Wilcoxon, Student's t-test, and logistic regression were the most effective simple approaches. It doesn't remove the need for a sanity check: features excluded during upstream variable-gene selection still won't show up no matter which method you use downstream.
Related pages
- Compare · UMAP vs t-SNE: Which One Should You Use?
- Guide · How to Detect Batch Effects in Single-Cell ATAC-seq
- Guide · How to Find and Remove Doublets in Single-Cell RNA-seq
- Guide · How to Find and Remove Doublets in Single-Nucleus RNA-seq
- Guide · How to Detect Integration Over-Correction in Single-Cell RNA-seq
Related reading on the blog
Sources
- FindAllMarkers, Seurat v5 Reference — Default parameters (test.use='wilcox', logfc.threshold=0.1, min.pct=0.01, return.thresh=0.01) and the 10 supported statistical tests
- Chapter 11: Marker gene detection, Orchestrating Single-Cell Analysis with Bioconductor — Pairwise vs. one-vs-rest comparisons, Wilcoxon AUC effect size, multiMarkerStats(), binomial test
- scanpy.tl.rank_genes_groups, Scanpy Documentation — Default t-test, four supported methods, and the inflated-p-value caveat on non-independent cells
- Marker gene selection using logistic regression and regularization for scRNAseq — Logistic regression/Elastic-Net as an alternative to differential-expression marker calling; MS4A1/CD20 excluded by variable-gene filtering