corbanyax commited on
Commit
123cdfa
·
verified ·
1 Parent(s): fcc2e06

Launch tabBench arena

Browse files
Files changed (4) hide show
  1. DEPLOY.md +33 -0
  2. README.md +56 -7
  3. app.py +772 -0
  4. requirements.txt +9 -0
DEPLOY.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploy tabBench to Hugging Face Spaces
2
+
3
+ This Space is configured to appear under `google/tabfm-1.0.0-pytorch` because `README.md` includes:
4
+
5
+ ```yaml
6
+ models:
7
+ - google/tabfm-1.0.0-pytorch
8
+ ```
9
+
10
+ After publishing, Hugging Face may take a short time to re-index the model page's **Spaces using this model** section.
11
+
12
+ ## One-shot upload
13
+
14
+ Replace `YOUR_USERNAME` with your Hugging Face username or organization.
15
+
16
+ ```bash
17
+ hf auth login
18
+ hf repo create YOUR_USERNAME/tabBench --type space --space-sdk gradio --exist-ok
19
+ hf upload YOUR_USERNAME/tabBench . . --repo-type space \
20
+ --include app.py \
21
+ --include README.md \
22
+ --include requirements.txt \
23
+ --commit-message "Launch tabBench"
24
+ ```
25
+
26
+ ## Suggested hardware
27
+
28
+ Start with `cpu-upgrade` for baseline-only runs. For reliable live TabFM benchmarking, use a GPU flavor such as `t4-small` or better:
29
+
30
+ ```bash
31
+ hf repo create YOUR_USERNAME/tabBench --type space --space-sdk gradio --flavor t4-small --exist-ok
32
+ ```
33
+
README.md CHANGED
@@ -1,13 +1,62 @@
1
  ---
2
- title: TabBench
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: red
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: tabBench
3
+ emoji: 🧮
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 5.35.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ python_version: 3.11
12
+ models:
13
+ - google/tabfm-1.0.0-pytorch
14
+ tags:
15
+ - tabular-classification
16
+ - tabular-regression
17
+ - benchmark
18
+ - leaderboard
19
+ - tabfm
20
  ---
21
 
