VIVS#

VIVS [Boyeau et al., 2024] (Variational Inference for Variable Selection; Python class VIVS) identifies which genes in a count matrix X are conditionally dependent on an external response Y — protein expression, niche composition, or any other obsm feature — using a conditional randomization test (CRT).

VIVS fits two components: a generative VAE over X (reusing scvi-tools’ standard VAE module, or an already-trained model passed via x_model — see “Compatible knockoff-sampler models” below), used only to sample conditional “knockoff” replacements of each gene; and an importance-score network predicting Y from X, whose per-cell negative log-likelihood is the CRT test statistic. For each gene (or, in the hierarchical variant, each cluster of correlated genes), the statistic is recomputed after substituting in a knockoff sample, yielding a calibrated, BH-corrected p-value for that gene’s/cluster’s conditional importance to Y.

The advantages of VIVS are:

  • Conditional (not marginal) dependence testing — controls for the rest of the transcriptome when assessing each gene’s relevance to Y.

  • Calibrated false-discovery-rate control via the CRT + Benjamini-Hochberg correction.

  • A hierarchical variant (get_hier_importance) that tests at multiple resolutions of correlated gene clusters, improving power for co-regulated genes.

  • Can reuse an already-trained model (e.g. a niche-aware SCVIVA model) as the knockoff sampler — see the compatibility table below for which models qualify.

The limitations of VIVS include:

  • The knockoff sampler’s quality (how well the generative VAE models gene expression p(X)) is necessary for the p-values returned by VIVS to be calibrated. A poorly fit VAE can inflate false-discovery rates.

  • Runtime scales with the number of genes tested; filtering to a smaller gene set (select_genes) is recommended above a few thousand genes.

  • Ported from VIVS’s original JAX implementation; large-scale runtime is not guaranteed to match the original’s vmap/jit-optimized performance (see use_vmap on get_importance/get_hier_importance).

  • fastcluster (for get_gene_groupings/get_hier_importance) and plotnine (for plot_hier_importance) are optional dependencies. fastcluster ships with pip install scvi-tools[optional]; plotnine has no dedicated lightweight extra — install it directly (pip install plotnine) or via pip install scvi-tools[tutorials].

Preliminaries#

VIVS takes as input a raw-count gene expression matrix X and a response matrix Y (registered via y_obsm_key in setup_anndata()). Training proceeds in two sequential phases: first the generative VAE over X is fit to convergence (or supplied pretrained via x_model), then it is frozen and the importance-score network is fit to predict Y from X. This order is required for CRT validity — the knockoff sampler must not be contaminated by information about Y.

Compatible knockoff-sampler models#

VIVS(..., x_model=...) requires x_model.module to be an instance of scvi.module.VAE (or a subclass), because the knockoff sampler calls module.inference(x=x, batch_index=batch_index) expecting a dict with z/library, then module.generative(z=z, library=library, batch_index=batch_index) expecting a dict with a px distribution supporting .sample(). Most non-VAE scvi-tools modules diverge from this signature (extra required arguments, or a different output-dict shape), so they fail either the isinstance check up front or the first knockoff-sampling call. Verified directly against module source (src/scvi/module/, src/scvi/external/*/):

Model

.module class

Works as x_model today?

Notes

SCVI

VAE

✅ Yes

Reference case; x_module_kwargs map onto VAE.__init__.

SCVIVA

nicheVAE(VAE)

✅ Yes

Only overrides __init__/encoder-decoder wiring, not inference/generative.

LinearSCVI

LDVAE(VAE)

✅ Yes

inference/generative fully inherited, unchanged.

AUTOZI

AutoZIVAE(VAE)

✅ Yes

generative override only adds optional kwargs (dropout rescaling); z/library/batch_index call still works, still returns px.

SCANVI

SCANVAE(SupervisedModuleClass, VAE)

✅ Yes

Same as LinearSCVI — no inference/generative override.

GIMVI

JVAE(BaseModuleClass)

⚠️ Blocked only by the isinstance check

Not a VAE subclass, so VIVS.__init__ currently rejects it — but its inference()/generative() happen to be duck-type compatible (z/library in, px out, mode defaults to 0). Relaxing the isinstance gate to a duck-type/allowlist check would likely make this work with no other changes; untested.

DestVI

MRDeconv(EmbeddingModuleMixin, BaseModuleClass)

❌ No

generative(self, z, ind_x, library, batch_index) — ind_x is a required positional argument VIVS never supplies; fails on the first knockoff call, not just the isinstance check.

RESOLVI

RESOLVAE(PyroBaseModuleClass)

❌ No

Pyro-based, no inference()/generative() VAE API at all.

TOTALVI

TOTALVAE(BaseMinifiedModeModuleClass)

❌ No

generative needs library_gene (not library) and a required label; output dict has no px key (px_/py_ nested dicts instead).

MULTIVI

MULTIVAE(BaseMinifiedModeModuleClass)

❌ No

generative needs a required qz_m positional arg; output dict has no px key (px_scale/px_rate/px_dropout separately).

PEAKVI

PEAKVAE(BaseModuleClass)

❌ No

generative needs a required qz_m positional arg (not produced by VIVS’s _encode_for_knockoffs), though it does return px.

CondSCVI

VAEC(EmbeddingModuleMixin, BaseModuleClass)

❌ No

generative requires a cell-type-label y positional arg — semantically unrelated to (and would collide with) VIVS’s own y (the response Y).

AmortizedLDA

AmortizedLDAPyroModule(PyroBaseModuleClass)

❌ No

Pyro-based, no VAE-style inference/generative.

Practical takeaway: today, the safest x_model choices are a plain SCVI, an SCVIVA model, or (with less real-world precedent) LinearSCVI/AUTOZI/SCANVI. Deconvolution (DestVI), Pyro-based (RESOLVI, AmortizedLDA), and paired/multimodal (TOTALVI, MULTIVI, PEAKVI, CondSCVI) models need either a model-specific adapter or a relaxed compatibility check before they can serve as VIVS’s knockoff sampler.