VotePredictor

AI Models

VotePredictor forecasts with TabPFN, a tabular foundation model. To check that's the right choice, we raced it against the whole machine-learning zoo — boosted trees, random forests, a Gaussian process, a neural net, kernel and nearest-neighbor methods, regularized linears, and an ensemble — on the same features, the same walk-forward splits, and the same races. Here's how they all did.

TabPFN
wins outright
best margin error and Brier of any model tested
Beats all but 1
the no-ML poll average
every fancier model except TabPFN is worse than just averaging
14 models
head-to-head
1,606 races, walk-forward, identical inputs
#ModelFamilyMargin errorWinnerBrier
1TabPFNTabular foundation model
5.20
88%0.085
2Blend (TabPFN+RF+CatBoost)Ensemble
5.26
88%0.086
3Poll avg (no ML)Baseline — no ML
5.48
87%0.089
4CatBoostGradient boosting
5.49
88%0.087
5RandomForestTree ensemble
5.65
87%0.094
6ExtraTreesTree ensemble
5.69
87%0.095
7XGBoostGradient boosting
5.71
86%0.093
8ElasticNetLinear
5.87
86%0.094
9LightGBMGradient boosting
6.02
85%0.101
10BayesianRidgeLinear
6.08
85%0.102
11HistGBMGradient boosting
6.22
86%0.102
12GaussianProcessGaussian process
6.50
84%0.108
13MLP (64,32)Neural net
6.87
83%0.115
14kNN (k=15)Nearest neighbors
7.26
83%0.116
15SVR (RBF)Kernel
7.67
84%0.108

15 approaches · 1,606 races · walk-forward, 2008+ · same features and splits for all. Margin error = average miss in points (the bar scales with it — shorter is better); Brier scores the win probability. Lower is better on both.

Why TabPFN wins — the small-data story

Election forecasting is a small-data problem: a few hundred comparable races per cycle, not millions of rows. That's the regime where conventional machine learning struggles — gradient-boosted trees, random forests and neural nets have the capacity to overfit, and with so little data they do. The tell is right there in the table: the bias-corrected poll average, with no machine learning at all, beats every model except TabPFN. The best boosting library (CatBoost) only ties it; everything fancier lands behind it.

TabPFN is built for exactly this regime — it's a transformer pre-trained on millions of synthetic small-data tasks, so it predicts in a single forward pass without overfitting the way a freshly-trained tree ensemble would. It's the one learner that adds real value over simply averaging the polls — and even blending it with the runners-up doesn't help (the ensemble buys a sliver of winner accuracy at worse margin error and Brier).

Every number here is strictly out-of-sample: to score any past race, each model only ever trained on earlier cycles. Full detail in the methodology; the bake-off is reproducible in aielect/exp_model_zoo.py.

Does stacking help? (the "run a lot of stacked models" question)

A fixed average barely moves the needle, so we tried the real thing — a stack: a meta-learner trained on the out-of-fold predictions of diverse base models (the polls-only model, a campaign-spending model, a fundamentals-only model, random forests), strictly walk-forward. Overall it still loses to TabPFN — with only ~9 cycles and bases that mostly read the same polls, the meta-learner overfits the blend.

Model / stackMargin MAEWinnerBrier
TabPFN+spend5.20487.3%0.088
TabPFNproduction5.28687.6%0.088
Stack-NNLS5.32286.3%0.091
Avg(TabPFN+RF+Cat)5.33888.0%0.089
Stack-Ridge5.47486.7%0.093
Fundamentals9.92074.2%0.170

But the interesting part is where. Split by how many polls a race has, stacking helps exactly where the bases actually disagree — thin-poll races, the same place campaign spending adds signal. Lowest error per row in green:

Poll densityracesTabPFNTabPFN+spendStack-NNLS
≤1 poll3547.036.666.82
2 polls2066.356.246.18
3–5 polls3545.185.295.34
6+ polls5033.703.693.91

On 2-poll races the stack is the single best model, and on ≤2-poll races the spending-aware models beat the polls-only TabPFN; on well-polled races (3+) the polls dominate and stacking only adds noise. So production stays TabPFN, with the spending/stacking edge reserved for the sparse-poll races where it's real — reproducible in aielect/exp_stack.py.