Chatomics Field GuideWhat They Don't Teach You

Sanity check · Single-Cell RNA-seq

How to Tell If You Overclustered in Single-Cell RNA-seq

Louvain and Leiden will hand you as many clusters as your resolution allows; the only test that matters is whether each one has markers you can name and cells from more than one sample.

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

You ran FindClusters() or sc.tl.leiden() with a resolution copied from a vignette, and the UMAP came back with 15 to 20 crisp, distinct colors. Every blob looks like a discovery. Nothing in the plot tells you that four of those blobs are the same T cell subset split by sequencing depth, two are a stress-response artifact from dissociation, and one is doublets sitting between a monocyte and a T cell cluster.

The cost of not checking shows up later: a cluster you annotated and reported as a novel cell state turns out, under a reviewer's question or a collaborator's re-analysis, to be explained entirely by library size or cell cycle phase. Louvain and Leiden are graph-partitioning algorithms, not discovery engines; resolution is a knob, and turning it up always produces more communities, whether or not the underlying separation is biologically real.

This page gives you a set of checks you can run against an object you already have, in the next hour, ordered from a single line of plotting code to a resampling-based stability test. Run them before you write a single marker gene name into your figure legend.

What it looks like when it's happening

  • Two clusters sit right next to each other on the UMAP with a thin bridge between them instead of a clean gap.
  • A cluster boundary lines up almost exactly with a gradient in total UMI count or number of detected genes, not with any marker gene.
  • FindAllMarkers/rank_genes_groups returns few or no genes with a meaningful log fold change for a cluster, or the top hits are ribosomal and mitochondrial housekeeping genes.
  • A cluster is made up almost entirely of cells from one sample, donor, or batch.
  • Two clusters you called different populations both express the same canonical marker set, e.g. both are CD3+CD4+ with no distinguishing gene.
  • Cluster count keeps climbing every time you nudge resolution up by 0.1, with no plateau.
  • A cluster scores high on S/G2M cell cycle genes or immediate-early stress genes (FOS, JUN, HSPA1A) instead of any lineage marker.
  • clustree shows a cluster split at higher resolution, then reshuffle into a different parent at the next resolution instead of staying stable.

Why it happens

Leiden and Louvain optimize a modularity-style objective over a k-nearest-neighbor graph, and the resolution parameter directly controls how many communities that optimization is willing to find. There is no built-in stopping rule tied to biological reality: push resolution high enough and any point cloud, including pure noise, gets carved into more pieces. Scanpy's Leiden defaults to resolution 1; Seurat vignettes commonly suggest 0.6 to 1.2 for a dataset around 3,000 cells. Both are starting points, not answers, because the "right" number of clusters depends on what markers and cross-sample checks say afterward, not on the parameter itself.

Sequencing depth is the most common technical axis that masquerades as biology. Droplet capture gives every cell a different total UMI count and a different number of detected genes, largely for technical reasons (capture efficiency, sequencing saturation). Cells with high depth have more genes detected and larger total counts almost by construction. If normalization does not fully account for this, high-depth and low-depth cells separate in PCA and UMAP purely by library size, and depth-imbalanced batches produce "batch clusters" whose main separating axis is depth, not cell state. Low depth also produces dropout, technical zeros for genes that are actually expressed, and dropout is not random across cells: cells with fewer UMIs get more zeros, which looks like an on/off biological pattern if you don't know to check for it.

Cell cycle, dissociation stress, and doublets each generate a transcriptional signature strong enough to dominate a principal component and earn its own cluster. Cycling cells upregulate a well-conserved S and G2M gene set regardless of lineage; cells stressed by tissue dissociation upregulate immediate-early genes like FOS and JUN; and a droplet that captured two cells produces a transcriptome that is a mixture of two real cell types, which clustering may place between them rather than merging into either. None of these are new cell types, but a naive clustering run cannot tell the difference between "new state" and "same cells, different technical condition."

