🎯 Core Concepts
The Challenge
Understanding how cells respond to perturbations (genetic knockouts, drug treatments, etc.) is
fundamental to biology and medicine. However:
-
Experimental Limitations: Testing all possible perturbations is prohibitively
expensive and time-consuming
-
Combinatorial Explosion: For n genes, there are n(n-1)/2 possible pairwise
combinations - grows exponentially
-
Cellular Heterogeneity: Individual cells respond differently to the same
perturbation
-
Context-Dependence: Effects vary across cell types, conditions, and genetic
backgrounds
-
Data Quality: Single-cell data is sparse, noisy, and affected by batch effects
Key Biological Facts
-
Only ~41% of gene perturbations have measurable transcriptome-wide effects
(Replogle et al., 2022)
-
Typical gene perturbation affects ~45 genes; essential genes affect
>500 genes
-
Perturbation effects exhibit mixture distributions - some cells escape
perturbation entirely
- Most effects are small: 86.6% below 0.01 log-fold change
-
Network structure matters: 77.3% of direct regulators (distance 1 in GRN)
confer moderate-to-strong effects
The Opportunity
Computational prediction methods aim to:
- Predict cellular responses to unseen perturbations without experiments
-
Model combinatorial perturbations (e.g., gene pairs) trained only on
single perturbations
-
Enable rational experimental design by prioritizing promising perturbations
- Uncover mechanisms of action and genetic interactions
- Accelerate drug discovery by predicting compound effects
📈 Large-Scale Datasets & Platforms (2024-2025)
Industrial-scale perturbation atlases provide massive training data for foundation models.
These represent significant experimental advances in scale and throughput.
Tahoe-100M: Giga-Scale Single-Cell Perturbation Atlas
2025
bioRxiv (Feb 2025)
Dataset (100M cells)
Largest public perturbation atlas: 100M cells, 50 cancer cell lines, 1,100 drugs.
Vevo Therapeutics' Mosaic platform with Parse GigaLab sequencing. Open-sourced on
Arc Virtual Cell Atlas and HuggingFace.
- SNP-based genetic demultiplexing (>98% accuracy)
- 1,786 sublibraries, 1.4 trillion reads
- Context-dependent drug response across diverse cancer backgrounds
- VERIFIED: Real dataset, publicly available
scPerturb: Harmonized Single-Cell Perturbation Data
2024
Nature Methods
Repository
Harmonized repository of 44 datasets from 25 publications. 32 CRISPR + 9 drug datasets,
average 160K cells per dataset. Introduces Energy statistics for perturbation quantification.
- Standardized processing across diverse experimental platforms
- E-statistics framework for perturbation effect quantification
- Unified access through scperturb.org
Replogle et al. 2022: Genome-Wide Perturb-seq
2022
Cell
Dataset
Landmark genome-wide study: K562 (>2.5M cells, 9,867 genes targeted with CRISPRi),
RPE1 Essential. Key finding: only 41% of perturbations show measurable transcriptome-wide effects.
- First genome-wide single-cell perturbation screen
- Revealed sparsity of perturbation effects
- Established standards for large-scale Perturb-seq
X-Atlas/Orion: Genome-Wide Perturb-seq via FiCS Platform
2025
bioRxiv
Platform (8M cells)
Fix-Cryopreserve-ScRNAseq platform achieving 8M cells targeting all human protein-coding genes.
sgRNA abundance as dose-dependent proxy (R=0.91 with KD efficiency).
- DSP fixation + superloading (5x throughput)
- Hamilton automation removes operator variability
- 140+ day cryopreservation stability
Mixscale: Systematic Reconstruction of Molecular Pathway Signatures
2025
Nature Cell Biology
Method + Dataset
Continuous quantification of perturbation strength replacing binary classification. Systematic
pathway profiling across 6 cell lines × 5 signaling contexts (2.6M cells).
- Mixscale scoring: s_i = p_i × (p̄_r - p̄_NT)
- Weighted differential expression (wmvReg)
- 93% replication rate vs 84-89% (standard methods)
📈 Key Datasets & Benchmarks (Summary)
Major Perturbation Datasets
-
Replogle et al. 2022 (Cell): Largest to date - K562 Genome-Wide (>2.5M cells,
9,867 genes targeted with CRISPRi), K562 Essential (~400K cells), RPE1 Essential (~300K cells).
Only 41% of perturbations show measurable effects.
-
Norman et al. 2019 (Science): K562, 287 perturbations (100 single + 131 double
combinations), CRISPRa, 19,264 genes measured. Primary benchmark for combinatorial perturbations.
-
Srivatsan et al. 2020 sci-Plex (Science): Chemical perturbations, 188 compounds
× 4 doses, K562/A549/MCF-7 cell lines, ~650K cells total.
-
scPerturb (Peidli et al., Nature Methods 2024): Harmonized repository of 44
datasets from 25 publications, 32 CRISPR + 9 drug datasets, average 160K cells per dataset.
Standard Evaluation Metrics
Population-Level (Fit-Based):
- MSE/RMSE on top 1,000-2,000 most expressed genes
- Pearson Correlation (PCC) between predicted and observed profiles
- Pearson Delta (R²): cor(ŷ - y^control, y - y^control)
- R² Score: Standard regression explained variance
Distribution-Based:
- Energy Distance: 2·E[||X-Y||] - E[||X-X'||] - E[||Y-Y'||]
- Wasserstein Distance (W2): Optimal transport cost
- MMD (Maximum Mean Discrepancy): With RBF kernel
Rank-Based (Critical for Mode Collapse Detection):
- Rank metric: Measures how well predictions rank-order cells
- Transposed-Rank: More challenging variant
- Matrix Distance: Similarity matrix divergence
- These metrics are ESSENTIAL - many papers only report MSE/R² which miss mode collapse
📚 Method Comparison
| Method |
Year |
Architecture |
Scale |
Key Innovation |
| Perturb-Seq |
2016 |
Experimental |
~10K-200K cells |
CRISPR + scRNA-seq pooled screens |
| scGen |
2019 |
VAE |
~50K cells |
Latent space vector arithmetic |
| CellOT |
2023 |
Neural OT |
~50K cells |
ICNN transport maps, single-cell predictions |
| SAMS-VAE |
2023 |
VAE |
~120K cells |
Sparse additive mechanism shifts |
| CPA |
2023 |
VAE |
~100K cells |
Compositional perturbation + covariates |
| GEARS |
2023/2024 |
GNN |
~600K cells |
Dual knowledge graphs (GO + coexpression) |
| CellOracle |
2023 |
GRN |
~100K cells |
scATAC-seq informed GRN inference |
| scGPT |
2024 |
Transformer (100M) |
33M cells pretrain |
Generative pretraining (BUT often underperforms) |
| scFoundation |
2024 |
Transformer (100M) |
50M cells |
Read-depth-aware (BUT mode collapse issues) |
| CIPHER |
2025 |
Statistical Physics |
~1.4M cells |
Linear response theory (Δx = Σu) |
Note: Publication years reflect official journal publication dates.
Some papers (e.g., GEARS) appeared online earlier than print publication.
💡 Practical Implementation Guide
Choosing the Right Method
Decision Framework
Step 1: Always implement simple baselines first:
- Additive model: Δ_combo = Δ_gene1 + Δ_gene2
- Linear regression with gene features
- Mean prediction (no-change model)
For Drug Response Prediction:
- Chemical perturbations → CellOT (single-cell), CPA if have dose info
- Cross-cell-type generalization → Test on multiple cell lines
- Dose-response modeling → Methods that model continuous covariates
For Genetic Perturbations:
- Single-gene → GEARS (if genes in GO), CIPHER, or simple linear models
- Combinatorial → Test additive baseline first, then GEARS or SAMS-VAE
- Genome-wide screens → scGPT/scFoundation
For Interpretability:
- Mechanistic understanding → CIPHER (covariance), CellOracle (GRN)
- Uncertainty quantification → GEARS (Bayesian), CIPHER (Horseshoe priors)
- Avoid black-box transformers if interpretability is priority
Common Pitfalls & Best Practices
-
Data Quality: Ensure high UMI counts (>1000), proper doublet removal,
batch correction before modeling. Use established pipelines (Scanpy, Seurat).
-
Evaluation: Use held-out PERTURBATIONS, not held-out cells from
training perturbations. This tests generalization properly.
-
Baseline Comparisons: Always compare against simple linear models.
-
Mode Collapse Detection: Use rank metrics, visualize prediction distributions.
If all predictions look similar, model has collapsed.
-
Generalization: Test cross-dataset, cross-cell-type, and out-of-distribution
generalization. Don't just report in-distribution performance.
-
Reproducibility: Report all hyperparameters, random seeds, preprocessing steps,
and provide code. Use version control.
-
Biological Validation: Computational predictions should be validated with
targeted experiments on key predictions.
Software Ecosystem
- Scanpy: Standard preprocessing, analysis, visualization (Python)
- scvi-tools: Implementations of scVI, scGen, CellOT
- Pertpy: Perturbation-specific analysis (Mixscape, Augur)
- Method-specific repos: GEARS, SAMS-VAE, scGPT, CPA all on GitHub
- PerturBench: Standardized benchmarking framework (altoslabs, 2024)
🛠️ Hands-On Practice
The walkthrough below runs a complete, purely computational perturbation-prediction experiment end to end: load a Perturb-seq dataset (Norman 2019 CRISPRa or Adamson 2016 CRISPRi), split it so that entire perturbations — not cells — are held out, train GEARS on the seen perturbations, predict post-perturbation expression for the unseen ones, and score the predictions with MSE on the top differentially expressed genes plus Pearson delta against a control-mean baseline. Everything is retrospective: no new experiments, only re-analysis of published screens.
Environment & packages
GEARS pins an older PyTorch Geometric stack, so give it its own environment rather than sharing one with scGPT or scvi-tools. Install PyTorch (matching your CUDA version) first, then the PyG companion wheels, then GEARS. scGen and CPA live in the scvi-tools ecosystem and can share a second environment; pertpy provides the dataset loaders and the differential-expression utilities used for scoring.
# conda / mamba recommended
conda create -n pertpred python=3.10 -y
conda activate pertpred
# 1. PyTorch first — pick the build that matches your driver
pip install torch --index-url https://download.pytorch.org/whl/cu121
# 2. PyTorch Geometric + companion wheels (GEARS' graph backend)
pip install torch_geometric
pip install torch_scatter torch_sparse \
-f https://data.pyg.org/whl/torch-2.1.0+cu121.html
# 3. Core stack
pip install cell-gears scanpy anndata pertpy
# Separate env for the VAE/latent-arithmetic family (scvi-tools pins conflict)
# conda create -n pertvae python=3.10 -y && conda activate pertvae
# pip install scgen cpa-tools scvi-tools scanpy
Hardware. Norman 2019 (~110k cells, 287 perturbations) trains on a single 16 GB GPU in roughly 1–2 hours for 20 epochs; CPU-only training is possible but an order of magnitude slower. Peak host RAM is dominated by the dense gene × perturbation tensors GEARS builds — budget 32–64 GB. Genome-scale Replogle K562 (>2.5M cells) needs a compute node with 128 GB+ RAM and is usually subsampled before any exploratory run.
Data structures & formats
AnnData — adata.X holds normalized, log1p-transformed expression (GEARS expects log-space, not raw counts); adata.obs carries the perturbation label; adata.var must contain a gene_name column so perturbation identifiers can be matched to measured genes.
- Perturbation label convention — GEARS parses
adata.obs["condition"] as "GENE+ctrl" for singles, "GENEA+GENEB" for doubles, and the literal string "ctrl" for unperturbed cells. Malformed labels are the single most common load failure.
- Gene co-expression / GO knowledge graph — two graphs are auto-built by
PertData: a gene–gene co-expression graph over the measured transcriptome, and a perturbation–perturbation similarity graph derived from Gene Ontology term overlap. The GO graph is what lets GEARS place a never-seen perturbed gene near functionally related seen genes and thus emit a non-trivial prediction for it.
- Split dictionary —
pert_data.set2conditions maps train/val/test to lists of condition strings. This is the object to inspect (and to save alongside results) when you want the split to be reproducible.
- DE gene index —
adata.uns["rank_genes_groups_cov_all"] stores, per condition, the ranked list of differentially expressed genes versus control. All headline metrics are computed on the top 20 of these, not on the full transcriptome.
- Model outputs —
model.predict([["GENE"], ["GENEA", "GENEB"]]) returns a dict keyed by condition, each value a length-n_genes vector of predicted mean post-perturbation expression in log space.
Minimal code walkthrough
Load Norman 2019 through the GEARS dataloader, create a split that holds out entire unseen perturbations, train, predict, and evaluate. The evaluation block is the part worth reading closely: it computes MSE on the top-20 DE genes, Pearson delta, and the control-mean baseline in the same loop so the three numbers are always reported together.
import numpy as np
from scipy.stats import pearsonr
from gears import PertData, GEARS
# 1. Load a published Perturb-seq screen. "norman" = Norman et al. 2019
# (K562, CRISPRa, 100 singles + 131 doubles); "adamson" = Adamson et al.
# 2016 (K562, CRISPRi, single-gene only). PertData downloads the h5ad,
# normalizes/log1p's it, and builds the gene co-expression graph.
pert_data = PertData("./data")
pert_data.load(data_name="norman")
# 2. Hold out entire PERTURBATIONS, not cells. Held-out cells from a
# perturbation the model already trained on measure nothing but denoising.
# split="simulation" mimics the realistic case: some test perturbations
# involve genes never perturbed during training (unseen singles), and some
# doubles combine a seen gene with an unseen one (0/1/2-seen subgroups).
pert_data.prepare_split(split="simulation", seed=1, train_gene_set_size=0.75)
pert_data.get_dataloader(batch_size=32, test_batch_size=128)
print({k: len(v) for k, v in pert_data.set2conditions.items()})
# 3. Train. GEARS embeds each perturbed gene via the GO-derived perturbation
# graph, so an unseen gene inherits signal from its functional neighbours
# instead of falling back to the training mean.
model = GEARS(pert_data, device="cuda")
model.model_initialize(hidden_size=64)
model.train(epochs=20, lr=1e-3)
model.save_model("gears_norman")
# 4. Predict post-perturbation expression for held-out conditions.
# Singles are single-element lists; doubles are two-element lists.
test_conds = pert_data.set2conditions["test"]
preds = model.predict([c.replace("+ctrl", "").split("+")
for c in test_conds if c != "ctrl"])
# 5. Reference quantities from the raw AnnData
adata = pert_data.adata
ctrl_mean = np.asarray(
adata[adata.obs["condition"] == "ctrl"].X.mean(axis=0)
).ravel()
gene_names = adata.var["gene_name"].values
de_map = adata.uns["rank_genes_groups_cov_all"] # condition -> ranked DE genes
# 6. Evaluate. Three numbers, always together:
# (a) MSE on the top-20 DE genes — all genes dilutes the signal to noise
# (b) Pearson delta — cor(pred - ctrl, true - ctrl)
# (c) the control-mean baseline — "predict no change at all"
rows = []
for cond in test_conds:
if cond == "ctrl" or cond not in preds:
continue
true_mean = np.asarray(
adata[adata.obs["condition"] == cond].X.mean(axis=0)
).ravel()
pred_mean = preds[cond]
de20 = de_map[cond][:20]
idx = np.where(np.isin(gene_names, de20))[0]
mse_de = np.mean((pred_mean[idx] - true_mean[idx]) ** 2)
mse_base = np.mean((ctrl_mean[idx] - true_mean[idx]) ** 2)
# Pearson DELTA, not raw Pearson. Raw cor(pred, true) over all genes runs
# ~0.98 for essentially any model, because most genes do not respond to
# the perturbation and both vectors are dominated by the shared baseline
# expression profile. Subtracting the control mean removes that shared
# component and scores only what the model actually had to get right:
# the direction and magnitude of the CHANGE.
r_delta = pearsonr(pred_mean[idx] - ctrl_mean[idx],
true_mean[idx] - ctrl_mean[idx])[0]
r_raw = pearsonr(pred_mean[idx], true_mean[idx])[0]
rows.append((cond, mse_de, mse_base, r_delta, r_raw))
mse_de, mse_base, r_delta, r_raw = (np.nanmean([r[i] for r in rows])
for i in (1, 2, 3, 4))
print(f"MSE (top-20 DE) : {mse_de:.4f}")
print(f"MSE control-mean baseline: {mse_base:.4f} <- must beat this")
print(f"Pearson delta : {r_delta:.3f}")
print(f"Pearson raw (inflated) : {r_raw:.3f}")
# 7. Break the score out by generalization difficulty. A model can look strong
# overall while being useless on the only split that matters.
subgroups = pert_data.subgroup["test_subgroup"] # e.g. unseen_single, combo_seen0/1/2
for grp, conds in subgroups.items():
sub = [r for r in rows if r[0] in set(conds)]
if sub:
print(f"{grp:15s} n={len(sub):3d} "
f"Pearson delta={np.nanmean([r[3] for r in sub]):.3f}")
Common pitfalls & tips
- Score DE genes, not all genes. Averaging MSE over ~19,000 measured genes buries the few dozen that actually respond; the metric then mostly rewards reproducing the baseline transcriptome. Restrict headline MSE and correlation to the top 20–100 DE genes per condition, and state the gene set explicitly — results are not comparable across papers that use different ones.
- Report the control-mean baseline or the number means nothing. "Predict the unperturbed mean for every condition" is a deceptively strong baseline: on Replogle only about 41% of perturbations produce a measurable effect, so no-change is exactly right most of the time. Several published models fail to beat it on unseen perturbations. Always print baseline MSE next to model MSE.
- Pearson delta, never raw Pearson. Raw cor(predicted, observed) sits near 0.98 for almost any model because both vectors are dominated by shared baseline expression. Only cor(pred − ctrl, true − ctrl) scores the change itself, and it typically drops to 0.3–0.6 — that gap is the honest measure of what the model learned.
- Unseen-single and unseen-double splits are different problems. Predicting a double knockout where both genes were seen individually is largely an additivity test that a simple ΔA + ΔB model handles well. Predicting a perturbation of a gene never seen in training requires genuine transfer through the GO/co-expression graph and is far harder. Report the subgroups separately (combo_seen0/1/2, unseen_single); a pooled average hides the failure mode.
- Weak knockdown looks identical to model failure. CRISPRi efficiency varies substantially per guide, and a target that was only partially repressed produces a near-null expression profile. The model is then penalized for predicting a real effect that the experiment never induced. Filter test perturbations to those with a verified on-target knockdown (or a significant Energy-distance/E-test effect versus control) before scoring, and report how many conditions were dropped.
- Batch and cell-line confounding travels with the screen. Norman/Adamson/Replogle are K562-dominated and each has its own 10x chemistry, depth, and gRNA library. A model trained on one and evaluated on another is measuring batch transfer as much as perturbation biology; check that predicted deltas are not simply reproducing the donor/lane axis, and integrate or regress out batch before cross-dataset claims.
- MOI determines how noisy your labels are. Low-MOI screens give near-clean one-guide-per-cell assignment; high-MOI designs boost combinatorial coverage but leave ambiguous or multiply-assigned cells. Run Mixscape (pertpy) to drop cells with no detectable perturbation signature, and treat guide-assignment confidence as a filter, not as ground truth.
- Keep the split file, not just the seed.
prepare_split behavior can change across GEARS versions; save pert_data.set2conditions to disk with the results so the same held-out perturbations can be re-scored later by a different method.