Coursework·

Bank loan payment default prediction - EOT Assignement

Predicting bank loan payment default in R, from exploratory analysis through to model comparison.

Context

This project is the end of term assignment in the Discriminant Analysis course led by Prof. Salim Lardjane, UBS.

Goals

The project has 2 main goals:

  • be familiar with standard machine learning models
  • implement from scratch those algorithms to understand the underlying statistics

Loading in the libraries

We’ll be reading a file containing information regarding the financial situation of a person to know weather this person is fit to pay the default or not.

library(foreign)
library(MASS)
library(ROCR)
library(mlogit)
library(caTools)
library(e1071)
library(ggplot2)
library(ROCit)
library("tidyverse")
data <- read.spss("./bankloan.sav",to.data.frame=TRUE)

Descriptive analysis

summary(data)
##       age                                   ed          employ      
##  Min.   :20.00   Did not complete high school:460   Min.   : 0.000  
##  1st Qu.:29.00   High school degree          :235   1st Qu.: 3.000  
##  Median :34.00   Some college                :101   Median : 7.000  
##  Mean   :35.03   College degree              : 49   Mean   : 8.566  
##  3rd Qu.:41.00   Post-undergraduate degree   :  5   3rd Qu.:13.000  
##  Max.   :56.00                                      Max.   :33.000  
##     address           income          debtinc         creddebt      
##  Min.   : 0.000   Min.   : 13.00   Min.   : 0.10   Min.   : 0.0117  
##  1st Qu.: 3.000   1st Qu.: 24.00   1st Qu.: 5.10   1st Qu.: 0.3822  
##  Median : 7.000   Median : 35.00   Median : 8.70   Median : 0.8851  
##  Mean   : 8.372   Mean   : 46.68   Mean   :10.17   Mean   : 1.5768  
##  3rd Qu.:12.000   3rd Qu.: 55.75   3rd Qu.:13.80   3rd Qu.: 1.8984  
##  Max.   :34.000   Max.   :446.00   Max.   :41.30   Max.   :20.5613  
##     othdebt         default       preddef1            preddef2        
##  Min.   : 0.04558   No  :517   Min.   :0.0001166   Min.   :0.0000387  
##  1st Qu.: 1.04594   Yes :183   1st Qu.:0.0456301   1st Qu.:0.0351757  
##  Median : 2.00324   NA's:150   Median :0.1715129   Median :0.1590730  
##  Mean   : 3.07879              Mean   :0.2585762   Mean   :0.2578660  
##  3rd Qu.: 3.90300              3rd Qu.:0.4073259   3rd Qu.:0.4332770  
##  Max.   :35.19750              Max.   :0.9993967   Max.   :0.9994570  
##     preddef3      
##  Min.   :0.07464  
##  1st Qu.:0.13476  
##  Median :0.20010  
##  Mean   :0.25858  
##  3rd Qu.:0.32863  
##  Max.   :0.94810

Discriminant Analysis without cross-validation

Separating the data (train/test)

library(MASS)
library(e1071)

na <- c(701:850)
apred <- data[na,] # the data that we're going to predict later on, once our models are trained
data2 <- subset(data, data$default != "NA'S") # the data we'll use for the train/test datasets
data2<-data2[1:9]
data2<-data2[-2]
data2[,8]<-as.numeric(data2$default)-1
n = dim(data2)[1]

id = sample(1:n,floor(0.75*n),replace=F)
train <- data2[id,]
test <- data2[-id,]

Correlation coefficient visualization

library(corrplot)
corrplot(cor(train),        # Correlation matrix
         method = "number", # Correlation plot method
         type = "full",    # Correlation plot style (also "upper" and "lower")
         diag = TRUE,      # If TRUE (default), adds the diagonal
         tl.col = "black", # Labels color
         bg = "white",     # Background color
         title = "",       # Main title
         col = NULL)       # Color palette

Graphs

In order to find the best way to represent our data, we’ll sort the variables in 3 classes : stability, demographic and financial data.

# stability variables : employ address
# demographic variables : age
# financial variables : income debtinc creddebt othdebt

# employ income

data2 %>% mutate(default = factor(default)) %>% 
  ggplot(aes(x = employ, y = income)) + 
  geom_point(mapping = aes(color = default)) +
  labs(x=" Years working for the current employer",
       y=" Income of the employee in thousands of USD",
       color = "Payment default") +
  geom_smooth() +
  scale_color_manual(values = c("0" = "tomato3", 
                                "1" = "blue"), 
                     labels = c("Yes",
                               "No")) +
  theme_bw()+
  ggtitle("Years working for the current employer versus the income of the employee")

