Chatomics Field GuideWhat They Don't Teach You

Sanity check · Spatial Transcriptomics

How to Detect Integration Over-Correction in Spatial Transcriptomics

The UMAP where every condition mixes perfectly isn't always good news, sometimes it means Harmony erased the exact difference you set out to measure.

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

You merged Visium or Xenium sections from tumor and adjacent normal tissue, or from high-grade and low-grade regions, ran Harmony or Seurat's CCA integration, and plotted UMAP colored by condition. Everything mixed beautifully. Every cell type lines up across sections. It looks like the integration worked exactly as advertised.

The problem is that integration methods don't know the difference between a batch effect and the biological difference your experiment was designed to detect. They're built to pull matching cell types together across samples, and they will do that whether the mismatch was caused by tissue processing or by real disease biology. If the population you actually care about, a disease-specific state, a tumor-grade-specific domain, only exists in one condition, integration will often smear it onto the closest healthy or low-grade analog, and you'll report "no difference" when the difference was there in your raw data the whole time.

This page walks through the checks to run in the next hour: compare per-sample and integrated embeddings side by side, look at whether marker genes and spatial domains survive integration, and quantify mixing with CellMixS or a more conservative method. It ends with concrete fixes and the specific case where you should not touch the correction at all.

What it looks like when it's happening

  • UMAP colored by condition shows every group perfectly overlapping after integration, when the same plot before integration showed a clearly separate cluster for one condition.
  • FindConservedMarkers reports your expected condition-specific genes as flatly conserved with no fold-change asymmetry between groups.
  • A tumor-grade or disease-specific spatial domain that was spatially coherent on the tissue image before integration is scrambled or merged into a shared domain afterward.
  • Marker gene FeaturePlots split by condition show near-identical expression levels post-integration, despite IHC or prior literature saying the marker should differ.
  • CellMixS ldfDiff shows large negative values concentrated specifically in the population you were studying, not spread evenly across the dataset.
  • DE analysis on the integrated object returns almost no significant condition-specific genes within a cell type, despite an obvious effect in per-sample analysis.
  • Different integration methods (Harmony vs. CCA vs. RPCA) give wildly different amounts of mixing for the same population, with the more aggressive method erasing it entirely.

Why it happens

Integration methods like Harmony, Seurat's CCA/RPCA anchors, and scVI work by finding cells or spots across samples that look similar in expression space and pulling their embeddings toward each other. That's the entire mechanism: identify shared structure, remove the sample-of-origin signal around it. The method has no way to distinguish "this separation is a batch effect from different tissue processing" from "this separation is a disease-specific cell state that genuinely doesn't exist in the healthy sample." Both look like the same thing to the algorithm, a population that's offset from its nearest neighbor in a different sample.

In spatial transcriptomics this gets worse for two reasons. First, Visium spots mix several cells, so a condition-specific shift in a minority cell population within a spot gets diluted before integration even runs, making it easier for the correction step to fold it into the majority signal. Second, spatial data carries a neighborhood structure that standard scRNA-seq-style integration methods ignore entirely, they treat each spot or cell as an independent row in a matrix, so any correction that reshuffles the embedding can scramble spatial domain coherence along with sample identity.

Sequencing depth compounds the problem. If one condition or section systematically has higher UMI counts or more detected genes per spot, that depth difference shows up as a strong axis in PCA before integration ever runs. Integration will happily "correct" that axis, and because depth differences often correlate with condition (different sections processed on different days, different RNA quality), removing the depth axis can look like removing the condition effect, especially if you never checked whether the two were confounded.

