Author: Ning Liu, Dharmesh Bhuva, Ahmed Mohamed, Chin Wee Tan, Melissa Davis

# A quick start guide to the standR package

# Installation

    if (!require("BiocManager", quietly = TRUE))
        install.packages("BiocManager")

    BiocManager::install("standR")

The development version of `standR` can be installed from GitHub:

    devtools::install_github("DavisLaboratory/standR")

# Quick start

    library(standR)
    library(SpatialExperiment)
    library(limma)
    library(ExperimentHub)

## Load data for this guide

This is the background for the data:

NanoString GeoMx DSP dataset of diabetic kidney disease (DKD) vs healthy kidney tissue. Seven slides were analyzed, 4 DKD and 3 healthy. Regions of Interest (ROI) were focused two different parts of a kidney’s structure: tubules or glomeruli. Individual glomeruli were identified by a pathologist as either relatively healthy or diseased regardless if the tissue was DKD or healthy. Tubule ROIs were segmented into distal (PanCK) and proximal (neg) tubules. While both distal and proximal tubules are called tubules, they perform very different functions in the kidney.

    eh <- ExperimentHub()

    query(eh, "standR")

    ## ExperimentHub with 3 records
    ## # snapshotDate(): 2026-05-19
    ## # $dataprovider: Nanostring
    ## # $species: NA
    ## # $rdataclass: data.frame
    ## # additional mcols(): taxonomyid, genome, description,
    ## #   coordinate_1_based, maintainer, rdatadateadded, preparerclass, tags,
    ## #   rdatapath, sourceurl, sourcetype
    ## # retrieve records with, e.g., 'object[["EH7364"]]'
    ##
    ##            title
    ##   EH7364 | GeomxDKDdata_count
    ##   EH7365 | GeomxDKDdata_sampleAnno
    ##   EH7366 | GeomxDKDdata_featureAnno

    countFile <- eh[["EH7364"]]

    sampleAnnoFile <- eh[["EH7365"]]

    featureAnnoFile <- eh[["EH7366"]]

    spe <- readGeoMx(countFile, sampleAnnoFile, featureAnnoFile = featureAnnoFile, rmNegProbe = TRUE)

## QC

### metadata visualization

Based on the description of the data, we know that all glomerulus are classified as abnormal and healthy, and tubule are classified as neg and PanCK.

We therefore merge the region-related annotations to avoid collinearity, which can affect the process of batch correction.

    colData(spe)$regions <- paste0(colData(spe)$region,"_",colData(spe)$SegmentLabel) |>
      (\(.) gsub("_Geometric Segment","",.))() |>
      paste0("_",colData(spe)$pathology) |>
      (\(.) gsub("_NA","_ns",.))()

    library(ggalluvial)

    plotSampleInfo(spe, column2plot = c("SlideName","disease_status","regions"))

### Gene level QC

    spe <- addPerROIQC(spe, rm_genes = TRUE)

    plotGeneQC(spe, ordannots = "regions", col = regions, point_size = 2)

Using the `plotGeneQC` function, we can have a look at which were the genes removed and the overall distribution of percentage of non-expressed genes in all ROIs. By default, top 9 genes are plotted here (arranging by mean expression), user can increase the number of plotted genes by changing the `top_n` parameter.

In this case we don’t see any specific biological pattern for the samples expressing this genes (figure above).

### ROI level QC

In the ROI level QC, we first aim to identify (if any) ROI(s) that have relatively low library size and low cell count because they are considered as low quality samples due to insufficient sequencing depth or lack of RNA in the chosen region.

In this case, looking at the distribution plots of library size and nuclei count, we don’t see any particular spike in the low ends, rather the distributions are relatively smooth. Looking at the dot plot, library sizes are mostly positively correlate with the nuclei count, with some data have relatively low library size while the nuclei count is reasonable. We therefore can try to draw an filtering threshold at the low end of the library size, in this case 50000. By coloring the dot with their slide names, we find that the ROIs below the threshold are all from slide disease1B, suggesting the reason for this might be some technical issues of slide disease1B.

    plotROIQC(spe, y_threshold = 50000, col = SlideName)

