Skip to content

API

PerturbVI fits transformed cell-by-gene expression X together with a 0/1 cell-by-perturbation matrix G. See the general guide for a full example of both matrices.

PerturbData

perturbvi.PerturbData dataclass

Expression, perturbation design, labels, and optional covariates.

control names the reference column to drop from G at construction time. Omit it when G is already baseline-free.

X: ArrayLike instance-attribute
G: ArrayLike instance-attribute
gene_names: Optional[Sequence[str]] = None class-attribute instance-attribute
perturbation_names: Optional[Sequence[str]] = None class-attribute instance-attribute
covariates: Optional[pd.DataFrame] = None class-attribute instance-attribute
__init__(X: ArrayLike, G: ArrayLike, gene_names: Optional[Sequence[str]] = None, perturbation_names: Optional[Sequence[str]] = None, covariates: Optional[pd.DataFrame] = None, control: InitVar[Optional[str]] = None) -> None

Use PerturbData when X and G are already DataFrames or arrays. DataFrame column names are used automatically:

from perturbvi import PerturbData

# control= drops the reference column; omit it when G is baseline-free
data = PerturbData(
    X=expression,
    G=G,
    covariates=covariates,
    control="Nontargeting",
)

CSV and TSV paths are not parsed automatically. Load them into pandas first, then pass the DataFrames to PerturbData so row indexes and headers are under your control. Pass control= when G keeps its reference column; omit it when G is already baseline-free.

X, G, and covariates must contain the same cells in the same row order. NumPy and sparse arrays have no column names, so provide them separately:

data = PerturbData(
    X=expression_array,
    G=G_array,
    gene_names=gene_names,
    perturbation_names=perturbation_names,
)

PerturbData checks DataFrame row alignment when it is created. Full matrix, name, value, and covariate checks run before fitting; load_screen() also runs them before returning.

Loading AnnData

perturbvi.load_screen(source: Any, *, x_key: Optional[str] = None, covariates: Optional[Sequence[str]] = None, g_key: str = 'G', control: Optional[str] = None) -> PerturbData

Build :class:PerturbData from AnnData.

Expression comes from adata.X unless x_key names a layer. The binary perturbation matrix is read from adata.obsm[g_key] (default "G") and must be a named pandas DataFrame with one row per cell. Column order is preserved exactly: perturbation_names[i] labels G[:, i]. control names a column of that frame to drop as the reference; pass None when the stored G is already baseline-free.

load_screen() reads an AnnData object, H5AD file, or AnnData Zarr folder and returns PerturbData. Expression comes from adata.X by default; x_key= selects a named layer. The binary perturbation matrix is read from adata.obsm[g_key] (default "G") and must be a named pandas DataFrame whose rows match the expression matrix. See AnnData inputs for the full AnnData layout.

from perturbvi import load_screen

data = load_screen(
    "screen.h5ad",
    covariates=["batch"],
)

obsm["G"] is a PerturbVI storage convention. AnnData reserves no obsm keys; G is an n_obs × perturbations cell-level annotation, and its DataFrame column names become perturbation_names. Column order is preserved exactly: perturbation_names[i] labels G[:, i].

If the stored G includes the reference column, pass its name to control= and the loader drops it before building PerturbData:

data = load_screen(
    adata,
    control="control",
)

control= is drop-only: the named column must exist or the loader raises. When the stored G is already baseline-free (reference rows are all zero), omit control= and G passes through unchanged. PerturbVI does not verify that all-zero rows are biological controls.

Covariates and fitting

perturbvi.residualize_screen(screen: PerturbData, *, block_size: int = DEFAULT_BLOCK_SIZE, output: Optional[Path] = None) -> PerturbData

Regress selected covariates out of expression in gene blocks.

block_size bounds peak memory (genes per projection chunk) and defaults to :data:perturbvi._defaults.DEFAULT_BLOCK_SIZE. Output is float32.

perturbvi.FitResults dataclass

Fitted posterior and labeled matrices returned by :func:fit_screen.

W, PIP_W, B, PIP_B, BW, and PVE are pandas DataFrames, computed on access. Pass the matrix needed to a plotting function directly, e.g. plot_factor_effects(fit.B). Raw inference arrays remain accessible through inference.

