- Better risk ranking, zero feature engineering. Straight from the 9-column raw dataframe, TabPFN reaches an exposure-weighted Gini of ~0.32 vs ~0.31 for the best hand-built GLM, better separation of safe from risky policies, which is what drives pricing.
- Per-policy uncertainty for free. TabPFN returns a full predictive distribution per policy, so you can read the expected claim and the tail (“how bad could this one get?”), the GLM gives you only a fitted mean.
- Calibration on par, with one optional line. With a single portfolio-level rescale — the same balancing a log-link GLM does implicitly — TabPFN matches the GLM on calibration while leaving the ranking untouched.
Requirements. This notebook runs on the hosted TabPFN client (tabpfn-client), so no local GPU is
needed; you just need a free API token (see the setup cells below). Baselines, dataset explanation and metrics follow scikit-learn’s
Tweedie regression on insurance claims example which motivated this tutorial.
The data
Each row is an insurance policy — a contract between the insurer and a policyholder — described by driver age, vehicle age, vehicle power, region, and so on. A claim is a request for compensation; the claim amount is what the insurer pays out. Exposure is how long the policy was actually observed, measured in years: a policy on the books for the full period hasExposure = 1.0, one observed for six months has Exposure ≈ 0.5. It matters because a policy
watched for two months has had far less opportunity to claim than one watched all year, so every
rate below is expressed per unit of exposure.
Our target is the pure premium: the expected claim cost per unit of exposure, i.e., the
break-even price of the risk. There are two classic ways to model it:
- Single-stage: model the total claim cost per unit exposure directly.
- Two-stage: model the claim frequency (claims per year) and the severity (average amount per claim) separately, then multiply.
reMTPL2freq holds one row per policy (risk features, claim count ClaimNb, and Exposure), freMTPL2sev holds the individual claim amounts. We aggregate claim amounts per policy and join;
policies with no claim get ClaimAmount = 0. We apply the same light cleaning as the scikit-learn
example (cap extreme values, make ClaimNb consistent with ClaimAmount) and derive the targets
Frequency = ClaimNb / Exposure, AvgClaimAmount = ClaimAmount / ClaimNb, and
PurePremium = ClaimAmount / Exposure.
Note the feature columns: VehBrand, VehGas, Region, Area are strings, the rest are
numeric on very different scales. A GLM cannot consume this directly; TabPFN can.
We hold out 25% of policies for testing with a random split. A production pricing model would
usually be validated out of time (train on older policy years, test on newer) to catch drift, but
freMTPL2 ships without a usable time index, so we follow the scikit-learn example and split at
random; a simplification worth remembering for similar use cases.
Metrics
Two questions matter for a pricing model, and they need different metrics. Ranking / lift — does the model sort safe from risky policies? This is what segmentation and pricing rely on, and our headline metric. We use the exposure-weighted Gini. Sort policies from safest to riskiest by predicted premium, then trace the cumulative share of actual claim cost against the cumulative share of exposure — the Lorenz curve . The Gini is (twice) the area between that curve and the diagonal: Higher is better; a model with no information scores 0. In code it is1 - 2 * auc(cum_exposure, cum_claims).
Calibration — is the predicted premium level right? We use the Tweedie deviance (lower is
better) and its normalized cousin D² (higher is better, an R²-style score), the standard loss for
zero-inflated, right-skewed cost targets. This is exactly the GLM’s own training objective.
We deliberately skip MAE (degenerate on a 96%-zero target — it rewards predicting near-zero for
everyone) and RMSE (dominated by a handful of catastrophic claims).
GLM baselines
A GLM (generalized linear model) is a linear model with a link function — here a log link, so predictions stay positive. Being linear, every feature has to be hand-engineered into a numeric design matrix: bin the skewed numerics, one-hot the categoricals, log-and-scale the heavy-tailed one. On that shared design matrix we fit the two classic recipes for the pure premium — a single-stage direct Tweedie model, then the two-stage frequency × severity product. The hyperparameters (Tweedie power, regularization strengths) are taken straight from the scikit-learn tutorial and left untuned; in practice you would grid-search them. That cuts both ways in this comparison — TabPFN is also run untuned, on its defaults.Single-stage GLM: direct Tweedie on pure premium
The simplest recipe: oneTweedieRegressor fit straight on PurePremium, weighted by Exposure. No
occurrence/severity split — a single model for the whole target. We set power=1.9 following the
scikit-learn tutorial.
Two-stage GLM: frequency × severity
The classic actuarial product: a Poisson frequency model (how often claims happen, weighted by exposure) times a Gamma severity model (average amount per claim, fit on claim rows only and weighted by claim count). Their product is the expected cost per unit exposure. Again, we follow the scikit-learn tutorial and set the hyperparameters accordingly.TabPFN
Now the same problem with TabPFN — but we feed the raw dataframe: strings, integers and floats together, no encoding, no scaling, no binning, no per-dataset tuning. GLMs fold exposure in throughsample_weight and an offset. TabPFN has neither, so we hand it the
information directly instead:
- Exposure becomes an ordinary input feature. TabPFN learns its effect from the data rather than
you specifying it as an offset. We model the bounded
ClaimAmountrather than the exploding per-exposurePurePremium(dividing a claim by a tiny exposure blows up the tail). - The pure premium is read off with a counterfactual query. We predict every test policy at a
full year of exposure (
Exposure = 1.0), so the output is already the expected cost per unit of exposure, no division needed.
Single-stage TabPFN: regress the claim amount directly
Start with the simplest thing that could work: hand a singleTabPFNRegressor the raw, 96%-zero
ClaimAmount and query at full exposure. One .fit(), one .predict(), default settings — nothing
else. .predict() returns the mean of TabPFN’s predicted distribution, the estimator of expected
cost.
1_plus_log (fit on log(1 + y), invert on output), the
natural choice for monetary amounts. This is the one modeling knob we touch.
Two-stage TabPFN: occurrence × severity
The actuarial decomposition, with no Poisson/Gamma machinery, just two TabPFN models:- Stage 1 — occurrence: a
TabPFNClassifierforP(claim > 0). - Stage 2 — severity: a
TabPFNRegressorforE[ClaimAmount | claim > 0], trained on claim rows only.
P(claim) × severity. Both stages are queried at Exposure = 1.0, so the product
is already the pure premium. We use each stage’s mean here; the next section pulls stage 2’s full
predictive distribution for a handful of example policies.
Per-policy uncertainty: the whole distribution, not just a number
A GLM’s severity model hands you one number per policy — the fitted mean. TabPFN’s severity model returns a full predictive distribution: for each policy we can read the expected claim and any quantile of how large it might be. In insurance that tail is not a nice-to-have — the 90th- or 99th-percentile claim is what drives risk loading, capital and reinsurance. We only need this for a few illustrative policies, so we ask stage 2 for the full output (output_type="full") on a small subset. (The hosted client caps full regression output at a few
hundred rows per call — plenty here, since we just want a spread of examples across the risk range.)
.predict;
the GLM’s Gamma gives you only the first.
Optional: matching the GLM on calibration
The two-stage TabPFN ranks well but under-prices the portfolio: summed over the test set it charges only ~60% of the actual claim cost. A log-link GLM never has this problem, fitting an intercept automatically balances its predicted total against the observed total on the training data. We can give TabPFN the same single degree of freedom: one constant factor = actual claim cost ÷ predicted cost, estimated on a calibration slice held out of training (never the test set). Because it is a constant,c · (p × s) orders policies exactly like p × s — so it fixes the level without
moving the ranking one bit.
One TabPFN-specific catch: TabPFN predicts by conditioning on its training rows in-context, so it
effectively memorizes them — a policy it saw at fit time gets an unrealistically good prediction.
Estimating the factor on such rows would under-correct, so we re-fit both stages on a subset
(df_fit) and score the held-out slice (df_cal) they never saw.
Results
All five models side by side, then the Lorenz curves for the ranking view (calibration was covered in the section above).
Takeaways
- Better ranking, zero feature engineering. From the raw 9-column dataframe — strings and all, one
.fit()/.predict()— TabPFN reaches Gini ~0.32 vs ~0.31 for the best GLM. The GLM needed a hand-built 75-column design matrix (binning, one-hot, log-scaling) and several modeling choices to get there. - Uncertainty comes built in. TabPFN returns a full predictive distribution per policy, so you read off the expected claim and its tail (the P90 “bad case”) directly — exactly what risk loading and capital planning need. The GLM gives a mean and stops.
- Calibration is a one-liner away. Out of the box TabPFN under-prices the portfolio (it has no exposure offset, a genuine convenience the GLMs keep). A single portfolio-level factor — the same balancing a log-link GLM performs implicitly — restores the level (balance ~0.6 → ~0.96) and brings the Tweedie deviance in line with the GLMs, while provably leaving the ranking untouched.
- The two-stage split helps TabPFN too. Occurrence × severity is better calibrated than a single regressor on the raw zero-inflated amount — the classic actuarial decomposition still pays off.
- Match the metric to the question. Judge ranking with the exposure-weighted Gini (the headline for pricing) and calibration with Tweedie deviance / D² and the decile chart. MAE and RMSE are actively misleading on a 96%-zero, heavy-tailed target.