Author: Wei Liu
Date: 2026-06-05

# PRECAST: simulation

#### Wei Liu

#### 2026-06-05

This vignette introduces the PRECAST workflow for the analysis of integrating multiple spatial transcriptomics dataset. The workflow consists of three steps

- Independent preprocessing and model setting
- Probabilistic embedding, clustering and alignment using PRECAST model
- Downstream analysis (i.e. visualization of clusters and embeddings, combined differential expression analysis)

We demonstrate the use of PRECAST to three simulated Visium data that are here, which can be downloaded to the current working path by the following command:

    githubURL <- "https://github.com/feiyoung/PRECAST/blob/main/vignettes_data/data_simu.rda?raw=true"
    download.file(githubURL,"data_simu.rda",mode='wb')

Then load to R

    load("data_simu.rda")

The package can be loaded with the command:

    library(PRECAST)
    library(Seurat)

## Load the simulated data

First, we view the the three simulated spatial transcriptomics data with Visium platform.

    data_simu ## a list including three Seurat object with default assay: RNA

Check the content in `data_simu`.

    head(data_simu[[1]])

    row.names(data_simu[[1]])[1:10]

## Create a PRECASTObject object

We show how to create a PRECASTObject object step by step. First, we create a Seurat list object using the count matrix and meta data of each data batch. Although `data_simu` is a prepared Seurat list object, we re-create a same objcet seuList to show the details.

- Note: the spatial coordinates must be contained in the meta data and named as `row` and `col`, which benefits the identification of spaital coordinates by PRECAST.

    ## Get the gene-by-spot read count matrices
    countList <- lapply(data_simu, function(x){
      assay <- DefaultAssay(x)
      GetAssayData(x, assay = assay, slot='counts')

    } )

    ## Check the spatial coordinates: Yes, they are named as "row" and "col"!
    head(data_simu[[1]]@meta.data)

    ## Get the meta data of each spot for each data batch
    metadataList <- lapply(data_simu, function(x) x@meta.data)

    ## ensure the row.names of metadata in metaList are the same as that of colnames count matrix in countList
    M <- length(countList)
    for(r in 1:M){
      row.names(metadataList[[r]]) <- colnames(countList[[r]])
    }

    ## Create the Seurat list  object

    seuList <- list()
    for(r in 1:M){
      seuList[[r]] <- CreateSeuratObject(counts = countList[[r]], meta.data=metadataList[[r]], project = "PRECASTsimu")
    }

### Prepare the PRECASTObject with preprocessing step.

Next, we use `CreatePRECASTObject()` to create a PRECASTObject based on the Seurat list object `seuList`. This function will do three things:

- 1. Filter low-quality spots and genes, controlled by the arguments `premin.features` and `premin.spots`, respectively; the spots are retained in raw data (seuList) with at least premin.features number of nonzero-count features (genes), and the genes are retained in raw data (seuList) with at least `premin.spots` number of spots. To ease presentation, we denote the filtered Seurat list object as data_filter1.
- 2. Select the top 2,000 variable genes (by setting `gene.number=2000`) for each data batch using `FindSVGs()` function in `DR.SC` package for spatially variable genes or `FindVariableFeatures()` function in `Seurat` package for highly variable genes. Next, we prioritized genes based on the number of times they were selected as variable genes in all samples and chose the top 2,000 genes. Then denote the Seurat list object as data_filter2, where only 2,000 genes are retained.
- 3. Conduct strict quality control for data_filter2 by filtering spots and genes, controlled by the arguments `postmin.features` and `postmin.spots`, respectively; the spots are retained with at least `post.features` nonzero counts across genes; the features (genes) are retained with at least `postmin.spots` number of nonzero-count spots. Usually, no genes are filltered because these genes are variable genes.

If the argument `customGenelist` is not `NULL`, then this function only does (3) based on `customGenelist` gene list.

In this simulated dataset, we don’t require to select genes, thus, we set `customGenelist=row.names(seuList[[1]])`, representing the user-defined gene list. User can retain the raw seurat list object by setting `rawData.preserve = TRUE`.

    ## Create PRECASTObject
    set.seed(2022)
    PRECASTObj <-  CreatePRECASTObject(seuList, customGenelist=row.names(seuList[[1]]))

    ## User can retain the raw seuList by the following commond.
    ##  PRECASTObj <-  CreatePRECASTObject(seuList, customGenelist=row.names(seuList[[1]]), rawData.preserve = TRUE)

