Chapter 6: Statistical Machine Learning

library(future)
library(patchwork)
library(ranger)
library(rpart.plot)
library(tidymodels)
library(tidyverse)
library(xgboost)

set.seed(123)

# Location of the data files. Adjust this path if you keep the data
# files in a different directory.
DATA_DIR <- '../data'

Statistical Machine Learning

K-Nearest Neighbors

A Small Example: Predicting Loan Default

loan200 <- read_csv(file.path(DATA_DIR, "loan200.csv"), show_col_types = FALSE) %>%
  mutate(outcome = factor(outcome, levels = c("paid off", "default")))

train_data <- loan200 %>% dplyr::slice(-1)
newloan <- loan200 %>% dplyr::slice(1)
model <- nearest_neighbor(mode = "classification", neighbors = 20,
  weight_func = "rectangular")
knn_model <- workflow() %>%
  add_model(model) %>%
  add_formula(outcome ~ payment_inc_ratio + dti) %>%
  fit(train_data)

knn_pred <- knn_model %>% predict(newloan)
knn_pred
# A tibble: 1 × 1
  .pred_class
  <fct>      
1 paid off   

Standardization (Normalization, z-Scores)

newloan
# A tibble: 1 × 3
  outcome payment_inc_ratio   dti
  <fct>               <dbl> <dbl>
1 <NA>                    9  22.5
library(FNN)
loan_data <- read_csv(file.path(DATA_DIR, "loan_data.csv.gz"), show_col_types = FALSE) %>%
  select(-c(index, status)) %>%
  mutate(
    across(where(is.character), as.factor),
    outcome = factor(outcome, levels = c("paid off", "default"))
  )

loan_df <- model.matrix(~ -1 + payment_inc_ratio + dti + revol_bal +
    revol_util, data = loan_data)
newloan <- loan_df[1, , drop = FALSE]
loan_df <- loan_df[-1, ]
outcome <- loan_data[-1, ]$outcome
knn_pred <- knn(train = loan_df, test = newloan, cl = outcome, k = 5)
loan_df[attr(knn_pred, "nn.index"), ]
      payment_inc_ratio  dti revol_bal revol_util
35537           1.47212 1.46      1686       10.0
33652           3.38178 6.37      1688        8.4
25864           2.36303 1.39      1691        3.5
42954           1.28160 7.14      1684        3.9
43600           4.12244 8.98      1684        7.2
loan_df <- model.matrix(~ -1 + payment_inc_ratio + dti + revol_bal +
    revol_util, data = loan_data)
loan_std <- scale(loan_df)
newloan_std <- loan_std[1, , drop = FALSE]
loan_std <- loan_std[-1, ]
loan_df <- loan_df[-1, ]
outcome <- loan_data[-1, ]$outcome
knn_pred <- knn(train = loan_std, test = newloan_std, cl = outcome, k = 5)
loan_df[attr(knn_pred, "nn.index"), ]
      payment_inc_ratio  dti revol_bal revol_util
2081            2.61091 1.03      1218        9.7
1439            2.34343 0.51       278        9.9
30216           2.71200 1.34      1075        8.5
28543           2.39760 0.74      2917        7.4
44738           2.34309 1.37       488        7.2

KNN as a Feature Engine

rec <- recipe(outcome ~ dti + revol_bal + revol_util + open_acc +
    delinq_2yrs_zero + pub_rec_zero, data = loan_data)
model <- nearest_neighbor(mode = "classification", neighbors = 20,
  weight_func = "rectangular")
borrow_knn <- workflow() %>%
  add_model(nearest_neighbor(mode = "classification", neighbors = 20)) %>%
  add_recipe(rec) %>%
  fit(loan_data)
borrow_feature <- borrow_knn %>%
  predict(loan_data, type = "prob") %>%
  pluck(".pred_default")
summary(borrow_feature)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.0000  0.3819  0.5017  0.5001  0.6200  1.0000 

Tree Models

A Simple Example

loan3000 <- read_csv(file.path(DATA_DIR, "loan3000.csv"), show_col_types = FALSE) %>%
  mutate(outcome = factor(outcome, levels = c("paid off", "default")))

loan_tree <- decision_tree(mode = "classification", engine = "rpart") %>%
  fit(outcome ~ borrower_score + payment_inc_ratio, data = loan3000)

rpart.plot(loan_tree %>% extract_fit_engine(), roundint = FALSE)

loan_tree
parsnip model object

n= 3000 

node), split, n, loss, yval, (yprob)
      * denotes terminal node

 1) root 3000 1445 paid off (0.5183333 0.4816667)  
   2) borrower_score>=0.575 878  261 paid off (0.7027335 0.2972665) *
   3) borrower_score< 0.575 2122  938 default (0.4420358 0.5579642)  
     6) borrower_score>=0.375 1639  802 default (0.4893228 0.5106772)  
      12) payment_inc_ratio< 10.42265 1157  547 paid off (0.5272256 0.4727744)  
        24) payment_inc_ratio< 4.42601 334  139 paid off (0.5838323 0.4161677) *
        25) payment_inc_ratio>=4.42601 823  408 paid off (0.5042527 0.4957473)  
          50) borrower_score>=0.475 418  190 paid off (0.5454545 0.4545455) *
          51) borrower_score< 0.475 405  187 default (0.4617284 0.5382716) *
      13) payment_inc_ratio>=10.42265 482  192 default (0.3983402 0.6016598) *
     7) borrower_score< 0.375 483  136 default (0.2815735 0.7184265) *

