🧬 Single-Cell Perturbation Modeling & Prediction

Computational Methods for Predicting Cellular Responses to Genetic & Chemical Perturbations

A comprehensive guide to state-of-the-art approaches combining single-cell transcriptomics, CRISPR screens, and machine learning to predict how cells respond to perturbations. Covering methods from 2016-2026, including recent critical evaluations of foundation models.

Four Grand Slam mini-scenes of the Healshu tennis player: serving on a blue hard court in summer heat, sliding into a long rally on orange clay, reaching for a skidding low ball on grass, and reviewing a dotted clipboard under night floodlights
Tennis lens · perturbation modeling

发球出预测,回球见真章。

Perturbation modeling is a rally between model and lab — played across four very different courts. The model serves a prediction, the wet lab returns the measured response, and the gap between them is the loss, the benchmark, and the next drill. 四大满贯,四种场地,同一个问题:your prediction only counts if it holds on every surface.

Healshu tennis player at the center of a 2x2 quadrant of four court surfaces — blue hard, orange clay, green grass, and a night-lit hard court — with four balls trailing differently colored arcs toward each quadrant

Read the Rally.

「读懂发球,才接得住回球。」

Every perturbation experiment is a rally: the model serves a predicted response, the cell returns the measured one. And the tour never stays on one surface — hard court, clay, grass, and night sessions each change how the ball behaves, the way new cell types, platforms, and conditions change how a perturbation reads out. Six moments from the practice courts, re-read as the concepts this module's methods and benchmarks formalize.

Scene 1 · 发球权在你手上

You choose the serve

On the practice court you pick the serve — flat, slice, topspin — and the drill is only as informative as the serves you bother to practice.

Perturbation design. The intervention is the serve: gene knockout, drug, dose, combination. CRISPR screens are the cataloged serve drills — thousands of serves, one court. See the task ↓

Scene 2 · 出手前报落点

Call the landing spot

A rally player calls the return before the ball crosses the net — from the serve's spin, speed, and the opponent's stance, not from watching it land.

Response prediction. Given control state + serve, call the perturbed state: control + Δ = perturbed. The whole task is calling the Δ before the experiment returns it. See the formulation ↓

Scene 3 · 只会一招的对手

The one-play opponent

On clay at Roland Garros you meet the grinder: serves that look wild but all land in the same corner. Stand in that corner and you look like a genius predictor — until the tour moves to a different surface.

Systematic variation & the mean baseline. Any perturbation shifts the whole transcriptome in one direction, so predicting the perturbed-group mean scores like SOTA without learning anything per-gene. Always check what the corner-stander scores. See the critique ↓

Scene 4 · 没见过的旋转

The unseen spin

Drilling a hundred known serves on hard court is not the Championships. The first morning on grass, the ball skids through low and late — a serve you have never seen, and your read has to travel.

Unseen-perturbation generalization. The hard split is not random cells but new genes, new combinations, new doses. Most methods' scores drop sharply here; this is the split that counts. See benchmarks ↓

Scene 5 · 名将输给基本功

The star loses to fundamentals

A highlight-reel champion with no footwork loses to the club player who simply returns every ball deep. Reputation is not a return game.

Foundation models vs simple baselines. On standard perturbation benchmarks, large pretrained models often fail to beat train-mean or linear baselines. Scale is not mechanism; check the fundamentals first. See the evidence ↓

Scene 6 · 赛后复盘

The match debrief

Good players debrief every rally — which reads were wrong, by how much, on which serve — and choose tomorrow's drill from the errors, not from the highlight reel.

Honest metrics + active learning. Debrief with centroid-referenced metrics (top-DEG Δ, E-distance), then spend the next experiment where the error is largest: optimal intervention design closes the loop. See the loop ↓

Four-panel tennis comic across the four Grand Slams: calling the landing spot on a blue hard court; a ball machine firing every ball into the same corner on clay; a skidding low ball missed on grass; and a night-session debrief with a dotted clipboard and a starred ball
One player, four courts. Call it before it lands (hard-court prediction) · every ball in one corner (the clay grinder = the mean baseline) · a spin never seen (first time on grass = an unseen perturbation) · debrief, then drill (honest metrics under the lights).

每一杆都是实验,每个回合都在校准 — every serve is a prediction, every return a measurement; the rally is the loop.

19 Curated Papers & Resources
5 Method Categories
8 Datasets & Benchmarks
2016-2026 Years Covered

🎯 Core Concepts

The Challenge

Understanding how cells respond to perturbations (genetic knockouts, drug treatments, etc.) is fundamental to biology and medicine. However:

Key Biological Facts

The Opportunity

Computational prediction methods aim to:

📊 Method Categories

1. Variational Autoencoder (VAE) Approaches

scGen - Latent Space Arithmetic

2019 Nature Methods VAE

Pioneering VAE-based approach using vector arithmetic in latent space for perturbation prediction. Assumes homogeneous responses and additive perturbation effects.

  • First to use VAEs for perturbation prediction
  • Simple latent arithmetic: perturbed = control + perturbation_vector
  • Struggles with cell-type-specific and heterogeneous responses

