Análise de Crédito Bancário - Bank Credit Analytics¶

Análise de inadimplência de crédito para uma instituição financeira.

Esse é um projeto de classificação.

Disponível também em meu github.

O conjunto de dados utilizado está disponível neste link.

1. Entendimento do Negócio¶

A ProfitCard é uma instituição financeira em expansão, que fornece empréstimos e financiamentos a seus clientes. Desde a sua fundação, a empresa sempre adotou uma cultura orientada a dados visando sempre otimizar seus resultados e tornar a experiência de seus clientes muito mais completa.

Devido ao grande aumento no volume de solicitações de crédito, a empresa decidiu iniciar um novo projeto de ciência de dados com o objetivo de implementar um programa de aconselhamento aos clientes mais propensos a inadimplir. Nesse programa, o principal objetivo é fornecer um aconselhamento individualizado para o titular da conta de modo a encorajá-lo a acertar suas pendências.

Logo, essa análise tem como objetivo identificar quais clientes possuem as maiores probabilidades de serem contatados e consequentemente pagarem suas dívidas, ou seja, a decisão que o modelo ajudará a tomar é de sim ou não:

Esse cliente deve passar pelo programa de aconselhamento da ProfitCard?

1.1 Dicionário de Dados¶

Em relação ao conjunto de dados, as seguintes informações foram disponibilizadas:

  • Os dados estão em formato estruturado e serão disponibilizados em um arquivo "xls".
  • Os dados financeiros estão em dólares taiwaneses e serão convertidos para facilitar a análise.
  • Cada registro do conjunto de dados representa a conta de um cliente, sendo assim, não deve haver registros duplicados.

Além disso, também foi disponibilizado o dicionário de dados:

  • ID: identificação da conta do cliente.

  • LIMIT_BAL: valor do crédito fornecido.

  • SEX: sexo do cliente:

    • 1 = masculino.
    • 2 = feminino.
  • EDUCATION: grau de instrução educacional do cliente:

    • 1 = pós graduação.
    • 2 = universidade.
    • 3 = ensino médio.
    • 4 = outros.
  • MARRIAGE: estado civil do cliente:

    • 1 = casado.
    • 2 = solteiro.
    • 3 = outros.
  • AGE: idade do cliente.

  • PAY_1 a PAY_6: status do pagamento mensal:

    • -2 = crédito não utilizado.
    • -1 = pagamento em dia.
    • 0 = pagamento mínimo realizado, mas ainda há saldo devedor.
    • 1 a 8 = quantidade de meses de atraso no pagamento.
  • BILL_AMT1 a BILL_AMT_6: valor da fatura mensal.

  • PAY_AMT1 a PAY_AMT6: valor do pagamento mensal realizado.

  • default payment next month: variável target:

    • 0 = não inadimplente.
    • 1 = inadimplente.

1.2 Estratégia da Solução¶

Como estratégia para a solução do projeto, definimos as seguintes etapas:

  • 1. Entendimento do Negócio: nesta etapa inicial, o principal objetivo é compreender o problema de negócio e as necessidades do cliente.
  • 2. Entendimento dos Dados: aqui, iremos realizar um tratamento inicial nos dados a fim de detectar possíveis inconsistências, além disso, iremos explorar as variáveis com o objetivo de entender melhor o que elas representam.
  • 3. Engenharia de Atributos: baseado nos dados existentes, criaremos novas variáveis a fim de resumir as informações e facilitar o aprendizado dos modelos. Além disso, também selecionaremos o conjunto das melhores variáveis.
  • 4. Pré-Processamento dos Dados: nesta etapa, nosso objetivo é preparar os dados para a aplicação do modelo de machine learning, iremos realizar a divisão dos dados em treino e teste, e aplicar técnicas como balanceamento de classes e padronização dos dados.
  • 5. Modelagem Preditiva: iremos testar diferentes algoritmos e escolher o de melhor performance, além de realizar a otimização de seus hiperparâmetros.
  • 6. Programa de Aconselhamento de Clientes: nesta etapa, ajudaremos a ProfitCard a responder a principal pergunta de negócio, identificando os clientes que deverão participar do programa de aconselhamento.
  • 7. Conclusões Finais: por fim, entregaremos o resultado final do projeto.

2. Entendimento dos Dados¶

Vamos iniciar o projeto carregando as bibliotecas e o conjunto de dados.

2.1 Bibliotecas Necessárias¶

In [22]:
%load_ext pycodestyle_magic
In [23]:
%pycodestyle_on
In [24]:
# Filtragem das mensagens de avisos.
import warnings
warnings.filterwarnings('ignore') 

# Manipulação de dados.
import numpy as np
import pandas as pd

# Criação de gráficos.
import matplotlib.pyplot as plt
import seaborn as sns

# Pré-processamento dos dados.
from imblearn.over_sampling import SMOTE
from scipy import stats
from sklearn.preprocessing import StandardScaler

# Algoritmos de Machine Learning.
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier

# Treinamento dos modelos.
from sklearn.model_selection import cross_val_predict, cross_val_score, GridSearchCV, StratifiedKFold, train_test_split

# Métricas para avaliação dos modelos.
from sklearn import metrics
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, f1_score
from sklearn.metrics import make_scorer, precision_score, recall_score, roc_auc_score

print('Bibliotecas carregadas com sucesso!')
3:34: W291 trailing whitespace
6:1: E402 module level import not at top of file
7:1: E402 module level import not at top of file
10:1: E402 module level import not at top of file
11:1: E402 module level import not at top of file
14:1: E402 module level import not at top of file
15:1: E402 module level import not at top of file
16:1: E402 module level import not at top of file
19:1: E402 module level import not at top of file
20:1: E402 module level import not at top of file
21:1: E402 module level import not at top of file
22:1: E402 module level import not at top of file
23:1: E402 module level import not at top of file
26:1: E402 module level import not at top of file
26:80: E501 line too long (119 > 79 characters)
29:1: E402 module level import not at top of file
30:1: E402 module level import not at top of file
30:80: E501 line too long (93 > 79 characters)
31:1: E402 module level import not at top of file
31:80: E501 line too long (85 > 79 characters)
In [25]:
# Versão da linguagem Python.
from platform import python_version
print('Versão da linguagem Python:', python_version())
In [26]:
# Versão dos pacotes.
%reload_ext watermark
%watermark --iversions
3:13: E225 missing whitespace around operator
In [27]:
# Configuração do notebook.

# Plotagens.
from matplotlib import rcParams
rcParams['figure.figsize'] = 15, 10
rcParams['lines.linewidth'] = 3

# Estilo dos gráficos.
plt.style.use('ggplot')

# Configuração Dataframe.
pd.set_option('display.max_columns', None)

2.2 Tratamento Inicial dos Dados¶

Nessa etapa, nosso objetivo é realizar uma análise geral no dataset a fim de tratar possíveis inconsistências nos dados.

In [28]:
# Carregando o conjunto de dados.
df = pd.read_excel('data/default of credit card clients.xls')

Criar uma cópia do dataset é uma boa prática para não perdermos o conteúdo original durante a manipulação dos dados.

In [29]:
# Cópia do dataset.
df1 = df.copy()
In [30]:
# Dimensão do dataframe.
df1.shape
Out[30]:
(30000, 25)

Temos 30.000 registros e 25 variáveis.