Bagging and the Random Forest

Random Forest

rf <- rand_forest(mode = "classification") %>%
  set_engine("ranger", seed = 123) %>%
  fit(outcome ~ borrower_score + payment_inc_ratio, data = loan3000)
rf
parsnip model object

Ranger result

Call:
 ranger::ranger(x = maybe_data_frame(x), y = y, seed = ~123, num.threads = 1,      verbose = FALSE, probability = TRUE) 

Type:                             Probability estimation 
Number of trees:                  500 
Sample size:                      3000 
Number of independent variables:  2 
Mtry:                             1 
Target node size:                 10 
Variable importance mode:         none 
Splitrule:                        gini 
OOB prediction error (Brier s.):  0.2300053 
num_trees <- c(1, seq(5, 500, 5))
oob_scores <- c()
for (trees in num_trees) {
  rf <- rand_forest(mode = "classification", trees = trees) %>%
    set_engine("ranger", seed = 123) %>%
    fit(outcome ~ borrower_score + payment_inc_ratio, data = loan3000)
  oob_scores <- c(oob_scores, (rf %>% extract_fit_engine())$prediction.error)
}
error_df <- tibble(error_rate = oob_scores, num_trees = num_trees)
ggplot(error_df, aes(x = num_trees, y = error_rate)) +
  geom_line()

rf_df <- augment(rf, loan3000)
ggplot(data = rf_df, aes(x = borrower_score, y = payment_inc_ratio,
    shape = .pred_class, color = .pred_class, size = .pred_class)) +
  geom_jitter(alpha = 0.8, height = 0, width = 0.003) +
  scale_color_manual(values = c("paid off" = "#b8e186", "default" = "#d95f02")) +
  scale_shape_manual(values = c("paid off" = 0, "default" = 1)) +
  scale_size_manual(values = c("paid off" = 0.5, "default" = 2))

Variable Importance

rf_all <- rand_forest(mode = "classification") %>%
  set_engine("ranger", seed = 123, importance = "impurity") %>%
  fit(outcome ~ ., data = loan_data)
rf_all
parsnip model object

Ranger result

Call:
 ranger::ranger(x = maybe_data_frame(x), y = y, seed = ~123, importance = ~"impurity",      num.threads = 1, verbose = FALSE, probability = TRUE) 

Type:                             Probability estimation 
Number of trees:                  500 
Sample size:                      45342 
Number of independent variables:  18 
Mtry:                             4 
Target node size:                 10 
Variable importance mode:         impurity 
Splitrule:                        gini 
OOB prediction error (Brier s.):  0.2123043 
rf_permutation <- rand_forest(mode = "classification") %>%
  set_engine("ranger", seed = 123, importance = "permutation") %>%
  fit(outcome ~ ., data = loan_data)
varimp_gini <- importance(rf_all %>% extract_fit_engine())
varimp_perm <- importance(rf_permutation %>% extract_fit_engine())
df <- tibble(
  predictor = names(varimp_gini),
  impurity = varimp_gini,
  permutation = varimp_perm
)
g1 <- ggplot(df, aes(x = permutation, y = reorder(predictor, permutation))) +
  geom_point() +
  labs(y = "Importance", title = "Accuracy Decrease")
g2 <- ggplot(df, aes(x = impurity, y = reorder(predictor, permutation))) +
  geom_point() +
  labs(y = "Importance", title = "Gini Decrease")
(g1 + theme_bw()) / (g2 + theme_bw())

Boosting

XGBoost

xgb <- boost_tree(mode = "classification",
  trees = 100, learn_rate = 0.1, sample_size = 0.63) %>%
  fit(outcome ~ borrower_score + payment_inc_ratio, data = loan3000)
xgb_df <- augment(xgb, loan3000)
ggplot(data = xgb_df, aes(x = borrower_score, y = payment_inc_ratio,
    color = .pred_class, shape = .pred_class, size = .pred_class)) +
  geom_jitter(alpha = 0.8, height = 0, width = 0.003) +
  scale_color_manual(values = c("paid off" = "#b8e186", "default" = "#d95f02")) +
  scale_shape_manual(values = c("paid off" = 0, "default" = 1)) +
  scale_size_manual(values = c("paid off" = 0.5, "default" = 2))

Regularization: Avoiding Overfitting

set.seed(400820)
loan_split <- initial_split(loan_data, prop = 0.75)
train_data <- training(loan_split)
test_data <- testing(loan_split)
train_x <- data.matrix(train_data %>% select(-outcome))
train_y <- factor(train_data$outcome, levels = c("paid off", "default"))
test_x <- data.matrix(test_data %>% select(-outcome))
test_y <- factor(test_data$outcome, levels = c("paid off", "default"))

