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. |
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 |
DEFAULT_INIT
|
learning_rate
|
float
|
Positive optimizer learning rate used only with |
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 ndarrayguide[GuideModel]: The guide modelfactors[FactorModel]: The factor modelloadings[LoadingModel]: The loading modelparams[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)