inference: 'InferResults' instance-attribute
gene_names: tuple[str, ...] instance-attribute
perturbation_names: tuple[str, ...] instance-attribute
params: 'ModelParams' property
elbo: Optional['ELBOResults'] property
pve property
pip property
W: pd.DataFrame property

Inclusion-weighted posterior mean loadings: factors by genes.

PIP_W: pd.DataFrame property

Loading inclusion probabilities: factors by genes.

B: pd.DataFrame property

Inclusion-weighted perturbation effects: perturbations by factors.

PIP_B: pd.DataFrame property

Perturbation-coefficient inclusion probabilities: perturbations by factors.

BW: pd.DataFrame property

Overall effects through all factors: perturbations by genes, B @ W.

PVE: pd.DataFrame property

Per-factor expression variance summary: factors by one.

__init__(inference: 'InferResults', gene_names: tuple[str, ...], perturbation_names: tuple[str, ...]) -> None

perturbvi.fit_screen(screen: PerturbData, *, z_dim: int, l_dim: int, tau: float = DEFAULT_TAU, p_prior: float = DEFAULT_P_PRIOR, standardize: bool = DEFAULT_STANDARDIZE, init: Literal['random', 'pca'] = DEFAULT_INIT, tol: float = DEFAULT_TOL, max_iter: int = DEFAULT_MAX_ITER, seed: int = DEFAULT_SEED, A: Optional[Union[ArrayLike, jax_sparse.JAXSparse]] = None, learning_rate: float = DEFAULT_LEARNING_RATE, verbose: bool = DEFAULT_VERBOSE) -> FitResults

Fit a validated screen, applying any selected covariates once.

Pass the covariates you want removed from expression. With AnnData, use their obs column names. fit_screen() regresses them out before fitting:

data = load_screen(
    adata,
    control="control",
    covariates=["batch", "percent_mito"],
)

fit = fit_screen(
    data,
    z_dim=12,
    l_dim=100,
    tau=100,
)

The main model settings are:

Setting Meaning
z_dim Number of gene programs to fit
l_dim Number of single-gene effects available to build each program
tau Starting inverse noise level for expression; PerturbVI updates it during fitting

Numeric covariates are treated as measurements. Text, categorical, and boolean covariates are treated as groups. If a covariate overlaps with G, shared signal may be removed; perfectly confounded effects cannot be separated. To reuse corrected expression across fits, call residualize_screen(data) once and pass its result to fit_screen(). fit_screen() centers every gene. Set standardize=True to also scale genes to unit variance. It does not perform raw-count QC, normalization, gene selection, or guide calling. See Covariates for the full behavior.

Save results and compute LFSR

perturbvi.save_results(results: InferResults | FitResults | str | Path, path: str | Path | None = None) -> None

Save model.pkl and six labeled CSV summaries.

Writes W.csv, PIP_W.csv, B.csv, PIP_B.csv, BW.csv, and PVE.csv. Matrices follow the model algebra: W is factors by genes, B is perturbations by factors, and BW is perturbations by genes. LFSR requires estimate_lfsr() or the perturbvi lfsr CLI; no sampling occurs here.

Pass a fitted result and destination, e.g. save_results(fit, "results"). To regenerate summaries without refitting, pass a saved directory alone: save_results("results"). This refreshes the six CSVs without rewriting its existing model.pkl. A second directory copies the posterior and writes summaries there. Existing LFSR files are not modified or copied.

Labeled matrices in memory

fit_screen() returns a FitResults object. Access each matrix directly; there is no separate analysis step or dictionary of tables.

Property / CSV stem Rows × columns Meaning
fit.W / W factors × genes Inclusion-weighted posterior mean loadings
fit.PIP_W / PIP_W factors × genes Gene-loading inclusion probabilities
fit.B / B perturbations × factors Inclusion-weighted mean effects on factors
fit.PIP_B / PIP_B perturbations × factors Coefficient inclusion probabilities
fit.BW / BW perturbations × genes Overall effects, B @ W
fit.PVE / PVE factors × 1 Per-factor expression variance summary

These properties return ordinary pandas DataFrames. They compute the requested matrix on access; they do not sample or write files. Store a matrix in a variable if you will reuse it. Raw arrays remain available through fit.inference.

from perturbvi import plotting as pp

fig = pp.plot_factor_effects(
    fit.B,
    perturbations=["ADNP", "PTEN", "SETD5"],
    show_significance=False,
    scale="asinh",
)

