Correct log-normalized Seurat data

knitr::opts_chunk$set(
  fig.width = 6,
  fig.align = "center",
  collapse = TRUE,
  comment = "#>"
)

This vignette shows Canek's default workflow on standard, log-normalized Seurat data. If your data is SCTransform-normalized instead, see Correct SCTransform-normalized Seurat data — SCTransform fit per batch produces residuals on different scales, so it needs an extra reconciliation step before Canek can correct it. For the older, gene-expression-space correction path (correctEmbeddings = FALSE), see the legacy version of this vignette.

library(Canek)
library(Seurat)

Create a Seurat object

We use the two simulated batches included in SimBatches.

x <- lapply(names(SimBatches$batches), function(batch) {
  CreateSeuratObject(SimBatches$batches[[batch]], project = batch)
})
x <- merge(x[[1]], x[[2]])
x[["cell_type"]] <- SimBatches$cell_types
x
table(x$orig.ident)

Standard preprocessing

We follow Seurat's standard log-normalization workflow on the whole merged object.

x <- NormalizeData(x, verbose = FALSE)
x <- FindVariableFeatures(x, nfeatures = 100, verbose = FALSE)
x <- ScaleData(x, verbose = FALSE)
x <- RunPCA(x, verbose = FALSE)

UMAP before correction

x <- RunUMAP(x, reduction = "pca", dims = 1:ncol(Embeddings(x, "pca")),
             reduction.name = "umap_uncorrected", verbose = FALSE)
DimPlot(x, reduction = "umap_uncorrected", group.by = "orig.ident")
DimPlot(x, reduction = "umap_uncorrected", group.by = "cell_type")

Cells separate by orig.ident in a way that doesn't line up with cell_type — that's the batch effect we want to correct.

Run Canek

We pass the column containing the batch information. By default, RunCanek() corrects in PCA-embedding space (correctEmbeddings = TRUE) rather than on gene expression directly. Since x already has a "pca" reduction from the step above, Canek infers the number of dimensions to use from it and reuses that embedding directly instead of recomputing its own PCA.

x <- RunCanek(x, "orig.ident")
Reductions(x)
ncol(Embeddings(x, "canek"))

The correction is stored as a new "canek" reduction, not a new assay — the original RNA assay is untouched.

Assays(x)

By default, RunCanek() also repeats the correction up to 5 times (maxLoop = 5), feeding each pass's result into the next and stopping early once the correction stops changing much (loopTol). Pass maxLoop = 1 for a single pass, or debug = TRUE to inspect the per-pass correction magnitude (x@tools$RunCanek).

UMAP after correction

Downstream steps should use the "canek" reduction directly, not re-run ScaleData/RunPCA on the original assay — the correction lives in the reduction, not in the assay's expression values.

x <- RunUMAP(x, reduction = "canek", dims = 1:ncol(Embeddings(x, "canek")),
             reduction.name = "umap_corrected", verbose = FALSE)
DimPlot(x, reduction = "umap_corrected", group.by = "orig.ident")
DimPlot(x, reduction = "umap_corrected", group.by = "cell_type")

Session info

sessionInfo()


Try the Canek package in your browser

Any scripts or data that you put into this service are public.

Canek documentation built on July 18, 2026, 1:06 a.m.