Loading...

Health Insurance Costs

In this example, we use a dataset of personal medical costs to create a model to estimate treatment costs.

The columns provided include:

  • age: age of the primary beneficiary.

  • sex: insurance contractor’s gender.

  • bmi: body mass index.

  • children: number of dependent children covered by health insurance.

  • smoker: smoker on non-smoker.

  • region: the beneficiary’s residential area in the US: northeast, southeast, southwest, northwest.

  • charges: individual medical costs billed by health insurance.

We will follow the data science cycle (Data Exploration - Data Preparation - Data Modeling - Model Evaluation - Model Deployment) to solve this problem.

Initialization

This example uses the following version of vastorbit:

import vastorbit as vo

vo.__version__

Connect to VAST. This example uses an existing connection called VASTDSN . For details on how to create a connection, see the Connection tutorial. You can skip the below cell if you already have an established connection.

vo.connect("VASTDSN")

Let’s assign the data to a VastFrame object.

data = vo.read_csv("insurance.csv")

Let’s take a look at the first few entries in the dataset.

data
123
age
Integer
100%
Abc
sex
Varchar(50)
100%
123
bmi
Double
100%
123
children
Integer
100%
Abc
smoker
Varchar(50)
100%
Abc
region
Varchar(50)
100%
123
charges
Double
100%
119female27.90yessouthwest16884.924
218male33.771nosoutheast1725.5523
328male33.03nosoutheast4449.462
433male22.7050nonorthwest21984.47061
532male28.880nonorthwest3866.8552
631female25.740nosoutheast3756.6216
746female33.441nosoutheast8240.5896
837female27.743nonorthwest7281.5056
937male29.832nonortheast6406.4107
1060female25.840nonorthwest28923.13692
1125male26.220nonortheast2721.3208
1262female26.290yessoutheast27808.7251
1323male34.40nosouthwest1826.843
1456female39.820nosoutheast11090.7178
1527male42.130yessoutheast39611.7577
1619male24.61nosouthwest1837.237
1752female30.781nonortheast10797.3362
1823male23.8450nonortheast2395.17155
1956male40.30nosouthwest10602.385
2030male35.30yessouthwest36837.467

Data Exploration

Let’s check our dataset for missing values. If we find any, we’ll have to impute them before we create any models.

data.count_percent()
countpercent
"age"1338.0100.0
"sex"1338.0100.0
"bmi"1338.0100.0
"children"1338.0100.0
"smoker"1338.0100.0
"region"1338.0100.0
"charges"1338.0100.0

There aren’t missing any values, so let’s get a summary of the features.

data.describe(method = "all")
"age""bmi""children""charges""sex""smoker""region"
dtypeintegerdoubleintegerdoublevarchar(50)varchar(50)varchar(50)
percent100100100100100100100
count1338133813381338133813381338
top1832.301639.5631malenosoutheast
top_percent5.1570.97242.90.14950.52379.52227.205
avg39.2070254110612930.663396860986521.094917787742913270.4222651412474.9895366218236182.2047832585949189.0
stddev14.049960379216156.0981869116790151.205492739781914212110.0112366941.00031913856875380.403694037545617170.0
min1815.9601121.8739429
approx_25%2726.46523029003267504686.733574174784429
approx_50%3930.19318273231622719564.609234800047529
approx_75%5134.67378366752696216773.739236061592629
max6453.13563770.42801639
range4637.17562648.554110000005210
empty[null][null][null][null]000

The dataset covers 1338 individuals up to age 64 from four different regions, each with up to six dependent children.

We might find some interesting patterns if we check age distribution, so let’s create a histogram.

data["age"].hist(method = "count", h = 1)

We have a pretty obvious trend here: the 18 and 19 year old age groups are significantly more frequent than any other, older age group. The other ages range from 20 to 30 people.

Before we do anything else, let’s discretize the age column using equal-width binning with a width of 5. Our goal is to see if there are any obvious patterns among the different age groups.

