spacr.run_compare

Two runs, side by side: what changed, how many fewer, which hits moved.

A screen is re-run with one setting nudged and the numbers come out different. Nothing in spaCR has ever answered why: the settings that produced a result were not recorded next to it, the object counts were never compared, and the hit list was a CSV you diffed by eye. So the honest answer to “is this better than last week’s run?” was to open two folders and squint.

This module answers it, in three parts, from spacr.artifacts — the registry every output now registers with — rather than from a filesystem scan:

Settings diff

Which parameters moved, grouped by the same headings the settings panel groups them by. spacr.qt.settings_diff does the work.

Count diff

Objects, wells and fields, per plate and overall. A run that produced 12% fewer cells is the single most useful early signal that something changed for the worse, and it is visible long before anyone looks at a p-value.

Hit-list diff

For runs that produced regression results: which hits appeared, which vanished, and which merely moved. Rank churn among a stable hit set matters as much as set membership — a screen whose top ten reshuffle every run is not a screen anyone should publish from.

Not every pair of runs can be compared. Different plates, a module one run never ran, a different spaCR version: comparability() says so in words, and compare_runs() refuses to produce the three tables rather than presenting a misleading one. A version difference is deliberately a warning and not a blocker — it does not stop the comparison, but it has to be on screen, because a Cellpose upgrade between two runs can account for a count change on its own.

Public API

runs_in

Group a project’s registered artifacts into RunRef runs.

comparability

Whether two runs may be compared, and what to say if not.

compare_runs

All three diffs, or the reason there are none.

count_database, diff_counts

The count half on its own, for a caller holding two database paths.

read_hits, diff_hits

The hit-list half on its own, for two result CSVs.

Attributes

Classes

Comparability

Whether two runs may be put side by side, and what to say about it.

CountDiff

The count comparison: overall first, then one block per plate.

CountRow

One count, on both sides.

Finding

One reason two runs are hard, or impossible, to compare.

Hit

One row of a hit list.

HitChange

What happened to one key between two hit lists.

HitDiff

Which hits appeared, which vanished, and which merely moved.

HitList

One run's hits, ranked.

RunComparison

Two runs and the three ways they differ.

RunCounts

What one run actually produced, counted out of its database.

RunRef

One run, assembled from the artifacts it registered.

Functions

comparability(→ Comparability)

Decide whether two runs may be compared, and say why not.

compare_runs(→ RunComparison)

Compare two runs: settings, counts and hit list.

count_database(→ RunCounts)

Count objects, wells and fields in one measurements database.

diff_counts(→ CountDiff)

Line two count tables up, overall first and then per plate.

diff_hits(→ HitDiff)

Compare two ranked hit lists: membership, then rank churn.

read_hits(→ HitList)

Read one hit list off a results CSV.

runs_in(→ List[RunRef])

Group everything a project registered into runs, newest first.

Module Contents

class spacr.run_compare.Comparability[source]

Whether two runs may be put side by side, and what to say about it.

Parameters:
  • comparable – no blocking finding was raised.

  • findings – everything found, blockers first.

  • shared_modules – modules both runs ran.

  • shared_kinds – output kinds both runs produced.

  • shared_plates – plates both runs touched. Empty when neither run names any, which is not the same as “no overlap”.

__bool__() bool[source]

True when the runs may be compared.

summary() str[source]

One line: the verdict and the reasons behind it.

property blockers: Tuple[Finding, Ellipsis][source]

The findings that stop the comparison.

comparable: bool[source]
findings: Tuple[Finding, Ellipsis] = ()[source]
shared_kinds: Tuple[str, Ellipsis] = ()[source]
shared_modules: Tuple[str, Ellipsis] = ()[source]
shared_plates: Tuple[str, Ellipsis] = ()[source]
property version_changed: bool[source]

True when the two runs came out of different spaCR versions.

Surfaced on its own because it explains a count change without any setting having moved.

property warnings: Tuple[Finding, Ellipsis][source]

The findings that do not stop it but have to be on screen.

class spacr.run_compare.CountDiff[source]

The count comparison: overall first, then one block per plate.

Parameters:
  • rows – every metric on both sides, unchanged ones included — a count that did not move is evidence, unlike a setting that did not move.

  • a – the baseline counts.

  • b – the comparison counts.

