Data Preparation

german_credit <- read.csv("german_credit.csv")
str(german_credit) #continuous or factors, convert category to factor
'data.frame':   1000 obs. of  21 variables:
 $ Creditability                    : int  1 1 1 1 1 1 1 1 1 1 ...
 $ Account.Balance                  : int  1 1 2 1 1 1 1 1 4 2 ...
 $ Duration.of.Credit..month.       : int  18 9 12 12 12 10 8 6 18 24 ...
 $ Payment.Status.of.Previous.Credit: int  4 4 2 4 4 4 4 4 4 2 ...
 $ Purpose                          : int  2 0 9 0 0 0 0 0 3 3 ...
 $ Credit.Amount                    : int  1049 2799 841 2122 2171 2241 3398 1361 1098 3758 ...
 $ Value.Savings.Stocks             : int  1 1 2 1 1 1 1 1 1 3 ...
 $ Length.of.current.employment     : int  2 3 4 3 3 2 4 2 1 1 ...
 $ Instalment.per.cent              : int  4 2 2 3 4 1 1 2 4 1 ...
 $ Sex...Marital.Status             : int  2 3 2 3 3 3 3 3 2 2 ...
 $ Guarantors                       : int  1 1 1 1 1 1 1 1 1 1 ...
 $ Duration.in.Current.address      : int  4 2 4 2 4 3 4 4 4 4 ...
 $ Most.valuable.available.asset    : int  2 1 1 1 2 1 1 1 3 4 ...
 $ Age..years.                      : int  21 36 23 39 38 48 39 40 65 23 ...
 $ Concurrent.Credits               : int  3 3 3 3 1 3 3 3 3 3 ...
 $ Type.of.apartment                : int  1 1 1 1 2 1 2 2 2 1 ...
 $ No.of.Credits.at.this.Bank       : int  1 2 1 2 2 2 2 1 2 1 ...
 $ Occupation                       : int  3 3 2 2 2 2 2 2 1 1 ...
 $ No.of.dependents                 : int  1 2 1 2 1 2 1 2 1 1 ...
 $ Telephone                        : int  1 1 1 1 1 1 1 1 1 1 ...
 $ Foreign.Worker                   : int  1 1 1 2 2 2 2 2 1 1 ...
id = c(1,2,4,5,7:13,15:21)
german_credit[id]=lapply(german_credit[id],as.factor)
library(caret)
library(rsample)

set.seed(999)

idx1<-initial_split(data=german_credit,prop=0.7,strata = "Creditability")
train1<-training(idx1)
test1<-testing(idx1)
control<-trainControl(method = "cv",number=10)

Models for Classification

set.seed(999)

logit<-train(Creditability~.,data=train1,method="glm",family="binomial",trControl=control)
set.seed(999)

single_gini<-train(Creditability~., data=train1,trControl=control,method="rpart",tuneLength=10,parms=list(split="gini"))

single_entropy<-train(Creditability~., data=train1,trControl=control,method="rpart",tuneLength=10,parms=list(split="information"))
library(doParallel) 
cl<-makePSOCKcluster(detectCores()/2)
registerDoParallel(cl)

set.seed(999)
control2<-trainControl(method="cv",number = 10,allowParallel = T)

tree_bag<-train(Creditability~., data=train1,method="treebag",trControl=control2,nbagg=100,parms=list(split="gini"),importance=T)

stopCluster(cl)
registerDoSEQ()
mtry<-sqrt(ncol(test1)-1) # sqrt of p features
tunegrid<-expand.grid(.mtry=seq(mtry,20)) # can be in the train function

library(doParallel) 
cl<-makePSOCKcluster(detectCores()/2)
registerDoParallel(cl)

set.seed(999)
tree_rf<-train(Creditability~., data=train1,method="rf",trControl=control2,tuneGrid=tunegrid)

stopCluster(cl)
registerDoSEQ()

Prediction

pred_logit<-predict(logit,newdata=test1)
pred_tree<-predict(single_gini,newdata=test1)
pred_bagging<-predict(tree_bag,newdata=test1)
pred_rf<-predict(tree_rf,newdata=test1)

Comparison

In-sample comparison

resamp<-resamples(list("Logistic"=logit,"Single Tree"=single_gini,"Bagging"=tree_bag,"Random Forest"=tree_rf))
summary(resamp)

Call:
summary.resamples(object = resamp)

Models: Logistic, Single Tree, Bagging, Random Forest 
Number of resamples: 10 

Accuracy 
                   Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
