.. _package-info:
===============================
CIPHER package
===============================
Availability
-----------------
The package is available on PyPI as ``cipher-perturb`` (`PyPI page `_) or GitHub as ``cipher`` (`GitHub page `_).
How do I use it?
------------------
#. Installation
It can be installed from PyPI using the following:
.. code-block:: python
pip install cipher-perturb
It can be installed from GitHub using the following:
.. code-block:: python
pip install -e .
The core install is lightweight (``numpy``, ``scipy``, ``pandas``, ``anndata``, ``h5py``, ``tqdm``). Optional extras add heavier, feature-specific dependencies:
.. code-block:: python
pip install -e ".[plot]" # matplotlib plotting helpers (cipher.plotting)
pip install -e ".[bayes]" # PyMC horseshoe reverse (bayesian_reverse)
pip install -e ".[dev]" # tests (pytest)
On the lab HPC the full conda environment (matching the exact versions used for the paper) can be built from the pinned spec:
.. code-block:: python
conda env create -f environment.yaml
#. Preparing the input sample sheet.
The input dataset should be a single-cell RNA-seq dataset with chemical or genetic perturbation in ``.h5ad`` format, with the following requirements:
- Should contain ``perturbation`` column in the ``adata.obs``, which indicates the perturbation applied to each cell. The control cells should be labeled as ``control``.
- Should contain ``gene_names`` column in the ``adata.var``, which indicates the gene names corresponding to each gene expression value.
#. Forward Prediction: ``ForwardResult``
This code provides the predicted transcriptome from the given perturbation set.
.. code-block:: python
import cipher
res = cipher.forward_prediction(
"path/to/perturbseq.h5ad",
normalization="log1p",
nulls=("meanfield", "shuffled"), # baseline covariance models
holdout_frac = 0.0, # 0.5 for out-of-sample gene holdout (paper setting)
max_perturbation = None # int for a quick smoke test
)
res.results # DataFrame per perturbation: r2_uncentered_real, r2_centered_real
# pearson_real, spearman_real, cosine_real, mse_real, sign_accuracy_real
# a_hat, n_train_genes, n_test_genes, r2_uncentered_, ...
res.summary # dict: mean_r2_uncentered_real, mean_pearson_real, mean_r2_uncentered_<>null>
res.save("path/to/output_dir")
You can also run forward metrics straight from a preprocessed directory (real Sigma only, matching the paper's final forward recompute): cipher.forward_from_precomputed("path/to/output_dir", "log1p", holdout_frac=0.5).
#. Reverse Prediction: ``ReverseResult``
This code identifies the causal perturbation from the Perturb-seq transcriptomic profile.
.. code-block:: python
import cipher
res = cipher.reverse_prediction(
"path/to/perturbseq.h5ad",
normalization="log1p",
method="matched_filter", # matched_filter (default) | pinv | ridge | lstsq
top_k=10,
)
res.summary["mean_auc"] # mean one-vs-rest ROC-AUC of the true driver
res.summary["top10_accuracy"] # fraction with the target gene in the top-k
res.results.sort_values("auc").head() # per-perturbation ranks / AUC
res.save("path/to/output_dir") # writes _reverse__.csv
cipher.reverse_from_precomputed(...) runs the same from a preprocessed directory. With the [bayes] extra, cipher.bayesian_reverse(Sigma, delta_x) fits a horseshoe prior and returns posterior inclusion probabilities.
#. Posterior inverse: ``InverseResult``
The strongest inverse solver is the fullH_diag posterior: it whitens dx by its per-perturbation sampling covariance (lambda/n0 + projected_var/nu), fits a prior variance by empirical Bayes, and scores each gene by its posterior perturbation strength (or a single-effect PIP). It runs on the artifacts written by preprocess_dataset (needs save_mean_var=True, the default) and is scored with pooled ROC / precision-recall curves.
.. code-block:: python
import cipher
res = cipher.posterior_inverse_from_precomputed(
"path/to/output_dir", "log1CP10k",
method="posterior", # "posterior" (default) or "pip"
)
res.summary["pooled_auc"] # pooled one-vs-rest ROC-AUC
res.summary["mean_per_pert_auc"] # mean over perturbations
res.summary["pooled_average_precision"] # pooled AP (precision-recall)
res.roc, res.prc # (fpr, tpr), (precision, recall) for plotting
# or in memory straight from an .h5ad (moderate datasets):
res = cipher.posterior_inverse_prediction("path/to/perturbseq.h5ad",
normalization="log1CP10k", method="posterior")
To reproduce the paper's per-dataset CRISPRi vs CRISPRa ROC/PR figure, run the inverse for each dataset, group with cipher.dataset_group(name), and draw each group with cipher.plotting.plot_inverse_group (thin per-dataset traces + a mean trace):
.. code-block:: python
import matplotlib.pyplot as plt
from cipher import plotting
results = {"CRISPRi": [...], "CRISPRa": [...]} # InverseResult per dataset, by group
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
plotting.plot_inverse_group(results["CRISPRi"], curve="roc", ax=axes[0], color="#d62728")
plotting.plot_inverse_group(results["CRISPRa"], curve="roc", ax=axes[1], color="#1f77b4")
#. Condition drivers: ``DriverResult``
The reverse problem applied outside Perturb-seq: given a control group and a condition group, rank the genes most likely to drive the condition. There is no ground-truth label, so the output is a ranked candidate list.
.. code-block:: python
import cipher
# From an .h5ad / AnnData with a grouping column in obs:
res = cipher.condition_drivers(
"path/to/dataset.h5ad",
condition_key="stim", # obs column grouping the cells
control_value="rest", # value marking control cells
condition_value="stim", # None => every non-control cell
normalization="log1p",
method="matched_filter", # robust default (no matrix inverse)
)
res.top(20) # top-ranked candidate driver genes
res.save("path/to/output_dir") # writes _drivers__.csv
# Or directly from two raw (cells x genes) matrices sharing a gene axis:
res = cipher.condition_drivers_from_matrices(
control_X, condition_X, gene_names,
normalization="log1p",
method="matched_filter",
)
#. Lower-level Dataset
For custom pipelines, load_dataset returns a Dataset that handles perturbation detection, target-gene mapping, and control/perturbation matrices.
.. code-block:: python
import cipher
ds = cipher.load_dataset(
"path/to/perturbseq.h5ad",
expression_threshold=1.0,
min_samples=100,
)
ds.n_genes, ds.n_perturbations
ds.gene_names # np.array[str], length p
ds.perturbations # list[str], non-control perturbation labels
ds.target_gene_indices # int64 array parallel to perturbations (-1 if absent)
control = ds.control_matrix(dense=True) # (n_control, p)
pert = ds.perturbation_matrix(ds.perturbations[0], dense=True)
g = ds.gene_index("GATA1")
# Estimate the control covariance and run one perturbation forward by hand:
Sigma = cipher.compute_covariance(cipher.normalize_matrix(control, "log1p"))
Command-line usage
-----------------------------------------------------------------------------
Installing the package registers a cipher console script with four subcommands. Each takes an input ``.h5ad`` and an output directory (-o).
.. code-block:: bash
# Precompute Sigma + per-perturbation stats for chosen modes.
cipher preprocess path/to/perturbseq.h5ad -o out/ --modes log1p log1CP10k
# Forward prediction (transcriptomic shift) against null covariances.
cipher forward path/to/perturbseq.h5ad -o out/ --normalization log1p \
--nulls meanfield shuffled
# Reverse prediction (recover the perturbed gene).
cipher reverse path/to/perturbseq.h5ad -o out/ --normalization log1p \
--method matched_filter --top-k 10
# Condition-driver prediction (control vs condition, no ground truth).
cipher driver path/to/dataset.h5ad -o out/ \
--condition-key stim --control rest --condition stim \
--method matched_filter --top 25
Run cipher --version or cipher --help for all options.