Credit Risk Assessment

Translated from the original Portuguese article. Ler o original em português

Cover of the article Credit Risk Assessment

Figure 1 of the article Credit Risk Assessment

On a sunny day in the middle of the Brazilian semi-arid region, a representative of a financial institution, more specifically Fintech λλ, contacted me by email, requesting a service to build a solution that would improve their customer credit risk assessment model.

Among the areas that make up these companies, one of those that causes the most problems and discomfort is the default rate across customer portfolios. This makes portfolio assessment a mandatory and extremely important activity, since defaults can cause enormous deficits on the balance sheet.

So I went digging and found out that, for these institutions, each customer is part of a credit portfolio, which indicates how likely customers are to be financially able to repay their debt (loans, credit cards and others); this portfolio assessment is reported as a ranking.

Because of this, these institutions are investing more and more in new technologies to develop and improve their assessment systems, always aiming to minimize the risk of default or of non-compliance with the obligations and/or conditions of a loan, and betting increasingly on Machine Learning models.

Countless solutions could be built to address these problems. In this project, I will create an algorithm to identify the probability that a customer will fail to meet their financial obligations. Note that this whole story is a fiction I created, and this work is one of the hands-on projects of Carlos Melo's Data Science na Prática course.

To start the work, I needed to evaluate the database provided by the institution. It is worth noting that these assessments are usually done when the customer applies for the card (typically at the first contact with the institution); however, we will use an existing database (Dataset). This dataset has 45,000 entries and 43 columns and is basically a CSV file.

