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

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 | 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.

| 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.

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.

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. :)