## Fit PRECAST using simulated data

### Add the model setting

Add adjacency matrix list and parameter setting of PRECAST. More model setting parameters can be found in `model_set()`.

    ## check the number of genes/features after filtering step
    PRECASTObj@seulist

    ## seuList is null since the default value `rawData.preserve` is FALSE.
    PRECASTObj@seuList

    ## Add adjacency matrix list for a PRECASTObj object to prepare for PRECAST model fitting.
    PRECASTObj <-  AddAdjList(PRECASTObj, platform = "Visium")

    ## Add a model setting in advance for a PRECASTObj object: verbose =TRUE helps outputing the information in the algorithm; coreNum set the how many cores are used in PRECAST. If you run PRECAST for multiple number of clusters, you can set multiple cores; otherwise, set it to 1.
    PRECASTObj <- AddParSetting(PRECASTObj, Sigma_equal=FALSE, maxIter=30, verbose=TRUE, int.model='kmeans',
                                 coreNum =1)

### Fit PRECAST

For function `PRECAST`, users can specify the number of clusters \(K\) or set `K` to be an integer vector by using modified BIC(MBIC) to determine \(K\). For convenience, we give a single K here.

    ### Given K
    set.seed(2022)
    PRECASTObj <- PRECAST(PRECASTObj, K=7)

Other options

Run for multiple K. Here, we set `K=6:9`.

    ## Reset  parameters by increasing cores.
    PRECASTObj2 <- AddParSetting(PRECASTObj, Sigma_equal=FALSE, maxIter=30, verbose=TRUE,
                                 coreNum =2)
    set.seed(2023)
    PRECASTObj2 <- PRECAST(PRECASTObj2, K=6:7)

    resList2 <- PRECASTObj2@resList
    PRECASTObj2 <- SelectModel(PRECASTObj2)

- Note: For parallel compuation based on Rcpp on Linux, users require to use the following system command to set the C_stack unlimited in case of R Error: C stack usage is too close to the limit.

`ulimit -s unlimited`

Besides, user can also use different initialization method by setting `int.model`, for example, set `int.model=NULL`; see the functions `AddParSetting()` and `model_set()` for more details.

Select a best model and re-organize the results by useing `SelectModel()`. Even though `K` is not a vector, it is also necessary to run `SelectModel()` to re-organize the results in `PRECASTObj`. The selected best K is 7 by using command `str(PRECASTObj@resList)`.

    ## check the fitted results: there are four list for the fitted results of each K (6:9).
    str(PRECASTObj@resList)
    ## backup the fitted results in resList
    resList <- PRECASTObj@resList
    # PRECASTObj@resList <- resList
    PRECASTObj <- SelectModel(PRECASTObj)
    ## check the best and re-organized results
    str(PRECASTObj@resList) ## The selected best K is 7

Use ARI to check the performance of clustering:

    true_cluster <- lapply(PRECASTObj@seulist, function(x) x$true_cluster)
    str(true_cluster)
    mclust::adjustedRandIndex(unlist(PRECASTObj@resList$cluster), unlist(true_cluster))

We provide two methods to correct the batch effects in gene expression level. Method (1) is using only PRECAST results to obtain the batch corrected gene expressions if the species of data is unknown or the number of overlapped housekeeping genes between the variable genes in `PRECASTObj@seulist` and the genes in database is less than five. Method (2) is using bouth housekeeping gene and PRECAST results to obtain the batch corrected gene expressions.

- Note: to obtain batch corrected gene expressions based on housekeeping genes as the negative control, users must specify the species of data source and use gene symbol names in `PRECASTObj@seulist`.

Integrate the two samples by the function `IntegrateSpaData`. Because this is a simulated data, we use Method (1) by setting `species='unknown'`.

    seuInt <- IntegrateSpaData(PRECASTObj, species='unknown')
    seuInt
    ## The low-dimensional embeddings obtained by PRECAST are saved in PRECAST reduction slot.

## Visualization

First, user can choose a beautiful color schema using `chooseColors()`.

    cols_cluster <- chooseColors(palettes_name = 'Nature 10', n_colors = 7, plot_colors = TRUE)

Show the spatial scatter plot for clusters

    p12 <- SpaPlot(seuInt, batch=NULL, cols=cols_cluster, point_size=2, combine=TRUE)
    p12
    # users can plot each sample by setting combine=FALSE