xgb_default <- xgboost(x = train_x, y = train_y,
  objective = "binary:logistic", nrounds = 250, verbosity = 1,
  eval_metric = "error")
[1] train-error:0.336646 
[2] train-error:0.329677 
[3] train-error:0.326031 
[4] train-error:0.321355 
[5] train-error:0.320032 
[6] train-error:0.317356 
[7] train-error:0.315915 
[8] train-error:0.313739 
[9] train-error:0.312445 
[10]    train-error:0.311386 
[11]    train-error:0.308740 
[12]    train-error:0.306769 
[13]    train-error:0.303946 
[14]    train-error:0.302711 
[15]    train-error:0.299712 
[16]    train-error:0.298653 
[17]    train-error:0.297065 
[18]    train-error:0.296859 
[19]    train-error:0.294183 
[20]    train-error:0.293419 
[21]    train-error:0.292801 
[22]    train-error:0.290508 
[23]    train-error:0.290213 
[24]    train-error:0.288390 
[25]    train-error:0.288008 
[26]    train-error:0.287067 
[27]    train-error:0.284656 
[28]    train-error:0.283450 
[29]    train-error:0.281597 
[30]    train-error:0.280421 
[31]    train-error:0.279627 
[32]    train-error:0.278304 
[33]    train-error:0.275745 
[34]    train-error:0.274540 
[35]    train-error:0.274246 
[36]    train-error:0.273716 
[37]    train-error:0.273099 
[38]    train-error:0.271599 
[39]    train-error:0.268953 
[40]    train-error:0.267659 
[41]    train-error:0.265659 
[42]    train-error:0.264336 
[43]    train-error:0.262689 
[44]    train-error:0.260983 
[45]    train-error:0.259013 
[46]    train-error:0.257925 
[47]    train-error:0.255955 
[48]    train-error:0.255014 
[49]    train-error:0.254573 
[50]    train-error:0.253338 
[51]    train-error:0.252161 
[52]    train-error:0.251220 
[53]    train-error:0.249574 
[54]    train-error:0.249044 
[55]    train-error:0.248133 
[56]    train-error:0.246604 
[57]    train-error:0.246162 
[58]    train-error:0.245398 
[59]    train-error:0.245339 
[60]    train-error:0.243516 
[61]    train-error:0.242751 
[62]    train-error:0.241252 
[63]    train-error:0.240752 
[64]    train-error:0.239752 
[65]    train-error:0.238193 
[66]    train-error:0.237282 
[67]    train-error:0.236223 
[68]    train-error:0.234812 
[69]    train-error:0.233518 
[70]    train-error:0.232606 
[71]    train-error:0.232077 
[72]    train-error:0.230753 
[73]    train-error:0.230048 
[74]    train-error:0.228607 
[75]    train-error:0.227989 
[76]    train-error:0.227372 
[77]    train-error:0.225695 
[78]    train-error:0.224813 
[79]    train-error:0.224519 
[80]    train-error:0.223519 
[81]    train-error:0.221461 
[82]    train-error:0.220814 
[83]    train-error:0.220373 
[84]    train-error:0.218608 
[85]    train-error:0.218138 
[86]    train-error:0.217932 
[87]    train-error:0.217315 
[88]    train-error:0.216991 
[89]    train-error:0.216050 
[90]    train-error:0.215344 
[91]    train-error:0.214462 
[92]    train-error:0.213286 
[93]    train-error:0.211580 
[94]    train-error:0.211492 
[95]    train-error:0.210610 
[96]    train-error:0.210198 
[97]    train-error:0.209463 
[98]    train-error:0.209198 
[99]    train-error:0.208199 
[100]   train-error:0.207434 
[101]   train-error:0.206581 
[102]   train-error:0.205346 
[103]   train-error:0.204905 
[104]   train-error:0.204317 
[105]   train-error:0.203052 
[106]   train-error:0.202053 
[107]   train-error:0.200729 
[108]   train-error:0.199406 
[109]   train-error:0.198553 
[110]   train-error:0.198524 
[111]   train-error:0.198024 
[112]   train-error:0.197642 
[113]   train-error:0.196877 
[114]   train-error:0.195583 
[115]   train-error:0.194848 
[116]   train-error:0.194083 
[117]   train-error:0.192260 
[118]   train-error:0.191025 
[119]   train-error:0.190584 
[120]   train-error:0.190114 
[121]   train-error:0.188761 
[122]   train-error:0.188555 
[123]   train-error:0.188408 
[124]   train-error:0.187673 
[125]   train-error:0.186320 
[126]   train-error:0.185732 
[127]   train-error:0.185644 
[128]   train-error:0.185203 
[129]   train-error:0.183821 
[130]   train-error:0.183291 
[131]   train-error:0.182585 
[132]   train-error:0.181644 
[133]   train-error:0.180645 
[134]   train-error:0.179792 
[135]   train-error:0.179086 
[136]   train-error:0.178027 
[137]   train-error:0.177116 
[138]   train-error:0.177116 
[139]   train-error:0.176263 
[140]   train-error:0.176087 
[141]   train-error:0.175234 
[142]   train-error:0.174999 
[143]   train-error:0.174263 
[144]   train-error:0.173205 
[145]   train-error:0.172470 
[146]   train-error:0.171529 
[147]   train-error:0.170352 
[148]   train-error:0.169794 
[149]   train-error:0.169235 
[150]   train-error:0.168676 
[151]   train-error:0.168235 
[152]   train-error:0.167529 
[153]   train-error:0.166471 
[154]   train-error:0.165853 
[155]   train-error:0.165030 
[156]   train-error:0.164442 
[157]   train-error:0.163559 
[158]   train-error:0.162883 
[159]   train-error:0.162677 
[160]   train-error:0.162148 
[161]   train-error:0.161883 
[162]   train-error:0.161560 
[163]   train-error:0.159913 
[164]   train-error:0.158707 
[165]   train-error:0.157560 
[166]   train-error:0.157031 
[167]   train-error:0.157119 
[168]   train-error:0.156325 
[169]   train-error:0.155737 
[170]   train-error:0.154914 
[171]   train-error:0.154296 
[172]   train-error:0.153649 
[173]   train-error:0.153061 
[174]   train-error:0.152120 
[175]   train-error:0.151414 
[176]   train-error:0.150944 
[177]   train-error:0.150179 
[178]   train-error:0.149856 
[179]   train-error:0.149503 
[180]   train-error:0.149180 
[181]   train-error:0.147327 
[182]   train-error:0.146592 
[183]   train-error:0.145974 
[184]   train-error:0.145004 
[185]   train-error:0.144416 
[186]   train-error:0.144210 
[187]   train-error:0.143622 
[188]   train-error:0.143151 
[189]   train-error:0.143239 
[190]   train-error:0.142122 
[191]   train-error:0.141269 
[192]   train-error:0.140710 
[193]   train-error:0.139740 
[194]   train-error:0.139828 
[195]   train-error:0.139093 
[196]   train-error:0.138299 
[197]   train-error:0.137093 
[198]   train-error:0.137182 
[199]   train-error:0.137123 
[200]   train-error:0.137005 
[201]   train-error:0.136799 
[202]   train-error:0.135947 
[203]   train-error:0.135270 
[204]   train-error:0.133947 
[205]   train-error:0.133241 
[206]   train-error:0.132859 
[207]   train-error:0.131477 
[208]   train-error:0.130095 
[209]   train-error:0.129948 
[210]   train-error:0.129507 
[211]   train-error:0.129007 
[212]   train-error:0.128713 
[213]   train-error:0.128213 
[214]   train-error:0.127713 
[215]   train-error:0.127036 
[216]   train-error:0.126448 
[217]   train-error:0.126272 
[218]   train-error:0.125948 
[219]   train-error:0.125360 
[220]   train-error:0.125096 
[221]   train-error:0.124302 
[222]   train-error:0.123596 
[223]   train-error:0.123449 
[224]   train-error:0.122919 
[225]   train-error:0.122008 
[226]   train-error:0.121420 
[227]   train-error:0.120979 
[228]   train-error:0.120214 
[229]   train-error:0.119744 
[230]   train-error:0.118773 
[231]   train-error:0.118097 
[232]   train-error:0.117215 
[233]   train-error:0.117038 
[234]   train-error:0.116215 
[235]   train-error:0.115715 
[236]   train-error:0.115244 
[237]   train-error:0.114597 
[238]   train-error:0.114215 
[239]   train-error:0.113451 
[240]   train-error:0.112598 
[241]   train-error:0.112333 
[242]   train-error:0.112627 
[243]   train-error:0.112392 
[244]   train-error:0.112098 
[245]   train-error:0.111304 
[246]   train-error:0.110657 
[247]   train-error:0.110363 
[248]   train-error:0.110039 
[249]   train-error:0.109392 
[250]   train-error:0.108775 
pred_default <- predict(xgb_default, test_x)
error_default <- abs((as.numeric(test_y) - 1) - pred_default) > 0.5
attributes(xgb_default)$evaluation_log[250, ]
    iter train_error
   <num>       <num>
