Loan Prediction Using Machine Learning: A Step-by-Step Guide
Predicting whether a loan applicant is likely to repay a loan or default is a critical business problem for banks and financial institutions. By accurately assessing the risk of loan default, lenders can make smarter decisions around which loans to approve and what interest rates to charge to maximize profits and minimize losses.
In this guide, we will walk through how to build a machine learning model to predict loan default risk from scratch using Python. By the end of this article, you will understand the key steps involved and best practices you can use to train a high performance loan prediction model. Even if you are new to machine learning, this guide will break down the concepts in an intuitive manner, while more advanced readers will pick up some useful tips and code snippets to power their own models.
We will cover:
- Exploratory Data Analysis
- Data Cleaning
- Feature Engineering
- Model Building, Evaluation and Tuning
- Interpreting Feature Importance
- Ideas for Further Improvement
Let‘s dive in!
The Dataset
We will be using a synthetic dataset that closely mirrors real-world loan data. It contains the following fields:
– Loan_ID: A unique identifier for each loan application
– Gender: Male/Female
– Married: Applicant‘s marital status (Y/N)
– Dependents: Number of dependents
– Education: Graduate/Under Graduate
– Self_Employed: Y/N
– ApplicantIncome: The primary applicant‘s income
– CoapplicantIncome: The co-applicant‘s income
– LoanAmount: The loan amount requested
– Loan_Amount_Term: The loan repayment term requested
– Credit_History: Whether the applicant has a good credit history (1) or not (0)
– Property_Area: Urban/Semi Urban/Rural
– Loan_Status: The target variable. Loan approved (Y) or not (N)
Exploratory Data Analysis
Let‘s start by reading in the data and taking a look at the first few rows:
import pandas as pd
data = pd.read_csv(‘loan_data.csv‘)
data.head()
Next, we can check the shape of the data and see if there are any missing values:
print(data.shape)
print(data.isnull().sum())
We have 614 rows and 13 columns. There are some missing values that we will need to deal with. Before that, let‘s create a quick summary of the numeric and categorical columns:
data.describe()
data.describe(include=‘O‘)
Already, we can start to form some hypotheses around which factors might impact loan approval:
- Applicants with higher incomes should have a better chance of approval
- Applicants with a credit history should have a better chance of approval
- Applicants with high dependents might have a harder time getting approved
- Loan amount relative to income likely plays a role in approval
We can test these hypotheses visually through creating some univariate and bivariate plots. Here is a plot of the applicant income distribution, segmented by loan approval status:
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10,6))
sns.histplot(data, x=‘ApplicantIncome‘, hue=‘Loan_Status‘, bins=30)
plt.title(‘Applicant Income Distribution by Loan Status‘)
plt.show()
There is a longer tail of high income applicants getting approved, which supports our hypothesis. We can run similar analyses for the other variables and build out more nuanced hypotheses that we can use to inform our feature engineering later on.
Data Cleaning
Now that we have a solid understanding of the data, let‘s deal with those pesky missing values. Here are a few different techniques we can use:
- Dropping rows with any missing values
- Imputing missing values based on the mean/median/mode
- Imputing missing values based on other correlated features
- Creating a separate category for missing values
Each method has its own tradeoffs. Dropping rows is the simplest but we lose potentially valuable data. Imputation maintains our sample size but can introduce bias if our imputation method is flawed. Adding a missing category can be useful when the fact that a value is missing has predictive power.
For our data, we will use the following:
- For Gender and Married: Impute with mode
- For Credit_History: Impute with mode
- For Self_Employed: Add a missing category
- For LoanAmount: Impute with median
- For Loan_Amount_Term: Impute with median
data[‘Gender‘] = data[‘Gender‘].fillna(data[‘Gender‘].mode()[0])
data[‘Married‘] = data[‘Married‘].fillna(data[‘Married‘].mode()[0])
data[‘Credit_History‘] = data[‘Credit_History‘].fillna(data[‘Credit_History‘].mode()[0])
data[‘Self_Employed‘] = data[‘Self_Employed‘].fillna(‘Missing‘)
data[‘LoanAmount‘] = data[‘LoanAmount‘].fillna(data[‘LoanAmount‘].median())
data[‘Loan_Amount_Term‘] = data[‘Loan_Amount_Term‘].fillna(data[‘Loan_Amount_Term‘].median())
Feature Engineering
With our data cleaned, we can now focus on creating some new variables that will improve the predictive power of our models. Here are a few ideas:
- Total Income: Combining applicant and co-applicant income
- Income to Loan Ratio: Sizing the loan amount relative to total income
- Credit History Category: Segmenting credit history into good/bad/missing
- Property Area Category: Grouping the property areas
data[‘TotalIncome‘] = data[‘ApplicantIncome‘] + data[‘CoapplicantIncome‘]
data[‘IncomeToLoanRatio‘] = data[‘TotalIncome‘] / data[‘LoanAmount‘]
data[‘CreditHistoryCategory‘] = data[‘Credit_History‘].map({1.0: ‘Good‘, 0.0: ‘Bad‘, numpy.nan: ‘Missing‘})
data[‘PropertyAreaCategory‘] = data[‘Property_Area‘].map({‘Urban‘: ‘Urban‘, ‘Semiurban‘: ‘NonUrban‘, ‘Rural‘: ‘NonUrban‘})
These are just a few possibilities. Feature engineering is a mixture of domain knowledge and trial-and-error. The key is using your understanding of the problem and the relationships you uncovered during EDA to design predictive features that align with your hypotheses.
Baseline Model
Now for the fun part – building our first model! Let‘s start with a simple logistic regression to get a baseline performance benchmark:
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
X = data.drop([‘Loan_Status‘,‘Loan_ID‘], axis=1)
y = data[‘Loan_Status‘]
X = pd.get_dummies(X) # One-hot encode categorical variables
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = LogisticRegression()
model.fit(X_train, y_train)
preds = model.predict(X_test)
print(accuracy_score(y_test, preds))
We get 80% accuracy right out of the box, which is not bad! However, accuracy isn‘t always the best metric for imbalanced classification problems like this. Let‘s take a look at the confusion matrix:
from sklearn.metrics import confusion_matrix
print(confusion_matrix(y_test, preds))
We can calculate additional metrics like precision, recall, and F1 score:
from sklearn.metrics import precision_score, recall_score, f1_score
print(precision_score(y_test, preds))
print(recall_score(y_test, preds))
print(f1_score(y_test, preds))
Our recall is quite high, meaning we are correctly identifying most of the loan defaults. However, precision is a bit lower – we are flagging more loans as defaults than is actually the case. Depending on the business context, we may prefer a model with higher recall or precision.
Model Tuning
Now that we have a baseline model, let‘s see if we can improve it through hyperparameter tuning. We can use grid search to systematically try different combinations of parameters:
from sklearn.model_selection import GridSearchCV
param_grid = {‘C‘: [0.01, 0.1, 1, 10],
‘solver‘: [‘liblinear‘, ‘lbfgs‘]}
grid = GridSearchCV(LogisticRegression(), param_grid, cv=5)
grid.fit(X_train, y_train)
print(grid.bestparams)
print(grid.bestscore)
The ideal parameters are a C of 1 and using the lbfgs solver. Let‘s evaluate this tuned model on the test set:
model = LogisticRegression(C=1, solver=‘lbfgs‘)
model.fit(X_train, y_train)
preds = model.predict(X_test)
print(confusion_matrix(y_test, preds))
print(f1_score(y_test, preds))
Performance improved a bit. We could keep iterating by engineering more features, trying other algorithms like random forests or XGBoost, and tuning those models.
Feature Importance
As a final step, let‘s take a look at which features had the biggest impact on our model‘s predictions. We can use the coefficients of the logistic regression to gauge feature importance:
features = X.columns
importances = model.coef_[0]
feat_imp = pd.Series(importances, index=features).sort_values(ascending=False)
feat_imp.plot(kind=‘bar‘, figsize=(10,6))
plt.title(‘Feature Importances‘)
plt.show()
The most important features are:
- Credit history
- Total income
- Loan amount
- Loan to income ratio
This matches many of our initial hypotheses and gives us confidence that our model is picking up on real signals. If we wanted to collect more data or design more features, we now know which areas to focus on to get the most bang for our buck.
Conclusion and Next Steps
In this guide, we walked through the key steps for building a loan default prediction model:
- Exploratory data analysis
- Data cleaning
- Feature engineering
- Establishing a baseline model
- Hyperparameter tuning
- Extracting feature importances
We were able to achieve a reasonably high-performing model and draw insights around the key drivers of loan approval or rejection.
Some ideas to improve the model further:
- Engineer features around applicant demographics and how they relate to defaulting
- Combine this tabular data with text data like applicant reviews or qualitative assessments
- Build separate models for different loan types or applicant profiles
- Rather than just predicting default, predict risk tiers or default probability
- Explore more advanced techniques like ensembling or neural networks
The most important thing is to use a systematic, hypothesis-driven approach and let the data guide your decisions. By following the process laid out here, you‘ll be well equipped to tackle loan default modeling and a wide range of other classification problems. Good luck!