corbanyax commited on
Commit
08f7e60
·
verified ·
1 Parent(s): 77c7374

Add datasets and TabFM tuning

Browse files
Files changed (2) hide show
  1. README.md +2 -2
  2. app.py +391 -23
README.md CHANGED
@@ -36,11 +36,11 @@ The app includes:
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
 
 
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, CalCOFI, Szeged Weather, WWII Weather, Montreal Bike Lanes, NYC Bike Crossings, UK Road Safety, KCBS BBQ
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. Live TabFM runs are opt-in because the model weights are large and CPU-only inference can be slow.
44
 
45
  Note: TabFM's weights use their own non-commercial license. Review the upstream model license before using this Space commercially.
46
 
app.py CHANGED
@@ -40,6 +40,12 @@ RANDOM_STATE = 42
40
  CANDY_DATA_URL = "https://raw.githubusercontent.com/fivethirtyeight/data/master/candy-power-ranking/candy-data.csv"
41
  GOOGLE_COLORS = ["#4285F4", "#DB4437", "#F4B400", "#0F9D58", "#A142F4", "#00ACC1"]
42
  METRIC_CHOICES = ["accuracy", "f1_weighted", "roc_auc", "rmse", "mae", "r2", "seconds"]
 
 
 
 
 
 
43
 
44
 
45
  @dataclass(frozen=True)
@@ -73,6 +79,54 @@ def find_first_data_file(root: str | Path, suffixes: tuple[str, ...]) -> Path:
73
  raise FileNotFoundError(f"No data file with suffixes {suffixes} found in {root}")
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @lru_cache(maxsize=1)
77
  def openml_titanic() -> pd.DataFrame:
78
  data = fetch_openml(data_id=40945, as_frame=True, parser="auto")
@@ -162,12 +216,95 @@ def kaggle_epirecipes() -> pd.DataFrame:
162
  "healthy",
163
  "vegetarian",
164
  "vegan",
 
165
  "rating",
166
  ]
167
  cols = [c for c in preferred if c in df.columns]
168
  return df[cols].dropna(subset=["rating"])
169
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  def load_iris_df(limit: int, seed: int) -> pd.DataFrame:
172
  data = load_iris(as_frame=True)
173
  df = data.frame.rename(columns={"target": "species"})
@@ -324,6 +461,19 @@ def load_epirecipes_df(limit: int, seed: int) -> pd.DataFrame:
324
  return load_epirecipes_proxy_df(limit, seed)
325
 
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  def load_candy_proxy_df(limit: int, seed: int) -> pd.DataFrame:
328
  rng = np.random.default_rng(seed)
329
  n = min(limit, 1200)
@@ -357,6 +507,13 @@ def load_candy_df(limit: int, seed: int) -> pd.DataFrame:
357
  return load_candy_proxy_df(limit, seed)
358
 
359
 
 
 
 
 
 
 
 
360
  def load_adult_income_proxy_df(limit: int, seed: int) -> pd.DataFrame:
361
  rng = np.random.default_rng(seed)
362
  n = min(limit, 30000)
@@ -398,6 +555,81 @@ def load_bike_demand_proxy_df(limit: int, seed: int) -> pd.DataFrame:
398
  return pd.DataFrame({"hour": hour, "temp": temp.round(1), "workingday": workingday, "weather": weather, "rental_count": count.clip(0).round(0)})
399
 
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  DATASETS: list[DatasetSpec] = [
402
  DatasetSpec("Titanic Survival", "classification", "survived", "OpenML data_id=40945", 1309, "Mixed categorical/numeric binary classification.", load_titanic_df),
403
  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),
