Skip to contents

Convenience wrapper that (optionally) builds niche covariates, fits the per-gene negative binomial model over the neighbourhood-interaction design, and tests for neighbourhood-dependent differential expression. Equivalent to calling buildNiches() (if the niches are not already present), fitSpiDE(), and testSpiDE() in sequence.

Usage

spiDE(spe, condition = NULL, ...)

# S4 method for class 'ANY'
spiDE(
  spe,
  condition = NULL,
  index = NULL,
  niche = NULL,
  covariates = character(),
  sigma = c(10, 30, 50, 70),
  assay = "counts",
  cell_type = "cell_type",
  sample_id = "sample_id",
  random = c("intercept", "none", "slope"),
  winsor = 4,
  lambda.a = 0,
  backend = c("auto", "cpu", "gpu"),
  name = "Niche",
  fdr = 0.05,
  combine = c("cauchy", "brown"),
  df.method = c("satterthwaite", "between"),
  re.celltype = TRUE,
  converge = TRUE,
  converge.maxit = 50L,
  converge.tol = 1e-08,
  block.size = NULL,
  gpu.mem.budget = NULL,
  BPPARAM = BiocParallel::SerialParam(),
  verbose = TRUE,
  ...
)

Arguments

spe

a SpatialExperiment with niche reducedDims (see buildNiches()).

condition

a character, the colData column of the tested condition, or NULL for a condition-free (niche-only) analysis. See fitSpiDE().

...

further arguments forwarded to fitNB.

index, niche

character vectors restricting the index / niche cell types considered (NULL = all).

covariates

a character vector of nuisance colData columns to adjust for (e.g. library size, age, sex).

sigma

a numeric vector of bandwidths to fit; NULL (default) uses every Niche<sigma> reducedDim present.

assay

a character, the counts assay to model.

cell_type

a character, the colData column of cell type labels.

sample_id

a character, the colData column identifying samples (used when niches must be built and for the random-effects fit).

random

one of "intercept" (the default), "none" or "slope". Adds patient-level random effects (implemented as ridge-penalised design columns) to correct anti-conservative inference caused by cell-level pseudo-replication.

The default changed from "none" to "intercept". "none" forms Wald standard errors from cell-level information, which treats every cell as an independent replicate of a patient-level contrast. On a null with per-sample intercepts it rejects at \(\approx 0.71\) against a nominal 0.05, worsening as cell counts become imbalanced, where the random intercept holds \(\approx 0.04\) against a calibrated pseudobulk reference of \(\approx 0.04\). "none" is retained for back-compatibility and for reproducing pre-correction results, but it should not be used for inference. "intercept" adds a per-sample random intercept; "slope" additionally adds per-sample random slopes on the niche covariates, which protects the response x niche tests when the niche-slope varies between samples. Note that "slope" estimates an extra variance component (tau2 for SampleSlope) that is collinear with the tested fixed effect and, with few samples, is less stable than the intercept-only variance; prefer "intercept" at small S, and "slope" when between-sample niche-slope variation is expected. The fit is also stochastic (fitNB subsamples cells for the dispersion estimate), so set a seed for reproducible variance components. See the mixed-effects and simulation vignettes.

In a condition-free analysis (condition = NULL) the random-slope block sits on exactly the CellType:niche columns being tested, so "slope" is the natural correction there when between-sample variation in niche slopes is plausible. Be aware of a limitation specific to that mode: the tested slope is a within-sample contrast on a spatially autocorrelated covariate, and spiDE does not model spatial autocorrelation. Neighbouring cells are therefore not independent replicates of the slope, and neither random-effect structure can recover that – on a null fixture with a planted per-sample intercept, the fixed-effects fit made 37 calls, "intercept" 5 and "slope" 5 (see longtests/testthat/test-nicheOnly-mixed.R). Random effects remove most of the inflation but niche mode remains mildly anti-conservative; treat borderline calls with corresponding caution.

winsor, lambda.a

fitting parameters forwarded to fitNB (coefficient winsorisation and the base ridge penalty on the fixed columns).

backend

a character, the compute backend for both the model fit ("auto", "cpu", or "gpu", forwarded to fitNB) and the inference stage (where the GPU backend batches the per-gene Wald covariance and negative-binomial working weights across each gene-block on the accelerator, forcing a serial BPPARAM in the process).

name

a character, the niche reducedDim prefix.

fdr

a numeric, the target false discovery rate.

combine

one of "cauchy" (default) or "brown", the within-gene combiner for the correlated niche p-values (passed to testSpiDE()).

df.method

one of "satterthwaite" (default) or "between"; only used when random != "none". "satterthwaite" derives a separate df per tested column from the shared variance-component fit, distinguishing between-sample contrasts (Response: small df, close to "between") from within-sample contrasts (ResponseNiche: larger df, more power) rather than applying S - 2 to both; @df is then a named per-column vector. "between" tests every Response/ResponseNiche coefficient against the same scalar between-sample reference df (S - 2), the original back-compatible behaviour, and @df is a scalar.

The default changed to "satterthwaite" after the benchmark study (research/) measured both arms on identically seeded data: "between" is severely over-conservative when samples are few (null type-I \(\approx 0.001\) at \(S = 4\) against a nominal 0.05, with correspondingly near-zero power), while "satterthwaite" holds type-I in \(0.042\)-\(0.065\) across the whole sampled range and gains \(\approx 0.10\) mean TPR. The trade is a mild liberal drift at larger \(S\) (worst measured \(\approx 0.065\)); use "between" when strict conservatism matters more than power, or for back-compatibility. Ignored when random == "none".

In a condition-free analysis (condition = NULL) the "between" reference df changes, because the tested CellType:niche slope is a within-sample contrast rather than a between-condition one: it is ncells - p_fixed under random = "intercept" (cells are the replicates) and S - 1 under random = "slope" (the per-sample random slopes sit on the tested columns, moving the contrast into the between-sample stratum). The name "between" is therefore a misnomer in the intercept case; it is retained for back-compatibility. "satterthwaite" computes this distinction from the fitted variance components and is preferred.

re.celltype

logical; add a nested (sample x cell type) random intercept so the tested niche slopes are within-group. Default TRUE. See fitSpiDE().

converge

logical; converge each gene to its own penalised NB optimum after the shared fit. Default TRUE. See fitSpiDE().

converge.maxit, converge.tol

iteration cap and relative log-likelihood tolerance for the per-gene convergence stage.

block.size

a numeric, genes per inference block (NULL = a single block on the CPU backend, or a memory-bounded auto-selected size on the GPU backend).

gpu.mem.budget

a numeric, the GPU memory budget in bytes used to size inference blocks (NULL auto-detects; only relevant for the GPU backend).

BPPARAM

a BiocParallelParam for niche construction and inference.

verbose

a logical, whether to print fitting progress.

Value

a SpiDEResults object with the tidy results table populated (see results()).

Examples

data(toySpiDE)
spe <- toySpiDE
res <- spiDE(spe, condition = "condition", sigma = 20, verbose = FALSE)
head(results(res))
#>   gene ct_index ct_niche bandwidth.max     coef       t DirectionGene
#> 1   G1        A        B            20 3.173422 4.63951            Up
#>   DirectionIndex DirectionNiche     fdr.gene    fdr.index    fdr.niche
#> 1             Up             Up 0.0005520939 2.760426e-05 9.201414e-06