Skip to content

feat: add mixscale continuous perturbation scoring#945

Open
stefanm808 wants to merge 1 commit into
scverse:mainfrom
stefanm808:feat/mixscale
Open

feat: add mixscale continuous perturbation scoring#945
stefanm808 wants to merge 1 commit into
scverse:mainfrom
stefanm808:feat/mixscale

Conversation

@stefanm808

Copy link
Copy Markdown

Implements the Mixscale scoring method (Jiang et al., Nat Cell Biol 2025) as a new method on the Mixscape class. Unlike the binary KO/NP classification in mixscape(), mixscale() computes a continuous perturbation efficiency score per cell likescalar projection onto the estimated perturbation direction vector.

Reuses existing _get_perturbation_markers() pipeline for DE gene detection and follows the same code patterns as mixscape() for split handling, layer access & scaling.

Closes #921 (partial - continuous scoring component)

PR Checklist

  • Referenced issue is linked
  • If you've fixed a bug or added code that should be tested, add tests!

Description of changes
Added Mixscape.mixscale() for continuous perturbation efficiency scoring, implementing the method from Jiang, Dalgarno et al., Nature Cell Biology (2025). Unlike the binary KO/NP classification in mixscape(), mixscale() computes a continuous perturbation efficiency score per cell via scalar projection onto the estimated perturbation direction vector. This is particularly useful for CRISPRi/CRISPRa screens where cells exhibit a gradient of perturbation responses.

Usage:

ms = pt.tl.Mixscape()
ms.perturbation_signature(adata, "perturbation", "NT", split_by="replicate")
ms.mixscale(adata, "gene_target", "NT", layer="X_pert")
# Continuous scores in adata.obs["mixscale_score"]

Technical details

Algorithm:

  1. Reuses existing _get_perturbation_markers() pipeline for DE gene detection
  2. Subsets perturbation signatures to DE genes
  3. Computes perturbation direction vector (mean perturbed − mean control)
  4. Scalar-projects each cell's signature onto this direction
  5. The Z-score standardizes relative to the NT control distribution

Follows the same code patterns as mixscape() for split handling, layer access, and scaling. No new dependencies added. 9 new tests added, all 5 existing Mixscape tests still pass.

Additional context

This addresses item 1 (continuous perturbation scoring) from #921. Items 2-4 (weighted DE, decomposition, program signatures) are planned for follow-up PRs.

@codecov-commenter

codecov-commenter commented Apr 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.29181% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.76%. Comparing base (12897e1) to head (89f8b7a).
⚠️ Report is 86 commits behind head on main.

Files with missing lines Patch % Lines
pertpy/tools/_perturbation_screen/_mixscale.py 76.19% 45 Missing ⚠️
pertpy/tools/_perturbation_screen/_base.py 81.60% 16 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #945      +/-   ##
==========================================
+ Coverage   73.54%   77.76%   +4.22%     
==========================================
  Files          48       50       +2     
  Lines        5613     6575     +962     
==========================================
+ Hits         4128     5113     +985     
+ Misses       1485     1462      -23     
Files with missing lines Coverage Δ
pertpy/tools/__init__.py 78.37% <100.00%> (+0.60%) ⬆️
pertpy/tools/_perturbation_screen/_mixscape.py 88.12% <100.00%> (ø)
pertpy/tools/_perturbation_screen/_base.py 81.60% <81.60%> (ø)
pertpy/tools/_perturbation_screen/_mixscale.py 76.19% <76.19%> (ø)

... and 10 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Zethson Zethson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

thanks for the contribution! I'd very happy to get this feature merged eventually.

This looks a bit AI generated. Could you please disclose that? There's a few things that the contributing guide outlines that was missed. It would be awesome if you could have another look, please.

Some initial feedback:

  1. Should this be in the mixscape code base or should this be its own Tool? Even from a user perspective.
  2. This also needs to show up in the tutorials. Maybe a general version of mixscape & mixscale? We might need a new term then.
  3. We should likely also point this out clearer in our documentation which pretty much just mentions mixscale for now.
  4. The ground truth is the R implementation. Could you please compare your version against the R version? We did the same for mixscape. They should be as close as possible.