@@ -405,6 +637,15 @@ DATASETS: list[DatasetSpec] = [
405
  DatasetSpec("Credit Card Fraud", "classification", "fraud", "KaggleHub mlg-ulb/creditcardfraud", 284807, "Large imbalanced binary fraud task.", load_credit_fraud_df),
406
  DatasetSpec("Epicurious Recipes", "regression", "rating", "KaggleHub hugodarwood/epirecipes", 20000, "Recipe nutrition and tags to rating.", load_epirecipes_df),
407
  DatasetSpec("Halloween Candy", "regression", "winpercent", "FiveThirtyEight GitHub CSV", 85, "Candy attributes to popularity score.", load_candy_df),
 
 
 
 
 
 
 
 
 
408
  DatasetSpec("Adult Income", "classification", "income_gt_50k", "OpenML data_id=1590", 48842, "Demographic and work attributes to income bucket.", load_adult_income_df),
409
  DatasetSpec("Bike Demand", "regression", "rental_count", "Kaggle-style proxy", 15000, "Weather and time features to rental demand.", load_bike_demand_proxy_df),
410
  DatasetSpec("Iris", "classification", "species", "sklearn", 150, "Classic multi-class flower classification.", load_iris_df),
@@ -473,16 +714,16 @@ def make_preprocessor(x: pd.DataFrame, scale_numeric: bool = False) -> ColumnTra
473
  def available_baselines(task: str) -> dict[str, object]:
474
  if task == "classification":
475
  models: dict[str, object] = {
476
- "Logistic": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", LogisticRegression(max_iter=1200, n_jobs=-1))]),
477
- "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestClassifier(n_estimators=180, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
478
- "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingClassifier(random_state=RANDOM_STATE))]),
479
  "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyClassifier(strategy="most_frequent"))]),
480
  }
481
  else:
482
  models = {
483
  "Ridge": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", Ridge(alpha=1.0))]),
484
- "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestRegressor(n_estimators=180, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
485
- "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingRegressor(random_state=RANDOM_STATE))]),
486
  "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyRegressor(strategy="median"))]),
487
  }
488
  if importlib.util.find_spec("xgboost"):
@@ -492,14 +733,14 @@ def available_baselines(task: str) -> dict[str, object]:
492
  models["XGBoost"] = Pipeline(
493
  [
494
  ("prep", make_preprocessor(pd.DataFrame())),
495
- ("model", XGBClassifier(n_estimators=160, max_depth=4, learning_rate=0.08, eval_metric="logloss", random_state=RANDOM_STATE)),
496
  ]
497
  )
498
  else:
499
  models["XGBoost"] = Pipeline(
500
  [
501
  ("prep", make_preprocessor(pd.DataFrame())),
502
- ("model", XGBRegressor(n_estimators=160, max_depth=4, learning_rate=0.08, random_state=RANDOM_STATE)),
503
  ]
504
  )
505
  return models
@@ -519,11 +760,44 @@ def load_tabfm_model():
519
  return tabfm_v1_0_0_pytorch.load()
520
 
521
 
522
- def run_tabfm(task: str, x_train: pd.DataFrame, x_test: pd.DataFrame, y_train: pd.Series):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  from tabfm import TabFMClassifier, TabFMRegressor
524
 
525
  foundation_model = load_tabfm_model()
526
- estimator = TabFMClassifier(model=foundation_model) if task == "classification" else TabFMRegressor(model=foundation_model)
 
 
 
 
527
  estimator.fit(x_train, y_train.to_numpy())
528
  pred = estimator.predict(x_test)
529
  proba = estimator.predict_proba(x_test) if task == "classification" and hasattr(estimator, "predict_proba") else None
@@ -560,6 +834,7 @@ def benchmark_frame(
560
  seed: int,
561
  selected_models: list[str],
562
  include_tabfm: bool,
 
563
  ) -> tuple[pd.DataFrame, pd.DataFrame, str]:
564
  df = df.sample(min(sample_size, len(df)), random_state=seed).reset_index(drop=True)
565
  x, y = split_xy(df, target)
@@ -589,9 +864,22 @@ def benchmark_frame(
589
  if include_tabfm:
590
  start = time.perf_counter()
591
  try:
592
- pred, proba = run_tabfm(task, x_train, x_test, y_train)
593
- metrics = score_predictions(task, y_test, pred, proba)
594
- rows.append({"model": "TabFM", "status": "ok", "seconds": time.perf_counter() - start, **metrics})
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  except Exception as exc:
596
  rows.append({"model": "TabFM", "status": f"unavailable: {exc}", "seconds": time.perf_counter() - start})
597
  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}`.")
@@ -609,6 +897,8 @@ def benchmark_frame(
609
  )
610
  if notes:
611
  summary += "\n\n" + "\n".join(f"- {note}" for note in notes)
 
 
612
  return results[["rank", "model", "status", *metric_cols]], preview, summary
613
 
614
 
@@ -716,10 +1006,20 @@ def run_catalog(
716
  include_tabfm: bool,
717
  selected_metrics: list[str],
718
  chart_style: str,
 
 
 
 
 
 
 
 
 
719
  ):
720
  spec = get_spec(dataset_name)
721
  df = get_dataset(dataset_name, sample_size, seed)
722
- results, preview, summary = benchmark_frame(df, spec.target, spec.task, sample_size, test_percent / 100, seed, selected_models, include_tabfm)
 
723
  return summary, results.round(4), metric_chart(results, selected_metrics, chart_style), time_chart(results), preview
724
 
725
 
@@ -734,12 +1034,22 @@ def run_upload(
734
  include_tabfm: bool,
735
  selected_metrics: list[str],
736
  chart_style: str,
 
 
 
 
 
 
 
 
 
737
  ):
738
  if file is None:
739
  raise gr.Error("Upload a CSV file first.")
740
  df = pd.read_csv(file.name)
741
  selected_task = None if task == "Auto" else task.lower()
742
- results, preview, summary = benchmark_frame(df, target, selected_task, sample_size, test_percent / 100, seed, selected_models, include_tabfm)
 
743
  return summary, results.round(4), metric_chart(results, selected_metrics, chart_style), time_chart(results), preview
744
 
745
 
@@ -794,7 +1104,7 @@ def build_app() -> gr.Blocks:
794
  """
795
  )
796
  with gr.Row():
797
- 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>')
798
  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>')
799
  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>')
800
  with gr.Tabs():
@@ -802,13 +1112,23 @@ def build_app() -> gr.Blocks:
802
  with gr.Row():
803
  with gr.Column(scale=1, elem_classes=["panel"]):
804
  dataset = gr.Dropdown(dataset_names(), value="Titanic Survival", label="Dataset")
805
- sample = gr.Slider(100, 50000, value=1200, step=100, label="Sample size")
806
  test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
807
  seed = gr.Number(value=42, precision=0, label="Random seed")
808
- models = gr.CheckboxGroup(DEFAULT_MODELS, value=["RandomForest", "HistGradientBoosting", "XGBoost", "Dummy"], label="Baselines")
809
  include_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
810
  metric_toggles = gr.CheckboxGroup(METRIC_CHOICES, value=["rmse", "mae", "r2", "accuracy", "f1_weighted", "roc_auc"], label="Chart metrics")
811
  chart_style = gr.Radio(["Line", "Radar"], value="Line", label="Chart style")
 
 
 
 
 
 
 
 
 
 
812
  run_btn = gr.Button("Run benchmark", variant="primary")
813
  with gr.Column(scale=3):
814
  summary = gr.Markdown()
@@ -817,7 +1137,25 @@ def build_app() -> gr.Blocks:
817
  chart = gr.Plot(label="Metric comparison")
818
  speed = gr.Plot(label="Speed")
819
  preview = gr.Dataframe(label="Held-out preview", interactive=False)
820
- run_inputs = [dataset, sample, test_pct, seed, models, include_tabfm, metric_toggles, chart_style]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  run_btn.click(run_catalog, run_inputs, [summary, leaderboard, chart, speed, preview])
822
  metric_toggles.change(redraw_metric_chart, [leaderboard, metric_toggles, chart_style], chart)
823
  chart_style.change(redraw_metric_chart, [leaderboard, metric_toggles, chart_style], chart)
@@ -828,13 +1166,23 @@ def build_app() -> gr.Blocks:
828
  file = gr.File(label="CSV file", file_types=[".csv"])
829
  target = gr.Textbox(label="Target column")
830
  task = gr.Radio(["Auto", "Classification", "Regression"], value="Auto", label="Task")
831
- upload_sample = gr.Slider(100, 50000, value=2000, step=100, label="Sample size")
832
  upload_test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
833
  upload_seed = gr.Number(value=42, precision=0, label="Random seed")
834
- upload_models = gr.CheckboxGroup(DEFAULT_MODELS, value=["RandomForest", "HistGradientBoosting", "XGBoost", "Dummy"], label="Baselines")
835
  upload_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
836
  upload_metric_toggles = gr.CheckboxGroup(METRIC_CHOICES, value=["rmse", "mae", "r2", "accuracy", "f1_weighted", "roc_auc"], label="Chart metrics")
837
  upload_chart_style = gr.Radio(["Line", "Radar"], value="Line", label="Chart style")
 
 
 
 
 
 
 
 
 
 
838
  upload_btn = gr.Button("Run uploaded dataset", variant="primary")
839
  with gr.Column(scale=3):
840
  upload_summary = gr.Markdown()
@@ -845,7 +1193,27 @@ def build_app() -> gr.Blocks:
845
  upload_preview = gr.Dataframe(label="Held-out preview", interactive=False)
846
  upload_btn.click(
847
  run_upload,
848
- [file, target, task, upload_sample, upload_test_pct, upload_seed, upload_models, upload_tabfm, upload_metric_toggles, upload_chart_style],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
  [upload_summary, upload_leaderboard, upload_chart, upload_speed, upload_preview],
850
  )
851
  upload_metric_toggles.change(redraw_metric_chart, [upload_leaderboard, upload_metric_toggles, upload_chart_style], upload_chart)
@@ -854,7 +1222,7 @@ def build_app() -> gr.Blocks:
854
  gr.Dataframe(catalog_table(), interactive=False, label="Included benchmark catalog")
855
  gr.Markdown(
856
  """
857
- 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`.
858
  """
859
  )
860
  with gr.Tab("Implementation Notes"):
@@ -862,7 +1230,7 @@ def build_app() -> gr.Blocks:
862
  """
863
  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.
864
 
865
- 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.
866
 
867
  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`.
868
  """
 
40
  CANDY_DATA_URL = "https://raw.githubusercontent.com/fivethirtyeight/data/master/candy-power-ranking/candy-data.csv"
41
  GOOGLE_COLORS = ["#4285F4", "#DB4437", "#F4B400", "#0F9D58", "#A142F4", "#00ACC1"]
42
  METRIC_CHOICES = ["accuracy", "f1_weighted", "roc_auc", "rmse", "mae", "r2", "seconds"]
43
+ TABFM_PRESETS = {
44
+ "Fast": {"n_estimators": 1, "max_num_rows": 256, "max_num_features": 64, "batch_size": 1, "enable_nnls": False, "n_feature_crosses": 0, "n_svd_features": 0, "max_eval_rows": 256},
45
+ "Balanced": {"n_estimators": 4, "max_num_rows": 512, "max_num_features": 128, "batch_size": 1, "enable_nnls": False, "n_feature_crosses": 0, "n_svd_features": 0, "max_eval_rows": 512},
46
+ "Default": {"n_estimators": 32, "max_num_rows": None, "max_num_features": 500, "batch_size": 1, "enable_nnls": False, "n_feature_crosses": 0, "n_svd_features": 0, "max_eval_rows": 1000},
47
+ "Ensemble": {"n_estimators": 32, "max_num_rows": None, "max_num_features": 500, "batch_size": 1, "enable_nnls": True, "n_feature_crosses": "sqrt", "n_svd_features": "sqrt", "max_eval_rows": 1000},
48
+ }
49
 
50
 
51
  @dataclass(frozen=True)
 
79
  raise FileNotFoundError(f"No data file with suffixes {suffixes} found in {root}")
80
 
81
 
82
+ def normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
83
+ df = df.copy()
84
+ df.columns = (
85
+ pd.Index(df.columns)
86
+ .astype(str)
87
+ .str.strip()
88
+ .str.lower()
89
+ .str.replace(r"[^0-9a-z]+", "_", regex=True)
90
+ .str.strip("_")
91
+ )
92
+ return df
93
+
94
+
95
+ def choose_col(df: pd.DataFrame, candidates: list[str], contains: list[str] | None = None) -> str:
96
+ normalized = {c.lower(): c for c in df.columns}
97
+ for candidate in candidates:
98
+ key = candidate.lower()
99
+ if key in normalized:
100
+ return normalized[key]
101
+ if contains:
102
+ for col in df.columns:
103
+ lower = str(col).lower()
104
+ if all(part in lower for part in contains):
105
+ return col
106
+ raise KeyError(f"None of {candidates} found in columns.")
107
+
108
+
109
+ def kaggle_csv(dataset_id: str, preferred_names: tuple[str, ...] = ()) -> pd.DataFrame:
110
+ import kagglehub
111
+
112
+ path = Path(kagglehub.dataset_download(dataset_id))
113
+ csvs = sorted(path.rglob("*.csv"))
114
+ if preferred_names:
115
+ for preferred in preferred_names:
116
+ for csv in csvs:
117
+ if preferred.lower() in csv.name.lower():
118
+ return pd.read_csv(csv)
119
+ if not csvs:
120
+ raise FileNotFoundError(f"No CSV files found in Kaggle dataset {dataset_id}.")
121
+ return pd.read_csv(csvs[0])
122
+
123
+
124
+ def select_numeric_features(df: pd.DataFrame, target: str, max_features: int = 12) -> pd.DataFrame:
125
+ numeric = df.select_dtypes(include=np.number).columns.tolist()
126
+ cols = [c for c in numeric if c != target][:max_features]
127
+ return df[[*cols, target]].dropna(subset=[target])
128
+
129
+
130
  @lru_cache(maxsize=1)
131
  def openml_titanic() -> pd.DataFrame:
132
  data = fetch_openml(data_id=40945, as_frame=True, parser="auto")
 
216
  "healthy",
217
  "vegetarian",
218
  "vegan",
219
+ "cakeweek",
220
  "rating",
221
  ]
222
  cols = [c for c in preferred if c in df.columns]
223
  return df[cols].dropna(subset=["rating"])
224
 
225
 
226
+ @lru_cache(maxsize=1)
227
+ def kaggle_calcofi() -> pd.DataFrame:
228
+ df = normalize_columns(kaggle_csv("sohier/calcofi", ("bottle",)))
229
+ target = choose_col(df, ["t_deg_c", "temperature"], ["t", "deg"])
230
+ salinity = choose_col(df, ["salnty", "salinity"], ["sal"])
231
+ cols = [c for c in [salinity, "depthm", "o2ml_l", "sio3um", "no3um", "po4um", target] if c in df.columns]
232
+ return df[cols].rename(columns={target: "water_temperature", salinity: "salinity"}).dropna(subset=["water_temperature"])
233
+
234
+
235
+ @lru_cache(maxsize=1)
236
+ def kaggle_szeged_weather() -> pd.DataFrame:
237
+ df = normalize_columns(kaggle_csv("budincsevity/szeged-weather"))
238
+ target = choose_col(df, ["apparent_temperature_c", "apparent_temperature"], ["apparent", "temperature"])
239
+ cols = [c for c in ["temperature_c", "humidity", "wind_speed_km_h", "wind_bearing_degrees", "visibility_km", "pressure_millibars", "summary", "precip_type", target] if c in df.columns]
240
+ return df[cols].rename(columns={target: "apparent_temperature"}).dropna(subset=["apparent_temperature"])
241
+
242
+
243
+ @lru_cache(maxsize=1)
244
+ def kaggle_weather_ww2() -> pd.DataFrame:
245
+ df = normalize_columns(kaggle_csv("smid80/weatherww2", ("summary",)))
246
+ target = choose_col(df, ["maxtemp", "max_temp", "max"], ["max"])
247
+ cols = [c for c in ["mintemp", "meantemp", "precip", "snowfall", "yr", "mo", "da", target] if c in df.columns]
248
+ return df[cols].rename(columns={target: "max_temperature"}).dropna(subset=["max_temperature"])
249
+
250
+
251
+ @lru_cache(maxsize=1)
252
+ def kaggle_montreal_bike_lanes() -> pd.DataFrame:
253
+ df = normalize_columns(kaggle_csv("pablomonleon/montreal-bike-lanes"))
254
+ numeric = df.select_dtypes(include=np.number)
255
+ if numeric.shape[1] < 2:
256
+ raise ValueError("Montreal bike lanes dataset needs at least two numeric count columns.")
257
+ target = numeric.columns[-1]
258
+ cols = numeric.columns[: min(8, len(numeric.columns))].tolist()
259
+ if target not in cols:
260
+ cols.append(target)
261
+ return df[cols].rename(columns={target: "rider_count"}).dropna(subset=["rider_count"])
262
+
263
+
264
+ @lru_cache(maxsize=1)
265
+ def kaggle_nyc_bike_crossings() -> pd.DataFrame:
266
+ df = normalize_columns(kaggle_csv("new-york-city/nyc-east-river-bicycle-crossings"))
267
+ numeric = df.select_dtypes(include=np.number)
268
+ target = choose_col(numeric, ["total", "total_bicycle_count"], ["total"])
269
+ cols = [c for c in numeric.columns[:8] if c != target]
270
+ return df[[*cols, target]].rename(columns={target: "total_bike_crossings"}).dropna(subset=["total_bike_crossings"])
271
+
272
+
273
+ @lru_cache(maxsize=1)
274
+ def kaggle_uk_road_safety() -> pd.DataFrame:
275
+ df = normalize_columns(kaggle_csv("bluehorseshoe/uk-2016-road-safety-data", ("accident",)))
276
+ target = choose_col(df, ["number_of_casualties", "casualties"], ["casual"])
277
+ preferred = [
278
+ "number_of_vehicles",
279
+ "day_of_week",
280
+ "speed_limit",
281
+ "light_conditions",
282
+ "weather_conditions",
283
+ "road_surface_conditions",
284
+ "urban_or_rural_area",
285
+ target,
286
+ ]
287
+ cols = [c for c in preferred if c in df.columns]
288
+ return df[cols].rename(columns={target: "casualty_count"}).dropna(subset=["casualty_count"])
289
+
290
+
291
+ @lru_cache(maxsize=1)
292
+ def kaggle_kcbs_bbq() -> pd.DataFrame:
293
+ df = normalize_columns(kaggle_csv("jaysobel/kcbs-bbq"))
294
+ numeric = df.select_dtypes(include=np.number)
295
+ if "place" in df.columns:
296
+ target = "place"
297
+ elif "rank" in df.columns:
298
+ target = "rank"
299
+ else:
300
+ target = numeric.columns[0]
301
+ out = df.copy()
302
+ out["first_place"] = pd.to_numeric(out[target], errors="coerce").eq(1).astype(int)
303
+ feature_cols = [c for c in numeric.columns if c != target][:10]
304
+ categorical_cols = [c for c in out.columns if c not in numeric.columns and c != target][:4]
305
+ return out[[*feature_cols, *categorical_cols, "first_place"]].dropna(subset=["first_place"])
306
+
307
+
308
  def load_iris_df(limit: int, seed: int) -> pd.DataFrame:
309
  data = load_iris(as_frame=True)
310
  df = data.frame.rename(columns={"target": "species"})
 
461
  return load_epirecipes_proxy_df(limit, seed)
462
 
463
 
464
+ def load_epirecipes_cakeweek_df(limit: int, seed: int) -> pd.DataFrame:
465
+ try:
466
+ df = kaggle_epirecipes().copy()
467
+ if "cakeweek" not in df.columns:
468
+ raise KeyError("cakeweek")
469
+ df["cakeweek"] = pd.to_numeric(df["cakeweek"], errors="coerce").fillna(0).astype(int)
470
+ return sample_df(df, limit, seed)
471
+ except Exception:
472
+ df = load_epirecipes_proxy_df(limit, seed).copy()
473
+ df["cakeweek"] = ((df["course"] == "dessert") & (df["rating"] >= df["rating"].median())).astype(int)
474
+ return df
475
+
476
+
477
  def load_candy_proxy_df(limit: int, seed: int) -> pd.DataFrame:
478
  rng = np.random.default_rng(seed)
479
  n = min(limit, 1200)
 
507
  return load_candy_proxy_df(limit, seed)
508
 
509
 
510
+ def load_candy_chocolate_df(limit: int, seed: int) -> pd.DataFrame:
511
+ df = load_candy_df(limit, seed).copy()
512
+ if "chocolate" not in df.columns:
513
+ raise gr.Error("Candy dataset does not include the chocolate target.")
514
+ return df
515
+
516
+
517
  def load_adult_income_proxy_df(limit: int, seed: int) -> pd.DataFrame:
518
  rng = np.random.default_rng(seed)
519
  n = min(limit, 30000)
 
555
  return pd.DataFrame({"hour": hour, "temp": temp.round(1), "workingday": workingday, "weather": weather, "rental_count": count.clip(0).round(0)})
556
 
557
 
558
+ def load_calcofi_df(limit: int, seed: int) -> pd.DataFrame:
559
+ try:
560
+ return sample_df(kaggle_calcofi(), limit, seed)
561
+ except Exception:
562
+ rng = np.random.default_rng(seed)
563
+ n = min(limit, 12000)
564
+ salinity = rng.normal(33.5, 0.7, n)
565
+ depth = rng.gamma(2.0, 40.0, n)
566
+ temp = 23 - 0.38 * depth / 10 - 1.7 * (salinity - 33.5) + rng.normal(0, 1.8, n)
567
+ return pd.DataFrame({"salinity": salinity, "depthm": depth, "water_temperature": temp})
568
+
569
+
570
+ def load_szeged_weather_df(limit: int, seed: int) -> pd.DataFrame:
571
+ try:
572
+ return sample_df(kaggle_szeged_weather(), limit, seed)
573
+ except Exception:
574
+ rng = np.random.default_rng(seed)
575
+ n = min(limit, 20000)
576
+ humidity = rng.beta(4, 2, n)
577
+ temp = rng.normal(12, 10, n)
578
+ apparent = temp - 5 * humidity + rng.normal(0, 2.5, n)
579
+ return pd.DataFrame({"temperature_c": temp, "humidity": humidity, "wind_speed_km_h": rng.gamma(2, 4, n), "apparent_temperature": apparent})
580
+
581
+
582
+ def load_weather_ww2_df(limit: int, seed: int) -> pd.DataFrame:
583
+ try:
584
+ return sample_df(kaggle_weather_ww2(), limit, seed)
585
+ except Exception:
586
+ rng = np.random.default_rng(seed)
587
+ n = min(limit, 15000)
588
+ mintemp = rng.normal(15, 9, n)
589
+ return pd.DataFrame({"mintemp": mintemp, "precip": rng.gamma(1.5, 2, n), "max_temperature": mintemp + rng.normal(8, 3, n)})
590
+
591
+
592
+ def load_montreal_bike_lanes_df(limit: int, seed: int) -> pd.DataFrame:
593
+ try:
594
+ return sample_df(kaggle_montreal_bike_lanes(), limit, seed)
595
+ except Exception:
596
+ return load_bike_demand_proxy_df(limit, seed).rename(columns={"rental_count": "rider_count"})
597
+
598
+
599
+ def load_nyc_bike_crossings_df(limit: int, seed: int) -> pd.DataFrame:
600
+ try:
601
+ return sample_df(kaggle_nyc_bike_crossings(), limit, seed)
602
+ except Exception:
603
+ df = load_bike_demand_proxy_df(limit, seed)
604
+ df["brooklyn_bridge"] = (df["rental_count"] * 0.32).round()
605
+ df["manhattan_bridge"] = (df["rental_count"] * 0.27).round()
606
+ df["total_bike_crossings"] = df["rental_count"]
607
+ return df.drop(columns=["rental_count"])
608
+
609
+
610
+ def load_uk_road_safety_df(limit: int, seed: int) -> pd.DataFrame:
611
+ try:
612
+ return sample_df(kaggle_uk_road_safety(), limit, seed)
613
+ except Exception:
614
+ rng = np.random.default_rng(seed)
615
+ n = min(limit, 30000)
616
+ vehicles = rng.integers(1, 5, n)
617
+ speed = rng.choice([20, 30, 40, 50, 60, 70], n)
618
+ casualties = rng.poisson(0.4 + vehicles * 0.28 + (speed > 50) * 0.25, n)
619
+ return pd.DataFrame({"number_of_vehicles": vehicles, "speed_limit": speed, "light_conditions": rng.choice(["daylight", "dark"], n), "casualty_count": casualties})
620
+
621
+
622
+ def load_kcbs_bbq_df(limit: int, seed: int) -> pd.DataFrame:
623
+ try:
624
+ return sample_df(kaggle_kcbs_bbq(), limit, seed)
625
+ except Exception:
626
+ rng = np.random.default_rng(seed)
627
+ n = min(limit, 8000)
628
+ score = rng.normal(165, 12, n)
629
+ first = rng.binomial(1, 1 / (1 + np.exp(-(score - 184) / 5)))
630
+ return pd.DataFrame({"score": score, "contest_size": rng.integers(10, 80, n), "category": rng.choice(["chicken", "ribs", "pork", "brisket"], n), "first_place": first})
631
+
632
+
633
  DATASETS: list[DatasetSpec] = [
634
  DatasetSpec("Titanic Survival", "classification", "survived", "OpenML data_id=40945", 1309, "Mixed categorical/numeric binary classification.", load_titanic_df),
635
  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),
 
637
  DatasetSpec("Credit Card Fraud", "classification", "fraud", "KaggleHub mlg-ulb/creditcardfraud", 284807, "Large imbalanced binary fraud task.", load_credit_fraud_df),
638
  DatasetSpec("Epicurious Recipes", "regression", "rating", "KaggleHub hugodarwood/epirecipes", 20000, "Recipe nutrition and tags to rating.", load_epirecipes_df),
639
  DatasetSpec("Halloween Candy", "regression", "winpercent", "FiveThirtyEight GitHub CSV", 85, "Candy attributes to popularity score.", load_candy_df),
640
+ DatasetSpec("Candy Chocolate", "classification", "chocolate", "FiveThirtyEight GitHub CSV", 85, "Predict whether a candy is chocolate from other candy attributes.", load_candy_chocolate_df),
641
+ DatasetSpec("Epicurious Cakeweek", "classification", "cakeweek", "KaggleHub hugodarwood/epirecipes", 20000, "Predict cakeweek recipes from nutrition and recipe tags.", load_epirecipes_cakeweek_df),
642
+ DatasetSpec("CalCOFI Ocean Temperature", "regression", "water_temperature", "KaggleHub sohier/calcofi", 864863, "Predict ocean water temperature from salinity and chemistry readings.", load_calcofi_df),
643
+ DatasetSpec("Szeged Apparent Temperature", "regression", "apparent_temperature", "KaggleHub budincsevity/szeged-weather", 96453, "Predict apparent temperature from humidity, wind, pressure, and weather.", load_szeged_weather_df),
644
+ DatasetSpec("WW2 Max Temperature", "regression", "max_temperature", "KaggleHub smid80/weatherww2", 119040, "Predict daily maximum temperature from minimum temperature and weather fields.", load_weather_ww2_df),
645
+ DatasetSpec("Montreal Bike Lane Counts", "regression", "rider_count", "KaggleHub pablomonleon/montreal-bike-lanes", 319, "Predict rider counts on one Montreal bike path from other paths.", load_montreal_bike_lanes_df),
646
+ DatasetSpec("NYC Bike Crossings", "regression", "total_bike_crossings", "KaggleHub new-york-city/nyc-east-river-bicycle-crossings", 210, "Predict total East River bicycle crossings from bridge counts.", load_nyc_bike_crossings_df),
647
+ DatasetSpec("UK Road Casualties", "regression", "casualty_count", "KaggleHub bluehorseshoe/uk-2016-road-safety-data", 136621, "Predict accident casualty count from road safety fields.", load_uk_road_safety_df),
648
+ DatasetSpec("KCBS BBQ First Place", "classification", "first_place", "KaggleHub jaysobel/kcbs-bbq", 1559, "Predict whether a BBQ competition team wins first place.", load_kcbs_bbq_df),
649
  DatasetSpec("Adult Income", "classification", "income_gt_50k", "OpenML data_id=1590", 48842, "Demographic and work attributes to income bucket.", load_adult_income_df),
650
  DatasetSpec("Bike Demand", "regression", "rental_count", "Kaggle-style proxy", 15000, "Weather and time features to rental demand.", load_bike_demand_proxy_df),
651
  DatasetSpec("Iris", "classification", "species", "sklearn", 150, "Classic multi-class flower classification.", load_iris_df),
 
714
  def available_baselines(task: str) -> dict[str, object]:
715
  if task == "classification":
716
  models: dict[str, object] = {
717
+ "Logistic": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", LogisticRegression(max_iter=800))]),
718
+ "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestClassifier(n_estimators=80, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
719
+ "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingClassifier(max_iter=120, random_state=RANDOM_STATE))]),
720
  "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyClassifier(strategy="most_frequent"))]),
721
  }
722
  else:
723
  models = {
724
  "Ridge": Pipeline([("prep", make_preprocessor(pd.DataFrame(), True)), ("model", Ridge(alpha=1.0))]),
725
+ "RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestRegressor(n_estimators=80, min_samples_leaf=2, n_jobs=-1, random_state=RANDOM_STATE))]),
726
+ "HistGradientBoosting": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", HistGradientBoostingRegressor(max_iter=120, random_state=RANDOM_STATE))]),
727
  "Dummy": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", DummyRegressor(strategy="median"))]),
728
  }
729
  if importlib.util.find_spec("xgboost"):
 
733
  models["XGBoost"] = Pipeline(
734
  [
735
  ("prep", make_preprocessor(pd.DataFrame())),
736
+ ("model", XGBClassifier(n_estimators=80, max_depth=4, learning_rate=0.08, eval_metric="logloss", random_state=RANDOM_STATE)),
737
  ]
738
  )
739
  else:
740
  models["XGBoost"] = Pipeline(
741
  [
742
  ("prep", make_preprocessor(pd.DataFrame())),
743
+ ("model", XGBRegressor(n_estimators=80, max_depth=4, learning_rate=0.08, random_state=RANDOM_STATE)),
744
  ]
745
  )
746
  return models
 
760
  return tabfm_v1_0_0_pytorch.load()
761
 
762
 
763
+ def resolve_tabfm_params(
764
+ preset: str,
765
+ n_estimators: int,
766
+ max_num_rows: int,
767
+ max_num_features: int,
768
+ batch_size: int,
769
+ enable_nnls: bool,
770
+ n_feature_crosses: str,
771
+ n_svd_features: str,
772
+ max_eval_rows: int,
773
+ ) -> dict[str, object]:
774
+ if preset in TABFM_PRESETS:
775
+ return dict(TABFM_PRESETS[preset])
776
+ resolved_rows = None if max_num_rows <= 0 else int(max_num_rows)
777
+ resolved = {
778
+ "n_estimators": int(n_estimators),
779
+ "max_num_rows": resolved_rows,
780
+ "max_num_features": int(max_num_features),
781
+ "batch_size": int(batch_size),
782
+ "enable_nnls": bool(enable_nnls),
783
+ "n_feature_crosses": 0 if n_feature_crosses == "0" else n_feature_crosses,
784
+ "n_svd_features": 0 if n_svd_features == "0" else n_svd_features,
785
+ "max_eval_rows": None if max_eval_rows <= 0 else int(max_eval_rows),
786
+ }
787
+ if resolved["enable_nnls"] and resolved["max_num_rows"] is not None:
788
+ resolved["max_num_rows"] = None
789
+ return resolved
790
+
791
+
792
+ def run_tabfm(task: str, x_train: pd.DataFrame, x_test: pd.DataFrame, y_train: pd.Series, tabfm_params: dict[str, object]):
793
  from tabfm import TabFMClassifier, TabFMRegressor
794
 
795
  foundation_model = load_tabfm_model()
796
+ estimator = (
797
+ TabFMClassifier(model=foundation_model, **tabfm_params)
798
+ if task == "classification"
799
+ else TabFMRegressor(model=foundation_model, **tabfm_params)
800
+ )
801
  estimator.fit(x_train, y_train.to_numpy())
802
  pred = estimator.predict(x_test)
803
  proba = estimator.predict_proba(x_test) if task == "classification" and hasattr(estimator, "predict_proba") else None
 
834
  seed: int,
835
  selected_models: list[str],
836
  include_tabfm: bool,
837
+ tabfm_params: dict[str, object] | None = None,
838
  ) -> tuple[pd.DataFrame, pd.DataFrame, str]:
839
  df = df.sample(min(sample_size, len(df)), random_state=seed).reset_index(drop=True)
840
  x, y = split_xy(df, target)
 
864
  if include_tabfm:
865
  start = time.perf_counter()
866
  try:
867
+ tabfm_params = tabfm_params or TABFM_PRESETS["Fast"]
868
+ tabfm_display_params = dict(tabfm_params)
869
+ tabfm_model_params = dict(tabfm_params)
870
+ tabfm_eval_rows = tabfm_model_params.pop("max_eval_rows", None)
871
+ if tabfm_eval_rows is not None and len(x_test) > int(tabfm_eval_rows):
872
+ eval_idx = x_test.sample(int(tabfm_eval_rows), random_state=seed).index
873
+ x_eval = x_test.loc[eval_idx]
874
+ y_eval = y_test.loc[eval_idx]
875
+ status = f"ok ({len(x_eval):,}/{len(x_test):,} test rows)"
876
+ else:
877
+ x_eval = x_test
878
+ y_eval = y_test
879
+ status = "ok"
880
+ pred, proba = run_tabfm(task, x_train, x_eval, y_train, tabfm_model_params)
881
+ metrics = score_predictions(task, y_eval, pred, proba)
882
+ rows.append({"model": "TabFM", "status": status, "seconds": time.perf_counter() - start, **metrics})
883
  except Exception as exc:
884
  rows.append({"model": "TabFM", "status": f"unavailable: {exc}", "seconds": time.perf_counter() - start})
885
  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}`.")
 
897
  )
