spacr.plate_measurements

One merged measurements frame from the plate rows of the input table.

A row of the regression input table is one plate: its score CSV, its count CSV and now its measurements database. This module is the headless half of that feature – given {plate: database path}, the tables the user ticked and the anchor they chose, it returns the merged frame.

This module adds no merge logic of its own. spaCR already has four places that join measurement tables, and one of them (io._read_and_join_tables) aggregates every numeric column with mean, which turns four pathogens’ total area into an average area and a minimum into a mean of minima. This module composes existing functions instead:

There is no sum() or mean() of a measurement column written in this file, and there must never be one.

Why the two existing functions do not compose by themselves

read_merged is many databases, one table. merge_tables is one database, many tables – and it takes a PATH, so it cannot be handed a frame that has already been stacked across databases. This feature needs both, so the composition is: read each chosen table across every database, then roll the children onto the anchor.

The roll-up keys are the identity columns plus screenID, source_database and the child’s anchor column. Without screenID, two screens that share plate1 (which describe_merge() permits) would collapse into one parent, which is the pooling multi_database prevents.

What a caller must show the user

A merge that silently changed how a measurement was combined produces a number that is wrong and looks fine, so PlateMerge carries what the panel has to say: the anchor and the row count, the rows each source contributed, the measurements columns='common' dropped, the plate ids shared across screens – and every column that fell through to spacr.merge_tables.DEFAULT_AGGREGATION because no rule matched it. That last set is recomputed by re-walking AGGREGATION_RULES (default_aggregated_columns()) rather than listed anywhere, so it cannot drift out of step with the rules.

Classes

PlateDatabase

One plate row of the input table and the database attached to it.

PlateMerge

The merged frame and the Measurements tab's audit information.

TableMerge

What one chosen table contributed to the merge, and how.

Functions

ambiguous_identifiers(→ Dict[str, Dict[str, Any]])

Return the text identifiers that vary within their roll-up group.

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

Return the object tables present in every attached database.

classify_default_columns(→ Dict[str, Tuple[str, ...]])

Split the columns no rule names by the aggregation they will receive.

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

Return the columns in plan that got the default because no rule matched.

describe_identifier_refusal(→ str)

One line saying why column was left out, with an example.

merge_plate_databases(, *, anchor, policy, screens, ...)

Merge every attached plate database into one frame, one row per anchor.

missing_databases(→ Tuple[PlateDatabase, ...])

Attached databases that are not on disk, named before the run starts.

plate_databases(→ Tuple[PlateDatabase, ...])

Return the plate rows that have a database, in input-table order.

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

Return the plates that have no database attached.

Module Contents

class spacr.plate_measurements.PlateDatabase[source]

One plate row of the input table and the database attached to it.

Parameters:
  • plate – input-table plate label used to map screen metadata and identify missing or duplicate attachments in diagnostics. A blank label normalised from an input row becomes "row N".

  • path – filesystem path to the plate’s measurements database. An empty value means no database is attached; a nonempty path is checked for existence before table discovery and merging.

property exists: bool[source]

Whether the file is still where the input table says it is.

Checked before the run rather than during it: the design asks that a database that has been moved is named up front, not four minutes into a regression.

class spacr.plate_measurements.PlateMerge[source]

The merged frame and the Measurements tab’s audit information.

Parameters:
  • frame – final measurements frame, with one row per surviving anchor object.

  • anchor – normalized object-table name whose rows define the output cardinality.

  • attachments – validated plate/database attachments included in the merge, in input order.

  • tables – per-table audit records in merge order, beginning with the anchor and including any child table skipped because it could not be linked.

describe() → str[source]

The disclosure, as lines – what this merge did and what it cost.

property default_aggregation_columns: Tuple[str, ...][source]

Merged columns whose aggregation is the default, no rule matching.

spacr.merge_tables.DEFAULT_AGGREGATION is mean. Columns that no rule matched are listed because a mean may not be the appropriate aggregation for them.

property dropped_columns: Tuple[str, ...][source]

Measurements columns='common' left out, as merged names.

A dropped measurement is a measurement the user came to compare, so it is reported rather than merely defaulted.

property rows: int[source]

How many anchor objects survived the merge.

property rows_per_source: Dict[str, int][source]

Anchor rows each database contributed to the final frame.

Less than rows_read_per_source wherever an inner join removed objects – a nucleus-less cell, or a pathogen-less one when keep_uninfected=False. The pair is what lets a panel say how many rows were dropped instead of only how many are left.

