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()).| 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 group2Quick 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")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_enrichmentpanoramic_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 14.286040 2.722301e-10 6.125178e-10
#> 2 B A 25 16.534666 1.912825e-63 1.721542e-62
#> 3 C A 25 6.785227 2.683109e-04 4.024663e-04
#> 4 A B 25 15.518254 4.122003e-56 1.854902e-55
#> 5 B B 25 15.159002 4.826270e-14 1.447881e-13
#> 6 C B 25 1.975333 2.034912e-01 2.034912e-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 14.286040 2.2626618 6.313820 2.722301e-10 6.125178e-10
#> 2 B A 25 16.534666 0.9833609 16.814443 1.912825e-63 1.721542e-62
#> 3 C A 25 6.785227 1.8619672 3.644117 2.683109e-04 4.024663e-04
#> 4 A B 25 15.518254 0.9832724 15.782254 4.122003e-56 1.854902e-55
#> 5 B B 25 15.159002 2.0114023 7.536534 4.826270e-14 1.447881e-13
#> 6 A C 25 3.599877 1.0709310 3.361447 7.753537e-04 9.968833e-04
#> 7 C C 25 3.275934 0.8273925 3.959347 7.515482e-05 1.352787e-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 -> Cpanoramic_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"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.panoramic_analyze)