Saved files

save_results(fit, "results") writes model.pkl and the six CSVs listed above. It does not sample LFSR or write duplicate TXT summaries. The saved model retains the fitted posterior and gene/perturbation names; plotting the CSVs does not require it. The fitting CLI also records fit arguments in run_config.json and input information in input_summary.json.

To regenerate summaries from a saved posterior without refitting:

from perturbvi import save_results

save_results("results")

This refreshes the six CSVs and leaves an existing model.pkl and LFSR_BW.csv untouched. Older params_file.pkl fits are accepted internally. Files without saved labels use positional identifiers; the original gene and perturbation order is needed to relabel them.

Optional LFSR

perturbvi.estimate_lfsr(results: FitResults | InferResults | str | Path, *, draws: int = 2000, seed: int = 0) -> pd.DataFrame

Estimate overall-effect LFSR from an in-memory or saved fit.

Returns a labeled DataFrame with perturbations on rows and genes on columns, matching BW.csv. Nothing is written; use to_csv() to save it. A directory is loaded internally from model.pkl (or the older params_file.pkl). No other summary tables are calculated.

Each draw samples perturbation coefficients and gene loadings from the fitted variational posterior and multiplies them across all factors. LFSR is the smaller of the fractions of nonnegative and nonpositive overall effects; exact zeros count in both fractions. draws controls Monte Carlo precision, and seed controls reproducibility.

Saved fits retain their labels. Low-level or older fits without labels use positional identifiers. This computes fresh samples, rather than reading an existing LFSR_BW.csv.

from perturbvi import estimate_lfsr

LFSR_BW = estimate_lfsr(
    "results",
    draws=2_000,
    seed=1,
)
LFSR_BW.to_csv("results/LFSR_BW.csv")

Replace "results" with fit to use the in-memory posterior. This samples only overall-effect sign uncertainty, returning perturbations on rows and genes on columns. It does not return or regenerate the six summary matrices. If LFSR has already been computed for this fit, read its CSV instead.

CLI

Fit a prepared file whose binary matrix lives at obsm["G"]:

perturbvi fit screen.h5ad \
  --output results --z-dim 12 --l-dim 100 --tau 100

If G includes the reference column, pass --control control; the loader drops it before fitting. Expression can be selected with --x-key <layer>, and the perturbation key with --g-key <obsm_key> (default "G").

Fitting already writes the six result tables. Compute LFSR separately:

perturbvi lfsr results --draws 2000 --seed 1

This command writes only LFSR_BW.csv.

Read result tables

import pandas as pd

BW = pd.read_csv("results/BW.csv", index_col=0)
BW.head()

The CLI writes the same CSVs directly. Read LFSR_BW.csv only when LFSR was computed for this fit. Load only the matrices needed for the plot; each plotting function receives an individual DataFrame. If identifiers such as 001 or NA must remain strings, use pandas options such as dtype={0: str} and keep_default_na=False for the first column.

Plot interpretation tables

Install Matplotlib for plotting:

uv pip install matplotlib

Each function accepts one labeled DataFrame and returns a Matplotlib Figure. Read the corresponding CSV directly, or pass fit.B, fit.W, or fit.BW. Full matrices and explicit subsets use the same functions. See the LUHMES Analysis with PerturbVI.

Appearance options are scale ("linear" or "asinh"), cmap, and colorbar_ticks. Typography, spacing, and italic gene labels are automatic. By default, each colorbar has five markers evenly spaced along the displayed scale, including zero and both limits. Labels retain original units and are rounded to one decimal place. The displayed data determine the symmetric range. For an explicit override, supplied colorbar_ticks define the range using their largest absolute value. Use ax to compose plots and ordinary Matplotlib commands to customize the returned figure or axis labels.

For gene heatmaps, gene_annotations accepts a DataFrame read directly from a CSV with gene_ID, gene_name, and annotation columns. No ordering index is needed. Genes are automatically grouped by annotation, with groups in first-seen CSV order and unannotated genes last. Supply genes=annotations["gene_ID"].tolist() to preserve CSV order within groups, or another gene list to choose the within-group order. Repeated annotations share a color and appear once in the two-column legend below the plot; annotation text is displayed verbatim.

