Within a gene, spiDE tests several
celltype:condition:niche (ResponseNiche)
coefficients — one per surrounding cell type — and must collapse them
into a single gene-level p-value and one p-value per index cell type.
These coefficients are correlated, because neighbouring
cell-type densities are themselves correlated, so the combination step
has to remain calibrated under dependence.
spiDE offers two combiners, selected with the
combine argument:
This vignette compares them in two settings: a controlled Monte-Carlo
experiment that feeds both combiners identical, exactly correlated
p-values, and the full spiDE pipeline on realistic
simulated data.
We draw \(m\) correlated z-scores with exchangeable correlation \(\rho\), convert them to one-sided p-values, and combine. Under the null the combined p-value should be Uniform(0, 1); we sweep the correlation \(\rho\) and the number of coefficients \(m\).
On a QQ plot a calibrated combiner lies on the diagonal. At \(\rho = 0\) both do; as correlation grows, Brown (handed the true correlation matrix) stays on the diagonal while Cauchy bends slightly below it in the body — mildly conservative — but the two converge again in the extreme tail that actually drives FDR control.
We now shift two of the \(m = 5\) columns by a common effect \(\delta\) at a challenging correlation \(\rho = 0.6\) and measure the detection rate.
The controlled experiment isolates the combiners; we also compare
them inside the complete spiDE pipeline on realistic
simulated data (ten-thousand-plus genes, low counts, a planted niche
effect in 5% of genes). The table below, from the benchmark study
(research/), reports the realised false discovery
proportion and true-positive rate under each combiner at several nominal
FDR thresholds.
(The full-pipeline summary table was not found; run the benchmark
study in research/ and install_results.R to
populate it.)
In the full pipeline the two combiners diverge sharply, and in Cauchy’s favour. Cauchy recovers far more of the planted genes at every threshold — recall around 0.30 at a nominal 20 % FDR against roughly 0.02 for Brown — while Brown makes few discoveries and a large fraction of them are false, its realised FDP sitting near 0.4 regardless of the threshold. Cauchy’s own FDP does exceed the nominal level at the strictest thresholds, where only a handful of genes are called and the proportion is dominated by one or two false positives, but it falls below the line as more true genes accumulate. Both combiners inherit their absolute FDP level from the underlying mixed-effects fit; this comparison holds that fit fixed and isolates the combination step, and on that axis Cauchy is clearly preferable.
Everything above combines one-sided p-values,
testing each direction separately at fdr/2 so that a gene
can be labelled Up, Down or Both. That convention is safe for Brown’s
method but not for Cauchy combination, and the reason
is the transform each uses.
| combiner | transform | value as \(p \to 1\) |
|---|---|---|
| Brown / Fisher | \(-2\log p\) | \(\to 0\) — contributes nothing |
| Cauchy / ACAT | \(\tan((0.5 - p)\pi)\) | \(\to -\infty\) — cancels the signal |
Consider a gene that is strongly up near one niche cell type and strongly down near another. In the up-direction combination the down coefficient contributes a one-sided \(p \approx 1\). Fisher’s transform maps that to \(\approx 0\), so it is simply ignored. Cauchy’s maps it to a large negative number that annihilates the large positive contribution from the genuine signal.
#> effect (t) opposing one-sided p Cauchy term Fisher term
#> 1 3 0.9979 -1.52e+02 4.21e-03
#> 2 8 1.0000 -3.83e+09 1.66e-10
#> 3 15 1.0000 -3.15e+14 2.00e-15
#> 4 25 1.0000 -3.15e+14 2.00e-15
The consequence is not subtle: the stronger the opposing signal, the more completely Cauchy discards the gene.
The left panel is the diagnostic one. One-sided Cauchy is non-monotone in the effect size: detection rises, peaks, and then falls back towards zero as the opposing coefficient’s p-value approaches 1. The clearest possible biology is the most likely to be discarded, and silently. Brown is immune, and two-sided Cauchy is immune while keeping the correlation-agnostic property that made Cauchy the default.
Two-sided input costs nothing where the one-sided convention was
already safe: in the right-hand panel the three curves are
indistinguishable, because the two-sided p-value’s factor of 2 is
exactly offset by dropping the fdr/2 direction split.
#> method type-I at 0.05
#> 1 Brown (1-sided) 0.044
#> 2 Cauchy (1-sided) 0.050
#> 3 Cauchy (2-sided) 0.054
spiDE doescombine = "cauchy" combines two-sided
p-values; combine = "brown" keeps
one-sided p-values. When the two-sided path is used the
fdr/2 direction split is not applied (there is only one
test), and Up / Down / Both are resolved from the signs of the
significant niche coefficients — which is where direction is well
defined in any case. Under the one-sided Cauchy convention the
Both label was effectively unreachable, because genes with
opposing effects were exactly the ones cancelled at the gene-level
gate.
On both the controlled experiment and the full pipeline,
Cauchy combination matches or beats Brown’s method: it
controls type-I error at every correlation level, loses no meaningful
power, and does so without estimating a correlation matrix or depending
on poolr. Its only cost is mild conservativeness in the
body of the null distribution under very strong dependence — irrelevant
for the extreme tail that FDR control depends on. For these reasons
Cauchy combination is the default
(combine = "cauchy"); Brown’s method remains available
(combine = "brown") for settings where an explicit,
correlation-aware combiner is preferred.
That verdict holds provided the Cauchy combiner is given
two-sided p-values. Its \(\tan\) transform diverges to \(-\infty\) as \(p
\to 1\), so with one-sided input a gene that is up near one niche
cell type and down near another cancels itself out, and does so more
completely the stronger the effect. Brown’s \(-2\log p\) is bounded at zero and has no
such failure mode, which is why the directional convention inherited
from the Brown era is safe there and unsafe here. spiDE
therefore combines two-sided p-values under
combine = "cauchy" and one-sided under
combine = "brown".
#> R version 4.5.2 (2025-10-31)
#> Platform: x86_64-conda-linux-gnu
#> Running under: Red Hat Enterprise Linux 9.8 (Plow)
#>
#> Matrix products: default
#> BLAS/LAPACK: /home/uqdbhuva/miniconda3/envs/latest-r/lib/libopenblasp-r0.3.30.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Australia/Brisbane
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] patchwork_1.3.2 ggplot2_4.0.1
#> [3] SpatialExperiment_1.20.0 SingleCellExperiment_1.32.0
#> [5] SummarizedExperiment_1.40.0 Biobase_2.70.0
#> [7] GenomicRanges_1.62.0 Seqinfo_1.0.0
#> [9] IRanges_2.44.0 S4Vectors_0.48.0
#> [11] BiocGenerics_0.56.0 generics_0.1.4
#> [13] MatrixGenerics_1.22.0 matrixStats_1.5.0
#> [15] spiDE_0.99.18
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 SparseArray_1.10.1 lattice_0.22-9
#> [4] digest_0.6.38 magrittr_2.0.4 evaluate_1.0.5
#> [7] grid_4.5.2 RColorBrewer_1.1-3 fastmap_1.2.0
#> [10] jsonlite_2.0.0 Matrix_1.7-4 limma_3.66.0
#> [13] viridisLite_0.4.2 scales_1.4.0 codetools_0.2-20
#> [16] jquerylib_0.1.4 abind_1.4-8 cli_3.6.5
#> [19] rlang_1.1.6 XVector_0.50.0 withr_3.0.2
#> [22] cachem_1.1.0 DelayedArray_0.36.0 yaml_2.3.10
#> [25] poolr_1.2-0 S4Arrays_1.10.0 tools_4.5.2
#> [28] parallel_4.5.2 BiocParallel_1.44.0 dplyr_1.1.4
#> [31] mathjaxr_1.8-0 vctrs_0.6.5 R6_2.6.1
#> [34] lifecycle_1.0.4 magick_2.9.0 fs_1.6.6
#> [37] pkgconfig_2.0.3 pillar_1.11.1 bslib_0.9.0
#> [40] gtable_0.3.6 glue_1.8.0 Rcpp_1.1.2
#> [43] statmod_1.5.1 tidyselect_1.2.1 tibble_3.3.0
#> [46] xfun_0.54 knitr_1.50 farver_2.1.2
#> [49] rjson_0.2.23 htmltools_0.5.8.1 labeling_0.4.3
#> [52] rmarkdown_2.30 compiler_4.5.2 S7_0.2.1