Healthcare premium predictor · segmented XGBoost

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.

400
real trees, walked in your tab
exported from the shipped model, no server and no lookup table
48,612
population cells, priced once
standing in for 1.02 billion Indian adults, raked to published margins
120,000
leaf traversals verified
every one lands on the same leaf here as it does in Python
01
Price a life
A real calculator. Move anything and 400 trees re-vote in under a millisecond.
02
Why this price
Exact TreeSHAP, built up contribution by contribution, summing to the premium exactly.
03
The two model split
What one birthday costs, and the noise ceiling that bounds both models.
04
A nation of policyholders
48,612 weighted cells raked to published margins, pricing the United States and India.
05
Is the model honest?
Calibration, residuals, and whether the 90% intervals really contain the truth 90% of the time.
06
What is wrong with the data
Seven spellings of a three valued column, 58 applicants older than 100, and one broken intercept.
The argument, in four movements
50,000 policies
Premiums are modelled in rupees and shown in
at 96.215 INR per USD, Federal Reserve H.10 foreign exchange rates, 2026-07-27
One life, priced
Student, no cover history
The cheapest life the model prices
Annual premium
Age19
Segment modelmodel_rest
SmokingNo Smoking
Medical historyNo Disease
PlanBronze
Income₹3L
800 trees, both ensembles scoring the same applicant0/400 each
each cell is one tree's correction, down to up
Element one

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.

Now pricing
Student, no cover history
Age
25 boundary
Annual income
Dependants
Insurance plan
Smoking status
BMI band
Employment
Region
Gender
Marital status
Medical history
Annual premium

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.

Element two

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.

Loading the ensemble...
The whole pricing surface
Every cell is a real prediction for the applicant above, moved to that age and that income. The dashed line is age 25, where one model hands over to the other.
loading
Element three

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.

Loading the explanation engine...
Element four

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.

Both models, across every age
One applicant, aged from 18 to 100, scored by each model at every age. The solid part of each line is where that model is actually used.
Loading...

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.

model_young20,096 rows
shipped
0.5806
0.5797
ceiling
0.9749
naive

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
model_rest29,904 rows
shipped
0.9976
0.9980
ceiling
1.0000
naive

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
Identical applicants, different premiums

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.

4k8k13k44 groups with the widest spread, of 60 repeated combinations

Pooled across every repeated group, one standard deviation of this irreducible spread is $19.

Element five

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.

Pricing 48,612 population cells with 400 trees each. This happens once.
Element six

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.

model_young predicted against actual, held out
predictedactualhollow points are decile means; the dashes are perfect calibration
r2
0.5806
MAE
$16
mean residual
$-0
model_rest predicted against actual, held out
predictedactualhollow points are decile means; the dashes are perfect calibration
r2
0.9976
MAE
$3
mean residual
$-0
Two independent routes to the same uncertainty

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.

SegmentMethodNominalEmpiricalBand width
model_youngSplit conformal, Mondrian90%90.70%$57
Quantile regression, 10th to 90th80%77.04%$48
model_restSplit conformal, Mondrian90%89.47%$10
Quantile regression, 10th to 90th80%78.87%$10
Method

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.

Seven spellings of a three valued column

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 workbookRowsWhat it is
No Smoking27,366canonical
Regular15,686canonical
Occasional6,915canonical
(blank)11missing
Smoking=08corrupted
Not Smoking8duplicate
Does Not Smoke6duplicate

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).

Three values that cannot be true
58
applicants older than 100
the oldest is 356. Ages outside the 0 to 100 bins fall out of every age band, so all four age band columns encode as zero, a state no legitimate applicant produces.
72
negative dependant counts
the lowest is -3. The value passes into the model unchanged and is treated as a real quantity.
10
incomes above 200 lakhs
the largest is 930 lakhs against a median of 17. The engineered income_plan feature multiplies it by the plan tier, so the outlier is amplified rather than damped.

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 retrain, and why it barely moved

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.

AttemptWhat changedFeaturesr2MAE
shippedthe artifact in the repository, on this split300.5806·
baselineshipped features and hyperparameters, refit300.5802$16
cleanedthree smoking labels, missing flagged, 4 dead columns dropped270.5792$16
log targettrained on log premium, predictions exponentiated270.5750$16
tunedshallower, slower, more regularised for the tight young range270.5913$16
no combined_riskthe 0.3 blend removed entirely260.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.

What this dataset actually is

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.