Google Research introduced TabFM, a foundation model built for tabular data. TabFM performs classification and regression without dataset-specific training. Every prediction comes from a single forward pass. The model reframes tabular prediction as an in-context learning problem. It is available now on Hugging Face and GitHub.
TL;DR
- TabFM predicts on unseen tables with no training, tuning, or feature engineering.
- It reads the full dataset as one prompt, then predicts via in-context learning.
- The architecture combines TabPFN-style row/column attention with TabICL-style in-context learning.
- Training used hundreds of millions of synthetic datasets from structural causal models.
- Google BigQuery will expose TabFM through an AI.PREDICT SQL command soon.
What is TabFM?
Tabular data forms the backbone of enterprise data infrastructure. Tasks like customer churn and financial fraud detection live in tables. For years, tree-based methods dominated this space. XGBoost, AdaBoost, and random forests offered robust results on structured data. Google frames TabFM as the tabular counterpart to TimesFM, its zero-shot time-series model.
That reliability carried a cost. Fitting XGBoost to a new dataset is rarely one .fit() call. Data scientists spend hours on hyperparameter optimization and feature engineering. They do this just to extract a reliable signal from raw data. TabFM targets exactly that bottleneck.
TabFM applies the zero-shot logic that large language models made familiar. LLMs learn new tasks from in-context examples, without updating any weights. This technique is called in-context learning (ICL). TabFM brings the same idea to tables. It generates predictions on previously unseen tables in one pass.
How It Works
Traditional models update parameters for each dataset’s distribution. TabFM skips that step entirely. It takes the whole dataset as a single unified prompt. That prompt holds both training examples and target testing rows. The model reads column and row relationships at inference time.
Tables are not text. They are two-dimensional and inherently orderless. Swapping two rows or two columns does not change their meaning. Standard language models process one-dimensional, ordered sequences instead. To bridge that gap, TabFM synthesizes TabPFN and TabICL into a hybrid design.
It relies on three mechanisms:
- Alternating row and column attention: The raw table passes through a multilayer attention module. Following TabPFN, attention alternates across columns (features) and rows (examples). This deep contextualization captures feature interactions and dependencies. It performs work that would otherwise need manual feature crafting.
- Row compression: Each row’s cross-attended information compresses into a single dense vector.
- In-context learning: A dedicated Transformer runs over these compressed embeddings. Following TabICL, attending to compressed rows cuts computation cost sharply. Prediction stays efficient even on much larger datasets.
Training On Synthetic Data at Scale
Foundation models need vast, diverse data. High-quality tabular datasets are scarce in the open-source space. Industrial tables carry proprietary schemas and sensitive information. That makes them inaccessible for broad pre-training.
Synthetic tables can be generated to be arbitrarily large. Google’s research team calls them effectively the only viable option at this scale. So TabFM trains entirely on hundreds of millions of synthetic datasets. These are generated dynamically using structural causal models (SCMs). Each incorporates a wide variety of random functions. The approach captures distributions and complex feature relationships found in real tables. The research team reports the model generalizes well to unseen real-world data.
Performance and Benchmarking
The research team evaluated TabFM on TabArena. TabArena is a living benchmark that computes Elo scores from head-to-head win rates. The evaluation spans 38 classification datasets and 13 regression datasets. Sample sizes range from 700 to 150,000.
Two configurations were tested. Plain TabFM runs out-of-the-box in a single forward pass. It needs no tuning or cross-validation. TabFM-Ensemble adds cross features and SVD (Singular Value Decomposition) features. It computes optimal weights for a 32-way ensemble using a non-negative least squares solver. For classification, it also adds Platt scaling as a calibration step.
The research team reports TabFM consistently outperforms heavily tuned, industry-standard supervised algorithms. Full per-fold metrics and head-to-head win rates sit on the GitHub page.
AspectTraditional GBDT (XGBoost)TabFMTabFM-EnsemblePer-dataset trainingRequiredNone (in-context learning)NoneHyperparameter tuningExtensive, manualNoneEnsemble weights via NNLSFeature engineeringManual, domain-specificLearned by attentionAdds cross + SVD featuresPredictionAfter full trainingSingle forward pass32-way ensembleCalibrationManual (optional)—Platt scaling (classification)
Getting Started: Installation and Code
Installation clones the repository and installs it locally. The base install uses CPU-only JAX. A cuda extra pulls the CUDA 12 plugin and NVIDIA libraries for GPU runs.
Core requirements are specific. You need Python 3.11 or later. It pins jax==0.10.1 and flax==0.12.7, using the modern flax.nnx API. Hugging Face Hub downloads the pre-trained weights automatically.
import numpy as np
import pandas as pd
from tabfm import tabfm_v1_0_0
from tabfm import TabFMClassifier
# Load pre-trained TabFM v1.0.0 (downloads from Hugging Face)
model = tabfm_v1_0_0.load()
# scikit-learn compatible classifier
clf = TabFMClassifier(model=model)
X_train = pd.DataFrame(
var t = TASKS[state.task];
var nr = clone(state.rows[state.rows.length-1] )
y_train = np.array([“low_risk”, “high_risk”, “low_risk”, “high_risk”])
X_test = pd.DataFrame()
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
probabilities = clf.predict_proba(X_test)
print(“Predictions:”, predictions)
print(“Class Probabilities:\n”, probabilities)
Here fit() prepares ordinal encoders and numerical scalers. It does not train model weights on your data. The regressor mirrors this pattern with TabFMRegressor and reg.predict().
Use Cases With Examples
The API fits common predictive tasks directly. For customer churn, the context holds past customers labeled churned or retained. TabFM scores churn risk for new customers in one pass.
For credit risk, rows carry age, job, and income features. Labels mark low_risk or high_risk, as in the sample code. New applicants get scored without a training cycle.
For regression, house price prediction is a natural fit. Context rows carry square footage and neighborhood. TabFM returns a predicted price for unseen listings.
Interactive Explainer
“;
var bodyHtml = “”;
state.rows.forEach(function(row,ri){
bodyHtml += “”;
cols().forEach(function(c)
var t = TASKS[state.task];
var nr = clone(state.rows[state.rows.length-1] );
// target cell
if(t.target.numeric) t.rows[0]);
state.rows.push(nr);
renderTable();
postSize();
else {
var sel = “”;
t.target.opts.forEach(function(o){ sel += “”+o+””; });
sel += “”;
bodyHtml += “”+sel+””;
}
bodyHtml += “×”;
bodyHtml += “”;
});
bodyHtml += “”;
tbl.innerHTML = head + bodyHtml;
tbl.querySelectorAll(“input,select”).forEach(function(el){
el.addEventListener(“change”, function(){
var ri = +el.getAttribute(“data-ri”), k = el.getAttribute(“data-k”);
var v = el.value;
var numeric = (el.type === “number”);
state.rows[ri][k] = numeric ? parseFloat(v) : v;
});
});
tbl.querySelectorAll(“.del”).forEach(function(b){
b.addEventListener(“click”, function(){
if(state.rows.length<=2) return;
state.rows.splice(+b.getAttribute(“data-del”),1);
renderTable();
});
});
$(“addRow”).disabled = state.rows.length>=10;
}
function cellInput(c,val,ri){
if(c.opts){
var s = “”;
c.opts.forEach(function(o){ s += “”+o+””; });
return s+””;
}
return “”;
}
function renderTestInputs(){
var t = TASKS[state.task], wrap = $(“testInputs”);
var html = “”;
cols().forEach(function(c){
html += “
“+c.label+””;
if(c.opts){
html += “”;
c.opts.forEach(function(o){ html += “”+o+””; });
html += “”;
} else {
html += “”;
}
html += “
“;
});
wrap.innerHTML = html;
wrap.querySelectorAll(“input,select”).forEach(function(el){
el.addEventListener(“change”, function(){
var k = el.getAttribute(“data-tk”);
state.test[k] = (el.type===”number”) ? parseFloat(el.value) : el.value;
});
});
}
// —- illustrative predictor: distance-weighted k-NN over context rows —-
function ranges(){
var t = TASKS[state.task], r = {};
t.numeric.forEach(function(c){
var vals = state.rows.map(function(row){return row[c.key];});
var mn = Math.min.apply(null,vals), mx = Math.max.apply(null,vals);
r[c.key] = (mx-mn) || 1;
});
return r;
}
function distance(a,b,rg){
var t = TASKS[state.task], d = 0;
t.numeric.forEach(function(c){
var diff = (a[c.key]-b[c.key])/rg[c.key];
d += diff*diff;
});
t.cat.forEach(function(c){
if(a[c.key]!==b[c.key]) d += 1;
});
return Math.sqrt(d);
}
function predict(){
var t = TASKS[state.task], rg = ranges();
var scored = state.rows.map(function(row){
var dist = distance(state.test,row,rg);
return {row:row, dist:dist, w:1/(dist*dist+0.05)};
}).sort(function(a,b){return a.dist-b.dist;});
var k = Math.min(5, scored.length);
var top = scored.slice(0,k);
var wsum = top.reduce(function(s,x){return s+x.w;},0);
if(t.target.numeric){
var est = top.reduce(function(s,x){return s + x.w*x.row[t.target.key];},0)/wsum;
showRegression(est, top);
} else {
var probs = {};
t.target.opts.forEach(function(o){ probs[o]=0; });
top.forEach(function(x){ probs[x.row[t.target.key]] += x.w; });
t.target.opts.forEach(function(o){ probs[o] = probs[o]/wsum; });
showClassification(probs, top);
}
}
function fmtMoney(v){ return “$”+Math.round(v).toLocaleString(“en-US”); }
function neighborLine(top){
var t = TASKS[state.task];
var parts = top.map(function(x){
var lab = t.target.numeric ? fmtMoney(x.row[t.target.key]) : x.row[t.target.key];
return “”+lab+””;
});
return “Nearest context rows used: “+parts.join(” “);
}
function showClassification(probs, top){
var t = TASKS[state.task];
var best = t.target.opts[0];
t.target.opts.forEach(function(o){ if(probs[o]>probs[best]) best=o; });
var isChurn = (best===”Yes”);
var conf = (probs[best]*100).toFixed(1);
var bars = “”;
t.target.opts.forEach(function(o){
var pct = (probs[o]*100);
var color = (o===”Yes”) ? “var(–neg)” : “var(–pos)”;
bars += “
“+
““+
“
“;
});
$(“result”).innerHTML =
“
Prediction · class probabilities
“+
“
Churn: “+best+”
“+
“
Confidence “+conf+”% · decided purely from the context rows above.
“+
“
“+bars+”
“+
“
“+neighborLine(top)+”
“+
“
In TabFM, this maps to predict_proba(). No weights were updated — “+
“the answer is read from the context in a single forward pass.
“;
reveal();
}
function showRegression(est, top){
$(“result”).innerHTML =
“
Prediction · estimated value
“+
“
“+fmtMoney(est)+”
“+
“
Estimated from the “+top.length+” nearest context rows, weighted by similarity.
“+
“
“+neighborLine(top)+”
“+
“
In TabFM, this maps to reg.predict(). No per-dataset training ran — “+
“the estimate comes from the context in one forward pass.
“;
reveal();
}
function reveal(){
var r = $(“result”);
r.className = “result show”;
setTimeout(function(){
r.querySelectorAll(“.bar-fill”).forEach(function(f){
f.style.width = f.getAttribute(“data-pct”)+”%”;
});
},40);
$(“runNote”).textContent = “Try editing the context, then predict again.”;
postSize();
}
// —- wiring —-
$(“tabs”).addEventListener(“click”, function(e){
var b = e.target.closest(“.tab”); if(!b) return;
$(“tabs”).querySelectorAll(“.tab”).forEach(function(x){x.classList.remove(“on”);});
b.classList.add(“on”);
loadTask(b.getAttribute(“data-task”));
postSize();
});
$(“addRow”).addEventListener(“click”, function(){
var t = TASKS[state.task];
var nr = clone(state.rows[state.rows.length-1] || t.rows[0]);
state.rows.push(nr);
renderTable();
postSize();
});
$(“resetRows”).addEventListener(“click”, function(){
state.rows = clone(TASKS[state.task].rows);
renderTable();
postSize();
});
$(“runBtn”).addEventListener(“click”, predict);
// —- auto-resize for WordPress iframe embed —-
function postSize(){
var h = document.body.scrollHeight;
if(window.parent){ window.parent.postMessage({tabfmHeight:h}, “*”); }
}
window.addEventListener(“load”, postSize);
window.addEventListener(“resize”, postSize);
var ro = window.ResizeObserver ? new ResizeObserver(postSize) : null;
if(ro) ro.observe(document.body);
loadTask(“churn”);
setTimeout(postSize, 60);
})();
