RPBC: Region-Preserving Batch Correction

library(RPBC)

Overview

Region-Preserving Batch Correction (RPBC) is designed for paired region-resolved single-cell and spatial molecular data in which patient-associated nuisance variation should be reduced while preserving a prespecified population-average regional effect.

RPBC supports three related workflows:

  • Ordinary RPBC, for paired region-resolved data without additional patient-level covariates;
  • Adaptive RPBC, which introduces group-size-dependent stabilization of patient-region scale adjustments; and
  • Covariate-adjusted RPBC, which protects specified patient-level covariates while harmonizing residual patient-associated variation.

The primary user interface is rpbc(). It accepts either a SingleCellExperiment or SpatialExperiment object and returns the input object with the harmonized expression matrix stored as a new assay.

Package version: 0.99.0

Example dataset

The package includes haley_gbm_example, a representative subset of a published glioblastoma imaging mass cytometry (IMC) dataset. The example retains all seven patients included in the RPBC real-world application and preserves the paired Core/Edge design, with 30 cells sampled from each patient-region combination.

The object contains 420 cells and 10 representative biological markers. Raw IMC intensities are stored in the "counts" assay.

data("haley_gbm_example", package = "RPBC")

haley_gbm_example
#> class: SpatialExperiment 
#> dim: 10 420 
#> metadata(0):
#> assays(1): counts
#> rownames(10): CD45 CD3 ... Ki67 Vimentin
#> rowData names(0):
#> colnames(420): ROI_1A1_cell_337 ROI_1A1_cell_460 ... ROI_7A3_cell_12293
#>   ROI_7A3_cell_12381
#> colData names(8): cell_id ROI ... mgmt_status sample_id
#> reducedDimNames(0):
#> mainExpName: NULL
#> altExpNames(0):
#> spatialCoords names(0) :
#> imgData names(0):
dim(haley_gbm_example)
#> [1]  10 420
SummarizedExperiment::assayNames(haley_gbm_example)
#> [1] "counts"

The paired patient-region structure can be inspected directly from the cell-level metadata.

table(
  haley_gbm_example$patient_id,
  haley_gbm_example$region_type
)
#>    
#>     Core Edge
#>   1   30   30
#>   2   30   30
#>   3   30   30
#>   4   30   30
#>   5   30   30
#>   6   30   30
#>   7   30   30

Each of the seven patients contributes cells from both Core and Edge, satisfying the paired regional structure required by RPBC.

The example data are derived from:

Haley MJ, et al. (2024). Hypoxia coordinates the spatial landscape of myeloid cells within glioblastoma to affect survival. Science Advances, 10(20), eadj3301. doi:10.1126/sciadv.adj3301.

Haley M, Couper K. (2024). Imaging mass cytometry data from IDH wildtype glioblastomas. Dryad. doi:10.5061/dryad.2z34tmprw.

Input assay and transformation

RPBC separates assay selection from transformation through the assay_name and transform arguments.

The available transformations are:

  • "none": use the selected assay exactly as supplied;
  • "asinh": apply asinh(x / cofactor);
  • "log1p": apply log(x + 1) to non-negative values.

For the bundled IMC example, the "counts" assay contains raw intensities, so the analyses below use:

transform = "asinh"
cofactor = 5

If an assay has already been appropriately normalized and transformed, such as a sequencing "logcounts" assay, it can instead be supplied with transform = "none".

The "log1p" option performs only the transformation log(x + 1); it is not a replacement for sequencing-data normalization.

Ordinary RPBC

Ordinary RPBC is the base workflow for a paired region-resolved design. RPBC harmonizes patient and patient-by-region nuisance components while retaining the population-average Core/Edge regional effect.

example_ordinary <- rpbc(
  object = haley_gbm_example,
  patient = "patient_id",
  region = "region_type",
  assay_name = "counts",
  transform = "asinh",
  cofactor = 5,
  adaptive = FALSE,
  output_assay = "rpbc"
)

SummarizedExperiment::assayNames(example_ordinary)
#> [1] "counts" "rpbc"

The original "counts" assay remains available, while the harmonized values are stored in the new "rpbc" assay.

dim(
  SummarizedExperiment::assay(
    example_ordinary,
    "rpbc"
  )
)
#> [1]  10 420

Adaptive RPBC

Adaptive RPBC uses the same paired biological target but incorporates group-size-dependent stabilization into the patient-region scale adjustment.

example_adaptive <- rpbc(
  object = haley_gbm_example,
  patient = "patient_id",
  region = "region_type",
  assay_name = "counts",
  transform = "asinh",
  cofactor = 5,
  adaptive = TRUE,
  output_assay = "rpbc_adaptive"
)

SummarizedExperiment::assayNames(example_adaptive)
#> [1] "counts"        "rpbc_adaptive"

The bundled example is deliberately balanced at 30 cells per patient-region combination. It therefore demonstrates the Adaptive RPBC interface and output structure rather than the effect of unequal patient-region sample sizes.

The Adaptive RPBC defaults are:

delta_adaptive_k = 1
delta_bounds = c(0.5, 2)

and can be modified through rpbc() when appropriate.

Covariate-adjusted RPBC

Covariate-adjusted RPBC is used when patient-level variables should be retained as part of the preserved mean structure. Covariates supplied through covariates must be constant within patient.

The Haley example contains patient age and sex, so both are included below.