898
  if notes:
899
  summary += "\n\n" + "\n".join(f"- {note}" for note in notes)
900
+ if include_tabfm:
901
+ summary += f"\n\n**TabFM params:** `{tabfm_display_params}`"
902
  return results[["rank", "model", "status", *metric_cols]], preview, summary
903
 
904
 
 
1006
  include_tabfm: bool,
1007
  selected_metrics: list[str],
1008
  chart_style: str,
1009
+ tabfm_preset: str,
1010
+ tabfm_n_estimators: int,
1011
+ tabfm_max_rows: int,
1012
+ tabfm_max_features: int,
1013
+ tabfm_batch_size: int,
1014
+ tabfm_enable_nnls: bool,
1015
+ tabfm_crosses: str,
1016
+ tabfm_svd: str,
1017
+ tabfm_max_eval_rows: int,
1018
  ):
1019
  spec = get_spec(dataset_name)
1020
  df = get_dataset(dataset_name, sample_size, seed)
1021
+ tabfm_params = resolve_tabfm_params(tabfm_preset, tabfm_n_estimators, tabfm_max_rows, tabfm_max_features, tabfm_batch_size, tabfm_enable_nnls, tabfm_crosses, tabfm_svd, tabfm_max_eval_rows)
1022
+ results, preview, summary = benchmark_frame(df, spec.target, spec.task, sample_size, test_percent / 100, seed, selected_models, include_tabfm, tabfm_params)
1023
  return summary, results.round(4), metric_chart(results, selected_metrics, chart_style), time_chart(results), preview