In [31]:
# Visualizando o dataframe.
df1.head()
Out[31]:
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_1 PAY_2 PAY_3 PAY_4 PAY_5 PAY_6 BILL_AMT1 BILL_AMT2 BILL_AMT3 BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default payment next month
0 798fc410-45c1 20000 2 2 1 24 2 2 -1 -1 -2 -2 3913 3102 689 0 0 0 0 689 0 0 0 0 1
1 8a8c8f3b-8eb4 120000 2 2 2 26 -1 2 0 0 0 2 2682 1725 2682 3272 3455 3261 0 1000 1000 1000 0 2000 1
2 85698822-43f5 90000 2 2 2 34 0 0 0 0 0 0 29239 14027 13559 14331 14948 15549 1518 1500 1000 1000 1000 5000 0
3 0737c11b-be42 50000 2 2 1 37 0 0 0 0 0 0 46990 48233 49291 28314 28959 29547 2000 2019 1200 1100 1069 1000 0
4 3b7f77cc-dbc0 50000 1 2 1 57 -1 0 -1 0 0 0 8617 5670 35835 20940 19146 19131 2000 36681 10000 9000 689 679 0
In [32]:
# Informações do dataframe.
df1.info()
In [33]:
# Valores duplicados.
df1.duplicated().sum()
Out[33]:
0

Aparentemente não temos valores ausentes ou duplicados, mas iremos nos certificar disso mais adiante.

Observe que a variável PAY_1 foi carregada como object, o que não era esperado conforme informado no dicionário de dados.

In [34]:
# Contagem de registros.
df1['PAY_1'].value_counts()
Out[34]:
0                13402
-1                5047
1                 3261
Not available     3021
-2                2476
2                 2378
3                  292
4                   63
5                   23
8                   17
6                   11
7                    9
Name: PAY_1, dtype: int64

3.021 registros possuem a string Not available que é a razão pela qual a variável foi carregada como object.

Vamos tratar esses registros.

In [35]:
# Máscara booleana.
pay1_booleano = df1['PAY_1'] != 'Not available'
pay1_booleano[0:5]
Out[35]:
0    True
1    True
2    True
3    True
4    True
Name: PAY_1, dtype: bool
In [36]:
# Filtrando os registros.
df1 = df1.loc[pay1_booleano, :].copy()
In [37]:
# Convertendo a variável.
df1['PAY_1'] = df1['PAY_1'].astype('int64')
In [38]:
# Contagem de registros.
df1['PAY_1'].value_counts()
Out[38]:
 0    13402
-1     5047
 1     3261
-2     2476
 2     2378
 3      292
 4       63
 5       23
 8       17
 6       11
 7        9
Name: PAY_1, dtype: int64

Pronto, as strings foram removidas!

Para facilitar a análise, vamos converter as variáveis financeiras que estão em dólares taiwaneses para reais.

In [39]:
# Convertendo as variáveis.

# Cotação do dólar taiwanês.
dol_tw = 0.20

# Variáveis financeiras.
fin_vars = ['LIMIT_BAL',
            'BILL_AMT1',
            'BILL_AMT2',
            'BILL_AMT3',
            'BILL_AMT4',
            'BILL_AMT5',
            'BILL_AMT6',
            'PAY_AMT1',
            'PAY_AMT2',
            'PAY_AMT3',
            'PAY_AMT4',
            'PAY_AMT5',
            'PAY_AMT6']
In [40]:
# Variáveis antes da conversão.
df1[fin_vars].head()
Out[40]:
LIMIT_BAL BILL_AMT1 BILL_AMT2 BILL_AMT3 BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6
0 20000 3913 3102 689 0 0 0 0 689 0 0 0 0
1 120000 2682 1725 2682 3272 3455 3261 0 1000 1000 1000 0 2000
2 90000 29239 14027 13559 14331 14948 15549 1518 1500 1000 1000 1000 5000
3 50000 46990 48233 49291 28314 28959 29547 2000 2019 1200 1100 1069 1000
4 50000 8617 5670 35835 20940 19146 19131 2000 36681 10000 9000 689 679
In [41]:
# Aplicando a conversão.
df1[fin_vars] = df1[fin_vars].apply(lambda x: x * dol_tw)
In [42]:
# Variáveis após a conversão.
df1[fin_vars].head()
Out[42]:
LIMIT_BAL BILL_AMT1 BILL_AMT2 BILL_AMT3 BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6
0 4000.0 782.6 620.4 137.8 0.0 0.0 0.0 0.0 137.8 0.0 0.0 0.0 0.0
1 24000.0 536.4 345.0 536.4 654.4 691.0 652.2 0.0 200.0 200.0 200.0 0.0 400.0
2 18000.0 5847.8 2805.4 2711.8 2866.2 2989.6 3109.8 303.6 300.0 200.0 200.0 200.0 1000.0
3 10000.0 9398.0 9646.6 9858.2 5662.8 5791.8 5909.4 400.0 403.8 240.0 220.0 213.8 200.0
4 10000.0 1723.4 1134.0 7167.0 4188.0 3829.2 3826.2 400.0 7336.2 2000.0 1800.0 137.8 135.8

Por fim, vamos renomear a variável default payment next month.

In [43]:
# Renomeando a variável.
df1.rename(columns={'default payment next month': 'TARGET'}, inplace=True)

Realizamos uma análise geral nos dados tratando as principais inconsistências observadas, agora, partiremos para uma análise mais detalhada.

2.3 Análise Exploratória¶

Baseado no dicionário de dados, vamos separar as variáveis.

In [44]:
# Variáveis do dataframe.
df1.columns
Out[44]:
Index(['ID', 'LIMIT_BAL', 'SEX', 'EDUCATION', 'MARRIAGE', 'AGE', 'PAY_1',
       'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6', 'BILL_AMT1', 'BILL_AMT2',
       'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1',
       'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6', 'TARGET'],
      dtype='object')
In [45]:
# Variável de identificação dos clientes.
id = ['ID']
In [46]:
# Variáveis numéricas.
nums = fin_vars.copy()
nums.insert(1, 'AGE')
nums
Out[46]:
['LIMIT_BAL',
 'AGE',
 'BILL_AMT1',
 'BILL_AMT2',
 'BILL_AMT3',
 'BILL_AMT4',
 'BILL_AMT5',
 'BILL_AMT6',
 'PAY_AMT1',
 'PAY_AMT2',
 'PAY_AMT3',
 'PAY_AMT4',
 'PAY_AMT5',
 'PAY_AMT6']
In [47]:
# Variáveis categóricas.
cats = ['SEX',
        'EDUCATION',
        'MARRIAGE',
        'PAY_1',
        'PAY_2',
        'PAY_3',
        'PAY_4',
        'PAY_5',
        'PAY_6']
In [48]:
# Variável target.
target = ['TARGET']

Também criaremos subgrupos das variáveis.

In [49]:
# Variáveis de status do pagamento.
pay_sts = ['PAY_1',
           'PAY_2',
           'PAY_3',
           'PAY_4',
           'PAY_5',
           'PAY_6']
In [50]:
# Variáveis de valores das faturas.
bills = ['BILL_AMT1',
         'BILL_AMT2',
         'BILL_AMT3',
         'BILL_AMT4',
         'BILL_AMT5',
         'BILL_AMT6']
In [51]:
# Variáveis de pagamento.
pays = ['PAY_AMT1',
        'PAY_AMT2',
        'PAY_AMT3',
        'PAY_AMT4',
        'PAY_AMT5',
        'PAY_AMT6']

2.3.1 Variável de Identificação dos Clientes¶

Vamos utilizar a coluna de ID's para verificar a quantidade de registros exclusivos.

In [52]:
# Registros exclusivos.
df1['ID'].nunique()
Out[52]:
26704

Inicialmente o conjunto de dados tinha 30.000 registros, desses 3.021 haviam a string Not available e foram excluídos.

Sobraram 26.979 registros, e desses 26.704 são registros únicos, ou seja, temos registros que estão repetidos.

