← All work Machine learning · Regression

Medical insurance cost prediction

What makes one policyholder cost four times more than another? I explored 1,338 policies, engineered risk features, compared four models and tuned the winner. It predicts annual charges within about $2,000 on average.

Question
Which traits drive medical insurance charges, and how well can we predict them?
Data
1,338 policyholders · 6 features · charges in USD
Method
Gradient boosting on log(charges), tuned with 5-fold GridSearchCV
Result
R² 0.874 · MAE $1,975 · RMSE $4,418 (in dollars)

1 · What the data says before any model

Each record is one primary beneficiary: age, sex, BMI, number of children covered, smoker status, US region and the annual charge. Charges run from $1,122 to $63,770, with a median of $9,382 and a mean of $13,270. A mean that far above the median means a long tail of very expensive policies.

Two scatter plots: age against charges and BMI against charges, coloured by smoker status
Smokers (red) sit far above non-smokers at nearly every age and BMI, and the gap widens for older and higher-BMI people. Smoking is the single strongest driver, and it compounds with age and BMI.
Annual chargesNon-smokerSmoker
Policies1,064274
Mean$8,434$32,050
Median$7,345$34,456
Std dev$5,994$11,542
Max$36,911$63,770

Smokers average about 4× the cost of non-smokers, and their costs vary twice as much.

Histogram of charges, right-skewed, next to a much more symmetric histogram of log charges
Raw charges are right-skewed. On a log scale they are close to normal, so I model log(charges) and convert predictions back to dollars.
Correlation heatmap; smoker has the strongest correlation with log charges
Smoker status correlates most with log charges (about +0.67), then age (about +0.53). BMI, sex and region are weaker.

2 · Features that encode health risk

The scatter plots show the smoking effect gets much larger for people with obesity. A linear model can't see that on its own, so I gave it three explicit features:

# BMI of 30 or more is the clinical threshold for obesity
df['is_obese'] = (df['bmi'] >= 30).astype(int)

# the combination is far costlier than either trait alone
df['smoker_obese_interaction'] = ((df['smoker'] == 'yes') & (df['is_obese'] == 1)).astype(int)

# life stages rather than a straight line in age
df['age_group'] = pd.cut(df['age'], bins=[18, 30, 45, 65, 100],
                         labels=['Young_Adult', 'Middle_Adult', 'Senior_Adult', 'Elderly'], right=False)

3 · Four models, one held-out test set

I one-hot encoded the categories, held out 20% of policies for testing, and trained four regressors on log(charges). Lower error is better; R² is the share of variation the model explains.

Model (log scale, test set)MAERMSER²
Linear regression0.2540.4010.821
Decision tree0.1880.4190.804
Random forest0.1970.3740.844
Gradient boosting0.1840.3470.866

Linear regression can't bend around the interactions. The tree ensembles can, and gradient boosting did best on every metric.

Tuning

A 5-fold grid search over 384 combinations of trees, learning rate, depth, split size and subsampling picked a slow, shallow model: 500 trees at a 0.01 learning rate, depth 3, with 80% subsampling. Shallow trees and subsampling keep it from memorising the training data. Train and test scores came out almost identical (R² 0.857 vs 0.866), so it generalises.

Actual against predicted log charges, clustered along the diagonal
Actual against predicted (log scale). Points hug the diagonal across the whole range.

4 · What it means in dollars

R² 0.874MAE $1,975RMSE $4,418

Back in dollars, the typical prediction is within about $2,000 of the real charge. RMSE is higher because it weights the rare very expensive policies more heavily, and those are the hardest to predict. The most important features were smoking, age, number of children and BMI.

What I'd tell a pricing team: smoking status is the lever. Combined with obesity it explains most of the extreme claims, so it belongs at the centre of any risk tier, while sex adds almost nothing.