A health insurance pricing engine, taken apart in your browser
This repository trains two gradient boosted models to price a year of health cover from eleven facts about a person. Here they run live: price anyone, watch 400 trees vote, see what every answer was worth, then scale one life to a nation of a billion.
It also measures the thing most projects skip. There is a hard limit on how accurate any model reading this data could ever be, and once you measure it, both models turn out to be sitting on it already.
Price a life
Move any control and 400 gradient boosted trees re-vote in this tab. Leave it alone and it prices a new applicant every few seconds. The two elements below read whichever applicant is showing, so one change moves all three.
Loading the boosters...
- Segment
- young, 25 and under
- Model
- XGBoost, 400 trees
- Recompute
- ·
Every figure here comes from walking the 400 real boosted trees exported from the shipped model, in this browser tab. No server round trip and no lookup table.
Inside the ensemble
A gradient boosted model is not one clever tree, it is four hundred shallow ones each correcting the last. Watch a real tree decide, and watch the running total build.
Why this price
The premium above, taken apart. Exact TreeSHAP gives every feature a contribution, and the contributions are additive: the base value plus all of them is the prediction, to the rupee.
The two model split
One boundary at age 25 cuts the book into two separately fitted models. This is where the interesting failure lives, and where the measurement that bounds it lives too.
The two models disagree about the same applicant by · at age 25. Neither is wrong: each was fit on a population the other never saw. It does mean a birthday moves this premium more than most underwriting facts would.
The shipped r2 of 0.5806 sits inside the interval for the ceiling, 0.5439 to 0.6114. On this evidence there is no headroom left to take.
- Repeated input combinations
- 1,017
- Pure error degrees of freedom
- 1,200
- Irreducible noise, one standard deviation
- $19
- Premium spread in this segment
- $29
- Dead columns inside this segment
- 4 of 30
The shipped r2 of 0.9976 sits inside the interval for the ceiling, 0.9974 to 0.9984. On this evidence there is no headroom left to take.
- Repeated input combinations
- 124
- Pure error degrees of freedom
- 126
- Irreducible noise, one standard deviation
- $3
- Premium spread in this segment
- $72
- Dead columns inside this segment
- 1 of 30
Each column is a group of young segment applicants whose 30 feature values are byte identical. The model sees one applicant. The data records several premiums. The height of a column is prediction error no model on these inputs can remove, and pooling that spread across every such group is where the ceiling comes from.
Pooled across every repeated group, one standard deviation of this irreducible spread is $19.
A nation of policyholders
The model prices one life. An actuary prices a book. Every distinct combination of inputs in the survey is weighted to a country's published margins by iterative proportional fitting, then priced once.
Is the model honest?
Predicted against actual on data the model never saw, residuals by decile, and whether the 90 percent intervals really contain the truth 90 percent of the time.
Split conformal takes a quantile of held out residuals and carries a distribution free coverage guarantee. Quantile regression fits the conditional 10th and 90th percentiles directly and assumes a model form. They share no machinery, so when their bands agree, the uncertainty is a property of the problem rather than of one method.
| Segment | Method | Nominal | Empirical | Band width |
|---|---|---|---|---|
| model_young | Split conformal, Mondrian | 90% | 90.70% | $57 |
| Quantile regression, 10th to 90th | 80% | 77.04% | $48 | |
| model_rest | Split conformal, Mondrian | 90% | 89.47% | $10 |
| Quantile regression, 10th to 90th | 80% | 78.87% | $10 |
What is wrong with the data, and what that costs
Every figure here is re-derived from the workbook by a gate that runs in continuous integration. Nothing on this page was typed in by hand.
Smoking_Status has seven distinct values where the underwriting question has three answers. The training pipeline rewrites one of them and one hot encodes the rest, so it emits 5 columns for a three state variable and three of them mean the same thing.
| Value in the workbook | Rows | What it is |
|---|---|---|
| No Smoking | 27,366 | canonical |
| Regular | 15,686 | canonical |
| Occasional | 6,915 | canonical |
| (blank) | 11 | missing |
| Smoking=0 | 8 | corrupted |
| Not Smoking | 8 | duplicate |
| Does Not Smoke | 6 | duplicate |
The duplicate spellings are harmless by accident: smoking_risk_val maps only the three canonical labels and sends everything else to zero, which happens to be the right answer for the two that mean non smoker. The 11 blank rows are not harmless. They are filled with the column mode, which is No Smoking, so an applicant with no recorded smoking status is silently underwritten as a non smoker. Blanks also appear in income_level (13 rows) and employment_status (2 rows).
None of these are handled by the training pipeline. The national simulation excludes the 130 rows with an impossible age or a negative dependant count, because a population model should not extrapolate from records that cannot describe a person. They are left in the single life calculator, so the behaviour stays visible.
The young model was the obvious thing to attack. Each attempt below is one change on top of the previous, so the effect of each is attributable.
| Attempt | What changed | Features | r2 | MAE |
|---|---|---|---|---|
| shipped | the artifact in the repository, on this split | 30 | 0.5806 | · |
| baseline | shipped features and hyperparameters, refit | 30 | 0.5802 | $16 |
| cleaned | three smoking labels, missing flagged, 4 dead columns dropped | 27 | 0.5792 | $16 |
| log target | trained on log premium, predictions exponentiated | 27 | 0.5750 | $16 |
| tuned | shallower, slower, more regularised for the tight young range | 27 | 0.5913 | $16 |
| no combined_risk | the 0.3 blend removed entirely | 26 | 0.5798 | $16 |
The best attempt reaches 0.5913, up 0.0107 on the shipped model. Cleaning the smoking labels moved it down 0.0010, which is the honest answer: the corrupted labels affect 22 rows out of 20,096 and the duplicates were already being handled correctly by accident. The log target made it worse.
All of this is happening inside the noise. The pure error ceiling for this segment is 0.5797, with a 95 percent interval of 0.5439 to 0.6114, and the best retrain lands inside it. There is no modelling gap here to close. An r2 of 0.58 on this segment is not a weak model, it is a hard question, and the right professional response is to widen the interval rather than to keep tuning.
The adult segment's noise ceiling is 0.9980. Applicants with byte identical inputs are charged almost byte identical premiums, which means the target is very nearly a deterministic function of the recorded inputs. Real insurance pricing is not like that. This data is synthetic, or generated from a formula.
That reframes the headline number rather than diminishing it. An r2 of 0.9976 on the adult segment means the model recovered the function that generated the data, which is a real and clean piece of fitting. It does not mean this model would score 0.9976 against real claims experience, and nothing on this page should be read as claiming it would. The young segment is the interesting one precisely because it is the one place where the generating process left genuine noise behind.