Evaluate a run¶
You have a run output, a joint embedding, a set of cell-type labels, a
clustering. Now you want a number. mtb.evaluate takes that output and
returns the scIB metrics that the scMultiBench paper reports, in a
metric.csv-shaped frame you can read, save, or feed straight into a bubble
plot.
TL;DR
import multibench as mtb
metrics = mtb.evaluate(
result.output, # embedding (ndarray or .h5 path)
category="vertical",
task="clustering", # ARI, NMI, ASW, iASW, iF1, cLISI
labels="cty.csv", # cell-type labels (required)
# clustering= is optional - omitted, an optimal-resolution Leiden
# clustering is derived from the embedding automatically
)
print(metrics) # index = metric, column "Value"
What evaluate does¶
mtb.evaluate(output, category, task="clustering", labels=None,
clustering=None, batch=None, metric_set="scib",
slow_metrics=False) -> pd.DataFrame
Under the hood it assembles a small AnnData carrying your embedding in
obsm["X_emb"], with celltype, cluster, and batch as observation
columns, builds a neighbour graph on the embedding, and runs the
scIB metric functions over it. The result is a
DataFrame indexed by metric name with a single "Value" column, the same
shape as the metric.csv files the benchmark ships.
task |
Metrics computed |
|---|---|
"clustering" |
ARI, NMI, ASW, iASW, iF1, cLISI |
"batch" |
ASW_batch, GC (graph connectivity), iLISI (+ kBET with slow_metrics=True) |
"all" |
both groups above |
The category argument (vertical, diagonal, mosaic, cross) is accepted
for API stability and future per-category metric dispatch, but is not yet used
in v1, pass the category your run belongs to and it will keep working when
dispatch lands.
Required inputs¶
evaluate is honest about what each metric group needs, and raises early if
something is missing.
labels is required; clustering is optional — clustering quality is
measured by comparing the predicted clusters against ground-truth
cell types.
Every input can be passed either as a path or as an in-memory
numpy.ndarray. Paths are read with the benchmark's own conventions:
output/clustering→ an HDF5 file (embedding from datasetdata, oriented as cells × dims; clustering from/obs/cluster_leiden).labels/batch→ a single-column CSV with one header line (typicallyx), one value per cell factorised to integer codes.
Missing labels raises
Calling evaluate with task="clustering" and no labels raises
ValueError (omitting clustering is fine - it is derived from the
embedding). Calling task="batch" or task="all" without batch
raises ValueError too. These are programmer errors, not missing
optional metrics, see graceful degradation below for the latter.
Graceful degradation¶
A few scIB metrics depend on external machinery that is not always present.
Rather than failing the whole evaluation when one optional metric can't run,
evaluate computes each metric defensively: if it raises, the metric is
recorded as NaN with a warning, and every other metric still returns.
| Metric | Needs | If unavailable |
|---|---|---|
cLISI, iLISI |
a compiled LISI binary | NaN + warning |
kBET |
rpy2 (R bridge) |
NaN + warning |
So on a machine without the LISI binary, a task="clustering" call still gives
you real ARI, NMI, ASW, iASW, and iF1, only cLISI comes back NaN.
Reading the result¶
>>> metrics
Value
ARI 0.812
NMI 0.847
ASW 0.591
iASW 0.604
iF1 0.733
cLISI NaN # LISI binary not available on this host
It's a plain DataFrame, so you can save it as the benchmark's metric.csv
directly:
Closing the loop: tidy → bubble plot¶
The wide "Value" frame is the right shape for one run. To compare across
methods and datasets, and to draw the scIB-style bubble table from the paper,
reshape it to the long frame that plot.bubble and load_results use, with
mtb.eval.to_long:
mtb.eval.to_long(value_df, method, dataset, category) -> pd.DataFrame
# columns: metric, value, method, dataset, category
to_long also maps each metric name to its canonical form, so the result lines
up with mtb.load_results and can be concatenated with published tables.
import pandas as pd
import multibench as mtb
# one run -> one long frame
long_df = mtb.eval.to_long(
metrics,
method="SCALEX",
dataset="D35",
category="diagonal",
)
# stack several runs, then draw the bubble table
all_runs = pd.concat([long_df, other_long_df])
fig = mtb.plot.bubble(
all_runs,
metrics=["ARI", "NMI", "ASW"],
aggregate="dataset", # "summary" averages ranks across datasets
title="Diagonal integration",
save="bubble.pdf",
)
plot.bubble returns a Matplotlib Figure (circle radius tracks rank, fill
tracks value, with an "overall" column). See the
plotting tutorial for the full set of options, and
load_results if you want to drop your run alongside the
published benchmark tables.
v1 supports metric_set="scib" only
Passing any other metric_set, or a task outside
{clustering, batch, all}, raises NotImplementedError. Additional metric
sets are planned but not yet wired.
Next: turn these numbers into a figure in Plot a bubble table.