| Title: | Test Changes in the Co-Localisation of Cell Types Between Groups of Patients |
|---|---|
| Description: | spicyR tests whether the spatial co-localisation of pairs of cell types differs between groups of patients, or is associated with patient outcome, in cell-resolution spatial omics data (imaging mass cytometry, CODEX, MIBI, Xenium, CosMx and others). Its default method, spicyR Cell, counts the cells of one type within a radius of each cell of another and compares the count with its exact expectation under random labelling of the observed cells. Images are combined within patients by a frailty generalised estimating equation, and differences are tested with a small-sample cluster-robust (CR2) variance with patients as the units. Covariates, several conditions, several radii and survival outcomes are supported, and the original image-level test is kept as an option. The computations run in a C++ core shared with the Python package spicyr. |
| Authors: | Nicolas Canete [aut], Ellis Patrick [aut, cre], Sadiq Dohadwalla [aut], Elijah Willie [aut], Nicholas Robertson [ctb], Alex Qin [ctb], Farhan Ameen [ctb], Shreya Rao [ctb] |
| Maintainer: | Ellis Patrick <[email protected]> |
| License: | GPL (>=2) |
| Version: | 1.99.47 |
| Built: | 2026-11-04 07:52:10 UTC |
| Source: | https://github.com/bioc/spicyR |
Returns, for each image, its condition and the value of each pair that spicy() tested: for the cell
method its effect (by default the extra fraction of to cells with a from cell within r;
with effect = "count", the extra from cells per to cell), or the L-function summary for
the image method. Use it for your own plots or models, or with from and to to look up the images
behind one pair.
bind(results, pairName = NULL, from = NULL, to = NULL, parent = NULL)bind(results, pairName = NULL, from = NULL, to = NULL, parent = NULL)
results |
The |
pairName |
A pair, as |
from, to
|
One pair: a table of that pair alone, with its value in a column |
parent |
For a Kontextual result, the parent population of the pair given by |
A data.frame with one row per image, named by image: imageID, condition,
subject (if given) and one column per pair, named "from__to", or with from and
to the columns effect (or value) and weight. NA where the pair could not be
measured in an image.
data(spicyTest) df <- bind(spicyTest) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = "Th") head(bind(res, from = "Tc", to = "Th"))data(spicyTest) df <- bind(spicyTest) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = "Th") head(bind(res, from = "Tc", to = "Th"))
Finds the k nearest neighbours of every cell within its own image and stores them as a
colPair of cells, ready for convPairs(). Distances are Euclidean, a cell is not its own
neighbour, and ties are broken as in spatstat.geom::nnwhich(). The graph is the one
imcRtools::buildSpatialGraph(type = "knn") builds, computed by the C++ core that spicy()
uses for k.
buildKnnGraph( cells, k = 20, imageID = "imageID", spatialCoords = c("x", "y"), name = "knn_interaction_graph", cores = 1 )buildKnnGraph( cells, k = 20, imageID = "imageID", spatialCoords = c("x", "y"), name = "knn_interaction_graph", cores = 1 )
cells |
A |
k |
The number of neighbours of each cell. Cells in images with at most |
imageID |
The |
spatialCoords |
The |
name |
The name of the |
cores |
The number of threads; images are processed in parallel. |
cells, with the graph in colPair(cells, name): one edge from each cell to each of
its neighbours.
data("diabetesData") diabetesData <- buildKnnGraph(diabetesData, k = 10) head(SingleCellExperiment::colPair(diabetesData, "knn_interaction_graph"))data("diabetesData") diabetesData <- buildKnnGraph(diabetesData, k = 10) head(SingleCellExperiment::colPair(diabetesData, "knn_interaction_graph"))
Shuffles the condition across patients (or across images, without subject), which removes any real difference
between the groups while keeping everything else about the study, and runs spicy() on each shuffle. If the test
is calibrated, about 5% of pairs have p < 0.05 and about 1% p < 0.01 in each shuffle. Pairs share cells, so the
percentage moves by a few points from one shuffle to the next; look at the average over shuffles.
calibrationCheck( cells, condition, subject = NULL, nShuffles = 10, thresholds = c(0.05, 0.01), ... ) ## S3 method for class 'spicyCalibration' summary(object, ...) ## S3 method for class 'spicyCalibration' plot(x, ...)calibrationCheck( cells, condition, subject = NULL, nShuffles = 10, thresholds = c(0.05, 0.01), ... ) ## S3 method for class 'spicyCalibration' summary(object, ...) ## S3 method for class 'spicyCalibration' plot(x, ...)
cells |
The cells, as for |
condition |
The column of the condition to shuffle (a factor, character or |
subject |
The column of the patient. Each patient keeps one condition, moved as a whole; without it, the condition is shuffled across images. |
nShuffles |
The number of shuffles. |
thresholds |
The p-value thresholds to report. |
... |
Other arguments of |
object, x
|
The result of |
A data.frame with one row per shuffle and threshold: shuffle, threshold and percent, the
percentage of pairs whose p-value is below the threshold. summary() gives the average percentage over the
shuffles at each threshold, with the expected one, and plot() shows each shuffle against the expected
percentage.
data("diabetesData") set.seed(1) shuffles <- calibrationCheck(diabetesData, condition = "stage", subject = "case", r = 50, nShuffles = 2) summary(shuffles) plot(shuffles)data("diabetesData") set.seed(1) shuffles <- calibrationCheck(diabetesData, condition = "stage", subject = "case", r = 50, nShuffles = 2) summary(shuffles) plot(shuffles)
Perform a simple wilcoxon-rank-sum test or t-test on the columns of a data frame
colTest( df, condition, type = NULL, feature = NULL, imageID = "imageID", subject = NULL )colTest( df, condition, type = NULL, feature = NULL, imageID = "imageID", subject = NULL )
df |
A data.frame or SingleCellExperiment, SpatialExperiment |
condition |
The condition of interest |
type |
The type of test, "wilcox", "ttest" or "survival". |
feature |
Can be used to calculate the proportions of this feature for each image |
imageID |
The imageID's if presenting a SingleCellExperiment |
subject |
Optional, with a SingleCellExperiment or SpatialExperiment: the column of the patient. The proportions are then those of each patient's cells, all images pooled, and each patient is one unit of the test. |
With a matrix or data.frame df and a condition whose names (a vector, factor or Surv with names) or
row names (a matrix or data.frame) are row names of df, the rows of df are matched to condition by name,
and rows without a condition are left out. Otherwise condition is taken in the row order of df.
A data.frame with one row per column of df (for example per cell type), ordered by p-value:
the mean (t-test) or median (Wilcoxon test) in each condition, the test statistic, the p-value (pval),
the Benjamini-Hochberg adjusted p-value (adjPval) and the column name (cluster).
# Test for a difference in cell-type proportions between onset and long-duration diabetes # (images are treated as independent here, which ignores that each donor has several) data("diabetesData") props <- getProp(diabetesData) images <- unique(as.data.frame(SummarizedExperiment::colData(diabetesData))[, c("imageID", "stage")]) condition <- setNames(as.character(images$stage), images$imageID) condition <- condition[condition %in% c("Long-duration", "Onset")] test <- colTest(props, condition) # the rows of props are matched to condition by name head(test) # with donors as the units: each donor's cells, all images pooled diabetes <- diabetesData[, diabetesData$stage %in% c("Long-duration", "Onset")] head(colTest(diabetes, condition = "stage", feature = "cellType", subject = "case", type = "wilcox"))# Test for a difference in cell-type proportions between onset and long-duration diabetes # (images are treated as independent here, which ignores that each donor has several) data("diabetesData") props <- getProp(diabetesData) images <- unique(as.data.frame(SummarizedExperiment::colData(diabetesData))[, c("imageID", "stage")]) condition <- setNames(as.character(images$stage), images$imageID) condition <- condition[condition %in% c("Long-duration", "Onset")] test <- colTest(props, condition) # the rows of props are matched to condition by name head(test) # with donors as the units: each donor's cells, all images pooled diabetes <- diabetesData[, diabetesData$stage %in% c("Long-duration", "Onset")] head(colTest(diabetes, condition = "stage", feature = "cellType", subject = "case", type = "wilcox"))
Converts colPairs object into an abundance matrix based on number of nearby interactions for every cell type.
convPairs(cells, colPair, imageID = "imageID", cellType = "cellType")convPairs(cells, colPair, imageID = "imageID", cellType = "cellType")
cells |
A SingleCellExperiment that contains objects in the colPairs slot. |
colPair |
The name of the object in the colPairs slot for which the dataframe is constructed from. |
imageID |
The image ID if using SingleCellExperiment. |
cellType |
The cell type if using SingleCellExperiment. |
Matrix of abundances
data("diabetesData") images <- c("A09", "A11", "A16", "A17") diabetesData <- diabetesData[ , SummarizedExperiment::colData(diabetesData)$imageID %in% images ] diabetesData <- buildKnnGraph(diabetesData, k = 20) pairAbundances <- convPairs(diabetesData, colPair = "knn_interaction_graph" )data("diabetesData") images <- c("A09", "A11", "A16", "A17") diabetesData <- diabetesData[ , SummarizedExperiment::colData(diabetesData)$imageID %in% images ] diabetesData <- buildKnnGraph(diabetesData, k = 20) pairAbundances <- convPairs(diabetesData, colPair = "knn_interaction_graph" )
Cell types and positions from imaging mass cytometry of pancreas sections from 12 donors at three stages of type 1 diabetes (Damond et al. 2019): 4 non-diabetic donors, 4 at onset and 4 with long-duration disease. The object holds 253,777 cells from 120 images, 10 images per donor. It has no assays: only the cell data used by spicyR.
data("diabetesData")data("diabetesData")
A SingleCellExperiment with one column per cell and these colData columns:
the image (character)
cell identifiers, in the whole data set and within its image
the cell's coordinates in the image, in micrometres
the cell type assigned by the authors (factor)
the donor (integer)
the slide, and the part of the pancreas (head, body or tail)
the stage of type 1 diabetes, as a code and as a factor with levels
"Non-diabetic", "Onset" and "Long-duration"
Damond N et al. (2019). Mendeley Data, doi:10.17632/cydmwsfztj.1, under the CC BY 4.0 licence. How the
subset was made is described in inst/scripts/make-diabetesData.R.
Damond N, Engler S, Zanotelli VRT, et al. (2019). A map of human type 1 diabetes progression by imaging mass cytometry. Cell Metabolism 29(3), 755-768. doi:10.1016/j.cmet.2018.11.014
data("diabetesData") table(unique(as.data.frame(SummarizedExperiment::colData(diabetesData))[, c("case", "stage")])$stage)data("diabetesData") table(unique(as.data.frame(SummarizedExperiment::colData(diabetesData))[, c("case", "stage")])$stage)
Get statistic from pairwise L curve of a single image.
getPairwise( cells, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL, sigma = NULL, from = NULL, to = NULL, cores = 1, minLambda = 0.05, window = "convex", window.length = NULL, edgeCorrect = TRUE, includeZeroCells = FALSE, BPPARAM = NULL, imageIDCol = imageID, cellTypeCol = cellType, spatialCoordCols = spatialCoords, nCores = cores, Rs = r, cellWeights = NULL )getPairwise( cells, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL, sigma = NULL, from = NULL, to = NULL, cores = 1, minLambda = 0.05, window = "convex", window.length = NULL, edgeCorrect = TRUE, includeZeroCells = FALSE, BPPARAM = NULL, imageIDCol = imageID, cellTypeCol = cellType, spatialCoordCols = spatialCoords, nCores = cores, Rs = r, cellWeights = NULL )
cells |
A data.frame, SingleCellExperiment or SpatialExperiment with one row (column) per cell, giving its image, cell type and coordinates. |
imageID |
The name of the image ID column. |
cellType |
The name of the cell type column. |
spatialCoords |
The names of the coordinate columns. |
r |
A vector of the radii that the measures of association should be calculated over. |
sigma |
A numeric variable used for scaling when fitting inhomogenous L-curves. |
from |
The 'from' cellType for generating the L curve. |
to |
The 'to' cellType for generating the L curve. |
cores |
Number of cores to use for parallel processing or a BiocParallel MulticoreParam or SerialParam object. |
minLambda |
Minimum value density for scaling when fitting inhomogeneous L-curves. |
window |
Should the window around the regions be 'square', 'convex' or 'concave'. |
window.length |
A tuning parameter for controlling the level of concavity when estimating concave windows. |
edgeCorrect |
A logical indicating whether to perform edge correction. |
includeZeroCells |
A logical indicating whether to include cells with zero counts in the pairwise association. |
BPPARAM |
{DEPRECATED} A BiocParallel MulticoreParam or SerialParam object. |
imageIDCol |
{DEPRECATED} The name of the imageID column if using a SingleCellExperiment or SpatialExperiment. |
cellTypeCol |
{DEPRECATED} The name of the cellType column if using a SingleCellExperiment or SpatialExperiment. |
spatialCoordCols |
{DEPRECATED} The names of the spatialCoords column if using a SingleCellExperiment. |
nCores |
{DEPRECATED} Number of cores to use for parallel processing or a BiocParallel MulticoreParam or SerialParam object. |
Rs |
{DEPRECATED} A vector of the radii that the measures of association should be calculated over. |
cellWeights |
Optional cell weights in \[0, 1\], in the order of |
Statistic from pairwise L-curve of a single image.
data("diabetesData") # Subset by imageID for fast example selected_cells <- diabetesData[ , SummarizedExperiment::colData(diabetesData)$imageID == "A09" ] pairAssoc <- getPairwise(selected_cells)data("diabetesData") # Subset by imageID for fast example selected_cells <- diabetesData[ , SummarizedExperiment::colData(diabetesData)$imageID == "A09" ] pairAssoc <- getPairwise(selected_cells)
Computes a per-image, per-cell-type-pair spatial localisation statistic as a
drop-in alternative to the fixed-radius L-function statistic returned by
getPairwise. Instead of a search radius r, each reference
cell's neighbourhood is its k nearest neighbours (Euclidean distance,
any cell type, the cell itself excluded).
getPairwiseProp( cells, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), k = 15, from = NULL, to = NULL, cores = 1, includeZeroCells = FALSE, BPPARAM = NULL )getPairwiseProp( cells, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), k = 15, from = NULL, to = NULL, cores = 1, includeZeroCells = FALSE, BPPARAM = NULL )
cells |
A SingleCellExperiment, SpatialExperiment or data.frame. |
imageID |
The name of the imageID column if a column name is provided. |
cellType |
The name of the cellType column if a column name is provided. |
spatialCoords |
The names of the spatial coordinates columns if provided. |
k |
Number of nearest neighbours to search per reference cell. |
from |
The reference cell types. Defaults to all cell types. |
to |
The target cell types. Defaults to all cell types. |
cores |
Number of threads for processing images in parallel (a BiocParallelParam object is also accepted, for backward compatibility). |
includeZeroCells |
If FALSE (default), image-pairs where the reference or target cell type is
absent are returned as NA. If TRUE, image-pairs whose reference type is
absent (but whose target type is present, so |
BPPARAM |
A BiocParallelParam object; its number of workers overrides
|
For an ordered pair (from = reference type A, to = target type
B), the statistic for image is the plain observed-over-expected ratio
where is the mean over reference cells of the fraction of their
k nearest neighbours that are type B, and is the image's
background proportion of type-B cells (B count / total cell count).
is the null (complete spatial randomness);
indicates attraction/enrichment and depletion. No
variance-stabilising transform (arcsine, log) is applied: is bounded
below at 0 and right-skewed, a rougher fit to the linear model's Gaussian-noise
assumption than a transformed statistic, but chosen for direct
interpretability ("observed is 1.4x expected").
The returned matrix is shaped identically to getPairwise()'s output
(images in rows, from__to cell-type pairs in columns, same order), so it
can be passed straight to spicy() as alternateResult to run the
standard spicyR weighting and linear (mixed) model pipeline on the ratio.
Weights. spicy()'s variance-weighting model (weights =
TRUE, the default) is calibrated to the L-function's numeric scale. It works
for this ratio when the dataset has enough cross-pair variance heterogeneity
(e.g. several cell types, some rare) to anchor the fit, but on a dataset with
few cell types or weak spatial structure it can degenerate and return an
all-NA p-value table. If that happens, refit with weights =
FALSE.
No edge correction is applied: estimates neighbourhood
composition rather than a count within an area, so a boundary cell's truncated
neighbourhood carries the same null label distribution as an interior cell's.
k controls only the variance of , not the location of its
null value.
A matrix with one row per image and one column per from__to
cell-type pair, containing the observed/expected proportion ratio.
data("diabetesData") propAssoc <- getPairwiseProp(diabetesData, k = 15) # Use as the spicyR response, reusing the standard model: spicy(diabetesData, condition = "stage", subject = "case", alternateResult = propAssoc )data("diabetesData") propAssoc <- getPairwiseProp(diabetesData, k = 15) # Use as the spicyR response, reusing the standard model: spicy(diabetesData, condition = "stage", subject = "case", alternateResult = propAssoc )
Get proportions from a SummarizedExperiment.
getProp(cells, feature = "cellType", imageID = "imageID")getProp(cells, feature = "cellType", imageID = "imageID")
cells |
A SingleCellExperiment, SpatialExperiment or data.frame. |
feature |
The feature of interest |
imageID |
The imageID's |
Proportions
data("diabetesData") prop <- getProp(diabetesData)data("diabetesData") prop <- getProp(diabetesData)
Builds a pairwise scatterplot of marker-marker associations for a single image
using the output of getPairwise().
imageCrossPlot( result, image = NULL, colourGradient = c("#4575B4", "white", "#D73027"), marksToPlot = NULL, limits = NULL )imageCrossPlot( result, image = NULL, colourGradient = c("#4575B4", "white", "#D73027"), marksToPlot = NULL, limits = NULL )
result |
A matrix or data frame produced by |
image |
Character scalar. Which image (row in |
colourGradient |
Character vector of length 3 giving the low, mid, and
high colours for a diverging palette used by |
marksToPlot |
Optional character vector of marker names. If supplied, the
plot is restricted to rows and columns where both |
limits |
Numeric length-2 vector giving the lower and upper caps applied
to association values for colour mapping. Use |
The function expects colnames(result) to contain "__" separating marker
names; otherwise an error is thrown. Values are transformed into two aesthetics:
value: the (possibly clamped) signed association used for colour.
size: the (possibly clamped) absolute association used for point size.
The colourbar shows three ticks at lower, 0, and upper
with labels "<lower", "0", and ">upper".
A ggplot2 object.
# Minimal example (toy data) set.seed(1) mks <- c("A","B","C") pairs <- as.vector(outer(mks, mks, paste, sep="__")) res <- matrix(rnorm(length(pairs)*2), nrow = 2, dimnames = list(c("img1","img2"), pairs)) p <- imageCrossPlot(res, image = "img1", colourGradient = c("#4575B4","white","#D73027"), marksToPlot = c("A","B","C"), limits = c(-3, 3)) print(p)# Minimal example (toy data) set.seed(1) mks <- c("A","B","C") pairs <- as.vector(outer(mks, mks, paste, sep="__")) res <- matrix(rnorm(length(pairs)*2), nrow = 2, dimnames = list(c("img1","img2"), pairs)) p <- imageCrossPlot(res, image = "img1", colourGradient = c("#4575B4","white","#D73027"), marksToPlot = c("A","B","C"), limits = c(-3, 3)) print(p)
from and to cells of a pairWithout from and to, every cell is shown, coloured by its cell type. With them, the density of all cells is
shown in blue, with the from and to cells on top. With r, a circle of radius r is drawn around each to
cell: the from cells inside the circles are those that spicyR counts. Several images are drawn side by side, on
the same scale.
plotImage( cells, imageToPlot, from = NULL, to = NULL, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL )plotImage( cells, imageToPlot, from = NULL, to = NULL, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL )
cells |
A SummarizedExperiment object. |
imageToPlot |
The ID of the image to plot, or several IDs. |
from |
The "from" cell type. Optional, with |
to |
The "to" cell type. Optional, with |
imageID |
The name of the imageID column in the SummarizedExperiment object. |
cellType |
The name of the cellType column in the SummarizedExperiment object. |
spatialCoords |
The names of the spatialCoords column if using a SingleCellExperiment. |
r |
Optional radius: draw a circle of this radius around each |
A ggplot object.
data("diabetesData") plotImage(diabetesData, "A09") plotImage(diabetesData, "A09", from = "acinar", to = "alpha") plotImage(diabetesData, "A09", from = "acinar", to = "alpha", r = 50) plotImage(diabetesData, c("A09", "A11"), from = "acinar", to = "alpha")data("diabetesData") plotImage(diabetesData, "A09") plotImage(diabetesData, "A09", from = "acinar", to = "alpha") plotImage(diabetesData, "A09", from = "acinar", to = "alpha", r = 50) plotImage(diabetesData, c("A09", "A11"), from = "acinar", to = "alpha")
For a spicy() result with several radii, plots the p-value of each chosen pair at every radius, to show the
scale at which the evidence is strongest. The combined p-value over the radii is in topPairs().
radiusPlot(results, from, to)radiusPlot(results, from, to)
results |
The |
from, to
|
The |
A ggplot.
data("diabetesData") diabetes <- diabetesData[, diabetesData$stage %in% c("Non-diabetic", "Onset")] res <- spicy(diabetes, condition = "stage", subject = "case", r = c(25, 50, 75), from = c("Tc", "Th"), to = "beta") radiusPlot(res, from = c("Tc", "Th"), to = "beta")data("diabetesData") diabetes <- diabetesData[, diabetesData$stage %in% c("Non-diabetic", "Onset")] res <- spicy(diabetes, condition = "stage", subject = "case", r = c(25, 50, 75), from = c("Tc", "Th"), to = "beta") radiusPlot(res, from = c("Tc", "Th"), to = "beta")
Plots result of signifPlot.
signifPlot( results, fdr = FALSE, type = "bubble", breaks = NULL, comparisonGroup = NULL, colours = c("#4575B4", "white", "#D73027"), marksToPlot = NULL, cutoff = 0.05, contextColours = NULL, contextLabels = waiver() )signifPlot( results, fdr = FALSE, type = "bubble", breaks = NULL, comparisonGroup = NULL, colours = c("#4575B4", "white", "#D73027"), marksToPlot = NULL, cutoff = 0.05, contextColours = NULL, contextLabels = waiver() )
results |
A spicy results object |
fdr |
TRUE if FDR correction is used. |
type |
Whether to make a bubble plot ( |
breaks |
Vector of 3 numbers giving breaks used in legend. The first number is the minimum, the second is the maximum, the third is the number of breaks. |
comparisonGroup |
A string specifying the name of the outcome group to compare with the base group. |
colours |
Vector of colours to use to colour legend. |
marksToPlot |
Vector of marks to include in plot. |
cutoff |
significance threshold for circles in bubble plot. |
contextColours |
Used for |
contextLabels |
Used for |
The heatmap has a tile for each pair, from in the rows and to in the columns. A tile's colour is the
pair's signed log10 p-value for the condition (of the BH-adjusted p-value with fdr = TRUE): -log10(p)
when the coefficient is positive and log10(p) when it is negative, so the most significant positive
differences are at the top of the scale and the most significant negative ones at the bottom. breaks sets
the scale; values beyond it take the end colours, and pairs without a p-value are grey. The values behind
either plot are available from topPairs() (n = NULL for every pair).
a ggplot object
data(spicyTest) p <- signifPlot(spicyTest, breaks = c(-3, 3, 0.5)) p signifPlot(spicyTest, type = "heatmap")data(spicyTest) p <- signifPlot(spicyTest, breaks = c(-3, 3, 0.5)) p signifPlot(spicyTest, type = "heatmap")
spicy() tests, for every ordered pair of cell types from → to, whether the co-localisation of
the two types differs between conditions, or is associated with survival. A pair asks whether to
cells are placed near from cells more than other cells are.
spicy( cells, condition, subject = NULL, covariates = NULL, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL, from = NULL, to = NULL, method = c("cell", "image"), effect = c("allocation", "count"), k = NULL, combine = c("maxT", "davies", "cauchy"), adjustAbundance = FALSE, variance = c("cr2", "hartung_knapp"), ref = NULL, cores = 1, tableKey = NULL, imageInfo = NULL, cellWeights = NULL, ... )spicy( cells, condition, subject = NULL, covariates = NULL, imageID = "imageID", cellType = "cellType", spatialCoords = c("x", "y"), r = NULL, from = NULL, to = NULL, method = c("cell", "image"), effect = c("allocation", "count"), k = NULL, combine = c("maxT", "davies", "cauchy"), adjustAbundance = FALSE, variance = c("cr2", "hartung_knapp"), ref = NULL, cores = 1, tableKey = NULL, imageInfo = NULL, cellWeights = NULL, ... )
cells |
A |
condition |
The column of the image-level condition: two groups, or a |
subject |
The column of the patient (unit) of each image. Images of one patient are combined; if omitted, every image is its own patient. |
covariates |
Image- or patient-level columns to adjust for (cell method: added to the design of
the excess, and the effect of each is reported as |
imageID, cellType, spatialCoords
|
Column names of the image, cell type and coordinates. For a Zarr
store, |
r |
Radius (or radii) of the neighbourhood, in the units of the coordinates. Cell method: one
radius (default 50), or several to be combined by |
from, to
|
Cell types to test (all ordered pairs by default). |
method |
|
effect |
Cell method: |
k |
Cell method: use the |
combine |
Cell method with several radii: |
adjustAbundance |
Cell method: adjust the test for the log share of the |
variance |
Cell method: |
ref |
Cell method: the reference level of |
cores |
Number of threads (cell method) or cores (image method). |
tableKey |
Cell method with a SpatialData store or object: the name of the cell table in |
imageInfo |
Cell method: a table with one row per image, for |
cellWeights |
Optional cell weights in [0, 1], in the order of |
... |
Arguments of the image method: |
method = "cell" (the default). In each image, the share of to cells with at least one from
cell within radius r is compared with its exact expectation q if the to cells were a random choice
among the cells that are not from cells. The effect (effect = "allocation", the default) is the
fraction of to cells placed next to (or kept away from) from cells. For a pair that attracts
(more to cells next to from cells than q over all images together) it is (observed share - q) / (1 - q):
if a fraction f of the to cells were moved next to from cells, the effect is f. For a pair that avoids
it is (observed share - q) / q: if a fraction f of the to cells that would have a from cell nearby were
moved away, the effect is -f. Either way it does not depend on how many from cells there are or how
densely they are packed. The side is chosen once per pair from all images with a condition, without the
conditions, and is reported in the side column. effect = "count" gives the number of extra from
cells within r of each to cell instead; it also reflects how many from cells surround a to cell
(depth of infiltration), but it grows with how densely the from cells are packed, so a change in
packing alone can appear as a change in co-localisation. Images are combined within patients and
patients within conditions by a frailty GEE with a second random effect for the images of a patient (each image
is weighted by 1 / (its sampling variance + the between-image variance), so one large image cannot outweigh the
patient's other images), and the difference between conditions is tested with a
CR2 cluster-robust variance on Satterthwaite degrees of freedom, with patients (subject) as the
units. The difference is adjusted for any covariates, and, with adjustAbundance = TRUE, for the
log share of the from type in each image; the unadjusted test is then reported alongside
(unadjusted_* columns).
When nearly every cell has a from cell within r (q close to 1), there is little room for attraction and
an attracting pair's images carry little information; a smaller r is more informative.
The allocation effect is bounded: an image of an avoiding pair with no to cell next to a from cell has the
effect -1, and an image of an attracting pair with every to cell next to one has +1. A patient is informative at
a radius when at least one of its images is not at -1 or +1. A radius where one condition has no informative
patient is not tested: that condition's patients all have the same effect, so its spread is exactly 0 and the
standard error would rest on the other condition alone. Its p-value is NA (with adjustment, adjusted_for is
"none (no_information)"), and it is left out of the combination over radii. With more than two conditions, a
pair where any condition has no informative patient is not tested.
The conditions share one dispersion. Each condition's CR2 variance is divided by its
model-based variance, the ratios are averaged over the conditions with their numbers of patients as weights,
and the model-based variance of the difference is multiplied by this average. The degrees of freedom are those
of CR2. A small condition's own spread does not set the standard error alone, which matters when the conditions
are of unequal size and the patients' effects are skewed. With adjustment, the ratios are those of the
coefficient of each condition in the adjusted design, and every coefficient's variance is scaled the same way.
variance = "hartung_knapp" keeps its own variance.
A patient's weight within its condition grows with the information of its images, so one or two patients can
carry nearly all of a condition's weight. A radius where a condition's effective patients (Kish's
(sum w)^2 / sum w^2 of the patients' weights w) fall below the smaller of 3 and half the number of its patients
that contribute at that radius is not tested: its p-value is NA (with adjustment, adjusted_for is
"none (few_effective_units)"), and it is left out of the combination over radii. A condition of 3 patients with
equal weights can therefore be tested. With more than two conditions, a pair where any condition falls below this
is not tested.
Images with a missing condition are left out of every step: the choice of the side, the between-image variance and the tests.
method = "image" (the original spicyR test). A per-image L-function summary of each pair is
compared between conditions with a weighted linear model, or a mixed model when subject is given
(Canete et al. 2022). With cellWeights (see getPairwise()), cells enter each label with a weight,
for example the confidence of a classifier, instead of through a single cell type.
A SpicyResults object. topPairs(), signifPlot(), spicyBoxPlot() and bind() work
for both methods. For the cell method, $cellResults holds the full table: for the allocation effect,
the pair's side ("attract" or "avoid", which sets the scale of the effect), the effect in each
condition (excess_ref, excess_comp, at the average covariates), the difference
(excess_difference), its standard error, df, p-value and BH-adjusted p-value, the frailty variance, the
between-image variance (tau2_image) and, when the test was adjusted, what for (adjusted_for), the effect
and p-value of each adjustment, and the unadjusted test (unadjusted_difference, unadjusted_p_value, unadjusted_p_adj). $effect
records which effect was estimated.
Canete NP et al. (2022). spicyR: spatial analysis of in situ cytometry data in R. Bioinformatics 38(11), 3099-3105. doi:10.1093/bioinformatics/btac268
Bell RM, McCaffrey DF (2002). Bias reduction in standard errors for linear regression with multi-stage samples. Survey Methodology 28(2), 169-181.
DerSimonian R, Laird N (1986). Meta-analysis in clinical trials. Controlled Clinical Trials 7(3), 177-188. doi:10.1016/0197-2456(86)90046-2
Pustejovsky JE, Tipton E (2018). Small-sample methods for cluster-robust variance estimation and hypothesis testing in fixed effects models. Journal of Business & Economic Statistics 36(4), 672-683. doi:10.1080/07350015.2016.1247004
Paule RC, Mandel J (1982). Consensus values and weighting factors. Journal of Research of the National Bureau of Standards 87(5), 377-385. doi:10.6028/jres.087.022
Hartung J, Knapp G (2001). A refined method for the meta-analysis of controlled clinical trials with binary outcome. Statistics in Medicine 20(24), 3875-3889. doi:10.1002/sim.1009
Liu Y, Xie J (2020). Cauchy combination test: a powerful test with analytic p-value calculation under arbitrary dependency structures. Journal of the American Statistical Association 115(529), 393-402. doi:10.1080/01621459.2018.1554485
data("diabetesData") # spicyR Cell: patients ("case") are the units # the extra fraction of Th cells, and of beta cells, with a Tc cell within 50 units res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) topPairs(res) res$cellResults # the extra number of Tc cells within 50 units of each Th cell resCount <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = "Th", effect = "count") # the original image-level test resImage <- spicy(diabetesData, condition = "stage", subject = "case", from = "Tc", to = "Th", method = "image") topPairs(resImage)data("diabetesData") # spicyR Cell: patients ("case") are the units # the extra fraction of Th cells, and of beta cells, with a Tc cell within 50 units res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) topPairs(res) res$cellResults # the extra number of Tc cells within 50 units of each Th cell resCount <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = "Th", effect = "count") # the original image-level test resImage <- spicy(diabetesData, condition = "stage", subject = "case", from = "Tc", to = "Th", method = "image") topPairs(resImage)
Shows the per-image value of one pair in each condition: a box plot with the images as points behind it.
For the cell method the value is the effect of spicy() in each image (by default the extra fraction of to
cells with a from cell within r; with effect = "count", the extra from cells per to cell), and each point
is sized by how much the image contributes to the test (its weight in the frailty model, relative to the
average image of its condition). With interactive = TRUE the plot is a plotly widget: hover over a point to
see its image, patient, excess and weight, which helps to find images worth looking at with plotImage().
spicyBoxPlot( results, from = NULL, to = NULL, rank = NULL, interactive = FALSE, parent = NULL )spicyBoxPlot( results, from = NULL, to = NULL, rank = NULL, interactive = FALSE, parent = NULL )
results |
The SpicyResults object returned by spicy() (either method). |
from |
The |
to |
The |
rank |
Alternatively, the rank of the pair by p-value (1 is the most significant). |
interactive |
Return an interactive plotly widget instead of a ggplot (needs the plotly package). |
parent |
For a Kontextual result ( |
A ggplot, or with interactive = TRUE a plotly htmlwidget. Images in which the pair was not
tested (no to cells, for example) are not shown.
data(spicyTest) spicyBoxPlot(spicyTest, rank = 1) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) spicyBoxPlot(res, from = "Tc", to = "Th")data(spicyTest) spicyBoxPlot(spicyTest, rank = 1) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) spicyBoxPlot(res, from = "Tc", to = "Th")
spicy() returns a SpicyResults object: a list with the elements below. Use topPairs(),
signifPlot(), spicyBoxPlot() and bind() on it, or take elements with $.
Elements for the cell method (method = "cell", the default):
cellResultsthe full table, one row per pair (and per level with more than two conditions):
excess_ref, excess_comp, excess_difference, se, df, p_value,
p_adj, tau2; with covariates or adjustAbundance = TRUE, what the test was adjusted
for (adjusted_for), the effect and p-value of each adjustment (<covariate>_effect,
<covariate>_p_value, abundance_effect) and the unadjusted test (unadjusted_difference,
unadjusted_se, unadjusted_df, unadjusted_p_value, unadjusted_p_adj). For
survival: score_coefficient, p_value, p_adj, hazard_ratio_sd,
hr_p_value and, with adjustAbundance = TRUE, the p-value and hazard ratio without it. With several radii: r, the radius with the strongest evidence, and
p_value_best_radius.
radiusResultswith several radii, the table at every radius.
pairwiseAssoca list with, for every pair, the effect in every image (NA where the pair
could not be measured).
effect"allocation" (the extra fraction of to cells with a from cell
within r), "count" (the extra from cells within r of each to cell), or
"kontextual" (Statial's Kontextual test).
imageWeightsa list with, for every pair, each image's weight in the test: its share of its condition's information (sums to 1 within each condition).
coefficient, p.value, se, statistic, df
matrices with one row per pair, as for the image method: the reference excess and the differences, and their tests.
condition, imageID, subject, nCells, r, k
the condition and patient of every image, the cell counts, and the settings of the analysis.
For the image method (method = "image") the object holds the matrices coefficient,
p.value, se, statistic and df, and pairwiseAssoc, as in spicyR 1.x.
The result of spicy(diabetesData, condition = "stage", subject = "case", method = "image"): the original
image-level test of spicyR, for all pairs of cell types, comparing the onset and long-duration stages with
non-diabetic donors. It is used in examples, so that they run quickly.
data("spicyTest")data("spicyTest")
A SpicyResults object (see spicy).
Made by inst/scripts/make-spicyTest.R from diabetesData.
data("spicyTest") topPairs(spicyTest, n = 5)data("spicyTest") topPairs(spicyTest, n = 5)
Lists the pairs tested by spicy(), most significant first. Give from and/or to to see chosen pairs, and
n = NULL to see every pair.
topPairs( x, coef = NULL, n = 10, adj = "fdr", cutoff = NULL, figures = NULL, from = NULL, to = NULL, full = FALSE )topPairs( x, coef = NULL, n = 10, adj = "fdr", cutoff = NULL, figures = NULL, from = NULL, to = NULL, full = FALSE )
x |
The output from |
coef |
Which coefficient to list: a column of |
n |
The number of pairs to list. |
adj |
Which p-value adjustment method to use, argument for p.adjust(). |
cutoff |
List only the pairs whose adjusted p-value is at most |
figures |
Round to |
from, to
|
Only the pairs with these |
full |
Cell method: every column of the test, not only the main ones. |
For the cell method (the default of spicy()) the table has one row per pair (and per level with more than two
conditions), with the columns of the test: from, to, excess_ref and excess_comp (the average
effect in the reference and the comparison group), excess_difference, p_value and p_adj; with several
radii, r, the radius with the strongest evidence; with covariates, the effect and p-value of each
(<covariate>_effect, <covariate>_p_value); for survival, hazard_ratio_sd. full = TRUE adds the
scale of the effect (side), the standard error, degrees of freedom, the variance between patients (tau2),
what the test was adjusted for and the unadjusted test. For the image method the columns are intercept, coefficient, p.value,
adj.pvalue, from and to, as in spicyR 1.x.
A data.frame, one row per pair, with the pair ("from__to") as row names.
data(spicyTest) topPairs(spicyTest) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) topPairs(res, from = "Tc", to = "Th")data(spicyTest) topPairs(spicyTest) data("diabetesData") res <- spicy(diabetesData, condition = "stage", subject = "case", r = 50, from = "Tc", to = c("Th", "beta")) topPairs(res, from = "Tc", to = "Th")
Writes a SpatialExperiment, SingleCellExperiment or data.frame of cells as a SpatialData Zarr store (Zarr v3),
which spicy() reads a block of rows at a time, spatialdataR's readSpatialData() reads as a SpatialData object,
and Python's spatialdata reads too. The cells become a table (AnnData, under tables/):
writeCellStore( cells, path, imageID = "imageID", spatialCoords = c("x", "y"), tableKey = "table", imageInfo = NULL, infoKey = "images", points = TRUE )writeCellStore( cells, path, imageID = "imageID", spatialCoords = c("x", "y"), tableKey = "table", imageInfo = NULL, infoKey = "images", points = TRUE )
cells |
A |
path |
The directory of the store (replaced if it exists). |
imageID, spatialCoords
|
The column of the image and the two columns of the coordinates. A
|
tableKey |
The name of the cell table. |
imageInfo |
Optional: a |
infoKey |
The name of the per-image table. |
points |
Whether to write the cells as points too, one points element per image (needs duckdb). |
obs: the colData (or the columns of a data.frame) other than the coordinates, factors as categorical
columns. The image column is the table's region key (always categorical, its categories the levels of a factor,
else the images in order of first appearance), and the cells are numbered 0, 1, ... in "instance_id".
X and layers: the first assay and the others (a sparse assay in compressed sparse row form).
var: the rowData, with the row names as its index.
obsm: the coordinates as "spatial", and the reducedDims.
The cells of each image also become a points element named after the image, with their coordinates and their
"instance_id", so that the table annotates elements of the store (image IDs must then be valid element names:
letters, digits, _, - and .). The images of a SpatialExperiment (imgData()) become image elements named <sample_id>_<image_id>, each in
the coordinate system of its sample and scaled to the coordinates of the cells. A table with one row per image can
be added. Every array is written in chunks (of 2^20 rows for a column) compressed with zstd, one chunk at a time,
so writing needs little memory beyond that of cells, however many cells it has. metadata(), altExps() and
column or row pairings are not written.
path, invisibly.
cells <- data.frame(imageID = factor(rep(c("a", "b"), each = 50)), cellType = factor(rep(c("T", "B"), 50)), x = runif(100, 0, 100), y = runif(100, 0, 100)) if (requireNamespace("Rarr", quietly = TRUE)) { path <- writeCellStore(cells, tempfile(fileext = ".zarr")) list.files(file.path(path, "tables")) }cells <- data.frame(imageID = factor(rep(c("a", "b"), each = 50)), cellType = factor(rep(c("T", "B"), 50)), x = runif(100, 0, 100), y = runif(100, 0, 100)) if (requireNamespace("Rarr", quietly = TRUE)) { path <- writeCellStore(cells, tempfile(fileext = ".zarr")) list.files(file.path(path, "tables")) }