1:   250   0.1087749
mean(error_default)
[1] 0.3591214
xgb_penalty <- xgboost(x = train_x, y = train_y,
  learning_rate = 0.1, subsample = 0.63, reg_lambda = 1000,
  objective = "binary:logistic", nrounds = 250, verbosity = 1,
  eval_metric = "error")
[1] train-error:0.363730 
[2] train-error:0.344116 
[3] train-error:0.342969 
[4] train-error:0.340116 
[5] train-error:0.338646 
[6] train-error:0.334882 
[7] train-error:0.336411 
[8] train-error:0.336588 
[9] train-error:0.336911 
[10]    train-error:0.336999 
[11]    train-error:0.335588 
[12]    train-error:0.337176 
[13]    train-error:0.335794 
[14]    train-error:0.334529 
[15]    train-error:0.335676 
[16]    train-error:0.334147 
[17]    train-error:0.334323 
[18]    train-error:0.333970 
[19]    train-error:0.334323 
[20]    train-error:0.334706 
[21]    train-error:0.334118 
[22]    train-error:0.333765 
[23]    train-error:0.333118 
[24]    train-error:0.332765 
[25]    train-error:0.331294 
[26]    train-error:0.331294 
[27]    train-error:0.331353 
[28]    train-error:0.331324 
[29]    train-error:0.331236 
[30]    train-error:0.330559 
[31]    train-error:0.330530 
[32]    train-error:0.329854 
[33]    train-error:0.328971 
[34]    train-error:0.329060 
[35]    train-error:0.328530 
[36]    train-error:0.329089 
[37]    train-error:0.328471 
[38]    train-error:0.328413 
[39]    train-error:0.328295 
[40]    train-error:0.328354 
[41]    train-error:0.328030 
[42]    train-error:0.327677 
[43]    train-error:0.327530 
[44]    train-error:0.326913 
[45]    train-error:0.327178 
[46]    train-error:0.327295 
[47]    train-error:0.327295 
[48]    train-error:0.326678 
[49]    train-error:0.326413 
[50]    train-error:0.326119 
[51]    train-error:0.326678 
[52]    train-error:0.326913 
[53]    train-error:0.326442 
[54]    train-error:0.326119 
[55]    train-error:0.325795 
[56]    train-error:0.326060 
[57]    train-error:0.325884 
[58]    train-error:0.325472 
[59]    train-error:0.325296 
[60]    train-error:0.325119 
[61]    train-error:0.324943 
[62]    train-error:0.324796 
[63]    train-error:0.324355 
[64]    train-error:0.324619 
[65]    train-error:0.324472 
[66]    train-error:0.324119 
[67]    train-error:0.324443 
[68]    train-error:0.324266 
[69]    train-error:0.324119 
[70]    train-error:0.323708 
[71]    train-error:0.323913 
[72]    train-error:0.323472 
[73]    train-error:0.323855 
[74]    train-error:0.323825 
[75]    train-error:0.323855 
[76]    train-error:0.324237 
[77]    train-error:0.324031 
[78]    train-error:0.323796 
[79]    train-error:0.323590 
[80]    train-error:0.323590 
[81]    train-error:0.323561 
[82]    train-error:0.323296 
[83]    train-error:0.322855 
[84]    train-error:0.323002 
[85]    train-error:0.322825 
[86]    train-error:0.322649 
[87]    train-error:0.322473 
[88]    train-error:0.322267 
[89]    train-error:0.322561 
[90]    train-error:0.322502 
[91]    train-error:0.322296 
[92]    train-error:0.321884 
[93]    train-error:0.321826 
[94]    train-error:0.321737 
[95]    train-error:0.321826 
[96]    train-error:0.321855 
[97]    train-error:0.321679 
[98]    train-error:0.321767 
[99]    train-error:0.321414 
[100]   train-error:0.321502 
[101]   train-error:0.321561 
[102]   train-error:0.321237 
[103]   train-error:0.321326 
[104]   train-error:0.321090 
[105]   train-error:0.321032 
[106]   train-error:0.320532 
[107]   train-error:0.320826 
[108]   train-error:0.321002 
[109]   train-error:0.321179 
[110]   train-error:0.320796 
[111]   train-error:0.320826 
[112]   train-error:0.320943 
[113]   train-error:0.320796 
[114]   train-error:0.320590 
[115]   train-error:0.320738 
[116]   train-error:0.320826 
[117]   train-error:0.320355 
[118]   train-error:0.320414 
[119]   train-error:0.320649 
[120]   train-error:0.320267 
[121]   train-error:0.320385 
[122]   train-error:0.320502 
[123]   train-error:0.320473 
[124]   train-error:0.320208 
[125]   train-error:0.320355 
[126]   train-error:0.320149 
[127]   train-error:0.319944 
[128]   train-error:0.319944 
[129]   train-error:0.319973 
[130]   train-error:0.319738 
[131]   train-error:0.319855 
[132]   train-error:0.319738 
[133]   train-error:0.319649 
[134]   train-error:0.319708 
[135]   train-error:0.319708 
[136]   train-error:0.319473 
[137]   train-error:0.319208 
[138]   train-error:0.319179 
[139]   train-error:0.319120 
[140]   train-error:0.319032 
[141]   train-error:0.318797 
[142]   train-error:0.318708 
[143]   train-error:0.318738 
[144]   train-error:0.319032 
[145]   train-error:0.318620 
[146]   train-error:0.318503 
[147]   train-error:0.318238 
[148]   train-error:0.318091 
[149]   train-error:0.317973 
[150]   train-error:0.318062 
[151]   train-error:0.318297 
[152]   train-error:0.318150 
[153]   train-error:0.318297 
[154]   train-error:0.317856 
[155]   train-error:0.318091 
[156]   train-error:0.317709 
[157]   train-error:0.317620 
[158]   train-error:0.317591 
[159]   train-error:0.317738 
[160]   train-error:0.317503 
[161]   train-error:0.317562 
[162]   train-error:0.317268 
[163]   train-error:0.317297 
[164]   train-error:0.317003 
[165]   train-error:0.317003 
[166]   train-error:0.317121 
[167]   train-error:0.316885 
[168]   train-error:0.316621 
[169]   train-error:0.316621 
[170]   train-error:0.316709 
[171]   train-error:0.316474 
[172]   train-error:0.316268 
[173]   train-error:0.316385 
[174]   train-error:0.316062 
[175]   train-error:0.316474 
[176]   train-error:0.316444 
[177]   train-error:0.316179 
[178]   train-error:0.315944 
[179]   train-error:0.316091 
[180]   train-error:0.316032 
[181]   train-error:0.315768 
[182]   train-error:0.315915 
[183]   train-error:0.315974 
[184]   train-error:0.316268 
[185]   train-error:0.315621 
[186]   train-error:0.315768 
[187]   train-error:0.315797 
[188]   train-error:0.315650 
[189]   train-error:0.315474 
[190]   train-error:0.315533 
[191]   train-error:0.315297 
[192]   train-error:0.315268 
[193]   train-error:0.315033 
[194]   train-error:0.315003 
[195]   train-error:0.315003 
[196]   train-error:0.315268 
[197]   train-error:0.314915 
[198]   train-error:0.314386 
[199]   train-error:0.314533 
[200]   train-error:0.314092 
[201]   train-error:0.314121 
[202]   train-error:0.314209 
[203]   train-error:0.314121 
[204]   train-error:0.314209 
[205]   train-error:0.314033 
[206]   train-error:0.314003 
[207]   train-error:0.313886 
[208]   train-error:0.313886 
[209]   train-error:0.313768 
[210]   train-error:0.313886 
[211]   train-error:0.313798 
[212]   train-error:0.313915 
[213]   train-error:0.313356 
[214]   train-error:0.313209 
[215]   train-error:0.313592 
[216]   train-error:0.313151 
[217]   train-error:0.313445 
[218]   train-error:0.313356 
[219]   train-error:0.313474 
[220]   train-error:0.313474 
[221]   train-error:0.313356 
[222]   train-error:0.313151 
[223]   train-error:0.313062 
[224]   train-error:0.312680 
[225]   train-error:0.312886 
[226]   train-error:0.313180 
[227]   train-error:0.312915 
[228]   train-error:0.312474 
[229]   train-error:0.312504 
[230]   train-error:0.312415 
[231]   train-error:0.312474 
[232]   train-error:0.312092 
[233]   train-error:0.311592 
[234]   train-error:0.311827 
[235]   train-error:0.311739 
[236]   train-error:0.311769 
[237]   train-error:0.311798 
[238]   train-error:0.311474 
[239]   train-error:0.311621 
[240]   train-error:0.311621 
[241]   train-error:0.311416 
[242]   train-error:0.310916 
[243]   train-error:0.311092 
[244]   train-error:0.311092 
[245]   train-error:0.310769 
[246]   train-error:0.310975 
[247]   train-error:0.310504 
[248]   train-error:0.310680 
[249]   train-error:0.310181 
[250]   train-error:0.310210 
pred_penalty <- predict(xgb_penalty, test_x)
error_penalty <- abs((as.numeric(test_y) - 1) - pred_penalty) > 0.5
attributes(xgb_penalty)$evaluation_log[250, ]
    iter train_error
   <num>       <num>