Vamos tratar esses registros para que eles não interfiram na análise.

In [53]:
# Contagem de registros.
id_counts = df1['ID'].value_counts()
id_counts.value_counts()
Out[53]:
1    26429
2      275
Name: ID, dtype: int64

Temos 275 ID's que estão duplicados.

Vamos identificá-los.

In [54]:
# ID's duplicados.
duple_id = id_counts == 2

# Índíces dos ID's duplicados.
duple_idx = id_counts.index[duple_id]
duple_idx
Out[54]:
Index(['d5aeb496-64e5', '443324fb-5cfc', 'f20d8a3d-d047', '693a0664-bde6',
       '8567249b-827e', '590a776e-5049', '2189fc56-f82a', '0913d642-c5d4',
       'af1e3f79-f628', '297edb0f-3bb1',
       ...
       '4dc45e9a-27bd', 'c9826d63-f7d3', 'fc73f07e-eb96', '5f483bdb-3aaf',
       '93b2c5f7-acea', '47d9ee33-0df0', '26bde6da-f148', 'f63d8fbe-d79e',
       'dda76366-a407', 'c3ddce11-35e2'],
      dtype='object', length=275)

Agora temos o objeto duple_idx contendo os ID's dos registros duplicados.

Podemos aplicar um filtro no dataframe para visualizá-los.

In [55]:
# ID's duplicados.
df1.loc[df1['ID'].isin(duple_idx[0:5]), :]
Out[55]:
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_1 PAY_2 PAY_3 PAY_4 PAY_5 PAY_6 BILL_AMT1 BILL_AMT2 BILL_AMT3 BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 TARGET
541 d5aeb496-64e5 30000.0 2 1 2 28 1 -2 -2 -2 -2 -1 0.0 0.0 0.0 0.0 0.0 63.6 0.0 0.0 0.0 0.0 63.6 0.0 0
641 d5aeb496-64e5 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 0
10491 8567249b-827e 100000.0 1 1 2 38 0 0 0 0 0 0 95493.6 97616.6 99767.8 82364.0 84534.2 78919.8 4200.0 4560.0 3040.0 3800.0 3200.0 2000.0 0
10591 8567249b-827e 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 0
19575 443324fb-5cfc 42000.0 2 1 1 31 0 0 0 0 0 0 31508.8 32087.4 25849.4 23915.8 24063.6 21424.4 1501.0 1414.4 801.8 1600.0 2000.0 2400.0 0
19675 443324fb-5cfc 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 0
21274 693a0664-bde6 10000.0 1 3 2 51 1 -2 -2 -2 -2 -2 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
21374 693a0664-bde6 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 0
27419 f20d8a3d-d047 10000.0 1 1 1 45 -1 -1 -1 -1 -1 -1 78.0 78.0 78.0 78.0 78.0 78.0 78.0 78.0 78.0 78.0 78.0 78.0 1
27519 f20d8a3d-d047 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 0

Aparentemente cada ID duplicado parece ter um registro de dados válidos e um somente com zeros.

Se temos ID's duplicados com valores zeros, também é viável verificar se temos ID's únicos contendo esses valores.

Para isso, iremos criar uma dataframe booleano baseado em todo o conjunto de dados.

In [56]:
# Dataframe booleano.
# True = 0, False = outro valor.
df_booleano = df1 == 0
df_booleano.head()
Out[56]:
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_1 PAY_2 PAY_3 PAY_4 PAY_5 PAY_6 BILL_AMT1 BILL_AMT2 BILL_AMT3 BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 TARGET
0 False False False False False False False False False False False False False False False True True True True False True True True True False
1 False False False False False False False False True True True False False False False False False False True False False False True False False
2 False False False False False False True True True True True True False False False False False False False False False False False False True
3 False False False False False False True True True True True True False False False False False False False False False False False False True
4 False False False False False False False True False True True True False False False False False False False False False False False False True

O objetivo é identificar os registros que possuem True para todas as variáveis.

In [57]:
# Identificando os registros.
# True = registros inválidos, False = registros válidos.
df_zero = df_booleano.iloc[:, 1:].all(axis=1)

# Total de registros inválidos.
sum(df_zero)
Out[57]:
315

315 registros possuem zeros em todas as variáveis, ou seja, além dos 275 registros duplicados, também temos registros únicos com essa característica.

In [58]:
# Filtrando os registros.
df2 = df1.loc[~df_zero, :].copy()
In [59]:
# Registros únicos.
df2['ID'].nunique()
Out[59]:
26664
In [60]:
# Dimensão do dataframe.
df2.shape
Out[60]:
(26664, 25)

Identificamos e eliminamos os registros com índices duplicados e também os que continham zero para todas as variáveis.

2.3.2 Variáveis Numéricas¶

In [61]:
# Estatísticas das variáveis.
df2[nums].describe().T
Out[61]:
count mean std min 25% 50% 75% max
LIMIT_BAL 26664.0 33583.810981 25967.890616 2000.0 10000.00 28000.0 48000.00 160000.0
AGE 26664.0 35.505213 9.227442 21.0 28.00 34.0 41.00 79.0
BILL_AMT1 26664.0 10281.146145 14726.737421 -33116.0 716.00 4472.2 13529.95 149362.8
BILL_AMT2 26664.0 9860.000300 14186.909907 -13955.4 599.95 4230.0 12879.10 134312.6
BILL_AMT3 26664.0 9405.268009 13741.071905 -31452.8 525.45 4015.9 12072.00 171017.2
BILL_AMT4 26664.0 8667.778908 12855.050148 -34000.0 468.35 3807.4 10945.50 141372.8
BILL_AMT5 26664.0 8067.627340 12141.188817 -16266.8 349.00 3613.2 10058.10 164708.0
BILL_AMT6 26664.0 7777.974467 11886.508331 -67920.6 251.20 3401.0 9850.75 139988.8
PAY_AMT1 26664.0 1140.817154 3339.879726 0.0 200.00 422.9 1005.40 174710.4
PAY_AMT2 26664.0 1176.221970 4242.861970 0.0 160.40 401.4 1000.00 245416.4
PAY_AMT3 26664.0 1051.902993 3453.087912 0.0 78.00 364.4 911.25 177808.6
PAY_AMT4 26664.0 977.409743 3191.269874 0.0 58.95 300.0 810.10 124200.0
PAY_AMT5 26664.0 968.745995 3062.344359 0.0 48.55 300.0 816.55 85305.8
PAY_AMT6 26664.0 1051.568609 3527.093637 0.0 22.20 300.0 803.00 105733.2
In [62]:
# Histogramas das variáveis.

# Amostra dos dados.
df_sample = df2.sample(1000, random_state=42)

# Redefinindo a área de plotagem.
plt.figure(figsize=(20, 10))

# Especificando as variáveis.
features = nums

# Plotagem.
for i in range(0, len(nums)):
    plt.subplot(5, 3, i + 1)
    sns.histplot(x=df_sample[features[i]], color='royalblue')
    plt.xlabel(features[i])
    plt.tight_layout()
  • As variáveis LIMIT_BAL e AGE estão corretas, não há valores abaixo de zero.

  • As variáveis BILL_AMT parecem corretas, as faturas possuem valores "normais", sendo que os valores negativos indicam um crédito para o titular.

  • Para as variáveis PAY_AMT precisaremos aplicar a transformação logarítmica para facilitar a visualização.

In [63]:
# Histogramas das variáveis.

# Filtrando os dados.
pay_zero_mask = df_sample[pays] == 0

# Plotagem.
df_sample[pays][~pay_zero_mask].apply(np.log10).hist(layout=(2, 3),
                                                     ec='w',
                                                     alpha=.7,
                                                     color='royalblue');
