spacr.bystanders

Label uninfected cells as bystanders or distal by distance to infection.

spaCR measures each cell on its own, but an infected cell can change the phenotype of its neighbours. Without a neighbourhood column, an uninfected cell touching an infected one and an uninfected cell on the far side of the well are indistinguishable rows.

This has two consequences. A bystander phenotype cannot be detected, because no column separates a bystander from a distant uninfected cell. In addition, if bystanders are phenotypically shifted and pooled into “uninfected”, the control population is a mixture: its variance is inflated and the effect size of every infection comparison is reduced.

Distances are not recomputed here. spacr.object_distances computes a Euclidean distance transform per object type once, at O(image pixels) rather than O(pairs), and this module reads that field and applies a threshold and a label.

The threshold is a length, not a pixel count. It is expressed as a multiple of the measured cell diameter, so it has the same meaning on a 20x and a 63x acquisition and on a plate whose cells are larger. A fixed micron value would suit one dataset and not the next.

Functions

classify(→ pandas.DataFrame)

Label every cell infected, bystander or distal.

compare(→ pandas.DataFrame)

Summarise one measured feature per neighbourhood state.

counts(→ Dict[str, int])

Count cells in each neighbourhood state, including empty states.

distance_to_infected(→ pandas.DataFrame)

Distance from each cell to the nearest infected cell's surface.

neighbourhood(→ pandas.DataFrame)

Fraction of each cell's k nearest neighbours that are infected.

reach_from_diameter(→ float)

The bystander reach in the units cell_diameter is measured in.

Module Contents

spacr.bystanders.classify(cell_mask, infected_labels: Iterable[int], *, reach: float, spacing=None) → pandas.DataFrame[source]

Label every cell infected, bystander or distal.

Parameters:
  • cell_mask – the cell label image.

  • infected_labels – labels of the cells carrying a pathogen.

  • reach – how close an uninfected cell must be to count as a bystander, in the same units as spacing. See reach_from_diameter().

  • spacing – voxel size.

Returns:

label, distance_to_infected and neighbourhood.

A non-positive reach makes every uninfected cell distal. This turns the bystander split off without a second setting, and a reach that is not a finite number is treated the same way.

A well with no parasites reports no bystanders. This follows from distance_to_infected returning infinity there, not from a special case.

spacr.bystanders.compare(frame: pandas.DataFrame, feature: str, *, statuses: Sequence[str] = STATUSES) → pandas.DataFrame[source]

Summarise one measured feature per neighbourhood state.

Parameters:
  • frame – measurements carrying a neighbourhood column, as classify() produces it, joined to the feature table.

  • feature – the column to summarise.

  • statuses – which neighbourhoods to report, in order.

Returns:

neighbourhood, n, mean, std and median.

Splitting “uninfected” into bystander and distal rows lets a bystander effect appear as a difference between groups instead of only as a wider control distribution.

Every requested status is a row, with n = 0 and NaN statistics where the frame has none, so results from different plates have the same rows. A missing feature or neighbourhood column also returns these empty rows.

No test statistic is computed. The choice of test depends on the design (pairing, nesting of wells, the null hypothesis) and is left to the caller; this function returns the per-group summaries.

spacr.bystanders.counts(frame: pandas.DataFrame) → Dict[str, int][source]

Count cells in each neighbourhood state, including empty states.

Parameters:

frame – the output of classify().

Returns:

every name in STATUSES, zero where absent.

Every key is always present, so a caller comparing wells receives 0 for an absent state rather than a KeyError.

spacr.bystanders.distance_to_infected(cell_mask, infected_labels: Iterable[int], *, spacing=None) → pandas.DataFrame[source]

Distance from each cell to the nearest infected cell’s surface.

Parameters:
  • cell_mask – the cell label image.

  • infected_labels – labels of the cells carrying a pathogen.

  • spacing – voxel size, so the distances carry physical units.

Returns:

label and distance_to_infected, one row per cell.

An infected cell reads 0 and its immediate neighbour reads one pixel step, not 0. The field is zero on every infected pixel and grows outward, so a cell that shares a border with an infected one has its nearest pixel one step away: the minimum over that cell is 1. Only a cell whose own pixels are infected reads 0.

surface_distance_transform’s docstring says “two touching objects come out at 0”, and that is true of the case it describes – reading the field at a point INSIDE the other object, which is what an overlapping or contained object gives. Two disjoint labels that merely abut do not overlap, so they do not. A threshold expressed in cell diameters is far larger than one pixel either way, so a value of 1 for an adjacent cell is expected.

Every distance is inf when the image has no infected cell, rather than 0 or NaN: infinity keeps distance <= reach False for every finite reach.

spacr.bystanders.neighbourhood(cell_mask, infected_labels: Iterable[int], *, k: int = 6, spacing=None) → pandas.DataFrame[source]

Fraction of each cell’s k nearest neighbours that are infected.

Parameters:
  • cell_mask – the cell label image.

  • infected_labels – labels of the cells carrying a pathogen.

  • k – how many neighbours to count, excluding the cell itself.

  • spacing – voxel size, so “nearest” means nearest in real space rather than in pixels on an anisotropic stack.

Returns:

label, neighbours_counted, neighbours_infected and local_infected_fraction.

classify() applies a threshold to one distance. This function measures the infected share of the neighbourhood, which separates a bystander at the edge of an infection focus from one in its middle. Neighbours are found among cell centroids.

A cell is never counted as its own neighbour, and infected cells also receive a fraction, so an infected cell among uninfected ones can be distinguished from one inside a cluster.

neighbours_counted is reported rather than assumed to be k. A field with fewer than k + 1 cells supplies fewer than k neighbours, and the column gives the denominator actually used. Fields with fewer than two cells return NaN fractions.

spacr.bystanders.reach_from_diameter(cell_diameter: float, diameters: float = DEFAULT_REACH_IN_DIAMETERS) → float[source]

The bystander reach in the units cell_diameter is measured in.

Parameters:
  • cell_diameter – the plate’s measured cell diameter, from spacr.diameter.

  • diameters – how many diameters count as neighbouring.

Returns:

a distance in the same units, never negative.

Kept as its own function so the conversion is visible in one place and a caller can see that the reach is derived rather than chosen.