"""Regenerate the perfmap: refit PCA, recluster, auto-derive cluster names.
Run from /Users/aabraham/hf/tabbench/. Writes:
- perfmap_freeze.json : new PCA basis + cluster labels
- perfmap_regen_report.json : Spearman correlations + cluster diagnostics
"""
import json
import os
import sys
from collections import Counter, defaultdict
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
from sklearn.cluster import SpectralCoclustering
from sklearn.decomposition import PCA
HERE = os.path.dirname(os.path.abspath(__file__))
os.chdir(HERE)
# Exec app.py up to gr.Blocks to get loaded per-dataset data + helpers
src = open("app.py").read()
END = src.find("with gr.Blocks(")
ns = {"__file__": os.path.join(HERE, "app.py"), "__name__": "__main__"}
exec(compile(src[:END], "app.py", "exec"), ns)
public_per_dataset = ns["public_per_dataset"]
public_per_dataset["dataset_id"] = public_per_dataset["dataset_id"].astype(str)
# Drop hidden display models (single-fit XGBoost/CatBoost/LightGBM) before
# fitting the PCA / clustering. Otherwise GBDTs get six columns per metric
# (single + ensemble) and the PCA basis is biased toward GBDT structure.
HIDDEN = ns.get("_HIDDEN_DISPLAY_MODELS", set())
if HIDDEN:
before = public_per_dataset["model"].nunique()
public_per_dataset = public_per_dataset[~public_per_dataset["model"].isin(HIDDEN)].copy()
after = public_per_dataset["model"].nunique()
print(f"Dropped hidden display models ({sorted(HIDDEN)}): {before} → {after} models in M")
# --- 1. Build the rank-of-rank matrix M (datasets x model-metric columns) ---
METRICS = ("Accuracy", "AUC", "F1_score", "Precision", "Recall", "Cross_entropy")
LOWER = {"Cross_entropy"}
rank_frames = []
for metric in METRICS:
piv = public_per_dataset.pivot_table(
index="dataset_id", columns="model", values=metric, aggfunc="mean",
)
asc = metric not in LOWER # higher-is-better → ascending=False
ranks = piv.rank(axis=1, ascending=not asc, method="average")
n = piv.notna().sum(axis=1).clip(lower=1)
rp = (ranks.sub(1, axis=0)).div(n - 1, axis=0).fillna(0.5)
rp.columns = [f"{metric}/{m}" for m in rp.columns]
rank_frames.append(rp)
M = pd.concat(rank_frames, axis=1)
M = 1 - M # higher = wins
print(f"M shape: {M.shape} (datasets × model_metric_cols)")
# --- 2. Refit PCA(2) on M ---
pca = PCA(n_components=2, random_state=0)
scores_all = pca.fit_transform(M.values)
print(f"PCA explained variance: PC1={pca.explained_variance_ratio_[0]:.4f} "
f"PC2={pca.explained_variance_ratio_[1]:.4f}")
# --- 3. Spearman correlations of PC1/PC2 vs dataset metadata ---
meta = pd.DataFrame(json.load(open("public_datasets_info.json")))
meta["dataset_id"] = meta["dataset_id"].astype(str)
acc_f1 = public_per_dataset.groupby("dataset_id").agg(
acc=("Accuracy", "mean"), f1=("F1_score", "mean"),
)
acc_f1["imbalance"] = acc_f1["acc"] - acc_f1["f1"]
scores_df = pd.DataFrame(scores_all, index=M.index, columns=["PC1", "PC2"])
joined = scores_df.merge(
meta.set_index("dataset_id")[["rows", "features", "n_classes", "pct_cat", "avg_card"]],
left_index=True, right_index=True, how="left",
).merge(acc_f1[["imbalance"]], left_index=True, right_index=True, how="left")
correlations = {}
for pc in ("PC1", "PC2"):
correlations[pc] = {}
for col in ("rows", "features", "n_classes", "pct_cat", "avg_card", "imbalance"):
sub = joined[[pc, col]].dropna()
if len(sub) < 5:
correlations[pc][col] = None
else:
rho, p = spearmanr(sub[pc], sub[col])
correlations[pc][col] = {"rho": float(rho), "p": float(p), "n": int(len(sub))}
print("\nSpearman ρ for PC1:")
for k, v in correlations["PC1"].items():
if v:
print(f" {k:14s} ρ={v['rho']:+.3f} (p={v['p']:.3e})")
print("Spearman ρ for PC2:")
for k, v in correlations["PC2"].items():
if v:
print(f" {k:14s} ρ={v['rho']:+.3f} (p={v['p']:.3e})")
# --- 4. Re-run SpectralCoclustering ---
N_CLUSTERS = 5
# clip to safe range for SVD
M_for_co = M.clip(0.0, 1.0).values
cc = SpectralCoclustering(n_clusters=N_CLUSTERS, random_state=0).fit(M_for_co)
row_labels = cc.row_labels_.astype(int)
col_labels = cc.column_labels_.astype(int)
print(f"\nCluster sizes:")
sizes = pd.Series(row_labels).value_counts().sort_index()
for c, n in sizes.items():
print(f" cluster {c}: {n} datasets")
# --- 5. Cluster name auto-derivation ---
# For each cluster, determine:
# - winner family (most often #1 in per-dataset rank)
# - 2nd family (most often #2 in rank)
# - median rows / pct_cat → size + structure
# - n_classes distribution → binary vs multi-class
FAMILIES = {
"FM2": ["seldon", "tabpfn_v3", "tabicl_v2"],
"FM1": ["tabicl", "tabpfn", "mitra", "limix", "tabdpt"],
"GBDT": ["xgboost_ensemble", "catboost_ensemble", "lightgbm_ensemble",
"xgboost", "catboost", "lightgbm"],
"NN": ["realmlp", "tabm", "modern_nca"],
}
FAMILY_OF = {m: fam for fam, models in FAMILIES.items() for m in models}
PRETTY = {
"FM2": "2nd-gen FM Supremacy",
"FM1": "1st-gen FM Supremacy",
"GBDT": "GBDT Supremacy",
"NN": "Tuned NN Supremacy",
}
RUNNER = {
"FM2": "1st-gen FMs runner-up",
"FM1": "GBDTs runner-up",
"GBDT": "2nd-gen FMs runner-up",
"NN": "FMs runner-up",
}
# Per-dataset accuracy pivot
acc_piv = public_per_dataset.pivot_table(
index="dataset_id", columns="model", values="Accuracy", aggfunc="mean",
).dropna(axis=0, how="any")
# Per-family mean accuracy per dataset → per-dataset family ranking
fam_acc = {}
for fam, models in FAMILIES.items():
cols = [m for m in models if m in acc_piv.columns]
if cols:
fam_acc[fam] = acc_piv[cols].mean(axis=1)
fam_acc = pd.DataFrame(fam_acc)
fam_rank = fam_acc.rank(axis=1, ascending=False, method="min") # 1 = best family
def derive_cluster_name(cluster_idx, member_ids):
sub_acc = fam_rank.loc[fam_rank.index.intersection(member_ids)]
if sub_acc.empty:
return f"Cluster {cluster_idx}"
# Most common family at rank 1 and at rank 2
rank1 = (sub_acc == 1).sum(axis=0).idxmax()
other_cols = [c for c in sub_acc.columns if c != rank1]
rank2_counts = (sub_acc[other_cols] == 2).sum(axis=0)
rank2_family = rank2_counts.idxmax() if not rank2_counts.empty else None
# Get metadata for size and structure
meta_sub = meta[meta["dataset_id"].isin(member_ids)]
median_rows = float(meta_sub["rows"].median()) if not meta_sub["rows"].dropna().empty else np.nan
median_pct_cat = float(meta_sub["pct_cat"].median()) if not meta_sub["pct_cat"].dropna().empty else np.nan
n_classes_vals = meta_sub["n_classes"].dropna().tolist()
pct_binary = sum(1 for c in n_classes_vals if c == 2) / max(len(n_classes_vals), 1)
# Composition descriptor
if median_rows < 3000:
size = "Small"
elif median_rows < 20000:
size = "Mid-size"
else:
size = "Large"
if pct_binary > 0.8:
cls = "binary"
elif pct_binary < 0.3:
cls = "multi-class"
else:
cls = "mixed"
if median_pct_cat is not np.nan and median_pct_cat > 0.4:
struct = "categorical"
elif median_pct_cat is not np.nan and median_pct_cat < 0.1:
struct = "numeric"
else:
struct = ""
composition_parts = [size, struct, cls] if struct else [size, cls]
composition = " ".join(p for p in composition_parts if p)
leader_label = PRETTY.get(rank1, rank1)
if rank2_family:
runner_label = (
f"{rank2_family.replace('FM2', '2nd-gen FMs').replace('FM1', '1st-gen FMs').replace('GBDT','GBDTs').replace('NN','Tuned NNs')} runner-up"
)
else:
runner_label = "Mixed runner-ups"
return f"{leader_label}
{runner_label}
{composition}"
cluster_names = {}
cluster_diag = {}
for c in sorted(set(row_labels)):
c = int(c) # python int for JSON
members = M.index[row_labels == c].tolist()
name = derive_cluster_name(c, members)
cluster_names[c] = name
meta_sub = meta[meta["dataset_id"].isin(members)]
cluster_diag[c] = {
"n_datasets": int((row_labels == c).sum()),
"name": name,
"median_rows": float(meta_sub["rows"].median()) if not meta_sub["rows"].dropna().empty else None,
"median_features": float(meta_sub["features"].median()) if not meta_sub["features"].dropna().empty else None,
"median_n_classes": float(meta_sub["n_classes"].median()) if not meta_sub["n_classes"].dropna().empty else None,
"median_pct_cat": float(meta_sub["pct_cat"].median()) if not meta_sub["pct_cat"].dropna().empty else None,
"pct_binary": float(sum(1 for v in meta_sub["n_classes"].dropna().tolist() if v == 2) / max(len(meta_sub["n_classes"].dropna()), 1)),
}
print(f"\nCluster {c} (n={cluster_diag[c]['n_datasets']}):")
print(f" name: {name}")
print(f" median rows={cluster_diag[c]['median_rows']} features={cluster_diag[c]['median_features']} classes={cluster_diag[c]['median_n_classes']}")
print(f" pct_cat median={cluster_diag[c]['median_pct_cat']} binary={cluster_diag[c]['pct_binary']:.2f}")
# --- 6. Write the new freeze ---
freeze = {
"pca_components": pca.components_.tolist(),
"pca_mean": pca.mean_.tolist(),
"feature_cols": list(M.columns),
"dataset_ids": list(M.index),
"row_labels": [int(x) for x in row_labels],
"col_labels": [int(x) for x in col_labels],
"cluster_names": {str(k): v for k, v in cluster_names.items()},
"regenerated_at": "2026-05-29",
"n_datasets": int(M.shape[0]),
"n_features": int(M.shape[1]),
}
# Backup old freeze
old_freeze_path = "perfmap_freeze.json"
if os.path.exists(old_freeze_path):
bak = old_freeze_path + ".bak.20260529"
if not os.path.exists(bak):
import shutil
shutil.copyfile(old_freeze_path, bak)
print(f"\nBacked up old freeze → {bak}")
with open(old_freeze_path, "w") as f:
json.dump(freeze, f)
print(f"Wrote {old_freeze_path}")
# --- 7. Diagnostics report ---
report = {
"pca_explained_variance": {
"PC1": float(pca.explained_variance_ratio_[0]),
"PC2": float(pca.explained_variance_ratio_[1]),
},
"spearman_correlations": correlations,
"cluster_diagnostics": {str(k): v for k, v in cluster_diag.items()},
"cluster_names_for_app": {str(k): v for k, v in cluster_names.items()},
}
with open("perfmap_regen_report.json", "w") as f:
json.dump(report, f, indent=2)
print("Wrote perfmap_regen_report.json")