10:72: E703 statement ends with a semicolon
  • Aparentemente as variáveis estão corretas, a distribuição dos pagamentos parece bem consistentes de um mês para o outro.

2.3.3 Variáveis Categóricas¶

In [64]:
# Plotagem das variáveis.
for i in range(0, len(cats)):
    plt.subplot(3, 3, i + 1)
    sns.countplot(x=df_sample[cats[i]], color='royalblue', ec='w')
    plt.tight_layout()
  • As variáveis EDUCATION, MARRIAGE, possuem valores não documentados no dicionário de dados.
  • Com exceção da PAY_1, as demais variáveis de status do pagamento não possuem o valor 1, indicando atraso de 1 mês no pagamento da fatura, mas possuem o valor 2, indicando o atraso de 2 meses, o que não faz sentido.
In [65]:
# Tratando as inconsistências observadas.

# Variável EDUCATION.
df2['EDUCATION'].replace(to_replace=[0, 5, 6], value=4, inplace=True)

# Variável MARRIAGE.
df2['MARRIAGE'].replace(to_replace=0, value=3, inplace=True)
  • Para a variável EDUCATION agrupamos os valores 0, 5 e 6 na categoria 4.
  • Para a variável MARRIAGE agrupamos o valor 0 na categoria 3.

Vamos analisar as variáveis de status de pagamento.

In [66]:
# Comparando as variáveis.
df2.loc[df2['PAY_2'] == 2, ['PAY_2', 'PAY_3']].head()
Out[66]:
PAY_2 PAY_3
0 2 -1
1 2 0
13 2 2
15 2 0
50 2 2

A saída acima, fica claro que os dados dessas variáveis não batem.

A única forma de chegarmos a um atraso de 2 meses seria havendo um atraso de 1 mês no mês anterior, o que não ocorre.

Em casos como esse, poderíamos consultar a ProfitCard para verificar quais procedimentos tomar em relação a essa situação.

Teríamos basicamente 2 opções:

  • Tentar resgatar os registros originais das variáveis.
  • Descartar as variáveis da análise.

Como se trata de uma variável categórica e que indica o status de pagamento de cada cliente, iremos optar pela segunda opção.

Portanto, de todas as variáveis de status de pagamentos, utilizaremos somente a PAY_1.

In [67]:
# Tratando as inconsistências observadas.
df2 = df2.drop(['PAY_2',
                'PAY_3',
                'PAY_4',
                'PAY_5',
                'PAY_6'], axis=1)
In [68]:
# Redefinindo a lista de variáveis categóricas.
cats2 = ['SEX',
         'EDUCATION',
         'MARRIAGE',
         'PAY_1']

2.3.4 Variável Target¶

In [69]:
# Variáveis em relação a target.

# Especificando as variáveis.
features = cats2

# Plotagem.
for i in range(0, len(features)):
    plt.subplot(2, 2, i + 1)
    sns.countplot(data=df2, x=features[i],
                  hue='TARGET', alpha=.7, palette=['red', 'green'])
    plt.tight_layout()

2.3.5 Perguntas de Negócio¶

  • P1. O limite de crédito fornecido impacta na taxa de inadimplência?
  • P2. Clientes inadimplentes tendem a inadimplir novamente?
  • P3. Há diferenças significativas na taxa de inadimplência baseado no grau de instrução educacional?

Para responder a primeira pergunta, criaremos uma nova variável baseada na LIMIT_BAL.

In [70]:
# Valor mínimo da variável.
df2['LIMIT_BAL'].min()
Out[70]:
2000.0
In [71]:
# Valor máximo da variável.
df2['LIMIT_BAL'].max()
Out[71]:
160000.0

Com base no valor mínimo e máximo da variável, agruparemos os clientes conforme as regras abaixo:

  • Limite de crédito de 0 a 30.000 receberá o valor 1.
  • Limite de crédito de 30.000 a 60.000 receberá o valor 2.
  • Limite de crédito de 60.000 a 90.000 receberá o valor 3.
  • Limite de crédito de 90.000 a 200.000 receberá o valor 4.
In [72]:
# Definindo os valores e labels.
bins = [0, 30000, 60000, 90000, 200000]
labels = [1, 2, 3, 4]
In [73]:
# Criando a variável.
df2['GROUP_LIMIT'] = pd.cut(df2['LIMIT_BAL'], bins,
                            labels=labels).astype('int64')
In [74]:
# Visualizando amostras aleatórias.
df2[['LIMIT_BAL', 'GROUP_LIMIT']].sample(5)
Out[74]:
LIMIT_BAL GROUP_LIMIT
3981 4000.0 1
18586 16000.0 1
27903 18000.0 1
9450 100000.0 4
11483 20000.0 1
In [75]:
# Contagem de registros.
df2['GROUP_LIMIT'].value_counts()
Out[75]:
1    14510
2     8074
3     2982
4     1098
Name: GROUP_LIMIT, dtype: int64

A partir dessa variável, vamos verificar a porcentagem de inadimplência de cada classe.

Para isso, vamos realizar uma série de agrupamento nos dados.

In [76]:
# Agrupando os dados.
df_group = df2.groupby('TARGET').agg({'ID': 'nunique'}).reset_index() 
df_group
Out[76]:
TARGET ID
0 0 20750
1 1 5914
2:70: W291 trailing whitespace

Esse primeiro agrupamento é basicamente a contagem de registros para cada classe da variável target.

Vamos "desmembrar" essa informação baseado na variável GROUP_LIMIT.

In [77]:
# Agrupando os dados.
df_group2 = df2.groupby(['TARGET', 'GROUP_LIMIT']).agg({'ID': 'nunique'}).reset_index()
df_group2
Out[77]:
TARGET GROUP_LIMIT ID
0 0 1 10487
1 0 2 6731
2 0 3 2561
3 0 4 971
4 1 1 4023
5 1 2 1343
6 1 3 421
7 1 4 127
2:80: E501 line too long (87 > 79 characters)

Acima temos a quantidade de registros para cada categoria baseado nas classes da variável target.

Vamos concatenar esses dois dataframes.

In [78]:
# Concatenando os dados.
df_group3 = df_group2.merge(df_group, on='TARGET')
df_group3
Out[78]:
TARGET GROUP_LIMIT ID_x ID_y
0 0 1 10487 20750
1 0 2 6731 20750
2 0 3 2561 20750
3 0 4 971 20750
4 1 1 4023 5914
5 1 2 1343 5914
6 1 3 421 5914
7 1 4 127 5914

O próximo passo é agrupar os dados pela variável GROUP_LIMIT.

In [83]:
# Agrupando os dados.
df_group4 = df2.groupby('GROUP_LIMIT').agg({'ID': 'nunique'}).reset_index() 
df_group4
Out[83]:
GROUP_LIMIT ID
0 1 14510
1 2 8074
2 3 2982
3 4 1098
2:76: W291 trailing whitespace
In [80]:
# Concatenando os dados.
df_group5 = df_group2.merge(df_group4, on='GROUP_LIMIT')
df_group5
Out[80]:
TARGET GROUP_LIMIT ID_x ID_y
0 0 1 10487 14510
1 1 1 4023 14510
2 0 2 6731 8074
3 1 2 1343 8074
4 0 3 2561 2982
5 1 3 421 2982
6 0 4 971 1098
7 1 4 127 1098

Para obtermos o percentual, basta realizar uma simples operação entre as colunas ID_x e ID_y.

In [81]:
# Coluna de percentual.
df_group5['Percentual(%)'] = df_group5['ID_x'] / df_group5['ID_y'] * 100

