15 RNA Batch Correction

Batch effects in the RNA modality are corrected using Seurat v3 canonical correlation analysis (CCA) integration. The strategy is to first process the Healthy and PASC subsets independently on the RNA assay to identify cell-type marker genes, then use those markers as anchor features to align the two conditions into a shared integrated assay. Using cell-type-defining marker genes rather than highly variable genes as anchors focuses the CCA on biological variation rather than technical differences between conditions.

15.1 Select the RNA assay

DefaultAssay(combined) <- 'RNA'

15.2 Per-condition processing

Each condition is processed independently to obtain per-condition clusters and marker genes. The ribosomal module score (ribo_genes2) computed in the confounding factors chapter is regressed out during scaling. Named reductions are used throughout to avoid any ambiguity with reductions from other chapters.

15.2.1 Healthy

healthy <- subset(combined, subset = individual_condition == "Healthy")
healthy <- NormalizeData(healthy, verbose = FALSE)
healthy <- FindVariableFeatures(healthy, selection.method = 'vst', nfeatures = 1000)
healthy <- ScaleData(healthy, vars.to.regress = "ribo_genes2")
healthy <- RunPCA(healthy,
                  features       = Seurat::VariableFeatures(object = healthy),
                  reduction.name = "pca.healthy",
                  reduction.key  = "PChealthy_")
healthy <- RunUMAP(healthy, dims = 1:15,
                   reduction      = "pca.healthy",
                   reduction.name = "umap.rna.healthy",
                   reduction.key  = "UMAPRNA_healthy_")
healthy <- FindNeighbors(healthy, reduction = "pca.healthy", dims = 1:15)

resolution <- 2
healthy <- FindClusters(healthy, resolution = seq(0.1, resolution, 0.1))

15.2.1.1 Clustree — Healthy resolution sweep

clustree(healthy, show_axis = TRUE) +
  theme(legend.key.size = unit(0.20, 'cm'))

Download PDF

15.2.1.2 UMAP — Healthy at resolution 1

Idents(healthy) <- healthy$RNA_snn_res.1
DimPlot(healthy, reduction = "umap.rna.healthy")

Download PDF

15.2.2 PASC

PASC <- subset(combined, subset = individual_condition == "PASC")
PASC <- NormalizeData(PASC, verbose = FALSE)
PASC <- FindVariableFeatures(PASC, selection.method = 'vst', nfeatures = 1000)
PASC <- ScaleData(PASC, vars.to.regress = "ribo_genes2")
PASC <- RunPCA(PASC,
               features       = Seurat::VariableFeatures(object = PASC),
               reduction.name = "pca.PASC",
               reduction.key  = "PCPASC_")
PASC <- RunUMAP(PASC, dims = 1:15,
                reduction      = "pca.PASC",
                reduction.name = "umap.rna.PASC",
                reduction.key  = "UMAPRNA_PASC_")
PASC <- FindNeighbors(PASC, reduction = "pca.PASC", dims = 1:15)

PASC <- FindClusters(PASC, resolution = seq(0.1, resolution, 0.1))
PASC$ident_originallabels <- Idents(PASC)

15.2.2.1 Clustree — PASC resolution sweep

clustree(PASC, show_axis = TRUE) +
  theme(legend.key.size = unit(0.20, 'cm'))

Download PDF

15.3 CCA Integration

15.3.1 Anchor feature selection

Marker genes are identified across the Healthy resolution sweep. The top 30 markers per cluster by average log2FC at resolution 1 are selected as anchor features. Duplicate genes are removed so each gene appears only once in the anchor feature list.

library(presto)

markers_list <- list()
for (res in seq(0.1, 1.9, 0.1)) {
  healthy     <- FindClusters(healthy, resolution = res, algorithm = 3)
  clustername <- paste0("RNA_snn_res.", res)
  Idents(healthy) <- healthy@meta.data[[clustername]]
  markers_list[[clustername]] <- wilcoxauc(healthy,
                                         group_by     = clustername,
                                         assay        = "data",
                                         seurat_assay = "RNA") %>%
  dplyr::filter(logFC > log(1.2) & padj < 0.01 & pct_in > 0.25)
}

healthy_markers_list <- markers_list[["RNA_snn_res.1"]]
found_markers <- healthy_markers_list
top30 <- found_markers %>%
  dplyr::group_by(group) %>%
  dplyr::top_n(n = 30, wt = logFC) %>%
  dplyr::select(feature)
top30    <- top30[!duplicated(top30$feature), ]
features <- top30$feature

ifnb.list <- list(healthy, PASC)
anchors   <- FindIntegrationAnchors(object.list = ifnb.list, anchor.features = features)
rna_integration  <- IntegrateData(anchorset = anchors)

15.3.2 Integrated PCA and UMAP

Scale the integrated assay and run PCA, storing the result under the named reduction pca_integration. The UMAP (rna.integration) is computed from pca_integration for visualisation only — the PCA is what feeds into the cross-modal WNN graph in the next chapter.

DefaultAssay(rna_integration) <- "integrated"

rna_integration <- ScaleData(rna_integration, verbose = FALSE)
rna_integration <- RunPCA(rna_integration, npcs = 30, verbose = FALSE,
                   reduction.name = "pca_integration",
                   reduction.key  = "PCintegration_")

rna_integration <- RunUMAP(rna_integration, dims = 1:15,
                    reduction      = "pca_integration",
                    reduction.name = "rna.integration",
                    reduction.key  = "UMAPintegration_")

rna_integration <- FindNeighbors(rna_integration, reduction = "pca_integration", dims = 1:15,
                          graph.name = c("rna.integration_nn", "rna.integration_snn"))
rna_integration <- FindClusters(rna_integration, graph.name = "rna.integration_snn",
                         resolution = seq(0.1, resolution, 0.1))

15.3.3 Integration UMAP

Colour the integrated UMAP by sample, donor, and condition to confirm that the integration has removed condition-driven separation while preserving cell-type structure.

DimPlot(rna_integration, reduction = "rna.integration", group.by = "sample")

Download PDF

DimPlot(rna_integration, reduction = "rna.integration", group.by = "sample_donor")

Download PDF

DimPlot(rna_integration, reduction = "rna.integration", group.by = "individual_condition")

Download PDF

15.3.4 Clustree — integrated RNA resolution sweep

clustree(rna_integration, prefix = 'rna.integration_snn_res.', show_axis = TRUE) +
  theme(legend.key.size = unit(0.20, 'cm'))

Download PDF

15.4 save the batch corrected assay

DefaultAssay(rna_integration)<-"RNA"
SaveSeuratRds(rna_integration, "data/rna_batch_corrected.rds")

15.5 Conclusion

RNA expression in this dataset is jointly driven by cell type and disease status (PASC vs Healthy), and these two axes are not separable in an uncorrected embedding — cluster boundaries would reflect both cell identity and disease state at once, confounding any later disease comparison. To resolve this, integration anchors were derived from marker genes identified in the Healthy subset only (healthy_markers_list), then used with FindIntegrationAnchors/IntegrateData (CCA) to integrate Healthy and PASC cells into a shared space. Because these anchor genes capture cell-type-defining programs under normal physiology, using them for integration corrects for disease as a confounding factor, analogous to a batch effect, while preserving cell-type identity as the organizing signal. As a result, clusters in the integrated (integration.rna/integration.wnn) space are driven by cell-type transcriptomic profile rather than disease-associated expression shifts. This is essential for the intended downstream analysis: within each cell type, PASC and Healthy cells now fall into shared clusters, allowing valid, cell-type-matched comparisons of disease vs healthy expression and chromatin accessibility.