for_plate(plate: str) Tuple[CountRow, Ellipsis][source]

The rows for one plate.

headline() str[source]

One sentence about the biggest drop, or that nothing moved.

overall() Tuple[CountRow, Ellipsis][source]

The whole-run rows.

worst() CountRow | None[source]

The overall row that dropped the most, in percent.

The one number worth putting in a headline: a run that produced 12% fewer cells has a problem, and the user should not have to find that by reading a table.

a: RunCounts[source]
property available: bool[source]

True when both sides could be counted.

b: RunCounts[source]
property changed: Tuple[CountRow, Ellipsis][source]

Only the rows that moved.

property note: str[source]

Why there is nothing to show, when there is nothing.

property plates: Tuple[str, Ellipsis][source]

Every plate either side reported, sorted.

rows: Tuple[CountRow, Ellipsis] = ()[source]
class spacr.run_compare.CountRow[source]

One count, on both sides.

Parameters:
  • scope"overall" or a plate id.

  • metric"cell", "wells", …

  • a – the baseline count, or None when that side has no such metric at all — which is not the same as zero.

  • b – the comparison count, or None.

a: int | None[source]
b: int | None[source]
property changed: bool[source]

True when the two sides do not hold the same number.

property delta: int | None[source]

b - a, or None when one side is missing.

metric: str[source]
property pct: float | None[source]

Percentage change from a to b.

None when a side is missing, and when the baseline is zero — “up from nothing” has no percentage, and printing inf in a table that a user scans for the biggest number is worse than printing nothing.

scope: str[source]
class spacr.run_compare.Finding[source]

One reason two runs are hard, or impossible, to compare.

Parameters:
  • code – machine-readable, e.g. "version-changed".

  • severityBLOCKING or WARNING.

  • message – one sentence, written for the user.

__str__() str[source]

The message.

property blocking: bool[source]

True when this finding stops the comparison.

code: str[source]
message: str[source]
severity: str[source]
class spacr.run_compare.Hit[source]

One row of a hit list.

Parameters:
  • key – the thing being called — a gene, a gRNA, a feature.

  • rank – 1-based position; see read_hits() for the ordering.

  • score – the effect size it was ranked on, when there was one.

key: str[source]
rank: int[source]
score: float | None = None[source]
class spacr.run_compare.HitChange[source]

What happened to one key between two hit lists.

Parameters:
  • key – the gene / gRNA / feature.

  • status"appeared", "vanished", "moved" or "held".

  • a_rank – rank in the baseline, or None.

  • b_rank – rank in the comparison, or None.

  • a_score – effect size in the baseline, when there was one.

  • b_score – effect size in the comparison.

a_rank: int | None = None[source]
a_score: float | None = None[source]
b_rank: int | None = None[source]
b_score: float | None = None[source]
key: str[source]
property rank_delta: int | None[source]

b_rank - a_rank; negative means it climbed.

status: str[source]
class spacr.run_compare.HitDiff[source]

Which hits appeared, which vanished, and which merely moved.

Parameters:
  • changes – every key from either list, in a stable order: appeared, vanished, then the shared ones by how far they moved.

  • a – the baseline list.

  • b – the comparison list.

headline() str[source]

One sentence: membership first, then churn.

a: HitList[source]
property appeared: Tuple[HitChange, Ellipsis][source]

Keys only the second run called.

property available: bool[source]

True when both lists could be read.

b: HitList[source]
changes: Tuple[HitChange, Ellipsis] = ()[source]
property churn: float[source]

Fraction of the shared hits whose rank moved, 0.0–1.0.

The number that says a screen is unstable even when its hit set is not: identical membership with a reshuffled top ten scores 1.0 here and 0 appeared / 0 vanished up there.

property held: Tuple[HitChange, Ellipsis][source]

Keys in both lists at the same rank.

property identical: bool[source]

True when both membership and order match exactly.

property moved: Tuple[HitChange, Ellipsis][source]

Keys in both lists whose rank changed, biggest move first.

property n_shared: int[source]

Keys both runs called.

property note: str[source]

Why there is nothing to show, when there is nothing.

property vanished: Tuple[HitChange, Ellipsis][source]

Keys only the first run called.

class spacr.run_compare.HitList[source]

One run’s hits, ranked.