# Renomeando as colunas.
df_group5.columns = ['Status do Cliente',
                     'Categoria do Cliente',
                     'Total Por Categoria',
                     'Total Geral',
                     'Percentual(%)']

# Visualizando o dataframe.
df_group5
Out[81]:
Status do Cliente Categoria do Cliente Total Por Categoria Total Geral Percentual(%)
0 0 1 10487 14510 72.274294
1 1 1 4023 14510 27.725706
2 0 2 6731 8074 83.366361
3 1 2 1343 8074 16.633639
4 0 3 2561 2982 85.881958
5 1 3 421 2982 14.118042
6 0 4 971 1098 88.433515
7 1 4 127 1098 11.566485

Vamos visualizar essa informação de maneira gráfica.

In [90]:
# Gráfico para a pergunta 1.

# Chart.
chart = sns.barplot(x='Categoria do Cliente', y='Percentual(%)',
                    data=df_group5, hue='Status do Cliente',
                    alpha=.7, palette=['green', 'red'])
# Título.
chart.text(x=0.5, y=95,
           s='Taxa de Inadimplência por Categoria de Clientes',
           fontsize=20, weight='bold', alpha=.75)

# Estilo e labels.
sns.set(font_scale=1.5)
sns.set_palette('prism')
chart.set_xlabel('\nCategorias', fontsize=14)
chart.set_ylabel('Percentual(%)', fontsize=14)

# Legenda.
plt.legend(loc='upper left', borderpad=1.0,
           labelspacing=1.0, fontsize=10, title='Status:');
20:59: E703 statement ends with a semicolon

Pergunta 1:

  • P1. O limite de crédito fornecido impacta na taxa de inadimplência?
    • Sim, podemos ver que à medida que o limite de crédito aumenta, a taxa de inadimplência diminui;
    • A categoria 1, possui uma taxa de inadimplência quase 3 vezes maior que a categoria 4.

Para responder a pergunta 2 utilizaremos a variável PAY_1 que representa o status do cliente.

In [92]:
# Taxa de inadimplência por status.
data = df2.groupby('PAY_1').agg({'TARGET': np.mean})
In [93]:
# Taxa geral de inadimplência.
data['Taxa Geral de Inadimplência'] = tx_target = df2['TARGET'].mean()
In [96]:
# Renomeando a coluna.
data.rename(columns={'TARGET': 'Taxa de Inadimplência por Status'},
            inplace=True)
data
Out[96]:
Taxa de Inadimplência por Status Taxa Geral de Inadimplência
PAY_1
-2 0.131664 0.221797
-1 0.170002 0.221797
0 0.128295 0.221797
1 0.336400 0.221797
2 0.694701 0.221797
3 0.773973 0.221797
4 0.682540 0.221797
5 0.434783 0.221797
6 0.545455 0.221797
7 0.777778 0.221797
8 0.588235 0.221797
In [ ]:
# Gráfico para a pergunta 2.

# Chart.
chart = sns.lineplot(data = data, 
                     alpha = .7,
                     palette = ['green', 'red'], 
                     linewidth = 2.5)

# Título.
chart.text(x = 0.40, 
           y = 0.85, 
           s = 'Taxa de Inadimplência por Status',
           fontsize = 20, 
           weight = 'bold', 
           alpha = .75)

# Estilo e labels.
sns.set(font_scale = 1.5)
chart.set_xlabel('\nStatus', fontsize = 14)
chart.set_ylabel('Percentual(%)', fontsize = 14)

# Legenda.
plt.legend(loc = 'upper left', 
           facecolor = 'w',
           borderpad = 1.0, 
           labelspacing = 1.0, 
           fontsize = 10, 
           title = 'Status:');

Pergunta 2:

  • P2. Clientes inadimplentes tendem a inadimplir novamente?
    • Sim, podemos ver que clientes inadimplentes (valores a partir de 1) apresentam maiores probabilidades de inadimplir novamente.
    • De acordo com a análise, pelo menos 30% das contas que estavam inadimplentes no último mês (valor 1), estarão inadimplentes novamente.
    • Por outro lado, as contas em boa situação (-2, -1, e 0) estão bem abaixo da taxa geral de inadimplência.

Para responder a pergunta 3 vamos realizar o mesmo procedimento realizado na pergunta 1.

In [ ]:
# Agrupando os dados.
df_group6 = df2.groupby('TARGET').agg({'ID':'nunique'}).reset_index() 
df_group6
In [ ]:
# Agrupando os dados.
df_group7 = df2.groupby(['TARGET','EDUCATION']).agg({'ID':'nunique'}).reset_index()
df_group7
In [ ]:
# Concatenando os dados.
df_group8 = df_group7.merge(df_group, on = 'TARGET')
df_group8
In [ ]:
# Agrupando os dados.
df_group9 = df2.groupby('EDUCATION').agg({'ID':'nunique'}).reset_index() 
df_group9
In [ ]:
# Concatenando os dados.
df_group10 = df_group7.merge(df_group9, on = 'EDUCATION')
df_group10
In [ ]:
# Coluna de percentual.
df_group10['Percentual(%)'] = df_group10['ID_x'] / df_group10['ID_y'] * 100

# Renomeando as colunas.
df_group10.columns = ['Status do Cliente', 
                      'Grau Educacional', 
                      'Total Por Categoria', 
                      'Total Geral', 
                      'Percentual(%)']

# Visualizando o dataframe.
df_group10
In [ ]:
# Gráfico para a pergunta 3.

# Chart.
chart = sns.barplot(x = 'Grau Educacional', 
                    y = 'Percentual(%)', 
                    data = df_group10, 
                    hue = 'Status do Cliente', 
                    alpha = .7,
                    palette = ['green', 'red'])
# Título.
chart.text(x = 0.5, 
           y = 100, 
           s = 'Taxa de Inadimplência por Grau Educacional',
           fontsize = 20, 
           weight = 'bold', 
           alpha = .75)

# Estilo e labels.
sns.set(font_scale = 1.5)
sns.set_palette('prism')
chart.set_xlabel('\nGrau Educacional', fontsize = 14)
chart.set_ylabel('Percentual(%)', fontsize = 14)

# Legenda.
plt.legend(loc = 'upper left', 
           borderpad = 1.0, 
           labelspacing = 1.0, 
           fontsize = 10, 
           title = 'Status:');

Pergunta 3:

  • P3. Há diferenças significativas na taxa de inadimplência baseado no grau de instrução educacional?
    • Não, embora haja uma diferença entre as classes, ela não é significativa.
    • A classe 4 (Outros) é a que demonstra a menor taxa de inadimplência.

2.3.6 Checando Outliers¶

In [ ]:
# Boxplots das variáveis.

# Especificando as variáveis.
features = bills

# Plotagem.
for i in range(0, len(features)):
    plt.subplot(2, 3, i + 1)
    sns.boxplot(y = df2[features[i]], 
                color = 'royalblue', 
                orient = 'v')
    plt.tight_layout()
In [ ]:
# Boxplots das variáveis.

# Especificando as variáveis.
features = pays

# Plotagem.
for i in range(0, len(features)):
    plt.subplot(2, 3, i + 1)
    sns.boxplot(y = df2[features[i]], 
                color = 'royalblue', 
                orient = 'v')
    plt.tight_layout()
In [ ]:
# Boxplots das variáveis.
sub_vars = ['LIMIT_BAL', 'AGE']

# Especificando as variáveis.
features = sub_vars

# Plotagem.
for i in range(0, len(features)):
    plt.subplot(1, len(features), i + 1)
    sns.boxplot(y = df2[features[i]], 
                color = 'royalblue', 
                orient = 'v')
    plt.tight_layout()

Todas as variáveis possuem valores outliers que precisarão de tratamento.

