Source code for phenotypic.measure._measure_grid_linreg_stats

from __future__ import annotations

from typing import ClassVar, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._grid_image import GridImage

import pandas as pd
from scipy.spatial.distance import euclidean

from phenotypic.abc_ import GridMeasureFeatures
from phenotypic.schema import OBJECT
from phenotypic.schema import BBOX, GRID, GRID_LINREG_STATS


[docs] class MeasureGridLinRegStats(GridMeasureFeatures): """Evaluate grid alignment quality using row-wise and column-wise linear regression. Fit linear regressions to colony centroid positions along each row and column of the grid, then compute per-colony residual error (Euclidean distance between observed and predicted centroid). High residual errors flag off-grid growth, misdetections, or plate warping. Args: section_num: Grid section index to restrict measurements to. ``None`` measures across the entire grid. Default: ``None``. Returns: pd.DataFrame: Per-object metrics indexed by object label: - RowM, RowB: row regression slope and intercept. - ColM, ColB: column regression slope and intercept. - PredRR, PredCC: predicted centroid from regression. - ResidualError: Euclidean distance between actual and predicted centroid. Best For: - Identifying colonies that grew outside their designated grid position on arrayed plates. - Detecting systematic rotation or shear across the plate to validate grid detection quality. - Filtering or weighting colonies by positional confidence before downstream phenotypic analysis. Consider Also: - :class:`MeasureGridSpatial` for neighbor-distance metrics between adjacent grid cells. - :class:`MeasureGridSpread` for detecting over-segmentation and multi-object wells. - :class:`MeasureBounds` for raw centroid and bounding box coordinates without regression. See Also: :doc:`/tutorials/notebooks/07_measuring_and_exporting` for a walkthrough of grid-level measurements. """ _measurement_infoclass: ClassVar[type] = GRID_LINREG_STATS section_num: Optional[int] = None def _operate(self, image: GridImage) -> pd.DataFrame: # Collect the relevant section info. If no section was specified perform calculation on the entire grid info table. if self.section_num is None: section_info = image.grid.info().reset_index(drop=False) else: grid_info = image.grid.info().reset_index(drop=False) section_info = grid_info.loc[ grid_info.loc[:, str(GRID.ROW_MAJOR_IDX)] == self.section_num, : ] # Get the current row-wise linreg info row_m, row_b = image.grid.get_centroid_alignment_info(axis=0) # Convert arrays to dataframe for join operation row_linreg_info = pd.DataFrame( data={ str(GRID_LINREG_STATS.ROW_LINREG_M): row_m, str(GRID_LINREG_STATS.ROW_LINREG_B): row_b, }, index=pd.Index(data=range(image.grid.nrows), name=str(GRID.ROW_NUM)), ) section_info = pd.merge( left=section_info, right=row_linreg_info, left_on=str(GRID.ROW_NUM), right_on=str(GRID.ROW_NUM), ) # NOTE: Row linear regression(CC) -> pred RR section_info.loc[:, str(GRID_LINREG_STATS.PRED_RR)] = ( section_info.loc[:, str(BBOX.CENTER_CC)] * section_info.loc[:, str(GRID_LINREG_STATS.ROW_LINREG_M)] + section_info.loc[:, str(GRID_LINREG_STATS.ROW_LINREG_B)] ) # Get the current column linreg info col_m, col_b = image.grid.get_centroid_alignment_info(axis=1) # convert array to dataframe for join operation col_linreg_info = pd.DataFrame( data={ str(GRID_LINREG_STATS.COL_LINREG_M): col_m, str(GRID_LINREG_STATS.COL_LINREG_B): col_b, }, index=pd.Index(data=range(image.grid.ncols), name=str(GRID.COL_NUM)), ) section_info = pd.merge( left=section_info, right=col_linreg_info, left_on=str(GRID.COL_NUM), right_on=str(GRID.COL_NUM), ) # NOTE: Col linear regression(RR) -> pred CC section_info.loc[:, str(GRID_LINREG_STATS.PRED_CC)] = ( section_info.loc[:, str(BBOX.CENTER_RR)] * section_info.loc[:, str(GRID_LINREG_STATS.COL_LINREG_M)] + section_info.loc[:, str(GRID_LINREG_STATS.COL_LINREG_B)] ) # Calculate the distance each object is from it's predicted center. This is the residual error section_info.loc[:, str(GRID_LINREG_STATS.RESIDUAL_ERR)] = ( section_info.apply( lambda row: euclidean( u=[row[str(BBOX.CENTER_CC)], row[str(BBOX.CENTER_RR)]], v=[ row[str(GRID_LINREG_STATS.PRED_CC)], row[str(GRID_LINREG_STATS.PRED_RR)], ], ), axis=1, ) ) return section_info.set_index(OBJECT.LABEL)
MeasureGridLinRegStats.__doc__ = GRID_LINREG_STATS.append_rst_to_doc( MeasureGridLinRegStats)