Users can re-plot the above figures for specific need by returning a ggplot list object. For example, we only plot the spatial heatmap of first two data batches.

    pList <- SpaPlot(seuInt, batch=NULL, cols=cols_cluster, point_size=2, combine=FALSE, title_name=NULL)
    drawFigs(pList[1:2], layout.dim = c(1,2), common.legend = TRUE, legend.position = 'right', align='hv')

Show the spatial UMAP/tNSE RGB plot

    seuInt <- AddUMAP(seuInt)
    SpaPlot(seuInt, batch=NULL,item='RGB_UMAP',point_size=1, combine=TRUE, text_size=15)

    ## Plot tSNE RGB plot
    #seuInt <- AddTSNE(seuInt)
    #SpaPlot(seuInt, batch=NULL,item='RGB_TSNE',point_size=2, combine=T, text_size=15)

Show the tSNE plot based on the extracted features from PRECAST to check the performance of integration.

    seuInt <- AddTSNE(seuInt, n_comp = 2)

    p1 <- dimPlot(seuInt, item='cluster', font_family='serif', cols=cols_cluster) # Times New Roman
    p2 <- dimPlot(seuInt, item='batch', point_size = 1,  font_family='serif')
    drawFigs(list(p1, p2), common.legend=FALSE, align='hv')
    # It is noted that only sample batch 1 has cluster 4, and only sample batch 2 has cluster 7.

Show the UMAP plot based on the extracted features from PRECAST.

    dimPlot(seuInt, reduction = 'UMAP3', item='cluster', cols=cols_cluster, font_family='serif')

Users can also use the visualization functions in Seurat package:

    library(Seurat)
    p1 <- DimPlot(seuInt[,1: 4226], reduction = 'position', cols=cols_cluster, pt.size =1) # plot the first data batch: first 4226 spots.
    p2 <- DimPlot(seuInt, reduction = 'tSNE',cols=cols_cluster, pt.size=1)
    drawFigs(list(p1, p2), layout.dim = c(1,2), common.legend = TRUE)

Combined differential expression analysis

    dat_deg <- FindAllMarkers(seuInt)
    library(dplyr)
    n <- 2
    dat_deg %>%
      group_by(cluster) %>%
      top_n(n = n, wt = avg_log2FC) -> top10

    head(top10)