Comment thread pertpy/tools/_mixscape.py Outdated
):
"""Calculate continuous perturbation scores using the Mixscale method.

Unlike :meth:`mixscape` which performs binary KO/NP classification via

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sphinx ref doesn't work. Please fix it.

Comment thread pertpy/tools/_mixscape.py Outdated
"""Calculate continuous perturbation scores using the Mixscale method.

Unlike :meth:`mixscape` which performs binary KO/NP classification via
Gaussian Mixture Models, this method assigns a continuous perturbation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please format these docstrings like the others are formatted. This is LLM generated.

Comment thread pertpy/tools/_mixscape.py Outdated
test_method=test_method,
)

# Get perturbation signature matrix

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of a noisy comment.

Comment thread pertpy/tools/_mixscape.py Outdated
categories = split_obs.unique()
split_masks = [split_obs == category for category in categories]

# Reuse the existing DE gene detection pipeline

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is LLM noise. Please remove it

Comment thread pertpy/tools/_mixscape.py Outdated
continue

de_genes = perturbation_markers[(category, gene)]
# Limit to max_de_genes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM noise?

Comment thread pertpy/tools/_mixscape.py Outdated
pvec = dat.dot(vec) / vec_norm_sq if isinstance(dat, spmatrix) else np.dot(dat, vec) / vec_norm_sq
pvec = np.asarray(pvec).flatten()

# Extract scores for guide and NT cells

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM noise?

Comment thread tests/tools/test_mixscale.py Outdated


@pytest.fixture
def synthetic_perturbation_adata():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you reuse the fixture that we're using to test mixscape?

Comment thread tests/tools/test_mixscale.py Outdated
return adata


class TestMixscale:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not really using test classes in pertpy

Comment thread tests/tools/test_mixscale.py Outdated
assert gene_a_scores.abs().mean() > 0
assert gene_b_scores.abs().mean() > 0

def test_sparse_input(self, synthetic_perturbation_adata):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of a useless test. But ideally this were parametrized for the different array types instead of being its own test.

@stefanm808

Copy link
Copy Markdown
Author

Hey thanks for taking a look! I ended up overlooking your styling conventions in the process. A separate tool suggestion is a good shout actually... probably makes more sense architecturally. I will swing back to this when I get a chance! Have a great day.

@Zethson

Zethson commented May 19, 2026

Copy link
Copy Markdown
Member

@stefanm808 very kind check in - is there anything you'd need from me to revive this PR?
I'd love to see you finalize it.

Thanks!

@Zethson

Zethson commented Jun 5, 2026

Copy link
Copy Markdown
Member

@stefanm808 I might take this PR over next week unless you want to finish it?

Zethson added a commit to stefanm808/pertpy that referenced this pull request Jun 9, 2026
Adds pt.tl.Mixscale for continuous per-cell perturbation scoring
(Jiang et al., Nature Cell Biology 2025), matching the satijalab/Mixscale
R implementation to floating-point precision.

Mixscape is refactored into a _mixscape/ package: a shared
PerturbationScreenAnalyzer base (perturbation_signature + DE marker
detection) with Mixscape (binary) and Mixscale (continuous) as siblings.
pt.tl.Mixscape is unchanged.

Builds on scverse#945.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds pt.tl.Mixscale for continuous per-cell perturbation scoring
(Jiang et al., Nature Cell Biology 2025), matching the satijalab/Mixscale
R implementation to floating-point precision.

Mixscape is refactored into a _perturbation_screen package: a shared
PerturbationScreenAnalyzer base (perturbation_signature + DE marker
detection) with Mixscape (binary) and Mixscale (continuous) as siblings.
pt.tl.Mixscape is unchanged.

Renames the tutorial to perturbation_efficacy and bumps the pertpy-tutorials
submodule to the matching commit (scverse/pertpy-tutorials#64).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add mixscale for continuous perturbation scoring

3 participants