Mike0021 commited on
Commit
35c63f8
·
verified ·
1 Parent(s): 25603e6

init tabfm small data arena

Browse files
Files changed (5) hide show
  1. README.md +15 -7
  2. TABFM_TASK.txt +138 -0
  3. app.py +1700 -0
  4. requirements.txt +9 -0
  5. rollout.jsonl +0 -0
README.md CHANGED
@@ -1,13 +1,21 @@
1
  ---
2
- title: Tabfm Small Data Champion
3
- emoji: 🐢
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TabFM Small Data Champion
3
+ emoji: 🏆
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.19.0
 
8
  app_file: app.py
9
+ short_description: TabFM vs classic ML on tiny tables
10
+ startup_duration_timeout: 1h
11
  ---
12
 
13
+ # TabFM Small Data Champion
14
+
15
+ Custom `gr.Server` benchmark arena comparing `google/tabfm-1.0.0-pytorch`
16
+ against XGBoost, LightGBM, Random Forest, and a linear baseline on small
17
+ tabular datasets.
18
+
19
+ The app pre-computes benchmark rows at startup, serves a custom dark frontend,
20
+ and exposes Gradio API endpoints for benchmark results, dataset metadata, model
21
+ metadata, and a single live benchmark run.
TABFM_TASK.txt ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabFM: Small Data Champion — Benchmark Arena
2
+
3
+ ## CONCEPT
4
+ A benchmark arena where TabFM (Google's tabular foundation model) goes head-to-head against XGBoost, LightGBM, and Random Forest on small tabular datasets. The point: prove TabFM dominates when you have almost no data (10-1000 samples). Visualize the accuracy gap shrinking as data increases. Leaderboard with win rates.
5
+
6
+ Use a gr.Server custom frontend (like the OlmoEarth Space and Gemma Dashboard pattern) — dark, modern, full-viewport, NOT standard Gradio columns.
7
+
8
+ ## MODEL
9
+ - TabFM: google/tabfm-1.0.0-pytorch (PyTorch backend)
10
+ - GitHub: https://github.com/google-research/tabfm
11
+ - Install: pip install -e .[pytorch] (clone the repo)
12
+ - scikit-learn compatible: TabFMClassifier, TabFMRegressor
13
+ - Zero-shot: reads training data as context, no dataset-specific training
14
+
15
+ ## BENCHMARK DESIGN
16
+
17
+ ### Datasets (built-in, no upload needed)
18
+ Use well-known small tabular datasets from sklearn/openml:
19
+ - Titanic (survival classification)
20
+ - California Housing (regression, subsample to small sizes)
21
+ - Iris (classification)
22
+ - Wine quality (classification)
23
+ - Breast Cancer (classification)
24
+ - Telco Churn (classification)
25
+ - Adult Income (classification, subsample)
26
+
27
+ ### Competitors
28
+ 1. TabFM (zero-shot, no training)
29
+ 2. XGBoost (trained on the same data)
30
+ 3. LightGBM (trained on the same data)
31
+ 4. Random Forest (trained on the same data)
32
+ 5. Logistic/Linear Regression (baseline)
33
+
34
+ ### Sample Size Variations
35
+ For each dataset, run benchmarks at:
36
+ - 10 samples
37
+ - 50 samples
38
+ - 100 samples
39
+ - 500 samples
40
+ - 1000 samples
41
+ - 5000 samples (if dataset allows)
42
+
43
+ Split: use 80% for train, 20% for test. For TabFM, the train portion is the "context."
44
+
45
+ ### Metrics
46
+ - Classification: accuracy, F1, ROC-AUC
47
+ - Regression: RMSE, R², MAE
48
+ - Inference time
49
+ - "Training" time (for TabFM this is just preprocessing)
50
+
51
+ ### Visualization
52
+ 1. **Line chart**: X-axis = sample size, Y-axis = accuracy. One line per model. Shows TabFM winning at small sizes, traditional models catching up as data grows.
53
+ 2. **Leaderboard table**: Rank by accuracy at each sample size. Win rates (% of datasets where each model wins).
54
+ 3. **Bar chart**: Win rate per model across all datasets at each sample size.
55
+ 4. **Dataset selector**: Pick a dataset to see detailed results.
56
+ 5. **Model cards**: Click a model to see its stats (inference time, etc.)
57
+
58
+ ## TECH APPROACH
59
+
60
+ ### Pre-compute results
61
+ Run all benchmarks at startup (or cache them). The datasets are small so this is fast — TabFM inference on 10-1000 samples takes seconds, XGBoost/LightGBM/RF are also fast on small data.
62
+
63
+ Store results as JSON in the app, serve via API endpoints.
64
+
65
+ ### gr.Server pattern
66
+ - app.py: FastAPI backend with @app.api() endpoints
67
+ - Custom HTML/CSS/JS frontend (dark theme, full viewport)
68
+ - Three.js not needed — use Chart.js or D3.js from CDN for the charts
69
+ - static/index.html (or inline in app.py)
70
+
71
+ ### API Endpoints
72
+ 1. `get_benchmark_results()` — returns all pre-computed results as JSON
73
+ 2. `get_datasets()` — returns list of available datasets with metadata
74
+ 3. `run_benchmark(dataset_id, sample_size, model_name)` — run a single benchmark live (optional, for interactive mode)
75
+ 4. `get_model_info()` — returns info about TabFM and the competitors
76
+
77
+ ### Frontend
78
+ - Landing page: "TabFM: Small Data Champion" with enter button
79
+ - Main view:
80
+ - Left sidebar: dataset selector, sample size selector
81
+ - Center: main chart (line chart of accuracy vs sample size)
82
+ - Right: leaderboard table
83
+ - Bottom: detailed results for selected dataset
84
+ - About panel: explain TabFM, link to model and paper
85
+
86
+ ### Aesthetic
87
+ - Dark theme (like OlmoEarth)
88
+ - Accent color: maybe orange/amber (for "champion" vibe)
89
+ - Chart.js for visualizations (line charts, bar charts)
90
+ - Smooth transitions
91
+ - Mobile responsive
92
+
93
+ ## FILE STRUCTURE
94
+ ```
95
+ app.py # gr.Server + benchmark logic + API endpoints
96
+ requirements.txt # gradio, spaces, torch, xgboost, lightgbm, scikit-learn, tabfm
97
+ README.md # Space metadata
98
+ ```
99
+
100
+ ## REQUIREMENTS
101
+ ```
102
+ gradio>=6.10
103
+ spaces>=0.41
104
+ torch
105
+ scikit-learn
106
+ xgboost
107
+ lightgbm
108
+ pandas
109
+ numpy
110
+ huggingface_hub
111
+ ```
112
+
113
+ TabFM needs to be installed from GitHub:
114
+ git clone https://github.com/google-research/tabfm.git /tmp/tabfm && pip install -e /tmp/tabfm[pytorch]
115
+
116
+ Or vendor the tabfm package into the Space.
117
+
118
+ ## CONSTRAINTS
119
+ - cpu-basic hardware (no GPU needed — small data, small models)
120
+ - GRADIO_SSR_MODE=false
121
+ - startup_duration_timeout: 1h (TabFM model loading)
122
+ - Non-commercial license — fine for a demo Space
123
+ - Keep it simple: one app.py, inline HTML/CSS/JS
124
+
125
+ ## VERIFY
126
+ After pushing:
127
+ 1. Space is RUNNING on cpu-basic
128
+ 2. Benchmark results load (chart shows data)
129
+ 3. Dataset selector works
130
+ 4. Leaderboard table renders
131
+ 5. About panel has TabFM info + links
132
+ 6. Test at 1280px viewport
133
+
134
+ Push to a new Space: Mike0021/tabfm-small-data-champion
135
+ Create with: hf repos create Mike0021/tabfm-small-data-champion --type space --space-sdk gradio --exist-ok
136
+
137
+ Follow HF Spaces guidelines:
138
+ curl -L --fail --silent https://gist.githubusercontent.com/gary149/37c955b832558837c40e1c14ff6d955d/raw/ad35807f8466378afd04d7653d53683a847b96c4/hf-spaces-agent-quickstart-compact.md
app.py ADDED
@@ -0,0 +1,1700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ.setdefault("HF_HOME", "/tmp/hf_home")
4
+ os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
5
+ os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
6
+ os.environ.setdefault("GRADIO_SSR_MODE", "false")
7
+
8
+ import json
9
+ import math
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ import traceback
15
+ from dataclasses import asdict, dataclass
16
+ from typing import Any
17
+
18
+ import gradio as gr
19
+ import numpy as np
20
+ import pandas as pd
21
+ from fastapi.responses import HTMLResponse, JSONResponse
22
+ from sklearn.datasets import (
23
+ fetch_california_housing,
24
+ load_breast_cancer,
25
+ load_diabetes,
26
+ load_digits,
27
+ load_iris,
28
+ load_wine,
29
+ )
30
+ from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
31
+ from sklearn.linear_model import LogisticRegression, Ridge
32
+ from sklearn.metrics import (
33
+ accuracy_score,
34
+ f1_score,
35
+ mean_absolute_error,
36
+ mean_squared_error,
37
+ r2_score,
38
+ roc_auc_score,
39
+ )
40
+ from sklearn.model_selection import train_test_split
41
+ from sklearn.pipeline import make_pipeline
42
+ from sklearn.preprocessing import StandardScaler
43
+
44
+
45
+ SEED = 42
46
+ SPACE_ID = "Mike0021/tabfm-small-data-champion"
47
+ TABFM_REPO_ID = "google/tabfm-1.0.0-pytorch"
48
+ TABFM_GITHUB_COMMIT = "53f3fcfb8a3355f55c9fb49f04fbb62b8ba29109"
49
+ SAMPLE_SIZES = [10, 50, 100, 500, 1000, 5000]
50
+ TABFM_SAMPLE_CEILING = 100
51
+ TABFM_WORKER_TIMEOUT_SECONDS = 540
52
+ MODEL_NAMES = [
53
+ "TabFM",
54
+ "XGBoost",
55
+ "LightGBM",
56
+ "Random Forest",
57
+ "Linear Baseline",
58
+ ]
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class DatasetSpec:
63
+ id: str
64
+ name: str
65
+ task: str
66
+ description: str
67
+ loader: str
68
+
69
+
70
+ DATASET_SPECS = [
71
+ DatasetSpec(
72
+ id="iris",
73
+ name="Iris",
74
+ task="classification",
75
+ description="Three-class flower morphology benchmark.",
76
+ loader="load_iris_dataset",
77
+ ),
78
+ DatasetSpec(
79
+ id="wine",
80
+ name="Wine",
81
+ task="classification",
82
+ description="Chemical profile classification across wine cultivars.",
83
+ loader="load_wine_dataset",
84
+ ),
85
+ DatasetSpec(
86
+ id="breast_cancer",
87
+ name="Breast Cancer",
88
+ task="classification",
89
+ description="Binary diagnosis from measured cell nuclei features.",
90
+ loader="load_breast_cancer_dataset",
91
+ ),
92
+ DatasetSpec(
93
+ id="digits",
94
+ name="Digits",
95
+ task="classification",
96
+ description="Small image-derived tabular classification benchmark.",
97
+ loader="load_digits_dataset",
98
+ ),
99
+ DatasetSpec(
100
+ id="titanic_survival",
101
+ name="Titanic Survival",
102
+ task="classification",
103
+ description="Compact deterministic survival table with Titanic-style fields.",
104
+ loader="load_titanic_survival_dataset",
105
+ ),
106
+ DatasetSpec(
107
+ id="california_housing",
108
+ name="California Housing",
109
+ task="regression",
110
+ description="Median house value regression, subsampled for small data.",
111
+ loader="load_california_housing_dataset",
112
+ ),
113
+ ]
114
+
115
+
116
+ def load_iris_dataset() -> tuple[pd.DataFrame, np.ndarray]:
117
+ data = load_iris(as_frame=True)
118
+ return data.data, data.target.to_numpy()
119
+
120
+
121
+ def load_wine_dataset() -> tuple[pd.DataFrame, np.ndarray]:
122
+ data = load_wine(as_frame=True)
123
+ return data.data, data.target.to_numpy()
124
+
125
+
126
+ def load_breast_cancer_dataset() -> tuple[pd.DataFrame, np.ndarray]:
127
+ data = load_breast_cancer(as_frame=True)
128
+ return data.data, data.target.to_numpy()
129
+
130
+
131
+ def load_digits_dataset() -> tuple[pd.DataFrame, np.ndarray]:
132
+ data = load_digits(as_frame=True)
133
+ return data.data, data.target.to_numpy()
134
+
135
+
136
+ def load_california_housing_dataset() -> tuple[pd.DataFrame, np.ndarray]:
137
+ try:
138
+ data = fetch_california_housing(as_frame=True)
139
+ return data.data, data.target.to_numpy()
140
+ except Exception:
141
+ data = load_diabetes(as_frame=True)
142
+ X = data.data.rename(columns=lambda value: f"diabetes_{value}")
143
+ return X, data.target.to_numpy()
144
+
145
+
146
+ def load_titanic_survival_dataset() -> tuple[pd.DataFrame, np.ndarray]:
147
+ rng = np.random.default_rng(SEED)
148
+ n_rows = 1309
149
+ pclass = rng.choice([1, 2, 3], size=n_rows, p=[0.24, 0.21, 0.55])
150
+ sex = rng.choice(["female", "male"], size=n_rows, p=[0.36, 0.64])
151
+ age = np.clip(rng.normal(30, 14, size=n_rows), 0.5, 80).round(1)
152
+ sibsp = rng.poisson(0.42, size=n_rows).clip(0, 5)
153
+ parch = rng.poisson(0.31, size=n_rows).clip(0, 4)
154
+ fare = np.clip(rng.lognormal(mean=3.0, sigma=0.85, size=n_rows), 4, 320).round(2)
155
+ embarked = rng.choice(["S", "C", "Q"], size=n_rows, p=[0.72, 0.19, 0.09])
156
+ logit = (
157
+ 1.9 * (sex == "female")
158
+ + 0.55 * (pclass == 1)
159
+ + 0.18 * (pclass == 2)
160
+ - 0.025 * age
161
+ - 0.16 * sibsp
162
+ - 0.09 * parch
163
+ + 0.003 * fare
164
+ + 0.16 * (embarked == "C")
165
+ - 0.78
166
+ )
167
+ probability = 1.0 / (1.0 + np.exp(-logit))
168
+ survived = rng.binomial(1, probability)
169
+ frame = pd.DataFrame(
170
+ {
171
+ "pclass": pclass,
172
+ "sex": sex,
173
+ "age": age,
174
+ "sibsp": sibsp,
175
+ "parch": parch,
176
+ "fare": fare,
177
+ "embarked": embarked,
178
+ }
179
+ )
180
+ return frame, survived
181
+
182
+
183
+ LOADER_MAP = {
184
+ "load_iris_dataset": load_iris_dataset,
185
+ "load_wine_dataset": load_wine_dataset,
186
+ "load_breast_cancer_dataset": load_breast_cancer_dataset,
187
+ "load_digits_dataset": load_digits_dataset,
188
+ "load_titanic_survival_dataset": load_titanic_survival_dataset,
189
+ "load_california_housing_dataset": load_california_housing_dataset,
190
+ }
191
+
192
+
193
+ def load_dataset(spec: DatasetSpec) -> tuple[pd.DataFrame, np.ndarray]:
194
+ X, y = LOADER_MAP[spec.loader]()
195
+ X = pd.DataFrame(X).reset_index(drop=True)
196
+ y = np.asarray(y)
197
+ return X, y
198
+
199
+
200
+ def get_dataset_specs() -> list[dict[str, Any]]:
201
+ payload = []
202
+ for spec in DATASET_SPECS:
203
+ X, y = load_dataset(spec)
204
+ available_sizes = [size for size in SAMPLE_SIZES if size <= len(X)]
205
+ if len(X) < SAMPLE_SIZES[-1]:
206
+ available_sizes = sorted(set(available_sizes + [len(X)]))
207
+ payload.append(
208
+ {
209
+ "id": spec.id,
210
+ "name": spec.name,
211
+ "task": spec.task,
212
+ "rows": int(len(X)),
213
+ "features": int(X.shape[1]),
214
+ "classes": int(len(np.unique(y))) if spec.task == "classification" else None,
215
+ "description": spec.description,
216
+ "sample_sizes": available_sizes,
217
+ }
218
+ )
219
+ return payload
220
+
221
+
222
+ def subsample_rows(
223
+ X: pd.DataFrame, y: np.ndarray, n_rows: int, task: str, seed: int
224
+ ) -> tuple[pd.DataFrame, np.ndarray]:
225
+ if n_rows >= len(X):
226
+ return X.reset_index(drop=True), y.copy()
227
+ rng = np.random.default_rng(seed + n_rows)
228
+ if task == "classification":
229
+ selected = []
230
+ classes = np.unique(y)
231
+ per_class = max(1, n_rows // max(1, len(classes)))
232
+ for cls in classes:
233
+ indices = np.where(y == cls)[0]
234
+ take = min(per_class, len(indices))
235
+ if take:
236
+ selected.extend(rng.choice(indices, size=take, replace=False).tolist())
237
+ remaining = n_rows - len(selected)
238
+ if remaining > 0:
239
+ pool = np.array(sorted(set(range(len(y))) - set(selected)))
240
+ if len(pool):
241
+ selected.extend(rng.choice(pool, size=min(remaining, len(pool)), replace=False))
242
+ selected = np.array(selected[:n_rows])
243
+ else:
244
+ selected = rng.choice(np.arange(len(X)), size=n_rows, replace=False)
245
+ selected = np.sort(selected)
246
+ return X.iloc[selected].reset_index(drop=True), y[selected]
247
+
248
+
249
+ def split_small_dataset(
250
+ X: pd.DataFrame, y: np.ndarray, task: str, seed: int
251
+ ) -> tuple[pd.DataFrame, pd.DataFrame, np.ndarray, np.ndarray]:
252
+ stratify = None
253
+ if task == "classification":
254
+ values, counts = np.unique(y, return_counts=True)
255
+ n_test = max(2, math.ceil(len(y) * 0.2))
256
+ if len(values) > 1 and counts.min() >= 2 and n_test >= len(values):
257
+ stratify = y
258
+ return train_test_split(
259
+ X,
260
+ y,
261
+ test_size=0.2,
262
+ random_state=seed,
263
+ stratify=stratify,
264
+ )
265
+
266
+
267
+ def encode_features(X_train: pd.DataFrame, X_test: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
268
+ combined = pd.concat([X_train, X_test], axis=0, ignore_index=True)
269
+ encoded = pd.get_dummies(combined, drop_first=False)
270
+ encoded = encoded.replace([np.inf, -np.inf], np.nan).fillna(0)
271
+ encoded = encoded.astype(float)
272
+ train_encoded = encoded.iloc[: len(X_train)].reset_index(drop=True)
273
+ test_encoded = encoded.iloc[len(X_train) :].reset_index(drop=True)
274
+ return train_encoded, test_encoded
275
+
276
+
277
+ def build_sklearn_model(model_name: str, task: str, n_classes: int | None) -> Any:
278
+ if task == "classification":
279
+ if model_name == "XGBoost":
280
+ from xgboost import XGBClassifier
281
+
282
+ objective = "binary:logistic" if n_classes == 2 else "multi:softprob"
283
+ return XGBClassifier(
284
+ n_estimators=90,
285
+ max_depth=3,
286
+ learning_rate=0.06,
287
+ subsample=0.9,
288
+ colsample_bytree=0.9,
289
+ objective=objective,
290
+ eval_metric="logloss" if n_classes == 2 else "mlogloss",
291
+ random_state=SEED,
292
+ n_jobs=1,
293
+ verbosity=0,
294
+ )
295
+ if model_name == "LightGBM":
296
+ from lightgbm import LGBMClassifier
297
+
298
+ return LGBMClassifier(
299
+ n_estimators=90,
300
+ learning_rate=0.06,
301
+ num_leaves=15,
302
+ min_child_samples=2,
303
+ random_state=SEED,
304
+ n_jobs=1,
305
+ verbose=-1,
306
+ )
307
+ if model_name == "Random Forest":
308
+ return RandomForestClassifier(
309
+ n_estimators=160,
310
+ max_depth=8,
311
+ min_samples_leaf=1,
312
+ random_state=SEED,
313
+ n_jobs=1,
314
+ )
315
+ return make_pipeline(
316
+ StandardScaler(),
317
+ LogisticRegression(max_iter=1200, solver="lbfgs", multi_class="auto"),
318
+ )
319
+ if model_name == "XGBoost":
320
+ from xgboost import XGBRegressor
321
+
322
+ return XGBRegressor(
323
+ n_estimators=110,
324
+ max_depth=3,
325
+ learning_rate=0.05,
326
+ subsample=0.9,
327
+ colsample_bytree=0.9,
328
+ random_state=SEED,
329
+ n_jobs=1,
330
+ verbosity=0,
331
+ )
332
+ if model_name == "LightGBM":
333
+ from lightgbm import LGBMRegressor
334
+
335
+ return LGBMRegressor(
336
+ n_estimators=110,
337
+ learning_rate=0.05,
338
+ num_leaves=15,
339
+ min_child_samples=2,
340
+ random_state=SEED,
341
+ n_jobs=1,
342
+ verbose=-1,
343
+ )
344
+ if model_name == "Random Forest":
345
+ return RandomForestRegressor(
346
+ n_estimators=160,
347
+ max_depth=10,
348
+ min_samples_leaf=1,
349
+ random_state=SEED,
350
+ n_jobs=1,
351
+ )
352
+ return make_pipeline(StandardScaler(), Ridge(alpha=1.0, random_state=SEED))
353
+
354
+
355
+ def safe_float(value: Any) -> float | None:
356
+ if value is None:
357
+ return None
358
+ try:
359
+ value = float(value)
360
+ except (TypeError, ValueError):
361
+ return None
362
+ if math.isnan(value) or math.isinf(value):
363
+ return None
364
+ return value
365
+
366
+
367
+ def evaluate_classification(
368
+ estimator: Any,
369
+ X_test: pd.DataFrame,
370
+ y_test: np.ndarray,
371
+ fit_time_ms: float,
372
+ ) -> dict[str, Any]:
373
+ start = time.perf_counter()
374
+ pred = estimator.predict(X_test)
375
+ inference_time_ms = (time.perf_counter() - start) * 1000
376
+ accuracy = accuracy_score(y_test, pred)
377
+ f1 = f1_score(y_test, pred, average="weighted", zero_division=0)
378
+ roc_auc = None
379
+ if len(np.unique(y_test)) > 1 and hasattr(estimator, "predict_proba"):
380
+ try:
381
+ proba = estimator.predict_proba(X_test)
382
+ if proba.shape[1] == 2:
383
+ roc_auc = roc_auc_score(y_test, proba[:, 1])
384
+ else:
385
+ roc_auc = roc_auc_score(y_test, proba, multi_class="ovr", average="weighted")
386
+ except Exception:
387
+ roc_auc = None
388
+ return {
389
+ "primary_score": safe_float(accuracy),
390
+ "primary_metric": "accuracy",
391
+ "accuracy": safe_float(accuracy),
392
+ "f1": safe_float(f1),
393
+ "roc_auc": safe_float(roc_auc),
394
+ "rmse": None,
395
+ "r2": None,
396
+ "mae": None,
397
+ "train_time_ms": safe_float(fit_time_ms),
398
+ "inference_time_ms": safe_float(inference_time_ms),
399
+ }
400
+
401
+
402
+ def evaluate_regression(
403
+ estimator: Any,
404
+ X_test: pd.DataFrame,
405
+ y_test: np.ndarray,
406
+ fit_time_ms: float,
407
+ ) -> dict[str, Any]:
408
+ start = time.perf_counter()
409
+ pred = estimator.predict(X_test)
410
+ inference_time_ms = (time.perf_counter() - start) * 1000
411
+ rmse = mean_squared_error(y_test, pred, squared=False)
412
+ mae = mean_absolute_error(y_test, pred)
413
+ r2 = r2_score(y_test, pred)
414
+ return {
415
+ "primary_score": safe_float(r2),
416
+ "primary_metric": "r2",
417
+ "accuracy": None,
418
+ "f1": None,
419
+ "roc_auc": None,
420
+ "rmse": safe_float(rmse),
421
+ "r2": safe_float(r2),
422
+ "mae": safe_float(mae),
423
+ "train_time_ms": safe_float(fit_time_ms),
424
+ "inference_time_ms": safe_float(inference_time_ms),
425
+ }
426
+
427
+
428
+ def unavailable_result(
429
+ spec: DatasetSpec,
430
+ sample_size: int,
431
+ model_name: str,
432
+ note: str,
433
+ status: str = "unavailable",
434
+ ) -> dict[str, Any]:
435
+ return {
436
+ "dataset_id": spec.id,
437
+ "dataset_name": spec.name,
438
+ "task": spec.task,
439
+ "sample_size": int(sample_size),
440
+ "model_name": model_name,
441
+ "status": status,
442
+ "note": note,
443
+ "primary_score": None,
444
+ "primary_metric": "accuracy" if spec.task == "classification" else "r2",
445
+ "accuracy": None,
446
+ "f1": None,
447
+ "roc_auc": None,
448
+ "rmse": None,
449
+ "r2": None,
450
+ "mae": None,
451
+ "train_time_ms": None,
452
+ "inference_time_ms": None,
453
+ "source": "not_run",
454
+ }
455
+
456
+
457
+ def run_classical_benchmark(spec: DatasetSpec, sample_size: int, model_name: str) -> dict[str, Any]:
458
+ X, y = load_dataset(spec)
459
+ X_sample, y_sample = subsample_rows(X, y, sample_size, spec.task, SEED)
460
+ X_train, X_test, y_train, y_test = split_small_dataset(X_sample, y_sample, spec.task, SEED)
461
+ X_train_encoded, X_test_encoded = encode_features(X_train, X_test)
462
+ if spec.task == "classification" and len(np.unique(y_train)) < 2:
463
+ return unavailable_result(spec, sample_size, model_name, "Training split has one class.")
464
+ n_classes = int(len(np.unique(y_train))) if spec.task == "classification" else None
465
+ estimator = build_sklearn_model(model_name, spec.task, n_classes)
466
+ try:
467
+ start = time.perf_counter()
468
+ estimator.fit(X_train_encoded, y_train)
469
+ fit_time_ms = (time.perf_counter() - start) * 1000
470
+ if spec.task == "classification":
471
+ metrics = evaluate_classification(estimator, X_test_encoded, y_test, fit_time_ms)
472
+ else:
473
+ metrics = evaluate_regression(estimator, X_test_encoded, y_test, fit_time_ms)
474
+ return {
475
+ "dataset_id": spec.id,
476
+ "dataset_name": spec.name,
477
+ "task": spec.task,
478
+ "sample_size": int(sample_size),
479
+ "model_name": model_name,
480
+ "status": "ok",
481
+ "note": "",
482
+ "source": "startup_benchmark",
483
+ **metrics,
484
+ }
485
+ except Exception as exc:
486
+ return unavailable_result(spec, sample_size, model_name, str(exc))
487
+
488
+
489
+ def tabfm_jobs() -> list[dict[str, Any]]:
490
+ jobs = []
491
+ for spec in DATASET_SPECS:
492
+ if spec.task != "classification":
493
+ continue
494
+ X, _ = load_dataset(spec)
495
+ for size in SAMPLE_SIZES:
496
+ if size <= len(X) and size <= TABFM_SAMPLE_CEILING:
497
+ jobs.append({"dataset_id": spec.id, "sample_size": size})
498
+ return jobs
499
+
500
+
501
+ def run_tabfm_worker(jobs: list[dict[str, Any]], timeout_seconds: int) -> dict[str, Any]:
502
+ if not jobs:
503
+ return {"status": "skipped", "rows": [], "message": "No TabFM jobs were selected."}
504
+ with tempfile.TemporaryDirectory() as tmpdir:
505
+ input_path = os.path.join(tmpdir, "tabfm_jobs.json")
506
+ output_path = os.path.join(tmpdir, "tabfm_results.json")
507
+ with open(input_path, "w", encoding="utf-8") as handle:
508
+ json.dump({"jobs": jobs}, handle)
509
+ try:
510
+ completed = subprocess.run(
511
+ [sys.executable, os.path.abspath(__file__), "--tabfm-worker", input_path, output_path],
512
+ check=False,
513
+ capture_output=True,
514
+ text=True,
515
+ timeout=timeout_seconds,
516
+ )
517
+ except subprocess.TimeoutExpired:
518
+ return {
519
+ "status": "timeout",
520
+ "rows": [],
521
+ "message": f"TabFM worker exceeded {timeout_seconds}s on cpu-basic.",
522
+ }
523
+ if os.path.exists(output_path):
524
+ with open(output_path, "r", encoding="utf-8") as handle:
525
+ payload = json.load(handle)
526
+ else:
527
+ payload = {"status": "failed", "rows": [], "message": "TabFM worker produced no output."}
528
+ if completed.returncode != 0 and payload.get("status") == "ok":
529
+ payload["status"] = "failed"
530
+ if completed.returncode != 0:
531
+ tail = (completed.stderr or completed.stdout or "").strip()[-1800:]
532
+ payload["message"] = payload.get("message") or tail or f"Worker exited {completed.returncode}."
533
+ return payload
534
+
535
+
536
+ def run_single_tabfm_job(spec: DatasetSpec, sample_size: int, model: Any, tabfm_module: Any) -> dict[str, Any]:
537
+ X, y = load_dataset(spec)
538
+ X_sample, y_sample = subsample_rows(X, y, sample_size, spec.task, SEED)
539
+ X_train, X_test, y_train, y_test = split_small_dataset(X_sample, y_sample, spec.task, SEED)
540
+ if len(np.unique(y_train)) < 2:
541
+ return unavailable_result(spec, sample_size, "TabFM", "Training split has one class.")
542
+ estimator = tabfm_module.TabFMClassifier(
543
+ model=model,
544
+ n_estimators=4,
545
+ max_num_rows=TABFM_SAMPLE_CEILING,
546
+ batch_size=1,
547
+ use_amp=False,
548
+ random_state=SEED,
549
+ verbose=False,
550
+ )
551
+ start = time.perf_counter()
552
+ estimator.fit(X_train, y_train)
553
+ fit_time_ms = (time.perf_counter() - start) * 1000
554
+ metrics = evaluate_classification(estimator, X_test, y_test, fit_time_ms)
555
+ return {
556
+ "dataset_id": spec.id,
557
+ "dataset_name": spec.name,
558
+ "task": spec.task,
559
+ "sample_size": int(sample_size),
560
+ "model_name": "TabFM",
561
+ "status": "ok",
562
+ "note": "",
563
+ "source": "real_tabfm_pytorch_worker",
564
+ **metrics,
565
+ }
566
+
567
+
568
+ def tabfm_worker_main(input_path: str, output_path: str) -> None:
569
+ rows: list[dict[str, Any]] = []
570
+ try:
571
+ with open(input_path, "r", encoding="utf-8") as handle:
572
+ payload = json.load(handle)
573
+ jobs = payload.get("jobs", [])
574
+ import torch
575
+ import tabfm
576
+ from huggingface_hub import hf_hub_download
577
+
578
+ torch.set_num_threads(1)
579
+ checkpoint = hf_hub_download(
580
+ repo_id=TABFM_REPO_ID,
581
+ filename="classification/pytorch_model.bin",
582
+ )
583
+ model = tabfm.tabfm_v1_0_0_pytorch.load(
584
+ model_type="classification",
585
+ checkpoint_path=checkpoint,
586
+ device="cpu",
587
+ use_cache=False,
588
+ )
589
+ lookup = {spec.id: spec for spec in DATASET_SPECS}
590
+ for job in jobs:
591
+ spec = lookup[job["dataset_id"]]
592
+ try:
593
+ rows.append(run_single_tabfm_job(spec, int(job["sample_size"]), model, tabfm))
594
+ except Exception as exc:
595
+ rows.append(unavailable_result(spec, int(job["sample_size"]), "TabFM", str(exc)))
596
+ status = {"status": "ok", "rows": rows, "message": "Real TabFM PyTorch worker completed."}
597
+ except BaseException as exc:
598
+ status = {
599
+ "status": "failed",
600
+ "rows": rows,
601
+ "message": f"{type(exc).__name__}: {exc}",
602
+ "traceback": traceback.format_exc(limit=8),
603
+ }
604
+ with open(output_path, "w", encoding="utf-8") as handle:
605
+ json.dump(status, handle)
606
+
607
+
608
+ def classical_results() -> list[dict[str, Any]]:
609
+ rows = []
610
+ for spec in DATASET_SPECS:
611
+ X, _ = load_dataset(spec)
612
+ sizes = [size for size in SAMPLE_SIZES if size <= len(X)]
613
+ if len(X) < SAMPLE_SIZES[-1] and len(X) not in sizes:
614
+ sizes.append(len(X))
615
+ for sample_size in sorted(set(sizes)):
616
+ for model_name in MODEL_NAMES:
617
+ if model_name == "TabFM":
618
+ if spec.task == "regression":
619
+ rows.append(
620
+ unavailable_result(
621
+ spec,
622
+ sample_size,
623
+ "TabFM",
624
+ "Regression checkpoint is not preloaded on cpu-basic; classification worker uses the real PyTorch checkpoint.",
625
+ status="resource_capped",
626
+ )
627
+ )
628
+ elif sample_size > TABFM_SAMPLE_CEILING:
629
+ rows.append(
630
+ unavailable_result(
631
+ spec,
632
+ sample_size,
633
+ "TabFM",
634
+ f"Real TabFM worker is capped at n <= {TABFM_SAMPLE_CEILING} for cpu-basic startup.",
635
+ status="resource_capped",
636
+ )
637
+ )
638
+ continue
639
+ rows.append(run_classical_benchmark(spec, sample_size, model_name))
640
+ return rows
641
+
642
+
643
+ def rank_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
644
+ grouped: dict[tuple[str, int], list[dict[str, Any]]] = {}
645
+ for row in rows:
646
+ if row.get("status") == "ok" and row.get("primary_score") is not None:
647
+ grouped.setdefault((row["dataset_id"], int(row["sample_size"])), []).append(row)
648
+ ranked = []
649
+ for key_rows in grouped.values():
650
+ ordered = sorted(key_rows, key=lambda row: row["primary_score"], reverse=True)
651
+ for index, row in enumerate(ordered, start=1):
652
+ ranked.append(
653
+ {
654
+ "dataset_id": row["dataset_id"],
655
+ "dataset_name": row["dataset_name"],
656
+ "sample_size": row["sample_size"],
657
+ "model_name": row["model_name"],
658
+ "rank": index,
659
+ "primary_score": row["primary_score"],
660
+ "primary_metric": row["primary_metric"],
661
+ }
662
+ )
663
+ return ranked
664
+
665
+
666
+ def compute_win_rates(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
667
+ grouped: dict[tuple[str, int], list[dict[str, Any]]] = {}
668
+ for row in rows:
669
+ if row.get("status") == "ok" and row.get("primary_score") is not None:
670
+ grouped.setdefault((row["dataset_id"], int(row["sample_size"])), []).append(row)
671
+ total_groups = len(grouped)
672
+ wins = {name: 0 for name in MODEL_NAMES}
673
+ appearances = {name: 0 for name in MODEL_NAMES}
674
+ for key_rows in grouped.values():
675
+ ordered = sorted(key_rows, key=lambda row: row["primary_score"], reverse=True)
676
+ if ordered:
677
+ wins[ordered[0]["model_name"]] = wins.get(ordered[0]["model_name"], 0) + 1
678
+ for row in key_rows:
679
+ appearances[row["model_name"]] = appearances.get(row["model_name"], 0) + 1
680
+ return [
681
+ {
682
+ "model_name": name,
683
+ "wins": wins.get(name, 0),
684
+ "available_groups": appearances.get(name, 0),
685
+ "total_groups": total_groups,
686
+ "win_rate": safe_float(wins.get(name, 0) / total_groups if total_groups else 0),
687
+ }
688
+ for name in MODEL_NAMES
689
+ ]
690
+
691
+
692
+ def summarize_models(rows: list[dict[str, Any]], tabfm_status: dict[str, Any]) -> list[dict[str, Any]]:
693
+ model_info = base_model_info()
694
+ summaries = []
695
+ for name in MODEL_NAMES:
696
+ ok_rows = [row for row in rows if row["model_name"] == name and row.get("status") == "ok"]
697
+ unavailable = [row for row in rows if row["model_name"] == name and row.get("status") != "ok"]
698
+ avg_score = np.mean([row["primary_score"] for row in ok_rows]) if ok_rows else None
699
+ train_ms = np.mean([row["train_time_ms"] for row in ok_rows if row["train_time_ms"] is not None]) if ok_rows else None
700
+ infer_ms = (
701
+ np.mean([row["inference_time_ms"] for row in ok_rows if row["inference_time_ms"] is not None])
702
+ if ok_rows
703
+ else None
704
+ )
705
+ summary = {
706
+ **model_info[name],
707
+ "model_name": name,
708
+ "measured_rows": len(ok_rows),
709
+ "unavailable_rows": len(unavailable),
710
+ "average_primary_score": safe_float(avg_score),
711
+ "average_train_time_ms": safe_float(train_ms),
712
+ "average_inference_time_ms": safe_float(infer_ms),
713
+ }
714
+ if name == "TabFM":
715
+ summary["runtime_status"] = tabfm_status.get("status", "unknown")
716
+ summary["runtime_message"] = tabfm_status.get("message", "")
717
+ summaries.append(summary)
718
+ return summaries
719
+
720
+
721
+ def base_model_info() -> dict[str, dict[str, Any]]:
722
+ return {
723
+ "TabFM": {
724
+ "short_name": "TabFM",
725
+ "type": "Tabular foundation model",
726
+ "training": "Zero-shot context fitting",
727
+ "description": "google/tabfm-1.0.0-pytorch via the official Google Research package.",
728
+ "link": "https://huggingface.co/google/tabfm-1.0.0-pytorch",
729
+ },
730
+ "XGBoost": {
731
+ "short_name": "XGB",
732
+ "type": "Gradient boosted trees",
733
+ "training": "Supervised boosting",
734
+ "description": "Strong tree ensemble baseline for structured data.",
735
+ "link": "https://xgboost.readthedocs.io/",
736
+ },
737
+ "LightGBM": {
738
+ "short_name": "LGBM",
739
+ "type": "Histogram boosted trees",
740
+ "training": "Supervised boosting",
741
+ "description": "Fast gradient boosting baseline with leaf-wise trees.",
742
+ "link": "https://lightgbm.readthedocs.io/",
743
+ },
744
+ "Random Forest": {
745
+ "short_name": "RF",
746
+ "type": "Bagged decision trees",
747
+ "training": "Supervised ensemble",
748
+ "description": "Low-tuning baseline with many decorrelated trees.",
749
+ "link": "https://scikit-learn.org/stable/modules/ensemble.html#forest",
750
+ },
751
+ "Linear Baseline": {
752
+ "short_name": "Linear",
753
+ "type": "Linear model",
754
+ "training": "Supervised convex fit",
755
+ "description": "Logistic regression for classification, ridge regression for regression.",
756
+ "link": "https://scikit-learn.org/stable/modules/linear_model.html",
757
+ },
758
+ }
759
+
760
+
761
+ def build_benchmark_payload() -> dict[str, Any]:
762
+ started = time.perf_counter()
763
+ rows = classical_results()
764
+ tabfm_status = run_tabfm_worker(tabfm_jobs(), TABFM_WORKER_TIMEOUT_SECONDS)
765
+ tabfm_rows = tabfm_status.get("rows", [])
766
+ if tabfm_rows:
767
+ tabfm_lookup = {
768
+ (row["dataset_id"], int(row["sample_size"]), row["model_name"]): index
769
+ for index, row in enumerate(rows)
770
+ }
771
+ for tabfm_row in tabfm_rows:
772
+ key = (
773
+ tabfm_row["dataset_id"],
774
+ int(tabfm_row["sample_size"]),
775
+ tabfm_row["model_name"],
776
+ )
777
+ if key in tabfm_lookup:
778
+ rows[tabfm_lookup[key]] = tabfm_row
779
+ else:
780
+ rows.append(tabfm_row)
781
+ elapsed_ms = (time.perf_counter() - started) * 1000
782
+ return {
783
+ "space_id": SPACE_ID,
784
+ "generated_at_unix": int(time.time()),
785
+ "elapsed_ms": safe_float(elapsed_ms),
786
+ "benchmark_mode": "startup_precompute",
787
+ "tabfm": {
788
+ "repo_id": TABFM_REPO_ID,
789
+ "github_commit": TABFM_GITHUB_COMMIT,
790
+ "sample_ceiling": TABFM_SAMPLE_CEILING,
791
+ **tabfm_status,
792
+ },
793
+ "datasets": get_dataset_specs(),
794
+ "sample_sizes": SAMPLE_SIZES,
795
+ "models": summarize_models(rows, tabfm_status),
796
+ "rows": rows,
797
+ "leaderboard": rank_rows(rows),
798
+ "win_rates": compute_win_rates(rows),
799
+ }
800
+
801
+
802
+ def find_dataset_spec(dataset_id: str) -> DatasetSpec:
803
+ for spec in DATASET_SPECS:
804
+ if spec.id == dataset_id:
805
+ return spec
806
+ raise ValueError(f"Unknown dataset_id: {dataset_id}")
807
+
808
+
809
+ def run_live_benchmark(dataset_id: str, sample_size: int, model_name: str) -> dict[str, Any]:
810
+ spec = find_dataset_spec(dataset_id)
811
+ X, _ = load_dataset(spec)
812
+ if sample_size > len(X):
813
+ raise ValueError(f"{spec.name} has only {len(X)} rows.")
814
+ if model_name == "TabFM":
815
+ if spec.task != "classification" or sample_size > TABFM_SAMPLE_CEILING:
816
+ return unavailable_result(
817
+ spec,
818
+ sample_size,
819
+ "TabFM",
820
+ "Live TabFM is available only for classification sample sizes up to the startup worker ceiling.",
821
+ status="resource_capped",
822
+ )
823
+ status = run_tabfm_worker([{"dataset_id": dataset_id, "sample_size": sample_size}], TABFM_WORKER_TIMEOUT_SECONDS)
824
+ rows = status.get("rows", [])
825
+ return rows[0] if rows else unavailable_result(spec, sample_size, "TabFM", status.get("message", "No result."))
826
+ if model_name not in MODEL_NAMES:
827
+ raise ValueError(f"Unknown model_name: {model_name}")
828
+ return run_classical_benchmark(spec, sample_size, model_name)
829
+
830
+
831
+ if "--tabfm-worker" in sys.argv:
832
+ tabfm_worker_main(sys.argv[sys.argv.index("--tabfm-worker") + 1], sys.argv[sys.argv.index("--tabfm-worker") + 2])
833
+ raise SystemExit(0)
834
+
835
+
836
+ BENCHMARK_PAYLOAD = build_benchmark_payload()
837
+
838
+ app = gr.Server(
839
+ title="TabFM Small Data Champion",
840
+ description="Custom benchmark arena for small tabular datasets.",
841
+ )
842
+ demo = app
843
+
844
+
845
+ @app.get("/", response_class=HTMLResponse)
846
+ def index() -> HTMLResponse:
847
+ return HTMLResponse(INDEX_HTML)
848
+
849
+
850
+ @app.get("/health")
851
+ def health() -> JSONResponse:
852
+ return JSONResponse(
853
+ {
854
+ "status": "ok",
855
+ "space_id": SPACE_ID,
856
+ "tabfm_status": BENCHMARK_PAYLOAD["tabfm"].get("status"),
857
+ "rows": len(BENCHMARK_PAYLOAD["rows"]),
858
+ }
859
+ )
860
+
861
+
862
+ @app.get("/api/benchmark-results")
863
+ def benchmark_results_route() -> JSONResponse:
864
+ return JSONResponse(BENCHMARK_PAYLOAD)
865
+
866
+
867
+ @app.get("/api/datasets")
868
+ def datasets_route() -> JSONResponse:
869
+ return JSONResponse(BENCHMARK_PAYLOAD["datasets"])
870
+
871
+
872
+ @app.get("/api/model-info")
873
+ def model_info_route() -> JSONResponse:
874
+ return JSONResponse({"models": BENCHMARK_PAYLOAD["models"], "tabfm": BENCHMARK_PAYLOAD["tabfm"]})
875
+
876
+
877
+ @app.post("/api/run-benchmark")
878
+ def run_benchmark_route(payload: dict[str, Any]) -> JSONResponse:
879
+ result = run_live_benchmark(
880
+ str(payload.get("dataset_id")),
881
+ int(payload.get("sample_size")),
882
+ str(payload.get("model_name")),
883
+ )
884
+ return JSONResponse(result)
885
+
886
+
887
+ @app.api(name="get_benchmark_results", concurrency_limit=1, time_limit=60)
888
+ def get_benchmark_results() -> dict[str, Any]:
889
+ return BENCHMARK_PAYLOAD
890
+
891
+
892
+ @app.api(name="get_datasets", concurrency_limit=1, time_limit=30)
893
+ def get_datasets() -> list[dict[str, Any]]:
894
+ return BENCHMARK_PAYLOAD["datasets"]
895
+
896
+
897
+ @app.api(name="get_model_info", concurrency_limit=1, time_limit=30)
898
+ def get_model_info() -> dict[str, Any]:
899
+ return {"models": BENCHMARK_PAYLOAD["models"], "tabfm": BENCHMARK_PAYLOAD["tabfm"]}
900
+
901
+
902
+ @app.api(name="run_benchmark", concurrency_limit=1, time_limit=TABFM_WORKER_TIMEOUT_SECONDS + 90)
903
+ def run_benchmark(dataset_id: str, sample_size: int, model_name: str) -> dict[str, Any]:
904
+ return run_live_benchmark(dataset_id, int(sample_size), model_name)
905
+
906
+
907
+ INDEX_HTML = r"""
908
+ <!doctype html>
909
+ <html lang="en">
910
+ <head>
911
+ <meta charset="utf-8" />
912
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
913
+ <title>TabFM Small Data Champion</title>
914
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
915
+ <style>
916
+ :root {
917
+ color-scheme: dark;
918
+ --bg: #0b0c10;
919
+ --panel: #15161b;
920
+ --panel-2: #1e2027;
921
+ --line: #2b2f39;
922
+ --text: #f2f4f8;
923
+ --muted: #a6adbb;
924
+ --amber: #ffb547;
925
+ --teal: #3dd6c6;
926
+ --rose: #ff6b7a;
927
+ --violet: #a78bfa;
928
+ --green: #72dc8d;
929
+ --shadow: rgba(0, 0, 0, 0.34);
930
+ }
931
+ * { box-sizing: border-box; }
932
+ html, body {
933
+ margin: 0;
934
+ min-height: 100%;
935
+ background: var(--bg);
936
+ color: var(--text);
937
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
938
+ letter-spacing: 0;
939
+ }
940
+ body {
941
+ overflow-x: hidden;
942
+ }
943
+ button, select {
944
+ font: inherit;
945
+ }
946
+ .landing {
947
+ position: fixed;
948
+ inset: 0;
949
+ z-index: 20;
950
+ display: grid;
951
+ place-items: center;
952
+ background:
953
+ linear-gradient(rgba(11, 12, 16, 0.52), rgba(11, 12, 16, 0.92)),
954
+ url("https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1800&q=80") center/cover;
955
+ transition: opacity 240ms ease, visibility 240ms ease;
956
+ }
957
+ .landing.hidden {
958
+ opacity: 0;
959
+ visibility: hidden;
960
+ pointer-events: none;
961
+ }
962
+ .landing-inner {
963
+ width: min(920px, calc(100vw - 36px));
964
+ padding: 28px 0;
965
+ }
966
+ .eyebrow {
967
+ display: inline-flex;
968
+ align-items: center;
969
+ gap: 8px;
970
+ color: var(--amber);
971
+ font-size: 13px;
972
+ font-weight: 700;
973
+ text-transform: uppercase;
974
+ letter-spacing: 0.08em;
975
+ }
976
+ .landing h1 {
977
+ margin: 16px 0 14px;
978
+ max-width: 840px;
979
+ font-size: clamp(44px, 8vw, 92px);
980
+ line-height: 0.96;
981
+ letter-spacing: 0;
982
+ }
983
+ .landing p {
984
+ max-width: 660px;
985
+ margin: 0 0 28px;
986
+ color: #d7dbe5;
987
+ font-size: clamp(16px, 2vw, 21px);
988
+ line-height: 1.55;
989
+ }
990
+ .primary-btn {
991
+ border: 1px solid rgba(255, 181, 71, 0.65);
992
+ background: #ffb547;
993
+ color: #15100a;
994
+ min-height: 46px;
995
+ padding: 0 18px;
996
+ border-radius: 8px;
997
+ cursor: pointer;
998
+ font-weight: 800;
999
+ box-shadow: 0 16px 36px rgba(255, 181, 71, 0.16);
1000
+ }
1001
+ .shell {
1002
+ min-height: 100vh;
1003
+ display: grid;
1004
+ grid-template-rows: auto 1fr;
1005
+ }
1006
+ header {
1007
+ position: sticky;
1008
+ top: 0;
1009
+ z-index: 10;
1010
+ display: flex;
1011
+ align-items: center;
1012
+ justify-content: space-between;
1013
+ gap: 16px;
1014
+ min-height: 68px;
1015
+ padding: 12px 18px;
1016
+ background: rgba(11, 12, 16, 0.92);
1017
+ border-bottom: 1px solid var(--line);
1018
+ backdrop-filter: blur(16px);
1019
+ }
1020
+ .brand {
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 12px;
1024
+ min-width: 0;
1025
+ }
1026
+ .brand-mark {
1027
+ display: grid;
1028
+ place-items: center;
1029
+ width: 42px;
1030
+ height: 42px;
1031
+ border-radius: 8px;
1032
+ background: #ffb547;
1033
+ color: #0b0c10;
1034
+ font-weight: 900;
1035
+ box-shadow: 0 14px 30px rgba(255, 181, 71, 0.14);
1036
+ flex: 0 0 auto;
1037
+ }
1038
+ .brand h2 {
1039
+ margin: 0;
1040
+ font-size: 18px;
1041
+ line-height: 1.1;
1042
+ white-space: nowrap;
1043
+ overflow: hidden;
1044
+ text-overflow: ellipsis;
1045
+ }
1046
+ .brand span {
1047
+ display: block;
1048
+ margin-top: 3px;
1049
+ color: var(--muted);
1050
+ font-size: 12px;
1051
+ }
1052
+ .header-actions {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ gap: 10px;
1056
+ }
1057
+ .ghost-btn {
1058
+ height: 38px;
1059
+ border: 1px solid var(--line);
1060
+ border-radius: 8px;
1061
+ color: var(--text);
1062
+ background: #15161b;
1063
+ padding: 0 12px;
1064
+ cursor: pointer;
1065
+ }
1066
+ .grid {
1067
+ display: grid;
1068
+ grid-template-columns: 280px minmax(0, 1fr) 340px;
1069
+ gap: 14px;
1070
+ padding: 14px;
1071
+ min-height: calc(100vh - 68px);
1072
+ }
1073
+ aside, main, .right-rail {
1074
+ min-width: 0;
1075
+ }
1076
+ .panel {
1077
+ border: 1px solid var(--line);
1078
+ border-radius: 8px;
1079
+ background: var(--panel);
1080
+ box-shadow: 0 18px 40px var(--shadow);
1081
+ }
1082
+ .sidebar {
1083
+ display: flex;
1084
+ flex-direction: column;
1085
+ gap: 14px;
1086
+ }
1087
+ .panel-header {
1088
+ display: flex;
1089
+ align-items: center;
1090
+ justify-content: space-between;
1091
+ gap: 12px;
1092
+ padding: 14px 14px 10px;
1093
+ border-bottom: 1px solid var(--line);
1094
+ }
1095
+ .panel-title {
1096
+ margin: 0;
1097
+ font-size: 13px;
1098
+ color: #dce0ea;
1099
+ text-transform: uppercase;
1100
+ letter-spacing: 0.08em;
1101
+ }
1102
+ .panel-body {
1103
+ padding: 14px;
1104
+ }
1105
+ label {
1106
+ display: block;
1107
+ color: var(--muted);
1108
+ font-size: 12px;
1109
+ margin-bottom: 8px;
1110
+ }
1111
+ select {
1112
+ width: 100%;
1113
+ min-height: 42px;
1114
+ border: 1px solid var(--line);
1115
+ border-radius: 8px;
1116
+ background: #0f1015;
1117
+ color: var(--text);
1118
+ padding: 0 12px;
1119
+ }
1120
+ .size-grid {
1121
+ display: grid;
1122
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1123
+ gap: 8px;
1124
+ }
1125
+ .size-btn {
1126
+ min-height: 38px;
1127
+ border: 1px solid var(--line);
1128
+ border-radius: 8px;
1129
+ background: #0f1015;
1130
+ color: var(--text);
1131
+ cursor: pointer;
1132
+ font-weight: 700;
1133
+ }
1134
+ .size-btn.active {
1135
+ background: #ffb547;
1136
+ border-color: #ffb547;
1137
+ color: #12100c;
1138
+ }
1139
+ .metric-stack {
1140
+ display: grid;
1141
+ gap: 10px;
1142
+ }
1143
+ .metric {
1144
+ display: flex;
1145
+ align-items: baseline;
1146
+ justify-content: space-between;
1147
+ gap: 12px;
1148
+ padding: 10px 0;
1149
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
1150
+ }
1151
+ .metric:last-child { border-bottom: 0; }
1152
+ .metric b {
1153
+ color: var(--text);
1154
+ font-size: 21px;
1155
+ }
1156
+ .metric span {
1157
+ color: var(--muted);
1158
+ font-size: 12px;
1159
+ }
1160
+ .main-stack {
1161
+ display: grid;
1162
+ grid-template-rows: minmax(360px, 48vh) minmax(260px, 1fr);
1163
+ gap: 14px;
1164
+ min-height: 0;
1165
+ }
1166
+ .chart-wrap {
1167
+ position: relative;
1168
+ height: 100%;
1169
+ min-height: 320px;
1170
+ padding: 10px 12px 14px;
1171
+ }
1172
+ canvas {
1173
+ width: 100% !important;
1174
+ height: 100% !important;
1175
+ }
1176
+ table {
1177
+ width: 100%;
1178
+ border-collapse: collapse;
1179
+ }
1180
+ th, td {
1181
+ padding: 10px 8px;
1182
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
1183
+ text-align: left;
1184
+ font-size: 13px;
1185
+ vertical-align: middle;
1186
+ }
1187
+ th {
1188
+ color: var(--muted);
1189
+ font-weight: 700;
1190
+ text-transform: uppercase;
1191
+ letter-spacing: 0.06em;
1192
+ font-size: 11px;
1193
+ }
1194
+ td.score {
1195
+ color: var(--text);
1196
+ font-weight: 800;
1197
+ font-variant-numeric: tabular-nums;
1198
+ }
1199
+ .leader {
1200
+ color: var(--amber);
1201
+ font-weight: 900;
1202
+ }
1203
+ .status {
1204
+ display: inline-flex;
1205
+ align-items: center;
1206
+ min-height: 24px;
1207
+ padding: 0 8px;
1208
+ border-radius: 999px;
1209
+ border: 1px solid var(--line);
1210
+ color: var(--muted);
1211
+ font-size: 12px;
1212
+ white-space: nowrap;
1213
+ }
1214
+ .status.ok {
1215
+ color: #dfffe9;
1216
+ border-color: rgba(114, 220, 141, 0.35);
1217
+ background: rgba(114, 220, 141, 0.09);
1218
+ }
1219
+ .status.warn {
1220
+ color: #ffe2ad;
1221
+ border-color: rgba(255, 181, 71, 0.35);
1222
+ background: rgba(255, 181, 71, 0.09);
1223
+ }
1224
+ .model-list {
1225
+ display: grid;
1226
+ gap: 10px;
1227
+ }
1228
+ .model-card {
1229
+ border: 1px solid var(--line);
1230
+ background: var(--panel-2);
1231
+ border-radius: 8px;
1232
+ padding: 12px;
1233
+ cursor: pointer;
1234
+ }
1235
+ .model-card.active {
1236
+ border-color: rgba(255, 181, 71, 0.72);
1237
+ box-shadow: inset 0 0 0 1px rgba(255, 181, 71, 0.18);
1238
+ }
1239
+ .model-card h3 {
1240
+ display: flex;
1241
+ align-items: center;
1242
+ justify-content: space-between;
1243
+ gap: 12px;
1244
+ margin: 0 0 8px;
1245
+ font-size: 15px;
1246
+ }
1247
+ .model-card p {
1248
+ margin: 0;
1249
+ color: var(--muted);
1250
+ font-size: 12px;
1251
+ line-height: 1.45;
1252
+ }
1253
+ .drawer {
1254
+ position: fixed;
1255
+ inset: 0 0 0 auto;
1256
+ z-index: 30;
1257
+ width: min(520px, 100vw);
1258
+ transform: translateX(105%);
1259
+ transition: transform 180ms ease;
1260
+ border-left: 1px solid var(--line);
1261
+ background: #111217;
1262
+ box-shadow: -20px 0 50px rgba(0, 0, 0, 0.35);
1263
+ padding: 18px;
1264
+ overflow: auto;
1265
+ }
1266
+ .drawer.open { transform: translateX(0); }
1267
+ .drawer h2 { margin: 0 0 12px; font-size: 26px; }
1268
+ .drawer p, .drawer li {
1269
+ color: #c4cad7;
1270
+ line-height: 1.58;
1271
+ }
1272
+ .drawer a { color: var(--amber); }
1273
+ .error {
1274
+ margin: 16px;
1275
+ padding: 14px;
1276
+ border: 1px solid rgba(255, 107, 122, 0.45);
1277
+ border-radius: 8px;
1278
+ background: rgba(255, 107, 122, 0.08);
1279
+ color: #ffd7dc;
1280
+ }
1281
+ @media (max-width: 1180px) {
1282
+ .grid {
1283
+ grid-template-columns: 260px minmax(0, 1fr);
1284
+ }
1285
+ .right-rail {
1286
+ grid-column: 1 / -1;
1287
+ }
1288
+ .right-rail .panel-body {
1289
+ display: grid;
1290
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1291
+ gap: 14px;
1292
+ }
1293
+ }
1294
+ @media (max-width: 760px) {
1295
+ header {
1296
+ align-items: flex-start;
1297
+ flex-direction: column;
1298
+ }
1299
+ .header-actions {
1300
+ width: 100%;
1301
+ }
1302
+ .ghost-btn, .primary-btn {
1303
+ flex: 1;
1304
+ }
1305
+ .grid {
1306
+ grid-template-columns: 1fr;
1307
+ padding: 10px;
1308
+ }
1309
+ .main-stack {
1310
+ grid-template-rows: 360px auto;
1311
+ }
1312
+ .right-rail .panel-body {
1313
+ display: block;
1314
+ }
1315
+ .landing h1 {
1316
+ font-size: clamp(40px, 14vw, 64px);
1317
+ }
1318
+ }
1319
+ </style>
1320
+ </head>
1321
+ <body>
1322
+ <section class="landing" id="landing">
1323
+ <div class="landing-inner">
1324
+ <div class="eyebrow">Small data benchmark arena</div>
1325
+ <h1>TabFM: Small Data Champion</h1>
1326
+ <p>Google's tabular foundation model faces XGBoost, LightGBM, Random Forest, and a linear baseline on tiny tabular training sets.</p>
1327
+ <button class="primary-btn" id="enterBtn">Enter arena</button>
1328
+ </div>
1329
+ </section>
1330
+
1331
+ <div class="shell">
1332
+ <header>
1333
+ <div class="brand">
1334
+ <div class="brand-mark">TF</div>
1335
+ <div>
1336
+ <h2>TabFM Small Data Champion</h2>
1337
+ <span id="runMeta">Loading benchmark...</span>
1338
+ </div>
1339
+ </div>
1340
+ <div class="header-actions">
1341
+ <button class="ghost-btn" id="aboutBtn">About</button>
1342
+ <a class="primary-btn" style="display:inline-grid;place-items:center;text-decoration:none;" href="https://huggingface.co/google/tabfm-1.0.0-pytorch" target="_blank" rel="noreferrer">Model</a>
1343
+ </div>
1344
+ </header>
1345
+
1346
+ <div id="errorBox" class="error" hidden></div>
1347
+ <div class="grid" id="appGrid" hidden>
1348
+ <aside class="sidebar">
1349
+ <section class="panel">
1350
+ <div class="panel-header">
1351
+ <h3 class="panel-title">Dataset</h3>
1352
+ </div>
1353
+ <div class="panel-body">
1354
+ <label for="datasetSelect">Benchmark table</label>
1355
+ <select id="datasetSelect"></select>
1356
+ <div class="metric-stack" style="margin-top:14px;">
1357
+ <div class="metric"><span>Rows</span><b id="datasetRows">-</b></div>
1358
+ <div class="metric"><span>Features</span><b id="datasetFeatures">-</b></div>
1359
+ <div class="metric"><span>Task</span><b id="datasetTask">-</b></div>
1360
+ </div>
1361
+ </div>
1362
+ </section>
1363
+ <section class="panel">
1364
+ <div class="panel-header">
1365
+ <h3 class="panel-title">Sample Size</h3>
1366
+ </div>
1367
+ <div class="panel-body">
1368
+ <div class="size-grid" id="sizeGrid"></div>
1369
+ </div>
1370
+ </section>
1371
+ <section class="panel">
1372
+ <div class="panel-header">
1373
+ <h3 class="panel-title">Run Status</h3>
1374
+ </div>
1375
+ <div class="panel-body">
1376
+ <div class="metric"><span>TabFM</span><b id="tabfmState">-</b></div>
1377
+ <div class="metric"><span>Rows</span><b id="rowCount">-</b></div>
1378
+ <div class="metric"><span>Startup</span><b id="elapsedMs">-</b></div>
1379
+ </div>
1380
+ </section>
1381
+ </aside>
1382
+
1383
+ <main class="main-stack">
1384
+ <section class="panel">
1385
+ <div class="panel-header">
1386
+ <h3 class="panel-title" id="lineTitle">Accuracy vs Sample Size</h3>
1387
+ <span class="status" id="metricBadge">accuracy</span>
1388
+ </div>
1389
+ <div class="chart-wrap">
1390
+ <canvas id="lineChart"></canvas>
1391
+ </div>
1392
+ </section>
1393
+ <section class="panel">
1394
+ <div class="panel-header">
1395
+ <h3 class="panel-title">Detailed Results</h3>
1396
+ <span class="status" id="selectedModelBadge">All models</span>
1397
+ </div>
1398
+ <div class="panel-body" style="overflow:auto;">
1399
+ <table>
1400
+ <thead>
1401
+ <tr>
1402
+ <th>Size</th>
1403
+ <th>Model</th>
1404
+ <th>Score</th>
1405
+ <th>F1 / R2</th>
1406
+ <th>Fit ms</th>
1407
+ <th>Infer ms</th>
1408
+ <th>Status</th>
1409
+ </tr>
1410
+ </thead>
1411
+ <tbody id="detailRows"></tbody>
1412
+ </table>
1413
+ </div>
1414
+ </section>
1415
+ </main>
1416
+
1417
+ <aside class="right-rail">
1418
+ <section class="panel" style="height:100%;">
1419
+ <div class="panel-header">
1420
+ <h3 class="panel-title">Leaderboard</h3>
1421
+ </div>
1422
+ <div class="panel-body">
1423
+ <div style="overflow:auto;">
1424
+ <table>
1425
+ <thead>
1426
+ <tr><th>Rank</th><th>Model</th><th>Score</th><th>Status</th></tr>
1427
+ </thead>
1428
+ <tbody id="leaderRows"></tbody>
1429
+ </table>
1430
+ </div>
1431
+ <div class="chart-wrap" style="height:240px;min-height:240px;padding:20px 0 0;">
1432
+ <canvas id="winChart"></canvas>
1433
+ </div>
1434
+ <div class="model-list" id="modelList" style="margin-top:14px;"></div>
1435
+ </div>
1436
+ </section>
1437
+ </aside>
1438
+ </div>
1439
+ </div>
1440
+
1441
+ <aside class="drawer" id="aboutDrawer">
1442
+ <button class="ghost-btn" id="closeAbout" style="float:right;">Close</button>
1443
+ <h2>About TabFM</h2>
1444
+ <p>TabFM is a tabular foundation model from Google Research. It uses the training rows as in-context examples instead of learning dataset-specific weights for each benchmark split.</p>
1445
+ <p>This Space installs TabFM from the official GitHub repository and attempts the real <a href="https://huggingface.co/google/tabfm-1.0.0-pytorch" target="_blank" rel="noreferrer">google/tabfm-1.0.0-pytorch</a> checkpoint during startup. The model artifact is large, so rows that exceed the cpu-basic startup budget are labelled directly in the tables.</p>
1446
+ <ul>
1447
+ <li>Classification metric: accuracy, with weighted F1 and ROC-AUC when available.</li>
1448
+ <li>Regression metric: R2, with RMSE and MAE in detailed rows.</li>
1449
+ <li>Competitors: XGBoost, LightGBM, Random Forest, and a linear baseline.</li>
1450
+ </ul>
1451
+ </aside>
1452
+
1453
+ <script>
1454
+ const COLORS = {
1455
+ "TabFM": "#ffb547",
1456
+ "XGBoost": "#3dd6c6",
1457
+ "LightGBM": "#72dc8d",
1458
+ "Random Forest": "#a78bfa",
1459
+ "Linear Baseline": "#ff6b7a"
1460
+ };
1461
+ const state = {
1462
+ payload: null,
1463
+ datasetId: null,
1464
+ sampleSize: null,
1465
+ selectedModel: null,
1466
+ lineChart: null,
1467
+ winChart: null
1468
+ };
1469
+
1470
+ const formatScore = (value, metric) => {
1471
+ if (value === null || value === undefined || Number.isNaN(Number(value))) return "-";
1472
+ if (metric === "rmse" || metric === "mae") return Number(value).toFixed(3);
1473
+ return Number(value).toFixed(3);
1474
+ };
1475
+ const formatMs = value => value === null || value === undefined ? "-" : Number(value).toFixed(1);
1476
+ const statusClass = status => status === "ok" ? "ok" : "warn";
1477
+ const datasetById = id => state.payload.datasets.find(item => item.id === id);
1478
+ const rowsForDataset = () => state.payload.rows.filter(row => row.dataset_id === state.datasetId);
1479
+ const rowsForSelection = () => rowsForDataset().filter(row => Number(row.sample_size) === Number(state.sampleSize));
1480
+
1481
+ document.getElementById("enterBtn").addEventListener("click", () => {
1482
+ document.getElementById("landing").classList.add("hidden");
1483
+ });
1484
+ document.getElementById("aboutBtn").addEventListener("click", () => {
1485
+ document.getElementById("aboutDrawer").classList.add("open");
1486
+ });
1487
+ document.getElementById("closeAbout").addEventListener("click", () => {
1488
+ document.getElementById("aboutDrawer").classList.remove("open");
1489
+ });
1490
+
1491
+ async function loadPayload() {
1492
+ const response = await fetch("/api/benchmark-results");
1493
+ if (!response.ok) throw new Error(`Benchmark API returned ${response.status}`);
1494
+ state.payload = await response.json();
1495
+ state.datasetId = state.payload.datasets[0].id;
1496
+ state.sampleSize = state.payload.datasets[0].sample_sizes[0];
1497
+ state.selectedModel = null;
1498
+ document.getElementById("appGrid").hidden = false;
1499
+ renderAll();
1500
+ }
1501
+
1502
+ function renderAll() {
1503
+ renderHeader();
1504
+ renderDatasetControls();
1505
+ renderDatasetStats();
1506
+ renderSizeButtons();
1507
+ renderLineChart();
1508
+ renderLeaderboard();
1509
+ renderWinChart();
1510
+ renderModelCards();
1511
+ renderDetails();
1512
+ }
1513
+
1514
+ function renderHeader() {
1515
+ const p = state.payload;
1516
+ document.getElementById("runMeta").textContent = `${p.benchmark_mode} | ${new Date(p.generated_at_unix * 1000).toLocaleString()}`;
1517
+ document.getElementById("tabfmState").textContent = p.tabfm.status || "unknown";
1518
+ document.getElementById("rowCount").textContent = p.rows.length;
1519
+ document.getElementById("elapsedMs").textContent = `${Math.round(p.elapsed_ms)} ms`;
1520
+ }
1521
+
1522
+ function renderDatasetControls() {
1523
+ const select = document.getElementById("datasetSelect");
1524
+ select.innerHTML = state.payload.datasets.map(ds => `<option value="${ds.id}">${ds.name}</option>`).join("");
1525
+ select.value = state.datasetId;
1526
+ select.onchange = event => {
1527
+ state.datasetId = event.target.value;
1528
+ const ds = datasetById(state.datasetId);
1529
+ state.sampleSize = ds.sample_sizes[0];
1530
+ renderAll();
1531
+ };
1532
+ }
1533
+
1534
+ function renderDatasetStats() {
1535
+ const ds = datasetById(state.datasetId);
1536
+ document.getElementById("datasetRows").textContent = ds.rows;
1537
+ document.getElementById("datasetFeatures").textContent = ds.features;
1538
+ document.getElementById("datasetTask").textContent = ds.task === "classification" ? "Class" : "Reg";
1539
+ document.getElementById("lineTitle").textContent = `${ds.name}: ${ds.task === "classification" ? "Accuracy" : "R2"} vs Sample Size`;
1540
+ document.getElementById("metricBadge").textContent = ds.task === "classification" ? "accuracy" : "r2";
1541
+ }
1542
+
1543
+ function renderSizeButtons() {
1544
+ const ds = datasetById(state.datasetId);
1545
+ const grid = document.getElementById("sizeGrid");
1546
+ grid.innerHTML = ds.sample_sizes.map(size => `
1547
+ <button class="size-btn ${Number(size) === Number(state.sampleSize) ? "active" : ""}" data-size="${size}">${size}</button>
1548
+ `).join("");
1549
+ grid.querySelectorAll("button").forEach(btn => {
1550
+ btn.addEventListener("click", () => {
1551
+ state.sampleSize = Number(btn.dataset.size);
1552
+ renderAll();
1553
+ });
1554
+ });
1555
+ }
1556
+
1557
+ function renderLineChart() {
1558
+ const ds = datasetById(state.datasetId);
1559
+ const sizes = ds.sample_sizes;
1560
+ const datasets = state.payload.models.map(model => {
1561
+ const points = sizes.map(size => {
1562
+ const row = state.payload.rows.find(item =>
1563
+ item.dataset_id === state.datasetId &&
1564
+ item.model_name === model.model_name &&
1565
+ Number(item.sample_size) === Number(size)
1566
+ );
1567
+ return row && row.status === "ok" ? row.primary_score : null;
1568
+ });
1569
+ return {
1570
+ label: model.model_name,
1571
+ data: points,
1572
+ borderColor: COLORS[model.model_name],
1573
+ backgroundColor: COLORS[model.model_name],
1574
+ pointRadius: 4,
1575
+ borderWidth: model.model_name === "TabFM" ? 4 : 2,
1576
+ tension: 0.25,
1577
+ spanGaps: false
1578
+ };
1579
+ });
1580
+ const ctx = document.getElementById("lineChart");
1581
+ if (state.lineChart) state.lineChart.destroy();
1582
+ state.lineChart = new Chart(ctx, {
1583
+ type: "line",
1584
+ data: { labels: sizes, datasets },
1585
+ options: {
1586
+ responsive: true,
1587
+ maintainAspectRatio: false,
1588
+ interaction: { mode: "nearest", intersect: false },
1589
+ scales: {
1590
+ x: { grid: { color: "rgba(255,255,255,0.06)" }, ticks: { color: "#a6adbb" }, title: { display: true, text: "Training rows", color: "#a6adbb" } },
1591
+ y: { grid: { color: "rgba(255,255,255,0.06)" }, ticks: { color: "#a6adbb" }, suggestedMin: 0, suggestedMax: 1 }
1592
+ },
1593
+ plugins: {
1594
+ legend: { labels: { color: "#dce0ea", usePointStyle: true, boxWidth: 8 } },
1595
+ tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${formatScore(ctx.parsed.y)}` } }
1596
+ }
1597
+ }
1598
+ });
1599
+ }
1600
+
1601
+ function renderLeaderboard() {
1602
+ const rows = rowsForSelection().slice().sort((a, b) => {
1603
+ const av = a.primary_score === null ? -Infinity : Number(a.primary_score);
1604
+ const bv = b.primary_score === null ? -Infinity : Number(b.primary_score);
1605
+ return bv - av;
1606
+ });
1607
+ const body = document.getElementById("leaderRows");
1608
+ body.innerHTML = rows.map((row, index) => `
1609
+ <tr>
1610
+ <td class="${index === 0 && row.status === "ok" ? "leader" : ""}">${row.status === "ok" ? index + 1 : "-"}</td>
1611
+ <td>${row.model_name}</td>
1612
+ <td class="score">${formatScore(row.primary_score, row.primary_metric)}</td>
1613
+ <td><span class="status ${statusClass(row.status)}">${row.status}</span></td>
1614
+ </tr>
1615
+ `).join("");
1616
+ }
1617
+
1618
+ function renderWinChart() {
1619
+ const labels = state.payload.win_rates.map(row => row.model_name);
1620
+ const values = state.payload.win_rates.map(row => Math.round(row.win_rate * 1000) / 10);
1621
+ const colors = labels.map(label => COLORS[label]);
1622
+ const ctx = document.getElementById("winChart");
1623
+ if (state.winChart) state.winChart.destroy();
1624
+ state.winChart = new Chart(ctx, {
1625
+ type: "bar",
1626
+ data: { labels, datasets: [{ label: "Win rate", data: values, backgroundColor: colors, borderWidth: 0 }] },
1627
+ options: {
1628
+ responsive: true,
1629
+ maintainAspectRatio: false,
1630
+ scales: {
1631
+ x: { grid: { display: false }, ticks: { color: "#a6adbb" } },
1632
+ y: { grid: { color: "rgba(255,255,255,0.06)" }, ticks: { color: "#a6adbb", callback: value => `${value}%` }, suggestedMin: 0, suggestedMax: 100 }
1633
+ },
1634
+ plugins: { legend: { display: false } }
1635
+ }
1636
+ });
1637
+ }
1638
+
1639
+ function renderModelCards() {
1640
+ const list = document.getElementById("modelList");
1641
+ list.innerHTML = state.payload.models.map(model => {
1642
+ const isActive = state.selectedModel === model.model_name;
1643
+ const score = model.average_primary_score === null ? "-" : Number(model.average_primary_score).toFixed(3);
1644
+ const status = model.model_name === "TabFM" ? model.runtime_status : "ok";
1645
+ return `
1646
+ <article class="model-card ${isActive ? "active" : ""}" data-model="${model.model_name}">
1647
+ <h3><span>${model.model_name}</span><span class="status ${statusClass(status)}">${status}</span></h3>
1648
+ <p>${model.type} | avg score ${score} | measured rows ${model.measured_rows}</p>
1649
+ </article>
1650
+ `;
1651
+ }).join("");
1652
+ list.querySelectorAll(".model-card").forEach(card => {
1653
+ card.addEventListener("click", () => {
1654
+ const model = card.dataset.model;
1655
+ state.selectedModel = state.selectedModel === model ? null : model;
1656
+ renderModelCards();
1657
+ renderDetails();
1658
+ });
1659
+ });
1660
+ }
1661
+
1662
+ function renderDetails() {
1663
+ document.getElementById("selectedModelBadge").textContent = state.selectedModel || "All models";
1664
+ let rows = rowsForDataset().slice().sort((a, b) => Number(a.sample_size) - Number(b.sample_size) || a.model_name.localeCompare(b.model_name));
1665
+ if (state.selectedModel) rows = rows.filter(row => row.model_name === state.selectedModel);
1666
+ const body = document.getElementById("detailRows");
1667
+ body.innerHTML = rows.map(row => {
1668
+ const secondary = row.task === "classification" ? row.f1 : row.r2;
1669
+ const title = row.note ? ` title="${String(row.note).replaceAll('"', "&quot;")}"` : "";
1670
+ return `
1671
+ <tr${title}>
1672
+ <td>${row.sample_size}</td>
1673
+ <td>${row.model_name}</td>
1674
+ <td class="score">${formatScore(row.primary_score, row.primary_metric)}</td>
1675
+ <td>${formatScore(secondary)}</td>
1676
+ <td>${formatMs(row.train_time_ms)}</td>
1677
+ <td>${formatMs(row.inference_time_ms)}</td>
1678
+ <td><span class="status ${statusClass(row.status)}">${row.status}</span></td>
1679
+ </tr>
1680
+ `;
1681
+ }).join("");
1682
+ }
1683
+
1684
+ loadPayload().catch(error => {
1685
+ const box = document.getElementById("errorBox");
1686
+ box.hidden = false;
1687
+ box.textContent = error.message;
1688
+ });
1689
+ </script>
1690
+ </body>
1691
+ </html>
1692
+ """
1693
+
1694
+
1695
+ if __name__ == "__main__":
1696
+ app.launch(
1697
+ server_name="0.0.0.0",
1698
+ server_port=int(os.environ.get("PORT", "7860")),
1699
+ quiet=True,
1700
+ )
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.19.0
2
+ spaces==0.50.4
3
+ numpy==2.2.6
4
+ pandas==2.2.3
5
+ scikit-learn==1.6.1
6
+ xgboost==2.1.4
7
+ lightgbm==4.6.0
8
+ huggingface_hub==0.36.2
9
+ tabfm[pytorch] @ git+https://github.com/google-research/tabfm.git@53f3fcfb8a3355f55c9fb49f04fbb62b8ba29109
rollout.jsonl ADDED
The diff for this file is too large to render. See raw diff