Chatomics Field GuideWhat They Don't Teach You

Sanity check · Spatial Transcriptomics

How to Tell If You Overclustered in Spatial Transcriptomics

Leiden and Louvain will happily cut your tissue into as many pieces as the resolution allows; the algorithm has no opinion on whether any of those pieces is a real cell type.

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

You ran Leiden or Louvain at the default resolution (0.8 in Seurat's FindClusters, 1.0 in scanpy's tl.leiden) on your Visium or Xenium object, and you got 15 to 20 clusters where you expected six or seven cell types. The UMAP looked clean, so you started naming clusters off the top marker in each one and moved on to the figure.

The algorithm never checks whether the split makes biological sense. Louvain and Leiden will keep carving the same point cloud into smaller pieces as you raise the resolution parameter, regardless of what's actually different between them. Spatial data makes this worse than plain scRNA-seq in two specific ways: a Visium spot cluster can just be a shift in cell-type mixture proportions rather than a new cell type, and an unintegrated tissue-section batch effect can look exactly like a novel population. If half your clusters are artifacts, every marker list, every "novel state" claim, and every deconvolution result built on top of them is wrong.

This page gives you an ordered set of checks to run before you name a single cluster: cheap metadata lookups first, marker and spatial validation next, stability testing last. Run them on your own object in the next hour and you'll know which clusters survive and which need to be merged, re-integrated, or reclustered at a lower resolution.

What it looks like when it's happening

  • A clustree plot shows a cluster splitting cleanly at resolution 0.6, then the same cells re-merge or reshuffle at 0.5 and 0.7, meaning the split isn't stable.
  • table(Idents(obj)) or adata.obs['leiden'].value_counts() shows one or more clusters with fewer than 10 to 20 cells or spots, including outright singletons.
  • FindAllMarkers on the 'new' cluster returns the same top three markers as its neighbor, or nothing that passes an adjusted p-value and logFC cutoff.
  • ImageDimPlot or sq.pl.spatial_scatter shows a cluster scattered as isolated single spots across the whole tissue (salt-and-pepper), not occupying a contiguous region.
  • A crosstab of cluster against orig.ident/sample ID shows one cluster is drawn almost entirely from a single tissue section.
  • A violin plot of nCount_Spatial or percent.mt by cluster shows the 'distinct' cluster is really just the low-depth or high-mito spots.
  • Two clusters separate mainly along a cell-cycle or stress-response axis (MKI67, HSPA1A) instead of a lineage marker.

Why it happens

Leiden and Louvain optimize modularity on a nearest-neighbor graph, and the resolution parameter directly controls how finely that graph gets cut. There is no built-in stopping rule tied to biology: raise the resolution and you get more, smaller partitions, full stop. At high resolution the algorithm can produce singleton clusters, and node coverage across communities drops progressively as you push resolution up, a behavior documented directly in scanpy's own issue tracker. The tool will give you as many clusters as you ask for.

Spatial data adds a failure mode that scRNA-seq doesn't have. A 55-micron Visium spot typically holds 1 to 10 cells, so a spot "cluster" is a tissue domain or a mixture of cell-type proportions, not a single cell identity. Pushing the resolution higher on spot data mostly partitions the mixture-proportion space (more tumor, less stroma, and back) rather than separating genuinely distinct cell types. On imaging platforms (Xenium, MERFISH, CosMx) the panel is a few hundred to a few thousand genes instead of the whole transcriptome, so a resolution tuned for a 2,000-gene scRNA-seq workflow is running on a much sparser, noisier feature space and will fragment real populations or manufacture fake ones out of that noise.

The other spatial-specific driver is technical, not statistical: tissue-section batch effects (permeabilization differences, RNA quality, section thickness) dominate the top principal components when sections aren't integrated. Leiden will carve those differences into clean-looking clusters that are really just "which slide this spot came from," not shared biology. Whether that's a bug or exactly what you want to see depends on your question: if you assume shared cell types across samples you need to integrate first, and if you're specifically looking for sample-specific states you shouldn't integrate at all.

There's also a field-level incentive that pushes toward overclustering: more clusters read as more discovery. A UMAP with 20 named populations looks more thorough than one with 7, independent of whether any of those 20 map to an actionable biological insight. That bias toward higher resolution, not just algorithmic behavior, is worth naming when you're deciding what number to report.

The checks

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