The result is a bias with no warning sign in the output. Harmony and Seurat integration don't report "we deleted 40% of your condition effect." They report a clean UMAP and a lower batch mixing score, both of which look like success. The only way to catch overcorrection is to have a baseline from before integration and to specifically check the population your experiment cares about, not just overall mixing quality.

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. Before running Harmony, CCA, or any batch-correction step, cluster and plot each section or sample separately (or color a merged, uncorrected UMAP by sample and by condition). Look at whether the population you care about, the disease spots, the high-grade region, the treated section, forms a distinct group in the uncorrected data.

    r
    DimPlot(merged_obj, reduction = "pca", group.by = "condition")
    DimPlot(merged_obj, reduction = "pca", group.by = "sample")
    Healthy
    A condition-specific or grade-specific cluster is visible even without correction, giving you a baseline to check integration against.
    Red flag
    You skip this step and go straight to integration, so you have no baseline to know what integration erased.
  2. Run the same DimPlot call on the uncorrected embedding and on the integrated embedding, splitting by condition or batch. Put the two plots next to each other.

    r
    DimPlot(obj, reduction = "pca", group.by = "condition", split.by = "batch")
    DimPlot(obj, reduction = "harmony", group.by = "condition", split.by = "batch")
    Healthy
    Cells of the same cell type from different batches land near each other, but a condition-specific population that was separate before integration still forms its own region after integration.
    Red flag
    The condition-specific cluster that was obvious in the uncorrected plot is gone in the integrated plot, and cells from disease and healthy sections now overlap completely.
  3. Color the pre- and post-integration UMAP by nCount and nFeature (total UMIs and detected genes per spot or cell). If a condition or batch has systematically different depth, this tells you whether the axis integration removed was depth, not biology.

    r
    FeaturePlot(obj, features = c("nCount_Spatial", "nFeature_Spatial"))
    Healthy
    Depth is roughly uniform across the axis that separates conditions, so the separation you saw was not a library-size artifact.
    Red flag
    The condition axis in the uncorrected embedding lines up with a clean depth gradient, in that case the pre-integration split you were worried about losing may itself have been a depth artifact, not real biology.
  4. Pick 2 to 4 marker genes you already expect to differ between conditions (tumor markers, activation markers, whatever motivated the experiment). Plot them split by condition on the integrated object.

    r
    FeaturePlot(obj, features = c("marker_gene_1", "marker_gene_2"), split.by = "condition")
    Healthy
    Expression of the marker is still visibly higher in the condition where you expect it, within the shared cell type cluster.
    Red flag
    The marker gene is expressed at similar levels across conditions after integration when you know from independent evidence (IHC, prior literature, the uncorrected data) that it should differ.
  5. Run FindConservedMarkers on the cell type or spot cluster of interest, grouped by condition. This is meant to confirm markers that should be shared are shared, but also check the per-condition marker lists it reports internally for genes that should differ and don't.

    r
    FindConservedMarkers(obj, ident.1 = "celltype_A", grouping.var = "condition")
    Healthy
    Canonical cell-type markers show up as conserved; condition-specific genes you expect to differ are absent from the conserved list or show large per-condition fold-change asymmetry.
    Red flag
    Genes you know are condition-specific from prior biology show up as flatly conserved across condition with no asymmetry, integration has smoothed them into agreement.
  6. Run CellMixS on the integrated embedding to get a per-cell Cell-specific Mixing Score and local density difference (ldfDiff). Compare the distribution for cells inside your population of interest against the rest.

    Healthy
    Most cell types show high CMS (good batch mixing) while ldfDiff stays near zero broadly, but the population you care about should still show some structure if it is a genuine condition effect rather than a batch artifact.
    Red flag
    ldfDiff shows large negative values concentrated exactly in the population you were studying, meaning local density was aggressively smoothed there specifically, a sign of overcorrection rather than uniform batch cleanup.
  7. Repeat the integration with RPCA instead of CCA, or with a lower Harmony theta/fewer iterations, using Seurat v5's IntegrateLayers with a different method argument. Compare the resulting UMAP and marker plots to the CCA/Harmony run.

    r
    obj <- IntegrateLayers(obj, method = RPCAIntegration, new.reduction = "rpca")
    DimPlot(obj, reduction = "rpca", group.by = "condition")
    Healthy
    The condition-specific population is still visible under RPCA, and the choice of method changes how much structure survives, telling you the effect is real and method-sensitive, not an artifact of one algorithm.
    Red flag
    Every method you try, aggressive or conservative, erases the same population, that is stronger evidence the populations genuinely share the same underlying state and your original per-sample separation was a batch effect, not biology.
  8. Compare spatial domain assignments (via clustering with spatial smoothing, or a spatial-aware method like PRECAST or GraphST) before and after standard scRNA-seq-style integration. Overlay domain labels back onto the tissue image.

    Healthy
    Spatial domains that correspond to known anatomical or histological regions remain spatially coherent and distinguishable by condition after integration.
    Red flag
    Domains that were spatially coherent and condition-specific in the uncorrected analysis become spatially scrambled or merge into a single domain that ignores tissue architecture after standard (non-spatial-aware) integration.

What to do about it

Drop integration for the comparison, keep it only for browsing

When: Your actual research question is about a condition-specific, disease-specific, or grade-specific population, the thing integration is built to erase.

Cluster and analyze each section or sample on its own uncorrected embedding for the comparison that matters (DE, cell-state calling, proportion tests). Use an integrated UMAP only as a navigation aid for annotating shared cell types, never as the input to the test that answers your biological question.

Caveat: Per-sample clustering reintroduces true batch and depth effects, so you still need to check that the effect you're calling biological isn't just a depth or protocol difference between sections (see the depth check above).

Switch to a more conservative integration method

When: You need some cross-section alignment for shared cell types but suspect Harmony or CCA is being too aggressive.

Use Seurat v5's IntegrateLayers with RPCAIntegration instead of CCAIntegration, or reduce Harmony's correction strength. Compare marker expression and cluster structure against the more aggressive method before committing.

Caveat: RPCA leaves more residual batch structure by design, so downstream shared-cell-type annotation across very different platforms or protocols may be messier.

Integrate only the shared reference population, project the rest

When: You know in advance which cell types or spot classes are shared across conditions and which are disease-specific or novel.

Subset to the presumed-shared cell types, compute integration anchors and correction on that subset only, then map the full dataset (including the condition-specific population) onto that reference without forcing it to align.

Caveat: Requires you to already have a rough cell-type call before integration, which for spot-based Visium data means deconvolving first, this adds a step, not a shortcut.