Finally, if you did not integrate samples (or integrated when you should not have), the sample or donor itself becomes a clustering axis, especially with few cells per sample. A cluster that is "cell type X in donor A" looks distinct from "cell type X in donor B" purely because of donor-specific technical and biological variation that has nothing to do with the cell state you're trying to describe. Whether that's a problem or the finding depends entirely on your question, which is exactly the judgment call integration forces you to make.

The checks

Run them in order. Each one tells you what healthy looks like and what the problem looks like.

0/9 checked · saved in this browser

  1. Before looking at any marker gene, plot the same UMAP/tSNE used for cluster boundaries, colored by total UMI count and number of detected genes per cell.

    python
    sc.pl.umap(adata, color=['total_counts', 'n_genes_by_counts'])
    Healthy
    Depth is smooth and roughly randomly distributed across clusters; no cluster boundary lines up with a depth gradient.
    Red flag
    A cluster edge follows the same contour as the total_counts/n_genes gradient, meaning the 'cluster' is really a bin of high- or low-depth cells.
  2. Recluster the same neighbor graph across a resolution sweep (e.g. 0.2 to 1.6 in steps of 0.2) and record the number of clusters at each step.

    r
    for (res in seq(0.2, 1.6, by = 0.2)) {
      obj <- FindClusters(obj, resolution = res)
      print(length(unique(Idents(obj))))
    }
    Healthy
    Cluster count rises then plateaus over a wide resolution band, e.g. stays at 9 from 0.6 to 1.0 before climbing again.
    Red flag
    Cluster count climbs almost linearly with resolution and never plateaus, meaning the algorithm is fragmenting the graph rather than finding stable structure.
  3. Build a contingency table of cluster assignment versus sample, donor, or batch ID.

    python
    pd.crosstab(adata.obs['leiden'], adata.obs['sample'])
    Healthy
    Every cluster has meaningful representation from most samples, particularly for common cell types.
    Red flag
    A cluster is more than about 90% from a single sample or batch, which points to a donor or depth-imbalanced-batch effect rather than a cell state, unless you have a strong biological reason to expect that population is condition-specific.
  4. Run a one-vs-rest marker test for every cluster and inspect the top 5 to 10 genes by log fold change and percent expressing.

    r
    FindAllMarkers(obj, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)
    Healthy
    Each cluster returns at least one or two genes with a large, specific log fold change that you or a collaborator can name as a lineage or activation marker.
    Red flag
    The top 'markers' are ribosomal, mitochondrial, or generic housekeeping genes, or the same markers top the list for two neighboring clusters.
  5. Score every cell for S phase and G2M phase genes and a short immediate-early stress panel (FOS, JUN, HSPA1A, HSPA1B, DUSP1), then check whether any cluster is explained mostly by these scores rather than a lineage marker.

    r
    obj <- CellCycleScoring(obj, s.features = cc.genes$s.genes, g2m.features = cc.genes$g2m.genes)
    VlnPlot(obj, features = c('S.Score', 'G2M.Score'), group.by = 'seurat_clusters')
    Healthy
    Cell cycle and stress scores are spread across clusters, not concentrated in one.
    Red flag
    One cluster has a visibly higher S/G2M or stress score than every other cluster and no distinguishing lineage marker, meaning the split is a cycling- or dissociation-stress artifact, not a new cell type.
  6. Run a doublet caller (scDblFinder in R, Scrublet in Python) per sample before merging, then compare the mean predicted doublet score or flagged-doublet rate inside each cluster.

    Healthy
    Doublet score or rate is roughly flat and low across clusters.
    Red flag
    One cluster has a doublet score several times higher than the rest of the dataset, especially if it sits between two other clusters and co-expresses markers from both.
  7. Run clustering at several resolutions, store each result as its own metadata column, and plot the resulting tree.

    r
    clustree(obj, prefix = 'RNA_snn_res.')
    Healthy
    Most branches show clean 1-to-1 or 1-to-2 splits as resolution increases, with cells flowing mostly into one child cluster.
    Red flag
    A cluster splits into two children at higher resolution, then those children reshuffle again at the next resolution instead of staying stable.
  8. Compute the Jaccard index between every pair of clusters from a lower-resolution run and a higher-resolution run, and plot it as a heatmap.

    Healthy
    Each higher-resolution cluster maps almost entirely onto one lower-resolution parent, a clean refinement.
    Red flag
    A higher-resolution cluster splits a single lower-resolution cluster roughly 50/50 with no other distinguishing signal, the same naive-CD4 and CD14-monocyte fragmentation pattern reported when tuning resolution with callback.
  9. Use scran's bootstrapCluster() (or an equivalent parametric bootstrap / significance-of-hierarchical-clustering test) to resample cells, recluster, and measure how often each pair of cells that co-cluster in the original run still co-clusters after resampling.

    r
    library(scran)
    bootstrapCluster(x, FUN = quickCluster, clusters = my.clusters)
    Healthy
    Co-clustering probability stays high, close to 1, for real clusters even after resampling.
    Red flag
    Co-clustering probability drops sharply for a cluster pair under resampling, meaning the split is sensitive to sampling noise rather than robust structure.