0/8 checked · saved in this browser

  1. Look up the resolution value you actually ran and the size of your feature set (whole transcriptome vs. targeted panel).

    r
    obj@commands$FindClusters$resolution
    nrow(obj[["Spatial"]])  # or length(rownames(obj)) for panel size
    Healthy
    Resolution falls in the range used in published spatial vignettes: around 0.3 for Slide-seq-style data, up to the 0.8 default for whole-transcriptome Visium, scaled down further for panels under a few hundred genes.
    Red flag
    You copied the scanpy/Seurat default (0.8 to 1.0) straight from a whole-transcriptome scRNA-seq tutorial onto a 300-gene Xenium panel without adjusting it.
  2. Tabulate the number of cells or spots in every cluster and sort the table ascending to surface the smallest groups first.

    python
    adata.obs['leiden'].value_counts().sort_values()
    Healthy
    The smallest real cluster still has enough cells or spots to compute stable markers, at least tens of cells depending on dataset size.
    Red flag
    One or more clusters have fewer than 10 to 20 cells/spots, or you see outright singleton clusters, a known effect of pushing Leiden resolution too high.
  3. Build a contingency table of cluster identity against sample/section ID.

    r
    table(obj$seurat_clusters, obj$orig.ident)
    Healthy
    Most clusters draw cells or spots from every sample in roughly the proportions you'd expect biologically.
    Red flag
    A cluster is over 90% derived from a single tissue section, which means it's tracking a batch effect, not a shared cell type.
  4. Plot QC metrics and cell-cycle scores split by cluster identity to see which covariates, if any, are driving the split.

    r
    VlnPlot(obj, features = c("nCount_Spatial", "percent.mt", "S.Score", "G2M.Score"), group.by = "seurat_clusters")
    Healthy
    These covariates look similar across clusters, or track a plausible biological axis such as a proliferating tumor front.
    Red flag
    A cluster is defined almost entirely by low counts or high mito% (a QC artifact), or by cell-cycle/stress genes rather than a lineage marker.
  5. Run marker detection for every cluster and check whether the top markers are unique to it.

    r
    markers <- FindAllMarkers(obj, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)
    Healthy
    Each cluster's top marker or markers are absent or clearly lower in every other cluster, and you can name the population from prior literature.
    Red flag
    Two adjacent clusters share the same top three to five markers, or the only genes separating them are ribosomal, mitochondrial, or stress-response genes.
  6. Plot cluster identity back onto the tissue image or spatial coordinate grid.

    r
    ImageDimPlot(obj, fov = "image", group.by = "seurat_clusters")
    Healthy
    Each cluster occupies a spatially coherent region, or a biologically plausible dispersed pattern such as immune cells threading through stroma, and lines up with histology.
    Red flag
    A cluster is scattered as salt-and-pepper noise across every region with no relationship to tissue architecture, or it exactly tracks a slide edge or tissue-fold artifact.
  7. Recluster the same object across a range of resolutions and plot the result.

    r
    obj <- FindClusters(obj, resolution = seq(0.1, 1.2, 0.1))
    clustree(obj, prefix = "SCT_snn_res.")
    Healthy
    Most clusters have a single incoming edge from the coarser resolution below (a clean split), and the number of stable branches plateaus before your chosen resolution.
    Red flag
    Clusters merge and re-split going up the ladder as cells shuffle between them, or the branch count keeps climbing linearly with no plateau at all.
  8. Use a resolution-selection or consensus tool instead of trusting one clustering call, and report the stability metric.

    r
    clustOpt::optimizeResolution(seurat_object, features = pc_features)
    Healthy
    A single resolution converges as the objective choice under cross-validation, or an ensemble/consensus method agrees with the partition you picked manually.
    Red flag
    The metric is flat or noisy across resolutions with no clear optimum, or the ensemble consensus collapses two of your clusters into one.

What to do about it

Merge clusters that fail the marker test

When: Two or more clusters share top markers and differ mainly by a QC covariate or a cell-cycle/stress gene set.

Manually reassign identities after confirming with FindAllMarkers, or use a formal method like ClusterDE to test and merge over-split groups.

Caveat: Manual merging is a judgment call: document why in your methods, and rerun differential expression after merging so p-values aren't inflated by comparing fragments of the same population against each other.

Drop back to a clustree-stable resolution

When: Clustree shows the split you're relying on isn't stable across neighboring resolutions.

Rerun clustering at the resolution just below where clustree shows the plateau, scaling toward the low end (around 0.3) for smaller or sparser datasets and toward the Visium default (0.8) for full-transcriptome data with strong signal.

Caveat: One global resolution can be too coarse for one tissue compartment and too fine for another; consider subclustering a spatially or biologically defined subset instead of forcing a single number to fit the whole section.

Re-integrate before reclustering if a cluster tracks sample of origin

When: The crosstab against sample ID shows a cluster is essentially one tissue section.

Integrate sections with Harmony or a comparable method before clustering, if your question assumes shared cell types or domains across samples.

Caveat: Integration can also erase a real sample-specific disease state; only integrate when you're after shared biology, not when the difference between sections is the finding.

Use marker-guided clustering when you already know the cell types