Parameters:
  • path – the CSV they were read from.

  • available – whether it could be read.

  • note – why not, when it could not.

  • hits – the rows, best rank first.

  • key_column – the column the keys came from.

  • score_column – the column the ranking used, or "" when the file’s own order was kept.

__bool__() bool[source]

True when there is a list to compare.

__len__() int[source]

Number of hits.

by_key() Dict[str, Hit][source]

key → Hit.

available: bool = False[source]
hits: Tuple[Hit, Ellipsis] = ()[source]
key_column: str = ''[source]
property keys: Tuple[str, Ellipsis][source]

Every key, in rank order.

note: str = ''[source]
path: str = ''[source]
score_column: str = ''[source]
class spacr.run_compare.RunComparison[source]

Two runs and the three ways they differ.

Parameters:
  • a – the baseline run.

  • b – the run being compared to it.

  • comparability – the verdict and its reasons.

  • settings – the settings diff, or None when the runs are not comparable and the caller did not force it.

  • counts – the count diff, same rule.

  • hits – the hit-list diff, same rule.

  • forced – the caller asked for the diffs despite a blocker.

headline() str[source]

The one line to put above the tables.

a: RunRef[source]
b: RunRef[source]
comparability: Comparability[source]
property comparable: bool[source]

Whether the three diffs were produced.

counts: CountDiff | None = None[source]
forced: bool = False[source]
hits: HitDiff | None = None[source]
settings: spacr.qt.settings_diff.SettingsDiff | None = None[source]
class spacr.run_compare.RunCounts[source]

What one run actually produced, counted out of its database.

Parameters:
  • path – the database it was counted from.

  • available – whether it could be read.

  • note – why not, when it could not.

  • overall – metric → count over the whole run.

  • per_plate – plate → metric → count.

__bool__() bool[source]

True when there is something to compare.

available: bool = False[source]
note: str = ''[source]
overall: Dict[str, int][source]
path: str = ''[source]
per_plate: Dict[str, Dict[str, int]][source]
property plates: Tuple[str, Ellipsis][source]

The plates found in the database, sorted.

class spacr.run_compare.RunRef[source]

One run, assembled from the artifacts it registered.

A run is what a spacr.artifacts.Artifact.run_id groups. An artifact registered without one is its own single-output run rather than being dropped: an output nobody stamped is still an output somebody may want to compare against.

Parameters:
  • run_id – the run id, or "artifact:<id>" for an unstamped one.

  • project – the project root the outputs belong to.

  • modules – producing modules, sorted.

  • kinds – output kinds, sorted.

  • plates – plates the run touched, sorted. Derived from its settings; the count diff finds the real ones in the database.

  • spacr_version – the version that produced the outputs. Empty when unrecorded; "mixed" when the run’s artifacts disagree.

  • settings – the material settings, from the newest artifact that carries any.

  • settings_hash – that run’s settings digest.

  • created_ns – the newest artifact’s registration time.

  • created_utc – the same instant, ISO-8601.

  • status – the worst status any of its artifacts recorded.

  • artifacts – the artifacts themselves, newest first.

artifact_of(kind: str) Any | None[source]

The newest artifact of kind this run produced, or None.

Parameters:

kind – a spacr.ports kind, e.g. "measurements-db".

artifacts: Tuple[Any, Ellipsis] = ()[source]
created_ns: int = 0[source]
created_utc: str = ''[source]
kinds: Tuple[str, Ellipsis] = ()[source]
property label: str[source]

when, what and which version.

Type:

A one-line name for a dropdown

modules: Tuple[str, Ellipsis] = ()[source]
plates: Tuple[str, Ellipsis] = ()[source]
project: str = ''[source]
run_id: str[source]
settings: Dict[str, Any][source]
settings_hash: str = ''[source]
spacr_version: str = ''[source]
status: str = 'complete'[source]
spacr.run_compare.comparability(a: RunRef, b: RunRef, *, a_plates: Sequence[str] | None = None, b_plates: Sequence[str] | None = None) Comparability[source]

Decide whether two runs may be compared, and say why not.

Blocking, because diffing past them produces a table that is worse than no table:

  • no shared module — the runs did different things, so there is no output of the same kind to line up.

  • no shared output kind — the modules overlap but what they actually produced does not.

  • different plates — both runs name plates and the sets are disjoint. Counting objects across two different experiments and calling the difference a regression is exactly the misleading table this check exists to prevent.