SAMS-VAE - Sparse Additive Mechanism Shifts

2023 NeurIPS VAE

Combines compositionality, disentanglement, and interpretability through sparse global perturbation variables. Each perturbation targets sparse subset of latent dimensions.

  • Sparse additive latent perturbation: z_perturbed = z_control + Σ sparse_effects
  • Disentangled perturbation-specific latent subspaces
  • Outperforms CPA on combinatorial perturbation tasks

CPA - Compositional Perturbation Autoencoder

2023 Mol Sys Bio VAE

Conditional VAE with additive composition of perturbation and covariate embeddings. Predicts counterfactual distributions but requires training data for each perturbation.

  • Compositional: combines perturbation + cell-type + dose embeddings
  • Predicts full distributions, not just means
  • First method to model combinatorial drug perturbations with continuous dose and covariate information

2. Graph Neural Network Methods

GEARS - Graph-Enhanced Gene Activation/Repression Simulator

2024 Nature Biotechnology GNN

Integrates dual knowledge graphs (gene coexpression + Gene Ontology perturbation network) with deep learning.

  • Dual GNN architecture: gene coexpression graph + GO perturbation graph
  • 40% higher precision in predicting genetic interaction subtypes vs baselines
  • Autofocus direction-aware loss + Bayesian uncertainty quantification
  • Limitation: Requires genes in GO knowledge graph; degrades for poorly connected genes

3. Optimal Transport Approaches

CellOT - Neural Optimal Transport

2023 Nature Methods Optimal Transport

Uses Input Convex Neural Networks (ICNNs) to learn transport maps between unpaired control and perturbed cell distributions. Predicts single-cell-level responses.

  • ICNNs ensure convexity of transport maps for guaranteed optimality
  • Handles unpaired data - doesn't need matched control/treated cells
  • Predicts individual cell trajectories, not just population means
  • Generalizes to holdout patients and cross-species transfer

4. Transformer & Foundation Models

scGPT - Generative Pretrained Transformer for scRNA-seq

2024 Nature Methods Transformer

Foundation model pretrained on 33+ million cells. Uses masked gene modeling with binned expression values. Fine-tuned for perturbation prediction with adapter layers.

  • 100M+ parameters, generative pre-training on massive scale
  • Gene-specific embeddings + expression value binning

scFoundation - 100M Parameter Foundation Model

2024 Nature Methods Transformer

Large-scale foundation model with read-depth-aware pretraining on 50M cells. Designed for multiple downstream tasks including perturbation prediction.

  • 100M parameters trained on diverse cell types and tissues
  • Read-depth normalization incorporated into architecture

State: Predicting Cellular Responses Across Diverse Contexts

2025 bioRxiv (June 2025) Transformer

Set-based transformer trained on 100M+ perturbation cells across 70 cell lines and 167M observational cells. Arc Institute's first virtual cell model using bidirectional attention over cell populations.

  • State Embedding (SE) + State Transition (ST) architecture
  • 50%+ improvement in perturbation effect discrimination on Tahoe-100M
  • 2× accuracy in identifying true differentially expressed genes
  • NOTE: Released June 2025 - too recent for independent validation

STACK: In-Context Learning of Single-Cell Biology

2026 bioRxiv In-Context Learning

Tabular-attention foundation model trained on 149M cells that performs true in-context learning at the cell-set level, reframing perturbation prediction as prompting rather than fine-tuning: give the model a labelled prompt cell-set at inference time and it predicts the response to a new, unseen perturbation with no weight updates.

  • Ranks 1st in 28/31 in-context-learning evaluations, spanning zero-shot perturbation, donor, and condition transfer
  • Releases Perturb-Sapiens: 513,870 cells, 28 tissues, 40 cell classes, 201 perturbations with cross-tissue validation
  • Directly targets the field's fine-tuning-data bottleneck — no per-dataset training loop required
  • See the Foundation Models module for the pretraining recipe behind STACK

Both State and STACK are 2026-era transformer approaches, but they represent opposite adaptation paradigms: State fine-tunes a set-based transformer on the target task, while STACK uses in-context learning with no weight updates. For the pretraining objectives, tokenisation choices, and scaling laws behind these and other foundation models, see the Pretraining & Foundation Models module.

5. GRN-Based & Physics-Inspired Methods

CellOracle - GRN-Based Perturbation Prediction

2023 Nature GRN

Two-stage pipeline: (1) infer base GRN from scATAC-seq, (2) refine with scRNA-seq using Bayesian/Bagging Ridge regression. Linear propagation through GRN.

  • Integrates chromatin accessibility (scATAC-seq) for regulatory links
  • Successfully predicted zebrafish embryogenesis trajectories
  • Linear assumption: effect propagates through GRN edges
  • Interpretable - directly maps to biological regulatory networks

CIPHER - Linear Response Theory for Perturbations