When: You're working from a targeted imaging panel chosen around known markers, and unsupervised clustering keeps fragmenting populations you can already name.

Use a marker-guided approach that anchors the partition on genes you trust from your panel design, rather than letting noise in a sparse feature space drive the split.

Caveat: This bakes your prior knowledge into the result, so you'll miss genuinely novel states outside your marker list; pair it with a smaller unsupervised pass to catch what the markers don't cover.

Pick resolution objectively instead of eyeballing a UMAP

When: You need a defensible, reproducible resolution for a paper or pipeline rather than 'default because that's what the tutorial used.'

Run a cross-validation or stability-based resolution selector and report the chosen resolution and its stability metric in your methods.

Caveat: These tools optimize for statistical separability, not biological relevance; a cluster can be statistically stable and still be a QC artifact, so run the marker and confounder checks above regardless of what the optimizer picks.

When not to "fix" it

Don't merge away a small cluster just because it's small. A spatially coherent group of cells with genuinely unique, literature-backed markers, a rare immune population, a tumor-boundary state, is real biology even if it's a fraction of your total cells. If your question is specifically about sample-specific or disease-specific states, don't integrate across sections to make an inconvenient cluster disappear; that cluster being section-specific may be the actual finding, not an artifact. On Visium, sub-clusters within one histological region often reflect real spot-level mixture gradients, such as tumor purity increasing from margin to core, and collapsing them into fewer bins just to make the story cleaner throws away information the data actually supports.

Five things experienced analysts do here

  1. Run clustering at three to five resolutions and pick where clustree shows a stable plateau, not whatever number the software ships as default.
  2. Never call a Visium spot cluster a 'cell type' in your notes or figures; call it a domain or a mixture until you've run deconvolution.
  3. Check every cluster against nCount, percent.mt, and cell-cycle score before you trust it as biology, not after you've already written the marker list.
  4. Cross-tabulate against sample of origin before you get excited about a 'new' cluster; a lot of novel populations are one slide's batch effect.
  5. If you can't independently name a cluster from its markers, don't publish it as a cell type; merge it or flag it as unresolved instead.

Questions people ask

What resolution should I use for Leiden or Louvain clustering in spatial transcriptomics?

There's no universal number. Seurat's spatial vignettes run resolution 0.3 for Slide-seq-style data and the 0.8 default for Visium, while scanpy's Leiden defaults to 1.0. Treat these as starting points, then run a resolution ladder with clustree and check markers and spatial coherence before you commit to one.

Are Visium spot clusters the same as cell types?

No. A 55-micron Visium spot typically contains 1 to 10 cells, so a cluster of spots is a tissue domain or a mixture of cell-type proportions, not a single cell type. Only single-cell-resolution platforms like Xenium, MERFISH, and CosMx let you cluster and call something a cell type directly; for Visium, deconvolve the spots first.

How many clusters is too many for a single tissue section?

There's no fixed count validated across platforms in the current literature. Judge each cluster by whether it has independently nameable markers and holds up across biological replicates and neighboring resolutions, not by hitting a target cluster count.

What's the difference between overclustering in scRNA-seq and in spatial transcriptomics?

The core mechanism is the same: Leiden and Louvain keep splitting at higher resolution regardless of biology. Spatial data adds two extra failure modes, spot mixture gradients from limited spatial resolution, and tissue-section batch effects that dominate the top principal components when sections aren't integrated.

How do I tell if a spatial cluster is a batch effect instead of real biology?

Cross-tabulate the cluster against sample or section ID. If one cluster is drawn almost entirely from a single section, it's tracking technical variation between slides, not a shared cell type, and integrating before reclustering is the right next step.

Related pages

Related reading on the blog

Sources

  1. Cluster Determination, FindClusters • Seurat — Default resolution (0.8) and how resolution controls cluster number
  2. Analysis, visualization, and integration of spatial datasets with Seurat — Spatial clustering resolutions used for Visium and Slide-seq, marker/reference validation
  3. scanpy.tl.leiden, Scanpy documentation — Leiden resolution default and effect on cluster count
  4. GitHub - gladstone-institutes/clustOpt: Optimizing the resolution parameter for Seurat's FindClusters — Objective resolution selection via cross-validation and silhouette metrics
  5. Fine tune the best clustering resolution for scRNAseq data: trying out callback — Manual multi-resolution approach and stability checking for overclustering
  6. How to construct a spatial object in Seurat — ImageDimPlot validation of clusters against tissue image
  7. Squidpy: Spatial neighborhood analysis — Spatial neighborhood graph construction used for spatial coherence checks
  8. Synthetic control removes spurious discoveries from double dipping in single-cell and spatial transcriptomics data analyses — ClusterDE method for merging over-clustered groups

Part of the Overclustering series.