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)
#> Loading required package: SeuratObject
#> Loading required package: sp
#> 'SeuratObject' was built under R 4.6.0 but the current version is
#> 4.6.1; it is recomended that you reinstall 'SeuratObject' as the ABI
#> for R may have changed
#> 
#> Attaching package: 'SeuratObject'
#> The following objects are masked from 'package:base':
#> 
#>     intersect, t

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)
})
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
#> Warning: Data is of class matrix. Coercing to dgCMatrix.
x <- merge(x[[1]], x[[2]])
x[["cell_type"]] <- SimBatches$cell_types
x
#> An object of class Seurat 
#> 500 features across 1579 samples within 1 assay 
#> Active assay: RNA (500 features, 0 variable features)
#>  2 layers present: counts.B1, counts.B2
table(x$orig.ident)
#> 
#>  B1  B2 
#> 631 948

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)
#> Warning in svd.function(A = t(x = object), nv = npcs, ...): You're computing
#> too large a percentage of total singular values, use a standard svd instead.

UMAP before correction

x <- RunUMAP(x, reduction = "pca", dims = 1:ncol(Embeddings(x, "pca")),
             reduction.name = "umap_uncorrected", verbose = FALSE)
#> Warning: The default method for RunUMAP has changed from calling Python UMAP via reticulate to the R-native UWOT using the cosine metric
#> To use Python UMAP via reticulate, set umap.method to 'umap-learn' and metric to 'correlation'
#> This message will be shown once per session
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)
#> [1] "pca"              "umap_uncorrected" "canek"
ncol(Embeddings(x, "canek"))
#> [1] 50

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

Assays(x)
#> [1] "RNA"

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()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] Seurat_5.5.1       SeuratObject_5.4.0 sp_2.2-1           Canek_0.3.1       
#> 
#> loaded via a namespace (and not attached):
#>   [1] RColorBrewer_1.1-3     jsonlite_2.0.0         magrittr_2.0.5        
#>   [4] spatstat.utils_3.2-4   modeltools_0.2-24      farver_2.1.2          
#>   [7] rmarkdown_2.31         fs_2.1.0               ragg_1.5.2            
#>  [10] vctrs_0.7.3            ROCR_1.0-12            spatstat.explore_3.8-1
#>  [13] htmltools_0.5.9        BiocNeighbors_2.6.0    sass_0.4.10           
#>  [16] sctransform_0.4.3      parallelly_1.48.0      KernSmooth_2.23-26    
#>  [19] bslib_0.11.0           htmlwidgets_1.6.4      desc_1.4.3            
#>  [22] ica_1.0-3              plyr_1.8.9             plotly_4.12.0         
#>  [25] zoo_1.8-15             cachem_1.1.0           igraph_2.3.3          
#>  [28] mime_0.13              lifecycle_1.0.5        pkgconfig_2.0.3       
#>  [31] Matrix_1.7-5           R6_2.6.1               fastmap_1.2.0         
#>  [34] fitdistrplus_1.2-6     future_1.70.0          shiny_1.14.0          
#>  [37] digest_0.6.39          patchwork_1.3.2        S4Vectors_0.50.1      
#>  [40] numbers_0.9-2          tensor_1.5.1           RSpectra_0.16-2       
#>  [43] irlba_2.3.7            textshaping_1.0.5      labeling_0.4.3        
#>  [46] progressr_1.0.0        spatstat.sparse_3.2-0  httr_1.4.8            
#>  [49] polyclip_1.10-7        abind_1.4-8            compiler_4.6.1        
#>  [52] withr_3.0.3            S7_0.2.2               BiocParallel_1.46.0   
#>  [55] fastDummies_1.7.6      MASS_7.3-65            bluster_1.22.0        
#>  [58] tools_4.6.1            lmtest_0.9-40          otel_0.2.0            
#>  [61] prabclus_2.3-5         httpuv_1.6.17          future.apply_1.20.2   
#>  [64] goftest_1.2-3          nnet_7.3-20            glue_1.8.1            
#>  [67] nlme_3.1-169           promises_1.5.0         grid_4.6.1            
#>  [70] Rtsne_0.17             cluster_2.1.8.2        reshape2_1.4.5        
#>  [73] generics_0.1.4         gtable_0.3.6           spatstat.data_3.1-9   
#>  [76] class_7.3-23           tidyr_1.3.2            data.table_1.18.4     
#>  [79] flexmix_2.3-20         BiocGenerics_0.58.1    spatstat.geom_3.8-1   
#>  [82] RcppAnnoy_0.0.23       ggrepel_0.9.8          RANN_2.6.2            
#>  [85] pillar_1.11.1          stringr_1.6.0          spam_2.11-4           
#>  [88] RcppHNSW_0.7.0         later_1.4.8            robustbase_0.99-7     
#>  [91] splines_4.6.1          dplyr_1.2.1            lattice_0.22-9        
#>  [94] survival_3.8-6         FNN_1.1.4.1            deldir_2.0-4          
#>  [97] tidyselect_1.2.1       miniUI_0.1.2           pbapply_1.7-4         
#> [100] knitr_1.51             gridExtra_2.3.1        scattermore_1.2       
#> [103] stats4_4.6.1           xfun_0.60              diptest_0.77-2        
#> [106] matrixStats_1.5.0      DEoptimR_1.2-0         stringi_1.8.7         
#> [109] lazyeval_0.2.3         yaml_2.3.12            evaluate_1.0.5        
#> [112] codetools_0.2-20       kernlab_0.9-33         tibble_3.3.1          
#> [115] cli_3.6.6              uwot_0.2.4             xtable_1.8-8          
#> [118] reticulate_1.46.0      systemfonts_1.3.2      jquerylib_0.1.4       
#> [121] Rcpp_1.1.2             globals_0.19.1         spatstat.random_3.5-0 
#> [124] png_0.1-9              spatstat.univar_3.2-0  parallel_4.6.1        
#> [127] pkgdown_2.2.1          ggplot2_4.0.3          dotCall64_1.2         
#> [130] mclust_6.1.3           listenv_1.0.0          viridisLite_0.4.3     
#> [133] scales_1.4.0           ggridges_0.5.7         purrr_1.2.2           
#> [136] fpc_2.2-14             rlang_1.3.0            cowplot_1.2.0