Compare the spatial expression patterns of genes between two samples
Source:R/compareSpatial.R
compareSpatial.RdFor every gene measured in two samples on one shared grid of
pixels, compareSpatial() tests whether the gene's expression
patterns are correlated beyond what the spatial autocorrelation of each
pattern alone would produce (spatial correlation test), and how often the
two samples' values at the same pixel are within a fold change of each
other (spatial similarity). It returns one row per gene.
Usage
compareSpatial(
x,
y = NULL,
assay = 1,
genes = NULL,
tests = c("correlation", "similarity"),
nPermutations = 10000,
exceedances = 10,
delta = c(0.01, 0.05, seq(0.1, 0.9, 0.1)),
maxDistPrctile = 0.25,
minDetected = NULL,
surrogate = c("remap", "gaussian"),
foldChange = 1,
minQuantile = 0.05,
minPixels = 0.1,
adjustMethod = "BH",
seed = 0L,
nThreads = getOption("STcompare.nThreads", 1L),
progress = interactive(),
verbose = TRUE,
keepNulls = FALSE
)Arguments
- x, y
Two
SpatialExperimentobjects with the same pixels, for example the elements of the list returned by oneSEraster::rasterizeGeneExpression()call. Alternativelyxis a list of two such objects andyisNULL, such as the list returned bySEraster::rasterizeGeneExpression(list(a = ..., b = ...)); the names of the list, if any, are used as the sample labels (otherwise"x"and"y").- assay
The assay to use: a name or an index, used for both objects, or two names or two indices, one per object. Default: the first assay.
- genes
The genes (row names) to compare. Default
NULL: every gene present in both objects (a message says how many were dropped). Genes given here must be present in both objects.- tests
Which tests to run:
"correlation"(the spatial correlation test with permutations) and/or"similarity"(no permutations).- nPermutations
The largest number of permutations per gene; the smallest possible p-value is
1 / (nPermutations + 1). Default 10000.- exceedances
The number of null correlations at least as extreme as the observed one after which a gene stops early (\(h\) of Besag and Clifford). The relative standard error of a p-value is about
1 / sqrt(exceedances). Default 10;Infgives every gene exactlynPermutationspermutations.- delta
The candidate smoothing bandwidths, as shares of the pixels in the kernel's window (values in (0, 1]). Values that leave fewer than 2 pixels in the window (\(\lfloor N\delta \rfloor < 2\) for \(N\) shared pixels) are dropped with a message. The default adds 0.01 and 0.05 to
seq(0.1, 0.9, 0.1), the grid the authors used for the kidney and MERFISH analyses: with the shorter grid, delta star is often its smallest value.- maxDistPrctile
The quantile of the pairwise pixel distances up to which the variograms are computed. Default 0.25.
- minDetected
The smallest share of the shared pixels at which a gene must be detected, in each sample, to be tested for correlation (see Rarely detected genes in Details);
0tests every gene that is not constant. DefaultNULL, which depends onsurrogate:0(no filter) with"remap", whose p-values are calibrated for rarely detected genes, and \(\sqrt{N}\) of the \(N\) shared pixels (18 of 311, 47 of 2170) with"gaussian", whose p-values are far too small for such genes. The number of pixels asked for is recorded inattr(result, "params")$minDetectedPixels.- surrogate
How the surrogates of the correlation test take their values (see Rank-remapped surrogates in Details):
"remap"(the default) gives each surrogate the gene's own values, in the surrogate's rank order, so that it has exactly the gene's distribution of values, which calibrates the p-values of sparse and skewed genes;"gaussian"uses them as the smoothing and the added noise leave them, the surrogates of the legacy functions and of the published analyses.- foldChange
The similarity band: pixels with \(|\log_2(y / x)| \le\)
foldChangeare similar. Default 1 (within two-fold).- minQuantile
The quantile of each gene's values in each sample used as its expression threshold for the similarity. Default 0.05.
- minPixels
The smallest share of the pixels that must pass the threshold for the similarity to be computed. Default 0.1.
- adjustMethod
The multiple-testing adjustment of
pValuePermuteacross the tested genes, which givespValuePermuteAdj; one ofstats::p.adjust.methods. Default"BH".- seed
An integer seed for the permutations and the noise. Default 0.
- nThreads
The number of threads. Default
getOption("STcompare.nThreads", 1L), so it can be set once per session withoptions(STcompare.nThreads = 8). The results do not depend on it.- progress
Show a progress line (with an estimate of the remaining time) while the permutations run. Default
interactive(). It is written as a message, sosuppressMessages()also hides it.- verbose
Show informative messages: genes or deltas that were dropped, genes whose delta star is at the edge of the grid, p-values limited by
nPermutations, and the total time. DefaultTRUE.- keepNulls
Keep the null correlations and delta stars of every permutation in
attr(result, "details"). DefaultFALSE.
Value
A data frame of class "STcompareResult" with one row per gene
(row names are the genes) and the columns below, in this order. The
columns of a test that was not run are absent. The column names are
those of the legacy functions, spatialCorrelationGeneExp()
and spatialSimilarity(), where the two report the same
quantity; the differences in meaning are stated for each column. In the
formulas, \(L\) is the number of permutations of the gene (the
nPermutations column), and \(b_x\) and \(b_y\) count
its null correlations whose absolute value is at least the observed
\(|r|\) when x and when y is permuted.
geneThe gene (also the row name).
nPixelsThe number of shared pixels, the same for every gene.
correlationCoefPearson's correlation \(r\) of the gene's values in
xandyover the shared pixels (cor()). Reported for every gene with finite values, tested or not. The same as inspatialCorrelationGeneExp().pValueNaiveThe analytical p-value of
cor.test(), which assumes that the pixels are independent. For reference only: under spatial autocorrelation it is far too small. The same as inspatialCorrelationGeneExp().pValuePermuteXThe sequential permutation p-value of the direction that permutes
x, \((b_x + 1) / (L + 1)\), at the common stopping point \(L\) of the two directions. Not adjusted. Descriptive: when a gene stops early, the direction that did not reachexceedanceshas an inflated value. InspatialCorrelationGeneExp()this column holds \((b_x + 1) / (B + 1)\) for a fixed number \(B\) of permutations, also unadjusted; its adjustment across genes is that function'spValuePermuteXAdj.pValuePermuteYThe same for the direction that permutes
y, \((b_y + 1) / (L + 1)\). Not adjusted.pValuePermuteThe permutation p-value of the gene, which combines the two directions:
exceedances / Lif the gene stopped early (stop = "exceedances"), otherwise \((\max(b_x, b_y) + 1) / (L + 1)\). This is the larger of the two directions' sequential p-values, which is valid without assuming that the two directions are independent (see Details); it equalsmax(pValuePermuteX, pValuePermuteY)for a gene that reachednPermutations(stop = "limit") and is slightly smaller,exceedances / Linstead of(exceedances + 1) / (L + 1), for a gene that stopped early. Not adjusted; its smallest possible value is1 / (nPermutations + 1). The legacy functions have no such column: they adjust each direction on its own (pValuePermuteXAdj,pValuePermuteYAdj).pValuePermuteAdjpValuePermuteadjusted across the tested genes withadjustMethod(stats::p.adjust(); skipped and failed genes are not counted). This is the column to use to call a gene significant.nPermutationsThe number of permutations \(L\) the gene used in each direction: fewer than the
nPermutationsargument when the gene stopped early, equal to it otherwise. The sum of this column is the work of the call.stopWhy the permutations stopped:
"exceedances"(early: a direction reachedexceedancesnull correlations at least as extreme as the observed one),"limit"(thenPermutationsargument was reached without that, sopValuePermuteis \((\max(b_x, b_y) + 1) / (L + 1)\)),"skipped"(not tested) or"failed".deltaStarMedianX,deltaStarMedianYThe median over the permutations of delta star, the bandwidth of
deltawhose surrogate variogram matched best, whenxand whenyis permuted. The same quantities as inspatialCorrelationGeneExp().deltaGridEdgeTRUEwhen, in either direction, more than half of the permutations chose the smallest or the largest delta of the grid used (attr(result, "params")$deltaGrid): the best bandwidth may lie outside the grid, so consider extendingdelta.NAwith fewer than 3 deltas.percentSimilarityThe proportion (between 0 and 1, despite the name) of the kept pixels with \(|\log_2(y / x)| \le\)
foldChange: the gene's spatial similarity. The same as inspatialSimilarity().percentDissimilarityXThe proportion of the kept pixels with \(\log_2(y / x) < -\)
foldChange: higher inx. The same as inspatialSimilarity().percentDissimilarityYThe proportion of the kept pixels with \(\log_2(y / x) >\)
foldChange: higher iny. The same as inspatialSimilarity().numPixelInThreshThe number of kept pixels: those whose value is above
t1inxor abovet2iny. The same as inspatialSimilarity().t1,t2The gene's expression thresholds in
xand iny: itsminQuantilequantile in each sample. The same as inspatialSimilarity().status"ok","skipped"(not tested for correlation: constant or rarely detected in a sample) or"failed"(see Details).messageWhy a gene was skipped or failed, or why its similarity is
NA(negative values); empty otherwise.
The result has four attributes:
paramsA list of the settings used: every argument after its default was applied (
tests,nPermutations,exceedances,delta,maxDistPrctile,minDetected,surrogate,foldChange,minQuantile,minPixels,adjustMethod,seed,nThreads,progress,verbose,keepNulls), plussamples(the two sample labels),assay(the assay used in each sample),nPixels(the number of shared pixels),genes,deltaGrid(the deltas actually used, after the unusable ones were dropped) andminDetectedPixels(the number of detected pixels a gene needed in each sample to be tested;NAwithout the correlation test).callThe matched call.
runtimeThe run time, as
proc.time()measures it.detailsOnly with
keepNulls = TRUE: a list with one element per gene, each a list of the null correlations (nullX,nullY) and the delta stars (deltaStarX,deltaStarY) of its permutations, in permutation order, whenxand whenyis permuted (empty for genes that were not tested).
print(), summary() and as.data.frame() (which returns
a plain data frame) have methods for it (STcompareResult); a
selection of rows keeps the class and the attributes, and a selection of
columns is a plain data frame.
Details
Input. The two samples must share their pixels: rasterize
them together, in one call such as
SEraster::rasterizeGeneExpression(list(x, y), ...), so that a pixel
name means the same location in both. The pixels used are those whose
names (column names) are in both objects, in the column order of
x, and their coordinates (SpatialExperiment::spatialCoords())
must agree.
Spatial correlation test. The observed statistic is Pearson's
correlation r of the gene's values in x and y over
the shared pixels. Its null distribution comes from spatially
autocorrelated permutations (Viladomat et al. 2014): the values of
x are shuffled across the pixels, smoothed with a Gaussian kernel
whose window holds a share \(\delta\) of the pixels, and rescaled with
added noise so that their variogram matches the variogram of x; the
\(\delta\) of delta whose variogram matches best (delta star) is
kept, the values of this surrogate are replaced by the gene's own values
in the surrogate's rank order (surrogate = "remap", the default;
see Rank-remapped surrogates), and its correlation with y is
one null correlation. The same is done with y permuted and
correlated with x. If there are more than 1000 shared pixels, the variograms use a
random subsample of 1000 of them, drawn from seed with R's default
random number generator whatever RNGkind() is set to. The smoother
and the variogram are those of the other correlation functions of the
package (spatialCorrelationGeneExp()).
Adaptive p-values. The permutations of a gene stop as soon as
its p-value is clearly not small (Besag and Clifford 1991). Both
directions (x permuted, y permuted) run in lockstep, and a
gene stops at the first permutation \(L\) at which either direction
has exceedances null correlations whose absolute value is at least
\(|r|\), or at nPermutations. Its p-value
(pValuePermute) is exceedances / L if it stopped early, and
otherwise
\((\max(b_x, b_y) + 1) / (L + 1)\), where
\(b_x\) and \(b_y\) count the exceedances of each direction.
This is the larger of the two directions' sequential p-values, each
\(h / L\) when the direction stopped the gene (\(b = h\)) and
\((b + 1) / (L + 1)\) at the limit, so it needs no
assumption about how the two directions are related: if the observed
correlation is distributed like the null correlations of either
direction, \(P(p \le \alpha) \le \alpha\) at
every \(\alpha\). The surrogates approximate that null distribution;
with surrogate = "gaussian" the approximation fails for genes
detected in few pixels (see Rarely detected genes). A
gene that is clearly not significant stops after a few dozen
permutations, and a significant gene runs to nPermutations, which
sets the smallest possible p-value, 1 / (nPermutations + 1). With
exceedances = Inf every gene gets exactly nPermutations
permutations. The columns pValuePermuteX and
pValuePermuteY always hold \((b + 1) / (L + 1)\), so
pValuePermute equals their maximum for a gene that reached
nPermutations and is slightly smaller, \(h / L\) instead of
\((h + 1) / (L + 1)\), for a gene that stopped early.
pValuePermute is adjusted across the tested
genes with adjustMethod, which gives pValuePermuteAdj.
Which p-value should I report? pValuePermuteAdj: the
permutation p-value of the gene, adjusted for the number of genes tested.
A gene with pValuePermuteAdj < 0.05 has spatial expression
patterns that are correlated between the samples beyond what the spatial
autocorrelation of each pattern explains, at a false discovery rate of 5
percent (with the default adjustMethod = "BH"). The unadjusted
pValuePermute is the per-gene p-value to report next to it, and
its smallest possible value is 1 / (nPermutations + 1).
pValuePermuteX and pValuePermuteY are descriptive: they are
not adjusted, and when a gene stops early the direction that did not
reach exceedances has an inflated value. pValueNaive
ignores the spatial autocorrelation and is far too small; it is reported
only to show how much the permutation test matters. The legacy
spatialCorrelationGeneExp() returns the same unadjusted
pValuePermuteX and pValuePermuteY (for a fixed number of
permutations), plus each of them adjusted across genes,
pValuePermuteXAdj and pValuePermuteYAdj; all four are per
direction, and there is no combined p-value: the published analyses
called a gene significant when both adjusted p-values were below 0.05.
Rank-remapped surrogates. With surrogate = "remap" (the
default), the values of each surrogate are replaced by the gene's own
values, placed in the rank order of the surrogate (the amplitude
adjustment of the AAFT surrogates of Theiler et al. 1992, in a single
step): the surrogate keeps the spatial arrangement of the smoothed,
rescaled permutation and has exactly the distribution of values of the
gene, zeros included. Delta star is chosen before the remapping, and for a
gene with a spatial pattern the remapping is a monotone distortion of the
surrogate's amplitudes, which changes its variogram a little. For a gene
without spatial structure the ranks of its surrogates are close to
uniformly random permutations, so its remapped surrogates are plain
random permutations of its values and the test is the exact permutation
test, whatever the gene's distribution (exactly so when the other gene has
no spatial structure either). Many rearrangements of a gene detected in
few pixels give exactly the observed correlation; as in the exact test,
such ties count as exceedances (a null correlation within a relative 1e-9
of \(|r|\) counts, because cor() rounds each rearrangement
differently). In simulations on the kidney and brain grids
(bench/calibration-results.md in the source repository), remapped
surrogates kept \(P(p \le \alpha) \le \alpha\)
for independent genes detected in 1 to 100 percent of the pixels, had
the same power as gaussian surrogates on correlated Gaussian fields,
recovered the published kidney and brain genes at least as well, and
cost about 7 percent more per permutation.
surrogate = "gaussian" uses the surrogates as the smoothing and
the added noise leave them. They are the surrogates of the other
correlation functions of the package and of the published analyses, and
are kept for comparability with them: their values are close to normally
distributed, which gives p-values that are far too small for genes
detected in few pixels (next paragraph) and conservative ones for skewed
genes with a spatial pattern.
Rarely detected genes. Gaussian surrogates are smoothed and mixed
with Gaussian noise, so their values are close to normally distributed.
A gene detected in few pixels has a few high values among many zeros, and
the correlation of two such genes is dominated by the few pixels where
both are detected: chance coincidences give extreme correlations more
often than gaussian surrogates do, so small p-values are too small, ten
times or more for genes detected in one or two pixels. With
surrogate = "gaussian", genes detected in fewer than a share
minDetected of the shared pixels of either sample are therefore
not tested for correlation (status = "skipped"). A gene is
detected at a pixel where its value is above its lowest value over the
shared pixels (for counts and normalized counts: where it is not zero).
With gaussian surrogates the default asks for \(\sqrt{N}\) of
the \(N\) shared pixels (18 of 311, 47 of 2170): the departure from the
surrogates grows roughly as \(N / (k_x k_y)\) for genes
detected in \(k_x\) and \(k_y\) pixels, and this removes the
genes for which it is largest; sparse or zero-inflated genes above it can
still get p-values that are somewhat too small, so with gaussian
surrogates test the genes with a spatial pattern in both samples, or
raise minDetected. Remapped surrogates have the gene's own values,
and the problem disappears: in the simulations their p-values were
calibrated down to genes detected in a single pixel, so with
surrogate = "remap" the default minDetected is 0 and every
gene that is not constant in a sample is tested. Such genes cost little
(a gene without spatial structure stops after about ten permutations),
and a gene detected in a single pixel cannot get a p-value below
\(1 / N\): the exact test is discrete.
Spatial similarity. The same computation as
spatialSimilarity(): for each gene, pixels are kept when
their value is above the gene's minQuantile quantile in x or
in y; zeros among them are replaced by 1e-4;
percentSimilarity is the proportion of kept pixels with
\(|\log_2(y / x)| \le\) foldChange. It is
NA when fewer than a share minPixels of the pixels are kept.
A fold change needs values that are not negative (counts or normalized
expression on a linear scale): for a gene with negative values the
similarity is NA, and a warning lists such genes.
Reproducibility. The permutations and the noise of each gene and
direction come from their own random stream, which depends only on
seed, the gene's name, the direction and the permutation number.
The results therefore do not depend on nThreads, on the order of
the genes, or on which other genes are in the call (except
pValuePermuteAdj, which is adjusted over the tested genes), and
the global random number generator (.Random.seed) is left
unchanged. The permutations are drawn over the shared pixels in the
column order of x (the order of y does not matter), and
each direction has its own streams: reordering the pixels of x, or
swapping x and y, gives other permutations and p-values
that differ within Monte Carlo error. Results are deterministic on a
given platform; the normal deviates use the C library's log() and
can differ in the last bits between platforms.
Run time. It is proportional to the total number of permutations
(the sum of the nPermutations column), and the work of all genes is
shared by nThreads threads. With the defaults, genes that are
clearly not significant use a few dozen permutations each and significant
genes 10000, so the run time is dominated by the significant genes. A
smaller nPermutations, such as 1000, was 6 to 7 times faster on the
published data and still resolves p-values down to 1 / 1001.
Skipped and failed genes. A gene that is constant in a sample
(for example never detected) or detected in fewer pixels than
minDetected asks for (by default only with
surrogate = "gaussian") is not tested for correlation: it gets
status = "skipped", NA p-values and the reason in
message (its similarity is still computed), and one message counts
such genes. A gene that cannot be tested gets status = "failed",
NA in the statistics that could not be computed, and the reason in
message; one warning lists the failed genes, and the other genes
are not affected. This happens for genes with missing or infinite values
on the shared pixels (every statistic is NA) and when the
permutations of a gene fail (for example a degenerate variogram fit).
References
Viladomat J, Mazumder R, McInturff A, McCauley DJ, Hastie T (2014). Assessing the significance of global and local correlations under spatial autocorrelation: a nonparametric approach. Biometrics 70(2):409-418. doi:10.1111/biom.12139
Besag J, Clifford P (1991). Sequential Monte Carlo p-values. Biometrika 78(2):301-304. doi:10.1093/biomet/78.2.301
Theiler J, Eubank S, Longtin A, Galdrikian B, Farmer JD (1992). Testing for nonlinearity in time series: the method of surrogate data. Physica D 58(1-4):77-94. doi:10.1016/0167-2789(92)90102-S
See also
spatialCorrelationGeneExp() and
spatialSimilarity(), which reproduce the published
analyses exactly. The tutorials:
vignette("getting-started-with-STcompare", package = "STcompare"),
vignette("how-STcompare-works", package = "STcompare") and
vignette("parameters-performance-reproducibility", package = "STcompare").
Examples
data(speKidney)
# rasterize the three samples together, onto one grid of pixels
rast <- SEraster::rasterizeGeneExpression(speKidney, assay_name = "counts",
resolution = 0.2, fun = "mean",
square = FALSE)
# A and B have opposite spatial patterns, A and C similar ones
ab <- compareSpatial(list(A = rast$A, B = rast$B), nPermutations = 999,
nThreads = 2)
#> compareSpatial: 1 gene on 273 shared pixels in 0.2 s (999 permutations in total, 2 threads)
ab
#> STcompareResult: 1 gene on 273 shared pixels; A vs B (assay pixelval)
#> Correlation: adaptive permutation p-values (stop at 10 exceedances, at most 999 permutations); 11 deltas from 0.01 to 0.9; rank-remapped surrogates
#> pValuePermuteAdj < 0.05: 1 gene (0 with correlationCoef > 0, 1 with correlationCoef < 0)
#> stopped early: 0; reached nPermutations: 1; skipped: 0; failed: 0
#> Similarity (|log2(y / x)| <= 1): median percentSimilarity 0.535 (0 genes with NA)
#>
#> gene nPixels correlationCoef pValueNaive pValuePermuteX pValuePermuteY
#> Gene 273 -0.947 5.65e-136 0.001 0.001
#> pValuePermute pValuePermuteAdj nPermutations stop deltaStarMedianX
#> 0.001 0.001 999 limit 0.2
#> deltaStarMedianY deltaGridEdge percentSimilarity percentDissimilarityX
#> 0.2 FALSE 0.535 0.267
#> percentDissimilarityY numPixelInThresh t1 t2 status message
#> 0.198 273 9.87 7.67 ok
ac <- compareSpatial(list(A = rast$A, C = rast$C), nPermutations = 999,
nThreads = 2)
#> compareSpatial: 1 gene on 277 shared pixels in 0.2 s (999 permutations in total, 2 threads)
summary(ac)
#> compareSpatial: 1 gene on 277 shared pixels (A vs C); 1 ok, 0 skipped, 0 failed
#> Correlation, pValuePermuteAdj < 0.05: 1 gene (1 with correlationCoef > 0, 0 with correlationCoef < 0)
#> stopped early: 0; reached nPermutations: 1; skipped: 0; failed: 0
#> nPermutations per gene: median 999, max 999; 999 in total
#> settings: nPermutations = 999, exceedances = 10, 11 deltas (0.01 to 0.9), rank-remapped surrogates, BH adjustment, seed 0
#> Similarity, percentSimilarity:
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 0 0 0 0 0 0
#> Run time: 0.2 s
as.data.frame(ac)[, c("gene", "correlationCoef", "pValuePermute",
"pValuePermuteAdj", "nPermutations", "percentSimilarity")]
#> gene correlationCoef pValuePermute pValuePermuteAdj nPermutations
#> Gene Gene 0.9431531 0.001 0.001 999
#> percentSimilarity
#> Gene 0