# othdebt income

data2 %>% mutate(default = factor(default)) %>% 
  ggplot(aes(x = othdebt, y = income)) + 
  geom_point(mapping = aes(color = default)) +
  labs(y="Income of the employee in thousands of USD",
       x="Other sources of credits in thousands of USD",
       color = "Payment default") +
  geom_smooth() +
  scale_color_manual(values = c("0" = "tomato3", 
                                "1" = "blue"), 
                     labels = c("Yes",
                               "No")) +
  theme_bw()+
  ggtitle("Other sources of credits versus the income of the employee")

# othdebt creddebt

data2 %>% mutate(default = factor(default)) %>% 
  ggplot(aes(x = othdebt, y = creddebt)) + 
  geom_point(mapping = aes(color = default)) +
  labs(y="CC credit in thousands of USD",
       x="Other source of credits in thousands of USD",
       color = "Payment default") +
  geom_smooth() +
  scale_color_manual(values = c("0" = "tomato3", 
                                "1" = "blue"), 
                     labels = c("Yes",
                               "No")) +
  theme_bw()+
  ggtitle("Other credits versus the CC credit in thousands of USD")

Defining the ROC function

This function will allow us to trace a ROC curve for each model that returns posterior probabilities

roc <- function(post1, col){
  trapeze <- function(ao,bo) {
    k <- length(ao)
    iz <- 0
    for (i in 1:(k-1)) {
      iz <- iz+(ao[i+1]-ao[i])*(bo[i]+bo[i+1])/2
    }
    return(iz)
  }
  delta=0.001
  valeurs <- seq(0,1,delta)
  N <- length(valeurs)
  sensibilite <- rep(NA,N)
  antispec <- rep(NA,N)
  i <- 0
  for (u in valeurs) {
    i <- i+1
    pr2predict <- (post1>u)+0
    
    TN1 <- sum((pr2predict==0)*(test$default==0))
    FP1 <- sum((pr2predict==1)*(test$default==0))
    FN1 <- sum((pr2predict==0)*(test$default==1))
    TP1 <- sum((pr2predict==1)*(test$default==1))
    
    Rap1 <- TP1/(TP1+FN1)
    Pre1 <- TP1/(TP1+FP1)
    F_sco1 <- (2*TP1)/(2*TP1+FP1+FN1)
    Spe1 <- TN1/(FP1+TN1)
    Err1 <- ((FP1+FN1)/(TN1+FP1+TP1+FN1)*100)
    
    sensibilite[i] <- Rap1
    antispec[i] <- 1-Spe1
  }
  antispec<- c(0,rev(antispec),1)
  sensibilite <- c(0,rev(sensibilite),1)
  plot(antispec,sensibilite,type="s",col=col,lwd=1)

  return(trapeze(antispec, sensibilite))
}

Defining the models

For each model we’ll compute recall, f-score, acc, specificity and add the values to a dataframe that we’ll comment at the end.

Closest Mean method

no.age <- mean(train[train$default==0,]$age)
yes.age <- mean(train[train$default==1,]$age)
no.employ <- mean(train[train$default==0,]$employ)
yes.employ <- mean(train[train$default==1,]$employ)
no.address <- mean(train[train$default==0,]$address)
yes.address <- mean(train[train$default==1,]$address)
no.income <- mean(train[train$default==0,]$income)
yes.income <- mean(train[train$default==1,]$income)
no.debtinc <- mean(train[train$default==0,]$debtinc)
yes.debtinc <- mean(train[train$default==1,]$debtinc)
no.creddebt <- mean(train[train$default==0,]$creddebt)
yes.creddebt <- mean(train[train$default==1,]$creddebt)
no.othdebt <- mean(train[train$default==0,]$othdebt)
yes.othdebt <- mean(train[train$default==1,]$othdebt)

prediction <- function(x,y,z,a,b,c,d) {
 if ((x-yes.age)^2+(y-yes.employ)^2+(z-yes.address)^2+(a-yes.income)^2+(b-yes.debtinc)^2+(c-yes.creddebt)^2+(d-yes.othdebt)^2 >(x-no.age)^2+(y-no.employ)^2+(z-no.address)^2+(a-no.income)^2+(b-no.debtinc)^2+(c-no.creddebt)^2+(d-no.othdebt)^2)
 {
 return(1)
 } else {return(0)}
}