Para realizar o tratamento utilizaremos o Z-Score.

Z-scores são o número de desvios padrão acima e abaixo da média.

Por exemplo, um escore Z de 2 indica que uma observação está dois desvios padrão acima da média, enquanto um escore Z de -2 significa que está dois desvios padrão abaixo da média. Um Z-score de zero representa um valor que é igual à média.

Um valor de corte padrão para encontrar valores discrepantes são escores Z de +/-3.

In [ ]:
# Variáveis para o tratamento.
vars_out = pays + bills + sub_vars
vars_out
In [ ]:
# Array vazio.
registros = np.array([True] * len(df2))
In [ ]:
# Tratando valores outliers.
for col in vars_out:
    
    # Z-score absoluto.
    zscore = abs(stats.zscore(df2[col])) 
    
    # Filtrando os dados.
    registros = (zscore < 3) & registros
In [ ]:
# Filtrando os registros.
df3 = df2[registros] 
In [ ]:
# Dimensão do dataframe.
df3.shape
In [ ]:
# Visualizando o dataframe.
df3.head()

3. Engenharia de Atributos¶

3.1 Extração de Variáveis (Feature Extraction)¶

A etapa de extração de variáveis foi constituída na criação da variável GROUP_LIMIT utilizada para responder uma das perguntas de negócio.

Seguindo o mesmo raciocínio criaremos a GROUP_AGE baseada nas seguintes regras:

  • Clientes de 18 a 25 anos receberam o valor 1.
  • Clientes de 26 a 60 anos receberam o valor 2.
  • Clientes de 61 a 100 anos receberam o valor 3.
In [ ]:
# Definindo os valores e labels.
bins_age = [18, 25, 60, 100]
labels_age = [1, 2, 3]
In [ ]:
# Criando a variável.
df3['GROUP_AGE'] = pd.cut(df3['AGE'],
                          bins_age, 
                          labels = labels_age).astype('int64')
In [ ]:
# Contagem de registros.
df3['GROUP_AGE'].value_counts()

3.2 Seleção de Variáveis (Feature Selection)¶

In [ ]:
# Matriz de correlação.
corr_df = df3.corr()

# Plotagem.
sns.heatmap(corr_df, 
            cmap = 'Blues', 
            annot = False, 
            fmt = '.2f');
  • As variáveis BILL_AMT estão altamente correlacionadas entre si.
  • As variáveis BILL_AMT estão pouco correlacionadas com as variáveis PAY_AMT, o que significa que a maioria dos clientes não estão pagando o valor exato de suas faturas.

Considerando a análise de correlação, iremos desconsiderar as seguintes variáveis:

  • LIMIT_BAL e AGE: representadas por GROUP_LIMIT e GROUP_AGE.
  • SEX e MARRIAGE: não são significativas.
  • BILL_AMT2 até BILL_AMT6: estão altamente correlacionadas.
  • Para as variáveis de pagamentos PAY_AMT, utilizaremos apenas as três primeiras, pois também estão correlacionadas.

Além dessas, também deixaremos de fora as variáveis:

  • ID: por se tratar apenas do código de identificação da conta.
  • PAY_2 até PAY_6: já excluídas por conta de dados inválidos.
In [ ]:
# Variáveis removidas.
del_vars = ['LIMIT_BAL',
            'AGE',
            'SEX',
            'MARRIAGE',
            'BILL_AMT2',
            'BILL_AMT3',
            'BILL_AMT4',
            'BILL_AMT5',
            'BILL_AMT6',
            'PAY_AMT4',
            'PAY_AMT5',
            'PAY_AMT6',
            'ID']
In [ ]:
# Melhores variáveis.
best_vars = [item for item in df3.columns if item not in del_vars]
best_vars
In [ ]:
# Seleção de variáveis.
df4 = df3[best_vars]
In [ ]:
# Visualizando o dataframe.
df4.head()

4. Pré-Processamento dos Dados¶

Nessa etapa, iremos preparar os dados para a etapa da modelagem preditiva.

Neste link, explico com mais detalhes sobre as principais técnicas de pré-processamento utilizando a linguagem Python.

4.1 Divisão Treino/Teste¶

Vamos dividir os dados em dois grupos, um para treinar os modelos e outro para testá-los.

Atribuiremos uma divisão 70/30.

In [ ]:
# Separando os dados.
X = df4.loc[:, df4.columns != 'TARGET']
y = df4['TARGET']
In [ ]:
# Divisão treino/teste.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42)
In [ ]:
# Dimensão dos conjuntos.
print('Exemplos de Treino: {}'.format(len(X_train)))
print('Exemplos de Teste: {}'.format(len(X_test)))

4.2 Balanceamento de Classes¶

Como esse é um problema de classificação e temos mais registros de uma classe do que de outra, essa tarefa ajuda o algoritmo a aprender sobre ambas as classes "de maneira igual".

Para nos auxiliar nessa tarefa utilizaremos uma técnica chamada SMOTE, que resumidamente, cria registros sintéticos da classe minoritária (clientes inadimplentes).

In [ ]:
# Instanciando a classe.
smt = SMOTE()

O balanceamento de classes é feito apenas nos dados de treino.

In [ ]:
# Aplicando o balanceamento.
X_train_smt, y_train_smt = smt.fit_resample(X_train, y_train)
In [ ]:
# Dimensão do dataframe.
X_train_smt.shape
In [ ]:
# Contagem de registros.
y_train_smt.value_counts()

4.3 Padronização dos Dados¶

A padronização é o processo de redimensionamento das variáveis para que elas tenham as propriedades de uma distribuição normal, ou seja, com a média igual a 0 e desvio padrão igual a 1.

Uma forma de padronizar os dados é através do método StandardScaler, que utilizaremos a seguir.

In [ ]:
# Instanciando o objeto.
scaler = StandardScaler()
In [ ]:
# Padronizando as variáveis.
X_train_sc = scaler.fit_transform(X_train_smt)
X_test_sc = scaler.transform(X_test)

Devemos normalizar/padronizar os conjuntos de forma individual para que não haja "vazamento" de informações.

5. Modelagem Preditiva¶

5.1 Seleção de Algoritmos¶

Nesta etapa, vamos testar o desempenho de alguns algoritmos utilizados para problemas de classificação, são eles:

  • LogisticRegression
  • LDA - Linear Discriminant Analysis
  • KNeighborsClassifier
  • DecisionTreeClassifier
  • RandomForestClassifier

Para isso, vamos criar uma função onde treinaremos os modelos através da validação cruzada.

Esse método irá dividir o conjunto de treinamento em 5 folds e para cada fold, o modelo usará uma parte dos dados para o treinamento e uma para o teste.

Ao final do processo, teremos um score de como os modelos performaram.

In [ ]:
# Criando a função.
def classifiersTraining(features, tTarget, printMeans = True, scoring = 'accuracy'):
    
    # Número de folds.
    num_folds = 5
    
    # Listas para armazenar informações.
    models = [] 
    results = [] 
    names = [] 
    means = pd.DataFrame(columns = ['mean'])

    # Modelos testados.
    models.append(('LR', LogisticRegression()))
    models.append(('LDA', LinearDiscriminantAnalysis()))
    models.append(('KNN', KNeighborsClassifier()))
    models.append(('CART', DecisionTreeClassifier()))
    models.append(('RF', RandomForestClassifier()))

    # Avaliação dos modelos.
    for name, model in models:
        skf = StratifiedKFold(n_splits = num_folds)
        cv_results = cross_val_score(model, features, tTarget, cv = skf, scoring = scoring)
        results.append(cv_results)
        names.append(name)

        # Adicionando os resultados gerados pelo modelo ao dataframe.
        means = means.append(pd.DataFrame(data = [[cv_results.mean()]], 
                                          columns = ['mean'], 
                                          index   = [name]))

        # Imprime a mensagem contendo os resultados obtidos.
        if printMeans:
            msg = '%s: %f' % (name, cv_results.mean())
            print(msg)

    # Salva os resultados em um dataframe.
    results = pd.DataFrame(np.transpose(results), columns = names)

    # Retorna o dataframe com os resultados.
    return (results, means)