ids target_default score_1 score_2 score_3 score_4 score_5 score_6 risk_rate last_amount_borrowed last_borrowed_in_months credit_limit reason income facebook_profile state zip channel job_name real_state ok_since n_bankruptcies n_defaulted_loans n_accounts n_issues application_time_applied application_time_in_funnel email external_data_provider_credit_checks_last_2_year external_data_provider_credit_checks_last_month external_data_provider_credit_checks_last_year external_data_provider_email_seen_before external_data_provider_first_name external_data_provider_fraud_score lat_lon marketing_channel profile_phone_number reported_income shipping_state shipping_zip_code profile_tags user_agent target_fraud
0 343b7e7b-2cf8-e508-b8fd-0a0285af30aa False 1Rk8w4Ucd5yR3KcqZzLdow== IOVu8au3ISbo6+zmfnYwMg== 350.0 101.800832 0.259555 108.427273 0.40 25033.92 36.0 0.0 mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... 65014.12 True sjJbkqJS7cXalHLBFA+EOQ== Ernn+uVXCMq/6ARrBCcd+A== NCqL3QBx0pscDnx3ixKwXg== mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... N5/CE7lSkAfB04hVFFwllw== NaN 0.0 0.0 18.0 18.0 07:52:34 444 outlook.com NaN 2 0.0 51.0 leidelaura 645 (-29.151545708122246, -51.1386461804385) Invite-email 514-9840782 57849.0 BR-MT 17528 {'tags': ['n19', 'n8']} Mozilla/5.0 (Linux; Android 6.0.1; SGP771 Buil... NaN
1 bc2c7502-bbad-0f8c-39c3-94e881967124 False DGCQep2AE5QRkNCshIAlFQ== SaamrHMo23l/3TwXOWgVzw== 370.0 97.062615 0.942655 92.002546 0.24 NaN NaN 39726.0 mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... 100018.91 False xsd3ZdsI3356I3xMxZeiqQ== rlWIXTBO+VOa34+SpGyhlQ== NCqL3QBx0pscDnx3ixKwXg== mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... N5/CE7lSkAfB04hVFFwllw== 50.0 0.0 0.0 14.0 14.0 02:34:29 346 gmail.com 0.0 1 0.0 17.0 diocezio 243 (-19.687710705798963, -47.94151536525154) Radio-commercial 251-3659293 4902.0 BR-RS 40933 {'tags': ['n6', 'n7', 'nim']} Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-... NaN
2 669630dd-2e6a-0396-84bf-455e5009c922 True DGCQep2AE5QRkNCshIAlFQ== Fv28Bz0YRTVAT5kl1bAV6g== 360.0 100.027073 0.351918 112.892453 0.29 7207.92 36.0 NaN mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... 65023.65 NaN Ygq6MsM98oC8yceExr69Ig== PjTIDfJsK0DKL9fO7vuW2g== NCqL3QBx0pscDnx3ixKwXg== mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... n+xK9CfX0bCn77lClTWviw== NaN 0.0 0.0 10.0 NaN 00:60:02 6 gmail.com NaN 2 NaN 9.0 veralucia 65 (-28.748023890412284, -51.867279334353995) Waiting-list 230-6097993 163679.0 BR-RR 50985 {'tags': ['n0', 'n17', 'nim', 'da']} Mozilla/5.0 (Linux; Android 6.0.1; SGP771 Buil... NaN
3 d235609e-b6cb-0ccc-a329-d4f12e7ebdc1 False 1Rk8w4Ucd5yR3KcqZzLdow== dCm9hFKfdRm7ej3jW+gyxw== 510.0 101.599485 0.987673 94.902491 0.32 NaN NaN 54591.0 mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... 68830.01 False KET/Pmr6rHp1RJ/P9ymztw== Cc/kWDLQH3dpHv5HU+pLVA== NCqL3QBx0pscDnx3ixKwXg== mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiE56f... n+xK9CfX0bCn77lClTWviw== NaN 1.0 0.0 19.0 19.0 11:20:49 406 spgov.com NaN 3 NaN 38.0 venice 815 (-17.520650158450454, -39.75801139933186) Waiting-list 261-3543751 1086.0 BR-RN 37825 {'tags': ['n4']} Mozilla/5.0 (Linux; Android 6.0; HTC One X10 B... NaN
4 9e0eb880-e8f4-3faa-67d8-f5cdd2b3932b False 8k8UDR4Yx0qasAjkGrUZLw== +CxEO4w7jv3QPI/BQbyqAA== 500.0 98.474289 0.532539 118.126207 0.18 NaN NaN NaN mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... 60011.29 True xsd3ZdsI3356I3xMxZeiqQ== i036nmJ7rfxo+3EvCD7Jnw== NCqL3QBx0pscDnx3ixKwXg== mLVIVxoGY7TUDJ1FyFoSIZi1SFcaBmO01AydRchaEiGYtU... n+xK9CfX0bCn77lClTWviw== NaN 0.0 0.0 11.0 NaN 13:39:03 240 gmail.com 0.0 2 1.0 46.0 darzisa 320 (-16.574259446978008, -39.90990074785962) Invite-email 102-3660162 198618.0 BR-MT 52827 {'tags': ['pro+aty', 'n19', 'da', 'b19']} Mozilla/5.0 (Linux; Android 7.0; Pixel C Build... NaN

In this project, it is extremely important to understand the meaning of each variable used in the work, which can be seen below:

  • id - anonymous identifier, a unique value per customer.
  • target_default - the target variable, which we will use to analyze the risk of default.
  • The columns score_3, score_4 and score_5 are numeric, while score_1 and score_2 are encoded in some way. We will have to check later whether they have a number of classes that can be converted into useful information.
  • There are other variables with some kind of encoding, such as reason, state, zip, channel, job_name and real_state, which will also need deeper analysis to determine whether any information can be extracted from them.
  • profile_tags - contains a dictionary with a label assigned to each customer.
  • target_fraud - would be the target variable for another model, where the goal would be fraud detection.
  • lat_lon - stored as a string containing a tuple with the coordinates.

As the first phase of any project, I will start with exploratory data analysis, in this order: missing values, data types, unique values, data balance and descriptive statistics.

Running this calculation, we can see that some columns, such as target_fraud, last_amount_borrowed, last_borrowed_in_months, ok_since and external_data_provider_credit_checks_last_2_year, have more than half of their data missing. The columns external_data_provider_credit_checks_last_year, credit_limit and n_issues have between 25% and 34% of their values missing. In addition, the unique value count shows that the columns external_data_provider_credit_checks_last_2_year and channel have a single possible value, so they cannot contribute to the model and can be dropped.

Next, when I analyzed the balance of our target variable target_default, the proportion of defaulters was False 77.95% and True 14.80%, so balancing will be needed later on.

Figure 2 of the article Credit Risk Assessment

score_3 score_4 score_5 score_6 risk_rate last_amount_borrowed last_borrowed_in_months credit_limit income ok_since n_bankruptcies n_defaulted_loans n_accounts n_issues application_time_in_funnel external_data_provider_credit_checks_last_2_year external_data_provider_credit_checks_last_month external_data_provider_credit_checks_last_year external_data_provider_email_seen_before external_data_provider_fraud_score reported_income shipping_zip_code
count 44438.000000 45000.000000 45000.000000 45000.000000 44438.000000 15044.000000 15044.000000 31200.000000 4.443800e+04 18455.000000 44303.000000 44426.000000 44438.000000 33456.000000 45000.000000 22372.0 45000.000000 29876.000000 42767.000000 45000.000000 45000.00 45000.000000
mean 347.983258 100.006797 0.500037 99.925228 0.296717 13782.341991 41.763893 34381.951154 7.160739e+04 35.069737 0.076270 0.004569 10.696701 11.068119 247.537000 0.0 1.501067 0.503381 12.534781 500.399800 inf 33923.514844
std 110.429165 3.178080 0.288326 10.016549 0.101933 8077.571933 10.252705 36586.982218 5.185897e+04 21.663194 0.274052 0.079400 4.592463 4.595501 146.118398 0.0 1.114941 0.499997 126.530487 287.968873 NaN 12692.406382
min 0.000000 86.191572 0.000035 60.663039 0.000000 1005.180000 36.000000 0.000000 4.821180e+03 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 0.000000 0.000000 -999.000000 0.000000 403.00 12001.000000
25% 270.000000 97.867036 0.252150 93.198353 0.220000 7519.502500 36.000000 10000.000000 4.501078e+04 17.000000 0.000000 0.000000 7.000000 8.000000 120.000000 0.0 1.000000 0.000000 11.000000 252.000000 50948.25 22937.000000
50% 340.000000 100.018900 0.500753 99.974796 0.290000 12023.465000 36.000000 25647.000000 6.126676e+04 32.000000 0.000000 0.000000 10.000000 10.000000 248.000000 0.0 2.000000 1.000000 27.000000 503.000000 101779.00 33896.000000
75% 420.000000 102.141797 0.748816 106.640275 0.360000 19220.270000 36.000000 47086.750000 8.601923e+04 50.000000 0.000000 0.000000 13.000000 14.000000 375.000000 0.0 2.000000 1.000000 43.000000 747.000000 151337.25 44910.000000
max 990.000000 113.978234 0.999973 142.192400 0.900000 35059.600000 60.000000 448269.000000 5.000028e+06 141.000000 5.000000 5.000000 49.000000 49.000000 500.000000 0.0 3.000000 1.000000 59.000000 1000.000000 inf 56000.000000

Finally, from the main descriptive statistics shown, we can highlight a few observations:

  • The column external_data_provider_credit_checks_last_2_year has minimum, maximum and standard deviation values all equal to zero.
  • The column reported_income has inf values, which will interfere with the analysis and the model. We will replace np.inf values with np.nan to work with the data.
  • The column external_data_provider_email_seen_before has a minimum value of -999, which is odd given the other information. After a closer look, I concluded that these values are outliers or were improperly handled. We will replace values equal to -999 with np.nan.

With most of the exploratory analysis done, the next step is to carry out the manipulations needed to build the best model, so we can now start preprocessing the data.

To do so, I performed the following cleaning steps:

  • Replace inf with NaN in the reported_income column;
  • Drop the columns ids, target_fraud, external_data_provider_credit_checks_last_2_year, channel and profile_phone_number;
  • Replace -999 in external_data_provider_email_seen_before with NaN;
  • Drop columns with no apparent information or that require further research;
  • Drop the entries where target_default is NaN.

With this first round of preprocessing done by analyzing the variables, I could then start preprocessing the entries. This dataset has a lot of missing and null data, but we do not have much information about the reasons behind these problems. To address this, I will replace the missing values (NaN) with zero for variables that have no reference, such as 'last_amount_borrowed', 'last_borrowed_in_months' and 'n_issues'. For the rest, I will use the median for numeric variables and the mode for categorical ones.

With all of this preprocessing done, the dataset looked like this:

target_default score_1 score_2 score_3 score_4 score_5 score_6 risk_rate last_amount_borrowed last_borrowed_in_months credit_limit income facebook_profile real_state ok_since n_bankruptcies n_defaulted_loans n_accounts n_issues application_time_in_funnel external_data_provider_credit_checks_last_month external_data_provider_credit_checks_last_year external_data_provider_email_seen_before external_data_provider_fraud_score reported_income
0 0 1Rk8w4Ucd5yR3KcqZzLdow== IOVu8au3ISbo6+zmfnYwMg== 350.0 101.800832 0.259555 108.427273 0.40 25033.92 36.0 0.0 65014.12 True N5/CE7lSkAfB04hVFFwllw== 32.0 0.0 0.0 18.0 18.0 444.0 2.0 0.0 51.0 645.0 57849.0
1 0 DGCQep2AE5QRkNCshIAlFQ== SaamrHMo23l/3TwXOWgVzw== 370.0 97.062615 0.942655 92.002546 0.24 0.00 0.0 39726.0 100018.91 False N5/CE7lSkAfB04hVFFwllw== 50.0 0.0 0.0 14.0 14.0 346.0 1.0 0.0 17.0 243.0 4902.0
2 1 DGCQep2AE5QRkNCshIAlFQ== Fv28Bz0YRTVAT5kl1bAV6g== 360.0 100.027073 0.351918 112.892453 0.29 7207.92 36.0 25213.0 65023.65 False n+xK9CfX0bCn77lClTWviw== 32.0 0.0 0.0 10.0 0.0 6.0 2.0 1.0 9.0 65.0 163679.0
3 0 1Rk8w4Ucd5yR3KcqZzLdow== dCm9hFKfdRm7ej3jW+gyxw== 510.0 101.599485 0.987673 94.902491 0.32 0.00 0.0 54591.0 68830.01 False n+xK9CfX0bCn77lClTWviw== 32.0 1.0 0.0 19.0 19.0 406.0 3.0 1.0 38.0 815.0 1086.0
4 0 8k8UDR4Yx0qasAjkGrUZLw== +CxEO4w7jv3QPI/BQbyqAA== 500.0 98.474289 0.532539 118.126207 0.18 0.00 0.0 25213.0 60011.29 True n+xK9CfX0bCn77lClTWviw== 32.0 0.0 0.0 11.0 0.0 240.0 2.0 1.0 46.0 320.0 198618.0

This left us with 41,741 entries and 25 columns. Note that most of the columns are encrypted, so I will use the LabelEncoder and GetDummies algorithms to transform the categorical data into numerical data, splitting out those with more than two categories.

The result was a dataset like this:

target_default score_3 score_4 score_5 score_6 risk_rate last_amount_borrowed last_borrowed_in_months credit_limit income facebook_profile ok_since n_bankruptcies n_defaulted_loans n_accounts n_issues application_time_in_funnel external_data_provider_credit_checks_last_month external_data_provider_credit_checks_last_year external_data_provider_email_seen_before external_data_provider_fraud_score reported_income score_1_0 score_1_1 score_1_2 score_1_3 score_1_4 score_1_5 score_1_6 score_2_0 score_2_1 score_2_2 score_2_3 score_2_4 score_2_5 score_2_6 score_2_7 score_2_8 score_2_9 score_2_10 score_2_11 score_2_12 score_2_13 score_2_14 score_2_15 score_2_16 score_2_17 score_2_18 score_2_19 score_2_20 score_2_21 score_2_22 score_2_23 score_2_24 score_2_25 score_2_26 score_2_27 score_2_28 score_2_29 score_2_30 score_2_31 score_2_32 score_2_33 score_2_34 real_state_0 real_state_1 real_state_2 real_state_3 real_state_4
0 0 350.0 101.800832 0.259555 108.427273 0.40 25033.92 36.0 0.0 65014.12 1 32.0 0.0 0.0 18.0 18.0 444.0 2.0 0.0 51.0 645.0 57849.0 1 0 0 0 0 0 0 0 0 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 0 0 0 0 0 0 0 0 1 0 0 0
1 0 370.0 97.062615 0.942655 92.002546 0.24 0.00 0.0 39726.0 100018.91 0 50.0 0.0 0.0 14.0 14.0 346.0 1.0 0.0 17.0 243.0 4902.0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 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 0 0 1 0 0 0
2 1 360.0 100.027073 0.351918 112.892453 0.29 7207.92 36.0 25213.0 65023.65 0 32.0 0.0 0.0 10.0 0.0 6.0 2.0 1.0 9.0 65.0 163679.0 0 0 0 1 0 0 0 0 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 0 0 0 0 0 0 0 0 0 0 0 1 0
3 0 510.0 101.599485 0.987673 94.902491 0.32 0.00 0.0 54591.0 68830.01 0 32.0 1.0 0.0 19.0 19.0 406.0 3.0 1.0 38.0 815.0 1086.0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 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 1 0
4 0 500.0 98.474289 0.532539 118.126207 0.18 0.00 0.0 25213.0 60011.29 1 32.0 0.0 0.0 11.0 0.0 240.0 2.0 1.0 46.0 320.0 198618.0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

As we saw earlier, the target data is heavily imbalanced, so some manipulation was needed, resulting in a boolean split of False 35,080 and True 35,025.

Figure 3 of the article Credit Risk Assessment

With all the manipulations done, I searched the literature and looked at other data scientists' work to find which model best fits this type of problem. Through a systematic search, I found that the best model for this problem, or rather the most commonly used, is RandomForest (RF). Then, to identify the best RF model, I created a validation function that uses the Recall metric, establishing a general baseline of how much the model could err and thereby confirming that it would be the best learning model to use.

#Função de validação

def valid(X, y, model, quite = False):

  X = np.array(X)
  y = np.array(y)
  pipeline = make_pipeline(StandardScaler(), model)
  scores = cross_val_score(pipeline, X, y, scoring="recall")

  if quite == False:
     print("Recall: {:.3f} (+/- {:.3f})".format(scores.mean(), scores.std()))

  return scores.mean()
rfc = RandomForestClassifier()
score_baseline = valid(X_treino_balanceado, y_treino_balanceado, rfc)

This yielded Recall: 0.967 (+/- 0.003), which allowed me to proceed with the RF algorithm and start testing with GridSearchCV to identify the best parameters, arriving at the following final model.

forest = RandomForestClassifier(criterion='entropy', n_estimators=100)
forest.fit(X_treino_balanceado, y_treino_balanceado)
RandomForestClassifier(bootstrap=True, ccp_alpha=0.0, class_weight=None,
                       criterion='entropy', max_depth=None, max_features='auto',
                       max_leaf_nodes=None, max_samples=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, n_estimators=100,
                       n_jobs=None, oob_score=False, random_state=None,
                       verbose=0, warm_start=False)

With the model ready, I could then make predictions and compare them with the results of the test set, obtaining an excellent result, as you can see below:

              precision    recall  f1-score   support

           0       0.99      0.96      0.97      8772
           1       0.96      0.99      0.97      8755

    accuracy                           0.97     17527
   macro avg       0.97      0.97      0.97     17527
weighted avg       0.97      0.97      0.97     17527

Área sobre a curva ASC: 0.971373390177927

The confusion matrix shows 96% True Positives and 99% True Negatives.

Figure 4 of the article Credit Risk Assessment

In this project, I carried out several procedures to build a credit risk prediction model, which required a number of manipulations, from dropping columns to balancing and standardization. The result was extremely satisfactory for the client and could be put into production. However, there is still a gap: trying other learning models for this task to determine whether RandomForest is in fact the best for this problem. You can see the full project in the notebook.

For Fintech λλ, the result was indeed very satisfactory, since it could improve their assessment service. We can see this with a simple example: for a client with a credit portfolio of 1 million reais, my model could ensure that the company would receive 990 thousand from the loans, erring in only 1% of cases. This makes the company's financial risk acceptable, as it would keep its credit portfolio in a very healthy state, with a default rate well below the national averages.

Did you like this kind of structure for the blog articles? Send me your feedback on my social media or by email, and if you enjoyed it, share it to support the work. :)

Keep reading

All articles

Credit Card Fraud Detection

Credit card fraud is one of the main concerns of financial institutions. In this post, I explore a dataset of European card transactions and build a …