Chatomics Field GuideWhat They Don't Teach You

Sanity check · Single-Cell ATAC-seq

Why Your UMAP Is Misleading You in Single-Cell ATAC-seq

The islands, the gaps between them, and the smear you're calling a trajectory are navigation aids, not measurements, so treat them that way before you write the discussion section.

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

You extended an scRNA-seq project to the 10x ATAC or Multiome kit, ran the standard TF-IDF, LSI, and UMAP pipeline because it looked like the Seurat workflow you already knew, and got a plot with a smooth gradient that reads exactly like a differentiation trajectory. Or you got two islands sitting far apart and started drafting a sentence about how distinct those two populations are. Neither read is safe by default in scATAC-seq, and this is the point in the analysis where a wrong call quietly becomes a wrong conclusion in a figure legend.

The stakes are higher here than in scRNA-seq because the scATAC-seq matrix is sparser to begin with: a few thousand Tn5 fragments per cell against a peak-by-cell matrix that's mostly zeros. TF-IDF plus LSI does not remove sequencing depth as cleanly as log-normalization plus PCA removes it in RNA data, so the first thing UMAP has to work with is often still dominated by how many fragments a cell happened to get, not what kind of cell it is.

In the next hour you can run the checks below on data you already have: confirm which LSI components are depth-contaminated, color your existing UMAP by depth and QC metrics to see if your "trajectory" is really a library-size gradient, and replace any distance or size claim you were about to make with a number computed in the high-dimensional space instead of read off the 2D picture.

What it looks like when it's happening

  • A UMAP shows a smooth, continuous gradient across a region that lines up with the color bar for total fragment count per cell, not with any known marker or motif.
  • Two islands sit far apart on the plot, but when you check average pairwise distance in the LSI space you clustered on, they're no farther apart than sub-populations within a single island.
  • A cluster looks large and diffuse on screen but contains fewer actual cells (by `table(Idents(object))`) than a small, tight island right next to it.
  • Re-running `RunUMAP` with a different `min.dist` or `n.neighbors` changes which clusters appear to touch or split apart, even though Louvain/Leiden clustering on the underlying LSI graph gives the same cluster membership.
  • Coloring the UMAP by FRiP (fraction of reads in peaks) or TSS enrichment score reproduces the exact same gradient shape you had just called a lineage trajectory.
  • `DepthCor()` reports |correlation| above roughly 0.5 between total fragment counts and an LSI component you're actively using for clustering or UMAP.
  • Switching the UMAP random seed rearranges the relative position of clusters (which ones are 'close') without changing which cells belong to which cluster.

Why it happens

TF-IDF plus LSI is the standard substitute for log-normalization plus PCA in scATAC-seq, and it exists because the peak-by-cell matrix is binarized and far sparser than a gene-by-cell matrix: a few thousand Tn5 fragments per cell means most peaks are 0 for most cells. TF-IDF weights each peak by how specific it is across cells and each cell by its total signal, and SVD then decomposes that weighted matrix into components. In practice, even after this weighting, sequencing depth remains the largest source of between-cell variation, because a cell with more fragments has a higher chance of hitting any given peak at all, independent of cell identity. That pushes the first singular vector, LSI component 1, to track total fragment count almost every time, which is exactly why Signac's own workflows default to dims = 2:30, explicitly skipping it, and why DepthCor() exists as a dedicated function to check for this before you go further.

Dropping component 1 helps, but it does not fully solve the depth problem, and it does nothing about a second, separate issue: how UMAP itself distorts what's left. UMAP is built to preserve local neighborhoods, meaning cells that are close to each other in the high-dimensional LSI space stay close in the 2D layout. It is not built to preserve global distances, cluster density, or relative area. The optimization that produces the 2D coordinates stretches sparse, spread-out neighborhoods over more visual space and compresses dense ones, so a cluster's footprint on the page is a function of local density in the embedding, not of how many cells it contains or how biologically distinct it is.

