A distributional robustness margin for pathology foundation models
4
stars
132
commits
Python
primary language
Aug 26, 2026
updated
croma measures how much a pathology foundation model's representation is driven by biology rather than by non-biological technical variation -- staining, scanning, tissue preparation -- across centers.
| Metric | Name | What it does |
|---|---|---|
RI | Robustness Index | Counts favourable vs. unfavourable neighbours |
MaRI | Margin-aware Robustness Index | Weights that same evidence by feature distance |
CRoMa | Cross-confounder Robustness Margin | A signed margin, with tail-aware reporting |
RI was introduced in the PathoROB study. croma provides a clean re-implementation of it, adds MaRI as its margin-aware extension, and introduces CRoMa, which overcomes limitations of both. MaRI and CRoMa are described in A distributional robustness margin for pathology foundation models.
For downstream shortcut-susceptibility experiments, croma also provides the
confounder-biased probe protocol, PathoROB's APD reduction, and normalized integrated
performance degradation (nIPD). nIPD expresses degradation relative to the
above-chance baseline performance available to lose and integrates it over Cramér's
V. See the downstream API.
pip install croma
The core package depends only on numpy, pandas, scikit-learn and tqdm. It never loads a model or reads an image -- you bring the embeddings. Add the paper-reproduction utilities with pip install "croma[repro]".
You need a manifest CSV with one row per sample, and an embeddings array of shape (N, D) whose row i is the embedding of manifest row i. Don't normalize them -- croma L2-normalizes internally and compares neighbours by cosine distance.
import numpy as np
import pandas as pd
from croma import CRoMa, MaRI, RI
manifest = pd.read_csv("manifest.csv")
features = np.load("embeddings.npy")
common = dict(confounder_column="center") # evaluation_design defaults to "all"
ri = RI.compute(features, manifest, k_candidates=[5, 11, 21], **common)
mari = MaRI.compute(features, manifest, k_candidates=[5, 11, 21], **common)
croma = CRoMa.compute(features, manifest, **common)
print(f"RI {ri.value:.3f} (k={ri.k}, support {ri.support:.1%})")
print(f"MaRI {mari.value:.3f} (tau={mari.tau:.4f}, support {mari.support:.1%})")
print(f"CRoMa {croma.value:+.3f} (lower-tail mean {croma.ltm_alpha:+.3f})")
Twenty-six encoders — twenty-five pathology foundation models and one natural-image control (†) — scored on three tile cohorts from PathoROB.
| Model | mean rank | CRoMa rank | tail rank | Camelyon CRoMa/LTM₁₀ | TCGA-4×4 CRoMa/LTM₁₀ | Tolkach-ESCA CRoMa/LTM₁₀ |
|---|---|---|---|---|---|---|
| Mascaret | 1.7 | 2.0 | 1.3 | 0.29/-0.02 | 0.27/-0.11 | 0.51/0.01 |
| RudolfV-2-S | 4.3 | 2.3 | 6.3 | 0.32/-0.02 | 0.19/-0.16 | 0.49/-0.00 |
| RudolfV-2 | 4.5 | 4.3 | 4.7 | 0.24/-0.04 | 0.17/-0.12 | 0.41/-0.01 |
| RudolfV-2-B | 5.5 | 4.7 | 6.3 | 0.24/-0.05 | 0.17/-0.14 | 0.41/-0.02 |
| CONCHv1.5 | 7.5 | 7.3 | 7.7 | 0.19/-0.14 | 0.15/-0.13 | 0.39/-0.03 |
| GenBio-PathFM | 8.5 | 7.0 | 10.0 | 0.19/-0.07 | 0.16/-0.19 | 0.39/-0.02 |
| CONCH | 9.2 | 6.0 | 12.3 | 0.20/-0.20 | 0.15/-0.15 | 0.44/-0.04 |
| Virchow2 | 9.2 | 8.3 | 10.0 | 0.20/-0.11 | 0.13/-0.17 | 0.35/-0.04 |
Top 8 of 25 ranked pathology encoders, over 3 tile cohorts; the DINOv2-B control is shown unranked in the full table. The CRoMa and tail ranks are the means of that encoder's within-cohort ranks — by median CRoMa, and by tail severity LTM₁₀ — and the mean rank averages those two. It orders the table; it does not replace them, because a strong median can hide a brittle tail and only the two columns show that — which is why each cohort shows both, median CRoMa/LTM₁₀. Bold marks the Pareto frontier: the encoders no other pathology encoder beats on both axes at once.
📊 Full panel, per-cohort detail and the distributions
RI and MaRI live in [0, 1]; above 0.5, biological evidence outweighs confounder evidence. CRoMa lives in (-1, 1) and is neutral at 0, positive when biology dominates.
Three habits will keep you out of trouble:
support next to RI and MaRI. It is the fraction of evaluation units with informative neighbours inside k: manifest samples under all, or subset occurrences under paired_2x2. A high RI over a thin support is not a strong result.tau. It defaults to None, which resolves it per model on the scale of that model's own neighbour distances. One fixed tau shared across models sharpens the margin for some and flattens it for others -- exactly the distortion MaRI exists to remove. See Choosing tau.croma.ltm_alpha is the mean of the worst 10% of samples. Pooled scores hide brittle subgroups.all, the default, scores every manifest row together and gives one number over the whole cohort at sample level; paired_2x2 controls what is compared and reports occurrence-level outputs. Includes the manifest contract and minimal valid examples for each.scripts/.If you use MaRI or CRoMa, please cite the paper — and the PathoROB study that introduced the Robustness Index these build on.
@article{grisi2026distributional,
title = {A distributional robustness margin for pathology foundation models},
author = {Grisi, Cl{\'e}ment and van der Laak, Jeroen and Litjens, Geert},
journal = {arXiv preprint arXiv:2607.25497},
year = {2026},
doi = {10.48550/arXiv.2607.25497},
url = {https://arxiv.org/abs/2607.25497}
}
To cite the software itself rather than the method, use CITATION.cff
directly; GitHub's Cite this repository button resolves to the paper above.
132 commits
Python
98.8%
JavaScript
1.0%
A distributional robustness margin for pathology foundation models
4
stars
132
commits
Python
primary language
Aug 26, 2026
updated
croma measures how much a pathology foundation model's representation is driven by biology rather than by non-biological technical variation -- staining, scanning, tissue preparation -- across centers.
| Metric | Name | What it does |
|---|---|---|
RI | Robustness Index | Counts favourable vs. unfavourable neighbours |
MaRI | Margin-aware Robustness Index | Weights that same evidence by feature distance |
CRoMa | Cross-confounder Robustness Margin | A signed margin, with tail-aware reporting |
RI was introduced in the PathoROB study. croma provides a clean re-implementation of it, adds MaRI as its margin-aware extension, and introduces CRoMa, which overcomes limitations of both. MaRI and CRoMa are described in A distributional robustness margin for pathology foundation models.
For downstream shortcut-susceptibility experiments, croma also provides the
confounder-biased probe protocol, PathoROB's APD reduction, and normalized integrated
performance degradation (nIPD). nIPD expresses degradation relative to the
above-chance baseline performance available to lose and integrates it over Cramér's
V. See the downstream API.
pip install croma
The core package depends only on numpy, pandas, scikit-learn and tqdm. It never loads a model or reads an image -- you bring the embeddings. Add the paper-reproduction utilities with pip install "croma[repro]".
You need a manifest CSV with one row per sample, and an embeddings array of shape (N, D) whose row i is the embedding of manifest row i. Don't normalize them -- croma L2-normalizes internally and compares neighbours by cosine distance.
import numpy as np
import pandas as pd
from croma import CRoMa, MaRI, RI
manifest = pd.read_csv("manifest.csv")
features = np.load("embeddings.npy")
common = dict(confounder_column="center") # evaluation_design defaults to "all"
ri = RI.compute(features, manifest, k_candidates=[5, 11, 21], **common)
mari = MaRI.compute(features, manifest, k_candidates=[5, 11, 21], **common)
croma = CRoMa.compute(features, manifest, **common)
print(f"RI {ri.value:.3f} (k={ri.k}, support {ri.support:.1%})")
print(f"MaRI {mari.value:.3f} (tau={mari.tau:.4f}, support {mari.support:.1%})")
print(f"CRoMa {croma.value:+.3f} (lower-tail mean {croma.ltm_alpha:+.3f})")
Twenty-six encoders — twenty-five pathology foundation models and one natural-image control (†) — scored on three tile cohorts from PathoROB.
| Model | mean rank | CRoMa rank | tail rank | Camelyon CRoMa/LTM₁₀ | TCGA-4×4 CRoMa/LTM₁₀ | Tolkach-ESCA CRoMa/LTM₁₀ |
|---|---|---|---|---|---|---|
| Mascaret | 1.7 | 2.0 | 1.3 | 0.29/-0.02 | 0.27/-0.11 | 0.51/0.01 |
| RudolfV-2-S | 4.3 | 2.3 | 6.3 | 0.32/-0.02 | 0.19/-0.16 | 0.49/-0.00 |
| RudolfV-2 | 4.5 | 4.3 | 4.7 | 0.24/-0.04 | 0.17/-0.12 | 0.41/-0.01 |
| RudolfV-2-B | 5.5 | 4.7 | 6.3 | 0.24/-0.05 | 0.17/-0.14 | 0.41/-0.02 |
| CONCHv1.5 | 7.5 | 7.3 | 7.7 | 0.19/-0.14 | 0.15/-0.13 | 0.39/-0.03 |
| GenBio-PathFM | 8.5 | 7.0 | 10.0 | 0.19/-0.07 | 0.16/-0.19 | 0.39/-0.02 |
| CONCH | 9.2 | 6.0 | 12.3 | 0.20/-0.20 | 0.15/-0.15 | 0.44/-0.04 |
| Virchow2 | 9.2 | 8.3 | 10.0 | 0.20/-0.11 | 0.13/-0.17 | 0.35/-0.04 |
Top 8 of 25 ranked pathology encoders, over 3 tile cohorts; the DINOv2-B control is shown unranked in the full table. The CRoMa and tail ranks are the means of that encoder's within-cohort ranks — by median CRoMa, and by tail severity LTM₁₀ — and the mean rank averages those two. It orders the table; it does not replace them, because a strong median can hide a brittle tail and only the two columns show that — which is why each cohort shows both, median CRoMa/LTM₁₀. Bold marks the Pareto frontier: the encoders no other pathology encoder beats on both axes at once.
📊 Full panel, per-cohort detail and the distributions
RI and MaRI live in [0, 1]; above 0.5, biological evidence outweighs confounder evidence. CRoMa lives in (-1, 1) and is neutral at 0, positive when biology dominates.
Three habits will keep you out of trouble:
support next to RI and MaRI. It is the fraction of evaluation units with informative neighbours inside k: manifest samples under all, or subset occurrences under paired_2x2. A high RI over a thin support is not a strong result.tau. It defaults to None, which resolves it per model on the scale of that model's own neighbour distances. One fixed tau shared across models sharpens the margin for some and flattens it for others -- exactly the distortion MaRI exists to remove. See Choosing tau.croma.ltm_alpha is the mean of the worst 10% of samples. Pooled scores hide brittle subgroups.all, the default, scores every manifest row together and gives one number over the whole cohort at sample level; paired_2x2 controls what is compared and reports occurrence-level outputs. Includes the manifest contract and minimal valid examples for each.scripts/.If you use MaRI or CRoMa, please cite the paper — and the PathoROB study that introduced the Robustness Index these build on.
@article{grisi2026distributional,
title = {A distributional robustness margin for pathology foundation models},
author = {Grisi, Cl{\'e}ment and van der Laak, Jeroen and Litjens, Geert},
journal = {arXiv preprint arXiv:2607.25497},
year = {2026},
doi = {10.48550/arXiv.2607.25497},
url = {https://arxiv.org/abs/2607.25497}
}
To cite the software itself rather than the method, use CITATION.cff
directly; GitHub's Cite this repository button resolves to the paper above.
132 commits
Python
98.8%
JavaScript
1.0%