1024
 
1025
 
 
1034
  include_tabfm: bool,
1035
  selected_metrics: list[str],
1036
  chart_style: str,
1037
+ tabfm_preset: str,
1038
+ tabfm_n_estimators: int,
1039
+ tabfm_max_rows: int,
1040
+ tabfm_max_features: int,
1041
+ tabfm_batch_size: int,
1042
+ tabfm_enable_nnls: bool,
1043
+ tabfm_crosses: str,
1044
+ tabfm_svd: str,
1045
+ tabfm_max_eval_rows: int,
1046
  ):
1047
  if file is None:
1048
  raise gr.Error("Upload a CSV file first.")
1049
  df = pd.read_csv(file.name)
1050
  selected_task = None if task == "Auto" else task.lower()
1051
+ tabfm_params = resolve_tabfm_params(tabfm_preset, tabfm_n_estimators, tabfm_max_rows, tabfm_max_features, tabfm_batch_size, tabfm_enable_nnls, tabfm_crosses, tabfm_svd, tabfm_max_eval_rows)
1052
+ results, preview, summary = benchmark_frame(df, target, selected_task, sample_size, test_percent / 100, seed, selected_models, include_tabfm, tabfm_params)
1053
  return summary, results.round(4), metric_chart(results, selected_metrics, chart_style), time_chart(results), preview