In [ ]:
# Avaliando os modelos.
results = classifiersTraining(features = X_train_sc, tTarget = y_train_smt)

Acima temos as médias das acurácias para cada um dos algoritmos.

Considerando apenas essa métrica, os algoritmos se saíram bem, sendo que o RandomForestClassifier apresentou o melhor desempenho.

Sendo assim, seguiremos com ele!

5.2 RandomForest - Modelo 1¶

Utilizaremos os hiperparâmetros padrões do algoritmo e posteriormente poderemos compará-lo com uma versão otimizada.

Para mais informações sobre o funcionamento desse algoritmo acesse este link em meu blog de estudos.

In [ ]:
# Instanciando o modelo.
classifierRF = RandomForestClassifier()

Começaremos definindo uma função, que irá treinar o modelo com dados de treino, e em seguida irá realizar a validação cruzada.

Aqui, já poderíamos utilizar o conjunto de teste, porém, deixaremos esse conjunto de lado para utilizá-lo apenas quando tivermos um modelo pronto.

Sendo assim, podemos utilizar a função cross_val_predict que realiza a validação cruzada K-fold, e nos retorna as previsões feitas em cada parte.

Isso significa que teremos uma previsão "limpa" para cada instância no conjunto de treino, ou seja, a previsão é feita por um modelo que nunca viu os dados durante o treinamento.

In [ ]:
# Criando a função.
def classifiermodel(model, X_train, y_train):
    
    # Treinamento do modelo.
    model = model.fit(X_train, y_train) 
    
    # Regra da validação cruzada.
    skf = StratifiedKFold(n_splits = 5)
    
    # Previsões obtidas na validação cruzada.
    y_train_pred = cross_val_predict(model, X_train, y_train, cv = skf)
    
    # Métricas de desempenho.
    accuracy = accuracy_score(y_train, y_train_pred)
    precision = precision_score(y_train, y_train_pred)
    recall = recall_score(y_train, y_train_pred) 
    f1 = f1_score(y_train, y_train_pred)
    conf = confusion_matrix(y_train, y_train_pred, labels = [1, 0])
    
    print('Modelo:', model)
    print('Acurácia do modelo:', (accuracy) * 100, '%')
    print('Precision do Modelo:', (precision) * 100, '%')
    print('Recall do modelo:', (recall) * 100, '%')
    print('F1 score:', (f1) * 100, '%')
    print('Matriz de Confusão:\n',  (conf))
In [ ]:
# Aplicando a função.
classifiermodel(classifierRF, X_train_sc, y_train_smt)

Acima temos as métricas do modelo, a matriz de confusão nos diz o seguinte:

  • 10.475 registros da classe 1 foram classificados corretamente.
  • 2.447 registros da classe 1 foram classificados erroneamente para a classe 0.
  • 2.695 registros da classe 0 foram classificados erroneamente para a classe 1.
  • 10.227 registros da classe 0 foram classificados corretamente.

Em relação as métricas temos:

  • Accuracy: indica uma performance geral do modelo, dentre todas as classificações, quantas o modelo classificou corretamente.
    • 20.702 (10.475 + 10.227) / 25.844 (total) = 80,10%.
  • Precision: dentre todas as previsões realizadas para uma classe, quantas o modelo acertou.
    • 10.475 / 13.170 (10.475 + 2.695) = 79,53% (classe 1).
  • Recall: dentre todos os registros reais de uma classe, quantos foram classificados corretamente.
    • 10.475 / 12.922 (10.475 + 2.447) = 81,06% (classe 1).
  • F1-score: média harmônica entre precision e recall.

Agora, vamos apresentar os dados de teste para esse modelo!

In [ ]:
# Previsões com dados de teste.
y_pred = classifierRF.predict(X_test_sc)

Para obter as métricas da performance com os dados de teste, podemos utilizar a função classification_report.

In [ ]:
# Avaliação do modelo.
print(classification_report(y_test, y_pred))
In [ ]:
# Matriz de confusão
conf_test = confusion_matrix(y_test, y_pred, labels = [1, 0])
print('Matriz de Confusão:\n', (conf_test))

Podemos concluir que o modelo possui melhor performance ao classificar amostras da classe negativa.

5.3 RandomForest - Modelo 2¶

Vamos iniciar a otimização do modelo RandomForest.

Para isso, vamos utilizar a classe GridSearchCV.

Essa ferramenta é usada para automatizar o processo de ajuste dos hiperparâmetros de um algoritmo.

In [ ]:
# Instanciação do modelo.
classifierRF_2 = RandomForestClassifier()

Criaremos um dicionário contendo os valores que iremos avaliar para cada hiperparâmetro.

In [ ]:
# Dicionário de parâmetros.
param_grid = [{'n_estimators': [100, 300, 500], 
               'max_features': ['auto', 'sqrt', 'log2']},
              {'bootstrap': [False], 
               'n_estimators': [100, 300, 500], 
               'max_features': ['auto', 'sqrt', 'log2']}]

Também criaremos um dicionário contendo as métricas para avaliar o modelo.

In [ ]:
# Métricas de desempenho.
dic_scores = {'accuracy' :make_scorer(accuracy_score),
              'recall'   :make_scorer(recall_score),
              'precision':make_scorer(precision_score),
              'f1'       :make_scorer(f1_score)}

Com isso, podemos instanciar o objeto da classe GridSearchCV.

In [ ]:
# Instanciando a classe.
grid_search = GridSearchCV(classifierRF_2, 
                           param_grid, 
                           scoring = dic_scores, 
                           refit = 'f1', 
                           cv = 5)

Observe que definimos o estimador, e especificamos os dicionários de hiperparâmetros e métricas.

Além disso, definimos refit = f1, para que o GridSearchCV saiba qual métrica utilizar para avaliar os conjuntos de hiperparâmetros.

In [ ]:
# Busca pelos melhores hiperparâmetros.
grid_search.fit(X_train_sc, y_train_smt)

Com a busca finalizada, podemos visualizar as métricas para cada combinação de valores.

In [ ]:
# Resultado da otimização.
pd.DataFrame(grid_search.cv_results_)[['params', 
                                       'mean_test_accuracy', 
                                       'mean_test_recall', 
                                       'mean_test_precision', 
                                       'mean_test_f1']]
In [ ]:
# Melhor combinação.
grid_search.best_params_
In [ ]:
# Modelo otimizado.
classifierRF_2 = grid_search.best_estimator_
In [ ]:
# Previsões com dados de teste.
y_pred_2 = classifierRF_2.predict(X_test_sc)
In [ ]:
# Avaliação do modelo.
print(classification_report(y_test, y_pred_2))
In [ ]:
# Matriz de confusão.
conf_test_2 = confusion_matrix(y_test, y_pred_2, labels = [1, 0])
print('Matriz de Confusão:\n', (conf_test_2))
In [ ]:
# Curva ROC AUC.

# Preparação dos dados.
y_pred_proba_RF = classifierRF.predict_proba(X_test_sc)[::,1]
fpr1, tpr1, _ = metrics.roc_curve(y_test,  y_pred_proba_RF)
auc1 = metrics.roc_auc_score(y_test, y_pred_proba_RF)