pred <- rep(NA,length(test$default))
for (i in 1:length(test$default)) {
 pred[i] <- prediction(test$age[i],test$employ[i],test$address[i], test$income[i], test$debtinc[i], test$creddebt[i], test$othdebt[i])
}

MCPPM = table(pred,test$default)
MCPPMerr = (MCPPM[2,1]+MCPPM[1,2])/sum(MCPPM)
MCPPMrappel = MCPPM[2,2]/(MCPPM[2,2]+MCPPM[1,2])
MCPPMprecision = MCPPM[2,2]/(MCPPM[2,2]+MCPPM[2,1])
MCPPMF = 2*MCPPM[2,2]/(2*MCPPM[2,2]+MCPPM[2,1]+2*MCPPM[1,2])
MCPPMspecificite = MCPPM[2,2]/(MCPPM[2,2]+MCPPM[2,1])

PPM = round(c(MCPPMerr,MCPPMrappel,MCPPMprecision,MCPPMF,MCPPMspecificite),2)

Quadratic Discriminant Analysis method

library(MASS)
model = predict(qda(default~.,train),test) 

MCqda = table(test$default,model$class) # correlation matrix

MCqdaerr = (MCqda[2,1]+MCqda[1,2])/sum(MCqda)
MCqdarappel = MCqda[2,2]/(MCqda[2,2]+MCqda[1,2])
MCqdaprecision = MCqda[2,2]/(MCqda[2,2]+MCqda[2,1])
MCqdaF = 2*MCqda[2,2]/(2*MCqda[2,2]+MCqda[2,1]+2*MCqda[1,2])
MCqdaspecificite = MCqda[2,2]/(MCqda[2,2]+MCqda[2,1])
qda = round(c(MCqdaerr,MCqdarappel,MCqdaprecision,MCqdaF,MCqdaspecificite),2)

Linear Discriminant Analysis method

library(MASS)
model = predict(lda(default~.,train),test) 

MClda = table(test$default,model$class)

MCldaerr = (MClda[2,1]+MClda[1,2])/sum(MClda)
MCldarappel = MClda[2,2]/(MClda[2,2]+MClda[1,2])
MCldaprecision = MClda[2,2]/(MClda[2,2]+MClda[2,1])
MCldaF = 2*MClda[2,2]/(2*MClda[2,2]+MClda[2,1]+2*MClda[1,2])
MCldaspecificite = MClda[2,2]/(MClda[2,2]+MClda[2,1])
lda = round(c(MCldaerr,MCldarappel,MCldaprecision,MCldaF,MCldaspecificite),2)

Naive Bayes method

library(e1071) 
model = predict(naiveBayes(default~.,train),test) 

MCbayes = table(test$default,model)

MCbayeserr = (MCbayes[2,1]+MCbayes[1,2])/sum(MCbayes)
MCbayesrappel = MCbayes[2,2]/(MCbayes[2,2]+MCbayes[1,2])
MCbayesprecision = MCbayes[2,2]/(MCbayes[2,2]+MCbayes[2,1])
MCbayesF = 2*MCbayes[2,2]/(2*MCbayes[2,2]+MCbayes[2,1]+2*MCbayes[1,2])
MCbayesspecificite = MCbayes[2,2]/(MCbayes[2,2]+MCbayes[2,1])
bayes = round(c(MCbayeserr,MCbayesrappel,MCbayesprecision,MCbayesF,MCbayesspecificite),2)

K closest neighbours method

library(class)
cl <- as.factor(train$default)

predtest <- knn(train,test,cl, k=1) 

MCknn1 = table(test$default,predtest)

MCknn1err = (MCknn1[2,1]+MCknn1[1,2])/sum(MCknn1)
MCknn1rappel = MCknn1[2,2]/(MCknn1[2,2]+MCknn1[1,2])
MCknn1precision = MCknn1[2,2]/(MCknn1[2,2]+MCknn1[2,1])
MCknn1F = 2*MCknn1[2,2]/(2*MCknn1[2,2]+MCknn1[2,1]+2*MCknn1[1,2])
MCknn1specificite = MCknn1[2,2]/(MCknn1[2,2]+MCknn1[2,1])

knn1 = round(c(MCknn1err,MCknn1rappel,MCknn1precision,MCknn1F,MCknn1specificite),2)


predtest <- knn(train,test,cl, k=2)

MCknn2 = table(test$default,predtest)