Warnings, because they change what the numbers mean but the comparison is still worth seeing:

  • version-changed — different spaCR versions. Called out on its own: a segmentation change between versions moves object counts with no setting having moved, so a count delta must never be read without it.

  • different project, partial plate overlap, modules only one run ran, a run that did not finish.

Parameters:
  • a – the baseline run.

  • b – the run being compared to it.

  • a_plates – the plates the baseline really touched, when the caller knows better than RunRef.plates does — which it does whenever it has counted the database.

  • b_plates – the same for the compared run.

Returns:

a Comparability; bool() of it is the verdict.

spacr.run_compare.compare_runs(a: RunRef, b: RunRef, *, include_same_settings: bool = False, force: bool = False) RunComparison[source]

Compare two runs: settings, counts and hit list.

Refuses by default when comparability() raises a blocker. That is the point of the check — two runs of different plates can be subtracted from one another, and the resulting table looks exactly like a regression report, which is why it must not be drawn without the user having said they know what they are doing.

Parameters:
  • a – the baseline run, from runs_in().

  • b – the run being compared to it.

  • include_same_settings – carry the settings both runs agree on, for the “show everything” view.

  • force – produce the diffs even when the runs are not comparable.

Returns:

a RunComparison. When it refused, RunComparison.comparable is False and the diffs are None; comparability.blockers says why.

spacr.run_compare.count_database(path: str | os.PathLike | None) RunCounts[source]

Count objects, wells and fields in one measurements database.

Deliberately tolerant: a database missing pathogen is a run that segmented no pathogens, not an error, and a run whose database has been deleted must report that rather than raise — the comparison screen has to say why half the table is empty.

Parameters:

path – the measurements.db to count.

Returns:

a RunCounts; available is False with a note when there was nothing to count.

spacr.run_compare.diff_counts(a: RunCounts, b: RunCounts) CountDiff[source]

Line two count tables up, overall first and then per plate.

Parameters:
  • a – the baseline counts.

  • b – the comparison counts.

Returns:

a CountDiff. Rows are emitted for every metric either side reported, so a table that vanished between runs shows as None on one side rather than being absent.

spacr.run_compare.diff_hits(a: HitList, b: HitList) HitDiff[source]

Compare two ranked hit lists: membership, then rank churn.

Parameters:
  • a – the baseline list.

  • b – the comparison list.

Returns:

a HitDiff.

spacr.run_compare.read_hits(path: str | os.PathLike | None, *, key_column: str = '', score_column: str = '', limit: int | None = None) HitList[source]

Read one hit list off a results CSV.

The rank is defined here, once, so both sides of a diff are ranked the same way: rows are sorted by descending absolute score, ties broken by key. Absolute, because a screen’s strongest hit is its largest effect in either direction and ranking a strong protective hit below a weak sensitising one would report rank churn that is an artefact of the sort. When no score column can be found the file’s own row order is kept and HitList.score_column is empty, so a caller can tell a real ranking from a preserved one.

Parameters:
  • path – a results*.csv from the regression module, or any CSV with a name column.

  • key_column – force the key column instead of detecting it.

  • score_column – force the score column. Pass "-" to rank by file order even when a score column exists.

  • limit – keep only the top limit rows after ranking.

Returns:

a HitList; available is False with a note when there was nothing to read.

spacr.run_compare.runs_in(registry: Any, project: str | os.PathLike | None = None, *, limit: int | None = None) List[RunRef][source]

Group everything a project registered into runs, newest first.

This is how the comparison screen fills its two dropdowns. It reads spacr.artifacts.Registry.by_project() rather than walking the filesystem, so a run whose outputs have since been deleted still appears — with the settings that produced them — instead of silently vanishing from the list of things you can compare against.

Parameters:
  • registry – a spacr.artifacts.Registry.

  • project – the project root; None means the registry’s own, "" means every project in a shared registry file.

  • limit – keep only the newest limit runs.

Returns:

RunRef objects, newest first.

spacr.run_compare.COUNT_METRICS: Tuple[str, Ellipsis] = ('plates', 'wells', 'fields')[source]
spacr.run_compare.OBJECT_TABLES: Tuple[str, Ellipsis] = ('cell', 'nucleus', 'pathogen', 'cytoplasm', 'png_list')[source]