1054
 
1055
 
 
1104
  """
1105
  )
1106
  with gr.Row():
1107
+ gr.HTML(f'<div class="stat-card"><div class="label">Benchmark catalog</div><div class="value">{len(DATASETS)} <span class="trend">mixed tasks</span></div><div class="label">Classification + regression</div></div>')
1108
  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>')
1109
  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>')
1110
  with gr.Tabs():
 
1112
  with gr.Row():
1113
  with gr.Column(scale=1, elem_classes=["panel"]):
1114
  dataset = gr.Dropdown(dataset_names(), value="Titanic Survival", label="Dataset")
1115
+ sample = gr.Slider(100, 50000, value=1000, step=100, label="Sample size")
1116
  test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
1117
  seed = gr.Number(value=42, precision=0, label="Random seed")
1118
+ models = gr.CheckboxGroup(DEFAULT_MODELS, value=["HistGradientBoosting", "Dummy"], label="Baselines")
1119
  include_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
1120
  metric_toggles = gr.CheckboxGroup(METRIC_CHOICES, value=["rmse", "mae", "r2", "accuracy", "f1_weighted", "roc_auc"], label="Chart metrics")
1121
  chart_style = gr.Radio(["Line", "Radar"], value="Line", label="Chart style")
1122
+ with gr.Accordion("TabFM tuning", open=False):
1123
+ tabfm_preset = gr.Dropdown(list(TABFM_PRESETS.keys()) + ["Custom"], value="Fast", label="Preset")
1124
+ tabfm_n_estimators = gr.Slider(1, 32, value=1, step=1, label="Estimators")
1125
+ tabfm_max_rows = gr.Slider(0, 5000, value=256, step=64, label="Max context rows (0 = no cap)")
1126
+ tabfm_max_features = gr.Slider(8, 500, value=64, step=8, label="Max features")
1127
+ tabfm_batch_size = gr.Slider(1, 8, value=1, step=1, label="Batch size")
1128
+ tabfm_enable_nnls = gr.Checkbox(value=False, label="NNLS ensemble weights")
1129
+ tabfm_crosses = gr.Radio(["0", "sqrt"], value="0", label="Feature crosses")
1130
+ tabfm_svd = gr.Radio(["0", "sqrt"], value="0", label="SVD features")
1131
+ tabfm_max_eval_rows = gr.Slider(0, 5000, value=256, step=64, label="Max TabFM test rows (0 = no cap)")
1132
  run_btn = gr.Button("Run benchmark", variant="primary")
1133
  with gr.Column(scale=3):
1134
  summary = gr.Markdown()
 
1137
  chart = gr.Plot(label="Metric comparison")
1138
  speed = gr.Plot(label="Speed")
1139
  preview = gr.Dataframe(label="Held-out preview", interactive=False)
1140
+ run_inputs = [
1141
+ dataset,
1142
+ sample,
1143
+ test_pct,
1144
+ seed,
1145
+ models,
1146
+ include_tabfm,
1147
+ metric_toggles,
1148
+ chart_style,
1149
+ tabfm_preset,
1150
+ tabfm_n_estimators,
1151
+ tabfm_max_rows,
1152
+ tabfm_max_features,
1153
+ tabfm_batch_size,
1154
+ tabfm_enable_nnls,
1155
+ tabfm_crosses,
1156
+ tabfm_svd,
1157
+ tabfm_max_eval_rows,
1158
+ ]
1159
  run_btn.click(run_catalog, run_inputs, [summary, leaderboard, chart, speed, preview])
1160
  metric_toggles.change(redraw_metric_chart, [leaderboard, metric_toggles, chart_style], chart)
1161
  chart_style.change(redraw_metric_chart, [leaderboard, metric_toggles, chart_style], chart)
 
1166
  file = gr.File(label="CSV file", file_types=[".csv"])
1167
  target = gr.Textbox(label="Target column")
1168
  task = gr.Radio(["Auto", "Classification", "Regression"], value="Auto", label="Task")
1169
+ upload_sample = gr.Slider(100, 50000, value=1000, step=100, label="Sample size")
1170
  upload_test_pct = gr.Slider(10, 40, value=25, step=5, label="Test split (%)")
1171
  upload_seed = gr.Number(value=42, precision=0, label="Random seed")
1172
+ upload_models = gr.CheckboxGroup(DEFAULT_MODELS, value=["HistGradientBoosting", "Dummy"], label="Baselines")
1173
  upload_tabfm = gr.Checkbox(value=False, label="Run TabFM live")
1174
  upload_metric_toggles = gr.CheckboxGroup(METRIC_CHOICES, value=["rmse", "mae", "r2", "accuracy", "f1_weighted", "roc_auc"], label="Chart metrics")
1175
  upload_chart_style = gr.Radio(["Line", "Radar"], value="Line", label="Chart style")
1176
+ with gr.Accordion("TabFM tuning", open=False):
1177
+ upload_tabfm_preset = gr.Dropdown(list(TABFM_PRESETS.keys()) + ["Custom"], value="Fast", label="Preset")
1178
+ upload_tabfm_n_estimators = gr.Slider(1, 32, value=1, step=1, label="Estimators")
1179
+ upload_tabfm_max_rows = gr.Slider(0, 5000, value=256, step=64, label="Max context rows (0 = no cap)")
1180
+ upload_tabfm_max_features = gr.Slider(8, 500, value=64, step=8, label="Max features")
1181
+ upload_tabfm_batch_size = gr.Slider(1, 8, value=1, step=1, label="Batch size")
1182
+ upload_tabfm_enable_nnls = gr.Checkbox(value=False, label="NNLS ensemble weights")
1183
+ upload_tabfm_crosses = gr.Radio(["0", "sqrt"], value="0", label="Feature crosses")
1184
+ upload_tabfm_svd = gr.Radio(["0", "sqrt"], value="0", label="SVD features")
1185
+ upload_tabfm_max_eval_rows = gr.Slider(0, 5000, value=256, step=64, label="Max TabFM test rows (0 = no cap)")
1186
  upload_btn = gr.Button("Run uploaded dataset", variant="primary")
1187
  with gr.Column(scale=3):
1188
  upload_summary = gr.Markdown()
 
1193
  upload_preview = gr.Dataframe(label="Held-out preview", interactive=False)
1194
  upload_btn.click(
1195
  run_upload,
1196
+ [
1197
+ file,
1198
+ target,
1199
+ task,
1200
+ upload_sample,
1201
+ upload_test_pct,
1202
+ upload_seed,
1203
+ upload_models,
1204
+ upload_tabfm,
1205
+ upload_metric_toggles,
1206
+ upload_chart_style,
1207
+ upload_tabfm_preset,
1208
+ upload_tabfm_n_estimators,
1209
+ upload_tabfm_max_rows,
1210
+ upload_tabfm_max_features,
1211
+ upload_tabfm_batch_size,
1212
+ upload_tabfm_enable_nnls,
1213
+ upload_tabfm_crosses,
1214
+ upload_tabfm_svd,
1215
+ upload_tabfm_max_eval_rows,
1216
+ ],
1217
  [upload_summary, upload_leaderboard, upload_chart, upload_speed, upload_preview],
1218
  )
1219
  upload_metric_toggles.change(redraw_metric_chart, [upload_leaderboard, upload_metric_toggles, upload_chart_style], upload_chart)
 
1222
  gr.Dataframe(catalog_table(), interactive=False, label="Included benchmark catalog")
1223
  gr.Markdown(
1224
  """
1225
+ Most catalog datasets are loaded from OpenML, KaggleHub, FiveThirtyEight GitHub data, or sklearn. Each remote loader has a fallback so the Space remains usable if an upstream dataset is temporarily unavailable.
1226
  """
1227
  )
1228
  with gr.Tab("Implementation Notes"):
 
1230
  """
1231
  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.
1232
 
1233
+ TabFM is attempted only when **Run TabFM live** is enabled because the first run downloads large model weights and CPU Basic inference can be slow. Use the **Fast** preset for a quick smoke test, then increase estimators/context rows for stronger but slower runs.
1234
 
1235
  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`.
1236
  """