1:   250     0.31021
mean(error_penalty)
[1] 0.3300988
error_default <- rep(0, 249)
error_penalty <- rep(0, 249)
for (ntree_limit in 1:249) {
  iteration_range <- c(1, ntree_limit + 1)
  pred_def <- predict(xgb_default, test_x, iteration_range = iteration_range)
  error_default[ntree_limit] <- mean(abs(as.numeric(test_y)-1-pred_def) >= 0.5)
  pred_pen <- predict(xgb_penalty, test_x, iteration_range = iteration_range)
  error_penalty[ntree_limit] <- mean(abs(as.numeric(test_y)-1-pred_pen) >= 0.5)
}
errors <- bind_rows(
  tibble(Iterations = 1:250, Dataset = "train", Penalty = "no",
    Error = attributes(xgb_default)$evaluation_log$train_error),
  tibble(Iterations = 1:249, Dataset = "test", Penalty = "no",
    Error = error_default),
  tibble(Iterations = 1:250, Dataset = "train", Penalty = "yes",
    Error = attributes(xgb_penalty)$evaluation_log$train_error),
  tibble(Iterations = 1:249, Dataset = "test", Penalty = "yes",
    Error = error_penalty),
)
ggplot(errors, aes(x = Iterations, y = Error, color = Penalty,
    linetype = Dataset)) +
  geom_line() +
  scale_color_manual(values = c("red", "black"))

