spacr.qt.widgets.feature_rank

Rank per-object measurements by class-separation performance.

The default statistic is the area under the receiver-operating-characteristic curve (AUC), computed from the Mann–Whitney U statistic. Results are reported as the unit-free separation |2·AUC − 1| in [0, 1] together with the direction of the class difference. AUC is rank based and therefore invariant under monotonic transformations; it does not require normality, equal variance or symmetry. These properties allow measurements with different units and distributions to be compared in one ranking.

AUC represents the probability that a randomly selected object from one class has a higher value than a randomly selected object from the other class. It detects stochastic ordering but can miss distribution-shape differences. For example, classes with the same centre but different spread can have an AUC of 0.5 despite differing distributions. The Kolmogorov–Smirnov (KS) statistic is therefore computed for every feature. A high KS statistic combined with AUC near 0.5 sets FeatureScore.is_shape_not_shift.

Alternative ranking statistics are available through STATISTICS:

  • COHEN_D measures a standardized mean difference but is sensitive to skew, extreme observations and unequal group spread.

  • KS measures the largest difference between empirical cumulative distributions and detects spread changes, but does not provide direction.

  • MUTUAL_INFO detects non-monotonic associations but depends on binning and is biased upward for small samples.

ExplorerSpec.n_permutations enables a family-wise permutation calibration. Class labels are permuted, the complete ranking is recomputed and the maximum score from each permutation is retained. The 95th percentile of these maxima is returned as ExplorerResult.null_threshold. Calibration uses a seeded subsample of at most NULL_MAX_ROWS rows and is disabled by default because computation scales with the number of permutations.

The implementation uses NumPy and pandas and does not require Qt.

Exceptions

ExplorerError

A ranking that cannot be computed, with the reason in the message.

Classes

ClassSummary

One class's distribution of one feature — what the panel draws.

ExplorerResult

A ranking, and everything that qualifies it.

ExplorerSpec

What to rank, against what, by which statistic.

FeatureScore

One feature's separation, every statistic, and the caveats.

Functions

auc_of(→ float)

P(a random b scores above a random a), ties counted as half.

candidate_features(→ Tuple[str, ...])

Every numeric column that is a measurement, minus the label.

candidate_labels(→ Tuple[str, ...])

Columns that could say which class an object is in.

cohen_d_of(→ float)

(mean(b) − mean(a)) / pooled SD, ddof 1. NaN when either group is