What to do about it

Merge clusters that fail the marker test

When: Two adjacent clusters share their top marker genes, or a cluster's top 'markers' are ribosomal or mitochondrial genes with no lineage signal.

Lower the resolution one notch and recluster, or manually merge cluster identities (RenameIdents in Seurat, relabel adata.obs['leiden'] in Scanpy), then rerun the marker test to confirm the merged group now has clean markers.

Caveat: If the two clusters differ by a real, subtle activation state (resting versus recently activated T cells, for example), merging throws away a finding worth reporting; check the literature or a targeted marker panel before merging.

Renormalize when the split tracks sequencing depth

When: The depth-overlay check shows a cluster boundary aligned with total_counts or n_genes_by_counts.

Confirm normalization is depth-aware (SCTransform in Seurat, or log-normalize plus explicit regression of nCount and percent.mt in ScaleData; in Scanpy, confirm sc.pp.normalize_total ran before log1p and PCA), then recompute PCA, neighbors and clustering.

Caveat: Some cell types genuinely carry more total RNA (plasma cells, hepatocytes), and depth correlates with real biology there; regressing out nCount blindly can erase that signal, so recheck markers afterward to confirm the cluster merges sensibly rather than just becoming noisier.

Regress out or gate on cell cycle and stress scores

When: The cell cycle and stress check shows a cluster explained almost entirely by S/G2M or stress-gene scores.

Add S.Score/G2M.Score (Seurat CellCycleScoring plus vars.to.regress) or the equivalent stress module score as a regression covariate before rescaling and re-running PCA and clustering, then confirm the cluster disappears into its parent lineage.

Caveat: In actively proliferating tissue such as tumors or developing organs, cycling state is part of the biology; regressing it out can hide a real proliferative subclone, so only do this when cycling is clearly a nuisance split within an otherwise identical lineage.

Remove doublets before reclustering, not after

When: The doublet-score check shows a cluster with elevated scores sitting between two other clusters and co-expressing both of their markers.

Run scDblFinder or Scrublet per sample, not on the merged object, before clustering, flag and drop predicted doublets, then recluster from scratch.

Caveat: No doublet caller is perfectly calibrated; in continuous differentiation systems, transitional cells can look doublet-like to these tools, and removing them too aggressively deletes a real intermediate state.

Pick resolution from stability evidence, not a vignette default

When: You're choosing a resolution for the first time, or defending an existing choice to a reviewer.

