Chatomics Field GuideWhat They Don't Teach You

Sanity check · Spatial Transcriptomics

Why Your UMAP Is Misleading You in Spatial Transcriptomics

UMAP islands, cluster sizes and arcs are optical illusions in spatial data; the tissue coordinates and a permutation test are the only evidence that counts.

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

You ran the standard Visium or Xenium pipeline through Seurat or Scanpy, clustered your spots or cells, and now you're looking at a UMAP with a few clean islands and one cluster stretched into an arc. It reads like a story: two distinct populations, maybe a progression from one state into another. You're about to write that sentence into the report or the figure legend.

That sentence is checkable, and in spatial data it's checkable against the one thing UMAP is not: the actual tissue coordinates sitting one plot away. If a reviewer or a pathologist opens SpatialDimPlot next to your UMAP and finds your two "distinct" islands salt-and-peppered across the same region of tissue, or your "trajectory" arc turns out to track total UMI counts instead of a real biological gradient, the narrative collapses. UMAP is a map for navigating local neighborhoods, not a ruler for measuring biological distance, and treating it as one is a bigger mistake in spatial transcriptomics than in scRNA-seq, because here you have ground-truth x,y coordinates to check it against and most people don't bother.

This page walks through the checks that take under an hour: color the embedding by technical covariates, reproject clusters back onto the histology image, perturb the UMAP parameters to see how much of the layout is arbitrary, and replace any distance or trajectory claim with a spatial statistic computed on real coordinates.

What it looks like when it's happening

  • Two clusters sit as separate islands on the UMAP, but SpatialDimPlot shows their spots interleaved in the same region of tissue.
  • An arc or smear connects two clusters, tempting a 'progression' or 'trajectory' story from a single time-point experiment.
  • Recomputing UMAP with a different min_dist or n_neighbors reshapes which islands look close or far apart, with no change to the underlying counts.
  • A cluster that looks like its own island turns out to be low-UMI spots concentrated at a tissue edge, fold, or tear.
  • The same cell type splits into two UMAP islands that correspond exactly to two different tissue sections or capture areas.
  • Spatially adjacent spots that touch on the slide land in different UMAP clusters, while spots on opposite ends of the tissue land in the same cluster.
  • Cluster size on the UMAP plot doesn't track spot or cell counts: a tiny population looks visually dominant, or vice versa.

Why it happens

UMAP builds a k-nearest-neighbor graph in high-dimensional expression space and optimizes a 2D layout to preserve those local neighbor relationships. It was never optimized to preserve global geometry. Island separation, island size, and the direction a cluster smears in are byproducts of the layout algorithm's local optimization, not measurements of transcriptional distance. Two clusters can sit far apart on the plot while being nearly identical in expression space, and two clusters can sit close together while being very different, the embedding doesn't guarantee either.

Spatial data compounds this. A Visium spot is 55 micrometers across and aggregates transcripts from one to ten cells, so a "spot cluster" on UMAP is already a mixture signature, not a single cell type, clustering that mixture and then reading spatial meaning off UMAP geometry stacks one abstraction on top of another. Spatial neighborhoods, by contrast, are defined by a connectivity matrix built from the real coordinates on the slide: two spots are neighbors because they are physically adjacent, full stop. That adjacency graph and the expression kNN graph UMAP is built from are different objects computed from different information, so agreement between a UMAP cluster map and the tissue's actual anatomy is not guaranteed, and disagreement between them isn't automatically a bug, it's a signal you need to check which one is right for the claim you're making.

Sequencing depth heterogeneity is the most common technical driver hiding behind a convincing-looking UMAP. Depth differences across spots, sections, or batches dominate the top principal components if not properly normalized, and PCA is what UMAP's neighbor graph is built on. Spots at tissue edges, folds, or dry patches systematically have lower UMI counts, and cells or spots with high depth cluster together in PCA/UMAP purely by library size. The result is an island that looks like a distinct population but is really a QC gradient.

