spacr.bystanders¶
Which uninfected cells are next to an infected one, and which are not.
THE COLUMN THAT DOES NOT EXIST YET. spaCR measures each cell on its own, and the biology of an intracellular pathogen is not confined to the cell it is in: an infected cell changes what its neighbours do. Right now an uninfected cell touching an infected one and an uninfected cell on the far side of the well are the same row.
TWO CONSEQUENCES, and the second is the one that costs results. A genuine bystander phenotype cannot be found, because no column separates a bystander from a distant uninfected cell. And every uninfected control is quietly diluted: if bystanders are phenotypically shifted and they are pooled into “uninfected”, the control population is a mixture, its variance is inflated, and the effect size of every infection comparison shrinks towards nothing. That is a confound in results the package already produces, not a missing feature.
DISTANCE IS ALREADY SOLVED AND NOTHING HERE RE-IMPLEMENTS IT.
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. The work here is 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 means the same thing on a 20x and a 63x acquisition and on a plate whose cells are simply larger. A hard-coded micron count would be right for one dataset and silently wrong for the next.
Functions¶
|
Label every cell infected, bystander or distal. |
|
One row per neighbourhood, for one measured feature. |
|
How many cells landed in each state, including the empty ones. |
|
How far each cell's surface is from the nearest INFECTED cell. |
|
What fraction of each cell's nearest neighbours are infected. |
|
The bystander reach in the units |
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. Seereach_from_diameter().spacing – voxel size.
- Returns:
label,distance_to_infectedandneighbourhood.
A NON-POSITIVE REACH MAKES EVERY UNINFECTED CELL DISTAL, which is the behaviour that lets a user turn the split off without a second setting – and the behaviour a mis-parsed setting falls back to, since it cannot invent bystanders.
THE ACCEPTANCE TEST THIS HAS TO PASS is that a well with no parasites reports no bystanders. It follows from
distance_to_infectedbeing infinite there rather than from a special case, which is why that function returns infinity.
- spacr.bystanders.compare(frame: pandas.DataFrame, feature: str, *, statuses: Sequence[str] = STATUSES) pandas.DataFrame[source]¶
One row per neighbourhood, for one measured feature.
- Parameters:
frame – measurements carrying a
neighbourhoodcolumn, asclassify()produces it, joined to the feature table.feature – the column to summarise.
statuses – which neighbourhoods to report, in order.
- Returns:
neighbourhood,n,mean,stdandmedian.
THE COMPARISON THE WHOLE ITEM IS FOR, and the reason it is worth having is subtraction rather than addition: the package can already compare infected against uninfected. What it cannot do is notice that “uninfected” was two populations, so a bystander effect shows up as nothing more than a wider control.
EVERY REQUESTED STATUS IS A ROW, at
n = 0where the well has none. A caller comparing plates needs the same three rows every time; a missing row silently changes what a downstream join means.NO TEST STATISTIC HERE, deliberately. Summarising is safe on any column; choosing a test is a decision about the design – paired or not, how the wells nest, what the null is – and belongs where those are known. What this hands the regression layer is three named groups it can contrast.
- spacr.bystanders.counts(frame: pandas.DataFrame) Dict[str, int][source]¶
How many cells landed in each state, including the empty ones.
- Parameters:
frame – the output of
classify().- Returns:
every name in
STATUSES, zero where absent.
EVERY KEY ALWAYS PRESENT. A caller comparing wells needs a zero rather than a missing key, because “no bystanders in this well” is a result and a KeyError is not.
- spacr.bystanders.distance_to_infected(cell_mask, infected_labels: Iterable[int], *, spacing=None) pandas.DataFrame[source]¶
How far each cell’s surface is from the nearest INFECTED cell.
- 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:
labelanddistance_to_infected, one row per cell.
ZERO FOR AN INFECTED CELL, ONE PIXEL FOR ITS IMMEDIATE NEIGHBOUR, and the distinction is worth stating because the obvious reading is wrong. The field is zero on any INFECTED pixel and grows outward, so a cell that shares a border with an infected one has its own nearest pixel one step away: the minimum over that cell is 1, not 0. 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; the note is here so nobody reads a 1 as a bug.infWHEN THERE IS NOTHING TO BE NEAR. A well with no infected cell has no distances, and the honest value is infinity rather than 0 or NaN: every cell is arbitrarily far from a population that is not there, and infinity is the only value that keepsdistance <= reachFalse for every finite reach.
- spacr.bystanders.neighbourhood(cell_mask, infected_labels: Iterable[int], *, k: int = 6, spacing=None) pandas.DataFrame[source]¶
What fraction of each cell’s nearest neighbours 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_infectedandlocal_infected_fraction.
THE SECOND HALF OF THE BYSTANDER QUESTION.
classify()answers “is there an infected cell near this one”, which is a threshold on one distance. This answers “how infected is this cell’s neighbourhood”, which is the quantity that separates a cell at the edge of a focus from one in the middle of it – both are bystanders and they are not in the same place.THE CELL ITSELF IS NEVER ITS OWN NEIGHBOUR, and an INFECTED cell still gets a fraction: “this infected cell sits among uninfected ones” is a different observation from “this one sits in a cluster”, and dropping the infected rows would throw the second away.
neighbours_countedIS REPORTED RATHER THAN ASSUMED TO BEk. A field with fewer thank + 1cells cannot supply k neighbours, and a fraction computed against an assumed denominator would be wrong in exactly the sparse fields where a bystander analysis matters most. The column says what the fraction was actually divided by.
- spacr.bystanders.reach_from_diameter(cell_diameter: float, diameters: float = DEFAULT_REACH_IN_DIAMETERS) float[source]¶
The bystander reach in the units
cell_diameteris 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.