Run the clustree and Jaccard checks across a resolution sweep (scanpy's default of 1, or the 0.6 to 1.2 band commonly used in Seurat, as starting points) and pick the lowest resolution at which every resulting cluster still passes the marker test and the cross-sample check.

Caveat: This still leaves a judgment call. Computational tools like clustree or knockoff-based methods such as callback should complement, not replace, biological review of the final cluster list.

When not to "fix" it

Don't merge or regress away a cluster just because it's small, driven by one sample, or thin on markers if that's the biology you're funded to find. A rare population like plasmacytoid dendritic cells or an expanded disease-specific T cell state can legitimately have only one or two markers, few cells, and show up almost entirely in one condition or one donor because it genuinely is condition-specific, not because integration failed. Forcing integration or a merge to make the UMAP look cleaner erases exactly the shifted or new state the experiment was designed to detect. Likewise, in a continuous differentiation system (hematopoiesis, embryonic development) discrete clusters are already an approximation of a gradient; mild fragmentation along that continuum can resolve genuinely distinct intermediate states, and chasing a single "correct" resolution there is the wrong frame entirely.

Five things experienced analysts do here

  1. Never trust a single resolution. Run a sweep and look at clustree before you touch annotation, not after you've already named the clusters.
  2. Keep nCount, nFeature, and percent.mt columns in your object through the entire pipeline so you can color-check any cluster against them in one line, any time a cluster looks suspicious.
  3. Score cell cycle and a short stress-gene panel before you cluster, not after, so you can decide upfront whether to regress them out rather than discovering the artifact downstream.
  4. Run doublet detection per sample before merging samples; a single global doublet-rate estimate averages away samples that were loaded at a higher cell concentration than the rest.
  5. Write down the marker that justifies each cluster's identity while you're annotating. If you can't name one, that's the moment to question the cluster, not after the figure is built.

Questions people ask

What resolution should I use for Leiden or Louvain clustering?

There's no universal number. Scanpy's Leiden defaults to resolution 1, and Seurat vignettes commonly suggest 0.6 to 1.2 for a dataset around 3,000 cells, but both are starting points. Run a resolution sweep, use clustree to see where splits stabilize, and keep the lowest resolution at which every cluster still has markers you can name and shows up in more than one sample.

What's the difference between overclustering and a genuine rare cell type?

A rare cell type has few cells but distinct, nameable markers and usually appears across more than one sample or donor. An overclustering artifact is typically explained by a technical axis, sequencing depth, cell cycle, or doublets, or it fragments a population that already has a name into pieces with no new marker signal.

Does clustree by itself tell me whether my clustering is correct?

No. clustree visualizes how clusters at adjacent resolutions relate to each other. Instability, where cells reassign between non-nested clusters as resolution changes, is a red flag, but a stable-looking tree doesn't prove biological reality on its own; you still need the marker and cross-sample checks.

Should I integrate my samples before clustering to avoid donor-driven overclustering?

It depends on your question. Integrate when you want a shared cell-type map and assume the same cell types exist across samples. Skip integration when you're looking for a condition-specific or donor-specific shifted state, because integration will actively smooth out the exact effect you're trying to detect.

How many marker genes does a cluster need to count as real?

There's no fixed field-wide threshold for gene count or fold-change cutoff. Practically, look for at least one or two genes with a large, cluster-specific log fold change that a domain expert can name as a lineage or state marker, not just a statistically significant gene with no biological meaning attached to it.

Related pages

Related reading on the blog

Sources

  1. Fine tune the best clustering resolution for scRNAseq data: trying out callback — Jaccard heatmap and the naive CD4/CD14 monocyte fragmentation example, cited in check 8
  2. Scanpy Leiden clustering documentation (stable version) — Default resolution value and behavior, cited in why_it_happens and FAQ
  3. GapClust: distinguishing rare cells from voluminous single cell expression profiles — Seurat resolution range guidance and marker/DEG validation approach, cited in fixes
  4. bootstrapCluster: Assess cluster stability in Bioconductor scran — Resampling-based stability test, cited in check 9
  5. Common Considerations for Quality Control Filters for Single Cell RNA-seq Data (10x Genomics) — UMI complexity and multiplet-detection considerations, cited in check 6
  6. Integrating datasets, OSCA multisample (Bioconductor) — Integration validation guidance, cited in fixes and when_not_to_fix

Part of the Overclustering series.