distributions(→ Tuple[numpy.ndarray, Dict[str, ...)

Shared bin edges and per-class counts, for drawing one feature.

ks_of(→ float)

The largest gap between the two empirical CDFs, in [0, 1].

mutual_info_of(→ float)

Binned mutual information, normalised by the class entropy.

rank_features(→ ExplorerResult)

Score every feature against spec.label and sort by separation.

Module Contents

exception spacr.qt.widgets.feature_rank.ExplorerError[source]

Bases: ValueError

A ranking that cannot be computed, with the reason in the message.

Initialize self. See help(type(self)) for accurate signature.

class spacr.qt.widgets.feature_rank.ClassSummary[source]

One class’s distribution of one feature — what the panel draws.

Parameters:
  • level – the class name.

  • n – how many objects of that class have a value; 0 makes every statistic below NaN.

  • median – median of the class’s values.

  • q25 – 25th percentile of the class’s values.

  • q75 – 75th percentile of the class’s values.

  • low – smallest value in the class.

  • high – largest value in the class.

describe() → str[source]

This class’s count, median and interquartile range.

Returns:

a one-line description.

property is_low_n: bool[source]

Whether this class has too few objects to read as a distribution.

ZERO IS NOT LOW-N, it is empty: a class with no objects is a different problem from one with four, and marking it “low n” would suggest the number could be trusted a little.

Returns:

True when sparse but not empty.

class spacr.qt.widgets.feature_rank.ExplorerResult[source]

A ranking, and everything that qualifies it.

Parameters:
  • spec – the ExplorerSpec the ranking was made with.

  • label – the class column the features were ranked against.

  • classes – the class levels found in that column, sorted.

  • scores – the kept features, most separated first.

  • n_rows – number of rows in the table that was ranked, before any row was dropped.

  • n_considered – how many features were scored, including the ones below ExplorerSpec.top.

  • skipped – {feature: why} for columns that could not be scored — constant, all missing, or nothing left after dropping NaN. Reported rather than dropped: a feature missing from a ranking looks the same as a feature that ranked last.

  • null_threshold – the 95th percentile of the best score under label shuffling, or None when the null was not run.

__len__() → int[source]

Return how many features were ranked.

above_null() → Tuple[FeatureScore, ...][source]

The features that beat the label-shuffling null.

The whole ranking when the null was not run — with the caveat that “not run” is not the same as “passed”, which summary() says.

score_for(feature: str) → FeatureScore[source]

One feature’s score.

Parameters:

feature – the column name.

Returns:

its score.

summary() → str[source]

What was ranked, over how much, and what was skipped.

NAMES THE SKIPPED ONES. A ranking that quietly dropped constant or all-null columns would read as a complete answer over a set of features the user did not choose.

Returns:

a one-line summary.

top(count: int | None = None) → Tuple[FeatureScore, ...][source]

The best-scoring features, already ordered.

Parameters:

count – how many to take; None or 0 takes them all.

Returns:

the top scores.

class spacr.qt.widgets.feature_rank.ExplorerSpec[source]

What to rank, against what, by which statistic.

Parameters:
  • label – the column holding the class or condition.

  • features – the columns to rank. Empty means every continuous column, which is the point of the screen.

  • statistic – one of STATISTICS.

  • top – how many to keep in ExplorerResult.scores. The rest are counted, not silently dropped.

  • bins – bins for the mutual information and for the drawn histograms.

  • n_permutations – label shuffles for the null; 0 is off.

  • seed – for the null and for any subsampling, so a ranking is the same ranking twice.

Raises:

ExplorerError – on an unknown statistic or a non-positive top.

__post_init__() → None[source]

Normalise the label and features, and validate the ranking settings.

Raises:

ExplorerError – if the separation statistic is not one this module offers, or if top is below 1. bins is floored at 2 and the permutation count at 0 rather than refused – neither can make a ranking wrong, only less informative.

describe() → str[source]

The ranking in one line: what against what, by which statistic.

Returns:

a one-line description.

classmethod from_dict(payload: Mapping[str, Any]) → ExplorerSpec[source]

Rebuild a spec from plain data.

UNKNOWN KEYS ARE IGNORED rather than raising, so a spec saved by a later version still opens with the parts this one knows.

Parameters:

payload – what to_dict() produced.

Returns:

the rebuilt spec.

classmethod from_json(text: str) → ExplorerSpec[source]

Rebuild a spec from JSON text.

Parameters:

text – the JSON text.

Returns:

the rebuilt spec.

to_dict() → Dict[str, Any][source]

This spec as plain data.

Returns:

a JSON-safe dict.

to_json() → str[source]

This spec as JSON text, keys sorted so the file is diffable.

Returns:

the JSON text.

with_features(features: Sequence[str]) → ExplorerSpec[source]

A copy ranking a different set of features.

Parameters:

features – the columns to rank; empty means every continuous one.

Returns:

the new spec.

with_label(label: str) → ExplorerSpec[source]

A copy split by a different label column.

Parameters:

label – the column holding the class of each object.

Returns:

the new spec.

with_statistic(statistic: str) → ExplorerSpec[source]

A copy ranked by a different statistic.

A COPY: a spec is a value, so the one a panel is already showing is never edited underneath it.

Parameters:

statistic – the statistic’s name.

Returns:

the new spec.

class spacr.qt.widgets.feature_rank.FeatureScore[source]

One feature’s separation, every statistic, and the caveats.

Parameters:
  • feature – the column that was scored.

  • statistic – the ranking statistic used, one of STATISTICS.

  • score – the ranking statistic’s value, bigger meaning more separated. For AUC this is |2·AUC − 1|, not the AUC.

  • auc – the directed AUC, so > 0.5 means higher_in scores above the rest.

  • cohen_d – Cohen’s d of the comparison that separated best, positive when the compared class has the larger mean.

  • ks – Kolmogorov–Smirnov distance between the two compared groups, in [0, 1].

  • mutual_info – binned mutual information of that comparison, normalised by the class entropy, in [0, 1].

  • higher_in – the side of the comparison whose values run higher: the compared class when auc is at least 0.5, otherwise against.

  • against – what the best-separated class was compared with: the other class when there are two, "rest" when there are more.

  • n_by_class – number of scored objects in each class level.

  • is_shape_not_shift – the rank test is near a coin flip while the CDFs are far apart — a difference in spread, which the default statistic cannot see. See the module docstring.

describe() → str[source]

The feature, its score, and whichever statistics are finite.

Returns:

a one-line description.

property is_low_n: bool[source]

Whether the smallest class is too small to trust this score.

Returns:

True when sparse but not empty.

property is_shape_not_shift: bool[source]

Whether the classes differ in SHAPE rather than in location.

AUC near 0.5 with a large KS means the two distributions overlap as much as chance would predict while still being different – so a feature that looks useless by AUC alone is separating on spread or modality. Worth marking, because ranking by AUC would bury it.

Returns:

True when the pattern holds and both statistics are finite.

property smallest_class: int[source]

The count of the least-populated class this feature was scored on.

The binding constraint on whether the score means anything: a separation computed against a class of four is four objects’ worth of evidence however many the other class has.

Returns:

the smallest class count, or 0 when there are none.

spacr.qt.widgets.feature_rank.auc_of(a: numpy.ndarray, b: numpy.ndarray) → float[source]

P(a random b scores above a random a), ties counted as half.

Computed from the Mann–Whitney U statistic, so it is exact for ties and costs one sort rather than len(a) × len(b) comparisons.

Parameters:
  • a – 1-D array of the reference group’s values; an empty array gives NaN.

  • b – 1-D array of the compared group’s values; an empty array gives NaN.

spacr.qt.widgets.feature_rank.candidate_features(frame: pandas.DataFrame, label: str | None = None) → Tuple[str, ...][source]

Every numeric column that is a measurement, minus the label.

Not column_kinds() == CONTINUOUS, and the difference matters. classify_columns() calls a numeric column with twelve or fewer distinct values a category, which is the right rule for deciding whether to offer a slider or a tick list — and the wrong one here, because pathogen_count runs 0–8 and is exactly the kind of feature a ranking exists to surface. A separation statistic is perfectly happy on a discrete count.

What is still excluded is what classify_columns calls skip: the identity columns (object_label, prcfo), which identify a row rather than describe it, and would each score an AUC of 0.5 or 1.0 depending on how the table happened to be sorted.

Booleans are included as 0/1. A plate or batch column that is numeric is included too, deliberately: if plateID ranks near the top, the classes are separated by which plate they were on, and that is the most useful thing this screen can tell anyone.

Parameters:

frame – measurement table whose columns are screened.

spacr.qt.widgets.feature_rank.candidate_labels(frame: pandas.DataFrame) → Tuple[str, ...][source]

Columns that could say which class an object is in.

Categorical by column_kinds(), with between two and MAX_CLASSES levels, and not floating point. A class label is text, an integer code or a boolean; a float column with eleven distinct values is a measurement that happens to be coarse, and offering it as “the thing to separate by” is how someone ends up ranking every feature against cell_eccentricity.

Parameters:

frame – measurement table whose columns are screened.

spacr.qt.widgets.feature_rank.cohen_d_of(a: numpy.ndarray, b: numpy.ndarray) → float[source]

(mean(b) − mean(a)) / pooled SD, ddof 1. NaN when either group is smaller than two or the pooled SD is zero.

Parameters:
  • a – 1-D array of the reference group’s values.

  • b – 1-D array of the compared group’s values.

spacr.qt.widgets.feature_rank.distributions(frame: pandas.DataFrame, feature: str, label: str, *, bins: int = 16) → Tuple[numpy.ndarray, Dict[str, numpy.ndarray]][source]

Shared bin edges and per-class counts, for drawing one feature.

The edges are computed over every class together, so the per-class histograms are comparable — the same rule spacr.qt.widgets.graph_spec.scales_for() applies to facets, and for the same reason.

Parameters:
  • frame – measurement table holding both columns.

  • feature – column to histogram; values that are not numeric count as missing, and an all-missing column gives empty edges and no counts.

  • label – class column that splits the rows; ExplorerError is raised when it is absent or has fewer than two classes.

spacr.qt.widgets.feature_rank.ks_of(a: numpy.ndarray, b: numpy.ndarray) → float[source]

The largest gap between the two empirical CDFs, in [0, 1].

Evaluated only at the end of each run of tied values, so a tie cannot produce a gap that neither CDF actually has.

Parameters:
  • a – 1-D array of the reference group’s values; an empty array gives NaN.

  • b – 1-D array of the compared group’s values; an empty array gives NaN.

spacr.qt.widgets.feature_rank.mutual_info_of(a: numpy.ndarray, b: numpy.ndarray, bins: int = 16) → float[source]

Binned mutual information, normalised by the class entropy.

Equal-frequency bins over the pooled values, so the binning adapts to the distribution instead of putting 99% of a skewed feature in one bin.

Parameters:
  • a – 1-D array of the reference group’s values; an empty array gives NaN.

  • b – 1-D array of the compared group’s values; an empty array gives NaN.

Returns:

I(feature; class) / H(class) in [0, 1] — the fraction of the class label this one feature explains. Biased upward at small n; see STATISTIC_FAILURE_MODES.

spacr.qt.widgets.feature_rank.rank_features(frame: pandas.DataFrame, spec: ExplorerSpec | None = None) → ExplorerResult[source]

Score every feature against spec.label and sort by separation.

Parameters:

frame – measurement table, one row per object, holding the class column spec.label and the features to score.

Raises:

ExplorerError – when there is no usable class column, or when none of the features can be scored — each with the reason.