data["age"].discretize(method = "same_width", h = 5)
Abc
age
Varchar
100%
Abc
sex
Varchar(50)
100%
123
bmi
Double
100%
123
children
Integer
100%
Abc
smoker
Varchar(50)
100%
Abc
region
Varchar(50)
100%
123
charges
Double
100%
1[15;19]female27.90yessouthwest16884.924
2[15;19]male33.771nosoutheast1725.5523
3[25;29]male33.03nosoutheast4449.462
4[30;34]male22.7050nonorthwest21984.47061
5[30;34]male28.880nonorthwest3866.8552
6[30;34]female25.740nosoutheast3756.6216
7[45;49]female33.441nosoutheast8240.5896
8[35;39]female27.743nonorthwest7281.5056
9[35;39]male29.832nonortheast6406.4107
10[60;64]female25.840nonorthwest28923.13692
11[25;29]male26.220nonortheast2721.3208
12[60;64]female26.290yessoutheast27808.7251
13[20;24]male34.40nosouthwest1826.843
14[55;59]female39.820nosoutheast11090.7178
15[25;29]male42.130yessoutheast39611.7577
16[15;19]male24.61nosouthwest1837.237
17[50;54]female30.781nonortheast10797.3362
18[20;24]male23.8450nonortheast2395.17155
19[55;59]male40.30nosouthwest10602.385
20[30;34]male35.30yessouthwest36837.467

Age probably influences one’s body mass index (BMI), so let’s compare the average of body mass indexes of each age group and look for patterns there. We’ll use a bar graph this time.

data.bar(
    ["age"],
    method = "mean",
    of = "bmi",
)

There’s a pretty clear trend here, and we can say that, in general, older individuals tend to have a greater BMIs.

Let’s check the average number of smokers for each age-group. Before we do, we’ll convert the ‘yes’ and ‘no’ ‘smoker’ values to more convenient boolean values.

import vastorbit.sql.functions as fun

# Applying the decode function
data["smoker_int"] = fun.decode(data["smoker"], "yes", 1, 0)

Now we can plot the average number of smokers for each age group.

data.bar(
    ["age"],
    method = "mean",
    of = "smoker_int",
)

Unfortunately, there’s no obvious relationship between age and smoking habits - none that we can find from this graph, anyway.

Let’s see if we can relate an individual’s smoking habits with their sex.

data.bar(
    ["sex"],
    method = "mean",
    of = "smoker_int",
)

Now we’re getting somewhere! Looks like we have noticeably more male smokers than female ones.

Let’s see how an individual’s BMI relates to their sex.

data.bar(
    ["sex"],
    method = "mean",
    of = "bmi",
)

Males seem to have a slightly higher BMI, but it’d be hard to draw any conclusions from such a small difference.

Going back to our earlier patterns, let’s check the distribution of sexes among age groups and see if the patterns we identified earlier skews toward one of the sexes.

data.pivot_table(["age", "sex"])

It seems that sex is pretty evenly distributed in each age group.

Let’s move onto costs: how much do people tend to spend on medical treatments?

data["charges"].hist(method = "count")

Based on this graph, the majority of insurance holders tend to spend less than 1500 and only a handful of people spend more than 5000.

Encoding

Since our features vary in type, let’s start by encoding our categorical features. Remember, we label-encoded smoker from boolean. Let’s label-encode some other features: sex, region, and age groups.

# encoding sex
data["sex"].label_encode()

# encoding region
data["region"].label_encode()

# encoding age (already discretized into a handful of bins above)
data["age"].label_encode()
123
age
Integer
100%
123
sex
Integer
100%
123
bmi
Double
100%
123
children
Integer
100%
Abc
smoker
Varchar(50)
100%
123
region
Integer
100%
123
charges
Double
100%
123
smoker_int
Integer
100%
10027.90yes316884.9241
20133.771no21725.55230
32133.03no24449.4620
43122.7050no121984.470610
53128.880no13866.85520
63025.740no23756.62160
76033.441no28240.58960
84027.743no17281.50560
94129.832no06406.41070
109025.840no128923.136920
112126.220no02721.32080
129026.290yes227808.72511
131134.40no31826.8430
148039.820no211090.71780
152142.130yes239611.75771
160124.61no31837.2370
177030.781no010797.33620
181123.8450no02395.171550
198140.30no310602.3850
203135.30yes336837.4671