Finally, resist reading a trajectory off an arc-shaped UMAP. A spatial transcriptomics assay, like scRNA-seq, is a single time-point snapshot. Gene expression is dynamic on timescales of minutes to hours; one snapshot cannot by itself reconstruct a real developmental or progression trajectory, no matter how smooth the arc looks in the embedding.

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. Before interpreting cluster shape, replot the same embedding colored by total_counts, n_genes_by_counts, and pct_counts_mt alongside the cluster labels.

    python
    sc.pl.umap(adata, color=['leiden', 'total_counts', 'pct_counts_mt'], ncols=3)
    Healthy
    Depth and mito% vary smoothly across the plot and don't line up with cluster boundaries; islands correspond to real expression differences.
    Red flag
    One or more islands light up uniformly high or low on total_counts, that cluster is a depth artifact, not a cell state.
  2. Group the obs table by cluster and look at the distribution of total_counts, n_genes, and pct_counts_mt within each cluster.

    python
    adata.obs.groupby('leiden')[['total_counts', 'n_genes_by_counts', 'pct_counts_mt']].describe()
    Healthy
    Median total_counts is similar across clusters and falls inside your QC window (roughly 5,000-35,000 for many Visium datasets).
    Red flag
    One cluster's median total_counts sits far outside the rest, or its pct_counts_mt sits right at your filter threshold, that's likely low-quality spots, not a biological population.
  3. If your object has more than one section, slide, or capture area, overlay sample_id or section on the same embedding used for clustering.

    python
    sc.pl.umap(adata, color=['leiden', 'sample_id'])
    Healthy
    Clusters mix across sections; the same cell type appears in every section that contains it.
    Red flag
    Clusters split cleanly along section boundaries, you're clustering the batch/section effect, not biology, and it needs correcting before the layout means anything.
  4. Plot the same cluster labels used for the UMAP onto the histology image with SpatialDimPlot (Seurat) or sc.pl.spatial / squidpy (Scanpy), and look at where each cluster physically sits on the section.

    r
    SpatialDimPlot(seurat_obj, group.by = "seurat_clusters")
    DimPlot(seurat_obj, reduction = "umap", group.by = "seurat_clusters")
    Healthy
    Clusters that look separated on UMAP occupy visibly distinct anatomical regions, tumor core vs. stroma vs. normal epithelium, for example.
    Red flag
    Two UMAP islands are salt-and-pepper mixed across the same tissue region, or a cluster is scattered as isolated spots along a tissue edge, the UMAP split isn't a spatial or biological one.
  5. Rerun the embedding on the same PCA coordinates with at least two or three parameter combinations away from the defaults (n_neighbors=15, min_dist=0.1) and compare island positions, gaps, and relative sizes across runs.

    python
    import umap
    for n_neighbors, min_dist in [(15, 0.1), (30, 0.3), (5, 0.01)]:
        reducer = umap.UMAP(n_neighbors=n_neighbors, min_dist=min_dist)
        embedding = reducer.fit_transform(adata.obsm['X_pca'])
    Healthy
    The same cells or spots group together across runs, even though the exact distances, shapes, and gaps between islands shift.
    Red flag
    Which cluster looks 'closer' or 'bigger' flips between parameter settings, that arrangement was never evidence of anything, it's a layout artifact.
  6. Build the spatial neighbor graph from the actual slide coordinates and run a permutation-based enrichment test for which clusters actually co-occur in tissue.

    python
    sq.gr.spatial_neighbors(adata, n_neighs=6, coord_type='grid')
    sq.gr.nhood_enrichment(adata, cluster_key='leiden')
    Healthy
    The z-scores from the permutation test either confirm or contradict the 'these clusters look close/far' impression from UMAP, report this number, not the plot.
    Red flag
    UMAP shows two clusters as neighboring islands but the nhood_enrichment z-score is near zero or negative, there's no real spatial association, and the UMAP layout misled you.
  7. For any cluster arrangement you're tempted to call a trajectory, inspect the genes driving that axis (PC or UMAP-adjacent loadings) and ask whether they are known progression markers or just depth/quality-correlated genes, and remember the data is a single time point.

    Healthy
    The genes driving the axis are recognized differentiation or progression markers, and you have supporting evidence beyond one snapshot, independent markers, time points, or lineage information.
    Red flag
    The genes loading heaviest on the 'trajectory' axis are ribosomal, mitochondrial, or otherwise depth-correlated, the arc is a QC gradient dressed up as biology.

What to do about it

Renormalize before re-clustering when depth drives the split

When: Check 1 or 2 shows a cluster or island tracking total_counts or pct_counts_mt rather than a distinct expression program.

Confirm normalization was applied (sc.pp.normalize_total with a fixed target_sum followed by log1p, or SCTransform in Seurat), then recompute PCA, neighbors, and UMAP, and rerun the depth-coloring check.

Caveat: Don't over-normalize: some real biology (necrotic vs. viable tissue, low-RNA cell types) genuinely has lower total counts, and aggressive correction can erase that difference along with the artifact.

Report spatial statistics instead of UMAP distance for colocalization claims

When: You want to claim two cell types or clusters are near each other, exclude each other, or form a niche.

Compute the neighborhood enrichment z-score with squidpy on the real coordinate-based spatial graph and cite that number, not the UMAP layout.

Caveat: Permutation-based enrichment needs enough spots per cluster to be stable; very small or rare clusters will give noisy z-scores that shouldn't be over-interpreted either.

Drop trajectory language unless you ran a trajectory method and can defend it

When: An arc or smear on the UMAP tempts you to describe a progression or developmental path.

State plainly that the assay is a single time-point snapshot; if pseudotime is actually needed, run a dedicated trajectory tool and validate the ordering against known markers, not the raw UMAP shape.

Caveat: Even dedicated trajectory methods can't establish real time direction without extra information, RNA velocity, an actual time course, or lineage tracing, so the caveat about a single snapshot doesn't fully go away.

Filter and re-inspect suspected tissue-artifact clusters