Function Required DataFrame Optional uncertainty input Significance rule
plot_factor_effects(B, ...) B: perturbations × factors pip=PIP_B PIP > 0.95
plot_gene_loadings(W, ...) W: factors × genes pip=PIP_W PIP > 0.95
plot_gene_effects(BW, ...) BW: perturbations × genes lfsr=LFSR_BW LFSR < 0.05

Dots are off by default (show_significance=False). Set the flag to True and supply the corresponding uncertainty matrix to show them. The effect and uncertainty matrices must have the same row and column identifiers; their order may differ. The plotting functions align them and handle display transposes internally. No model loading, file loading, or sampling occurs.

from perturbvi import plotting as pp

fig = pp.plot_gene_effects(
    BW,
    genes=genes,
    perturbations=["ADNP", "PTEN", "SETD5"],
    gene_annotations=annotations,
    show_significance=False,
    scale="asinh",
)

perturbvi.plotting.plot_factor_effects(B: pd.DataFrame, *, pip: pd.DataFrame | None = None, perturbations: Sequence | None = None, factors: Sequence | None = None, show_significance: bool = False, scale: str = 'linear', cmap=None, colorbar_ticks: Sequence | None = None, ax=None)

Plot a perturbation-by-factor posterior mean effect matrix B.

Read B.csv with pd.read_csv(..., index_col=0). Rows identify perturbations and columns identify factors. Optional selections use these IDs, e.g. factors=['factor_0'] displays Factor 1. Perturbation labels are italic automatically. No result dictionary or saved model is needed.

Dots are off by default. To mark PIP > 0.95, pass show_significance=True and pip=PIP_B, a DataFrame with the same identifiers and orientation. Supplying pip alone does not enable dots. Probabilities are aligned by identifiers; their row and column order need not match B.

Choose scale='linear' or scale='asinh' (asinh(x / 0.03)). Legend labels retain original units. cmap accepts a Matplotlib colormap name or object. By default, five markers are evenly spaced on the displayed scale, including zero and both limits, with labels rounded to one decimal place. The symmetric color range covers the displayed values; supplied colorbar_ticks set a symmetric range using their largest absolute value. Triangular caps mark saturation. Use identical ticks for comparable panels. Typography and spacing are automatic.

Returns a Matplotlib Figure. Use ax for composition, save with fig.savefig(), or customize the returned artists with Matplotlib.

perturbvi.plotting.plot_gene_loadings(W: pd.DataFrame, *, pip: pd.DataFrame | None = None, genes: Sequence | None = None, factors: Sequence | None = None, gene_annotations: pd.DataFrame | None = None, show_significance: bool = False, scale: str = 'linear', cmap=None, colorbar_ticks: Sequence | None = None, ax=None)

Plot a factor-by-gene posterior mean loading matrix W.

Read W.csv with pd.read_csv(..., index_col=0). The function transposes it internally to display genes on rows and factors on columns. Loadings are already inclusion-weighted and are not multiplied by PIP again.

Dots are off by default. Pass pip=PIP_W and show_significance=True to mark PIP > 0.95. PIP_W must have the same factor-by-gene identifiers as W; it is aligned and transposed internally.

Optional gene_annotations contains gene_ID, gene_name, and annotation columns, read directly from a CSV. Genes are grouped by annotation in first-seen CSV category order; the genes list controls within-group order (fitted order when omitted). Unannotated genes appear last. Without annotations, the supplied gene order stays unchanged. Annotation text labels the two-column bottom legend verbatim, with no automatic wrapping. Gene names are italic automatically.

Scale and returned Figure follow :func:plot_factor_effects. Omitting selections includes every gene and factor. Dense labels may be thinned; all selected cells remain present in fig.perturbvi_data.

perturbvi.plotting.plot_gene_effects(BW: pd.DataFrame, *, lfsr: pd.DataFrame | None = None, genes: Sequence | None = None, perturbations: Sequence | None = None, gene_annotations: pd.DataFrame | None = None, show_significance: bool = False, scale: str = 'linear', cmap=None, colorbar_ticks: Sequence | None = None, ax=None)

Plot a perturbation-by-gene overall-effect matrix BW.

Read BW.csv with pd.read_csv(..., index_col=0). The function transposes it internally to display genes on rows and perturbations on columns. It plots the supplied effects without recomputing a factor product, thresholding colors, or subtracting a reference condition.