Since library size of 50000 seems to be a reasonable threshold, here we subset the spatial experiment object based on the library size in `colData`.

    spe <- spe[,rownames(colData(spe))[colData(spe)$lib_size > 50000]]

## Inspection of variations on ROI level

### RLE

Here we can see obvious variation from slides to slides, and small variations are also observed within each slide.

    plotRLExpr(spe, ordannots = "SlideName", assay = 2, col = SlideName)

### PCA

Here we color the PCA with slide information, and shape by regions (tissue). We can see that PC1 is mainly spread out by regions, especially glomerulus and tubule. And grouping based on slide within each tissue are observed. The subtypes in tubule are clearly separated, but different subtypes of glomerulus is still grouping together. Moreover, diseased tissues and control tissues are mixed as well (disease slides and normal slides).

    drawPCA(spe, assay = 2, col = SlideName, shape = regions)

# Data normalization

As we observed the technical variations in the data in both RLE and PCA plots. It is necessary to perform normalization in the data.

In the `standR` package, we offer normalization options including TMM, RPKM, TPM, CPM, upperquartile and sizefactor. Among them, RPKM and TPM required gene length information (add `genelength` column to the `rowData` of the object). For TMM, upperquartile and sizefactor, their normalized factor will be stored their `metadata`.

Here we used TMM to normalize the data.

    colData(spe)$biology <- paste0(colData(spe)$disease_status, "_", colData(spe)$regions)

    spe_tmm <- geomxNorm(spe, method = "TMM")

# Batch correction

In the Nanostring’s GeoMX DSP protocol, due to the fact that one slide is only big enough for a handful of tissue segments (ROIs), it is common that we see the DSP data being confounded by the batch effect introduced by different slides. In order to establish fair comparison between ROIs later on, it is necessary to remove this batch effect from the data.

To run RUV4 batch correction, we need to provide a list of “negative control genes (NCGs)”.

The function `findNCGs` allows identifying the NCGs from the data. In this case, since the batch effect is mostly introduced by slide, we therefore want to identify NCGs across all slides, so here we set the `batch_name` to “SlideName”, and select the top 500 least variable genes across different slides as NCGs.

    spe <- findNCGs(spe, batch_name = "SlideName", top_n = 500)

    metadata(spe) |> names()

    ## [1] "NegProbes"         "lcpm_threshold"    "genes_rm_rawCount"
    ## [4] "genes_rm_logCPM"   "NCGs"

Here we use k of 5 to perform RUV-4 normalization.

    spe_ruv <- geomxBatchCorrection(spe, factors = "biology",
                       NCGs = metadata(spe)$NCGs, k = 5)

We can then inspect the PCA of the corrected data with annotations, to inspect the removal of batch effects, and the retaining of the biological factors.

    plotPairPCA(spe_ruv, assay = 2, color = disease_status, shape = regions, title = "RUV4")

Moreover, we can also have a look at the RLE plots of the normalized count.

    plotRLExpr(spe_ruv, assay = 2, color = SlideName) + ggtitle("RUV4")

For more detailed analysis pipeline and usage of the standR package, please see https://github.com/DavisLaboratory/GeoMXAnalysisWorkflow

