Chatomics Field GuideWhat They Don't Teach You

Sanity check · Single-Cell ATAC-seq

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

Louvain and Leiden will keep splitting your point cloud for as long as you keep raising resolution, here's how to find out whether the clusters you kept are real.

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

You extended your scRNA-seq pipeline to the 10x ATAC or Multiome kit, ran the Seurat-style workflow you already trust, and FindClusters() at resolution 0.8 handed you 23 clusters instead of the 8 to 10 cell types you expected from your sort gates. A few of those clusters look like obvious cell types on the UMAP. A few look like slivers cut off the edge of a bigger blob. Your scRNA-seq instincts don't transfer cleanly here, because the peak-by-cell matrix is far sparser and binary-ish, and the dimensionality reduction (TF-IDF plus LSI) behaves nothing like log-normalized PCA.

The cost of getting this wrong isn't cosmetic. Name a depth artifact or a doublet blob as a new cell type, and you'll spend weeks chasing marker peaks that don't replicate, running differential accessibility tests against a population that doesn't exist, and defending a UMAP boundary to a reviewer that a fragment-count colored plot would have killed in five minutes.

This page walks you through a resolution sweep, a depth-confound check, a stability check, and a marker/motif validation pass, in that order, so you can look at your current cluster list and decide, cluster by cluster, keep or merge.

What it looks like when it's happening

  • Cranking resolution from 0.2 up through 1.5 keeps adding clusters with no plateau, cluster count climbs roughly linearly instead of leveling off
  • Two adjacent clusters on the UMAP share the same gene activity scores and chromVAR motif deviations for every marker you check, with no significant hits from FindMarkers between them
  • A cluster's outline lines up almost exactly with the high-fragment or low-fragment tail when you color the UMAP by total fragments per cell or detected peaks
  • A cluster sits geometrically between two known cell types and shows intermediate or co-occurring gene activity for both of their canonical markers
  • Silhouette width for one or more clusters hovers near zero or goes negative when computed on the LSI embedding
  • Cluster boundaries shift meaningfully between reruns with a different random seed or a 0.1 change in resolution
  • A cluster is present almost entirely in one sample or one batch and absent from the others, with no known biological reason for that split
  • FindAllMarkers returns nothing significant, or the same handful of low-specificity peaks, for a cluster you're about to name

Why it happens

Louvain and Leiden are modularity-optimization algorithms, not classifiers with a built-in stopping rule. Raise the resolution parameter and the graph will keep fragmenting into more communities, there is no resolution at which the algorithm says "no more real structure here." That's true for scRNA-seq too, but scATAC-seq makes the failure mode worse because the underlying signal is thinner. Each cell yields a few thousand Tn5 insertion events spread across hundreds of thousands of candidate peaks, giving a peak-by-cell matrix that's 5 to 50 percent sparse and largely binary. Pairwise distances between cells in that space are noisier than distances in a gene-expression matrix, so a modularity-maximizing algorithm finds spurious extra communities more readily, especially once you nudge the resolution past whatever value first recovered your known cell types.

The dominant technical confound is sequencing depth. Cells with more fragments have more peaks called "accessible" purely by sampling, and if you cluster before correcting for that, the first component of the standard TF-IDF-plus-LSI reduction tracks total fragment count almost by definition, it has to be dropped, and neighbor graphs and clustering should run on LSI components 2 through 50, not 1 through 50. Skip that step, or fail to filter out the lowest-depth cells before TF-IDF normalization, and Louvain will hand you a "cluster" that's really just the depth distribution's tail, standing in for a cell type that never existed.

The other reliable source of fake clusters is contamination that rides along with real biology: doublets formed when two nuclei end up in the same droplet, ambient chromatin signal, and cell-cycle or stress-response accessibility programs that vary within a cell type but get treated by the algorithm as if they defined a new one. A doublet made of two real cell types lands geometrically between them on the UMAP and inherits marker peaks from both, which is exactly what a hasty read of the UMAP mistakes for a "transitional" or "hybrid" population.