last_plot()  +
  theme_bw() +
  annotate("text", 165, 0.2, label="Default / Training set") +
  annotate("text", 150, 0.3, label="Penalty / Training set") +
  annotate("text", 220, 0.34, label="Penalty / Test set") +
  annotate("text", 75, 0.36, label="Default / Test set") +
  theme(legend.position = "none")

Hyperparameters and Cross-Validation

set.seed(1234)
resamples <- vfold_cv(loan_data, v = 5)
grid <- crossing(
  tree_depth = c(3, 6, 12),
  learn_rate = c(0.1, 0.5, 0.9),
)
plan(multisession, workers = parallel::detectCores(logical = FALSE))

rec <- recipe(outcome ~ ., data = loan_data) %>%
  step_dummy(all_nominal_predictors())
model <- boost_tree(mode = "classification",
  tree_depth = tune(), learn_rate = tune())

wf <- workflow() %>%
  add_model(model) %>%
  add_recipe(rec)

model_cv <- tune_grid(wf, resamples, grid = grid)
collect_metrics(model_cv) %>%
  filter(.metric == "accuracy") %>%
  mutate(error_rate = round(100 * (1 - mean), 2)) %>%
  select(c(tree_depth, learn_rate, error_rate)) %>%
  arrange(error_rate)
