spacr.plate_qc¶
Workflow inputs and outputs¶
Plate Viewer¶
Aggregate the selected measurement by well and inspect plate patterns and controls.
Open: Graph Builder → Plate Viewer.
Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.
Inputs
Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route:
cell,nucleus,pathogen,cytoplasm. Relevant columns, depending on the route:plateID,rowID,columnID,fieldID.Experimental layout — Exported plate/condition/control/replicate map. Keep its plate and well identifiers consistent with the acquired data.
Outputs
Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.
Plate-level QC: is that hit biology, or is it the edge of the plate?
The outer ring of a microtitre plate evaporates faster, sits at a different temperature and is handled differently from the interior. A well in row A or column 24 that reads three sigma from the plate mean is therefore much more likely to be an artefact of where it sits than of what is in it — and the usual way that gets discovered is a failed follow-up experiment six weeks later.
This module answers the question up front. It takes the same long-format
per-object frame the rest of spaCR passes around (a prc identifier
plus feature columns), collapses it to one value per well, and then asks
three separate questions of the resulting grid:
detect_edge_effectDoes the outer ring read differently from the interior — and by how much? Ring-by-ring, not just outermost-vs-rest.
row_column_trendsWhat does each row and each column average, and is there a monotonic drift across the plate?
plate_layout/layout_matrixThe well grid itself, tidy or pivoted, ready to draw.
format_edge_reportAll of it as text.
Statistics this module refuses to get wrong¶
Rank tests, not t-tests. Per-well aggregates of object measurements are routinely skewed and heavy-tailed — a well with three enormous cells is not a normal deviate. The ring comparison is a two-sided Mann-Whitney U; the gradient test is Spearman. Neither assumes a shape.
Effect size leads, p-value follows. With 384 wells, almost any
systematic drift clears p < 0.05; that fact carries no information. Every
comparison therefore reports the median difference, the difference as a
percentage of the interior median, and Cliff’s delta — a rank-based
standardised effect in [-1, 1] read straight off the same U statistic
(δ = 2U/n₁n₂ − 1). Detection requires both a small p and an
effect size above DEFAULT_MIN_EFFECT, so “significant but 0.4 %”
is reported as what it is: not an edge effect.
Evaporation is not a step function. The profile walks inward ring by ring (outermost = ring 0), comparing each against the plate core, so a gradient that reaches two wells in is visible instead of being averaged into “the interior”.
A gradient is not an edge effect. An incubator or plate-reader
gradient runs across the plate; evaporation runs around it. A linear
column gradient leaves the outer ring with the same median as the
interior (it collects both the high and the low end), so the ring test
stays quiet while Spearman on the column index fires. Both are computed,
both are reported, and EdgeEffectReport.dominant names which one
better explains the plate.
Wells with too few objects are noise. min_count drops them — and
the number dropped is reported everywhere, because a heatmap quietly
missing a third of its wells looks exactly like data.
The plate format is inferred, never assumed. 6/12/24/48/96/384/1536 are recognised from the observed row and column labels, and the ring geometry uses the nominal grid of the inferred format. A screen that only used rows A-H of a 384 plate does not get row H promoted to “edge” just because nothing was pipetted below it.
Degenerate input explains itself. An empty frame, a single well, or a
plate where every well reads the same comes back as a report that says
so, with None where a statistic is genuinely undefined. Nothing here
returns NaN and nothing here raises on empty.
Nothing in this module imports torch, cellpose or any GPU stack — numpy, pandas, scipy and the standard library only — so the Qt screen can draw a plate without waking a multi-second import chain.
Classes¶
Everything |
|
A monotonic drift along one axis of the plate. |
|
One concentric ring of the plate, compared against its core. |
Functions¶
|
Return |
|
Test a plate for an edge artefact and for a row/column gradient. |
|
Render an |
|
Infer the plate format containing a |
|
Pivot a layout onto the full nominal plate grid. |
|
Read the well identifiers plus |
|
Return the columns of |
|
Return the 1-based column index of |
|
Return the 1-based row index of |
|
Collapse a long per-object frame into one row per well. |
|
Return the plate IDs present in |
|
Per-row and per-column summaries, plus the drift across each axis. |
|
Return the letter label of a 1-based row index: |
|
Return the column names of |
|
Return the user tables + views of |
|
Return the canonical well name, e.g. |
|
Write the well grid to |
Module Contents¶
- class spacr.plate_qc.EdgeEffectReport[source]¶
Everything
detect_edge_effect()worked out, in one object.The headline numbers are
pct_differenceandcliffs_delta— “the outer ring reads 31 % higher, δ = 0.78” — withp_valueas supporting evidence rather than the verdict.- Parameters:
plate – plate identifier analysed, or None when none was selected.
value_col – measurement aggregated per well, or None for counts.
grouping – per-well aggregation used to build the layout.
ok – False when the plate could not be tested at all (empty frame, one well, no interior);
notessays why.plate_format – nominal well count used to choose the plate grid, or
Nonewhen geometry is non-standard or unavailable.n_rows – number of rows in the grid used to classify plate rings.
n_cols – number of columns in the grid used to classify plate rings.
n_wells – wells retaining usable values after count and NaN filtering.
n_edge_wells – usable wells assigned to the outermost ring.
n_interior_wells – usable wells inside the outermost ring.
min_count – minimum number of objects a well needed to remain in the analysis.
edge_detected – the outer ring differs from the interior by more than
min_effect, at better thanalpha.p_value – two-sided Mann-Whitney U p-value comparing outer-ring and interior wells, or
Nonewhen the comparison is unavailable.cliffs_delta – signed rank effect size for that comparison; positive values mean the outer ring reads higher.
edge_median – median usable value on the outermost ring.
interior_median – median usable value inside the outermost ring.
median_difference –
edge_median - interior_median.pct_difference – median difference as a percentage of the interior median, or
Nonewhen the baseline is zero or unavailable.gradient_detected – at least one axis shows a monotonic drift.
dominant –
'edge','gradient', or'none'— which pattern better explains the plate.alpha – p-value threshold used together with
min_effect.min_effect – minimum absolute Cliff’s delta required to flag an edge.
min_gradient_rho – minimum absolute Spearman correlation required to flag a row or column gradient.
rings – ring-by-ring profile, outermost first.
gradients – one
GradientStatsper axis.n_dropped_min_count – wells removed by
min_count. A heatmap missing a third of its wells looks like data; this is the number that says it isn’t.notes – filtering, geometry, and degenerate-input explanations safe to present to the user.
- gradient(axis: str) GradientStats | None[source]¶
Return the
GradientStatsfor'row'or'column'.- Parameters:
axis – gradient axis to retrieve.
- ring(index: int) RingStats | None[source]¶
Return the
RingStatsfor ringindex, if computed.- Parameters:
index – zero-based ring depth, outermost first, to retrieve.
- class spacr.plate_qc.GradientStats[source]¶
A monotonic drift along one axis of the plate.
- Parameters:
axis – plate axis tested, either
"row"or"column".spearman_rho – Spearman rank correlation between each usable well’s value and its row or column index, or
Nonewhen fewer than three wells are available or the correlation is undefined.p_value – two-sided p-value for
spearman_rho, orNonewhen the correlation is unavailable.first_label – label of the lowest-index row or column present in the usable wells.
last_label – label of the highest-index row or column present in the usable wells.
delta_first_last – median value at the last axis index minus the median at the first, or
Nonewhen either median is unavailable.pct_first_last –
delta_first_lastas a percentage of the absolute first-index median, orNonewhen the difference or baseline is unavailable or zero.detected – whether
p_value < alphaandabs(spearman_rho) >= min_gradient_rhofor the thresholds used to produce the profile.
- class spacr.plate_qc.RingStats[source]¶
One concentric ring of the plate, compared against its core.
- Parameters:
ring – zero-based distance from the plate edge: zero is the outermost ring, one is one well inward, and so on.
n_wells – number of usable wells in this ring after minimum-count and missing-value filtering.
median – finite median of the per-well values in this ring, or
Nonewhen it is unavailable.mean – finite mean of the per-well values in this ring, or
Nonewhen it is unavailable.delta – ring median minus the selected core median, or
Nonewhen either median is unavailable.pct –
deltaas a percentage of the absolute core median, orNonewhen the difference or baseline is unavailable or zero.p_value – two-sided Mann-Whitney U p-value comparing this ring with the selected core, or
Nonewhen the comparison is unavailable.cliffs_delta – signed Cliff’s delta for the same comparison, in
[-1, 1]; positive values mean this ring reads higher than the core, orNonewhen unavailable.
- spacr.plate_qc.colour_limits(layout: pandas.DataFrame, min_max: Any = 'allq') Tuple[float, float][source]¶
Return
(vmin, vmax)for a heatmap oflayout.Same specification language as
spacr.plot.generate_plate_heatmap():'all'for the full range,'allq'for the 2nd-98th percentile (which stops one dead well from flattening the whole plate to a single colour), or an explicit[vmin, vmax]— floats in[0, 1]are read as quantiles, anything else as absolute limits.Unlike the original, quantiles are computed over the present wells only; the original quantiles a grid where every absent well has already been turned into a zero.
- Parameters:
layout – frame from
plate_layout().min_max – colour-scale specification.
- Returns:
(vmin, vmax), never degenerate.
- spacr.plate_qc.detect_edge_effect(df: pandas.DataFrame, value_col: str | None = None, plate: str | None = None, grouping: str = 'mean', min_count: int = 0, alpha: float = DEFAULT_ALPHA, min_effect: float = DEFAULT_MIN_EFFECT, min_gradient_rho: float = DEFAULT_MIN_GRADIENT_RHO, core_depth: int = DEFAULT_CORE_DEPTH, max_rings: int = DEFAULT_MAX_RINGS, plate_format: int | None = None) EdgeEffectReport[source]¶
Test a plate for an edge artefact and for a row/column gradient.
The outer ring is compared against everything inside it with a two-sided Mann-Whitney U — rank-based, because per-well aggregates of object measurements are skewed often enough that a t-test would be testing its own assumptions rather than the plate. The accompanying Cliff’s delta comes off the same U statistic, so effect size and p-value cannot disagree about which comparison was made.
Detection deliberately needs both
p < alphaand|cliffs_delta| >= min_effect. On 384 wells a p-value on its own flags drifts far below the level at which anyone would act.The ring-by-ring profile then walks inward, comparing each ring against the plate core (depth >=
core_depth), because evaporation reaches past the outermost well and a single outer-vs-rest test would average that away.Row and column gradients are tested separately with Spearman on the well index. This is what keeps a plate-reader or incubator gradient from being reported as an edge effect: a linear gradient across the plate leaves the outer ring straddling both extremes, so its median matches the interior and the ring test correctly stays quiet.
- Parameters:
df – long per-object frame, or a layout from
plate_layout().value_col – measurement to aggregate per well.
plate – plate to test;
Nonetakes the first present.grouping – per-well aggregation, one of
GROUPINGS.min_count – drop wells with fewer than this many objects.
alpha – significance threshold. Default
DEFAULT_ALPHA.min_effect – minimum
|Cliff's delta|to call an edge effect. DefaultDEFAULT_MIN_EFFECT.min_gradient_rho – minimum
|Spearman rho|to call a gradient. DefaultDEFAULT_MIN_GRADIENT_RHO.core_depth – ring depth defining “the core” for the profile.
max_rings – how many rings inward to profile.
plate_format – force 96 / 384 / 1536 instead of inferring.
- Returns:
an
EdgeEffectReport. Degenerate input comes back withok=Falseand an explanation innotes— never a NaN and never an exception.
Example
from spacr.plate_qc import detect_edge_effect, format_edge_report report = detect_edge_effect(df, 'cell_area', min_count=20) print(format_edge_report(report))
See also
row_column_trends()— the per-row/per-column detail behind the gradient statistics.
- spacr.plate_qc.format_edge_report(report: EdgeEffectReport) str[source]¶
Render an
EdgeEffectReportas text, effect size first.The layout is deliberate: the verdict and the percentage difference come before the p-value, because on a 384-well plate the p-value is the least informative number in the report.
- Parameters:
report – the report to render.
- Returns:
a multi-line string, safe to drop into a label or a log.
- spacr.plate_qc.infer_plate_format(n_rows: int, n_cols: int) Tuple[int | None, int, int][source]¶
Infer the plate format containing a
n_rows×n_colsextent.Picks the smallest standard format whose nominal grid contains every observed label, so an assay run only in rows A-H of a 384 plate is still recognised as a 96 grid — and one run in rows A-H across 24 columns is recognised as a 384 plate whose lower half is empty, rather than having row H mistaken for the bottom edge.
- Parameters:
n_rows – largest observed row index.
n_cols – largest observed column index.
- Returns:
(n_wells_or_None, nominal_rows, nominal_cols). The format isNonefor a grid bigger than 1536, in which case the observed extent is returned unchanged.
- spacr.plate_qc.layout_matrix(layout: pandas.DataFrame, column: str = 'value') pandas.DataFrame[source]¶
Pivot a layout onto the full nominal plate grid.
The result always spans every row and column of the inferred format, with
NaN— never0— where a well is absent, so a heatmap cannot pass an empty well off as a measurement.- Parameters:
layout – frame from
plate_layout().column – which layout column to pivot (
'value'or'n').
- Returns:
DataFrame indexed by row letter (
A,B, …) with integer column labels1..n_cols.
- spacr.plate_qc.load_plate_frame(db_path: str, table: str, value_col: str, limit: int | None = None) pandas.DataFrame[source]¶
Read the well identifiers plus
value_colfromtable.Only the columns needed to place and colour a well are selected — a spaCR feature table can be 500 columns wide and half a million rows long, and
SELECT *on one is a minute of nothing happening.- Parameters:
db_path – path to
measurements.db(opened read-only).table – table to read.
value_col – the measurement column to plot.
limit – optional
LIMITfor previews.
- Returns:
a long DataFrame, one row per object.
- Raises:
ValueError – for an unknown table or column, or when the table carries no usable well identifier at all.
- spacr.plate_qc.numeric_columns(db_path: str, table: str, sample: int = 500) List[str][source]¶
Return the columns of
tablethat hold plottable numbers.Declared affinity is the first filter, but spaCR writes plenty of feature tables through
pandas.to_sqlwhere everything lands asREAL/INTEGERanyway — includingobject_labeland the row index. So a column also has to actually contain numbers in the firstsamplerows, and the well-identifier columns are excluded by name.- Parameters:
db_path – path to
measurements.db.table – table to inspect.
sample – rows sampled to confirm the values are numeric.
- Returns:
candidate column names, in table order.
- spacr.plate_qc.parse_column_label(label: Any) int | None[source]¶
Return the 1-based column index of
label, orNone.Understands
'c12','C12','column12','12'and12.- Parameters:
label – column identifier.
- Returns:
1-based column index, or
Nonewhen unparseable.
- spacr.plate_qc.parse_row_label(label: Any) int | None[source]¶
Return the 1-based row index of
label, orNone.Understands every row spelling spaCR and plate readers produce:
'r3','R3','row3',3,'C'(letter rows) and'AA'(1536 plates go past Z).- Parameters:
label – row identifier of any of the above shapes.
- Returns:
1-based row index, or
Nonewhen unparseable.
- spacr.plate_qc.plate_layout(df: pandas.DataFrame, value_col: str | None = None, plate: str | None = None, grouping: str = 'mean', min_count: int = 0, plate_format: int | None = None) pandas.DataFrame[source]¶
Collapse a long per-object frame into one row per well.
This is the grid everything else in the module works on: the heatmap draws it,
detect_edge_effect()tests it, andwrite_layout_csv()exports it.Aggregation and the well-identifier handling follow
spacr.plot.generate_plate_heatmap()so the two agree well for well, with two deliberate departures, each of which fixes a way the plotter misleads:Missing wells stay missing.
generate_plate_heatmapends in.fillna(0), which paints an unpipetted or filtered-out well as a real measurement of zero. Here an absent well is absent.The grid is nominal, not observed. The plotter’s axes span the wells that are present; these span the whole inferred format, so a row nobody pipetted still holds its place on the plate and the ring geometry
detect_edge_effect()needs stays intact.
The third departure has since been closed from the other side. The plotter used to pin rows to
r1..r16and columns toc1..c27, silently dropping every well of a 1536 plate past row P; it now reads its axes off the data throughparse_row_label()/parse_column_label()— these functions — so'B'and'AA'are row labels in both places, and neither module owns a private letter walk.- Parameters:
df – long frame with a
prcidentifier (orrowID+columnID, or awellcolumn) andvalue_col. A layout produced by a previous call is passed straight back.value_col – measurement to aggregate. Ignored (and optional) when
grouping='count'.plate – plate to keep.
Nonetakes the first plate present and records the choice in the layout’snotes.grouping – one of
GROUPINGS.min_count – drop wells with fewer than this many objects. The number dropped is recorded in
layout.attrs['n_dropped_min_count'].plate_format – force a format (96 / 384 / 1536) instead of inferring one.
- Returns:
tidy DataFrame with
LAYOUT_COLUMNS, carrying the inferred geometry and drop counts in.attrs.- Raises:
ValueError – for an unknown
grouping, a missingvalue_col, or a frame with no usable well identifier.
Example
from spacr.plate_qc import plate_layout, layout_matrix wells = plate_layout(df, 'cell_area', grouping='median', min_count=20) grid = layout_matrix(wells) # 16 x 24, NaN where empty
- spacr.plate_qc.plates_in(df: pandas.DataFrame) List[str][source]¶
Return the plate IDs present in
df, sorted.- Parameters:
df – raw long measurements or an existing plate layout.
Accepts either a raw long frame or a layout from
plate_layout(). Returns[]for anything unusable rather than raising — this feeds a combo box.
- spacr.plate_qc.row_column_trends(df: pandas.DataFrame, value_col: str | None = None, plate: str | None = None, grouping: str = 'mean', min_count: int = 0) pandas.DataFrame[source]¶
Per-row and per-column summaries, plus the drift across each axis.
One output row per plate row and per plate column, each carrying the number of wells behind it — a row average over three surviving wells is not the same claim as one over twenty-four, and the
n_wellscolumn is what stops it being read as one.The Spearman statistic attached to each axis is computed over the individual wells (not over the 16 row means), because that is where the degrees of freedom are.
- Parameters:
df – long per-object frame, or a layout from
plate_layout().value_col – measurement to aggregate (see
plate_layout()).plate – plate to summarise;
Nonetakes the first.grouping – per-well aggregation, one of
GROUPINGS.min_count – drop wells with fewer than this many objects.
- Returns:
DataFrame with
axis,label,index,n_wells,n_objects,mean,median,std,delta_vs_plate_median,spearman_rhoandspearman_p. Empty (but correctly typed) when there are no wells.
- spacr.plate_qc.row_label(row_index: int) str[source]¶
Return the letter label of a 1-based row index:
3→'C'.- Parameters:
row_index – 1-based plate row index to convert.
- spacr.plate_qc.table_columns(db_path: str, table: str) List[str][source]¶
Return the column names of
table, in declaration order.- Parameters:
db_path – path to the SQLite database to inspect read-only.
table – table or view whose declared columns are returned.
- Raises:
ValueError – when the database has no such table. The name is checked against
sqlite_masterbefore it is ever interpolated into SQL — identifiers cannot be bound, so this is the gate.
- spacr.plate_qc.tables(db_path: str) List[str][source]¶
Return the user tables + views of
db_path, alphabetically.- Parameters:
db_path – path to the SQLite database to inspect read-only.
- spacr.plate_qc.well_id(row_index: int, column_index: int) str[source]¶
Return the canonical well name, e.g.
(3, 7)→'C07'.- Parameters:
row_index – 1-based plate row index.
column_index – 1-based plate column index.
Agrees with
spacr.schema.well_id()on every real well; it differs only in refusing to raise, because a layout table has to render every cell it was handed and'?00'is a more useful report than a traceback.
- spacr.plate_qc.write_layout_csv(layout: pandas.DataFrame, path: str) str[source]¶
Write the well grid to
pathas CSV, one row per well.The tidy form is exported rather than the pivoted matrix: it carries the object count, the ring index and the edge flag alongside the value, which is what anybody re-analysing the plate outside spaCR actually needs.
- Parameters:
layout – frame from
plate_layout().path – destination file. Parent directories are created.
- Returns:
the absolute path written.
- Raises:
ValueError – when
pathis empty.
Nested helpers¶
- _default_lims_fetch.SameOriginRedirect.redirect_request(self, req, fp, code, msg, response_headers, newurl)¶
Refuse a redirected request that changes the LIMS origin.
spacr/plate_qc.py:1810
- _locate_records.whole(value)¶
Convert whole-valued floats to integers before parsing well labels.
spacr/plate_qc.py:1959