- 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.
- A rich conditional cost distribution per policy. Given that a claim occurs, TabPFN returns the expected cost and any quantile, with a shape that adapts per policy, handy for understanding the severity tail.
- Calibration on par, with one optional adjustment. A single portfolio-level rescale, estimated on held-out training data, brings TabPFN’s Tweedie deviance close to the GLMs while leaving its stronger 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 × conditional aggregate cost
A hurdle-model analogue of the actuarial decomposition, with no Poisson/Gamma machinery, just two TabPFN models:- Stage 1 — occurrence: a
TabPFNClassifierforP(claim > 0). - Stage 2 — conditional cost: a
TabPFNRegressorforE[ClaimAmount | claim > 0], trained on positive-loss policy rows only.
P(claim) × conditional cost. 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 conditional cost distribution
Conditional on a positive claim amount, both models give a distribution per policy. The difference is shape: a standard Gamma uses one dispersion φ for the whole portfolio, so every policy gets the same coefficient of variation and skew and its quantiles are just the mean rescaled. TabPFN’s distribution can change shape from policy to policy, and reading it off costs nothing extra,output_type="full" returns the mean plus any
quantile.
We pull the full output for a spread of example policies.
P(claim) × conditional cost carries the cost of rare large claims, which a
median-only summary would miss.
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. The actuarial GLMs handle the portfolio level more naturally through their exposure-weighted likelihood and fitted intercept, although their balance can still depend on the model and regularization. We can give TabPFN one matching degree of freedom: a 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 adjusts the portfolio
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. - A rich conditional cost distribution per policy. Given a positive claim amount, TabPFN gives the conditional mean and quantiles with a shape that varies per policy; the Gamma benchmark has a constant-CV distribution.
- Calibration is a simple adjustment away. Out of the box TabPFN under-prices the portfolio. A single factor estimated on held-out training data improves the balance from ~0.6 to ~0.96 and brings Tweedie deviance close to the GLMs, while provably leaving the ranking untouched.
- The two-stage split helps TabPFN too. Occurrence × conditional aggregate cost is better calibrated than a single regressor on the raw zero-inflated amount — the actuarial intuition 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.