# A tibble: 9 × 3
  tree_depth learn_rate error_rate
       <dbl>      <dbl>      <dbl>
1          3        0.5       33.3
2          3        0.9       33.5
3          6        0.1       33.5
4          6        0.5       33.8
5          3        0.1       34.2
6         12        0.1       34.7
7          6        0.9       34.9
8         12        0.5       36.4
9         12        0.9       38.8

Supplementary Material

Figure 6-2. KNN prediction of loan default using two variables: debt-to-income ratio and loan-payment-to-income ratio

train_data <- loan200 %>%
  dplyr::slice(-1)
newloan <- loan200 %>%
  dplyr::slice(1) %>%
  select(payment_inc_ratio, dti)

# The knn function from the class package gives us access to a list of the nearest
# neighbors and their distances via attributes "nn.index" and "nn.dist"
knn_pred <- knn(train = train_data[c("payment_inc_ratio", "dti")], test = newloan,
  cl = train_data$outcome, k = 20)

nearest_points <- loan200[c(attr(knn_pred, "nn.index") + 1), ]
nearest_points
# A tibble: 20 × 3
   outcome  payment_inc_ratio   dti
   <fct>                <dbl> <dbl>
 1 default               8.66  22.2
 2 default               9.06  21.6
 3 paid off              9.45  23.3
 4 default               8.71  24.1
 5 default               9.43  24.2
 6 default               8.03  20.9
 7 default               6.92  22.5
 8 paid off              9.64  20.2
 9 paid off              7.70  24.6
10 paid off             11.5   21.4
11 paid off              8.65  19.8
12 paid off             11.9   22.5
13 paid off              9.52  19.7
14 paid off              7.90  19.7
15 paid off             10.2   25.5
16 default               5.82  22  
17 default              12.4   22.2
18 paid off              5.62  22.4
19 paid off              8.16  19.1
20 default              10.0   19.1
dist <- attr(knn_pred, "nn.dist")

circle_coordinates <- function(center = c(0, 0), r = 1, npoints = 100) {
  tt <- seq(0, 2 * pi, length.out = npoints - 1)
  xx <- center[1] + r * cos(tt)
  yy <- center[2] + r * sin(tt)
  return(data.frame(x = c(xx, xx[1]), y = c(yy, yy[1])))
}

circle_df <- circle_coordinates(center = unlist(newloan), r = max(dist),
  npoints = 201)
# set first entry as target - requires adding additional level to factor
levels(loan200$outcome) <- c(levels(loan200$outcome), "newloan")
loan200[1, "outcome"] <- "newloan"
head(loan200)
# A tibble: 6 × 3
  outcome  payment_inc_ratio   dti
  <fct>                <dbl> <dbl>
1 newloan               9    22.5 
2 default               5.47 21.3 
3 paid off              6.90  8.97
4 paid off             11.1   1.83
5 default               3.72 10.8 
6 paid off              1.90 11.3 
levels(nearest_points$outcome)
[1] "paid off" "default" 
graph <- ggplot(data = loan200, aes(x = payment_inc_ratio, y = dti, color = outcome)) +
  geom_point(aes(shape = outcome), size = 2, alpha = 0.4) +
  geom_point(data = nearest_points, aes(shape = outcome), size = 2) +
  geom_point(data = loan200[1, ], aes(shape = outcome), size = 2) +
  scale_shape_manual(values = c(15, 16, 4)) +
  scale_color_manual(values = c("paid off" = "#1b9e77", "default" = "#d95f02", "newloan" = "black")) +
  geom_path(data = circle_df, aes(x = x, y = y), color = "black") +
  coord_cartesian(xlim = c(3, 15), ylim = c(17, 29)) +
  theme_bw()
graph

Figure 6-4. The first five rules for a simple tree model fit to the loan data

## Figure 6-4: View of partition rules
r_tree <- tibble(
  x1 = c(0.575, 0.375, 0.375, 0.375, 0.475),
  x2 = c(0.575, 0.375, 0.575, 0.575, 0.475),
  y1 = c(0, 0, 10.42, 4.426, 4.426),
  y2 = c(25, 25, 10.42, 4.426, 10.42),
  rule_number = factor(c(1, 2, 3, 4, 5)))