Dots are off by default. Pass lfsr=LFSR_BW and show_significance=True to mark LFSR < 0.05. LFSR_BW must have the same perturbation-by-gene identifiers as BW. LFSR is aligned and transposed internally; it is never computed by the plotting function. No LFSR table is required when dots are off.

Annotation and color options follow :func:plot_gene_loadings. Gene and perturbation names are italic automatically. Returns a Matplotlib Figure; displayed values and settings are available in fig.perturbvi_data.

Selected matrices and color settings are recorded in fig.perturbvi_data, with one record per heatmap. Biological labels/groups are optional user-supplied tables. Enrichment and its visualization use direct R code in the tutorial.

Using arrays directly

To call the core model without PerturbData, pass X and G directly:

results = infer(
    X,
    G,
    z_dim=20,
    l_dim=10,
    tau=10.0,
)

perturbvi.infer.infer(X: ArrayLike | sparse.JAXSparse, G: ArrayLike | sparse.JAXSparse, *, z_dim: int, l_dim: int, tau: float = DEFAULT_TAU, A: Optional[ArrayLike | sparse.JAXSparse] = None, p_prior: Optional[float] = DEFAULT_P_PRIOR, standardize: bool = DEFAULT_STANDARDIZE, init: Literal['random', 'pca'] = DEFAULT_INIT, learning_rate: float = DEFAULT_LEARNING_RATE, max_iter: int = DEFAULT_MAX_ITER, tol: float = DEFAULT_TOL, seed: int = DEFAULT_SEED, verbose: bool = DEFAULT_VERBOSE) -> InferResults

Fit PerturbVI from preprocessed array inputs.

Parameters:

Name Type Description Default
X ArrayLike | JAXSparse

Cell-by-gene expression matrix. Dense array-like and JAX sparse matrices are supported.

required
G ArrayLike | JAXSparse

Cell-by-perturbation guide design matrix.

required
z_dim int

Number of latent factors.

required
l_dim int

Number of single effects per factor.

required
tau float

Positive initial residual precision.

DEFAULT_TAU
A Optional[ArrayLike | JAXSparse]

Optional gene-by-annotation matrix for parameterized loading priors.

None
p_prior Optional[float]

Prior inclusion probability for perturbation effects. None or zero selects deterministic dense guide regression.

DEFAULT_P_PRIOR
standardize bool

Scale each expression column to unit population variance after centering. Centering is always applied. Constant columns are rejected when scaling is enabled.

DEFAULT_STANDARDIZE
init Literal['random', 'pca']

Latent-factor initialization, either "pca" or "random".

DEFAULT_INIT
learning_rate float

Positive optimizer learning rate used only with A.

DEFAULT_LEARNING_RATE
max_iter int

Positive maximum number of variational iterations.

DEFAULT_MAX_ITER
tol float

Positive absolute ELBO convergence tolerance.

DEFAULT_TOL
seed int

Integer JAX random seed.

DEFAULT_SEED
verbose bool

Log initialization, iteration, and convergence progress.

DEFAULT_VERBOSE

Returns:

Type Description
InferResults

Inferred parameters, ELBO, PVE, and PIP values.

Raises:

Type Description
ValueError

If dimensions, values, guide columns, or scalar controls are invalid. Validation occurs before the iterative fit begins.

perturbvi.infer.compute_elbo(X: DataMatrix, guide: GuideModel, factors: FactorModel, loadings: LoadingModel, params: ModelParams) -> ELBOResults

Create function to compute evidence lower bound (ELBO)

Arguments:

  • X [Array]: The observed data, an N by P ndarray
  • guide [GuideModel]: The guide model
  • factors [FactorModel]: The factor model
  • loadings [LoadingModel]: The loading model
  • params [ModelParams]: The dictionary contains all the inferred parameters

Returns: - ELBOResults [ELBOResults]: The object contains all components in ELBO

perturbvi.infer.compute_pip(params: ModelParams) -> Array

Compute the posterior inclusion probabilities (PIPs).

Arguments:

-params [ModelParams]: Instance of inferred parameters

Returns:

-PIP [Array]: Array of posterior inclusion probabilities (PIPs) for each of K x P factor, feature combinations

perturbvi.infer.compute_pve(params: ModelParams) -> Array

Compute the percent of variance explained (PVE).

Arguments:

-params [ModelParams]: Instance of inferred parameters

Returns:

-PVE [Array]: Array of length K that contains percent of variance explained by each factor (PVE)