Session Info

    sessionInfo()
    #> R version 4.6.0 (2026-04-24 ucrt)
    #> Platform: x86_64-w64-mingw32/x64
    #> Running under: Windows 11 x64 (build 26100)
    #>
    #> Matrix products: default
    #>   LAPACK version 3.12.1
    #>
    #> locale:
    #> [1] LC_COLLATE=C
    #> [2] LC_CTYPE=Chinese (Simplified)_China.utf8
    #> [3] LC_MONETARY=Chinese (Simplified)_China.utf8
    #> [4] LC_NUMERIC=C
    #> [5] LC_TIME=Chinese (Simplified)_China.utf8
    #>
    #> time zone: Asia/Shanghai
    #> tzcode source: internal
    #>
    #> attached base packages:
    #> [1] parallel  stats     graphics  grDevices utils     datasets  methods
    #> [8] base
    #>
    #> other attached packages:
    #> [1] PRECAST_1.9  gtools_3.9.5
    #>
    #> loaded via a namespace (and not attached):
    #>   [1] RcppAnnoy_0.0.23            splines_4.6.0
    #>   [3] later_1.4.8                 tibble_3.3.1
    #>   [5] polyclip_1.10-7             fastDummies_1.7.6
    #>   [7] lifecycle_1.0.5             rstatix_0.7.3
    #>   [9] globals_0.19.1              lattice_0.22-9
    #>  [11] MASS_7.3-65                 backports_1.5.1
    #>  [13] magrittr_2.0.5              plotly_4.12.0
    #>  [15] sass_0.4.10                 rmarkdown_2.31
    #>  [17] jquerylib_0.1.4             yaml_2.3.12
    #>  [19] httpuv_1.6.17               otel_0.2.0
    #>  [21] Seurat_5.5.0                sctransform_0.4.3
    #>  [23] spam_2.11-4                 sp_2.2-1
    #>  [25] spatstat.sparse_3.2-0       reticulate_1.46.0
    #>  [27] cowplot_1.2.0               pbapply_1.7-4
    #>  [29] RColorBrewer_1.1-3          abind_1.4-8
    #>  [31] Rtsne_0.17                  GenomicRanges_1.64.0
    #>  [33] purrr_1.2.2                 BiocGenerics_0.58.1
    #>  [35] IRanges_2.46.0              S4Vectors_0.50.1
    #>  [37] ggrepel_0.9.8               irlba_2.3.7
    #>  [39] listenv_0.10.1              spatstat.utils_3.2-3
    #>  [41] goftest_1.2-3               RSpectra_0.16-2
    #>  [43] spatstat.random_3.5-0       fitdistrplus_1.2-6
    #>  [45] parallelly_1.47.0           codetools_0.2-20
    #>  [47] DelayedArray_0.38.2         scuttle_1.22.0
    #>  [49] tidyselect_1.2.1            farver_2.1.2
    #>  [51] ScaledMatrix_1.20.0         viridis_0.6.5
    #>  [53] matrixStats_1.5.0           stats4_4.6.0
    #>  [55] spatstat.explore_3.8-1      Seqinfo_1.2.0
    #>  [57] jsonlite_2.0.0              BiocNeighbors_2.6.0
    #>  [59] progressr_0.19.0            Formula_1.2-5
    #>  [61] ggridges_0.5.7              survival_3.8-6
    #>  [63] scater_1.40.1               tools_4.6.0
    #>  [65] ica_1.0-3                   Rcpp_1.1.1-1.1
    #>  [67] glue_1.8.1                  gridExtra_2.3
    #>  [69] SparseArray_1.12.2          xfun_0.58
    #>  [71] MatrixGenerics_1.24.0       ggthemes_5.2.0
    #>  [73] dplyr_1.2.1                 formatR_1.14
    #>  [75] fastmap_1.2.0               digest_0.6.39
    #>  [77] rsvd_1.0.5                  R6_2.6.1
    #>  [79] mime_0.13                   colorspace_2.1-2
    #>  [81] scattermore_1.2             tensor_1.5.1
    #>  [83] spatstat.data_3.1-9         tidyr_1.3.2
    #>  [85] generics_0.1.4              data.table_1.18.4
    #>  [87] httr_1.4.8                  htmlwidgets_1.6.4
    #>  [89] S4Arrays_1.12.0             uwot_0.2.4
    #>  [91] pkgconfig_2.0.3             gtable_0.3.6
    #>  [93] lmtest_0.9-40               S7_0.2.2
    #>  [95] SingleCellExperiment_1.34.0 XVector_0.52.0
    #>  [97] htmltools_0.5.9             carData_3.0-6
    #>  [99] dotCall64_1.2               SeuratObject_5.4.0
    #> [101] scales_1.4.0                Biobase_2.72.0
    #> [103] png_0.1-9                   spatstat.univar_3.2-0
    #> [105] knitr_1.51                  rstudioapi_0.18.0
    #> [107] reshape2_1.4.5              nlme_3.1-169
    #> [109] cachem_1.1.0                zoo_1.8-15
    #> [111] stringr_1.6.0               KernSmooth_2.23-26
    #> [113] miniUI_0.1.2                vipor_0.4.7
    #> [115] pillar_1.11.1               grid_4.6.0
    #> [117] vctrs_0.7.3                 RANN_2.6.2
    #> [119] promises_1.5.0              ggpubr_0.6.3
    #> [121] BiocSingular_1.28.0         car_3.1-5
    #> [123] DR.SC_3.7                   beachmat_2.28.0
    #> [125] xtable_1.8-8                cluster_2.1.8.2
    #> [127] beeswarm_0.4.0              evaluate_1.0.5
    #> [129] cli_3.6.6                   compiler_4.6.0
    #> [131] rlang_1.2.0                 future.apply_1.20.2
    #> [133] ggsignif_0.6.4              mclust_6.1.2
    #> [135] plyr_1.8.9                  ggbeeswarm_0.7.3
    #> [137] stringi_1.8.7               viridisLite_0.4.3
    #> [139] deldir_2.0-4                BiocParallel_1.46.0
    #> [141] lazyeval_0.2.3              spatstat.geom_3.8-1
    #> [143] CompQuadForm_1.4.4          Matrix_1.7-5
    #> [145] RcppHNSW_0.7.0              patchwork_1.3.2
    #> [147] future_1.70.0               ggplot2_4.0.3
    #> [149] shiny_1.13.0                SummarizedExperiment_1.42.0
    #> [151] ROCR_1.0-12                 igraph_2.3.2
    #> [153] broom_1.0.13                bslib_0.11.0