mantispy.tl.perturbation_score#

mantispy.tl.perturbation_score(adata, groupby='Metadata_Gene', reference='negcon', use_rep=None, layer=None, replicate='Metadata_sgRNA', by=None, n_features=200, n_folds=5, min_cells=50, seed=0, key_added='perturbation_score', copy=False)[source]#

Score each cell for how clearly it shows its group’s phenotype, by a classifier against the reference cells.

In an optical pooled screen many cells carry a guide but show no phenotype, because the knockout did not take or the cell escaped it, and averaging them with the responders dilutes a real hit. This is the perturbation score of Brieflow [Di Bernardo and others, 2026]: for each group, its cells and an equal-sized random sample of the reference cells are told apart by a logistic regression on the n_features features with the largest ANOVA F statistic. Each cell’s score is its out-of-fold probability of belonging to the group, from a model that never saw it, and the group’s ROC AUC over those probabilities says how separable the group is at all. Feature selection is refitted inside every fold, so the held-out cells never inform which features are used.

The features should already be centred and scaled on the reference cells within each batch, which normalize() does once for the whole screen with method="standardize", reference="negcon" and by the plate and well. Brieflow scales each group’s sample on its own sampled controls instead, which fits the scale to the very controls the model then sees, so its AUCs run slightly higher.

To drop the non-responders, subset on the score, for example adata[~(adata.obs["perturbation_score"] < 0.5)], which keeps the reference cells and the unscored groups, whose score is missing.

Parameters:
adata AnnData

Cells by features, with the group label and the reference class in obs.

groupby str (default: 'Metadata_Gene')

obs column holding the group each cell is scored as, usually the gene.

reference str (default: 'negcon')

The cells every group is told apart from: "negcon" for the rows whose Metadata_Control_Type is "negcon", or the name of a boolean obs column.

use_rep str | None (default: None)

Read obsm[use_rep] instead of X; cannot be combined with layer.

layer str | None (default: None)

Read this layer instead of X.

replicate str | Sequence[str] | None (default: 'Metadata_sgRNA')

obs column, or columns, whose groups are kept whole within a cross-validation fold, the guide by default ("Metadata_sgRNA", as composition_shift() also defaults to). The model is then always tested on guides it was not trained on, so a phenotype one guide carries alone, such as an off-target effect, does not raise the group’s score. A named column that obs lacks raises, naming the argument. Replicates are ordered by their labels as strings, so a string column and a categorical one draw the same folds. None draws folds over cells, as Brieflow does, and warns on an object stamped object resolution, since non-targeting guides grouped into pseudo-genes can then score above one half.

by str | None (default: None)

obs column whose levels are scored apart, for example "Metadata_Class" to score interphase and mitotic cells separately, as Brieflow does. Each group is scored within each level against a reference sample drawn from that level, so a cell’s score and its group’s AUC come from its own level; a group too small within a level is left unscored there. None scores every cell in one pass.

n_features int (default: 200)

How many features, ranked by the ANOVA F statistic within each fold, the model reads.

n_folds int (default: 5)

Cross-validation folds, capped by the cells, or with replicate the replicates, on the smaller side.

min_cells int (default: 50)

Groups with fewer cells than this are left unscored. Brieflow’s minimum of 100 counts the group and its equal-sized reference sample together.

seed int (default: 0)

Seeds the reference sample and the fold split, together with the group’s name, so a group’s score does not depend on which other groups are present.

key_added str (default: 'perturbation_score')

obs column for the per-cell score and the uns["mantispy"] key for the per-group table.

copy bool (default: False)

Return a modified copy instead of mutating in place.

Return type:

AnnData | None

Returns:

None, or the modified copy. Writes obs[key_added], each cell’s out-of-fold probability of its group, missing for the reference cells and for unscored groups. Writes uns["mantispy"][key_added], one row per group with group, n_cells, n_reference (the size of its reference sample), n_folds and auc, which is missing for an unscored group; with by, one row per group and level and a column named for by holding the level.

Raises:
  • ValueError – n_features is not a positive integer, n_folds is not an integer of at least two, reference selects no cells, or a scored cell or a reference cell has a missing feature value.

  • KeyError – groupby, replicate or by names a column that obs lacks.

Notes

The reference cells set the null each group is tested against and are not scored themselves. Cells of a reference class score as a group only when they are not in reference, for example intergenic guides scored against non-targeting ones, which is a check of the calibration; intergenic guides cut the genome, so they can carry a mild phenotype of their own.

Cells in one well share its plate position, seeding and focus, but in a pooled screen every well holds every group, so the well does not separate a group from the reference and needs no grouping here. The guide does: random folds let the model learn what a guide’s own cells share, and pass that off as the gene’s phenotype.

The cost grows with the number of cells, not with the number of groups, since each group’s fit sees its own cells and as many reference cells. Every fold of every group is fitted in parallel on numba’s threads, which numba.set_num_threads caps. The fit is a port of the liblinear solver LogisticRegression(solver="liblinear") runs, so the scores match scikit-learn’s as closely as liblinear matches itself when its input moves by rounding. A backed object is read a batch of groups at a time, each group’s rows and its reference sample, so it is never loaded whole.