Put those two mechanisms together and you get the two most common misreadings in scATAC-seq specifically. First, a gradient that looks like a differentiation trajectory is very often just residual depth variation that survived TF-IDF normalization and leaked into whichever components you kept, because scATAC-seq's TF-IDF is measurably less effective at removing library-size effects than log-normalization is for RNA counts. Second, island size and inter-island distance get read as if they were measurements of abundance or biological divergence, when they are artifacts of the embedding's local density-preserving objective, not properties of the underlying peak-by-cell matrix. Neither failure requires anything unusual about your data; it's the expected behavior of the pipeline unless you check for it.

This is compounded by the fact that different tools (Signac, ArchR, CellRanger-ATAC) produce visibly different clustering results from the same data even with the same nominal normalization method, because implementation details of TF-IDF, feature selection, and SVD differ. That means a UMAP shape you get from one pipeline is not even guaranteed to reproduce in another, which is one more reason to treat the picture as a tool-specific rendering, not a fixed biological fact about your cells.

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

  1. After RunTFIDF(), FindTopFeatures() and RunSVD(), call Signac's DepthCor() on the object to correlate every LSI component against total fragment counts per cell. Do this before touching UMAP, not after you already have a plot you like.

    r
    atac <- RunTFIDF(atac)
    atac <- FindTopFeatures(atac, min.cutoff = 'q0')
    atac <- RunSVD(atac)
    DepthCor(atac)
    Healthy
    Component 1 shows strong correlation with depth (this is expected and why it gets dropped); components 2 through the range you plan to use for clustering/UMAP show |r| well below ~0.1-0.2.
    Red flag
    Any component you are actually using downstream (say, component 2 or 3, or whichever axis produces the gradient you're excited about) shows |r| > 0.5 with total fragment counts.
  2. Grep your own analysis script for RunUMAP, FindNeighbors, and RunSVD/RunPCA calls and check the dims or reduction.name argument matches what DepthCor() told you to use.

    r
    atac <- RunUMAP(atac, reduction = 'lsi', dims = 2:30)
    atac <- FindNeighbors(atac, reduction = 'lsi', dims = 2:30)
    Healthy
    `dims` starts at 2 (or later, if DepthCor flagged more than one component), matching the range you validated as depth-clean.
    Red flag
    You copy-pasted a scRNA-seq PCA template and left `dims = 1:30`, so component 1 is quietly driving your clustering and UMAP.
  3. Plot the identical UMAP embedding colored by nCount_peaks (or your assay's fragment count column), TSS.enrichment, and fraction of reads in peaks (FRiP), side by side with your celltype or pseudotime coloring.

    r
    FeaturePlot(atac, features = c('nCount_peaks', 'TSS.enrichment'), reduction = 'umap')
    Healthy
    No visible gradient tracking these QC metrics along the axis you're calling biological (e.g., a trajectory or a cluster split).
    Red flag
    The 'differentiation trajectory' or cluster boundary you were about to write up reproduces the exact shape of the depth or FRiP gradient.
  4. Pull the real number of cells per cluster from metadata and compare it to how much area each island occupies visually. UMAP spreads sparse, spread-out cells over more 2D area regardless of how many cells are actually in the group.

    r
    table(Idents(atac))
    Healthy
    You use this table, not the size of the blob on screen, whenever you report or imply relative abundance of a population.
    Red flag
    A visually large, diffuse island turns out to hold fewer cells than a small tight island next to it, and you had assumed the opposite from the plot.
  5. Compute Euclidean distance between cluster centroids in the LSI space you actually clustered on (the depth-excluded dims), and run a differential accessibility test between the two pairs you're comparing visually.

    r
    lsi_emb <- Embeddings(atac, 'lsi')[, 2:30]
    centroids <- aggregate(lsi_emb, by = list(Idents(atac)), FUN = mean)
    dist(centroids[, -1])
    
    markers <- FindMarkers(atac, ident.1 = 'clusterA', ident.2 = 'clusterB')
    Healthy
    High-dimensional distance and number/effect size of significant DA peaks roughly track what the UMAP suggested.
    Red flag
    Two clusters that look far apart in 2D have LSI distance no larger than the spread within a single cluster, or DA testing returns almost nothing significant.
  6. Re-run RunUMAP with a couple of different min.dist, n.neighbors, and seed.use values. Compare the Louvain/Leiden cluster assignments (computed on the neighbor graph, not the 2D layout) across runs, not just the picture.

    r
    atac <- RunUMAP(atac, reduction = 'lsi', dims = 2:30, min.dist = 0.1, n.neighbors = 15)
    atac <- RunUMAP(atac, reduction = 'lsi', dims = 2:30, min.dist = 0.5, n.neighbors = 50, seed.use = 123)
    Healthy
    Cluster membership from graph-based clustering is stable across the sweep; only cosmetic layout details (spacing, rotation) change.
    Red flag
    Changing `min.dist` or the random seed rearranges which islands sit next to each other, or makes a gradient you called a trajectory appear or disappear.
  7. If you're calling an arc or smear a differentiation trajectory, check known lineage marker gene activity scores (via GeneActivity() in Signac, or chromVAR motif deviations) ordered along a pseudotime computed from the high-dimensional LSI space, not read off the 2D arc by eye.

    Healthy
    A monotonic, biologically sensible marker trend along the ordering that holds up regardless of how the UMAP happens to be laid out that day.
    Red flag
    The only evidence for the trajectory is the visual arc itself, and the marker trend, when you actually check it, tracks total fragment count decreasing along the same arc instead of any known lineage gene.

What to do about it

Drop the depth-correlated LSI component from every downstream call

When: `DepthCor()` shows a component you were using (usually component 1, sometimes more in shallow libraries) correlated with total fragment counts.

Set dims = 2:N (or whatever range clears depth correlation) consistently in RunSVD, FindNeighbors, and RunUMAP. Re-run DepthCor() on the new range to confirm it actually cleared, don't assume dropping component 1 alone is always enough.

Caveat: If you already interpreted plots or wrote conclusions using the uncorrected embedding, you need to redo that analysis, not just relabel the axes.

Replace visual distance claims with a computed number

When: You're about to write or say that one cluster is 'closer to' or 'farther from' another based on the UMAP picture.

Compute centroid distance in the LSI space you clustered on, and pair it with a differential accessibility test (FindMarkers or equivalent) between the groups you're comparing. Report the distance and the DA result, not the plot.

Caveat: Small clusters have low power for DA testing, so pair the test with marker gene activity review rather than trusting a p-value alone.

Read abundance from the cell count table, not blob size

When: You need to report or reason about how common a population is.

Pull table(Idents(object)) or the equivalent metadata count, and use that number in any figure legend or text claiming relative abundance.

Caveat: Watch for doublets or ambient signal inflating counts in small clusters; a count table fixes the UMAP-area problem but doesn't fix contamination.

Validate a suspected trajectory with markers, independent of the 2D arc

When: You see a smear or arc shape and want to call it a differentiation or lineage trajectory.

Order cells by a pseudotime computed on the high-dimensional LSI embedding (not the 2D UMAP coordinates), then check known lineage marker gene activity scores or motif deviation scores along that ordering for a monotonic, biologically sensible trend.

Caveat: Trajectory inference methods have their own assumptions (a continuous process, a defined start), so this is stronger evidence than eyeballing the plot but still needs an orthogonal marker check, not proof on its own.

Report and sweep UMAP parameters before trusting a qualitative shape

When: A specific gap, branch, or arc in the UMAP is central to your interpretation.

Re-run RunUMAP with at least two different min.dist/n.neighbors values and a different seed, and confirm graph-based cluster membership is stable across the sweep. State the parameters you used in methods.

Caveat: Costs extra compute time re-clustering multiple times, but it's cheap compared to publishing a shape that turns out to be a UMAP artifact.

Don't compare UMAP layouts across samples or tools directly

When: You're tempted to say two datasets or two tools (Signac vs ArchR) 'look similar' because their UMAPs look alike.

Project both datasets into a shared feature space (a common or union peak set) and compare in that space, or use an integration diagnostic, rather than eyeballing two separately-computed 2D layouts.

Caveat: Requires re-calling or harmonizing peaks across the datasets, which is more setup than just screenshotting two plots side by side.

When not to "fix" it

If your only use of the UMAP is to pick out which cells belong to which island so you can pull them for a downstream step, like per-cluster peak calling to recover a rare cell type, you're using UMAP for exactly what it's good at: navigation. You don't need a DepthCor sweep or a trajectory validation in that case. Just confirm cluster membership is stable across a couple of clustering resolutions or a small parameter sweep, then move on. The problem described on this page is specifically about treating distance, size, or shape as evidence, not about using the plot to select cells.

Five things experienced analysts do here

  1. Make `DepthCor()` (or an equivalent depth-correlation check) a mandatory step right after `RunSVD()` in your template, before `RunUMAP` ever runs, so depth contamination gets caught before it shapes a plot you'll get attached to.
  2. Keep a depth/QC-colored UMAP (nCount, FRiP, TSS enrichment) pinned next to every celltype or trajectory UMAP in your slides, so anyone in the room can check for themselves whether the pattern tracks depth instead of biology.
  3. Never write 'cluster A is closer to B than to C' in a manuscript or Slack message without a computed high-dimensional distance or a differential accessibility result behind it; treat that sentence as a claim that needs a number attached.
  4. Re-run UMAP with at least two seeds and a min_dist/n_neighbors sweep before trusting any qualitative shape, arc, branch, or gap, that you're about to interpret biologically.
  5. Remember that a genuine rare cell type with naturally low fragment counts looks identical on a UMAP to a broken low-quality cell; don't filter or merge a cluster based on how sparse or diffuse it looks in 2D, check its depth relative to the rest of the data and its marker gene activity first.

Questions people ask

Can I trust distances between clusters in a scATAC-seq UMAP?

No. UMAP is built to preserve local neighbor relationships, not global distances, so a gap or a close pairing between islands in 2D does not reliably reflect anything in the high-dimensional LSI space you clustered on. If you need to claim two populations are more or less related, compute the distance in LSI space and back it with a differential accessibility test, then report that number instead of the picture.

Should I use UMAP or t-SNE for scATAC-seq?

Both share the same limitation described here: neither preserves global geometry, so switching between them does not fix the misreading problem. UMAP is the practical default in Signac and ArchR because it scales better and its neighbor graph doubles as the graph used for clustering, not because it gives you more trustworthy distances than t-SNE.

Why does my scATAC-seq UMAP look like a continuous gradient instead of separated clusters?

Check sequencing depth first. Color the same UMAP by total fragment count or FRiP score; if the gradient reproduces that coloring, you're looking at a depth artifact surviving in your LSI components, not a differentiation trajectory. This is a well documented failure mode tied to the first LSI component in scATAC-seq.

How many LSI dimensions should I use for UMAP in scATAC-seq?

Signac's published workflows drop the first LSI component and use dims 2:30, because component 1 is almost always dominated by sequencing depth rather than biology. Confirm this on your own data with DepthCor() instead of assuming the default range is correct, since how many components stay depth-clean depends on your library depth and cell numbers.

Can I infer a differentiation trajectory directly from a UMAP shape in scATAC-seq?

Not from the shape alone. An arc or smear on a UMAP is consistent with a trajectory but equally consistent with a depth gradient or a UMAP parameter artifact, so back it up with lineage marker gene activity scores or motif deviations ordered independently of the 2D layout before calling it a trajectory.

Related pages

Related reading on the blog

Sources

  1. Analyzing adult mouse brain scATAC-seq — Source of the DepthCor() function and the dims = 2:30 convention that excludes the depth-correlated first LSI component.
  2. ArchR's LSI Implementation — Explains ArchR's depth normalization to a constant 10,000 and why depth still tends to dominate the first LSI component.
  3. Single-cell Embeddings in ArchR — States that UMAP inter-cluster distances are not reliable and that UMAP parameters strongly affect the resulting embedding.
  4. Clustering scATACseq data: the TF-IDF way — Walkthrough of the TF-IDF formula, binarization, and peak selection used before LSI, and note that the first PC often tracks depth.
  5. Sequence depth issues in LSI, LSA, LDA, PCA — Community discussion documenting that multiple dimensionality-reduction methods struggle to separate technical depth from genuine low-depth rare populations.

Part of the UMAP misreading series.