Logistic      0.6571429 0.6954451 0.7357143 0.7324638 0.7571429 0.8142857    0
Single Tree   0.6428571 0.6892857 0.7285714 0.7296687 0.7582298 0.8428571    0
Bagging       0.6571429 0.7285714 0.7428571 0.7439959 0.7821429 0.7971014    0
Random Forest 0.7000000 0.7142857 0.7357143 0.7411180 0.7678571 0.7857143    0

Kappa 
                    Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
Logistic      0.11111111 0.2436845 0.3199513 0.3188794 0.3880979 0.5255474    0
Single Tree   0.03100775 0.2148760 0.2430442 0.2750166 0.3715603 0.6099291    0
Bagging       0.08396947 0.2528346 0.3484353 0.3384645 0.4521512 0.5098039    0
Random Forest 0.16000000 0.2374809 0.3150845 0.3027506 0.3773289 0.4000000    0
bwplot(resamp,metric="Accuracy")

Out-of-sample comparison

cm_logit <- confusionMatrix(pred_logit, test1$Creditability)
cm_tree <- confusionMatrix(pred_tree, test1$Creditability)
cm_bag  <- confusionMatrix(pred_bagging, test1$Creditability)
cm_rf   <- confusionMatrix(pred_rf, test1$Creditability)

# combine test-set performance

com_results <- data.frame(
  Model = c("Logistic","Single Tree", "Bagging", "Random Forest"),
  Accuracy = c(
    cm_logit$overall["Accuracy"],
    cm_tree$overall["Accuracy"],
    cm_bag$overall["Accuracy"],
    cm_rf$overall["Accuracy"]
  ),
  Kappa = c(
    cm_logit$overall["Kappa"],
    cm_tree$overall["Kappa"],
    cm_bag$overall["Kappa"],
    cm_rf$overall["Kappa"]
  ),
  Sensitivity = c(
    cm_logit$byClass["Sensitivity"],
    cm_tree$byClass["Sensitivity"],
    cm_bag$byClass["Sensitivity"],
    cm_rf$byClass["Sensitivity"]
  ),
  Specificity = c(
    cm_logit$byClass["Specificity"],
    cm_tree$byClass["Specificity"],
    cm_bag$byClass["Specificity"],
    cm_rf$byClass["Specificity"]
  ),
  Mcnemar = c(
    cm_logit$overall["McnemarPValue"],
    cm_tree$overall["McnemarPValue"],
    cm_bag$overall["McnemarPValue"],
    cm_rf$overall["McnemarPValue"]
  )
)

com_results
  • Output Results
library(knitr)

kable(
  com_results,
  digits = 4,
  col.names = c(
    "Model", "Accuracy", "Kappa",
    "Sensitivity", "Specificity", "McNemar's p-value"
  ),
  caption = "Comparison of Classification Models",
  format = "html"
)
Comparison of Classification Models
Model Accuracy Kappa Sensitivity Specificity McNemar’s p-value
Logistic 0.7774 0.4190 0.4778 0.9052 0.0015
Single Tree 0.7409 0.3067 0.3778 0.8957 0.0002
Bagging 0.7641 0.4051 0.5111 0.8720 0.0576
Random Forest 0.7940 0.4410 0.4444 0.9431 0.0000

AUC - ROC Curve

library(pROC)
library(ggplot2)


actual <- test1$Creditability

# Predicted probability of Bad Credit (class 1)

p_logit <- predict(logit, test1, type = "prob")[, "1"]
p_tree  <- predict(single_gini, test1, type = "prob")[, "1"]
p_bag   <- predict(tree_bag, test1, type = "prob")[, "1"]
p_rf    <- predict(tree_rf, test1, type = "prob")[, "1"]

# Calculate ROC curves
roc_list <- list(
  "Logistic" = roc(actual, p_logit,
                   levels = c("0", "1"), direction = "<"),
  "Single Tree" = roc(actual, p_tree,
                      levels = c("0", "1"), direction = "<"),
  "Bagging" = roc(actual, p_bag,
                  levels = c("0", "1"), direction = "<"),
  "Random Forest" = roc(actual, p_rf,
                        levels = c("0", "1"), direction = "<")
)

auc_labels <- paste0(
  names(roc_list),
  " (AUC = ",
  sprintf("%.3f", sapply(roc_list, \(x) as.numeric(auc(x)))),
  ")"
)

names(roc_list) <- auc_labels

ggroc(roc_list, legacy.axes = TRUE) +
  geom_abline(
    intercept = 0, slope = 1,
    linetype = "dashed", colour = "grey60"
  ) +
  labs(
    title = "ROC Curves: Credit Default Prediction",
    x = "False Positive Rate (1 - Specificity)",
    y = "True Positive Rate (Sensitivity)",
    colour = "Model"
  ) +
  theme_minimal(base_size = 13)