r_tree <- as.data.frame(r_tree)

rules <- tibble(
  x = c(0.575, 0.375, 0.4, 0.4, 0.475),
  y = c(24, 24, 10.42, 4.426, 9.42),
  rule_number = factor(c(1, 2, 3, 4, 5))
)

labs <- tibble(
  x = c(
    0.575 + (1 - 0.575) / 2,
    0.375 / 2,
    (0.375 + 0.575) / 2,
    (0.375 + 0.575) / 2,
    (0.475 + 0.575) / 2,
    (0.375 + 0.475) / 2
  ),
  y = c(
    12.5,
    12.5,
    10.42 + (25 - 10.42) / 2,
    4.426 / 2,
    4.426 + (10.42 - 4.426) / 2,
    4.426 + (10.42 - 4.426) / 2
  ),
  decision = factor(c(
    "paid off",
    "default",
    "default",
    "paid off",
    "paid off",
    "default"
  ))
)
shift <- 0.004
shift <- 0
graph <- loan3000 %>%
  #  mutate(borrower_score = ifelse(outcome == "paid off", borrower_score + shift, borrower_score - shift)) %>%
  mutate(
    borrower_score = borrower_score + rnorm(dim(loan3000)[1], sd = 2 * shift)
  ) %>%
  ggplot(aes(x = borrower_score, y = payment_inc_ratio)) +
    geom_point(aes(color = outcome, size = outcome, shape = outcome), alpha = 0.5) +
    # scale_color_manual(values = c("paid off" = "#7fbc41", "default" = "#d95f02")) +
    scale_shape_manual(values = c("paid off" = 0, "default" = 1)) +
    scale_size_manual(values = c("paid off" = 1, "default" = 1)) +
    geom_segment(data = r_tree, aes(x = x1, y = y1, xend = x2, yend = y2), linewidth = 1.5) +
    guides(color = guide_legend(override.aes = list(linewidth = 1.5)),
      linetype = guide_legend(keywidth = 3, override.aes = list(linewidth = 1))) +
    scale_x_continuous(expand = c(0, 0)) +
    scale_y_continuous(expand = c(0, 0)) +
    coord_cartesian(ylim = c(0, 25)) +
    geom_label(data = labs, aes(x = x, y = y, label = decision)) +
    geom_label(data = rules, aes(x = x, y = y, label = rule_number),
      size = 2.5, fill = "#eeeeee",
      label.r = unit(0, "lines"), label.padding = unit(0.2, "lines")) +
    guides(color = guide_legend(override.aes = list(size = 2))) +
    stat_density_2d(aes(linetype = outcome, color = outcome),  
    # variation geom = "polygon",
    # variation aes(alpha = 0.1 * ..level.., fill = outcome),
                    bins = 6)  +
    theme_bw()
graph

graph <- ggplot(data = loan3000, aes(x = borrower_score, y = payment_inc_ratio)) +
  geom_point(aes(color = outcome, shape = outcome, size = outcome), alpha = 0.8) +
  scale_color_manual(values = c("paid off" = "#7fbc41", "default" = "#d95f02")) +
  scale_shape_manual(values = c("paid off" = 0, "default" = 1)) +
  scale_size_manual(values = c("paid off" = 0.5, "default" = 2)) +
  geom_segment(data = r_tree, aes(x = x1, y = y1, xend = x2, yend = y2), size = 1.5) +
  guides(color = guide_legend(override.aes = list(size = 1.5)),
    linetype = guide_legend(keywidth = 3, override.aes = list(size = 1))) +
  scale_x_continuous(expand = c(0, 0)) +
  scale_y_continuous(expand = c(0, 0)) +
  coord_cartesian(ylim = c(0, 25)) +
  geom_label(data = labs, aes(x = x, y = y, label = decision)) +
  geom_label(data = rules, aes(x = x, y = y, label = rule_number),
    size = 2.5, fill = "#eeeeee",
    label.r = unit(0, "lines"), label.padding = unit(0.2, "lines")) +
  guides(color = guide_legend(override.aes = list(size = 2))) +
  theme_bw()
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
graph

Figure 6-5. Gini impurity and entropy measures

info <- function(x) {
  info <- ifelse(x == 0, 0, -x * log2(x) - (1 - x) * log2(1 - x))
  return(info)
}

gini <- function(x) {
  return(x * (1 - x))
}

x <- 0:50 / 100
impure <- bind_rows(
  tibble(p = x, impurity = 2 * x, type = "Accuracy"),
  tibble(p = x, impurity = gini(x) / gini(0.5) * info(0.5), type = "Gini"),
  tibble(p = x, impurity = info(x), type = "Entropy")
)

graph <- ggplot(data = impure, aes(x = p, y = impurity, linetype = type, color = type)) +
  geom_line(size = 1.5) +
  guides(linetype = guide_legend(keywidth = 3, override.aes = list(size = 1))) +
  scale_x_continuous(expand = c(0, 0.01)) +
  scale_y_continuous(expand = c(0, 0.01)) +
  theme_bw() +
  theme(legend.title = element_blank())
graph