PANORAMIC is designed for multi-sample spatial colocalization analysis. It estimates sample-level spatial effects, then pools them with multilevel meta-analysis to test group-level differences.
The published PANORAMIC paper describes the default workflow as edge-corrected neighborhood enrichment combined with spatial bootstrap uncertainty estimation and multilevel random-effects meta-analysis (Chang et al., Bioinformatics 42(8): btag546, https://doi.org/10.1093/bioinformatics/btag546).
Compared with single-sample analyses, PANORAMIC gives you:
beta_diff, p_diff, fdr_diff)This vignette focuses on practical usage:
panoramic_analyze()).if (!requireNamespace("BiocManager", quietly = TRUE)) {
install.packages("BiocManager")
}
BiocManager::install("panoramic")
library(panoramic)
library(SummarizedExperiment)
library(BiocParallel)
library(dplyr)
library(ggplot2)
| Step | Function | Primary output | Why it matters |
|---|---|---|---|
| 1 | panoramic_prepare() |
list of prepared SpatialExperiments with cached spatial objects |
standardizes per-sample inputs |
| 2 | panoramic_spatialstats() |
SummarizedExperiment with sample-level yi / vi |
quantifies within-sample spatial effects |
| 3 | panoramic_meta_mv() |
pooled multilevel results + differential contrasts | tests group-level biology |
| Convenience | panoramic_analyze() |
list with prep, stats, pooled, tables |
one-call reproducible pipeline |
We simulate two groups of samples with different spatial structure:
group1: mostly random cell mixinggroup2: stronger local colocalization for early cell typesset.seed(123)
toy <- panoramic_simulate_dataset(
n_group1 = 3,
n_group2 = 3,
n_cells_group1 = 200,
n_cells_group2 = 350,
group_labels = c("group1", "group2"),
scenario_group1 = "random",
scenario_group2 = "colocalized",
seed = 123
)
spe_list <- toy$spe_list
design <- toy$design
design
#> sample group
#> 1 sample_1 group1
#> 2 sample_2 group1
#> 3 sample_3 group1
#> 4 sample_4 group2
#> 5 sample_5 group2
#> 6 sample_6 group2
Quick geometric sanity check:
plot_df <- do.call(rbind, lapply(spe_list, function(spe) {
coords <- SpatialExperiment::spatialCoords(spe)
data.frame(
x = coords[, 1],
y = coords[, 2],
cell_type = SummarizedExperiment::colData(spe)$cell_type,
sample_id = SummarizedExperiment::colData(spe)$sample_id,
stringsAsFactors = FALSE
)
}))
ggplot(plot_df, aes(x = x, y = y, color = cell_type)) +
geom_point(size = 0.7, alpha = 0.7) +
facet_wrap(~ sample_id, nrow = 2) +
coord_equal() +
theme_bw() +
labs(x = "x", y = "y", color = "Cell type")
prep <- panoramic_prepare(
spe_list = spe_list,
design = design,
cell_type = "cell_type",
min_cells = 5,
window = "concave"
)
names(S4Vectors::metadata(prep[[1]])$panoramic)
#> [1] "sample_id" "group_id" "ppp" "ppp_rescaled" "marks_tab"
#> [6] "results"
The default PANORAMIC statistic is stat = "local_comp_enrichment",
which reports edge-corrected neighborhood enrichment on a percentage-point
scale relative to a sample-specific null expectation.
For tissue with strong large-scale density variation, an alternative is
stat = "local_comp_global_enrichment". It compares local composition with
the sample-wide target proportion, corresponding to a random-label null that
conditions on the observed cell locations. This can be useful when the
scientific question is label mixing within an observed tissue architecture;
it should not be interpreted as correcting for spatially varying abundance or
as evidence of a direct cell-cell interaction.
radii_um <- c(25)
se <- panoramic_spatialstats(
prep = prep,
radii_um = radii_um,
stat = "local_comp_enrichment",
nsim = 30,
seed = 123,
BPPARAM = BiocParallel::SerialParam()
)
dim(se)
#> [1] 9 6
head(as.data.frame(rowData(se))[, c("ct1", "ct2", "radius_um", "stat")])
#> ct1 ct2 radius_um stat
#> A|A|25 A A 25 local_comp_enrichment
#> B|A|25 B A 25 local_comp_enrichment
#> C|A|25 C A 25 local_comp_enrichment
#> A|B|25 A B 25 local_comp_enrichment
#> B|B|25 B B 25 local_comp_enrichment
#> C|B|25 C B 25 local_comp_enrichment
panoramic_meta_mv() pools sample-level effects and computes
differential contrasts between groups.
# Here patient_col and sample_col are both "sample" in toy data.
# In real data, patient_col can differ from sample_col (e.g., multiple cores per patient).
se_meta <- panoramic_meta_mv(
se,
patient_col = "sample",
group_col = "group",
sample_col = "sample",
tau_structure = "patient",
method = "REML",
group_tau2 = "separate"
)
Each row in rowData(se_meta) is one feature (ct1, ct2,
radius_um) with pooled effects and contrasts.
For differential terms:
beta_diff: stronger spatial effect in group2 vs
group1beta_diff: weaker spatial effect in group2fdr_diff: stronger evidence after multiplicity correctionres <- panoramic_extract_contrast(se_meta)
head(res[, c("ct1", "ct2", "radius_um", "beta_diff", "p_diff", "fdr_diff")])
#> ct1 ct2 radius_um beta_diff p_diff fdr_diff
#> 1 A A 25 13.778909 6.386861e-08 1.437044e-07
#> 2 B A 25 16.573803 1.110021e-79 9.990190e-79
#> 3 C A 25 6.304412 9.937553e-04 1.277685e-03
#> 4 A B 25 15.591724 1.828896e-56 8.230034e-56
#> 5 B B 25 14.802806 2.928143e-11 8.784428e-11
#> 6 C B 25 2.151487 1.724900e-01 1.724900e-01
res %>% dplyr::filter(fdr_diff < 0.05)
#> ct1 ct2 radius_um beta_diff se_diff z_diff p_diff fdr_diff
#> 1 A A 25 13.778909 2.5480554 5.407618 6.386861e-08 1.437044e-07
#> 2 B A 25 16.573803 0.8768544 18.901430 1.110021e-79 9.990190e-79
#> 3 C A 25 6.304412 1.9149024 3.292289 9.937553e-04 1.277685e-03
#> 4 A B 25 15.591724 0.9847328 15.833457 1.828896e-56 8.230034e-56
#> 5 B B 25 14.802806 2.2259389 6.650140 2.928143e-11 8.784428e-11
#> 6 A C 25 3.881903 0.8770517 4.426082 9.596003e-06 1.727281e-05
#> 7 C C 25 2.884636 0.7419748 3.887781 1.011648e-04 1.517473e-04
#> coloc_source coloc_target coloc_direction
#> 1 A A A -> A
#> 2 A B A -> B
#> 3 A C A -> C
#> 4 B A B -> A
#> 5 B B B -> B
#> 6 C A C -> A
#> 7 C C C -> C
panoramic_analyze)Use panoramic_analyze() when you want a single reproducible entry
point for end-to-end runs.
out <- panoramic_analyze(
spe_list = spe_list,
design = design,
cell_type = "cell_type",
radii_um = radii_um,
nsim = 20,
min_cells = 5,
window = "concave",
BPPARAM = BiocParallel::SerialParam()
)
names(out)
#> [1] "prep" "stats" "pooled" "tables"
names(out$tables)
#> [1] "spatialstats" "meta" "contrast"
plot_forest(
se_meta,
ct1 = "A",
ct2 = "B",
radius_um = 25,
group_col = "group"
)
plot_volcano(se_meta)
net <- create_spatial_network(
se_meta,
fdr_threshold = 0.2,
directed = FALSE,
leiden_resolution = 1.0
)
plot_spatial_network(
se_meta,
fdr_threshold = 0.2,
directed = FALSE,
layout = "fr",
node_size_by = "degree"
)
local_comp_enrichment unless you have a specific reason
to use the global-composition or L/K-function alternatives.nsim for final analyses (the vignette uses modest values
for speed).sample, group, patient) explicit and
consistent early in your workflow.For complementary spatial analysis workflows, see:
For methodological details and biological applications in colorectal cancer and head and neck squamous cell carcinoma tissue microarrays, see the published PANORAMIC paper:
sessionInfo()
#> R version 4.6.1 Patched (2026-06-24 r90190)
#> Platform: x86_64-apple-darwin20
#> Running under: macOS Ventura 13.7.8
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
#>
#> locale:
#> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> time zone: America/New_York
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] ggplot2_4.0.3 dplyr_1.2.1
#> [3] BiocParallel_1.47.0 SummarizedExperiment_1.43.0
#> [5] Biobase_2.73.2 GenomicRanges_1.65.4
#> [7] Seqinfo_1.3.2 IRanges_2.47.5
#> [9] S4Vectors_0.51.10 BiocGenerics_0.59.12
#> [11] generics_0.1.4 MatrixGenerics_1.25.0
#> [13] matrixStats_1.5.0 panoramic_0.99.4
#>
#> loaded via a namespace (and not attached):
#> [1] tidyselect_1.2.1 viridisLite_0.4.3
#> [3] concaveman_1.2.0 farver_2.1.2
#> [5] viridis_0.6.5 S7_0.2.2
#> [7] ggraph_2.2.2 fastmap_1.2.0
#> [9] SingleCellExperiment_1.35.2 spatstat.geom_3.8-3
#> [11] tweenr_2.0.3 mathjaxr_2.0-0
#> [13] digest_0.6.39 lifecycle_1.0.5
#> [15] spatstat.data_3.1-9 magrittr_2.0.5
#> [17] compiler_4.6.1 rlang_1.3.0
#> [19] tools_4.6.1 yaml_2.3.12
#> [21] igraph_2.3.3 knitr_1.52
#> [23] S4Arrays_1.13.1 labeling_0.4.3
#> [25] graphlayouts_1.2.5 curl_8.0.0
#> [27] DelayedArray_0.39.7 RColorBrewer_1.1-3
#> [29] abind_1.4-8 withr_3.0.3
#> [31] purrr_1.2.2 numDeriv_2016.8-1.1
#> [33] grid_4.6.1 polyclip_1.10-7
#> [35] scales_1.4.0 MASS_7.3-66
#> [37] spatstat.utils_3.2-5 dichromat_2.0-1
#> [39] cli_3.6.6 rmarkdown_2.32
#> [41] metafor_5.2-1 otel_0.2.0
#> [43] rjson_0.2.23 cachem_1.1.0
#> [45] ggforce_0.5.0 metadat_1.6-0
#> [47] parallel_4.6.1 BiocManager_1.30.27
#> [49] XVector_0.53.0 vctrs_0.7.3
#> [51] V8_8.2.0 Matrix_1.7-6
#> [53] jsonlite_2.0.0 ggrepel_0.9.8
#> [55] magick_2.9.1 spatstat.univar_3.2-0
#> [57] tidyr_1.3.2 glue_1.8.1
#> [59] codetools_0.2-20 gtable_0.3.6
#> [61] deldir_2.0-4 tibble_3.3.1
#> [63] pillar_1.11.1 htmltools_0.5.9
#> [65] R6_2.6.1 tidygraph_1.3.1
#> [67] evaluate_1.0.5 lattice_0.23-1
#> [69] SpatialExperiment_1.23.0 memoise_2.0.1
#> [71] BiocStyle_2.41.0 Rcpp_1.1.2
#> [73] gridExtra_2.3.1 SparseArray_1.13.3
#> [75] nlme_3.1-171 xfun_0.61
#> [77] pkgconfig_2.0.3