example_covariate <- rpbc(
  object = haley_gbm_example,
  patient = "patient_id",
  region = "region_type",
  assay_name = "counts",
  transform = "asinh",
  cofactor = 5,
  covariates = c("age", "sex"),
  adaptive = FALSE,
  output_assay = "rpbc_covariate"
)

SummarizedExperiment::assayNames(example_covariate)
#> [1] "counts"         "rpbc_covariate"

The harmonized expression matrix is stored in "rpbc_covariate".

dim(
  SummarizedExperiment::assay(
    example_covariate,
    "rpbc_covariate"
  )
)
#> [1]  10 420

Adaptive RPBC and covariate-adjusted RPBC are currently implemented as separate extensions and therefore should not be requested simultaneously.

Accessing harmonized values

The harmonized matrix can be accessed using the standard SummarizedExperiment assay interface. For example:

rpbc_matrix <- SummarizedExperiment::assay(
  example_ordinary,
  "rpbc"
)

rpbc_matrix[1:5, 1:5]
#>       ROI_1A1_cell_337 ROI_1A1_cell_460 ROI_1A1_cell_514 ROI_1A1_cell_3185
#> CD45      4.332762e-07     6.476164e-07     2.945675e-07      4.883923e-07
#> CD3       1.924978e-06     1.187254e-06     1.770623e-06      1.436945e-06
#> Cd8a      1.289758e-06     1.564613e-06     2.083473e-06      4.455594e-07
#> CD68     -4.757505e-09     1.286778e-07    -2.303632e-08     -1.162583e-07
#> CD163    -3.429965e-07     3.253885e-07     7.140947e-07     -1.045584e-07
#>       ROI_1A1_cell_3214
#> CD45       6.384304e-07
#> CD3        9.352934e-07
#> Cd8a       2.159198e-06
#> CD68       6.386567e-07
#> CD163     -2.871333e-08

The matrix retains the same marker-by-cell dimensions and dimnames as the selected input assay, allowing the harmonized values to be used in downstream single-cell or spatial analyses.

RPBC does not prescribe a particular downstream workflow. Dimensionality reduction, clustering, visualization, differential analyses, or other biological analyses can therefore be performed using methods appropriate for the study after harmonization.

Choosing an RPBC workflow

The three workflows address related but distinct analysis settings.

Ordinary RPBC is appropriate when the primary objective is to reduce patient and patient-by-region nuisance variation while preserving the population-average regional effect and no additional patient-level covariates need to be protected.

Adaptive RPBC is appropriate when group-size-dependent stabilization of patient-region scale adjustment is desired, particularly in designs with unequal patient-region sample sizes.

Covariate-adjusted RPBC is appropriate when known patient-level covariates, such as age or sex, should remain part of the protected mean structure during harmonization.

Only one of the Adaptive or covariate-adjusted extensions should currently be used in a single RPBC call.

Additional fit information

By default, rpbc() returns the modified Bioconductor object. Users who require internal fitted quantities or convergence information can set:

return_fit = TRUE

The result is then a list containing the harmonized object in object and the fitted RPBC quantities in fit.

For example:

example_fit <- rpbc(
  object = haley_gbm_example,
  patient = "patient_id",
  region = "region_type",
  assay_name = "counts",
  transform = "asinh",
  cofactor = 5,
  adaptive = FALSE,
  output_assay = "rpbc_fit_example",
  return_fit = TRUE
)

names(example_fit)
#> [1] "object" "fit"
all(example_fit$fit$converged)
#> [1] TRUE

Session information

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 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.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] RPBC_0.99.0      BiocStyle_2.41.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] Matrix_1.7-6                jsonlite_2.0.0             
#>  [3] rjson_0.2.23                compiler_4.6.1             
#>  [5] BiocManager_1.30.27         Rcpp_1.1.2                 
#>  [7] SpatialExperiment_1.23.0    SummarizedExperiment_1.43.0
#>  [9] Biobase_2.73.2              magick_2.9.1               
#> [11] GenomicRanges_1.65.4        jquerylib_0.1.4            
#> [13] IRanges_2.47.5              Seqinfo_1.3.2              
#> [15] yaml_2.3.12                 fastmap_1.2.0              
#> [17] lattice_0.23-1              XVector_0.53.0             
#> [19] R6_2.6.1                    S4Arrays_1.13.2            
#> [21] generics_0.1.4              knitr_1.52                 
#> [23] BiocGenerics_0.59.12        DelayedArray_0.39.8        
#> [25] MatrixGenerics_1.25.0       maketools_1.3.2            
#> [27] bslib_0.12.0                rlang_1.3.0                
#> [29] cachem_1.1.0                xfun_0.61                  
#> [31] sass_0.4.10                 sys_3.4.3                  
#> [33] otel_0.2.0                  SparseArray_1.13.4         
#> [35] cli_3.6.6                   magrittr_2.0.5             
#> [37] digest_0.6.39               grid_4.6.1                 
#> [39] lifecycle_1.0.5             S4Vectors_0.51.10          
#> [41] SingleCellExperiment_1.35.2 evaluate_1.0.5             
#> [43] buildtools_1.0.0            abind_1.4-8                
#> [45] stats4_4.6.1                rmarkdown_2.32             
#> [47] matrixStats_1.5.0           tools_4.6.1                
#> [49] htmltools_0.5.9