MCknn2err = (MCknn2[2,1]+MCknn2[1,2])/sum(MCknn2)
MCknn2rappel = MCknn2[2,2]/(MCknn2[2,2]+MCknn2[1,2])
MCknn2precision = MCknn2[2,2]/(MCknn2[2,2]+MCknn2[2,1])
MCknn2F = 2*MCknn2[2,2]/(2*MCknn2[2,2]+MCknn2[2,1]+2*MCknn2[1,2])
MCknn2specificite = MCknn2[2,2]/(MCknn2[2,2]+MCknn2[2,1])

knn2 = round(c(MCknn2err,MCknn2rappel,MCknn2precision,MCknn2F,MCknn2specificite),2)

predtest <- knn(train,test,cl,k=3)

MCknn3 = table(test$default,predtest)

MCknn3err = (MCknn3[2,1]+MCknn3[1,2])/sum(MCknn3)
MCknn3rappel = MCknn3[2,2]/(MCknn3[2,2]+MCknn3[1,2])
MCknn3precision = MCknn3[2,2]/(MCknn3[2,2]+MCknn3[2,1])
MCknn3F = 2*MCknn3[2,2]/(2*MCknn3[2,2]+MCknn3[2,1]+2*MCknn3[1,2])
MCknn3specificite = MCknn3[2,2]/(MCknn3[2,2]+MCknn3[2,1])

knn3 = round(c(MCknn3err,MCknn3rappel,MCknn3precision,MCknn3F,MCknn3specificite),2)

Logistic regression model

Preparing the data

data2$default <- as.factor(data2$default)

Defining the model

reglog <- glm(default~., family = binomial, data=train, control=list(maxit=1000, trace=TRUE, epsilon=1e-16))

preds <- predict(reglog, test, type='response')
optimal_cutoff <- ifelse(preds > 0.5, 1, 0)

summary(reglog)
## 
## Call:
## glm(formula = default ~ ., family = binomial, data = train, control = list(maxit = 1000, 
##     trace = TRUE, epsilon = 1e-16))
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.3241  -0.6734  -0.3020   0.2628   2.6057  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.657904   0.664307  -2.496   0.0126 *  
## age          0.044607   0.020375   2.189   0.0286 *  
## employ      -0.268156   0.037089  -7.230 4.82e-13 ***
## address     -0.113424   0.027131  -4.181 2.91e-05 ***
## income      -0.008652   0.008473  -1.021   0.3072    
## debtinc      0.067861   0.035900   1.890   0.0587 .  
## creddebt     0.611694   0.134228   4.557 5.19e-06 ***
## othdebt      0.080870   0.085357   0.947   0.3434    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 602.75  on 524  degrees of freedom
## Residual deviance: 418.16  on 517  degrees of freedom
## AIC: 434.16
## 
## Number of Fisher Scoring iterations: 7
#confusion matrix
MCreg = table(test$default, optimal_cutoff)

MCregerr = (MCreg[2,1]+MCreg[1,2])/sum(MCreg)
MCregrappel = MCreg[2,2]/(MCreg[2,2]+MCreg[1,2])
MCregprecision = MCreg[2,2]/(MCreg[2,2]+MCreg[2,1])
MCregF = 2*MCreg[2,2]/(2*MCreg[2,2]+MCreg[2,1]+2*MCreg[1,2])
MCregspecificite = MCreg[2,2]/(MCreg[2,2]+MCreg[2,1])
reg = round(c(MCregerr,MCregrappel,MCregprecision,MCregF,MCregspecificite),2)
titles = c("Error rate","Recall","Accuracy","F-Score","Specificity")
data.frame(titles,lda,qda,knn1,knn2,knn3,PPM, reg)
##        titles  lda  qda knn1 knn2 knn3  PPM  reg
## 1  Error rate 0.19 0.21 0.29 0.30 0.24 0.54 0.18
## 2      Recall 0.70 0.66 0.46 0.42 0.56 0.22 0.68
## 3    Accuracy 0.50 0.46 0.46 0.35 0.39 0.14 0.59
## 4     F-Score 0.52 0.47 0.36 0.30 0.39 0.13 0.55
## 5 Specificity 0.50 0.46 0.46 0.35 0.39 0.14 0.59

Disciminant Analysis with cross-validation method Leave-One-Out

data2$default <- as.numeric(data2$default)-1

n = dim(data2)[1]
lda = 0
qda = 0
naive = 0
knn1 = 0
knn2 = 0
knn3 = 0
ppm = 0