Always run differential expression on raw or lognormalized counts, never on integrated values

When: Anytime you run DE after any integration method, regardless of which method you used.

After IntegrateLayers, call JoinLayers(object) and run DE (FindMarkers, pseudobulk DESeq2, etc.) on the original RNA/Spatial assay counts, not on the corrected low-dimensional embedding or any 'integrated' assay values.

Caveat: You still need to control for depth and batch statistically in the DE model (e.g., include batch as a covariate), skipping integration for DE doesn't remove the need to handle technical variation.

Use a spatial-aware integration method

When: You're working with Visium, Xenium, or other spatial data and standard scRNA-seq integration is destroying spatial domain structure.

Try PRECAST, GraphST, or SPIRAL, which integrate while modeling the spatial neighborhood graph instead of treating spots as an unordered cell-by-gene matrix.

Caveat: These tools are newer, less battle-tested, and have smaller user communities than Harmony or Seurat integration, so expect more troubleshooting and fewer worked examples to lean on.

When not to "fix" it

If your experiment was specifically designed to compare anatomical regions, tumor grades, or disease states, and the condition-specific population that integration erased is exactly the population the experiment was built to find, the "fix" is not to force better integration, it's to not integrate for that comparison at all. Correcting harder in that scenario doesn't recover accuracy, it just makes the analysis more confidently wrong. The tell that you're in this case: the condition-specific cluster is reproducible across biological replicates within the same condition before integration, and independent evidence (IHC, known marker genes, prior literature) supports that the populations really are different.

Five things experienced analysts do here

  1. Never run integration as the first thing you do to a merged spatial object. Cluster per-sample first, so you have a baseline of what condition-specific structure looked like before any correction touched it.
  2. Treat every integration method as a knob between two failure modes: leave batch effects in, or erase real biology. There is no setting that avoids both, pick the side of that tradeoff that matches your question before you run anything.
  3. Keep DE testing on raw or lognormalized counts from the original assay, never on Harmony-corrected PCA coordinates or an 'integrated' expression assay. Harmony only touches the embedding; treating corrected values as expression data invents differences that were never in the count matrix.
  4. When a colleague hands you a UMAP where everything looks beautifully mixed across condition, ask what it looked like before integration. A too-perfect merge is a request for the pre-integration plot, not a compliment.
  5. For spatial data, always check that domain structure survives integration by looking at the tissue image, not just the UMAP. A UMAP can look fine while the spatial coherence of your domains has been scrambled.

Questions people ask

How do I know if Harmony over-corrected my spatial data?

Compare a UMAP or PCA plot colored by condition before and after integration. If a cluster that was clearly separate by condition in the uncorrected data disappears or fully overlaps after Harmony, check marker gene expression for that population split by condition. If the marker signal also flattens out after integration, you're looking at overcorrection, not successful batch removal.

Should I integrate spatial transcriptomics sections from different tumor grades?

Only if your question is about shared cell types across grades, like building a common reference for annotation. If your question is whether grade-specific populations or domains exist, integrating first will often erase the signal you're trying to measure. Run the comparison on the uncorrected, per-section data instead, and use integration only as a visualization aid.

Can I run differential expression on Harmony-integrated data?

Not on the integrated embedding itself. Harmony only corrects the PCA coordinates, it doesn't touch the original count matrix. For DE, use the raw or lognormalized counts from the original assay (in Seurat v5, call JoinLayers() first), and include batch as a covariate in your DE model if needed.

What metrics quantify integration over-correction?

CellMixS gives you Cell-specific Mixing Score (CMS) for how well-mixed a cell's neighborhood is across batches, and ldfDiff for local density changes that flag both over- and under-correction. F1LISI balances batch mixing against biological conservation. None of these tools give you a universal pass/fail threshold, you compare them against your own uncorrected baseline and across cell populations within the same dataset.

Are there integration methods built specifically for spatial transcriptomics?

Yes. PRECAST, GraphST, and SPIRAL integrate while modeling the spatial neighborhood graph instead of treating spots as an unordered expression matrix, which helps preserve spatial domain structure that standard scRNA-seq-style integration (Harmony, CCA) can scramble. They're newer and less widely adopted than Harmony or Seurat integration, so expect more manual troubleshooting.

Related pages

Related reading on the blog

Sources

  1. Seurat v5 Integration — IntegrateLayers() syntax and the five available integration methods including RPCA
  2. Introduction to scRNA-seq Integration (Seurat) — DimPlot/FeaturePlot split.by patterns and FindConservedMarkers usage
  3. CellMixS: Explore Data Integration and Batch Effects — CMS and ldfDiff metrics for quantifying over- and under-correction
  4. Probabilistic Embedding, Clustering, and Alignment for Integrating Spatial Transcriptomics Data with PRECAST — Spatial-aware integration methods as a response to overcorrection in standard methods
  5. Harmony Batch Correction for Spatial Integration Issues (GitHub) — F1LISI comparison and residual batch-specific patterns after Seurat+Harmony

Part of the Integration over-correction series.