property rows_read_per_source: Dict[str, int][source]

Anchor rows each database held, before any join removed rows.

property shared_plates_across_screens: Dict[str, Tuple[str, ...]][source]

Plate ids that appear in more than one screen.

Two screens sharing a guide library both have plate1; with screenID in the frame those are two identities, so this is not a collision. They are reported so that a user can see that two screens were merged.

property sources: Tuple[str, ...][source]

The database labels, as spacr.multi_database named them.

class spacr.plate_measurements.TableMerge[source]

What one chosen table contributed to the merge, and how.

One of these per table the user ticked, including the anchor. It is the record a panel reads to answer “what happened to my numbers”: which aggregation each column got, whether the table was rolled up or joined directly, and which join mode the merge policy selected.

Parameters:
  • table – Chosen source object-table name, including the anchor table.

  • plan – Multi-database read plan used to read this table.

  • rows – Number of source rows read across the attached databases before joining or child roll-up.

  • keys – Source identity, join, or grouping columns retained without a table prefix.

  • how – Child-table join mode returned by spacr.merge_tables.MergePolicy.how_for(); empty for the anchor or a skipped table.

  • rolled_up – Whether this child table’s rows were aggregated onto the anchor; false for the anchor, a directly joined one-row-per-cell table, or a skipped table.

  • aggregations – Source-column-to-aggregation mapping actually applied during child roll-up; empty for the anchor, a direct join, or a skipped table.

  • default_columns – Source columns in aggregations that used the default because no named aggregation rule matched and no override applied.

  • dropped – Source columns absent from at least one attached database and therefore omitted by the common-column read.

  • note – Explanation when the table was skipped and contributed no merged columns; empty otherwise.

merged_column(column: str) → str[source]

The name column carries in the merged frame.

Parameters:

column – source-table column name to express in merged-frame form.

The same rule spacr.merge_tables.roll_up() applies: a join key keeps its name, a column that already starts with the table’s name is left alone – nucleus_area must not become nucleus_nucleus_area – and everything else is prefixed with the table it measures.

spacr.plate_measurements.ambiguous_identifiers(child: pandas.DataFrame, keys: Sequence[str], *, plan: Mapping[str, str] | None = None, overrides: Mapping[str, str] | None = None, examples: int = 1) → Dict[str, Dict[str, Any]][source]

Return the text identifiers that vary within their roll-up group.

A text identifier that takes first must have one value per group. When it has more, the column is reported rather than resolved silently. A cell with three pathogens has one path_name if all three came from the same image; if it has three, first would name one of them and the merged row would claim a provenance the data does not support.

Only text columns are examined. object_label also takes first, by the identity rule, and is expected to differ across the children: spacr.merge_tables.AGGREGATION_RULES carries the label only so a row can be traced back. A numeric label that varies is the normal case; a text identifier that varies is ambiguous.

Parameters:
  • child – the many-per-parent table, before the roll-up.

  • keys – the group keys the roll-up will use.

  • plan – the aggregation plan, from spacr.merge_tables.aggregation_plan(). Recomputed when omitted.

  • overrides – the caller’s explicit choices. A column set here is not examined.

  • examples – how many offending group keys to record per column.

Returns:

{column: {'groups': n, 'examples': [(key, [values])]}} – empty when every identifier is constant, which is the normal case.

spacr.plate_measurements.available_tables(attachments: Any) → Tuple[str, ...][source]

Return the object tables present in every attached database.

Parameters:

attachments – input-table attachment rows whose existing databases define the table intersection.

The intersection is returned, not the union, because spacr.multi_database.describe_merge() raises a bare sqlite3.OperationalError: no such table when one database lacks the chosen table.

Returns:

the tables in spacr.merge_tables.OBJECT_TABLES order, so the picker’s order is the registry’s order rather than sqlite’s, and so a sixth object kind reaches this list by being declared once. png_list is not offered: it holds one row per crop rather than per object, and a crop is not a measurement to aggregate.

spacr.plate_measurements.classify_default_columns(columns: Sequence[str], kinds: Mapping[str, str] | None = None, *, overrides: Mapping[str, str] | None = None) → Dict[str, Tuple[str, ...]][source]

Split the columns no rule names by the aggregation they will receive.

Parameters:
  • columns – the column names about to be aggregated.

  • kinds – {column: 'numeric' | 'text' | 'unknown'}, as spacr.multi_database.column_kinds() returns it. A column absent from the mapping is 'unknown'.

  • overrides – the caller’s explicit choices. A column they set is not a fall-through and appears in neither bucket – they chose it.