2025 bioRxiv Statistical Physics

Physics-inspired approach using linear response theory: Δx = Σu where Σ is covariance matrix from control cell fluctuations. Bayesian inference with Horseshoe priors.

  • Covariance matrix Σ_ij = ⟨δx_i δx_j⟩ from control cells
  • R² up to 1.0 on synthetic networks with known ground truth
  • Soft mode analysis reveals ~3 dominant regulatory modules
  • Interpretable - connects to equilibrium statistical mechanics

CellOracle and CIPHER predict perturbation responses through a fixed or inferred regulatory graph. For methods that instead discover the underlying causal graph from perturbation data (DCDI, ENCO, and related structure-learning approaches), see the Causal Inference module — the two problems share data (Perturb-seq screens) but answer different questions: this module asks "what will happen," causal inference asks "what regulates what."

📈 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 + 3 datasets from other perturbation/readout modalities, 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)

📊 Benchmarking & Evaluation (2024-2026)

Ahlmann-Eltze, Huber & Anders: Deep-learning-based gene perturbation effect prediction does not yet outperform simple linear baselines

2025 Nature Methods Benchmark

Systematic benchmark pitting five foundation models plus two additional deep-learning methods against deliberately simple linear/additive baselines for single- and double-gene-perturbation transcriptome prediction. None of the deep models beat the baselines.

  • Linear additive model often has LOWER MSE than foundation models
  • Detected mode collapse: predictions don't vary across perturbations
  • For double perturbations, additive baseline outperformed all deep learning
  • Changed how field evaluates perturbation prediction methods

Virtual Cell Challenge

2025-2026 Cell (Commentary) Competition

Arc Institute's recurring benchmark competition for perturbation prediction, aiming to establish rigorous, CASP-style standards for the field. The inaugural 2025 challenge (H1 hESC context generalization) closed with results in early 2026; the 2026 edition raises the bar to zero-shot CRISPRi-response prediction across six held-out cell lines with no released training set.

  • 2025 winner: Team BM_xTVC (BioMap Research) with xTrimoSCPerturb, combining deep learning with classical statistical features — 5,000+ participants across 114 countries, 1,200+ teams, 300+ final submissions
  • Even the winning approach needed hand-engineered statistical features layered on deep learning — pure end-to-end learning has not yet solved the problem
  • 2026 edition: $100K grand prize (NVIDIA, 10x Genomics, Ultima Genomics); harder zero-shot generalization task, no released training data
  • Adopts PerturBench's rank-based metrics as part of the evaluation standard

PerturBench: Benchmarking Machine Learning Models for Cellular Perturbation Analysis

2024 NeurIPS / Nat Methods 2025 Benchmark Framework

Standardized benchmarking framework from Altos Labs — unified data loaders, biologically meaningful train/test splits, a modular model zoo, and rank-based metrics designed to complement (and expose the limits of) standard RMSE/R² fit measures.

  • Rank-based metrics later adopted as part of the Virtual Cell Challenge's evaluation standard
  • Shows simpler architectures often match complex ones and surfaces mode-collapse failure modes across published models
  • Modular model zoo + diverse datasets at multiple scales enable apples-to-apples comparison across methods

📈 Key Datasets & Benchmarks (Summary)

Major Perturbation Datasets

Standard Evaluation Metrics

Population-Level (Fit-Based):

Distribution-Based:

Rank-Based (Critical for Mode Collapse Detection):

📚 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 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)
State 2025 Transformer (Set-based) 100M+ cells pretrain State Embedding + State Transition, bidirectional attention over cell populations
STACK 2026 Transformer (ICL) 149M cells pretrain True in-context learning — no fine-tuning, no weight updates

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

For Zero-Shot / Few-Shot Transfer (no fine-tuning):

  • STACK's in-context-learning recipe → provide a labelled prompt cell-set at inference time, no weight updates. Strongest for cross-tissue and cross-perturbation transfer, but requires a model pretrained on cell-set-level inputs in the first place — not a free downstream choice for arbitrary architectures.
  • Zero-shot in-silico knockout via a pretrained scFM's attention/embedding perturbation (e.g., Geneformer) → cheaper than full fine-tuning if you only need a coarse ranked target list.
  • If adaptation is still needed → Geneformer-scaling's 4-bit QLoRA recipe fits a full 316M-parameter fine-tune on a single consumer GPU, making genome-scale in-silico screens routinely affordable (<$5k vs $25k for a 30K-cell × 4K-gene screen).

Common Pitfalls & Best Practices

Software Ecosystem

🛠️ 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 — pin the exact version so it matches the PyG wheel below
pip install torch==2.1.0 --index-url https://download.pytorch.org/whl/cu121

# 2. PyTorch Geometric + companion wheels (GEARS' graph backend).
#    The wheel URL is locked to torch 2.1.0+cu121 — if you install a
#    different torch/CUDA combo above, update this URL to match
#    (see https://data.pyg.org/whl/ for the available combinations).
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

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