Sanity check · Single-Cell RNA-seq
How to Sanity-Check Marker Genes and Cell Type Labels in Single-Cell RNA-seq
A FindMarkers table full of RPL, RPS, MT- and FOS genes isn't a cell type. Here's how to catch it before the label ships.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read
You ran FindMarkers or rank_genes_groups on your clusters, sorted by p_val_adj, and the top genes for cluster 7 looked reasonable. Then you printed pct.1 and pct.2 and noticed half the "specific" markers are RPL13, RPS6, MT-CO1, and FOS. This happens on almost every real dataset, and it happens because the marker-calling machinery is doing exactly what it was asked to do: find genes that differ between groups, with no opinion on whether those genes mean anything biologically.
Once you assign a cell type label from that table, it propagates into every downstream figure: the UMAP legend, the differential abundance test, the paper's Figure 1. If the label came from a dissociation-stress or ambient-RNA signature instead of real identity, nobody catches it until a reviewer asks why your "novel population" is defined by heat-shock genes.
This page walks through the checks to run on a marker table before you trust it: reading pct.1/pct.2 instead of just fold-change, screening for the ribosomal/mito/stress signature, checking MALAT1, and confirming markers form clean diagonal blocks in a heatmap. None of it takes more than an hour once you know where to look.
What it looks like when it's happening
- Top markers for a cluster are dominated by RPS/RPL ribosomal protein genes or MT- mitochondrial genes
- FOS, JUN, JUNB, or HSPA1A sit at the top of the marker list for a cluster that's supposed to be a distinct cell type
- pct.1 and pct.2 are both high and close together (e.g. 0.95 vs 0.88) even though the gene has p_val_adj near zero
- Several clusters in the same dataset share the same top 3-4 markers
- p_val_adj is 0 (or 1e-300) for dozens of genes and you can't tell which ones actually matter
- A heatmap of top markers shows off-diagonal expression: a cluster's 'specific' gene also lights up in two or three other clusters
- The cluster you're trying to label has noticeably lower UMI or gene counts than the rest of the dataset
- MALAT1 is near zero in one cluster while every other cluster shows strong, even expression
Why it happens
Marker detection is circular by construction. Clusters are defined by an expression-based clustering algorithm run on the same gene expression matrix, and then that same matrix is tested for differences between the clusters it just produced. Testing a grouping against the data used to create it inflates significance: p-values near zero for dozens of genes are expected even when the underlying clusters don't correspond to real biological populations. The p-value is telling you the clusters are statistically separable, which they always are by design, not that any given gene is a trustworthy marker of identity.
Ambient RNA compounds the problem at the counting stage. Every droplet captures the encapsulated cell's transcriptome plus a variable fraction of cell-free ambient RNA floating in the surrounding solution, typically 3 to 35% of total counts per cell. Ambient RNA approximates the average expression profile of the whole sample, so highly expressed transcripts from an abundant population leak into every other cell's counts regardless of type. A rare or fragile population sitting alongside a large one inherits that population's genes as background, and those genes then show up as spurious "markers" at low fold-change across clusters that shouldn't share them.
Dissociation adds a second, independent artifact. Standard collagenase-based tissue dissociation induces a conserved immediate-early and heat-shock response, FOS, JUN, JUNB, HSPA1A and related genes, in essentially every cell type subjected to it. A cluster distinguished mainly by these genes is reporting how roughly the tissue was handled during dissociation, not what the cells are. Cold-active protease protocols measurably reduce this signature, but for data you've already generated there's no way to undo it after the fact.
Ribosomal and mitochondrial genes look like strong markers for a structural reason that has nothing to do with identity: they are highly and consistently expressed, so any variation in cell size, total RNA content, or translation activity between clusters produces large effect sizes in whatever statistical test you run. Cells under stress or with compromised membranes also show shifted mitochondrial-to-nuclear read ratios, which the test picks up as a "significant" difference even though it reflects cell condition, not cell type.
The checks
Run them in order. Each one tells you what healthy looks like and what the problem looks like.
0/7 checked · saved in this browser
For each candidate marker in the FindMarkers or rank_genes_groups table, look at pct.1 (fraction of cells in your cluster expressing the gene) and pct.2 (fraction outside the cluster expressing it). Sort by the gap (pct.1 - pct.2) instead of by p_val_adj alone.
rSeurat::FindMarkers(object, ident.1 = cluster_id, test.use = 'wilcox', logfc.threshold = 0.1, min.pct = 0.01) %>% dplyr::arrange(desc(pct.1 - pct.2))- Healthy
- Top markers show high pct.1 (roughly >0.5-0.7) and a clear drop to pct.2, e.g. pct.1 = 0.85, pct.2 = 0.10.
- Red flag
- pct.1 and pct.2 are both high and close together, e.g. 0.95 and 0.88 - the gene is expressed almost everywhere and isn't marking this cluster specifically.
Screen the ranked gene names for the families known to dominate marker lists for technical reasons.
bashgrep -E '^RPS|^RPL|^MT-|^FOS$|^JUN' marker_genes.txt- Healthy
- These prefixes are rare or absent from the top 20-30 ranked markers.
- Red flag
- Several of the top 10 markers start with RPL/RPS/MT-, or are FOS/JUN/JUNB/HSPA1A.
Plot MALAT1 per cluster with a violin or feature plot. MALAT1 is a nuclear lncRNA that should be broadly and evenly expressed in healthy cells across nearly all clusters.
rSeurat::VlnPlot(object, features = "MALAT1", group.by = "seurat_clusters")- Healthy
- MALAT1 expressed at comparable levels across nearly all clusters.
- Red flag
- One cluster shows MALAT1 near zero (below roughly 1% of the dataset median) while others don't - that cluster is likely damaged cells or debris, and any 'marker' pulled from it is a QC artifact, not a cell type.
Use DoHeatmap in Seurat or sc.pl.rank_genes_groups_heatmap / sc.pl.heatmap in Scanpy with the top 5-10 markers per cluster, cells grouped by cluster on the x-axis.
- Healthy
- Clean diagonal blocks: each cluster's markers light up strongly within that cluster and stay dim everywhere else.
- Red flag
- Off-diagonal expression - a gene ranked as a 'cluster 3 marker' also lights up brightly in clusters 5 and 8.
Because clustering-then-testing is circular, p-values are inflated by construction. Use pct.1 - pct.2 (delta-detected) or AUC as the primary ranking, and treat p_val_adj as a filter, not a ranking signal.
- Healthy
- Genes ranked highly by effect size overlap substantially with the genes ranked highly by p-value.
- Red flag
- A gene has p_val_adj = 0 but a delta-detected gap under roughly 0.1 - statistically significant, but not practically distinguishing.
Search the top 3-5 marker genes per cluster in CellMarker or the Mouse Cell Atlas (or a published panel for your tissue), and check whether the combination, not any single gene, matches a known population.
- Healthy
- At least 2-3 of the top markers independently point to the same known cell type for your tissue.
- Red flag
- No combination of top markers matches any known population, or the markers only make sense individually and never as a set.
Scan for highly expressed, tissue-abundant transcripts (hemoglobin genes if there's blood contamination, albumin in liver tissue, etc.) appearing as weak but significant markers in clusters that have no business expressing them. If present, run CellBender on the raw, unfiltered droplet matrix and redo clustering and marker calling on the corrected counts.
- Healthy
- After correction, spurious low-level detection of unrelated-tissue genes drops out of the marker lists.
- Red flag
- A gene known to be specific to a different cell type or tissue (e.g. hemoglobin in a T cell cluster) shows up as a 'significant' marker at low fold-change.
What to do about it
Filter the interpretation, not the object
When: Top markers are dominated by RPL/RPS/MT-/FOS/JUN genes across multiple clusters.
Apply an exclusion pattern to the ranked marker table (grep the output, e.g. ^RPS|^RPL|^MT-|^FOS$|^JUN) and inspect the next tier of genes for cluster identity. Keep this pattern as a standing step you run on every marker table before interpreting it.
Caveat: Don't remove these genes from the Seurat object or AnnData matrix itself. You still need MT- genes for percent.mt QC, and some of these genes are genuinely differential in real biology, e.g. plasma cells have unusually high ribosomal content because they're protein factories.
Flag, don't label, dissociation-stress clusters
When: A cluster's markers are mostly immediate-early or heat-shock genes (FOS, JUN, HSPA1A) with nothing else distinguishing it.
Treat it as a handling artifact rather than a cell type. Check whether it correlates with sample or processing batch rather than biological condition. For future experiments, switch to cold-active protease dissociation, which measurably reduces this signature.
Caveat: You can't retroactively fix the stress response in data you already have. Flag the cluster, consider excluding it from cell-type figures, or merge it with its likely parent population if the remaining markers overlap.
Rank by effect size when p-values all look significant
When: p_val_adj is 0 or near-0 for dozens of genes and you can't tell which ones matter.
Re-rank the marker table by pct.1 - pct.2 (delta-detected) or AUC instead of by p-value; cluster-vs-rest p-values are inflated by construction because the same expression data defined the clusters being tested.
Caveat: Effect-size-first ranking can surface lowly expressed genes with a large relative jump. Require a reasonable pct.1 floor, roughly 0.25-0.4, so you're not chasing noise.
Correct ambient RNA before re-clustering
When: The same abundant transcripts (ribosomal genes, hemoglobin, or other highly expressed background genes) show up as weak markers across many unrelated clusters.
Run CellBender on the raw, unfiltered count matrix (including empty droplets) before QC and clustering, or use SoupX/DecontX as lighter-weight alternatives. Redo clustering and FindMarkers on the corrected matrix.
Caveat: CellBender needs the raw droplet matrix and some GPU time to run well, and it's a preprocessing step: you have to go back before clustering, not patch the marker table after the fact.
Don't force a label onto an unmatched cluster
When: A cluster's markers don't match any known cell type in CellMarker or the literature for your tissue.
Report it with a provisional name (e.g. unknown_cluster_9) or a broader lineage-level call instead of a specific identity, and pursue orthogonal validation, FACS, qPCR, FISH, or IHC on a matched sample, before publishing a specific label.
Caveat: Orthogonal validation costs time and sometimes more sample. Reserve it for clusters that matter to the paper's conclusion, not every minor cluster in a supplementary figure.
When not to "fix" it
Don't strip ribosomal or stress genes just because they're on your standing exclusion list. Plasma cells, secretory epithelium, and other high-translation cell types genuinely have elevated ribosomal gene expression as part of their real biology, and a heat-shock or immediate-early signature can be the actual phenotype you're studying, for example a heat-shock experiment, an inflammatory time course, or an ischemia model. In those cases the "artifact" gene list is the finding, not the noise. Before filtering anything, check whether the signature correlates with your experimental design, treatment versus control, time point, genotype, rather than with sample identity or processing batch. If it tracks the biology you're testing rather than which tube it came from, it's not an artifact and shouldn't be filtered out.
Five things experienced analysts do here
- Always read pct.1 and pct.2 next to fold-change; a gene with both high and close together is ubiquitous, not a marker, no matter how small its p-value is.
- Keep a standing exclusion pattern for ribosomal, mitochondrial, and immediate-early stress genes to screen marker tables with, but never hard-delete those genes from the count matrix itself.
- Check MALAT1 per cluster before trusting any cluster's identity, especially for a cluster with suspiciously low UMI counts - near-zero MALAT1 usually means damaged cells or debris, not a cell type.
- Rank candidate markers by effect size (delta-detected or AUC), not by p-value, since cluster-vs-rest p-values are inflated by the same circularity that defined the clusters.
- Cross-check the top 2-3 markers per cluster against CellMarker or a published panel for your tissue before that label goes into a figure; a combination of markers that doesn't match anything known is a reason to hold off, not to guess.
Questions people ask
- Why are ribosomal genes (RPL/RPS) showing up as top markers in my Seurat FindMarkers output?
Ribosomal protein genes are highly and consistently expressed across almost all cell types, so small differences in total RNA content or translation activity between clusters produce large, statistically 'significant' fold-changes even though the genes aren't specific to any cell type. Check pct.1 and pct.2 for these genes: they're usually both high, which is the tell that a gene is ubiquitous rather than a real marker.
- What does it mean when p_val_adj is 0 for dozens of marker genes?
It usually means the statistical test is circular, not that every gene is a strong marker. Clusters were defined by an expression-based clustering algorithm, and the same expression data is then tested for differences between those clusters, which inflates significance by construction. Rank by effect size, pct.1 minus pct.2 or delta-detected, instead of trusting the p-value order.
- Should I remove mitochondrial and ribosomal genes before clustering?
Filter them out of your marker interpretation, not out of the count matrix before clustering. You still need mitochondrial percentage for cell QC, and stripping these genes pre-clustering can remove real biological signal, for example high-translation or stressed cell states. Apply the exclusion at the marker-table stage instead.
- How do I tell a dissociation-stress cluster from a real cell type?
Look at whether its top markers are dominated by immediate-early and heat-shock genes (FOS, JUN, HSPA1A) with nothing else distinguishing it, and whether it correlates with sample or processing batch rather than a biological condition. A real cell type usually has 2-3 markers that independently match a known population in a database like CellMarker or the Mouse Cell Atlas.
- What's a reasonable pct.1/pct.2 gap for trusting a marker gene?
There's no universal numeric cutoff, and it depends on cluster size and tissue, so treat any fixed threshold with suspicion. As a working rule, look for pct.1 well above 0.5 with a clear gap over pct.2, not both hovering near 0.9, and confirm the gene with a heatmap showing a clean diagonal block rather than relying on one ratio alone.
Related pages
- Guide · How to Choose Cell QC Thresholds in Single-Cell RNA-seq
- Guide · How to Find and Remove Doublets in Single-Cell RNA-seq
- Guide · How to Log-Transform Counts Without Fooling Yourself in Single-Cell RNA-seq
- Guide · How to Sanity-Check Marker Genes and Cell Type Labels in Spatial Transcriptomics
- Guide · How to Tell If You Overclustered in Single-Cell RNA-seq
- Glossary · Spatial transcriptomics
Related reading on the blog
Sources
- FindMarkers • Seurat — pct.1/pct.2/avg_logFC output columns and default filtering parameters
- scanpy.tl.rank_genes_groups, Scanpy — Scanpy equivalent output columns (pts, logfoldchanges, pvals) and test methods
- Marker gene detection (Bioconductor scrapbook) — Circularity of cluster-then-test marker calling, effect size ranking, heatmap diagonal check, orthogonal validation
- Dissociation of solid tumour tissues with cold active protease for single-cell RNA-seq minimizes conserved collagenase-associated stress responses — Conserved dissociation-stress gene signature and cold-protease mitigation
- filtering out MT/RPS genes from a list of genes (GitHub Issue #6883) — Common practice of filtering ribosomal/mitochondrial genes from marker lists
Part of the Marker gene sanity series.