mantispy.tl.cluster_composition

mantispy.tl.cluster_composition#

mantispy.tl.cluster_composition(adata, cluster_key='leiden', by=('Metadata_Plate', 'Metadata_Well'), reference='negcon')#

Fraction of each well’s cells in each cluster, as a well-level object.

Parameters:
  • adata (AnnData) – Clustered single-cell object.

  • cluster_key (str (default: 'leiden')) – obs column holding the cluster label.

  • by (Sequence[str] | str (default: ('Metadata_Plate', 'Metadata_Well'))) – Columns defining a row of the result, normally the well.

  • reference (str | None (default: 'negcon')) – Controls to test each well’s composition against, or None to skip the test.

Return type:

AnnData

Returns:

A new object with wells as rows and clusters as columns, holding the fraction of each well’s cells in each cluster. It is a well-level mantispy object, so map(), normalize() and the plots accept it. uns["mantispy"]["composition_test"] holds a chi-square test of each well against the pooled control composition, with group, statistic, pvalue and qvalue. uns["mantispy"]["composition_dispersion"] holds the factor the controls’ own spread contributed, described below.

Raises:

KeyErrorobs has no column cluster_key.

Notes

A well with few cells has a noisy composition. The chi-square test is computed on counts and accounts for this, but the fractions in X do not. Filter with well_qc() first.

Chi-square alone asks whether a well’s cells are a multinomial draw from the control composition, and wells vary beyond that: seeding, position and edge effects all move a composition without any perturbation. The statistic is therefore divided by the dispersion the control wells show, mean(control statistic) / df, floored at one. Without that correction, wells drawn from a single composition with mild jitter were called at q = 9e-10, 10 of 20 of them.

The correction is only as good as the dispersion estimate. With 16 control wells the false positive rate ran near 0.10 against a nominal 0.05 in simulation, and with 32 it ran near 0.06; below _DISPERSION_MIN_CONTROLS wells the function warns. Power falls accordingly: a composition shift of a few percentage points is not separable from well-to-well variation, and reporting it as significant was the bug.

Clusters no control cell reached are left out of the test, since the controls give them no expected frequency. Their fractions stay in X, and subpopulation_hits() compares within a cluster.

The test holds one row per well, in the order of the rows of the returned object. A well with no cells in the clusters the controls occupy gets NaN. So does every well when the controls occupy fewer than two clusters, since a chi-square over a single category has no degrees of freedom. That case warns, because the table keeps its row per well and would otherwise read as no well’s composition differing.