# SessionInfo

    sessionInfo()

    ## R version 4.6.0 (2026-04-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=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] stats4    stats     graphics  grDevices utils     datasets  methods
    ## [8] base
    ##
    ## other attached packages:
    ##  [1] ggalluvial_0.12.6           ggplot2_4.0.3
    ##  [3] ExperimentHub_3.3.0         AnnotationHub_4.3.0
    ##  [5] BiocFileCache_3.3.0         dbplyr_2.5.2
    ##  [7] limma_3.69.1                SpatialExperiment_1.23.0
    ##  [9] SingleCellExperiment_1.35.1 SummarizedExperiment_1.43.0
    ## [11] Biobase_2.73.1              GenomicRanges_1.65.0
    ## [13] Seqinfo_1.3.0               IRanges_2.47.2
    ## [15] S4Vectors_0.51.3            BiocGenerics_0.59.6
    ## [17] generics_0.1.4              MatrixGenerics_1.25.0
    ## [19] matrixStats_1.5.0           standR_1.17.0
    ##
    ## loaded via a namespace (and not attached):
    ##   [1] DBI_1.3.0            gridExtra_2.3        httr2_1.2.2
    ##   [4] rlang_1.2.0          magrittr_2.0.5       scater_1.41.1
    ##   [7] compiler_4.6.0       RSQLite_3.53.1       mgcv_1.9-4
    ##  [10] png_0.1-9            vctrs_0.7.3          pkgconfig_2.0.3
    ##  [13] crayon_1.5.3         fastmap_1.2.0        backports_1.5.1
    ##  [16] magick_2.9.1         XVector_0.53.0       scuttle_1.23.1
    ##  [19] labeling_0.4.3       rmarkdown_2.31       tzdb_0.5.0
    ##  [22] ggbeeswarm_0.7.3     purrr_1.2.2          bit_4.6.0
    ##  [25] xfun_0.57            cachem_1.1.0         beachmat_2.29.0
    ##  [28] jsonlite_2.0.0       blob_1.3.0           DelayedArray_0.39.3
    ##  [31] BiocParallel_1.47.0  broom_1.0.13         irlba_2.3.7
    ##  [34] parallel_4.6.0       R6_2.6.1             bslib_0.11.0
    ##  [37] RColorBrewer_1.1-3   car_3.1-5            jquerylib_0.1.4
    ##  [40] Rcpp_1.1.1-1.1       knitr_1.51           readr_2.2.0
    ##  [43] Matrix_1.7-5         splines_4.6.0        tidyselect_1.2.1
    ##  [46] viridis_0.6.5        abind_1.4-8          yaml_2.3.12
    ##  [49] codetools_0.2-20     curl_7.1.0           lattice_0.22-9
    ##  [52] tibble_3.3.1         withr_3.0.2          KEGGREST_1.53.0
    ##  [55] S7_0.2.2             evaluate_1.0.5       Biostrings_2.81.2
    ##  [58] ggpubr_0.6.3         pillar_1.11.1        BiocManager_1.30.27
    ##  [61] filelock_1.0.3       carData_3.0-6        vroom_1.7.1
    ##  [64] BiocVersion_3.24.0   hms_1.1.4            scales_1.4.0
    ##  [67] glue_1.8.1           maketools_1.3.2      tools_4.6.0
    ##  [70] BiocNeighbors_2.7.2  sys_3.4.3            ScaledMatrix_1.21.0
    ##  [73] ggsignif_0.6.4       locfit_1.5-9.12      buildtools_1.0.0
    ##  [76] cowplot_1.2.0        grid_4.6.0           tidyr_1.3.2
    ##  [79] AnnotationDbi_1.75.0 edgeR_4.11.1         nlme_3.1-169
    ##  [82] patchwork_1.3.2      beeswarm_0.4.0       BiocSingular_1.29.0
    ##  [85] vipor_0.4.7          Formula_1.2-5        rsvd_1.0.5
    ##  [88] cli_3.6.6            ruv_0.9.7.1          rappdirs_0.3.4
    ##  [91] viridisLite_0.4.3    S4Arrays_1.13.0      dplyr_1.2.1
    ##  [94] gtable_0.3.6         rstatix_0.7.3        sass_0.4.10
    ##  [97] digest_0.6.39        ggrepel_0.9.8        SparseArray_1.13.2
    ## [100] rjson_0.2.23         farver_2.1.2         memoise_2.0.1
    ## [103] htmltools_0.5.9      lifecycle_1.0.5      httr_1.4.8
    ## [106] statmod_1.5.2        bit64_4.8.2