leaderboard / api.py
LennartPurucker's picture
Systems as their own class of entrant, and per-dataset results
9a668d8
Raw
History Blame Contribute Delete
33.8 kB
"""The Space's machine-readable API, for agents rather than browsers.
Hugging Face serves a generated ``agents.md`` for every Gradio Space
(``huggingface.co/spaces/<id>/agents.md``). It is not a repo file and cannot be
overridden: it tells an agent to read ``/gradio_api/info`` and call whatever it
finds there. So the endpoints registered here *are* the agent-facing contract.
Two rules keep that contract readable. Everything in this file is a named,
described, typed endpoint; and every UI event listener elsewhere in the app
passes ``api_visibility="private"``, so render callbacks stay out of the schema
(a public listener would otherwise show up as ``/lambda_7`` with a parameter
called ``value_11``).
The same four functions are also served as MCP tools at ``/gradio_api/mcp/``,
from these type hints and docstrings, because ``main.py`` launches with
``mcp_server=True``. Anything written here is read by an agent twice over.
Two things follow from how an MCP client actually reads this, and both shape the
wording below.
*Each tool description has to introduce the subject.* Gradio 6 offers no
server-level instructions field, so there is nowhere to say once what TabArena
is; a client may surface a single tool with no sibling for context. So every
``api_description`` names the domain (predicting a target column from
structured, rows-and-columns data) and the methods people actually ask about by
name, and spells out the questions the tool answers. A description that only
says "TabArena results" is invisible to an agent whose user asked about TabPFN
or about which model to use on a CSV.
*Gradio keeps only the first line of each ``Args:`` entry.*
``utils.get_function_description`` splits on the first colon per line and drops
continuations, so a wrapped parameter description reaches the schema cut off
mid-sentence. Every entry below is therefore one long line, however wide it
reads in source.
Records are the published CSVs with agent-friendly keys, not a second source of
truth: the same files the site reads, the same model-cell parser the table uses.
For bulk access, skip the queue and fetch the CSV directly (see
:func:`list_leaderboards`, which hands out the URL template).
"""
from __future__ import annotations
import os
import re
from itertools import combinations
from pathlib import Path
from typing import Any, Literal, get_args
import gradio as gr
import pandas as pd
from constants import Constants
from data_loading import (
BEYOND_DATA_DIR,
BEYOND_SUBSET_LABELS,
DATA_DIR,
DATASET_LABELS,
SYSTEM_CATEGORY_LABELS,
BeyondSubset,
LBContainer,
Subset,
TASK_LABELS,
entrants_key,
entrants_name,
entrants_note,
parse_model,
)
# Axis values as Literals, so /gradio_api/info carries an enum per parameter.
# That schema is the only place an agent reliably learns the valid values: a
# rejected call comes back over the REST route as a bare `event: error` with a
# null payload, message dropped. _validate_axes below keeps these in step with
# the label dicts that define the data layout.
# A leaderboard row is either a single model (evaluated in its default / tuned /
# tuned+ensembled variants) or a whole system such as AutoGluon, which the artifacts mark
# with method_class="system". Agents conflate the two otherwise, and "the best tabular
# model" answered with an AutoML system is a wrong answer, so the endpoints return models
# only unless asked for something else.
KindAxis = Literal["models", "systems", "all"]
SYSTEM_TYPE = Constants.system
# Which benchmark a cross-benchmark endpoint should read.
BenchmarkAxis = Literal["tabarena", "beyondarena"]
# What "best" means. The leaderboards rank by Elo; the rest are complementary.
QualityAxis = Literal["elo", "score", "rank", "harmonic_rank", "improvability_pct"]
_QUALITY_HIGHER_IS_BETTER = {
"elo": True,
"score": True,
"rank": False,
"harmonic_rank": False,
"improvability_pct": False,
}
# What "cheap" means, and the record key each maps to.
CostAxis = Literal["predict_time", "train_time"]
_COST_KEY = {
"predict_time": "median_predict_time_s_per_1k",
"train_time": "median_train_time_s_per_1k",
}
_COST_UNITS = {
"median_predict_time_s_per_1k": "seconds per 1000 rows to predict",
"median_train_time_s_per_1k": "seconds per 1000 rows to train",
}
# Which field the numbers were computed against. Not a filter: each pool is its own
# evaluation, because Elo is pairwise over the participants and improvability is measured
# against the best of them. One value per combination of the system categories, so `models`
# is models only and `open_llm_api` is everything; `llm` is models plus LLM-based systems
# without the plain open-source ones.
EntrantsAxis = Literal[
"models",
"open",
"llm",
"api",
"open_llm",
"open_api",
"llm_api",
"open_llm_api",
]
TasksAxis = Literal["all", "classification", "regression", "binary", "multiclass"]
DatasetsAxis = Literal["all", "small", "medium"]
ImputationAxis = Literal["yes", "no"]
SplitsAxis = Literal["all", "lite"]
BeyondSubsetAxis = Literal[
"full",
"random",
"temporal",
"grouped",
"tiny",
"small",
"medium",
"large",
"low-dim",
"high-dim",
"text",
"high-cardinality",
]
IMPUTATION_VALUES = list(get_args(ImputationAxis))
SPLITS_VALUES = list(get_args(SplitsAxis))
KIND_VALUES = list(get_args(KindAxis))
def _validate_axes() -> None:
"""Fail at import if an axis Literal has drifted from the data layout."""
for name, literal, allowed in (
("TasksAxis", TasksAxis, TASK_LABELS),
("DatasetsAxis", DatasetsAxis, DATASET_LABELS),
("BeyondSubsetAxis", BeyondSubsetAxis, BEYOND_SUBSET_LABELS),
):
if set(get_args(literal)) != set(allowed):
raise RuntimeError(
f"api.{name} is out of sync with data_loading: "
f"{sorted(set(get_args(literal)) ^ set(allowed))} on one side only."
)
def _validate_entrants_axis() -> None:
"""Fail at import if EntrantsAxis has drifted from the category combinations."""
keys = list(SYSTEM_CATEGORY_LABELS)
expected = {entrants_key(c) for size in range(len(keys) + 1) for c in combinations(keys, size)}
if set(get_args(EntrantsAxis)) != expected:
raise RuntimeError(
"api.EntrantsAxis is out of sync with data_loading.SYSTEM_CATEGORY_LABELS: "
f"{sorted(set(get_args(EntrantsAxis)) ^ expected)} on one side only."
)
_validate_axes()
_validate_entrants_axis()
# The Space serving this app; HF sets SPACE_ID in the container.
SPACE_ID = os.environ.get("SPACE_ID", "TabArena/leaderboard")
_RAW_URL = f"https://huggingface.co/spaces/{SPACE_ID}/resolve/main"
# Column headers carry a sort-direction marker for the table widget; it is not
# part of the data, so it is stripped before the header becomes a JSON key.
_DIRECTION_RE = re.compile(r"\s*\[[⬆⬇]️?\]$")
_KEY_OVERRIDES = {"#": "position", "TypeName": "type_name"}
# Bulk-download URLs, templated on the same rel_path the app reads from so the
# two cannot drift.
_TABARENA_CSV_TEMPLATE = (
f"{_RAW_URL}/{DATA_DIR.name}/"
+ Subset("{entrants}", "{imputation}", "{splits}", "{tasks}", "{datasets}").rel_path
+ "/website_leaderboard.csv"
)
_BEYOND_CSV_TEMPLATE = (
f"{_RAW_URL}/{BEYOND_DATA_DIR.name}/"
+ BeyondSubset("{subset}").rel_path
+ "/website_leaderboard.csv"
)
def _api_key(column: str) -> str:
"""Turn a CSV header into a JSON key: ``Improvability (%) [⬇️]`` -> ``improvability_pct``."""
if column in _KEY_OVERRIDES:
return _KEY_OVERRIDES[column]
name = _DIRECTION_RE.sub("", column).replace("%", "pct").replace("/", "_per_")
return re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_").lower()
def _records(df: pd.DataFrame) -> list[dict[str, Any]]:
"""One JSON record per leaderboard row."""
# NaN is not JSON; None is. Casting to object first keeps ints from turning
# into floats on the way through.
df = df.astype(object).where(pd.notna(df), None)
records = []
for row in df.to_dict("records"):
record = {_api_key(key): value for key, value in row.items()}
# The Model cell is markdown (`[TabFM (default)](url)`); agents want the
# three parts separately.
name, variant, url = parse_model(str(row["Model"]))
record["model"] = name
record["variant"] = variant
record["model_url"] = url
record["verified"] = str(row.get("Verified") or "").strip() == "✔️"
record["kind"] = "system" if row.get("TypeName") == SYSTEM_TYPE else "model"
# Semicolon-joined upstream; agents want a list. Empty for every model, and for a
# system that is open-source, local and LLM-free.
record["tags"] = [t for t in str(row.get("Tags") or "").split(";") if t]
records.append(record)
return records
def _select_kind(records: list[dict[str, Any]], kind: str) -> list[dict[str, Any]]:
if kind == "all":
return records
wanted = "system" if kind == "systems" else "model"
return [r for r in records if r["kind"] == wanted]
def _check(axis: str, value: str, allowed: list[str]) -> None:
if value not in allowed:
raise gr.Error(f"Unknown {axis}={value!r}. Valid values: {', '.join(allowed)}.")
def _pareto_frontier(
records: list[dict[str, Any]], quality: str, cost_key: str
) -> list[dict[str, Any]]:
"""The non-dominated records: nothing is both better and cheaper.
Walks cheapest-first and keeps a record only when it beats every cheaper one on
quality, which is the 2-D skyline. Ties on cost keep the better-quality record.
"""
higher_is_better = _QUALITY_HIGHER_IS_BETTER[quality]
usable = [
r
for r in records
if isinstance(r.get(quality), (int, float)) and isinstance(r.get(cost_key), (int, float))
]
ordered = sorted(
usable,
key=lambda r: (r[cost_key], -r[quality] if higher_is_better else r[quality]),
)
frontier: list[dict[str, Any]] = []
best = None
for record in ordered:
value = record[quality]
if best is None or (value > best if higher_is_better else value < best):
frontier.append(record)
best = value
return frontier
def _best(records: list[dict[str, Any]], quality: str) -> dict[str, Any] | None:
"""The single best record on `quality`, ignoring rows that lack it."""
usable = [r for r in records if isinstance(r.get(quality), (int, float))]
if not usable:
return None
return max(
usable,
key=lambda r: r[quality] if _QUALITY_HIGHER_IS_BETTER[quality] else -r[quality],
)
def _brief(record: dict[str, Any] | None, quality: str, cost_key: str) -> dict[str, Any] | None:
"""The fields worth returning for one model in a Pareto answer."""
if record is None:
return None
return {
"model": record.get("model"),
"variant": record.get("variant"),
"kind": record.get("kind"),
"type_name": record.get("type_name"),
"verified": record.get("verified"),
"model_url": record.get("model_url"),
quality: record.get(quality),
cost_key: record.get(cost_key),
"median_train_time_s_per_1k": record.get("median_train_time_s_per_1k"),
"median_predict_time_s_per_1k": record.get("median_predict_time_s_per_1k"),
}
def _describe(name: str | None, variant: str | None) -> str:
return f"{name} ({variant})" if variant else str(name)
def _load(data_root: Path, subset: Subset | BeyondSubset) -> list[dict[str, Any]]:
lb = LBContainer(data_root=data_root, subset=subset, name="")
try:
return _records(lb.load_df())
except FileNotFoundError:
raise gr.Error(
f"No results for {subset.rel_path!r} in this deployment. "
"Call list_leaderboards for the subsets that are available."
) from None
def get_tabarena_leaderboard(
tasks: TasksAxis = "all",
datasets: DatasetsAxis = "all",
kind: KindAxis = "models",
imputation: ImputationAxis = "yes",
splits: SplitsAxis = "all",
entrants: EntrantsAxis = "models",
) -> list[dict[str, Any]]:
"""Ranked results for one subset of TabArena, the IID tabular machine-learning benchmark.
Each row is a method evaluated on 51 curated datasets whose task is to predict a target
column from structured, rows-and-columns data: tabular foundation models (TabPFN, TabICL,
TabDPT, Mitra, TabM), gradient-boosted trees (LightGBM, XGBoost, CatBoost), neural networks
(RealMLP, ModernNCA) and AutoML systems (AutoGluon).
Args:
tasks: Restrict to a task type. ``all`` (default), ``classification``, ``regression``, ``binary`` or ``multiclass``.
datasets: Restrict by dataset size. ``all`` (default), ``small`` (up to 2500 rows) or ``medium`` (up to 100k rows).
kind: What to rank. ``models`` (default) returns individual models such as TabPFN, RealMLP or LightGBM; ``systems`` returns whole AutoML systems such as AutoGluon, which tune and ensemble many models inside their own budget and so are not comparable to a single model; ``all`` returns both, as the website's table does. Ask for ``models`` when the question is "which model should I use" and ``systems`` when it is "which AutoML framework should I use". Every record also carries its own ``kind``.
imputation: Whether to keep methods with incomplete coverage. ``yes`` (default, and what the website shows) includes models that cannot run on every dataset, filling their missing runs with a default RandomForest; ``no`` drops those models entirely.
splits: Which evaluation protocol. ``all`` (default) is the full repeated cross-validation; ``lite`` is the cheaper single-split TabArena-Lite protocol.
entrants: Which field the numbers were computed against, not a row filter: Elo is a pairwise rating over whoever competed and Improvability is the gap to the best of them, so each pool is a separate published evaluation and switching re-ranks everything. ``models`` (default) is individual models only; ``open`` adds open-source local systems such as AutoGluon; ``llm`` adds systems with an LLM in the loop; ``api`` adds systems behind a closed-source API; the compound values (``open_llm``, ``open_api``, ``llm_api``, ``open_llm_api``) admit those categories together.
Returns:
One record per model variant, best first, with ``model``, ``variant``,
``elo``, ``score``, ``rank``, ``median_train_time_s_per_1k`` and the
remaining leaderboard columns. ``variant`` is ``default``, ``tuned`` or
``tuned + ensembled``, and empty for systems such as AutoGluon, which
are whole pipelines rather than one model's variant (``kind`` identifies
those, and ``tags`` lists any caveats such as ``with-llm`` or
``closed-source-api``). Note the website's variant filter groups them with
the tuned ensembles instead.
"""
_check("tasks", tasks, list(TASK_LABELS))
_check("datasets", datasets, list(DATASET_LABELS))
_check("kind", kind, KIND_VALUES)
_check("imputation", imputation, IMPUTATION_VALUES)
_check("splits", splits, SPLITS_VALUES)
_check("entrants", entrants, list(get_args(EntrantsAxis)))
subset = Subset(
entrants=entrants, imputation=imputation, splits=splits, tasks=tasks, datasets=datasets
)
return _select_kind(_load(DATA_DIR, subset), kind)
def get_beyondarena_leaderboard(
subset: BeyondSubsetAxis = "full", kind: KindAxis = "models"
) -> list[dict[str, Any]]:
"""Ranked BeyondArena results for one subset, on the recommended core protocol.
BeyondArena runs the same tabular methods (TabPFN, LightGBM, CatBoost, AutoGluon and the
rest) where the IID assumption does not hold: random, temporal and grouped splits, across
dataset sizes and feature types. It has a single subset axis rather than TabArena's grid.
Args:
subset: Which slice of BeyondArena to read. ``full`` (default) is everything; ``random`` / ``temporal`` / ``grouped`` pick a split type; ``tiny`` / ``small`` / ``medium`` / ``large`` pick a dataset size; ``low-dim`` / ``high-dim`` / ``text`` / ``high-cardinality`` pick a feature profile.
kind: What to rank: ``models`` (default), ``systems`` for whole AutoML systems, or ``all`` for both. See `get_tabarena_leaderboard` for why the two are not directly comparable.
Returns:
One record per model variant, best first, in the same shape as
`get_tabarena_leaderboard`.
"""
_check("subset", subset, list(BEYOND_SUBSET_LABELS))
_check("kind", kind, KIND_VALUES)
return _select_kind(_load(BEYOND_DATA_DIR, BeyondSubset(subset=subset)), kind)
def get_pareto_frontier(
benchmark: BenchmarkAxis = "tabarena",
quality: QualityAxis = "elo",
cost: CostAxis = "predict_time",
kind: KindAxis = "models",
tasks: TasksAxis = "all",
datasets: DatasetsAxis = "all",
beyond_subset: BeyondSubsetAxis = "full",
max_train_time_s_per_1k: float = 0.0,
max_predict_time_s_per_1k: float = 0.0,
imputation: ImputationAxis = "yes",
splits: SplitsAxis = "all",
entrants: EntrantsAxis = "models",
) -> dict[str, Any]:
"""The accuracy-versus-time trade-off, for answering "which tabular model should I use".
The top of a leaderboard is only one answer, and often the wrong one: the
highest-Elo model can be orders of magnitude slower than one a hair behind it.
This returns the whole trade-off: the outright best, the models that nothing
beats on both axes at once, and the best model that fits a time budget. It
covers tabular foundation models (TabPFN, TabICL, TabDPT), boosted trees
(LightGBM, XGBoost, CatBoost), neural networks (RealMLP, TabM) and AutoML
systems (AutoGluon), all on predicting a target column from structured data.
Args:
benchmark: Which benchmark to read: ``tabarena`` (default, IID splits) or ``beyondarena`` (temporal, grouped and other non-IID splits).
quality: What "better" means: ``elo`` (default) or ``score``, where higher wins, or ``rank`` / ``harmonic_rank`` / ``improvability_pct``, where lower wins.
cost: What "cheaper" means: ``predict_time`` (default, what matters for serving) or ``train_time`` (what matters for retraining).
kind: What to consider: ``models`` (default), ``systems`` for whole AutoML systems, or ``all``. See `get_tabarena_leaderboard`.
tasks: TabArena task subset (``all``, ``classification``, ``regression``, ``binary``, ``multiclass``); ignored when `benchmark` is ``beyondarena``.
datasets: TabArena dataset-size subset (``all``, ``small``, ``medium``); ignored when `benchmark` is ``beyondarena``.
beyond_subset: BeyondArena subset (``full``, ``temporal``, ``grouped``, a size or a feature profile); ignored when `benchmark` is ``tabarena``.
max_train_time_s_per_1k: Ceiling on median train seconds per 1000 rows; 0 (default) means no limit. Use it for "what can I afford to retrain".
max_predict_time_s_per_1k: Ceiling on median predict seconds per 1000 rows; 0 (default) means no limit. Use it for "what is the best model I can afford to serve".
imputation: TabArena only; ``yes`` (default) keeps models with incomplete dataset coverage, ``no`` drops them. See `get_tabarena_leaderboard`.
splits: TabArena only; ``all`` (default) is repeated cross-validation, ``lite`` the single-split protocol. See `get_tabarena_leaderboard`.
entrants: TabArena only; which field the numbers were computed against, ``models`` by default. See `get_tabarena_leaderboard`.
Returns:
``best_overall`` (the outright leader, ignoring cost), ``frontier`` (the
non-dominated models, cheapest first, each with ``speedup_vs_best`` and
``quality_gap_vs_best``), ``best_within_limits`` (the leader among models
satisfying the time ceilings, absent when no ceiling was given),
``dominated_count``, and a ``summary`` sentence to relay.
"""
_check("benchmark", benchmark, list(get_args(BenchmarkAxis)))
_check("quality", quality, list(_QUALITY_HIGHER_IS_BETTER))
_check("cost", cost, list(_COST_KEY))
_check("kind", kind, KIND_VALUES)
if benchmark == "beyondarena":
records = get_beyondarena_leaderboard(subset=beyond_subset, kind=kind)
where = f"BeyondArena ({beyond_subset}, core protocol)"
else:
records = get_tabarena_leaderboard(
tasks=tasks,
datasets=datasets,
kind=kind,
imputation=imputation,
splits=splits,
entrants=entrants,
)
where = f"TabArena ({tasks} tasks, {datasets} datasets, {entrants})"
cost_key = _COST_KEY[cost]
best_overall = _best(records, quality)
frontier = _pareto_frontier(records, quality, cost_key)
limits = {
"median_train_time_s_per_1k": max_train_time_s_per_1k,
"median_predict_time_s_per_1k": max_predict_time_s_per_1k,
}
active_limits = {key: value for key, value in limits.items() if value and value > 0}
within = records
for key, ceiling in active_limits.items():
within = [
r for r in within if isinstance(r.get(key), (int, float)) and r[key] <= ceiling
]
best_within = _best(within, quality) if active_limits else None
entries = []
for record in frontier:
entry = _brief(record, quality, cost_key)
if best_overall and isinstance(best_overall.get(cost_key), (int, float)):
reference = best_overall[cost_key]
entry["speedup_vs_best"] = (
round(reference / record[cost_key], 2) if record[cost_key] else None
)
entry["quality_gap_vs_best"] = round(
abs(best_overall[quality] - record[quality]), 4
)
entries.append(entry)
return {
"benchmark": benchmark,
"subset": where,
"quality": quality,
"higher_is_better": _QUALITY_HIGHER_IS_BETTER[quality],
"cost": cost_key,
"kind": kind,
"best_overall": _brief(best_overall, quality, cost_key),
"frontier": entries,
"dominated_count": max(0, len(records) - len(frontier)),
"limits": active_limits or None,
"best_within_limits": _brief(best_within, quality, cost_key),
"summary": _pareto_summary(
where, quality, cost_key, best_overall, entries, active_limits, best_within
),
}
def _pareto_summary(
where: str,
quality: str,
cost_key: str,
best_overall: dict[str, Any] | None,
frontier: list[dict[str, Any]],
limits: dict[str, float],
best_within: dict[str, Any] | None,
) -> str:
"""One paragraph an agent can relay instead of reciting the whole frontier."""
if best_overall is None:
return f"No {quality} values are available for {where}."
lead = _describe(best_overall.get("model"), best_overall.get("variant"))
units = _COST_UNITS.get(cost_key, cost_key)
parts = [
f"On {where}, the best {quality} is {lead} at {best_overall[quality]} "
f"({best_overall.get(cost_key)} {units})."
]
if len(frontier) > 1:
cheapest = frontier[0]
parts.append(
f"{len(frontier)} models are on the accuracy/time frontier. The cheapest, "
f"{_describe(cheapest.get('model'), cheapest.get('variant'))}, is "
f"{cheapest.get('speedup_vs_best')}x faster but {cheapest.get('quality_gap_vs_best')} "
f"{quality} behind."
)
# The trade-off worth naming is the smallest sacrifice that still buys a real
# speedup, not the biggest speedup (that is just the cheapest model again).
bargains = [
entry
for entry in frontier
if (entry.get("speedup_vs_best") or 0) >= 2 and entry.get("quality_gap_vs_best")
]
if bargains:
pick = min(bargains, key=lambda e: e["quality_gap_vs_best"])
parts.append(
# "at a cost of" rather than "less", which reads backwards for the
# metrics where lower is better.
f"The best compromise is {_describe(pick.get('model'), pick.get('variant'))}: "
f"{pick.get('speedup_vs_best')}x faster at a cost of "
f"{pick.get('quality_gap_vs_best')} {quality}."
)
if limits:
stated = ", ".join(f"{key} <= {value}" for key, value in limits.items())
if best_within:
parts.append(
f"Within {stated}, the best is "
f"{_describe(best_within.get('model'), best_within.get('variant'))} at "
f"{best_within[quality]}."
)
else:
parts.append(f"No model satisfies {stated}.")
return " ".join(parts)
def list_leaderboards() -> dict[str, Any]:
"""Describe the available leaderboards, their subset axes, and the bulk-download URLs.
TabArena benchmarks tabular machine learning: predicting a target column from
structured, rows-and-columns data. Call this first: it lists the valid argument
values for the other endpoints and the record keys they return. It also gives the
raw CSV URL template for each benchmark, which serves the identical numbers over
plain HTTP with no queue or session, and is the better choice for reading many
subsets.
"""
default_rows = _load(DATA_DIR, Subset())
return {
"about": (
"TabArena benchmarks tabular machine learning: predicting a target column from "
"structured, rows-and-columns data such as CSV files, spreadsheets, dataframes "
"and database tables. Entrants range from tabular foundation models (TabPFN, "
"TabICL, TabDPT, Mitra, TabM) through gradient-boosted trees (LightGBM, XGBoost, "
"CatBoost) and neural networks (RealMLP, ModernNCA) to AutoML systems "
"(AutoGluon). Every method is run under one protocol with its training and "
"inference time measured, so accuracy and cost can be read together."
),
"leaderboards": [
{
"name": "tabarena",
"endpoint": "/get_tabarena_leaderboard",
"description": (
"Tabular ML on 51 curated datasets with IID (random) splits, ranked by "
"Elo. The default question: which method predicts best."
),
"axes": {
"tasks": list(TASK_LABELS),
"datasets": list(DATASET_LABELS),
"kind": KIND_VALUES,
"imputation": IMPUTATION_VALUES,
"splits": SPLITS_VALUES,
"entrants": list(get_args(EntrantsAxis)),
},
"csv_url_template": _TABARENA_CSV_TEMPLATE,
},
{
"name": "beyondarena",
"endpoint": "/get_beyondarena_leaderboard",
"description": (
"Tabular ML beyond the IID assumption: random, temporal and grouped "
"splits across dataset sizes and feature types. Read this one when the "
"data shifts over time or arrives in groups."
),
"axes": {"subset": list(BEYOND_SUBSET_LABELS), "kind": KIND_VALUES},
"csv_url_template": _BEYOND_CSV_TEMPLATE,
},
],
"record_keys": sorted(default_rows[0]) if default_rows else [],
"kinds": {
"models": "Individual models (TabPFN, RealMLP, LightGBM, ...). The default.",
"systems": (
f"Whole systems (AutoGluon, TabFM+, hosted APIs, ...), marked '{SYSTEM_TYPE}' "
"in the artifacts. They tune and ensemble many models inside their own budget, "
"so ranking them against a single model compares different things. Only present "
"when `entrants` admits them; see the `entrants` axis."
),
"all": "Both, as the website's table shows them.",
},
"entrants": {
key: f"{entrants_name(key)}. {entrants_note(key)}" for key in get_args(EntrantsAxis)
},
"tags": {
"with-llm": (
"An LLM is involved somewhere in this system, possibly as an agent. Its results "
"depend on a model that can change and whose training data cannot be audited."
),
"closed-source-api": (
"The system runs behind a remote API whose internals cannot be inspected, so the "
"numbers are not reproducible from source."
),
},
"choosing_a_model": (
"For 'which model should I use', call get_pareto_frontier rather than reading the "
"top row here: it returns the accuracy/time trade-off, since the highest-Elo model "
"is often orders of magnitude slower than one just behind it. It also takes "
"train- and predict-time budgets."
),
"notes": (
"Scores are read from the published artifacts, so they match the website exactly. "
"Higher is better for elo and score; lower is better for rank, harmonic_rank, "
"improvability_pct and the time columns. Every record carries a `kind` of "
"'model' or 'system'; the get_* endpoints return models only unless asked."
),
}
# Every description opens by naming the subject, because an MCP client may show one tool with
# no sibling for context and Gradio has no server-level instructions field. See the module
# docstring.
_WHAT_IS_TABARENA = (
"TabArena is a living benchmark for tabular machine learning: predicting a target column "
"from structured, rows-and-columns data (CSV files, spreadsheets, dataframes, database "
"tables). It ranks tabular foundation models (TabPFN, TabICL, TabDPT, Mitra, TabM), "
"gradient-boosted trees (LightGBM, XGBoost, CatBoost), neural networks (RealMLP, "
"ModernNCA) and AutoML systems (AutoGluon) by Elo over 51 curated datasets, with "
"measured training and inference time for each."
)
def register_api() -> None:
"""Register the endpoints. Call inside the app's ``gr.Blocks`` context."""
gr.api(
list_leaderboards,
api_name="list_leaderboards",
api_description=(
f"{_WHAT_IS_TABARENA} This endpoint is the index: it lists the available "
"leaderboards, the valid subset values for every other endpoint, the difference "
"between a model and a whole system, the keys each record carries, and the raw "
"CSV URLs for bulk download. Call it before the get_* endpoints when you are "
"unsure which arguments exist."
),
)
gr.api(
get_tabarena_leaderboard,
api_name="get_tabarena_leaderboard",
api_description=(
f"{_WHAT_IS_TABARENA} This endpoint returns the ranked results (Elo, score, "
"ranks, train and predict time) for one subset of the IID benchmark, selected by "
"task type, dataset size, imputation and split protocol. Use it to look up where "
"a named method stands, to compare two of them (\"is TabPFN better than "
"LightGBM on small data\"), or to report the current state of the art on tabular "
"data. Pass kind='models' (the default) for individual models, kind='systems' "
"for whole AutoML systems such as AutoGluon, or kind='all' for both: a question "
"about the best model wants 'models', one about the best AutoML framework wants "
"'systems'."
),
)
gr.api(
get_beyondarena_leaderboard,
api_name="get_beyondarena_leaderboard",
api_description=(
"Ranked results for BeyondArena, the companion benchmark that measures the same "
"tabular models (TabPFN, LightGBM, CatBoost, AutoGluon and the rest) where the "
"IID assumption does not hold: random, temporal and grouped splits, sliced by "
"dataset size and feature type. Use it whenever the question involves "
"distribution shift, time-ordered rows, leakage-prone grouped splits, or how "
"well a tabular model generalizes beyond a random train/test split. Takes the "
"same kind='models' | 'systems' | 'all' distinction as the TabArena endpoint."
),
)
gr.api(
get_pareto_frontier,
api_name="get_pareto_frontier",
api_description=(
"The accuracy-versus-time trade-off among tabular models (TabPFN, TabICL, "
"LightGBM, XGBoost, CatBoost, RealMLP, AutoGluon and the rest), measured on "
"TabArena or BeyondArena. Returns the outright best model, the models nothing "
"beats on both accuracy and speed at once (each with its speedup and accuracy "
"gap versus the leader), and the best model that fits a train- or predict-time "
"budget. Prefer it over the leaderboard endpoints for 'which model should I use "
"on my tabular data', 'what is the best tabular model', 'what is a faster "
"alternative to TabPFN', or anything about cost of training or serving: the top "
"of the leaderboard is often orders of magnitude slower than a model just behind "
"it. Returns a summary sentence you can relay."
),
)