for(i in 1:n) {
  

  train <- data2[-i,]
  test <- data2[i,]
  
  # LDA #
  
  model = predict(lda(default~.,train),test)
  erlda = (data2$default[i] != model$class) + 0
  MClda = table(model$class,test$default)
  
  lda[i] = erlda
  
  # QDA #
  
  model2 = predict(qda(default~.,train),test)
  erqda = (data2$default[i] != model2$class) + 0
  
  qda[i] = erqda
  
  # Naive Bayes #
  
  model3 = predict(naiveBayes(default~.,train),test)
  ernaive = (data2$default[i] != model3) + 0
  
  naive[i] = ernaive
  
  # KNN #
  
  cl <- as.factor(train$default)
  
  # Prediction K=1 #
  predtest <- knn(train,test,cl, k=1)
  
  # errors K=1 #
  erknn1 = (data2$default[i] != predtest) + 0
  knn1[i] = erknn1
  
  # Prediction K=2 #
  predtest <- knn(train,test,cl, k=2)
  
  # errors K=2 #
  erknn2 = (data2$default[i] != predtest) + 0
  knn2[i] = erknn2
  
  # Prediction K=3 #
  predtest <- knn(train,test,cl, k=3)
  
  # errors K=3 #
  erknn3 = (data2$default[i] != predtest) + 0
  knn3[i] = erknn3
  
  # Prediction for closest mean #

  no.age <- mean(train[train$default==0,]$age)
  yes.age <- mean(train[train$default==1,]$age)
  no.employ <- mean(train[train$default==0,]$employ)
  yes.employ <- mean(train[train$default==1,]$employ)
  no.address <- mean(train[train$default==0,]$address)
  yes.address <- mean(train[train$default==1,]$address)
  no.income <- mean(train[train$default==0,]$income)
  yes.income <- mean(train[train$default==1,]$income)
  no.debtinc <- mean(train[train$default==0,]$debtinc)
  yes.debtinc <- mean(train[train$default==1,]$debtinc)
  no.creddebt <- mean(train[train$default==0,]$creddebt)
  yes.creddebt <- mean(train[train$default==1,]$creddebt)
  no.othdebt <- mean(train[train$default==0,]$othdebt)
  yes.othdebt <- mean(train[train$default==1,]$othdebt)
  
  prediction <- function(x,z,a,b,c,d,e) {
   if ((x-yes.age)^2+(z-yes.employ)^2+(a-yes.address)^2+
       (b-yes.income)^2+(c-yes.debtinc)^2+(d-yes.creddebt)^2+
       (e-yes.othdebt)^2 >
       (x-no.age)^2+(z-no.employ)^2+(a-no.address)^2+
       (b-no.income)^2+(c-no.debtinc)^2+(d-no.creddebt)^2+
       (e-no.othdebt)^2)
  {
   return(1)
   } else {return(0)}
  }

  pred <- prediction(test$age,test$employ,test$address, test$income, test$debtinc, test$creddebt, test$othdebt)
  
  erppm = (data2$default[i] != pred) + 0
  ppm[i] <- erppm
}

validationCroisée = c("taux d'erreur")
res = c(mean(lda),mean(qda),mean(naive),mean(knn1),mean(knn2),mean(knn3), mean(ppm))
lda = mean(lda)
qda = mean(qda)
naive = mean(naive)
knn1 = mean(knn1)
knn2 = mean(knn2)
knn3 = mean(knn3)
ppm = mean(ppm)
data.frame(validationCroisée,lda,qda,naive,knn1,knn2,knn3,ppm)
##   validationCroisée       lda       qda     naive      knn1      knn2      knn3
## 1     taux d'erreur 0.1871429 0.2228571 0.2471429 0.2842857 0.2928571 0.2471429
##         ppm
## 1 0.5414286

Here we can see that the model with the smallest error rate is the LDA model.

Using our best model for the prediction on the data

model = predict(lda(default~.,data2),apred) 
model$class
##   [1] 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
##  [38] 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 1
##  [75] 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 0 0 1
## [112] 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0
## [149] 0 0
## Levels: 0 1
yes = 0
no = 0
for (i in 1:150){
  if(model$class[i] == 1){
    yes=yes+1
  }else{
    no=no+1
  }
}
yes
## [1] 23
no
## [1] 127
prediction = yes/150;prediction
## [1] 0.1533333

So out of 150 people, 23 are likely to repay their credit and 127 are not likely to repay their credit, iow -> 15.3% of the population