Finally, how you called peaks upstream shapes how easily overclustering happens downstream. Peaks called on the pooled dataset are biased toward whatever's common across all cells, which under-represents rare-population-specific regulatory elements and can push a genuine rare cell type toward looking like noise rather than a coherent cluster, the opposite failure, but one that often gets "fixed" by cranking resolution higher, which reintroduces overclustering everywhere else in the dataset.

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. Re-run FindClusters (or ArchR's addClusters) at a range of resolutions, 0.2, 0.4, 0.6, 0.8, 1.0, 1.5, and plot the number of resulting clusters against resolution. Look for where the curve flattens.

    r
    library(Signac)
    library(Seurat)
    
    res_range <- c(0.2, 0.4, 0.6, 0.8, 1.0, 1.5)
    n_clusters <- sapply(res_range, function(r) {
      atac <- FindClusters(atac, resolution = r, algorithm = 3, verbose = FALSE)
      length(unique(Idents(atac)))
    })
    plot(res_range, n_clusters, type = "b",
         xlab = "resolution", ylab = "number of clusters")
    Healthy
    A curve that rises steeply at low resolution, then flattens into a plateau, the plateau resolution is usually your best candidate.
    Red flag
    Cluster count keeps rising roughly linearly across the whole range with no plateau, or every resolution step you check produces a different-looking UMAP with no stable core structure.
  2. Plot total fragments (or detected peaks) per cell on the UMAP embedding you clustered on, and visually compare that pattern to your cluster boundaries.

    r
    atac$log_fragments <- log10(atac$nCount_peaks + 1)
    FeaturePlot(atac, features = "log_fragments", reduction = "umap")
    Healthy
    Depth varies smoothly and roughly randomly across clusters, or shows a gradient within a cluster rather than a sharp cut that matches a cluster boundary.
    Red flag
    One or more cluster boundaries line up almost exactly with a depth threshold, the 'cluster' is really the high- or low-fragment tail of the distribution.
  3. Compute the correlation between LSI component 1 and total fragments per cell. Confirm your clustering and UMAP call used components 2 onward (typically 2:50), not component 1.

    r
    lsi <- Embeddings(atac, "lsi")
    cor(lsi[, 1], atac$nCount_peaks)
    # should be strongly correlated with depth, this is why component 1 is dropped
    Healthy
    LSI component 1 correlates strongly with fragment count (that's expected and fine), and it is explicitly excluded from `dims` used for FindNeighbors/RunUMAP/FindClusters.
    Red flag
    Component 1 is included in the dims used for clustering, or the correlation check was never run and you can't confirm which components drove the clustering.
  4. Using the LSI embedding (components 2 onward), compute per-cell silhouette width against current cluster labels.

    r
    library(cluster)
    lsi <- Embeddings(atac, "lsi")[, 2:30]
    d <- dist(lsi)
    sil <- silhouette(as.integer(Idents(atac)), d)
    summary(sil)$clus.avg.widths
    Healthy
    Most clusters average clearly positive (well-separated in LSI space); scATAC-seq baselines run lower than scRNA-seq due to sparsity, so judge clusters relative to each other, not against a scRNA-seq threshold.
    Red flag
    One or more clusters average near zero or negative, those cells are about as close to a neighboring cluster as to their own.
  5. For every pair of clusters that sit next to each other on the UMAP, run FindMarkers on the gene activity assay (or peak assay) and require a real marker set, not a handful of low-specificity hits.

    r
    DefaultAssay(atac) <- "GeneActivity"
    markers <- FindMarkers(atac, ident.1 = "5", ident.2 = "7", min.pct = 0.1)
    head(markers[order(markers$p_val_adj), ])
    Healthy
    A clear set of genes with strong fold-change and adjusted p-values you can name and assign to a known cell type.
    Red flag
    No significant markers, or the top hits are housekeeping genes / peaks correlated with fragment count rather than cell-type-specific loci.
  6. Run chromVAR (via Signac's RunChromVAR or ArchR's addDeviationsMatrix) and compare bias-corrected motif deviation z-scores between the two clusters in question.

    Healthy
    Distinct, cluster-specific motif enrichment (e.g. a lymphoid TF motif enriched in one cluster, a myeloid TF motif in the other) that matches the gene activity marker call.
    Red flag
    Motif deviations look identical between the two clusters, or separation only appears without bias correction, a sign the split is driven by depth or mapping bias rather than regulatory biology.
  7. For any small or intermediate-looking cluster, check three things together: fragment count relative to the dataset median, co-expression of gene activity for two mutually exclusive lineage markers, and UMAP position between the two putative parent clusters.

    Healthy
    Suspected doublet cells are a minority, roughly consistent with your expected multiplet rate for the loading concentration used, and disappear as a distinct cluster once removed.
    Red flag
    A whole cluster has elevated fragment counts, co-expresses markers from two unrelated lineages, and sits geometrically between those two lineages on the UMAP.
  8. Build a contingency table of cluster identity by sample or batch of origin.

    r
    table(Idents(atac), atac$sample)
    Healthy
    Most clusters are represented across most samples, in proportions that make biological sense for your design.
    Red flag
    A cluster is confined almost entirely to one sample with no known biological reason (e.g. a disease-only cell state), more likely a sample-specific technical artifact than a real population.
  9. Resample cells with replacement, re-run the neighbor graph and clustering on each resample, and compute the adjusted Rand index between the bootstrap clustering and your original labels.

    r
    library(bluster)
    # lsi_mat: cells x LSI components (2:50)
    boot <- bootstrapStability(lsi_mat,
                                FUN = function(x) igraph::cluster_louvain(scran::buildSNNGraph(x, transposed = TRUE))$membership,
                                clusters = Idents(atac))
    boot
    Healthy
    High agreement (adjusted Rand index close to 1) between bootstrap runs and the original clustering for the clusters you plan to keep.
    Red flag
    Low, unstable agreement for a specific cluster across bootstrap iterations, its boundary is an artifact of that particular resample, not a robust partition.

What to do about it

Pick the resolution just before the plateau breaks, and stop there

When: The resolution sweep shows a clear plateau in cluster count and marker separation holds up to that point but degrades past it.

Fix your final resolution at the last value inside the plateau where every cluster still has a distinguishable marker/motif signature. Rerun FindClusters at that single resolution and treat it as final for the manuscript or downstream analysis.

Caveat: A plateau can hide a genuinely rare population that only separates at a higher resolution, cross-check any cluster you're about to discard against known rare cell-type markers before settling on the lower resolution.

Drop LSI component 1 and re-filter by depth

When: Cluster boundaries track total fragments or detected peaks, or LSI component 1 wasn't excluded from the dims used for neighbors/clustering.

Rebuild the neighbor graph and UMAP using LSI components 2 through 50 only, and re-check the low end of the fragment-count distribution for cells that should have been filtered out before TF-IDF normalization.

Caveat: Filtering harder on depth will also remove genuinely small, low-input, or quiescent cells, check that the cells you're dropping aren't a real rare population before discarding them.

Merge clusters with no distinguishing markers or motifs

When: Two neighboring clusters fail both the gene-activity marker test and the chromVAR motif test.

Reassign one cluster's identity to the other (Idents(atac)[cells] <- "target_cluster" in Signac, or lower the resolution parameter and refit in ArchR's addClusters) and re-run downstream marker calling on the merged group to confirm it now has a coherent signature.

Caveat: Eyeballing which cluster to merge into can propagate an earlier labeling mistake, re-derive markers for the merged cluster rather than assuming the larger parent's identity is correct.

Remove doublets and re-cluster

When: The doublet screen (high fragment count, dual-lineage marker co-expression, intermediate UMAP position) flags a coherent subset of cells.

Score doublets with ArchR's built-in addDoubletScores (or an equivalent fragment-count-based filter), remove flagged cells, and rerun the full LSI-to-clustering pipeline rather than just relabeling the existing clusters.

Caveat: Aggressive doublet filtering will also delete large, transcriptionally or chromatin-active real cells at the tail of the fragment distribution, sanity-check the removed cell count against your expected multiplet rate for the loading concentration you used.

Call peaks per cluster instead of on the pooled dataset

When: A cluster is unstable across bootstraps and low-silhouette specifically because it represents a rare population whose peaks were washed out by pooled peak calling.

After an initial global clustering pass, call peaks separately within each cluster, rebuild the peak-by-cell matrix from that union of peak sets, and re-cluster.

Caveat: This is compute-heavy and circular by construction, peaks are being defined by clusters that then redefine the clusters, so use it as a refinement step on an already-reasonable clustering, not as your first pass.

When not to "fix" it

Don't force a merge when the split tracks a known biological axis that just happens to correlate loosely with a technical variable, proliferating progenitors with elevated accessibility at cell-cycle genes are a real, separable population, not a depth artifact, even though they may also have somewhat higher fragment counts. Check the marker/motif signature before assuming any depth correlation makes a cluster fake.

Don't collapse a genuine rare population just because it has few cells and looks unstable on a naive bootstrap, rare cell types are exactly the case where pooled peak calling underrepresents the signal, so the fix there is calling peaks per cluster, not deleting the cluster. And don't try to force a continuous differentiation trajectory into discrete, well-separated clusters at all: if gene activity and motif scores shift gradually rather than in steps, that's a continuum, and pseudotime or trajectory methods are the right tool, not another round of resolution tuning aimed at making Louvain draw a hard boundary where biology doesn't have one.

Five things experienced analysts do here

  1. Never report a cluster count without also reporting the resolution sweep that produced it, a single FindClusters() call at a default resolution is not a result, it's a starting point.
  2. Treat Seurat/Signac's default resolution (0.8) as an arbitrary placeholder carried over from scRNA-seq tutorials, not a value with any special meaning for your dataset.
  3. Don't name a cluster in a figure or paper until you can point to a specific gene activity marker or motif that justifies the name, 'cluster 7' isn't a cell type.
  4. Keep a depth-colored UMAP in your QC output for every scATAC-seq project, generated automatically alongside the cluster UMAP, so a depth confound is visible before you've invested time interpreting the clusters.
  5. Use automated resolution-selection tools like clustree or callback to narrow down candidate resolutions, but make the final call by checking markers and motifs yourself, these tools optimize a statistical criterion, not cell-type biology, and will both over- and under-split relative to what's real.

Questions people ask

What resolution should I use for scATAC-seq clustering in Seurat or Signac?

There's no universal number. Run a resolution sweep (e.g. 0.2 through 1.5), plot cluster count against resolution, and pick the value at the plateau where every cluster still has a distinct gene activity marker and chromVAR motif signature. Treat the Signac/Seurat tutorial default of 0.8 as a starting guess, not an answer.

How many clusters should I expect from a 10x scATAC-seq or Multiome experiment?

It depends entirely on what cell types are actually in your sample, there's no target number to hit. Judge your cluster count by whether each cluster has identifiable, reproducible markers and motifs, not by comparing it to a number from another paper or dataset.

Why does silhouette width look bad for all my scATAC-seq clusters?

scATAC-seq peak-by-cell matrices are far sparser and more binary than scRNA-seq gene-expression matrices, so pairwise distances in LSI space are noisier and silhouette widths run lower by default. Compute silhouette on LSI components 2 onward (never component 1, which tracks depth), and compare clusters against each other rather than against a scRNA-seq threshold.

Can I use clustree to pick the best resolution for scATAC-seq data?

Yes, as a way to visualize how cells reshuffle across a resolution range and to narrow down candidates, but don't stop there. Clustree shows you where clusters split and merge; it doesn't tell you whether a split is biological. Follow it up with marker and motif validation on every split it flags.

How do I know if a scATAC-seq cluster is a doublet rather than a real cell type?

Check three things together: elevated fragment count relative to the dataset median, co-expression of gene activity or motif signal from two normally mutually exclusive lineages, and a position on the UMAP that sits between those two lineages. Any one of these alone is weak evidence; all three together is a strong doublet signal.

Related pages

Related reading on the blog

Sources

  1. Clustering, redux — Definitions and code patterns for silhouette width, cluster purity, WCSS, adjusted Rand index, graph modularity, and bootstrap stability used in the checks above
  2. ChromVAR: Inferring transcription factor variation from single-cell epigenomic data — Bias-corrected motif deviations are needed for stable cluster separation; uncorrected chromVAR is confounded by depth and mapping bias
  3. ArchR is a scalable software package for integrative single-cell chromatin accessibility analysis — Iterative LSI and built-in doublet scoring referenced in the doublet-removal fix
  4. Integrating scRNA-seq and scATAC-seq data • Seurat — Signac's TF-IDF plus LSI preprocessing workflow referenced throughout the checks
  5. clustering scATACseq data: the TF-IDF way — Source for dropping LSI component 1 and retaining components 2-50 before clustering
  6. Benchmarking computational methods for single-cell chromatin data analysis — Comparative resolution-parameter testing across scATAC-seq clustering tools

Part of the Overclustering series.