When: Check 2 or 4 shows a cluster concentrated at tissue edges, folds, or tears with abnormal QC values.

Apply QC thresholds (e.g., total_counts and pct_counts_mt filters appropriate to your tissue) to remove the flagged spots, then rerun clustering and confirm the artifact cluster disappears rather than just shrinking.

Caveat: Fixed QC thresholds don't transfer across tissue types or platforms; tune them per dataset, and check you're not deleting a real, thin anatomical region along with the artifact.

Make the spatial plot the primary figure, UMAP the supplementary one

When: Any figure or claim in a report or manuscript leans on UMAP proximity, island size, or shape as evidence.

Pair every UMAP cluster panel with the same clusters shown on SpatialDimPlot or sc.pl.spatial, and write the interpretive sentence from the spatial panel, treating the UMAP as a navigation aid only.

Caveat: This prevents overclaiming but doesn't fix an underlying artifact, still run the QC and depth checks above first.

When not to "fix" it

If clusters that look visually separate on the UMAP are confirmed by marker genes to be genuinely distinct cell types or states, and those same clusters map cleanly onto anatomically distinct regions in SpatialDimPlot, the separation isn't an artifact to correct, it's the expected picture of real, spatially validated biology. Forcing those clusters back together or over-integrating would erase a distinction the tissue image already confirms. Likewise, if you're only using the UMAP interactively to page through clusters and pick genes to check, without putting a distance, size, or trajectory claim into a report, there's nothing here that needs fixing at all.

Five things experienced analysts do here

  1. Always open the UMAP and the SpatialDimPlot side by side for the same cluster labels; the tissue plot with real coordinates is the ground truth, the UMAP is the index.
  2. Never report a distance or 'closeness' number read off UMAP coordinates; if you need a quantitative similarity, use PCA/expression distance or a squidpy spatial statistic instead.
  3. Refit the UMAP with two or three different min_dist/n_neighbors combinations before trusting a cluster arrangement destined for a figure, if the story changes, it wasn't real.
  4. Make coloring the UMAP by technical covariates (total_counts, sample ID, pct_counts_mt) a reflex you do before coloring by cell type, so depth and batch artifacts get caught early.
  5. Don't call a UMAP shape a 'trajectory' unless you've run an actual trajectory or velocity method and can defend the ordering against the single-snapshot limitation of the assay.

Questions people ask

Can I interpret distances between clusters on a UMAP plot?

No. UMAP preserves local neighborhoods but distorts global geometry, so the distance between two islands doesn't reflect biological or transcriptional distance. Use PCA distances, expression correlation, or a dedicated statistical test if you need a quantitative comparison between clusters.

Why don't my UMAP clusters match the tissue image?

UMAP clusters come from a neighbor graph built in expression space, while SpatialDimPlot shows real tissue coordinates; these are two different graphs built from different information, so they aren't guaranteed to agree. When they disagree, trust the spatial plot for claims about tissue organization and treat the UMAP as a navigation tool.

Should I use UMAP or t-SNE for spatial transcriptomics?

Both are nonlinear embeddings with the same core limitation: they preserve local structure and distort global distances, so the choice between them doesn't solve the misreading problem. Pick whichever your pipeline already supports for browsing clusters, and rely on the tissue coordinates and spatial statistics for actual conclusions.

Can I infer a developmental trajectory from a UMAP in spatial data?

Not reliably from the UMAP shape alone. Spatial transcriptomics, like scRNA-seq, captures a single time-point snapshot, and an arc-shaped embedding can just as easily reflect a sequencing-depth gradient as a real progression. If you need pseudotime, use a dedicated trajectory method and validate it against known markers.

What UMAP parameters should I use for spatial transcriptomics data?

There's no spatial-specific default backed by the primary documentation; start from UMAP's general defaults (n_neighbors=15, min_dist=0.1) and treat any single parameter setting as provisional. The more useful habit is rerunning the embedding with a few different settings and checking whether your conclusions survive, rather than searching for one 'correct' parameter pair.

Related pages

Related reading on the blog

Sources

  1. UMAP: Uniform Manifold Approximation and Projection, Basic UMAP Parameters — Default n_neighbors=15, min_dist=0.1 and what perturbing them does to the layout.
  2. Load10X_Spatial, Seurat — Spatial object structure and SpatialDimPlot for projecting clusters onto tissue.
  3. Spatial Data Analysis, Scanpy — QC filtering thresholds and normalization before clustering/UMAP.
  4. Neighbors enrichment analysis, Squidpy — Permutation-based spatial neighborhood enrichment as the alternative to UMAP proximity.
  5. SpatialMap: Spatial Mapping of Unmeasured Gene Expression Profiles in Spatial Transcriptomic Data Using Generalized Linear Spatial Models — Visium spot size (55 micrometers) and 1-10 cells per spot.
  6. SpatialArtifacts: a computational framework for tissue artifact detection in spatial transcriptomics data — Tissue artifacts (dry patches, tears) manifesting as low-UMI regions at tissue borders.

Part of the UMAP misreading series.