y_pred_proba_RF2 = classifierRF_2.predict_proba(X_test_sc)[::,1]
fpr2, tpr2, _ = metrics.roc_curve(y_test,  y_pred_proba_RF2)
auc2 = metrics.roc_auc_score(y_test, y_pred_proba_RF2)

# Plotagem.
plt.plot([0, 1], [0, 1], 'k--')
plt.plot(fpr1, tpr1, label = 'Modelo 1 = ' + str(round(auc1, 2)))
plt.plot(fpr2, tpr2, label = 'Modelo 2 = ' + str(round(auc2, 2)))
plt.legend(loc = 4, title = 'Resultado', facecolor = 'white')
plt.xlabel('Taxa de Falsos Positivos', fontsize = 14)
plt.ylabel('Taxa de Verdadeiros Positivos', fontsize = 14)
plt.title('Curva ROC', size = 20);

A otimização dos hiperparâmetros não afetou a performance do modelo, sendo assim, o modelo 1 seria o escolhido para a entrega final por se tratar de uma versão mais "simplificada".

6. Programa de Aconselhamento de Clientes¶

Já temos um modelo que classifica os clientes em inadimplentes ou não inadimplentes, agora, precisamos definir quais desses clientes devem participar do programa de aconselhamento da ProfitCard.

Em reunião com a empresa, foi nos informado que o custo por sessão do programa é de R$1.200,00 e a taxa de sucesso esperada é de 70%. Os possíveis benefícios do aconselhamento bem-sucedido são que o valor da cobrança mensal de uma conta será percebido como economia. Atualmente, as cobranças mensais de contas inadimplentes são relatadas como perdas.

Para calcular a possível economia que o programa de aconselhamento trará para a ProfitCard, precisaríamos saber o valor da fatura do próximo mês, porém, como não temos essa informação, utilizaremos a média dos valores da fatura mais recente, ou seja, a média da variável BILL_AMT1.

In [ ]:
# Informações do programa de aconselhamento.

# Custo por aconselhamento.
cost_per_counseling = 1200

# Taxa de eficácia esperada.
effectiveness = 0.70

# Média de valores da última fatura.
mean_bill = np.mean(X_test['BILL_AMT1'])

O próximo passo é obter as probabilidades das previsões para cada cliente.

In [ ]:
# Probabilidade das previsões.
predict_proba = classifierRF.predict_proba(X_test.values)
predict_proba
In [ ]:
# Histograma das previsões.

# Chart.
chart = sns.histplot(x = predict_proba[:,1],
                     bins = 30,
                     color = 'royalblue')

# Estilo e labels.
sns.set(font_scale = 1.5)
sns.set_palette('prism')
chart.set_xlabel('\nProbabilidades', fontsize = 14)
chart.set_ylabel('Número de Contas', fontsize = 14);

Precisamos descobrir qual será o limite de probabilidade ideal para determinarmos se um cliente passará ou não pelo programa de aconselhamento.

In [ ]:
# Intervalo de limites.
thresholds = np.linspace(0, 1, 101)
thresholds

Calcularemos os custos e economias para cada um dos limites.

  • Para cada valor de limite haverá um número diferente de previsões positivas, de acordo, com o número de contas que estiverem acima desse valor.
  • Cada conta prevista como inadimplente, terá o custo de aconselhamento associado.
In [ ]:
# Arrays para armazenar os resultados da análise.

# Número de contas inadimplentes para cada limite.
n_pos_pred = np.empty_like(thresholds)

# Custo total dos aconselhamentos para cada limite.
cost_of_all_counselings = np.empty_like(thresholds)

# Quantidade de verdadeiros positivos (contas previstas como inadimplentes e que de fato, ficaram inadimplentes).
n_true_pos = np.empty_like(thresholds)

# Economia bruta por aconselhamento para cada limite.
savings_of_all_counselings = np.empty_like(thresholds)
In [ ]:
# Criando o loop.
counter = 0

for threshold in thresholds:
    pos_pred = predict_proba[:,1] > threshold
    n_pos_pred[counter] = sum(pos_pred)   
    cost_of_all_counselings[counter] = n_pos_pred[counter] * cost_per_counseling   
    true_pos = pos_pred & y_test.astype(bool)    
    n_true_pos[counter] = sum(true_pos)  
    savings_of_all_counselings[counter] = n_true_pos[counter] * mean_bill * effectiveness   
    counter += 1

Para calcular a economia líquida de cada limite, basta subtrair o array de custos do array das economias brutas.

In [ ]:
# Economia líquida.
net_savings = savings_of_all_counselings - cost_of_all_counselings
In [ ]:
# Economia líquida gerada por cada valor de limite.

# Chart.
chart = sns.lineplot(thresholds, 
                     net_savings,
                     color = 'royalblue',
                     alpha = .7,
                     linewidth = 2.5)

# Estilo e labels.
sns.set(font_scale = 1.5)
chart.set_xlabel('\nThreshold (Limite)', fontsize = 14)
chart.set_ylabel('Economia Gerada', fontsize = 14);
In [ ]:
# Valor limite ideal.
max_savings_ix = np.argmax(net_savings)
thresholds[max_savings_ix]
In [ ]:
# Economia gerada com a definição do limite.
net_savings[max_savings_ix]
  • A economia líquida mais alta é gerada com um valor de limite igual a 0.26. Isso significa que clientes com probabilidades de inadimplência maiores que 26% devem passar pelo programa de aconselhamento da ProfitCard.
  • O valor da economia líquida obtida nesse limite é de R$ 2.887.081,19.

Podemos ainda calcular o custo de todas as inadimplências supondo a ausência do programa de aconselhamento.

In [ ]:
# Custo de inadimplências sem o programa de aconselhamento.
cost_of_defaults = sum(y_test) * mean_bill
cost_of_defaults
In [ ]:
# Percentual de redução dos custos com o programa de aconselhamento.
net_savings[max_savings_ix]/cost_of_defaults

Com o programa de aconselhamento guiado pelo nosso modelo, podemos diminuir o custo de inadimplências em 21,6%.

In [ ]:
# Contas previstas como inadimplentes em cada limite (flag rate).

# Chart.
chart = sns.lineplot(thresholds, 
                     n_pos_pred/len(y_test),
                     alpha = .7,
                     linewidth = 2.5)

# Estilo e labels.
sns.set(font_scale = 1.5)
chart.set_xlabel('\nThreshold (Limite)', fontsize = 14)
chart.set_ylabel('Flag Rate', fontsize = 14);

Com a definição do limite igual a 0.26, aproximadamente 25% das contas receberão aconselhamento.

7. Conclusões Finais¶

Os dois modelos avaliados apresentaram bons resultados, porém, o modelo 1 foi o escolhido por ser mais "simples". Para um primeiro ciclo, conseguimos um bom resultado, embora, o modelo tenha maior dificuldade em prever amostras da classe positiva com um recall de 56%, ainda assim, conseguimos uma acurácia total de 74%.

Após a etapa de modelagem, tivemos que identificar qual o valor limite ideal para considerar se um cliente deveria ou não receber o aconselhamento. Com base em nossa análise o valor limite ideal foi de 0.26, ou seja, clientes que possuem 26% de chances de inadimplir devem participar do programa. Considerando os custos por cada sessão, com nosso modelo, conseguiríamos uma economia líquida de R$ 2.887.081,19, o que representa uma diminuição de 21,6% dos custos supondo a ausência do programa de aconselhamento.

Por fim, para um segundo ciclo do projeto, poderíamos utilizar abordagens diferentes em ralação ao tratamento de valores ausentes, padronização dos dados, seleção das melhores variáveis, e a otimização de outros algoritmos de classificação.