(assignment for MITx Analytics Edge)
In the lending industry, investors provide loans to borrowers in exchange for the promise of repayment with interest. If the borrower repays the loan, then the lender profits from the interest. However, if the borrower is unable to repay the loan, then the lender loses money. Therefore, lenders face the problem of predicting the risk of a borrower being unable to repay a loan.
To address this problem, we will use publicly available data from LendingClub.com, a website that connects borrowers and investors over the Internet. This dataset represents 9,578 3-year loans that were funded through the LendingClub.com platform between May 2007 and February 2010. The binary dependent variable not.fully.paid indicates that the loan was not paid back in full (the borrower either defaulted or the loan was “charged off,” meaning the borrower was deemed unlikely to ever pay it back).
To predict this dependent variable, we will use the following independent variables available to the investor when deciding whether to fund a loan:
loans <- read.csv('loans.csv')
str(loans)
## 'data.frame': 9578 obs. of 14 variables:
## $ credit.policy : int 1 1 1 1 1 1 1 1 1 1 ...
## $ purpose : Factor w/ 7 levels "all_other","credit_card",..: 3 2 3 3 2 2 3 1 5 3 ...
## $ int.rate : num 0.119 0.107 0.136 0.101 0.143 ...
## $ installment : num 829 228 367 162 103 ...
## $ log.annual.inc : num 11.4 11.1 10.4 11.4 11.3 ...
## $ dti : num 19.5 14.3 11.6 8.1 15 ...
## $ fico : int 737 707 682 712 667 727 667 722 682 707 ...
## $ days.with.cr.line: num 5640 2760 4710 2700 4066 ...
## $ revol.bal : int 28854 33623 3511 33667 4740 50807 3839 24220 69909 5630 ...
## $ revol.util : num 52.1 76.7 25.6 73.2 39.5 51 76.8 68.6 51.1 23 ...
## $ inq.last.6mths : int 0 0 1 1 0 0 0 0 1 1 ...
## $ delinq.2yrs : int 0 0 0 0 1 0 0 0 0 0 ...
## $ pub.rec : int 0 0 0 0 0 0 1 0 0 0 ...
## $ not.fully.paid : int 0 0 0 0 0 0 1 1 0 0 ...
summary(loans)
## credit.policy purpose int.rate
## Min. :0.000 all_other :2331 Min. :0.0600
## 1st Qu.:1.000 credit_card :1262 1st Qu.:0.1039
## Median :1.000 debt_consolidation:3957 Median :0.1221
## Mean :0.805 educational : 343 Mean :0.1226
## 3rd Qu.:1.000 home_improvement : 629 3rd Qu.:0.1407
## Max. :1.000 major_purchase : 437 Max. :0.2164
## small_business : 619
## installment log.annual.inc dti fico
## Min. : 15.67 Min. : 7.548 Min. : 0.000 Min. :612.0
## 1st Qu.:163.77 1st Qu.:10.558 1st Qu.: 7.213 1st Qu.:682.0
## Median :268.95 Median :10.928 Median :12.665 Median :707.0
## Mean :319.09 Mean :10.932 Mean :12.607 Mean :710.8
## 3rd Qu.:432.76 3rd Qu.:11.290 3rd Qu.:17.950 3rd Qu.:737.0
## Max. :940.14 Max. :14.528 Max. :29.960 Max. :827.0
## NA's :4
## days.with.cr.line revol.bal revol.util inq.last.6mths
## Min. : 179 Min. : 0 Min. : 0.00 Min. : 0.000
## 1st Qu.: 2820 1st Qu.: 3187 1st Qu.: 22.70 1st Qu.: 0.000
## Median : 4140 Median : 8596 Median : 46.40 Median : 1.000
## Mean : 4562 Mean : 16914 Mean : 46.87 Mean : 1.572
## 3rd Qu.: 5730 3rd Qu.: 18250 3rd Qu.: 71.00 3rd Qu.: 2.000
## Max. :17640 Max. :1207359 Max. :119.00 Max. :33.000
## NA's :29 NA's :62 NA's :29
## delinq.2yrs pub.rec not.fully.paid
## Min. : 0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.: 0.0000 1st Qu.:0.0000 1st Qu.:0.0000
## Median : 0.0000 Median :0.0000 Median :0.0000
## Mean : 0.1638 Mean :0.0621 Mean :0.1601
## 3rd Qu.: 0.0000 3rd Qu.:0.0000 3rd Qu.:0.0000
## Max. :13.0000 Max. :5.0000 Max. :1.0000
## NA's :29 NA's :29
paste('Loans not paid in full :', signif(sum(loans$not.fully.paid)/length(loans$not.fully.paid)))
## [1] "Loans not paid in full : 0.160054"
no_missing_values <- !is.na(loans$log.annual.inc) & !is.na(loans$days.with.cr.line) & !is.na(loans$revol.util) & !is.na(loans$inq.last.6mths) & !is.na(loans$delinq.2yrs) & !is.na(loans$pub.rec)
loans_no_missing_values <- subset(loans, no_missing_values)
loans_missing_values <- subset(loans, no_missing_values == FALSE)
paste('Number of loans with no missing values :', nrow(loans_no_missing_values))
## [1] "Number of loans with no missing values : 9516"
paste('Number of loans with a missing values :', nrow(loans_missing_values))
## [1] "Number of loans with a missing values : 62"
paste('Loans not paid in full if no missing value :', signif(sum(loans_no_missing_values$not.fully.paid)/length(loans_no_missing_values$not.fully.paid)))
## [1] "Loans not paid in full if no missing value : 0.159836"
paste('Loans not paid in full if a missing value :', signif(sum(loans_missing_values$not.fully.paid)/length(loans_missing_values$not.fully.paid)))
## [1] "Loans not paid in full if a missing value : 0.193548"
library(mice)
set.seed(144)
vars.for.imputation = setdiff(names(loans), "not.fully.paid")
imputed = complete(mice(loans[vars.for.imputation]))
##
## iter imp variable
## 1 1 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 1 2 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 1 3 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 1 4 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 1 5 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 2 1 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 2 2 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 2 3 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 2 4 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 2 5 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 3 1 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 3 2 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 3 3 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 3 4 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 3 5 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 4 1 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 4 2 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 4 3 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 4 4 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 4 5 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 5 1 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 5 2 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 5 3 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 5 4 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
## 5 5 log.annual.inc days.with.cr.line revol.util inq.last.6mths delinq.2yrs pub.rec
# predict missing variable values using the available independent variables for each observation
loans[vars.for.imputation] = imputed
loans_imputed <- read.csv('loans_imputed.csv')
# (imputed using another computer with potentially different settings)
set.seed(144) # fix the random number generator
library(caTools) # provides splitting tools
split = sample.split(loans_imputed$not.fully.paid, SplitRatio = 0.7) # ensure balance of violators in sets
train = subset(loans, split == TRUE)
test = subset(loans, split == FALSE)
train_imputed <- subset(loans_imputed, split == TRUE)
test_imputed <- subset(loans_imputed, split == FALSE)
model_x <- glm(not.fully.paid ~ ., data = train, family = 'binomial')
summary(model_x)
##
## Call:
## glm(formula = not.fully.paid ~ ., family = "binomial", data = train)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.2097 -0.6214 -0.4950 -0.3601 2.6414
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.260e+00 1.554e+00 5.958 2.55e-09 ***
## credit.policy -3.327e-01 1.011e-01 -3.292 0.000995 ***
## purpose2 -6.100e-01 1.344e-01 -4.538 5.67e-06 ***
## purpose3 -3.181e-01 9.185e-02 -3.463 0.000534 ***
## purpose4 1.386e-01 1.753e-01 0.791 0.429074
## purpose5 1.774e-01 1.479e-01 1.199 0.230496
## purpose6 -4.783e-01 2.009e-01 -2.381 0.017260 *
## purpose7 4.159e-01 1.419e-01 2.932 0.003373 **
## int.rate 6.202e-01 2.085e+00 0.297 0.766111
## installment 1.279e-03 2.093e-04 6.110 9.96e-10 ***
## log.annual.inc -4.357e-01 7.151e-02 -6.093 1.11e-09 ***
## dti 4.733e-03 5.501e-03 0.861 0.389508
## fico -9.406e-03 1.707e-03 -5.510 3.60e-08 ***
## days.with.cr.line 3.174e-06 1.587e-05 0.200 0.841463
## revol.bal 3.103e-06 1.169e-06 2.653 0.007966 **
## revol.util 1.796e-03 1.532e-03 1.172 0.241022
## inq.last.6mths 8.386e-02 1.577e-02 5.317 1.06e-07 ***
## delinq.2yrs -7.794e-02 6.532e-02 -1.193 0.232814
## pub.rec 3.191e-01 1.134e-01 2.814 0.004899 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5896.6 on 6704 degrees of freedom
## Residual deviance: 5485.7 on 6686 degrees of freedom
## AIC: 5523.7
##
## Number of Fisher Scoring iterations: 5
model <- glm(not.fully.paid ~ ., data = train_imputed, family = 'binomial')
summary(model)
##
## Call:
## glm(formula = not.fully.paid ~ ., family = "binomial", data = train_imputed)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.2049 -0.6205 -0.4951 -0.3606 2.6397
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.187e+00 1.554e+00 5.910 3.42e-09 ***
## credit.policy -3.368e-01 1.011e-01 -3.332 0.000861 ***
## purposecredit_card -6.141e-01 1.344e-01 -4.568 4.93e-06 ***
## purposedebt_consolidation -3.212e-01 9.183e-02 -3.498 0.000469 ***
## purposeeducational 1.347e-01 1.753e-01 0.768 0.442201
## purposehome_improvement 1.727e-01 1.480e-01 1.167 0.243135
## purposemajor_purchase -4.830e-01 2.009e-01 -2.404 0.016203 *
## purposesmall_business 4.120e-01 1.419e-01 2.905 0.003678 **
## int.rate 6.110e-01 2.085e+00 0.293 0.769446
## installment 1.275e-03 2.092e-04 6.093 1.11e-09 ***
## log.annual.inc -4.337e-01 7.148e-02 -6.067 1.30e-09 ***
## dti 4.638e-03 5.502e-03 0.843 0.399288
## fico -9.317e-03 1.710e-03 -5.448 5.08e-08 ***
## days.with.cr.line 2.371e-06 1.588e-05 0.149 0.881343
## revol.bal 3.085e-06 1.168e-06 2.641 0.008273 **
## revol.util 1.839e-03 1.535e-03 1.199 0.230722
## inq.last.6mths 8.437e-02 1.600e-02 5.275 1.33e-07 ***
## delinq.2yrs -8.320e-02 6.561e-02 -1.268 0.204762
## pub.rec 3.300e-01 1.139e-01 2.898 0.003756 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 5896.6 on 6704 degrees of freedom
## Residual deviance: 5485.2 on 6686 degrees of freedom
## AIC: 5523.2
##
## Number of Fisher Scoring iterations: 5
Consider two loan applications, which are identical other than the fact that the borrower in Application A has FICO credit score 700 while the borrower in Application B has FICO credit score 710.
Let Logit(A) be the log odds of loan A not being paid back in full, according to our logistic regression model, and define Logit(B) similarly for loan B. What is the value of Logit(A) - Logit(B)?
logit <- model$coefficients['fico'][[1]]*(700-710)
paste("Value of Logit(A)-Logit(B) =", signif(logit))
## [1] "Value of Logit(A)-Logit(B) = 0.0931679"
paste("Odds ratio of A not being paid back, compared to B =", signif(exp(logit)))
## [1] "Odds ratio of A not being paid back, compared to B = 1.09765"
Where threshold = 0.5
predicted.risk <- predict(model, newdata = test_imputed, type = 'response')
test_imputed$predicted.risk <- predicted.risk
accuracy.table <- table(test_imputed$not.fully.paid, predicted.risk >= 0.5)
accuracy.table
##
## FALSE TRUE
## 0 2400 13
## 1 457 3
paste('Accuracy of logistic regression model :',
signif((accuracy.table['0','FALSE'] + accuracy.table['1','TRUE']) / sum(accuracy.table[,])))
## [1] "Accuracy of logistic regression model : 0.836408"
paste('Accuracy of baseline model :',
signif(max(sum(test$not.fully.paid == TRUE), sum(test$not.fully.paid == FALSE))/nrow(test)))
## [1] "Accuracy of baseline model : 0.839889"
library(ROCR)
pred_ROCR <- prediction(predicted.risk, test$not.fully.paid)
auc_ROCR <- performance(pred_ROCR, measure = 'auc')
plot(performance(pred_ROCR, measure = 'tpr', x.measure = 'fpr'), colorize = TRUE,
print.cutoffs.at = seq(0, 1, 0.1), text.adj = c(-0.2, 1.7))
paste('Area under Curve :', signif(auc_ROCR@y.values[[1]]))
## [1] "Area under Curve : 0.672099"
We built a logistic regression model that has an AUC significantly higher than the AUC of 0.5 that would be obtained by randomly ordering observations.
However, LendingClub.com assigns the interest rate to a loan based on their estimate of that loan’s risk. This variable, int.rate, is an independent variable in our dataset. In this part, we will investigate using the loan’s interest rate as a “smart baseline” to order the loans according to risk.
A bivariate logistic regression model (aka a logistic regression model with a single independent variable) that predicts the dependent variable not.fully.paid using only the variable int.rate.
baseline.model <- glm(not.fully.paid ~ int.rate, data = train_imputed, family = 'binomial')
summary(baseline.model)
##
## Call:
## glm(formula = not.fully.paid ~ int.rate, family = "binomial",
## data = train_imputed)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -1.0547 -0.6271 -0.5442 -0.4361 2.2914
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.6726 0.1688 -21.76 <2e-16 ***
## int.rate 15.9214 1.2702 12.54 <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: 5896.6 on 6704 degrees of freedom
## Residual deviance: 5734.8 on 6703 degrees of freedom
## AIC: 5738.8
##
## Number of Fisher Scoring iterations: 4
The variable int.rate is highly significant in the bivariate model, but it is not significant at the 0.05 level in the model trained with all the independent variables. The most likely explanation for this difference is that int.rate is correlated with other risk-related variables, and therefore does not incrementally improve the model when those other variables are included.
baseline.model.test.predictions <- predict(baseline.model, newdata = test_imputed, type = 'response')
paste('Highest predicted probability of loan not being paid in full on testing set', signif(max(baseline.model.test.predictions)))
## [1] "Highest predicted probability of loan not being paid in full on testing set 0.426624"
paste('With logistic regression cutoff of 0.5, the number of loans not being paid in full on the testing set :', sum(baseline.model.test.predictions >= 0.5))
## [1] "With logistic regression cutoff of 0.5, the number of loans not being paid in full on the testing set : 0"
pred_ROCR <- prediction(baseline.model.test.predictions, test_imputed$not.fully.paid)
auc_ROCR <- performance(pred_ROCR, measure = 'auc')
plot(performance(pred_ROCR, measure = 'tpr', x.measure = 'fpr'), colorize = TRUE,
print.cutoffs.at = seq(0, 1, 0.1), text.adj = c(-0.2, 1.7))
paste('Area under Curve :', signif(auc_ROCR@y.values[[1]]))
## [1] "Area under Curve : 0.623908"
Assume a $1 investment (aka c=1). To create the variable, we first assign to the profit for a fully paid loan, exp(rt)-1, to every observation, and we then replace this value with -1 in the cases where the loan was not paid in full. All the loans in our dataset are 3-year loans, meaning t=3 in our calculations.
test_imputed$profit <- exp(test_imputed$int.rate*3)-1
test_imputed$profit[test_imputed$not.fully.paid == 1] = -1
paste('Maximum profit of a $10 investment in any loan in the testing set:',
signif(max(test_imputed$profit)*10))
## [1] "Maximum profit of a $10 investment in any loan in the testing set: 8.89477"
Investors seek loans that balance reward with risk, in that they simultaneously have high interest rates and a low risk of not being paid back.
We will analyze an investment strategy in which the investor only purchases loans with a high interest rate (a rate of at least 15%), but amongst these loans selects the ones with the lowest predicted risk of not being paid back in full. We will model an investor who invests $1 in each of the most promising 100 loans.
highinterest <- subset(test_imputed, test_imputed$int.rate >= 0.15)
paste('Average profit of a $1 investment in one of these high-interest loans :',
signif(mean(highinterest$profit)))
## [1] "Average profit of a $1 investment in one of these high-interest loans : 0.225102"
paste('Proportion of high-interest loans not paid back in full:',
signif(mean(highinterest$not.fully.paid)))
## [1] "Proportion of high-interest loans not paid back in full: 0.251716"
We will determine the 100th smallest predicted probability of not paying in full by sorting the predicted risks in increasing order and selecting the 100th element of this sorted list.
We then build a data frame called selectedloans consisting of the 100 lowest risk high-interest loans.
cutoff <- sort(highinterest$predicted.risk, decreasing=FALSE)[100]
selectedloans <- subset(highinterest, highinterest$predicted.risk <= cutoff)
paste("Profit of the investor who invested $1 each of these 100 'low-risk' high-interest loans :",
signif(sum(selectedloans$profit)))
## [1] "Profit of the investor who invested $1 each of these 100 'low-risk' high-interest loans : 31.2782"
paste("Number of these 100 'low-risk' high-interest loans not paid back in full:",
sum(selectedloans$not.fully.paid))
## [1] "Number of these 100 'low-risk' high-interest loans not paid back in full: 19"
We have now seen how analytics can be used to select a subset of the high-interest loans that were paid back at only a slightly lower rate than average, resulting in a significant increase in the profit from our investor’s $100 investment. Although the logistic regression models developed in this problem did not have large AUC values, we see that they still provided the edge needed to improve the profitability of an investment portfolio.
We conclude with a note of warning. Throughout this analysis we assume that the loans we invest in will perform in the same way as the loans we used to train our model, even though our training set covers a relatively short period of time. If there is an economic shock like a large financial downturn, default rates might be significantly higher than those observed in the training set and we might end up losing money instead of profiting. Investors must pay careful attention to such risk when making investment decisions.