22
+ # tabBench
23
+
24
+ A Hugging Face Space for benchmarking Google's TabFM against practical tabular classification and regression baselines.
25
+
26
+ This Space is linked to [`google/tabfm-1.0.0-pytorch`](https://huggingface.co/google/tabfm-1.0.0-pytorch), so it should appear in the model page's **Spaces using this model** section after the Space is pushed and indexed by Hugging Face.
27
+
28
+ The app includes:
29
+
30
+ - A benchmark catalog with 10+ common tabular tasks, including Titanic-style survival, housing prices, fraud detection, recipe ratings, Halloween candy ranking, and classic sklearn datasets.
31
+ - A leaderboard with model metrics, timing, and task-aware ranking.
32
+ - Charts for accuracy/F1/AUC or RMSE/MAE/R2.
33
+ - Controls for sample size, train/test split, random seed, model selection, and TabFM inclusion.
34
+ - A CSV upload flow so visitors can run the same arena on their own dataset.
35
+
36
+ The built-in catalog uses real open datasets where possible:
37
+
38
+ - OpenML: Titanic, Ames Housing, Adult Income
39
+ - KaggleHub: Credit Card Fraud, Epicurious Recipes
40
+ - FiveThirtyEight GitHub data: Halloween Candy
41
+ - sklearn: California Housing, Iris, Wine, Breast Cancer, Digits, Diabetes
42
+
43
+ TabFM is loaded through the public Google Research package when available. The app keeps graceful fallbacks so the Space still works on CPU-only or dependency-constrained runtimes.
44
+
45
+ Note: TabFM's weights use their own non-commercial license. Review the upstream model license before using this Space commercially.
46
+
47
+ Links:
48
+
49
+ - GitHub: https://github.com/google-research/tabfm
50
+ - Hugging Face weights: https://huggingface.co/google/tabfm-1.0.0-pytorch
51
+
52
+ ## Deploy
53
+
54
+ ```bash
55
+ hf auth login
56
+ hf repo create YOUR_USERNAME/tabBench --type space --space-sdk gradio --exist-ok
57
+ hf upload YOUR_USERNAME/tabBench . . --repo-type space \
58
+ --include app.py \
59
+ --include README.md \
60
+ --include requirements.txt \
61
+ --commit-message "Launch tabBench"
62
+ ```
app.py ADDED
@@ -0,0 +1,772 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import math
5
+ import time
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+ from typing import Callable
10
+
11
+ import gradio as gr
12
+ import numpy as np
13
+ import pandas as pd
14
+ import plotly.express as px
15
+ import plotly.graph_objects as go
16
+ from sklearn.base import clone
17
+ from sklearn.compose import ColumnTransformer
18
+ from sklearn.datasets import fetch_california_housing, fetch_openml, load_breast_cancer, load_diabetes, load_digits, load_iris, load_wine, make_classification
19
+ from sklearn.dummy import DummyClassifier, DummyRegressor
20
+ from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor, RandomForestClassifier, RandomForestRegressor
21
+ from sklearn.impute import SimpleImputer
22
+ from sklearn.linear_model import LogisticRegression, Ridge
23
+ from sklearn.metrics import (
24
+ accuracy_score,
25
+ f1_score,
26
+ mean_absolute_error,
27
+ mean_squared_error,
28
+ r2_score,
29
+ roc_auc_score,
30
+ )
31
+ from sklearn.model_selection import train_test_split
32
+ from sklearn.pipeline import Pipeline
33
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
34
+
35
+
36
+ APP_TITLE = "tabBench"
37
+ TABFM_MODEL_ID = "google/tabfm-1.0.0-pytorch"
38
+ RANDOM_STATE = 42
39
+ CANDY_DATA_URL = "https://raw.githubusercontent.com/fivethirtyeight/data/master/candy-power-ranking/candy-data.csv"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class DatasetSpec:
44
+ name: str
45
+ task: str
46
+ target: str
47
+ source: str
48
+ rows: int
49
+ description: str
50
+ loader: Callable[[int, int], pd.DataFrame]
51
+
52
+
53
+ def _add_categorical_noise(df: pd.DataFrame, rng: np.random.Generator, prefix: str) -> pd.DataFrame:
54
+ df = df.copy()
55
+ df[f"{prefix}_segment"] = rng.choice(["A", "B", "C", "D"], len(df), p=[0.35, 0.25, 0.25, 0.15])
56
+ df[f"{prefix}_region"] = rng.choice(["north", "south", "east", "west"], len(df))
57
+ return df
58
+
59
+
60
+ def sample_df(df: pd.DataFrame, limit: int, seed: int) -> pd.DataFrame:
61
+ return df.sample(min(limit, len(df)), random_state=seed).reset_index(drop=True)
62
+
63
+
64
+ def find_first_data_file(root: str | Path, suffixes: tuple[str, ...]) -> Path:
65
+ root = Path(root)
66
+ for suffix in suffixes:
67
+ matches = sorted(root.rglob(f"*{suffix}"))
68
+ if matches:
69
+ return matches[0]
70
+ raise FileNotFoundError(f"No data file with suffixes {suffixes} found in {root}")
71
+
72
+
73
+ @lru_cache(maxsize=1)
74
+ def openml_titanic() -> pd.DataFrame:
75
+ data = fetch_openml(data_id=40945, as_frame=True, parser="auto")
76
+ df = data.frame.copy()
77
+ keep = [c for c in ["pclass", "sex", "age", "sibsp", "parch", "fare", "embarked", "survived"] if c in df.columns]
78
+ return df[keep].dropna(subset=["survived"])
79
+
80
+
81
+ @lru_cache(maxsize=1)
82
+ def openml_ames_housing() -> pd.DataFrame:
83
+ data = fetch_openml(data_id=42165, as_frame=True, parser="auto")
84
+ df = data.frame.copy()
85
+ target = "SalePrice" if "SalePrice" in df.columns else data.target_names[0]
86
+ useful = [
87
+ "OverallQual",
88
+ "GrLivArea",
89
+ "GarageCars",
90
+ "GarageArea",
91
+ "TotalBsmtSF",
92
+ "FullBath",
93
+ "YearBuilt",
94
+ "Neighborhood",
95
+ "HouseStyle",
96
+ target,
97
+ ]
98
+ cols = [c for c in useful if c in df.columns]
99
+ return df[cols].rename(columns={target: "sale_price"}).dropna(subset=["sale_price"])
100
+
101
+
102
+ @lru_cache(maxsize=1)
103
+ def openml_adult_income() -> pd.DataFrame:
104
+ data = fetch_openml(data_id=1590, as_frame=True, parser="auto")
105
+ df = data.frame.copy()
106
+ if "class" in df.columns:
107
+ df = df.rename(columns={"class": "income_gt_50k"})
108
+ elif "income" in df.columns:
109
+ df = df.rename(columns={"income": "income_gt_50k"})
110
+ return df.dropna(subset=["income_gt_50k"])
111
+
112
+
113
+ @lru_cache(maxsize=1)
114
+ def sklearn_california_housing() -> pd.DataFrame:
115
+ data = fetch_california_housing(as_frame=True)
116
+ df = data.frame.rename(columns={"MedHouseVal": "median_house_value"})
117
+ return df
118
+
119
+
120
+ @lru_cache(maxsize=1)
121
+ def fivethirtyeight_candy() -> pd.DataFrame:
122
+ df = pd.read_csv(CANDY_DATA_URL)
123
+ return df.drop(columns=[c for c in ["competitorname"] if c in df.columns]).dropna(subset=["winpercent"])
124
+
125
+
126
+ @lru_cache(maxsize=1)
127
+ def kaggle_credit_card_fraud() -> pd.DataFrame:
128
+ import kagglehub
129
+
130
+ path = kagglehub.dataset_download("mlg-ulb/creditcardfraud")
131
+ csv_path = find_first_data_file(path, (".csv",))
132
+ df = pd.read_csv(csv_path)
133
+ if "Class" in df.columns:
134
+ df = df.rename(columns={"Class": "fraud"})
135
+ return df.dropna(subset=["fraud"])
136
+
137
+
138
+ @lru_cache(maxsize=1)
139
+ def kaggle_epirecipes() -> pd.DataFrame:
140
+ import kagglehub
141
+
142
+ path = kagglehub.dataset_download("hugodarwood/epirecipes")
143
+ try:
144
+ json_path = find_first_data_file(path, (".json",))
145
+ df = pd.read_json(json_path)
146
+ except FileNotFoundError:
147
+ csv_path = find_first_data_file(path, (".csv",))
148
+ df = pd.read_csv(csv_path)
149
+ if "rating" not in df.columns:
150
+ raise ValueError("Epicurious dataset does not include a rating column.")
151
+ preferred = [
152
+ "calories",
153
+ "protein",
154
+ "fat",
155
+ "sodium",
156
+ "dessert",
157
+ "dinner",
158
+ "breakfast",
159
+ "healthy",
160
+ "vegetarian",
161
+ "vegan",
162
+ "rating",
163
+ ]
164
+ cols = [c for c in preferred if c in df.columns]
165
+ return df[cols].dropna(subset=["rating"])
166
+
167
+
168
+ def load_iris_df(limit: int, seed: int) -> pd.DataFrame:
169
+ data = load_iris(as_frame=True)
170
+ df = data.frame.rename(columns={"target": "species"})
171
+ df["species"] = df["species"].map(dict(enumerate(data.target_names)))
172
+ return df.sample(min(limit, len(df)), random_state=seed)
173
+
174
+
175
+ def load_wine_df(limit: int, seed: int) -> pd.DataFrame:
176
+ data = load_wine(as_frame=True)
177
+ df = data.frame.rename(columns={"target": "wine_class"})
178
+ return df.sample(min(limit, len(df)), random_state=seed)
179
+
180
+
181
+ def load_breast_cancer_df(limit: int, seed: int) -> pd.DataFrame:
182
+ data = load_breast_cancer(as_frame=True)
183
+ df = data.frame.rename(columns={"target": "diagnosis"})
184
+ df["diagnosis"] = df["diagnosis"].map({0: "malignant", 1: "benign"})
185
+ return df.sample(min(limit, len(df)), random_state=seed)
186
+
187
+
188
+ def load_digits_df(limit: int, seed: int) -> pd.DataFrame:
189
+ data = load_digits(as_frame=True)
190
+ df = data.frame.rename(columns={"target": "digit"})
191
+ return df.sample(min(limit, len(df)), random_state=seed)
192
+
193
+
194
+ def load_diabetes_df(limit: int, seed: int) -> pd.DataFrame:
195
+ data = load_diabetes(as_frame=True)
196
+ df = data.frame.rename(columns={"target": "disease_progression"})
197
+ return df.sample(min(limit, len(df)), random_state=seed)
198
+
199
+
200
+ def load_california_housing_df(limit: int, seed: int) -> pd.DataFrame:
201
+ try:
202
+ return sample_df(sklearn_california_housing(), limit, seed)
203
+ except Exception:
204
+ return load_synthetic_housing_df(limit, seed).rename(columns={"sale_price": "median_house_value"})
205
+
206
+
207
+ def load_ames_housing_df(limit: int, seed: int) -> pd.DataFrame:
208
+ try:
209
+ return sample_df(openml_ames_housing(), limit, seed)
210
+ except Exception:
211
+ return load_synthetic_housing_df(limit, seed)
212
+
213
+
214
+ def load_synthetic_housing_df(limit: int, seed: int) -> pd.DataFrame:
215
+ rng = np.random.default_rng(seed)
216
+ n = min(limit, 12000)
217
+ bedrooms = rng.integers(1, 7, n)
218
+ sqft = rng.normal(1750, 650, n).clip(450, 5200)
219
+ age = rng.integers(0, 90, n)
220
+ zipcode = rng.choice(["94016", "98101", "10011", "60614", "78704", "30309"], n)
221
+ price = 120000 + sqft * rng.normal(230, 20, n) + bedrooms * 18000 - age * 1400
222
+ price += pd.Series(zipcode).map({"94016": 260000, "98101": 140000, "10011": 210000, "60614": 80000, "78704": 110000, "30309": 70000}).to_numpy()
223
+ price += rng.normal(0, 45000, n)
224
+ return pd.DataFrame(
225
+ {
226
+ "sqft": sqft.round(0),
227
+ "bedrooms": bedrooms,
228
+ "home_age": age,
229
+ "zipcode": zipcode,
230
+ "has_garage": rng.choice(["yes", "no"], n, p=[0.72, 0.28]),
231
+ "sale_price": price.round(0),
232
+ }
233
+ )
234
+
235
+
236
+ def load_titanic_proxy_df(limit: int, seed: int) -> pd.DataFrame:
237
+ rng = np.random.default_rng(seed)
238
+ n = min(limit, 891)
239
+ sex = rng.choice(["female", "male"], n, p=[0.38, 0.62])
240
+ pclass = rng.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55])
241
+ age = rng.normal(30, 14, n).clip(0.5, 78)
242
+ fare = np.exp(rng.normal(3.1, 0.85, n)) * (4 - pclass)
243
+ embarked = rng.choice(["S", "C", "Q"], n, p=[0.72, 0.19, 0.09])
244
+ logit = 1.6 * (sex == "female") + 0.9 * (pclass == 1) + 0.25 * (pclass == 2) - 0.025 * age + 0.01 * fare - 1.1
245
+ survived = rng.binomial(1, 1 / (1 + np.exp(-logit)))
246
+ return pd.DataFrame(
247
+ {
248
+ "pclass": pclass,
249
+ "sex": sex,
250
+ "age": age.round(1),
251
+ "sibsp": rng.integers(0, 5, n),
252
+ "parch": rng.integers(0, 4, n),
253
+ "fare": fare.round(2),
254
+ "embarked": embarked,
255
+ "survived": survived,
256
+ }
257
+ )
258
+
259
+
260
+ def load_titanic_df(limit: int, seed: int) -> pd.DataFrame:
261
+ try:
262
+ return sample_df(openml_titanic(), limit, seed)
263
+ except Exception:
264
+ return load_titanic_proxy_df(limit, seed)
265
+
266
+
267
+ def load_credit_fraud_proxy_df(limit: int, seed: int) -> pd.DataFrame:
268
+ rng = np.random.default_rng(seed)
269
+ n = min(limit, 50000)
270
+ x, y = make_classification(
271
+ n_samples=n,
272
+ n_features=18,
273
+ n_informative=8,
274
+ n_redundant=4,
275
+ weights=[0.985, 0.015],
276
+ class_sep=1.6,
277
+ random_state=seed,
278
+ )
279
+ df = pd.DataFrame(x, columns=[f"v{i}" for i in range(1, 19)])
280
+ df["amount"] = np.exp(rng.normal(3.2, 1.0, n)).round(2)
281
+ df["merchant_category"] = rng.choice(["travel", "grocery", "electronics", "fuel", "cash"], n)
282
+ df["fraud"] = y
283
+ return df
284
+
285
+
286
+ def load_credit_fraud_df(limit: int, seed: int) -> pd.DataFrame:
287
+ try:
288
+ return sample_df(kaggle_credit_card_fraud(), limit, seed)
289
+ except Exception:
290
+ return load_credit_fraud_proxy_df(limit, seed)
291
+
292
+
293
+ def load_epirecipes_proxy_df(limit: int, seed: int) -> pd.DataFrame:
294
+ rng = np.random.default_rng(seed)
295
+ n = min(limit, 20000)
296
+ calories = rng.gamma(4, 120, n)
297
+ protein = rng.gamma(2, 12, n)
298
+ fat = rng.gamma(2.5, 9, n)
299
+ sodium = rng.gamma(2.4, 180, n)
300
+ course = rng.choice(["main", "dessert", "side", "salad", "breakfast"], n)
301
+ cuisine = rng.choice(["american", "italian", "mexican", "asian", "mediterranean"], n)
302
+ rating = 2.8 + 0.12 * (course == "dessert") + 0.18 * (cuisine == "italian") - 0.00035 * sodium + rng.normal(0, 0.65, n)
303
+ return pd.DataFrame(
304
+ {
305
+ "calories": calories.round(0),
306
+ "protein": protein.round(1),
307
+ "fat": fat.round(1),
308
+ "sodium": sodium.round(0),
309
+ "course": course,
310
+ "cuisine": cuisine,
311
+ "make_again": rng.choice(["yes", "no"], n, p=[0.66, 0.34]),
312
+ "rating": rating.clip(0, 5).round(2),
313
+ }
314
+ )
315
+
316
+
317
+ def load_epirecipes_df(limit: int, seed: int) -> pd.DataFrame:
318
+ try:
319
+ return sample_df(kaggle_epirecipes(), limit, seed)
320
+ except Exception:
321
+ return load_epirecipes_proxy_df(limit, seed)
322
+
323
+
324
+ def load_candy_proxy_df(limit: int, seed: int) -> pd.DataFrame:
325
+ rng = np.random.default_rng(seed)
326
+ n = min(limit, 1200)
327
+ chocolate = rng.binomial(1, 0.45, n)
328
+ fruity = rng.binomial(1, 0.38, n)
329
+ caramel = rng.binomial(1, 0.24, n)
330
+ pricepercent = rng.beta(2, 4, n)
331
+ sugarpercent = rng.beta(3, 2, n)
332
+ winpercent = 35 + 18 * chocolate + 8 * caramel + 9 * sugarpercent - 10 * pricepercent + rng.normal(0, 8, n)
333
+ return pd.DataFrame(
334
+ {
335
+ "chocolate": chocolate,
336
+ "fruity": fruity,
337
+ "caramel": caramel,
338
+ "peanutyalmondy": rng.binomial(1, 0.2, n),
339
+ "nougat": rng.binomial(1, 0.14, n),
340
+ "crispedricewafer": rng.binomial(1, 0.16, n),
341
+ "hard": rng.binomial(1, 0.28, n),
342
+ "bar": rng.binomial(1, 0.36, n),
343
+ "sugarpercent": sugarpercent.round(3),
344
+ "pricepercent": pricepercent.round(3),
345
+ "winpercent": winpercent.clip(5, 95).round(2),
346
+ }
347
+ )
348
+
349
+
350
+ def load_candy_df(limit: int, seed: int) -> pd.DataFrame:
351
+ try:
352
+ return sample_df(fivethirtyeight_candy(), limit, seed)
353
+ except Exception:
354
+ return load_candy_proxy_df(limit, seed)
355
+
356
+
357
+ def load_adult_income_proxy_df(limit: int, seed: int) -> pd.DataFrame:
358
+ rng = np.random.default_rng(seed)
359
+ n = min(limit, 30000)
360
+ education_num = rng.integers(6, 17, n)
361
+ hours = rng.normal(40, 12, n).clip(1, 80)
362
+ age = rng.normal(39, 13, n).clip(18, 75)
363
+ occupation = rng.choice(["tech", "sales", "ops", "admin", "service", "exec"], n)
364
+ logit = -6 + 0.16 * age + 0.36 * education_num + 0.035 * hours + 0.9 * (occupation == "exec") + 0.55 * (occupation == "tech")
365
+ income = rng.binomial(1, 1 / (1 + np.exp(-logit)))
366
+ return pd.DataFrame(
367
+ {
368
+ "age": age.round(0),
369
+ "education_num": education_num,
370
+ "hours_per_week": hours.round(0),
371
+ "occupation": occupation,
372
+ "marital_status": rng.choice(["single", "married", "divorced"], n),
373
+ "income_gt_50k": income,
374
+ }
375
+ )
376
+
377
+
378
+ def load_adult_income_df(limit: int, seed: int) -> pd.DataFrame:
379
+ try:
380
+ return sample_df(openml_adult_income(), limit, seed)
381
+ except Exception:
382
+ return load_adult_income_proxy_df(limit, seed)
383
+
384
+
385
+ def load_bike_demand_proxy_df(limit: int, seed: int) -> pd.DataFrame:
386
+ rng = np.random.default_rng(seed)
387
+ n = min(limit, 15000)
388
+ hour = rng.integers(0, 24, n)
389
+ temp = rng.normal(21, 9, n).clip(-5, 40)
390
+ workingday = rng.binomial(1, 0.69, n)
391
+ weather = rng.choice(["clear", "mist", "rain", "storm"], n, p=[0.55, 0.28, 0.14, 0.03])
392
+ commute_peak = ((hour >= 7) & (hour <= 9)) | ((hour >= 16) & (hour <= 18))
393
+ count = 80 + 115 * commute_peak + 5.5 * temp + 45 * workingday - 75 * (weather == "rain") - 130 * (weather == "storm")
394
+ count += rng.normal(0, 45, n)
395
+ return pd.DataFrame({"hour": hour, "temp": temp.round(1), "workingday": workingday, "weather": weather, "rental_count": count.clip(0).round(0)})
396
+
397
+
398
+ DATASETS: list[DatasetSpec] = [
399
+ DatasetSpec("Titanic Survival", "classification", "survived", "OpenML data_id=40945", 1309, "Mixed categorical/numeric binary classification.", load_titanic_df),
400
+ DatasetSpec("Ames Housing Prices", "regression", "sale_price", "OpenML data_id=42165", 1460, "Ames real-estate regression with neighborhood and quality features.", load_ames_housing_df),
401
+ DatasetSpec("California Housing", "regression", "median_house_value", "sklearn California housing", 20640, "Block-level California housing value regression.", load_california_housing_df),
402
+ DatasetSpec("Credit Card Fraud", "classification", "fraud", "KaggleHub mlg-ulb/creditcardfraud", 284807, "Large imbalanced binary fraud task.", load_credit_fraud_df),
403
+ DatasetSpec("Epicurious Recipes", "regression", "rating", "KaggleHub hugodarwood/epirecipes", 20000, "Recipe nutrition and tags to rating.", load_epirecipes_df),
404
+ DatasetSpec("Halloween Candy", "regression", "winpercent", "FiveThirtyEight GitHub CSV", 85, "Candy attributes to popularity score.", load_candy_df),
405
+ DatasetSpec("Adult Income", "classification", "income_gt_50k", "OpenML data_id=1590", 48842, "Demographic and work attributes to income bucket.", load_adult_income_df),
406
+ DatasetSpec("Bike Demand", "regression", "rental_count", "Kaggle-style proxy", 15000, "Weather and time features to rental demand.", load_bike_demand_proxy_df),
407
+ DatasetSpec("Iris", "classification", "species", "sklearn", 150, "Classic multi-class flower classification.", load_iris_df),
408
+ DatasetSpec("Wine", "classification", "wine_class", "sklearn", 178, "Chemical analysis to cultivar class.", load_wine_df),
409
+ DatasetSpec("Breast Cancer", "classification", "diagnosis", "sklearn", 569, "Diagnostic measurements to benign/malignant label.", load_breast_cancer_df),
410
+ DatasetSpec("Digits", "classification", "digit", "sklearn", 1797, "Pixel features to handwritten digit class.", load_digits_df),
411
+ DatasetSpec("Diabetes", "regression", "disease_progression", "sklearn", 442, "Clinical variables to disease progression.", load_diabetes_df),
412
+ ]
413
+
414
+
415
+ def dataset_names() -> list[str]:
416
+ return [d.name for d in DATASETS]
417
+
418
+
419
+ def get_spec(name: str) -> DatasetSpec:
420
+ return next(d for d in DATASETS if d.name == name)
421
+
422
+
423
+ def get_dataset(name: str, sample_size: int, seed: int) -> pd.DataFrame:
424
+ spec = get_spec(name)
425
+ return spec.loader(sample_size, seed).reset_index(drop=True)
426
+
427
+
428
+ def split_xy(df: pd.DataFrame, target: str) -> tuple[pd.DataFrame, pd.Series]:
429
+ cleaned = df.dropna(axis=1, how="all").copy()
430
+ if target not in cleaned.columns:
431
+ raise gr.Error(f"Target column '{target}' was not found.")
432
+ y = cleaned[target]
433
+ x = cleaned.drop(columns=[target])
434
+ if x.empty:
435
+ raise gr.Error("Dataset must include at least one feature column.")
436
+ return x, y
437
+
438
+
439
+ def infer_task(y: pd.Series) -> str:
440
+ if y.dtype.kind in "ifu" and y.nunique(dropna=True) > 20:
441
+ return "regression"
442
+ return "classification"
443
+
444
+
445
+ def make_preprocessor(x: pd.DataFrame, scale_numeric: bool = False) -> ColumnTransformer:
446
+ numeric_cols = x.select_dtypes(include=np.number).columns.tolist()
447
+ categorical_cols = [c for c in x.columns if c not in numeric_cols]
448
+ numeric_steps: list[tuple[str, object]] = [("impute", SimpleImputer(strategy="median"))]
449
+ if scale_numeric:
450
+ numeric_steps.append(("scale", StandardScaler()))
451
+ transformers: list[tuple[str, object, list[str]]] = []
452
+ if numeric_cols:
453
+ transformers.append(("num", Pipeline(numeric_steps), numeric_cols))
454
+ if categorical_cols:
455
+ transformers.append(
456
+ (
457
+ "cat",
458
+ Pipeline(
459
+ [
460
+ ("impute", SimpleImputer(strategy="most_frequent")),
461
+ ("encode", OneHotEncoder(handle_unknown="ignore", sparse_output=False, max_categories=32)),
462
+ ]
463
+ ),
464
+ categorical_cols,
465
+ )
466
+ )
467
+ return ColumnTransformer(transformers=transformers, remainder="drop", verbose_feature_names_out=False)
468
+
469
+
470
+ def available_baselines(task: str) -> dict[str, object]:
471
+ if task == "classification":
472
+ models: dict[str, object] = {
473
+ "Logistic": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", LogisticRegression(max_iter=1200, n_jobs=-1))]),
474
+ "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestClassifier(n_estimators=180, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
475
+ "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingClassifier(random_state=RANDOM_STATE))]),
476
+ "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyClassifier(strategy="most_frequent"))]),
477
+ }
478
+ else:
479
+ models = {
480
+ "Ridge": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", Ridge(alpha=1.0))]),
481
+ "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestRegressor(n_estimators=180, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
482
+ "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingRegressor(random_state=RANDOM_STATE))]),
483
+ "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyRegressor(strategy="median"))]),
484
+ }
485
+ if importlib.util.find_spec("xgboost"):
486
+ from xgboost import XGBClassifier, XGBRegressor
487
+
488
+ if task == "classification":
489
+ models["XGBoost"] = Pipeline(
490
+ [
491
+ ("prep", make_preprocessor(pd.DataFrame())),
492
+ ("model", XGBClassifier(n_estimators=160, max_depth=4, learning_rate=0.08, eval_metric="logloss", random_state=RANDOM_STATE)),
493
+ ]
494
+ )
495
+ else:
496
+ models["XGBoost"] = Pipeline(
497
+ [
498
+ ("prep", make_preprocessor(pd.DataFrame())),
499
+ ("model", XGBRegressor(n_estimators=160, max_depth=4, learning_rate=0.08, random_state=RANDOM_STATE)),
500
+ ]
501
+ )
502
+ return models
503
+
504
+
505
+ def rebuild_pipeline(model: Pipeline, x_train: pd.DataFrame) -> Pipeline:
506
+ pipe = clone(model)
507
+ wants_scale = pipe.steps[0][1].transformers and "scale" in str(pipe.steps[0][1].transformers[0][1])
508
+ pipe.steps[0] = ("prep", make_preprocessor(x_train, scale_numeric=wants_scale))
509
+ return pipe
510
+
511
+
512
+ @lru_cache(maxsize=1)
513
+ def load_tabfm_model():
514
+ from tabfm import tabfm_v1_0_0_pytorch
515
+
516
+ return tabfm_v1_0_0_pytorch.load()
517
+
518
+
519
+ def run_tabfm(task: str, x_train: pd.DataFrame, x_test: pd.DataFrame, y_train: pd.Series):
520
+ from tabfm import TabFMClassifier, TabFMRegressor
521
+
522
+ foundation_model = load_tabfm_model()
523
+ estimator = TabFMClassifier(model=foundation_model) if task == "classification" else TabFMRegressor(model=foundation_model)
524
+ estimator.fit(x_train, y_train.to_numpy())
525
+ pred = estimator.predict(x_test)
526
+ proba = estimator.predict_proba(x_test) if task == "classification" and hasattr(estimator, "predict_proba") else None
527
+ return pred, proba
528
+
529
+
530
+ def score_predictions(task: str, y_true: pd.Series, pred, proba=None) -> dict[str, float]:
531
+ if task == "classification":
532
+ metrics = {
533
+ "accuracy": accuracy_score(y_true, pred),
534
+ "f1_weighted": f1_score(y_true, pred, average="weighted", zero_division=0),
535
+ }
536
+ if proba is not None and len(np.unique(y_true)) == 2:
537
+ try:
538
+ metrics["roc_auc"] = roc_auc_score(y_true, proba[:, 1])
539
+ except Exception:
540
+ metrics["roc_auc"] = np.nan
541
+ else:
542
+ metrics["roc_auc"] = np.nan
543
+ metrics["rank_score"] = np.nanmean([metrics["accuracy"], metrics["f1_weighted"], metrics["roc_auc"]])
544
+ return metrics
545
+ rmse = math.sqrt(mean_squared_error(y_true, pred))
546
+ mae = mean_absolute_error(y_true, pred)
547
+ r2 = r2_score(y_true, pred)
548
+ return {"rmse": rmse, "mae": mae, "r2": r2, "rank_score": -rmse}
549
+
550
+
551
+ def benchmark_frame(
552
+ df: pd.DataFrame,
553
+ target: str,
554
+ task: str | None,
555
+ sample_size: int,
556
+ test_size: float,
557
+ seed: int,
558
+ selected_models: list[str],
559
+ include_tabfm: bool,
560
+ ) -> tuple[pd.DataFrame, pd.DataFrame, str]:
561
+ df = df.sample(min(sample_size, len(df)), random_state=seed).reset_index(drop=True)
562
+ x, y = split_xy(df, target)
563
+ task = task or infer_task(y)
564
+ if task == "classification" and y.nunique(dropna=True) < 2:
565
+ raise gr.Error("Classification needs at least two target classes.")
566
+ stratify = y if task == "classification" and y.value_counts().min() >= 2 else None
567
+ x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=test_size, random_state=seed, stratify=stratify)
568
+
569
+ rows: list[dict[str, object]] = []
570
+ notes: list[str] = []
571
+ models = available_baselines(task)
572
+ for name, model in models.items():
573
+ if name not in selected_models:
574
+ continue
575
+ start = time.perf_counter()
576
+ try:
577
+ pipe = rebuild_pipeline(model, x_train)
578
+ pipe.fit(x_train, y_train)
579
+ pred = pipe.predict(x_test)
580
+ proba = pipe.predict_proba(x_test) if task == "classification" and hasattr(pipe, "predict_proba") else None
581
+ metrics = score_predictions(task, y_test, pred, proba)
582
+ rows.append({"model": name, "status": "ok", "seconds": time.perf_counter() - start, **metrics})
583
+ except Exception as exc:
584
+ rows.append({"model": name, "status": f"failed: {exc}", "seconds": time.perf_counter() - start})
585
+
586
+ if include_tabfm:
587
+ start = time.perf_counter()
588
+ try:
589
+ pred, proba = run_tabfm(task, x_train, x_test, y_train)
590
+ metrics = score_predictions(task, y_test, pred, proba)
591
+ rows.append({"model": "TabFM", "status": "ok", "seconds": time.perf_counter() - start, **metrics})
592
+ except Exception as exc:
593
+ rows.append({"model": "TabFM", "status": f"unavailable: {exc}", "seconds": time.perf_counter() - start})
594
+ notes.append(f"TabFM did not run in this environment. On Spaces, keep Python 3.11 and allow the GitHub dependency plus model download for `{TABFM_MODEL_ID}`.")
595
+
596
+ results = pd.DataFrame(rows)
597
+ metric_cols = [c for c in ["accuracy", "f1_weighted", "roc_auc", "rmse", "mae", "r2", "rank_score", "seconds"] if c in results.columns]
598
+ if not results.empty and "rank_score" in results.columns:
599
+ results = results.sort_values("rank_score", ascending=False, na_position="last").reset_index(drop=True)
600
+ results.insert(0, "rank", np.arange(1, len(results) + 1))
601
+ preview = pd.concat([x_test.reset_index(drop=True).head(12), y_test.reset_index(drop=True).head(12).rename(target)], axis=1)
602
+ summary = (
603
+ f"**Task:** {task} \n"
604
+ f"**Rows used:** {len(df):,} | **Train:** {len(x_train):,} | **Test:** {len(x_test):,} | **Features:** {x.shape[1]:,} \n"
605
+ f"**Primary rank:** {'higher accuracy/F1/AUC' if task == 'classification' else 'lower RMSE'}"
606
+ )
607
+ if notes:
608
+ summary += "\n\n" + "\n".join(f"- {note}" for note in notes)
609
+ return results[["rank", "model", "status", *metric_cols]], preview, summary
610
+
611
+
612
+ def metric_chart(results: pd.DataFrame) -> go.Figure:
613
+ if results is None or results.empty:
614
+ return go.Figure()
615
+ long_cols = [c for c in ["accuracy", "f1_weighted", "roc_auc", "r2"] if c in results.columns and results[c].notna().any()]
616
+ if long_cols:
617
+ long = results.melt(id_vars=["model"], value_vars=long_cols, var_name="metric", value_name="score")
618
+ fig = px.bar(long, x="model", y="score", color="metric", barmode="group", color_discrete_sequence=["#f97316", "#8b5cf6", "#14b8a6", "#2563eb"])
619
+ fig.update_yaxes(range=[0, 1])
620
+ else:
621
+ long_cols = [c for c in ["rmse", "mae"] if c in results.columns and results[c].notna().any()]
622
+ long = results.melt(id_vars=["model"], value_vars=long_cols, var_name="metric", value_name="score")
623
+ fig = px.bar(long, x="model", y="score", color="metric", barmode="group", color_discrete_sequence=["#f97316", "#8b5cf6"])
624
+ fig.update_layout(template="plotly_white", height=360, margin=dict(l=20, r=20, t=25, b=20), legend_title_text="")
625
+ return fig
626
+
627
+
628
+ def time_chart(results: pd.DataFrame) -> go.Figure:
629
+ if results is None or results.empty or "seconds" not in results:
630
+ return go.Figure()
631
+ fig = px.scatter(
632
+ results,
633
+ x="seconds",
634
+ y="model",
635
+ size=np.maximum(results.get("rank_score", pd.Series([1] * len(results))).fillna(0).abs(), 0.1),
636
+ color="model",
637
+ color_discrete_sequence=px.colors.qualitative.Set2,
638
+ )
639
+ fig.update_layout(template="plotly_white", height=300, margin=dict(l=20, r=20, t=25, b=20), showlegend=False)
640
+ return fig
641
+
642
+
643
+ def run_catalog(dataset_name: str, sample_size: int, test_percent: int, seed: int, selected_models: list[str], include_tabfm: bool):
644
+ spec = get_spec(dataset_name)
645
+ df = get_dataset(dataset_name, sample_size, seed)
646
+ results, preview, summary = benchmark_frame(df, spec.target, spec.task, sample_size, test_percent / 100, seed, selected_models, include_tabfm)
647
+ return summary, results.round(4), metric_chart(results), time_chart(results), preview
648
+
649
+
650
+ def run_upload(file, target: str, task: str, sample_size: int, test_percent: int, seed: int, selected_models: list[str], include_tabfm: bool):
651
+ if file is None:
652
+ raise gr.Error("Upload a CSV file first.")
653
+ df = pd.read_csv(file.name)
654
+ selected_task = None if task == "Auto" else task.lower()
655
+ results, preview, summary = benchmark_frame(df, target, selected_task, sample_size, test_percent / 100, seed, selected_models, include_tabfm)
656
+ return summary, results.round(4), metric_chart(results), time_chart(results), preview
657
+
658
+
659
+ def catalog_table() -> pd.DataFrame:
660
+ return pd.DataFrame(
661
+ [
662
+ {
663
+ "dataset": d.name,
664
+ "task": d.task,
665
+ "target": d.target,
666
+ "rows": d.rows,
667
+ "source": d.source,
668
+ "description": d.description,
669
+ }
670
+ for d in DATASETS
671
+ ]
672
+ )
673
+
674
+
675
+ DEFAULT_MODELS = ["Logistic", "Ridge", "RandomForest", "HistGradientBoosting", "XGBoost", "Dummy"]
676
+
677
+
678
+ def build_app() -> gr.Blocks:
679
+ css = """
680
+ body, .gradio-container { background: #f7f9fd; color: #101828; }
681
+ .shell { max-width: 1440px; margin: 0 auto; }
682
+ .hero { background: linear-gradient(135deg, #ffffff 0%, #f6f9ff 56%, #fff7ed 100%); border: 1px solid #e9edf5; border-radius: 18px; padding: 26px 28px; box-shadow: 0 20px 55px rgba(15, 23, 42, 0.07); }
683
+ .hero h1 { font-size: 36px; line-height: 1.05; margin: 0 0 8px; letter-spacing: 0; }
684
+ .hero p { margin: 0; color: #667085; font-size: 15px; }
685
+ .stat-card { background: #fff; border: 1px solid #edf0f5; border-radius: 14px; padding: 18px; box-shadow: 0 12px 35px rgba(15, 23, 42, 0.05); min-height: 118px; }
686
+ .stat-card .label { color: #667085; font-size: 13px; }
687
+ .stat-card .value { font-size: 28px; font-weight: 760; margin-top: 12px; }
688
+ .stat-card .trend { display: inline-block; margin-left: 8px; font-size: 12px; color: #027a48; background: #ecfdf3; border-radius: 999px; padding: 2px 8px; }
689
+ .panel { background: #fff; border: 1px solid #edf0f5; border-radius: 14px; padding: 14px; box-shadow: 0 12px 35px rgba(15, 23, 42, 0.04); }
690
+ .gr-button-primary { background: #f97316 !important; border-color: #f97316 !important; }
691
+ footer { display: none !important; }
692
+ """
693
+ with gr.Blocks(title=APP_TITLE, css=css, theme=gr.themes.Soft(primary_hue="orange", secondary_hue="violet")) as demo:
694
+ with gr.Column(elem_classes=["shell"]):
695
+ gr.HTML(
696
+ """
697
+ <div class="hero">
698
+ <h1>tabBench</h1>
699
+ <p>A clean arena for benchmarking <strong>google/tabfm-1.0.0-pytorch</strong> against practical tabular baselines across small, classic, imbalanced, and user-uploaded datasets.</p>
700
+ </div>
701
+ """
702
+ )
703
+ with gr.Row():
704
+ gr.HTML('<div class="stat-card"><div class="label">Benchmark catalog</div><div class="value">12 <span class="trend">mixed tasks</span></div><div class="label">Classification + regression</div></div>')
705
+ gr.HTML('<div class="stat-card"><div class="label">Linked HF model</div><div class="value">TabFM <span class="trend">1.0</span></div><div class="label">google/tabfm-1.0.0-pytorch</div></div>')
706
+ gr.HTML('<div class="stat-card"><div class="label">User datasets</div><div class="value">CSV <span class="trend">upload</span></div><div class="label">Pick target, task, sample size</div></div>')
707
+ with gr.Tabs():
708
+ with gr.Tab("Arena"):
709
+ with gr.Row():
710
+ with gr.Column(scale=1, elem_classes=["panel"]):
711
+ dataset = gr.Dropdown(dataset_names(), value="Titanic Survival", label="Dataset")
712
+ sample = gr.Slider(100, 50000, value=1200, step=100, label="Sample size")
713
+ test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
714
+ seed = gr.Number(value=42, precision=0, label="Random seed")
715
+ models = gr.CheckboxGroup(DEFAULT_MODELS, value=["RandomForest", "HistGradientBoosting", "XGBoost", "Dummy"], label="Baselines")
716
+ include_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
717
+ run_btn = gr.Button("Run benchmark", variant="primary")
718
+ with gr.Column(scale=3):
719
+ summary = gr.Markdown()
720
+ leaderboard = gr.Dataframe(label="Leaderboard", interactive=False)
721
+ with gr.Row():
722
+ chart = gr.Plot(label="Metric comparison")
723
+ speed = gr.Plot(label="Speed")
724
+ preview = gr.Dataframe(label="Held-out preview", interactive=False)
725
+ run_btn.click(run_catalog, [dataset, sample, test_pct, seed, models, include_tabfm], [summary, leaderboard, chart, speed, preview])
726
+ demo.load(run_catalog, [dataset, sample, test_pct, seed, models, include_tabfm], [summary, leaderboard, chart, speed, preview])
727
+ with gr.Tab("Upload Dataset"):
728
+ with gr.Row():
729
+ with gr.Column(scale=1, elem_classes=["panel"]):
730
+ file = gr.File(label="CSV file", file_types=[".csv"])
731
+ target = gr.Textbox(label="Target column")
732
+ task = gr.Radio(["Auto", "Classification", "Regression"], value="Auto", label="Task")
733
+ upload_sample = gr.Slider(100, 50000, value=2000, step=100, label="Sample size")
734
+ upload_test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
735
+ upload_seed = gr.Number(value=42, precision=0, label="Random seed")
736
+ upload_models = gr.CheckboxGroup(DEFAULT_MODELS, value=["RandomForest", "HistGradientBoosting", "XGBoost", "Dummy"], label="Baselines")
737
+ upload_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
738
+ upload_btn = gr.Button("Run uploaded dataset", variant="primary")
739
+ with gr.Column(scale=3):
740
+ upload_summary = gr.Markdown()
741
+ upload_leaderboard = gr.Dataframe(label="Upload leaderboard", interactive=False)
742
+ with gr.Row():
743
+ upload_chart = gr.Plot(label="Metric comparison")
744
+ upload_speed = gr.Plot(label="Speed")
745
+ upload_preview = gr.Dataframe(label="Held-out preview", interactive=False)
746
+ upload_btn.click(
747
+ run_upload,
748
+ [file, target, task, upload_sample, upload_test_pct, upload_seed, upload_models, upload_tabfm],
749
+ [upload_summary, upload_leaderboard, upload_chart, upload_speed, upload_preview],
750
+ )
751
+ with gr.Tab("Dataset Catalog"):
752
+ gr.Dataframe(catalog_table(), interactive=False, label="Included benchmark catalog")
753
+ gr.Markdown(
754
+ """
755
+ Kaggle datasets such as the credit-card fraud and Epicurious recipe datasets are license/account gated, so this Space ships proxy tasks and accepts the real CSV through the upload tab. For a private Space, place the real CSVs in `data/` and swap the catalog loaders to `pd.read_csv`.
756
+ """
757
+ )
758
+ with gr.Tab("Implementation Notes"):
759
+ gr.Markdown(
760
+ """
761
+ This Space declares `models: google/tabfm-1.0.0-pytorch` in its README metadata, which is what Hugging Face uses to associate Spaces with model pages.
762
+
763
+ TabFM is attempted only when **Run TabFM live** is enabled because loading foundation model weights can be slow on free CPU Spaces. The leaderboard remains useful without it by comparing deterministic tabular baselines.
764
+
765
+ The TabFM integration follows the Google Research README pattern: load `tabfm_v1_0_0_pytorch`, wrap it with `TabFMClassifier` or `TabFMRegressor`, call `fit` for context preparation, then `predict`.
766
+ """
767
+ )
768
+ return demo
769
+
770
+
771
+ if __name__ == "__main__":
772
+ build_app().queue(max_size=16).launch()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.35.0
2
+ pandas>=2.2.0,<3
3
+ numpy>=1.26.0,<2.4
4
+ scikit-learn>=1.5.0,<1.9
5
+ plotly>=5.22.0,<7
6
+ scipy>=1.13.0,<1.18
7
+ xgboost>=2.1.0,<3
8
+ kagglehub>=0.3.6,<1
9
+ tabfm[pytorch] @ git+https://github.com/google-research/tabfm.git