Returns:

{'mean': (...), 'identifier': (...), 'unknown': (...)}:

mean

numeric, no rule matched, so spacr.merge_tables.DEFAULT_AGGREGATION is what it takes.

identifier

text. It takes spacr.merge_tables.TEXT_AGGREGATION, not a mean, and whether it is carried or refused depends on the data – see ambiguous_identifiers(), which reads the rows.

unknown

the database declared no type, so neither aggregation can be stated in advance. Listed separately so that a column of unknown type is not reported as numeric or text.

spacr.plate_measurements.default_aggregated_columns(plan: Mapping[str, str], *, overrides: Mapping[str, str] | None = None) → Tuple[str, ...][source]

Return the columns in plan that got the default because no rule matched.

Recomputed by re-walking spacr.merge_tables.AGGREGATION_RULES, so this cannot drift out of step with them: a rule added for a measurement tomorrow removes that measurement from this list the same day, and a rule deleted puts it back.

A column the caller overrode is not reported however it was set – they chose it. Nor is a text column, which takes spacr.merge_tables.TEXT_AGGREGATION because text does not add up, not because nobody thought about it.

Parameters:
spacr.plate_measurements.describe_identifier_refusal(table: str, column: str, detail: Mapping[str, Any]) → str[source]

One line saying why column was left out, with an example.

Parameters:
  • table – child table whose identifier could not be carried safely.

  • column – varying text-identifier column that was omitted.

  • detail – ambiguity record from ambiguous_identifiers(), including the affected group count and optional examples.

Written here rather than in the panel so the headless merge and the Qt one say the same sentence.

spacr.plate_measurements.merge_plate_databases(attachments: Any, tables: Sequence[str] = (), *, anchor: str | None = None, policy: spacr.merge_tables.MergePolicy | None = None, screens: Any = None, columns: str = 'common', report: Callable[[str], None] | None = None, on_ambiguous_identifier: str = 'refuse') → PlateMerge[source]

Merge every attached plate database into one frame, one row per anchor.

Calls spacr.multi_database.read_merged() once per chosen table and spacr.merge_tables.roll_up() once per child table. It performs no aggregation of its own, and the join type comes from spacr.merge_tables.MergePolicy.how_for() per table rather than from a blanket how.

Parameters:
  • attachments – any shape plate_databases() accepts. Plates with no database are skipped, not fatal.

  • tables – the object tables to join. The anchor is added if absent.

  • anchor – the object type one row of the result represents. cell by default (spacr.merge_tables.DEFAULT_PRIMARY), overriding policy.primary when both are given.

  • policy – the aggregation and join policy – na, overrides, consolidate_on_cell, keep_uninfected.

  • screens – optional screen label per plate: a mapping keyed by plate, or a sequence parallel to the attached databases.

  • columns – 'common' or 'union', passed to read_merged.

  • report – called with one line per thing the merge cost, the table named.

  • on_ambiguous_identifier – 'refuse' (default) leaves out a text identifier that differs within a roll-up group and names it on report; 'first' keeps the first value without a report. See ambiguous_identifiers(). The Measurements tab applies the same default, so the headless and Qt merges of one screen agree.

Returns:

the PlateMerge.

Raises:

MergeRefused – nothing attached, a database that has moved, one database attached to two plates, an anchor that is not one row per cell, a table missing from some database, or – from read_merged itself – a plate id repeated inside one screen.

spacr.plate_measurements.missing_databases(attachments: Any) → Tuple[PlateDatabase, ...][source]

Attached databases that are not on disk, named before the run starts.

Parameters:

attachments – input-table attachment rows in any shape accepted by plate_databases().

spacr.plate_measurements.plate_databases(attachments: Any) → Tuple[PlateDatabase, ...][source]

Return the plate rows that have a database, in input-table order.

Parameters:

attachments – {plate: path}, a sequence of PlateDatabase, a sequence of (plate, path) pairs, or the input table’s own rows (mappings carrying plate and database).

Returns:

one PlateDatabase per attached row. Rows with no database are left out rather than carried as blanks – see unattached_plates(), which is where they are named.

spacr.plate_measurements.unattached_plates(attachments: Any) → Tuple[str, ...][source]

Return the plates that have no database attached.

Parameters:

attachments – input-table attachment rows in any shape accepted by plate_databases().

The regression runs on scores and counts; the database is needed only for the Measurements tab. A plate without one is unavailable there but does not fail the run, and it is listed so that the omission is visible.