API Reference¶
Reference for the Python API. See Getting Started for introductory examples and Metrics for what each metric measures.
croma exposes three metric classes, the downstream probe protocol and two
reductions, and one alignment helper. Each metric class is a namespace of classmethods –
you never instantiate them; apd, nipd and probe_sweep are plain functions. The
short names are the ones to import:
from croma import CRoMa, MaRI, RI, apd, nipd, probe_sweep
Import as |
Class |
Returns |
|---|---|---|
|
||
|
||
|
||
|
|
|
|
|
|
|
|
RI¶
- class croma.RobustnessIndex¶
Bases:
BaseRobustnessIndex- classmethod compute(features, manifest, *, confounder_column, k_candidates, evaluation_design='all')¶
Compute RI at the operating
kselected by kNN balanced accuracy.- Parameters:
evaluation_design (
str) –"all"(the default) or"paired_2x2"; see the constants incroma.metrics.basefor what each scope scores.- Return type:
MaRI¶
- class croma.MarginAwareRobustnessIndex¶
Bases:
BaseRobustnessIndex- classmethod recommend_tau(features, manifest, *, confounder_column, k, evaluation_design='all')¶
Recommended
taufor this dataset: the median typed (SO/OS) neighbour distance.Returns
nanwhen no typed neighbour exists within the top-kset, and0.0when typed neighbours exist but half or more of them sit at distance0(a collapsed embedding, or a manifest that duplicates rows). Neither can be put on a meaningful scale, but they are distinct datasets: seeTAU_FALLBACK.- Return type:
- classmethod compute(features, manifest, *, confounder_column, k_candidates, tau=None, evaluation_design='all', warn_tau=True)¶
Compute MaRI at the operating
kselected by kNN balanced accuracy.- Parameters:
evaluation_design (
str) –"all"(the default) or"paired_2x2"; see the constants incroma.metrics.basefor what each scope scores.tau (
float|None) – Distance-decay temperature. Leave asNone(recommended) to resolve it automatically as this dataset’s median typed-neighbour distance at the operatingk– the on-scale value. A pinnedtauis only comparable across models whose typed-neighbour distances share a scale; because that scale is a property of each embedding, a single fixedtausilently sharpens the margin for some models and flattens it for others.warn_tau (
bool) – Warn when a pinnedtausits off the typed-neighbour scale. Ignored whentauis resolved automatically, which is on-scale by construction.
- Return type:
- classmethod compute_curve(features, manifest, *, confounder_column, k_values, tau=None, evaluation_design='all')¶
MaRI at every
kink_values, all scored at a singletau.Auto-
tau(the default) is resolved once, at the operatingkselected overk_values, and then held fixed across the sweep: atauthat moved withkwould confound the curve’s shape with the temperature’s.
CRoMa¶
- class croma.CrossConfounderRobustnessMargin¶
Bases:
object- classmethod compute(features, manifest, *, confounder_column, evaluation_design='all', m=5, alpha=0.1, start_k=200, k_growth_factor=2.0)¶
Compute the cross-confounder robustness margin.
- Parameters:
evaluation_design (
str) –"all"(the default) or"paired_2x2"; see the constants incroma.metrics.basefor what each scope scores.- Return type:
Probe protocol¶
probe_sweep produces the matrix the reductions below consume. It trains a probe to
predict the biological class from frozen embeddings while a schedule walks the training
set from balanced to fully confounded, and scores each probe on test rows that do not
move. It takes embeddings and a split assignment: no model is loaded, no manifest read and
no output layout touched.
from croma import apd, probe_sweep
from croma.downstream import pathorob_schedule
accuracies = probe_sweep(
embeddings, # (n_rows, n_features)
center_index, # (n_rows,) confounder index per row
class_index, # (n_rows,) biological class index per row
schedule=pathorob_schedule("camelyon", rows_per_slide=300),
rows_per_slide=300,
)
apd(accuracies)
- croma.probe_sweep(embeddings, confounders, labels, *, schedule, rows_per_slide=1, iterations=20, seed=1000, validation_fraction=None)¶
Run the confounder-biased probe sweep and return its accuracy matrix.
One row per split, one column per resampling replicate:
accuracies[s][i] = balanced accuracy of the probe trained on split s, replicate i
Row
0is whatever the caller put first inschedule– by construction the balanced baseline, the split at which the confounder carries no information about the biological class. Every later row is a more confounded split. That is exactly the matrixcroma.apd()andcroma.nipd()reduce, so the return value goes into either of them untouched.Each replicate reshuffles every
(confounder, class)cell by whole slides, then walks the schedule, cutting each split’s training rows off the front of the cells, its validation rows from directly behind them, and its test rows from a tail that starts beyond the widest training block any split asks for. The tail therefore holds the same rows for every split, which is what makes accuracies across splits comparable: the training composition moves, the test composition does not.- Parameters:
embeddings (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) –(n_rows, n_features)frozen representations. Rows are grouped into slides by position, so they must arrive in the order the slides do.confounders (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) –(n_rows,)confounder index per row – the medical centre, scanner or provider whose influence is being injected. These are theiof a schedule entry, so they index it and must run0 .. n_confounders - 1.labels (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) –(n_rows,)biological class index per row: thejof a schedule entry, running0 .. n_classes - 1. This is what the probe predicts.schedule (
Sequence[tuple[Sequence[tuple[int,int,int]],int]]) – OneSplitPlanper split, the balanced baseline first. Two splits at minimum, since a sweep with no confounded split reduces to nothing.rows_per_slide (
int) – How many consecutive rows make up one slide. Slides, not rows, are the unit that is shuffled and counted, so train and test never share one.iterations (
int) – How many resampling replicates to run – the width of the matrix.seed (
int) – Seed of the replicate seeds. The sweep is a pure function of it, so two runs with the same seed return the same matrix.validation_fraction (
float|None) – Size of each cell’s validation slice, as a fraction of its training rows.Noneselects PathoROB’s own rule,1 / max_train_slidesof the cell’s training rows, which is the faithful setting for a sweep over patches. It underflows to zero when a slide is one row, which is why a slide-level sweep has to state the fraction instead.
- Return type:
- Returns:
The
(n_splits, iterations)matrix of balanced accuracies on the held-out in-domain rows.- Raises:
ValueError – If the inputs are not aligned rectangular arrays of non-negative integer indices; if
rows_per_slideoriterationsis not positive, oriterationsexceeds the 10,000 seeds a replicate can be drawn from; ifvalidation_fractionis outside[0, 1); or if the schedule holds fewer than two splits, names a cell no row carries, asks a cell for more rows than it holds or thanmax_train_slidesallows, or leaves some split with no training, validation or test rows. Those last few are the same fault wearing different clothes: a schedule and a cohort that were not written for each other, which would otherwise run to completion at a confounder bias nobody asked for.
Scoring unseen confounders¶
PathoROB scores every probe both on held-out rows of the confounders it trained on and on
an unseen confounder. Both come off one training pass, so they are one call. This form also
takes arrange_slides, which replaces the one step of a replicate that decides which
slides a split trains on and which sit in the held-out tail – for a cohort whose slides
cannot be ordered freely. Its default is the sweep’s own shuffle, which is the reference
protocol.
- croma.downstream.probe_sweep_over_test_sets(embeddings, confounders, labels, *, schedule, test_sets=None, rows_per_slide=1, iterations=20, seed=1000, validation_fraction=None, arrange_slides=None)¶
Run one sweep and score every probe it trains on more than one test set.
Same protocol as
probe_sweep(), and its in-domain matrix is identical – this form only adds test sets that were never trained on, typically rows from confounders the sweep never saw. They ride along on the probes the sweep already trained, so asking for an unseen-confounder matrix costs one prediction per split and replicate rather than a second sweep.- Parameters:
embeddings (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) – Asprobe_sweep().confounders (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) – Asprobe_sweep().labels (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) – Asprobe_sweep().schedule (
Sequence[tuple[Sequence[tuple[int,int,int]],int]]) – Asprobe_sweep().test_sets (
Mapping[str,tuple[_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str],_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]]] |None) – Extra test sets as{name: (embeddings, labels)}. Their labels are biological class indices, aslabelsis; they carry no confounder index, because nothing about a test set is confounder-dependent. Their names come back as the keys of the result and cannot be"in_domain".rows_per_slide (
int) – Asprobe_sweep().iterations (
int) – Asprobe_sweep().seed (
int) – Asprobe_sweep().arrange_slides (
Callable[[Sequence[Sequence[Sequence[Sequence[int]]]],Random],Sequence[Sequence[Sequence[Sequence[int]]]]] |None) – How one replicate orders each cell’s slides – the step that decides which slides a split trains on and which sit in the held-out tail.Noneselects the sweep’s own step, a shuffle of every cell with the replicate’s generator, which is the reference protocol. Pass aSlideArrangementfor a cohort whose slides cannot be ordered freely: PathoROB’s Tolkach-ESCA sweep, for one, draws a train/test case split per replicate and pushes those cases to the tail, because a case there carries patches of several biological classes and may not be trained on and tested on at once. An arrangement receives the cells in input order and returns them reordered; it may draw from the generator it is given, and must draw before it shuffles for its sweep to match a driver that does, since the two share one stream. It may not add, drop or break up a slide.
- Return type:
- Returns:
{"in_domain": matrix}plus one(n_splits, iterations)matrix per named test set. Each is a well-formed input forcroma.apd()andcroma.nipd().- Raises:
ValueError – Everything
probe_sweep()raises, plus a test set whose embeddings and labels disagree in length, whose feature count differs fromembeddings, that is empty, or that is named"in_domain"; and anarrange_slidesthat returns anything but a rearrangement of the slides it was handed.
PathoROB’s schedules¶
- croma.downstream.pathorob_schedule(dataset, *, rows_per_slide, n_splits=None)¶
PathoROB’s own schedule for one of its three downstream datasets.
A schedule is the sequence of training compositions a sweep walks, from balanced to fully confounded. This one is not croma’s: it comes from the split-mapping helper vendored verbatim from PathoROB, so a sweep run on it is the reference protocol rather than something resembling it. Pass the result straight to
probe_sweep().Cells are addressed as
(confounder, class)in the order PathoROB lists them –camelyonasRUMC, UMCUxnormal, tumor, and so on – so the caller’sconfoundersandlabelshave to be indexed the same way. Datasets with their own schedule, croma’s included, build the sequence themselves; nothing here is required.- Parameters:
dataset (
str) –"camelyon","tcga"or"tolkach_esca"– PathoROB’s names, kept as PathoROB spells them (note"tcga"for the 4x4 cohort).rows_per_slide (
int) – How many rows one slide contributes, which is what the schedule’s per-slide counts are multiplied by. PathoROB publishes 300 forcamelyon, 30 fortcgaand 100 fortolkach_esca; a slide-level sweep passes1.n_splits (
int|None) – How far to walk the schedule. Defaults to the number of splits PathoROB runs for this dataset (PATHOROB_SPLITS), and cannot exceed it – past that point the formulas run the favourable cells negative, so a longer walk would not be more confounded, it would be undefined. A shorter one is a prefix of the reference protocol, which is what a smoke run wants.
- Return type:
- Returns:
One
SplitPlanper split, balanced baseline first.- Raises:
ValueError – If
datasetis not one PathoROB has a schedule for, ifrows_per_slideis not positive, or ifn_splitsis not positive or runs past the schedule’s last split.
These two, and croma.downstream.IN_DOMAIN – the key the sweep’s own held-out matrix
comes back under – are reachable under croma.downstream but are not promoted to the
top level, so they carry no stability promise.
Downstream reductions¶
Unlike the three metrics above, apd and nipd read no embeddings: they reduce the
balanced accuracies a confounder-biased probe sweep already produced. Both take the same
(n_splits, n_iterations) matrix, with the balanced baseline in row 0 and each
later row a progressively more confounded split. nipd additionally takes the
Cramér’s-V coordinate of every row.
APD¶
apd is PathoROB’s metric, reported as the faithful reference. Its reduction is
vendored verbatim from PathoROB (BSD 3-Clause, © 2025 BIFOLD Pathomics; see the
distribution’s NOTICE) rather than reimplemented, so a value reported as “APD” is the
value PathoROB would report. It takes no chance argument: it normalizes by raw
accuracy.
from croma import apd
apd(accuracies) # -> e.g. -0.046
- croma.apd(accuracies)¶
Average Performance Drop: the mean relative accuracy change a confounder costs.
accuraciesis an(n_splits, n_iterations)matrix of balanced-accuracy scores from a confounder-biased probe sweep, where row0is the balanced baseline and every later row is a progressively more confounded split. APD is the mean, over the confounded splits, of each split’s accuracy relative to the baseline, minus one:APD = mean_i( mean_{s>0}( accuracies[s][i] / accuracies[0][i] ) - 1 )
So
0means no split lost accuracy, and a value near-1that the confounded splits collapsed. Closer to zero is more robust.This is PathoROB’s metric, not croma’s, and it is reported because it is faithful: the reduction itself is vendored verbatim from PathoROB (
croma.downstream._pathorob) rather than re-derived, so a number reported as “APD” means what the PathoROB paper means by it. This function adds argument validation and averages the replicate axis – exactly what PathoROB’s own driver does to turn the per-replicate scores into the scalar it publishes.Note that the ratio is taken per replicate and averaged afterwards, preserving PathoROB’s published reduction.
croma.nipd()is a separate estimand: it averages repeats first, normalizes by above-chance baseline skill, and integrates over Cramér’s V.- Parameters:
accuracies (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) –(n_splits, n_iterations)balanced accuracies, row0the balanced baseline. At least two splits and one replicate.- Return type:
- Returns:
The mean relative performance drop, a plain
float.- Raises:
ValueError – If
accuraciesis not a 2-D matrix with at least one confounded split and at least one replicate, if it holds a non-finite score, or if any replicate’s baseline accuracy is zero or negative. The last is the reduction’s domain running out: mean-of-ratios divides by each replicate’s own baseline, so a zero there has no ratio and a negative one inverts every sign.
nIPD¶
nipd is the normalized integrated performance degradation: the signed area under
the chance-normalized degradation curve over Cramér’s V. It divides performance
changes by baseline skill – balanced accuracy above chance – rather than by raw
baseline accuracy. This corrects unequal baseline headroom across models and tasks.
For mean balanced accuracy across repeated training runs, \(\bar a(V)\), baseline \(a_0 = \bar a(0)\) and chance \(\pi\),
The integral is estimated by the trapezoidal rule at the supplied Cramér’s-V
coordinates. Consequently, interval widths – not the number of sampled conditions –
weight the curve. The coordinates must be finite, strictly increasing, aligned with the
accuracy rows and span 0 to 1. The mean baseline must exceed chance; there is no
additional weak-skill threshold.
from croma import nipd
nipd(
accuracies=[
[0.90, 0.90],
[0.70, 0.70],
[0.50, 0.50],
],
cramers_v=[0.0, 0.5, 1.0],
chance=0.5,
) # -> -0.5
- croma.nipd(accuracies, cramers_v, chance)¶
Normalized Integrated Performance Degradation over Cramér’s V.
accuraciesholds one row per sampled Cramér’s-V value and one column per repeated training run. Row0is the balanced baseline atV=0. We first average balanced accuracy across repeats at each sampled value, then express the change from baseline as a fraction of baseline skill (accuracy above chance):g(V) = (mean_accuracy(V) - mean_accuracy(0)) / (mean_accuracy(0) - chance)
nIPD is the signed area under
gon[0, 1], estimated by trapezoidal integration over the supplied Cramér’s-V coordinates. Thus0means no degradation, increasingly negative values mean greater shortcut susceptibility, and-0.5is the area of a linear fall from baseline performance atV=0to chance performance atV=1.- Parameters:
accuracies (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) –(n_splits, n_iterations)balanced accuracies, row0the balanced baseline.cramers_v (
_Buffer|_SupportsArray[dtype[Any]] |_NestedSequence[_SupportsArray[dtype[Any]]] |complex|bytes|str|_NestedSequence[complex|bytes|str]) – One finite, strictly increasing Cramér’s-V value per accuracy row, beginning at0and ending at1.chance (
float) – Balanced-accuracy chance level,1 / n_biological_classes.
- Return type:
- Returns:
The signed trapezoidal area under the normalized degradation curve.
- Raises:
ValueError – If the accuracy matrix is malformed, the Cramér’s-V coordinates are malformed or do not span
[0, 1], chance lies outside[0, 1), or mean baseline balanced accuracy is at or below chance.
For reproducibility, \(\bar a(V)\) is formed before normalization: nIPD therefore
uses a ratio of repeat means. apd retains PathoROB’s mean-of-repeat-specific-ratios
order because changing it would break faithfulness to the reference implementation.
Alignment¶
Result types¶
- class croma.types.RobustnessResult(dataset, k, value, std, n_pairs, pair_values, sample_values, sample_values_aligned, occurrence_defined_mask, sample_undefined_types, occurrence_subsets, occurrence_source_indices, undefined_frac=0.0, ss_dominated_undefined_frac=0.0, oo_dominated_undefined_frac=0.0, mixed_undefined_frac=0.0, evaluation_design='all', evaluation_unit='sample', alpha=0.1, median_value=nan, q_alpha=nan, ltm_alpha=nan, tau=nan)¶
Bases:
object
- class croma.types.CRoMaResult(dataset, m, value, std, n_pairs, pair_values, sample_values, sample_values_aligned, occurrence_defined_mask, undefined_frac, evaluation_design='all', evaluation_unit='sample', occurrence_subsets=None, occurrence_source_indices=None, k_start=0, k_final=0, retries=0, alpha=0.1, q_alpha=nan, ltm_alpha=nan, f0=nan)¶
Bases:
objectOne CRoMa evaluation: the pooled margin, its distribution, and its tail.
f0is the confounder-dominant fraction \(F(0)\) – the empirical CDF of the per-sample margin at zero, i.e. the fraction of defined evaluation units whose margin is<= 0. Exact zero counts as confounder-dominant; undefined units are excluded from the denominator, as they are forq_alphaandltm_alpha, and are reported separately byundefined_frac. It isnanwhen nothing is defined.