Getting started deep learning classification on credit risk area

Using powerful Keras API to train a sequential neural network to classify default of loan car customer. This post is for beginner in deep learning, it’s a simple approach of deep learning for binary classification.

David Saada (David Saada)
2018-12-20

A credit risk is the risk of default on a debt that may arise from a borrower failing to make required payments. In the first resort, the risk is that of the lender and includes lost principal and interest, disruption to cash flows, and increased collection costs. We’ll focuse on retail banking area.

The dataset from Banque de France is composed of 10 features and 99.302 customers.

We’ll procede first to load packages and functions, and work on light data processing. At the end we’ll implement deep learning model and check output model indicators.

Configuation packages / functions

Packages: we load 5 packages for data processing, train deep learning model and indicators output


# packages
library(data.table) # for data processing
library(dummies) # for data processing
library(tensorflow) # for classification
library(keras) # for classification
library(ROCR) # to measure model performance

Functions: functions for data processing & output model indicators


# compute lift 
whatsMyLift <- function(score,cible, perc = 0.1,target){
  
  n = ceiling(perc*length(score))
  percentile = cible[order(score, decreasing = T)[1:n]]
  length(percentile[percentile==target])/length(cible[cible==target])
} 

# compute gini + lift 
IndicatorsModels<-function(base,proba,target){
  
  pred <- prediction(proba, FactorToBinNum(base$target,target))
  perf <- performance(pred, measure = "tpr", x.measure = "fpr")
  auc <- performance(pred, measure = "auc")
  GiniIndex<-as.numeric(auc@y.values[[1]]*2-1)
  plot(perf)
  L5<-whatsMyLift(proba,base$target,perc=0.05,target)
  L10<-whatsMyLift(proba,base$target,perc=0.1,target)
  ResInd<-c(L5,L10,GiniIndex)
  names(ResInd)<-c("Lift5","Lift10","gini")
  round(ResInd*100,0)
}

# normalize vector
normalize <- function(x) {
  
  ((x - min(x)) / (max(x) - min(x)))
}

# transform vector
BinToFactor<-function(vect,NonTarget,target){
  
  vect[vect==0]<-NonTarget
  vect[vect==1]<-target
  as.factor(vect)
}

# transform vector
FactorToBin<-function(vect,target){
  vect<-as.character(vect)
  vect[vect!=target]<-"0"
  vect[vect==target]<-"1"
  as.factor(vect)
}

# transform vector
FactorToBinNum<-function(vect,target){
  vect<-as.character(vect)
  vect[vect!=target]<-"0"
  vect[vect==target]<-"1"
  as.numeric(vect)
}

# split train/test
trainTestSplit <- function(df,trainPercent,seed1){
  ## sample size percent
  samp_size <- floor(trainPercent/100 * nrow(df))
  ## set the seed 
  set.seed(seed1)
  idx <- sample(seq_len(nrow(df)), size = samp_size)
  idx
}

PropTable2<-function(vect){
  round(prop.table(table(vect)),2)
}

Data processing

Reading credit risk dataset. We use fread function from data.table package (fast read).


path<-paste(getwd(),"data",sep="/")
data<-fread(paste(path,"dataset.csv",sep="/"),sep=";",stringsAsFactors = F,colClasses = "character")
data<-data.frame(data)

Processing numerical & factor features. We’ll scale et replace NA value for numerical features, and dummies the categorical features.


# select data
ListNum<-c("amount","old.work","default","time","downpayment")
ListFact<-c("type","type.work","gender","working.sector","used.car")

# numerical features processing: NA & scale
DataNum<-data[,match(ListNum,names(data))]
for (i in 1:dim(DataNum)[2]){
  col<-as.numeric(DataNum[,i])
  col[is.na(col)]<-median(col[!is.na(col)])
  col<-normalize(col)
  DataNum[,i]<-col
}
str(DataNum)

'data.frame':   99392 obs. of  5 variables:
 $ amount     : num  0.506 0.509 0.449 0.503 0.405 ...
 $ old.work   : num  0.327 0.232 0.048 0.166 0.156 ...
 $ default    : num  0 0 0 0 0 0 0 0 0 0 ...
 $ time       : num  0.514 0.667 0.667 0.5 0.167 ...
 $ downpayment: num  0 0 0.434 0 0.716 ...

# factor features processing
DataFact<-data[,match(ListFact,names(data))]
for (i in 1:dim(DataFact)[2]){
  col<-as.factor(DataFact[,i])
  DataFact[,i]<-col
}
str(DataFact)

'data.frame':   99392 obs. of  5 variables:
 $ type          : Factor w/ 2 levels "PRI","PRO": 2 2 1 2 1 1 1 2 2 1 ...
 $ type.work     : Factor w/ 17 levels "","0","10","11",..: 6 2 6 6 4 5 4 2 4 6 ...
 $ gender        : Factor w/ 7 levels "","C","D","M",..: 4 2 2 4 4 4 4 2 2 2 ...
 $ working.sector: Factor w/ 17 levels "AGR","ATR","BTP",..: 10 1 2 10 3 4 2 1 3 2 ...
 $ used.car      : Factor w/ 2 levels "VN","VO": 1 1 1 1 1 1 1 1 1 1 ...

# decompose factor dataset on dummy
DataFact<-dummy.data.frame(DataFact,sep="-")

Final dataset: merge between numerical and dummies features.


# final data
data<-data.frame(DataNum,DataFact)

