Spaces:
Build error
Build error
Add datasets and TabFM tuning
Browse files
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=
|
| 477 |
-
"RandomForest": Pipeline([("prep", make_preprocessor(pd.DataFrame())), ("model", RandomForestClassifier(n_estimators=
|
| 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=
|
| 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=
|
| 496 |
]
|
| 497 |
)
|
| 498 |
else:
|
| 499 |
models["XGBoost"] = Pipeline(
|
| 500 |
[
|
| 501 |
("prep", make_preprocessor(pd.DataFrame())),
|
| 502 |
-
("model", XGBRegressor(n_estimators=
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
from tabfm import TabFMClassifier, TabFMRegressor
|
| 524 |
|
| 525 |
foundation_model = load_tabfm_model()
|
| 526 |
-
estimator =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 593 |
-
|
| 594 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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">
|
| 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=
|
| 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=["
|
| 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 = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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=["
|
| 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 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 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 |
"""
|