remove(list=ls())
library(visdat)
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(tidyr)
library(stargazer)
Hlavac, Marek (2022). stargazer: Well-Formatted Regression and Summary Statistics Tables.
R package version 5.2.3. https://CRAN.R-project.org/package=stargazer
Loading required package: ggplot2
Loading required package: lattice
df <- read.csv("~/Downloads/insurance-training-data2.csv")
test_df <- read.csv("~/Downloads/insurance-testing-data2.csv")
vis_dat(df)
'data.frame': 6528 obs. of 26 variables:
$ INDEX : int 8023 4658 2005 4396 3706 3871 8670 833 5896 7432 ...
$ TARGET_FLAG: int 0 0 0 0 0 0 0 1 0 0 ...
$ TARGET_AMT : num 0 0 0 0 0 ...
$ KIDSDRIV : int 0 0 0 0 0 2 0 0 0 0 ...
$ AGE : int 40 48 51 56 51 45 65 56 53 49 ...
$ HOMEKIDS : int 0 0 0 0 0 2 0 0 1 0 ...
$ YOJ : int 9 0 14 10 9 11 15 13 10 12 ...
$ INCOME : chr "$78,658" "$0" "$89,606" "$199,336" ...
$ PARENT1 : chr "No" "No" "No" "No" ...
$ HOME_VAL : chr "$236,005" "$75,688" "$288,266" "$501,076" ...
$ MSTATUS : chr "z_No" "Yes" "z_No" "Yes" ...
$ SEX : chr "z_F" "z_F" "z_F" "z_F" ...
$ EDUCATION : chr "Masters" "Masters" "Bachelors" "PhD" ...
$ JOB : chr "Lawyer" "Home Maker" "Clerical" "" ...
$ TRAVTIME : int 69 39 32 23 46 14 39 48 51 26 ...
$ CAR_USE : chr "Private" "Private" "Private" "Commercial" ...
$ BLUEBOOK : chr "$17,040" "$1,500" "$10,460" "$41,280" ...
$ TIF : int 4 6 1 1 6 3 4 1 10 1 ...
$ CAR_TYPE : chr "Pickup" "Sports Car" "z_SUV" "Van" ...
$ RED_CAR : chr "no" "no" "no" "no" ...
$ OLDCLAIM : chr "$0" "$3,867" "$21,581" "$0" ...
$ CLM_FREQ : int 0 3 2 0 0 2 0 1 0 0 ...
$ REVOKED : chr "No" "No" "Yes" "No" ...
$ MVR_PTS : int 1 5 0 1 0 3 1 7 0 0 ...
$ CAR_AGE : int 15 NA 8 16 14 7 11 15 NA 10 ...
$ URBANICITY : chr "z_Highly Rural/ Rural" "Highly Urban/ Urban" "Highly Urban/ Urban" "Highly Urban/ Urban" ...
Data Cleaning
df_clean <- df %>%
# Remove ID and leakage variables
select(-INDEX, -TARGET_AMT) %>%
# Convert money columns to numeric
mutate(
INCOME = as.numeric(gsub("[$,]", "", INCOME)),
HOME_VAL = as.numeric(gsub("[$,]", "", HOME_VAL)),
BLUEBOOK = as.numeric(gsub("[$,]", "", BLUEBOOK)),
OLDCLAIM = as.numeric(gsub("[$,]", "", OLDCLAIM))
) %>%
# Replace blank strings with NA
mutate(
across(where(is.character), ~na_if(.x, ""))
) %>%
# Fill missing numeric values with median
mutate(
across(
where(is.numeric),
~ifelse(is.na(.x), median(.x, na.rm = TRUE), .x)
)
) %>%
# Fill missing categorical values with most common category
mutate(
across(
where(is.character),
~replace_na(.x, "Unknown")
)
) %>%
# Convert categorical variables to factors
mutate(
across(where(is.character), as.factor),
TARGET_FLAG = as.factor(TARGET_FLAG)
)
str(df_clean)
'data.frame': 6528 obs. of 24 variables:
$ TARGET_FLAG: Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 2 1 1 ...
$ KIDSDRIV : int 0 0 0 0 0 2 0 0 0 0 ...
$ AGE : int 40 48 51 56 51 45 65 56 53 49 ...
$ HOMEKIDS : int 0 0 0 0 0 2 0 0 1 0 ...
$ YOJ : int 9 0 14 10 9 11 15 13 10 12 ...
$ INCOME : num 78658 0 89606 199336 161325 ...
$ PARENT1 : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 2 1 1 1 1 ...
$ HOME_VAL : num 236005 75688 288266 501076 429578 ...
$ MSTATUS : Factor w/ 2 levels "Yes","z_No": 2 1 2 1 1 2 1 1 1 2 ...
$ SEX : Factor w/ 2 levels "M","z_F": 2 2 2 2 1 2 1 2 1 1 ...
$ EDUCATION : Factor w/ 5 levels "<High School",..: 3 3 2 4 4 2 4 2 5 2 ...
$ JOB : Factor w/ 9 levels "Clerical","Doctor",..: 4 3 1 8 5 6 2 1 9 6 ...
$ TRAVTIME : int 69 39 32 23 46 14 39 48 51 26 ...
$ CAR_USE : Factor w/ 2 levels "Commercial","Private": 2 2 2 1 1 2 2 1 1 1 ...
$ BLUEBOOK : num 17040 1500 10460 41280 24650 ...
$ TIF : int 4 6 1 1 6 3 4 1 10 1 ...
$ CAR_TYPE : Factor w/ 6 levels "Minivan","Panel Truck",..: 3 4 6 5 2 1 4 3 1 2 ...
$ RED_CAR : Factor w/ 2 levels "no","yes": 1 1 1 1 2 1 2 1 1 2 ...
$ OLDCLAIM : num 0 3867 21581 0 0 ...
$ CLM_FREQ : int 0 3 2 0 0 2 0 1 0 0 ...
$ REVOKED : Factor w/ 2 levels "No","Yes": 1 1 2 1 1 1 1 1 1 2 ...
$ MVR_PTS : int 1 5 0 1 0 3 1 7 0 0 ...
$ CAR_AGE : int 15 8 8 16 14 7 11 15 8 10 ...
$ URBANICITY : Factor w/ 2 levels "Highly Urban/ Urban",..: 2 1 1 1 1 1 1 1 2 1 ...
test_clean <- test_df %>%
# Remove ID and leakage variables
select(-INDEX, -TARGET_AMT) %>%
# Convert money columns to numeric
mutate(
INCOME = as.numeric(gsub("[$,]", "", INCOME)),
HOME_VAL = as.numeric(gsub("[$,]", "", HOME_VAL)),
BLUEBOOK = as.numeric(gsub("[$,]", "", BLUEBOOK)),
OLDCLAIM = as.numeric(gsub("[$,]", "", OLDCLAIM))
) %>%
# Replace blank strings with NA
mutate(
across(where(is.character), ~na_if(.x, ""))
) %>%
# Fill missing numeric values with median
mutate(
across(
where(is.numeric),
~ifelse(is.na(.x), median(.x, na.rm = TRUE), .x)
)
) %>%
# Fill missing categorical values with most common category
mutate(
across(
where(is.character),
~replace_na(.x, "Unknown")
)
) %>%
# Convert categorical variables to factors
mutate(
across(where(is.character), as.factor),
TARGET_FLAG = as.factor(TARGET_FLAG)
)
str(test_clean)
'data.frame': 1633 obs. of 24 variables:
$ TARGET_FLAG: Factor w/ 2 levels "0","1": 1 1 2 1 1 1 2 1 2 2 ...
$ KIDSDRIV : int 0 0 0 0 0 0 0 0 1 0 ...
$ AGE : num 51 54 43 52 38 47 40 56 45 33 ...
$ HOMEKIDS : int 0 0 0 0 0 0 0 0 1 4 ...
$ YOJ : num 14 11 13 8 11 8 11 16 14 12 ...
$ INCOME : num 56532 18755 37214 51278 37754 ...
$ PARENT1 : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 1 ...
$ HOME_VAL : num 306251 163671 163671 230340 138823 ...
$ MSTATUS : Factor w/ 2 levels "Yes","z_No": 1 1 1 1 1 1 2 1 1 1 ...
$ SEX : Factor w/ 2 levels "M","z_F": 1 2 1 2 2 2 1 1 2 2 ...
$ EDUCATION : Factor w/ 5 levels "<High School",..: 1 1 1 2 5 5 1 2 3 5 ...
$ JOB : Factor w/ 9 levels "Clerical","Doctor",..: 9 9 9 6 1 7 9 5 4 3 ...
$ TRAVTIME : int 32 33 52 37 47 35 20 30 50 46 ...
$ CAR_USE : Factor w/ 2 levels "Commercial","Private": 2 2 1 2 2 1 1 2 2 2 ...
$ BLUEBOOK : num 15440 8780 26560 1500 12080 ...
$ TIF : int 7 1 1 4 1 6 4 13 6 13 ...
$ CAR_TYPE : Factor w/ 6 levels "Minivan","Panel Truck",..: 1 6 2 6 6 6 3 1 5 6 ...
$ RED_CAR : Factor w/ 2 levels "no","yes": 2 1 2 1 1 1 2 2 1 1 ...
$ OLDCLAIM : num 0 0 0 0 0 ...
$ CLM_FREQ : int 0 0 0 0 0 2 1 0 2 3 ...
$ REVOKED : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 1 2 1 ...
$ MVR_PTS : int 0 0 3 1 2 5 13 0 0 0 ...
$ CAR_AGE : num 6 1 1 10 9 8 6 6 13 1 ...
$ URBANICITY : Factor w/ 2 levels "Highly Urban/ Urban",..: 1 1 1 1 2 1 1 1 1 1 ...
Regression
full_glm <- glm(
TARGET_FLAG ~ .,
data = df_clean,
family = binomial(link = "logit")
)
# Stepwise optimized logistic regression
best_glm <- step(
full_glm,
direction = "both",
trace = 0
)
# View final model
summary(best_glm)
Call:
glm(formula = TARGET_FLAG ~ KIDSDRIV + INCOME + PARENT1 + HOME_VAL +
MSTATUS + EDUCATION + JOB + TRAVTIME + CAR_USE + BLUEBOOK +
TIF + CAR_TYPE + OLDCLAIM + CLM_FREQ + REVOKED + MVR_PTS +
URBANICITY, family = binomial(link = "logit"), data = df_clean)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -6.683e-01 2.128e-01 -3.140 0.001687 **
KIDSDRIV 4.699e-01 6.218e-02 7.556 4.16e-14 ***
INCOME -3.257e-06 1.199e-06 -2.716 0.006607 **
PARENT1Yes 4.679e-01 1.060e-01 4.415 1.01e-05 ***
HOME_VAL -1.133e-06 3.808e-07 -2.976 0.002924 **
MSTATUSz_No 4.813e-01 8.926e-02 5.393 6.94e-08 ***
EDUCATIONBachelors -4.367e-01 1.213e-01 -3.602 0.000316 ***
EDUCATIONMasters -2.629e-01 1.793e-01 -1.466 0.142611
EDUCATIONPhD -1.356e-01 2.207e-01 -0.614 0.538901
EDUCATIONz_High School -7.829e-03 1.057e-01 -0.074 0.940938
JOBDoctor -1.218e+00 3.415e-01 -3.566 0.000362 ***
JOBHome Maker -1.124e-01 1.519e-01 -0.740 0.459155
JOBLawyer -3.403e-01 2.037e-01 -1.671 0.094792 .
JOBManager -9.333e-01 1.613e-01 -5.784 7.27e-09 ***
JOBProfessional -2.353e-01 1.381e-01 -1.704 0.088388 .
JOBStudent -8.795e-02 1.404e-01 -0.626 0.531126
JOBUnknown -3.922e-01 2.198e-01 -1.785 0.074295 .
JOBz_Blue Collar -3.041e-02 1.193e-01 -0.255 0.798795
TRAVTIME 1.334e-02 2.099e-03 6.358 2.05e-10 ***
CAR_USEPrivate -7.504e-01 1.032e-01 -7.275 3.48e-13 ***
BLUEBOOK -2.398e-05 5.317e-06 -4.510 6.48e-06 ***
TIF -5.662e-02 8.224e-03 -6.884 5.82e-12 ***
CAR_TYPEPanel Truck 5.389e-01 1.706e-01 3.158 0.001589 **
CAR_TYPEPickup 5.494e-01 1.117e-01 4.918 8.76e-07 ***
CAR_TYPESports Car 9.220e-01 1.206e-01 7.643 2.12e-14 ***
CAR_TYPEVan 6.032e-01 1.371e-01 4.399 1.09e-05 ***
CAR_TYPEz_SUV 7.289e-01 9.559e-02 7.625 2.44e-14 ***
OLDCLAIM -1.552e-05 4.283e-06 -3.625 0.000289 ***
CLM_FREQ 2.053e-01 3.176e-02 6.463 1.03e-10 ***
REVOKEDYes 9.446e-01 1.001e-01 9.439 < 2e-16 ***
MVR_PTS 1.169e-01 1.520e-02 7.688 1.49e-14 ***
URBANICITYz_Highly Rural/ Rural -2.351e+00 1.251e-01 -18.794 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 7533.1 on 6527 degrees of freedom
Residual deviance: 5828.7 on 6496 degrees of freedom
AIC: 5892.7
Number of Fisher Scoring iterations: 5
# Predict probabilities on test data
test_prob <- predict(
best_glm,
newdata = test_clean,
type = "response"
)
# Apply your optimized threshold
test_pred <- ifelse(test_prob > 0.23, 1, 0)
# Convert to factor
test_pred <- factor(
test_pred,
levels = c(0,1)
)
# View predictions
head(test_pred)
1 2 3 4 5 6
0 1 1 0 0 1
Levels: 0 1
confusionMatrix(
test_pred,
test_clean$TARGET_FLAG,
positive = "1"
)
Confusion Matrix and Statistics
Reference
Prediction 0 1
0 810 96
1 392 335
Accuracy : 0.7012
95% CI : (0.6783, 0.7233)
No Information Rate : 0.7361
P-Value [Acc > NIR] : 0.9993
Kappa : 0.3697
Mcnemar's Test P-Value : <2e-16
Sensitivity : 0.7773
Specificity : 0.6739
Pos Pred Value : 0.4608
Neg Pred Value : 0.8940
Prevalence : 0.2639
Detection Rate : 0.2051
Detection Prevalence : 0.4452
Balanced Accuracy : 0.7256
'Positive' Class : 1
\[F1 = \frac{2(335)}{2(335)+392+96}=0.5786\]