# numerical format
for (i in 1:dim(data)[2]){
  data[,i]<-as.numeric(data[,i])
}

Define the target: we just rename the target.


# define target
names(data)[match("default",names(data))]<-"target"

# target proportion
PropTable2(data$target)

vect
   0    1 
0.98 0.02 

We see umbalanced data, we have two ways here: first we work with umbalanced data on the algorithm or we work now on the data to get balanced.

Umbalanced data

We have to deal with the umbalanced dataset: we’ll generate artifical data to get balanced.


# take umbalanced data
DataTarget<-data[data$target==1,]
d1<-dim(DataTarget)[1]
d2<-dim(data)[1]
t<-round(d2/d1,0)
res<-NULL
for (i in 1:t){
  res<-rbind(res,DataTarget)
}
data<-rbind(data,res)

# mixed data
Random<-557
data_idx<-trainTestSplit(data,100,Random)
data<-data[data_idx,]

# proportion with balanced data
PropTable2(data$target)

vect
   0    1 
0.49 0.51 

Split dataset

We just rename the target as “Default” or “NonDefault”


# named target
NonTarget<-"NonDefault"
target<-"default"

# change target to names
data$target<-BinToFactor(data$target,NonTarget,target)

Split train/test set: we take 70% of train and 30% of test set


# named target
NonTarget<-"NonDefault"
target<-"default"

# change target to names
data$target<-BinToFactor(data$target,NonTarget,target)

# train and test
Random<-558
train_idx <- trainTestSplit(data,70,Random)
train <- data[train_idx, ]
test <- data[-train_idx, ]

# check target proportion on train/test dataset
r1<-PropTable2(train$target)
r2<-PropTable2(test$target)
r3<-rbind(r1,r2)
rownames(r3)<-c("train","test")
print(r3)

      default NonDefault
train    0.51       0.49
test     0.51       0.49

Processing final data for tensorflow format model


# match target
mTarget<-match("target",names(data))

# data processing for tensorflow
x_train <- data.matrix(train[,-mTarget])
x_test <- data.matrix(test[,-mTarget])
y_train <- as.numeric(FactorToBin(train$target,target))-1
y_train <- to_categorical(y_train, 2)
y_test <- as.numeric(FactorToBin(test$target,target))-1
y_test <- to_categorical(y_test, 2)

Model

Set up sequential neural network model using Keras API. The sequential model makes the assumption that the network has exactly one input and exactly one output, and that it consists of a linear stack of layers.


# dimension dataset
d<-dim(train)[2]-1

# set up model
model <- keras_model_sequential() 
model %>% 
  layer_dense(units = 5, activation = 'relu', input_shape = d) %>% 
  layer_dropout(rate = 0.1) %>%
  layer_dense(units = 7, activation = 'relu', input_shape = d) %>% 
  layer_dropout(rate = 0.1) %>%
  layer_dense(units = 7, activation = 'relu', input_shape = d) %>% 
  layer_dropout(rate = 0.1) %>%
  layer_dense(units = 2, activation = 'softmax')

# compile model
model %>% compile(
  loss = 'categorical_crossentropy',
  optimizer = optimizer_rmsprop(),
  metrics = 'accuracy'
)
print(model)

Model
______________________________________________________________________
Layer (type)                   Output Shape                Param #    
======================================================================
dense_1 (Dense)                (None, 5)                   250        
______________________________________________________________________
dropout_1 (Dropout)            (None, 5)                   0          
______________________________________________________________________
dense_2 (Dense)                (None, 7)                   42         
______________________________________________________________________
dropout_2 (Dropout)            (None, 7)                   0          
______________________________________________________________________
dense_3 (Dense)                (None, 7)                   56         
______________________________________________________________________
dropout_3 (Dropout)            (None, 7)                   0          
______________________________________________________________________
dense_4 (Dense)                (None, 2)                   16         
======================================================================
Total params: 364
Trainable params: 364
Non-trainable params: 0
______________________________________________________________________

# fit model
history <- model %>% fit(
  x_train, y_train, 
  epochs = 20, batch_size = 100, 
  validation_split = 0.2
)
print(history)

Trained on 110,964 samples, validated on 27,742 samples (batch_size=100, epochs=20)
Final epoch (plot to see history):
val_loss: 0.5481
 val_acc: 0.7107
    loss: 0.559
     acc: 0.7135 

Result of training


plot(history)

Model indicators

AUC is common metric on credit risk: AUC is area under the curve, In a ROC curve the true positive rate (Sensitivity) is plotted in function of the false positive rate (100-Specificity) for different cut-off points of a parameter. We’ll compute gini index: gini = 2*AUC-1.


model %>% evaluate(x_test, y_test)

$loss
[1] 0.5422537

$acc
[1] 0.7153551

proba<-model %>% predict_proba(x_train)
proba<-proba[,2]
hist(proba)


ResTrain<-IndicatorsModels(train,proba,target)["gini"]


proba<-model %>% predict_proba(x_test)
proba<-proba[,2]
hist(proba)


ResTest<-IndicatorsModels(test,proba,target)["gini"]


res<-rbind(ResTrain,ResTest)
rownames(res)<-c("train","test")
print(res)

      gini
train   59
test    59

The gini index for train set is very close to test set, it’s a good thing. But the value of the index is bigger than 50%, it’s good result. Our model have a good predictive ability.

This example show how to use Keras API for binary classification and to measure the model performance with AUC.

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/dsaada/CreditRiskTensorFlow/, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".