Before going further, let’s check the correlation of the variables with the predictor charges.

data.corr(focus = "charges")
data.to_db("insurance.final_ins_data", relation_type = "table")

Predicting insurance charges

Since our response variable is continuous, we can use regression to predict it. For this example, let’s use a Random Forest model.

from vastorbit.machine_learning.vast.ensemble import RandomForestRegressor

# define the random forest model
rf_model = RandomForestRegressor(
    n_estimators = 5,
    max_depth = 3,
)

# train the model
rf_model.fit(
    data,
    X = ["age", "sex", "bmi", "children", "smoker_int", "region"],
    y = "charges",
)

We can create a regression report to check our model’s performance.

rf_model.report()
value
explained_variance0.8585266043359364
max_error24659.68335406437
median_absolute_error1647.8091462821956
mean_absolute_error2652.973237504626
mean_squared_error20733456.258315813
root_mean_squared_error4553.400515912892
r20.8585160035229124
r2_adj0.8578782093990487
aic22555.790701502156
bic22592.02532545013

The results seem to be quite good! We have an explained variance around 0.8. Let’s plot the predicted values and compare them to the real ones.

# plot the predicted values and real ones
result = rf_model.predict(
    data,
    name = "pred_charges",
)

# add an index
result["id"] = "ROW_NUMBER() OVER()"

# plot them along the id
result.plot(
    ts = "id",
    columns = ['charges', 'pred_charges'],
)
data.to_db("insurance.final_ins_data", relation_type = "table")

Now, let’s examine the importance of each feature for this model. Ours is a random forest model, so we can use the built-in VAST function to calculate the importance of each predictor with Mean Decrease in Impurity (MDI).

# feature importance for our random forest model
rf_model.features_importance()
data.to_db("final_ins_data", relation_type = "table")
rf_model.features_importance(show = False)
importancesign
smoker_int0.71122122690658851.0
bmi0.176738006203029761.0
age0.108244686726926251.0
children0.0037960801634554591.0
sex0.00.0
region0.00.0

We can examine how our model works by visualizing one of the trees in our Random Forest.

# plot one of the trees comprising the forest
rf_model.plot_tree(tree_id = 3)
_images/examples_insurance_table_rf_tree.png

What affects medical costs?

We have a couple ways to approach this question. First, let’s see what features are linearly correlated with the cost.

It seems that smoking habits have a significant effect on medical costs. Next in line comes BMI, the number of dependents, and sex.

As one might expect, the correlation between charges and region is almost 0.

Now, let’s see what we can learn from a stepwise model with backward elimination using Bayesian information criterion (BIC) as a selection criteria.

from vastorbit.machine_learning.vast.linear_model import LinearRegression

model = LinearRegression()

# backward
from vastorbit.machine_learning.model_selection import stepwise

stepwise(
    model,
    input_relation = data,
    direction = "backward",
    X = ["age","sex", "bmi", "children", "smoker_int", "region"],
    y = "charges",
)
featuresbicchangevariableimportance
0['region', 'sex', 'children', 'bmi', 'age', 'smoker_int']28811.737130133086[null][null]0.0
1['sex', 'children', 'bmi', 'age', 'smoker_int']28827.22335983746+"region"100.0
2['region', 'children', 'bmi', 'age', 'smoker_int']28810.06683163958-"sex"0.0
3['region', 'bmi', 'age', 'smoker_int']28757.562416450593-"children"0.0
4['region', 'age', 'smoker_int']27654.092097818455-"bmi"0.0
5['region', 'smoker_int']26847.61591803939-"age"0.0
6['region']25173.12462253033-"smoker_int"0.0

From here we see that, again, the same features have similarly significant effects on medical costs.

Conclusion

In this example, we used several methods to identify the primary factors that affect one’s insurance costs.