Construção de modelo preditivo de análise de risco de crédito usando XGBoost

Raffaela Loffredo
55 min readNov 7, 2023

--

Click here to read this article in English.

A análise de risco de crédito é peça chave para a boa manutenção dos balanços das instituições financeiras. Manter uma taxa de inadimplência baixa garante que os empréstimos que estão sendo feitos estão gerando lucro. Para isso, tem-se intensificado o uso de aprendizado de máquina para a construção de modelos capazes de identificar padrões e prever se um cliente pode vir a se tornar default.

Nesse artigo mostro o caminho que eu fiz para encontrar o melhor modelo preditivo para esse problema, com o uso de análise exploratória, análises estatísticas (descritiva, diagnóstica e prescritiva), indicadores e gráficos, limpeza e tratamento dos dados, preparação dos dados com padronização (Standard Scaler) e balanceamento (Undersampling), tratamento de variáveis categóricas com Label Encoder e variáveis dummy, criação de uma função para a criar e avaliar modelos, construção de modelos para comparações com 7 algoritmos diferentes, otimização de hiperparâmetros do XGBoost com o uso de validação cruzada (Stratified KFold) e busca em grade (Grid Search), aplicação de engenharia de atributos, e avaliação de performance dos modelos com a construção de matrizes de confusão e pelas métricas de Área abaixo da curva (AUC) e Recall. Finalmente, comprova-se que um modelo tem melhor desempenho do que o outro com teste de hipótese z.

* Observação

Este é um relato completo do estudo, incluindo códigos e metodologias utilizadas. Caso queira, publiquei um resumo mais direto, onde trago apenas os resultados dessa pesquisa:

Para acessar o artigo resumido acesse aqui.

Sumário

  1. Sobre o Projeto
  2. Objetivo Geral
    2.1. Objetivos Específicos
  3. Obtenção dos Dados
  4. Dicionário de Variáveis
  5. Importação dos Dados e das Bibliotecas
  6. Análise Exploratória dos Dados
  7. Limpeza e Tratamento dos Dados
    7.1. Remoção de atributos
    7.2. Remoção dos dados ausentes em target_default
    7.3. Tratar erros de tipo em email
    7.4. Tratar dados ausentes
    7.5. Modificar tipo das variáveis
    7.6. Remoção de outliers em income
  8. Preparação dos dados
    8.1. Tratar variáveis categóricas
    8.2. Separar a classe-alvo das classes independentes
    8.3. Conjunto de treino e de teste
    8.4. Função val_model
    8.5. Modelo base
    8.6. Padronização e balanceamento dos dados
  9. Métricas de Avaliação de Performance
  10. Criação de modelos de Machine-Learning
  11. Por que XGBoost?
  12. XGBoost: Otimização de Hiperparâmetros
  13. Engenharia de Atributos
    13.1. Remover variáveis
    13.2. Criar novas variáveis
    13.2.1. Extrair informações de lat_lon
    13.2.2. Extrair informações de shipping_state
    13.3. Preparação dos dados
    13.3.1. Tratar variáveis categóricas
    13.3.2. Separar a classe-alvo das classes independentes
    13.3.3. Conjunto de treino e de teste
    13.3.4. Modelo base
    13.3.5. Padronização e balanceamento dos dados
    13.4. Criação de modelos de Machine-Learning
  14. XGBoost: Engenharia de Atributos + Otimização de Hiperparâmetros
  15. Comparativo dos Modelos XGBoost
    15.1. Matriz de Confusão
    15.2. AUC
    15.3. Recall
  16. Teste de Hipótese
  17. Conclusão

1. Sobre o Projeto

A análise de crédito nas instituições financeiras é crucial para avaliar se um tomador de empréstimo tem potencial para cumprir com o contrato ou se ele poderá se tornar inadimplente, ou seja, deixar de pagar o empréstimo efetuado. Isso porque emprestar ou não irá impactar diretamente o balanço da instituição financeira, podendo alavancar os resultados ou prejudicá-lo consideravelmente.

Essa avaliação do crédito geralmente envolve analisar o histórico de crédito do cliente, como por exemplo se ele já foi inadimplente anteriormente ou não, se ele possui bens que poderiam servir como garantia, entre diversos outros fatores que visam medir a capacidade dele em cumprir com suas obrigações de devolver o valor concedido.

Quando o cliente se torna inadimplente, ou seja, deixa de pagar sua dívida, essa pessoa se torna um default. E, em razão do risco de default tanto paras as instituições financeiras, quanto para todo o sistema financeiro e até mesmo para a economia de um país, cada vez mais se investe em modelos de inteligência artificial para prever os possíveis defaults e minimizá-los, com a finalidade de evitar um prejuízo maior.

Esses modelos buscam identificar padrões e tendências em dados históricos que não são óbvios para os olhos humanos e, dessa forma, eles conseguem prever um comportamento futuro com maior precisão, como no nosso caso qual seria o risco de um cliente vir a se tornar inadimplente, ou, default. Dessa forma, evitam-se prejuízos e aumentam-se os lucros das instituições credoras. Além de tornar todo o sistema mais resiliente.

Uma das instituições financeiras que vem investindo nesse tipo de ferramenta, até por conta de ter surgido com o intuito de dar crédito a pessoas que antes não tinham acesso a esse tipo de produto em outros bancos, é a fintech NuBank. Por esse motivo, tanto sua equipe de ciência de dados e seus usos de inteligência artificial tem se tornado referência nessa área. Com isso, o banco tem conseguido manter sua taxa de inadimplência em números menores do que em bancos tradicionais, de acordo com relatório de 2022 publicado pela UBS BB.

Com todos esses fatores em mente, esse estudo, que se baseia em dados disponibilizados pela Nubank em uma competição realizada por ela para revelar talentos, terá como objetivo a criação de um modelo de inteligência artificial capaz de prever se um cliente tem potencial de se tornar default ou não.

2. Objetivo Geral

Desenvolver um modelo de machine-learning que preveja se um novo cliente pode vir a se tornar inadimplente.

2.1. Objetivos Específicos

  • Fazer uma análise exploratória nos dados com o objetivo de conhecer o dataset e extrair insights que possam auxiliar as etapas posteriores.
  • Criar e avaliar modelos de machine-learning com diversos tipos de algoritmos supervisionados.
  • Otimizar os hiperparâmetros do algoritmo XGBoost para buscar um melhor desempenho do modelo.
  • Realizar engenharia de atributos como objetivo de melhorar a previsão de default do modelo criado com o XGBoost.
  • Avaliar os dois modelos produzidos e encontrar o que tem o melhor desempenho.

3. Obtenção dos Dados

Os dados utilizados neste projeto foram originalmente disponibilizados pela Nubank. O dataset completo foi salvo em nuvem, por garantia, caso venha a ser removido e pode ser acessado neste link.

4. Dicionário de Variáveis

A compreensão do conjunto de dados passa pela checagem das variáveis disponíveis nele para que se possa realizar uma boa análise. Embora não haja uma documentação oficial sobre o significado de cada variável, foi possível inferir o significado de algumas delas, de acordo com os registros no data frame. Dessa forma, foram separados em 2 tabelas diferentes: com e sem significado auferido.

em ordem alfabética

application_time_applied: hora que aplicou a solicitação (string) (HH:MM:SS)
application_time_in_funnel: posição do cliente no funil de vendas no momento da aplicação (int)
credit_limit: limite de crédito (float)
email: provedor de e-mail do cliente (string)
external_data_provider_fraud_score: pontuação de score (int)
facebook_profile: a pessoa tem perfil no Facebook? Sim (True)/Não (False) (string)
ids: identificação única do cliente (string)
income: renda do cliente (float)
last_amount_borrowed: valor total do último empréstimo/crédito concedido (float)
last_borrowed_in_months: quando foi feito o último empréstimo (em meses) (float)
lat_lon: localização do cliente (latitude e longitude) (string — tupla)
marketing_channel: canal pelo qual o cliente fez a aplicação (string)
n_defaulted_loans: quantidade de empréstimos inadimplentes (float)
profile_phone_number: número de telefone (string)
reported_income: valor da renda informado pelo cliente (float)
risk_rate: pontuação de risco (float)
shipping_state: local de entrega do cartão (sigla do país e do Estado) (string)
shipping_zip_code: código postal para envio do cartão (int)
target_default: variável-alvo, indica se o cliente foi inadimplente (True) ou não (False) (string)
target_fraud: variável-alvo, indica se o cliente foi fraudado (string)
user_agent: tipo de aparelho que o usuário está utilizando (se é PC, celular, marca, sistema operacional, etc.) (string)

Outras variáveis
channel: string (anonimizados)
external_data_provider_credit_checks_last_2_year: float
external_data_provider_credit_checks_last_month: int
external_data_provider_credit_checks_last_year: float
external_data_provider_email_seen_before: float
external_data_provider_first_name: string
job_name: string (anonimizados)
n_accounts: float
n_bankruptcies: float
n_issues: float
ok_since: float
profile_tags: string (dicionário)
real_state: string (anonimizados)
reason: string (anonimizados)
score_1: string (anonimizados)
score_2: string (anonimizados)
score_3: float
score_4: float
score_5: float
score_6: float
state: string (anonimizados)
zip: string (anonimizados)

5. Importação dos Dados e das Bibliotecas

Ao iniciar um projeto é necessário instalar pacotes, importar as bibliotecas que possuem funções específicas a serem utilizadas nas linhas de código seguintes e realizar as configurações necessárias para a saída do código. Também, se prossegue com a importação do dataset, salvando-o em uma variável específica para que seja usada posteriormente.

# instalar pacotes adicionais
!pip install scikit-plot -q # visualização de dados
!pip install scipy -q # análise estatística
# importar as bibliotecas necessárias
import pandas as pd # manipulação de dados
import numpy as np # manipulação de arrays
import missingno as msno # avaliação dados ausentes
import matplotlib.pyplot as plt # visualização de dados
import seaborn as sns # visualização estatística dos dados
import scikitplot as skplt # visualização de dados e métricas de machine-learning
from sklearn.impute import SimpleImputer # tratamento de valores ausentes
from sklearn.preprocessing import LabelEncoder # transformação de dados categóricos
from sklearn.model_selection import train_test_split # divisão em conjuntos de treino e teste
from sklearn.pipeline import make_pipeline # construção de fluxo de trabalho
from sklearn.model_selection import cross_val_score # avaliação de desempenho por cross-validation
from sklearn.preprocessing import StandardScaler # padronização dos dados
from imblearn.under_sampling import RandomUnderSampler # balanceamento dos dados
from sklearn.model_selection import StratifiedKFold # avaliação de desempenho com dados estratificados
from sklearn.model_selection import GridSearchCV # criação de grade para avaliar hiperparâmetros
from sklearn.metrics import classification_report # geração de relatório de desempenho
from sklearn.metrics import roc_auc_score # avaliação de desempenho por AUC
from sklearn.metrics import recall_score # avaliação de desempenho por recall
from scipy.stats import norm # análise estatística (distribuição normal)

# modelos de classificação de dados
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import SGDClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier


import warnings # notificações
warnings.filterwarnings('ignore') # configurar notificações para serem ignoradas

# configurações adicionais
plt.style.use('ggplot')
sns.set_style('dark')
np.random.seed(123)

# configurar a saída para mostrar todas as linhas e colunas
pd.options.display.max_columns = None
# importar os dados e atribuir em uma variável
data_path = "http://dl.dropboxusercontent.com/s/xn2a4kzf0zer0xu/acquisition_train.csv?dl=0"
df_raw = pd.read_csv(data_path)

6. Análise Exploratória dos Dados

Essa é uma etapa essencial em projetos de ciência de dados onde se busca compreender melhor os dados seja por identificar padrões, outliers, possíveis relações entre as variáveis, etc. Nesse estudo, se exploraram informações que fossem relevantes para orientar as respostas dos objetivos indicados anteriormente (ver Objetivo Geral e Objetivos Específicos).

Para isso serão utilizadas diversas técnicas e ferramentas, como gráficos, tabelas de frequência, dados estatísticos, entre outros métodos que se acharem necessários. Nessa fase, o cientista de dados vira um detetive em busca de coisas que não estão explícitas no data frame. Em razão disso, os dados também serão plotados de diferentes maneiras, para melhor visualizá-los e testar hipóteses iniciais, com o objetivo de obter insights que possam orientar o restante do projeto.

Primeiro, gerei a visualização das 5 primeiras e das 5 últimas entradas para checar a composição do dataset, e verifiquei se ao final dele não havia nenhum registro indevido, como por exemplo somas totais.

# checar as 5 primeiras entradas
df_raw.head()
# checar as 5 últimas entradas
df_raw.tail()

De primeira impressão ao olhar esse conjunto pode-se perceber que houve a anonimização dos dados, o que é muito comum atualmente para o cumprimento da Lei Geral de Proteção dos Dados (LGPD), bem como legislações internacionais, quando for o caso.

Além disso, podemos observar, além de alguns valores ausentes:

  • ids provavelmente representa a identificação única de um cliente, logo, é um atributo que deverá ser removido por não conter nenhuma informação relevante para a construção do modelo de machine-learning e, para além disso, pode ser uma variável que venha até a prejudicar o desenvolvimento do modelo caso o algoritmo venha a conseguir relacioná-lo aos outros dados de alguma outra forma.
  • target_default indica em valores booleanos o risco de default para aquele cliente em específico, ou seja, trata-se da nossa variável-alvo. Para isso, True aponta para um cliente que se tornou inadimplente, enquanto False diz que o cliente não foi um default.
  • Na sequência temos 6 atributos denominados como score, sendo que os dos primeiros (score_1 e score_2) estão em formato string, enquanto as demais (score_3, score_4, score_5 e score_6) são do tipo float. Pelos seus valores e informações, não foi possível inferir um significado inicial.
  • risk_rate, apresentado no tipo float, indica alguma pontuação de risco.
  • last_amount_borrowed, no tipo float, mostra o valor total do último empréstimo/crédito concedido. Nesse atributo é possível ver valores ausentes, o que pode indicar que a pessoa não fez nenhum empréstimo ou utilizou o seu limite de crédito.
  • last_borrowed_in_months, também em tipo float, mostra quando foi realizado o último empréstimo/crédito concedido, em meses. Nesse atributo é possível ver valores ausentes, o que pode indicar que a pessoa não fez nenhum empréstimo ou utilizou o seu limite de crédito.
  • No atributo credit_limit, em float, temos o valor do limite de crédito. Nesse atributo é possível ver valores ausentes, bem como valores iguais a 0, o que significa que a pessoa não tem limite de crédito a ser concedido.
  • income, tipo float, trata-se da renda do cliente.
  • facebook_profile, em booleano, indica se a pessoa tem um perfil na rede social Facebook (True) ou não (False). Nesse atributo é possível ver valores ausentes, o que pode indicar que o campo não foi preenchido e, portanto, pode-se inferir que a pessoa não tenha perfil na rede.
  • As variáveis reason, state, zip, channel, job_name (com valores ausentes), real_state, do tipo string, foram anonimizadas.
  • ok_since (com valores ausentes), n_bankruptcies, n_defaulted_loans, n_accounts, n_issues (com valores ausentes) estão em formato float.
  • application_time_applied refere-se à hora em que o cliente aplicou a solicitação, em formato (HH:MM:SS).
  • application_time_in_funnel está em formato int e indica a posição do cliente no funil de vendas no momento da aplicação.
  • email, tipo string, indica o provedor de e-mail do cliente.
  • external_data_provider_credit_checks_last_2_year (com valores ausentes) e tipo float, external_data_provider_credit_checks_last_month tipo int, external_data_provider_credit_checks_last_year (com valores ausentes) e tipo float, external_data_provider_email_seen_before tipo float, external_data_provider_first_name tipo stringe, external_data_provider_fraud_score tipo int, indicam capturas de informações externas para preencher esse banco de dados, sendo que, o último, (fraud_score) parece indicar o score do Serasa, uma vez que seu intervalo é de 0 a 1000 pontos.
  • lat_lon tipo string, indica a localização do cliente em formato de tupla que informa a latitute e a longitude.
  • marketing_channel tipo string mostra por qual canal o cliente fez a aplicação.
  • profile_phone_number tipo string informa o número de telefone do cliente.
  • reported_income tipo float é o valor da renda informado pelo cliente.
  • shipping_state tipo string, mostra para onde o cartão deverá ser enviado para o cliente, possui 5 valores sendo as 2 primeiras siglas o país, com um separador tipo -, e dois valores para informar a sigla do estado.
  • shipping_zip_code tipo int, refere-se ao código postal para envio do cartão.
  • profile_tags tipo string, está em formato de dicionário.
  • user_agent tipo string, parece indicar o tipo de aparelho que o usuário está utilizando (se é PC, celular, marca, sistema operacional, entre outros).
  • target_fraud (com valores ausentes) parece ser uma variável-alvo para a detecção de fraude.

A próxima etapa é saber o tamanho desse conjunto de dados.

# verificar tamanho do data frame
print('Dimensões do conjunto de dados')
print('-' * 30)
print('Total de registros:\t {}'.format(df_raw.shape[0]))
print('Total de atributos:\t {}'.format(df_raw.shape[1]))
'''
Dimensões do conjunto de dados
------------------------------
Total de registros: 45000
Total de atributos: 43
'''

Vamos olhar um pouco melhor para essas 43 variáveis. O objetivo será de compreender o tipo de variável que se encontram nesses atributos, checar valores ausentes, distribuição dos dados, outliers, etc.

# gerar informações do data frame
df_raw.info()

Com a saída acima podemos confirmar que há variáveis com dados ausentes e que algumas estão com o tipo de variável incorreto:

  • target_default e facebook_profile estão no tipo string mas deve ser booleano, por possuir dados ausentes deverá ser feito esse tratamento antes da conversão de tipo.
  • application_time_applied está em formato string e pode ser convertido em datetime (HH:MM:SS), porém o Scikit-Learn não aceita formatos em Timestamp. Por isso, esse atributo será descartado.

Além dessas alterações, observou-se que seria possível a aplicação de engenharia de atributos nas seguintes variáveis:

  • Em lat_lon as informações estão unidas por uma tupla. Poderíamos separar essas informações, bem como reduzir a quantidade de casas decimais para que haja uma aproximação das informações.
  • A separação de informações também pode ser feita em shipping_state que contém dados em sigla do país e do Estado, separados por um -.
  • Já em user_agent poderia ser segregada as informações referentes à marca e modelo do aparelho utilizado na conexão, em atributos distintos, ou seja, marca e modelo.

Conforme foi visto, temos uma quantidade razoável de valores ausentes. Vamos verificar isso de forma mais detalhada imprimindo a quantidade (em porcentagem) de dados ausentes em casa atributo, organizados de forma descendente, ou seja, do maior para o menor.

# verificar quantidade de dados ausentes
print(((df_raw.isnull().sum() / df_raw.shape[0]) * 100).sort_values(ascending=False).round(2))

Observa-se no topo da coluna acima os atributos com a maior quantidade de dados ausentes. Ou seja, target_fraud, last_amount_borrowed, last_borrowed_in_months, ok_since e external_data_provider_credit_checks_last_2_year tem mais da metade dos dados ausentes. Algumas delas podem melhorar com um processo de tratamento de dados, por exemplo, em last_amount_borrowed e last_borrowed_in_months um dado ausente pode simplesmente significar que o cliente não fez nenhum empréstimo e, portanto, também não haveria nenhum valor preenchido quanto ao valor desse empréstimo, já que nunca ocorreu.

Já nas variáveis external_data_provider_credit_checks_last_year, credit_limit e n_issues a quantidade de dados ausentes representa entre 25 e 35% dos dados.

Na nossa variável alvo target_default temos 7.24% de dados ausentes, que precisarão ser excluídos do dataset para a modelagem do algoritmo preditivo.

Portanto, de forma geral há que se pontuar a necessidade de tratamentos de valores ausentes.

Vamos visualizar abaixo a quantidade de dados ausentes em cada atributo, para facilitar o entendimento da qualidade desse conjunto de dados.

# imprimir gráfico para verificar dados ausentes
msno.bar(df_raw, figsize=(10,4), fontsize=8);

A seguir, vale a pena olhar para a quantidade de entradas únicas em cada variável.

# checar valores únicos de entrada para cada atributo
df_raw.nunique().sort_values()

De cima para baixo, na saída acima, temos que:

  • external_data_provider_credit_checks_last_2_year e channel possuem um único valor de entrada, logo, por não termos acesso ao dicionário original dos atributos, vou remover esses atributos uma vez que não irão acrescentar informações ao modelo de machine-learning.
  • os atributos score_4, score_5, score_6 e profile_phone_number possuem cada um, 45 mil entradas únicas. Isso significa que cada registro tem um valor o que não acrescenta informações para o algoritmo. Porém, no caso do número telefone isso faz sentido, ou seja, que cada pessoa tenha um número diferentes, portanto, como não acrescenta informação, esse atributo será removido. Já nas demais variáveis score por serem numéricas e, provavelmente podem ser resultado de cálculos matemáticos e/ou mesmo terem passado por algum tipo de normalização, como estão em valores float essas variáveis serão mantidas.

Agora, nos atributos que possuem até 5 valores únicos, vou checar para confirmar que não nenhum valor indevido que tenha sido inserido erroneamente. Esses atributos serão: target_fraud, target_default, external_data_provider_credit_checks_last_year, facebook_profile, last_borrowed_in_months, external_data_provider_credit_checks_last_month, n_defaulted_loans e real_state.

# criar variável com atributos com até 5 entradas únicas
five_unique = df_raw[['target_fraud', 'target_default',
'external_data_provider_credit_checks_last_year',
'facebook_profile', 'last_borrowed_in_months',
'external_data_provider_credit_checks_last_month',
'n_defaulted_loans', 'real_state']]

# gerar resultado para cada atributo
for i in five_unique:
print('{}'.format(i), df_raw[i].unique())
'''
target_fraud [nan 'fraud_friends_family' 'fraud_id']
target_default [False True nan]
external_data_provider_credit_checks_last_year [ 0. nan 1.]
facebook_profile [True False nan]
last_borrowed_in_months [36. nan 60.]
external_data_provider_credit_checks_last_month [2 1 3 0]
n_defaulted_loans [ 0. 1. nan 2. 3. 5.]
real_state ['N5/CE7lSkAfB04hVFFwllw==' 'n+xK9CfX0bCn77lClTWviw=='
'nSpvDsIsslUaX6GE6m6eQA==' nan 'UX7AdFYgQh+VrVC5eIaU9w=='
'+qWF9pJpVGtTFn4vFjb/cg==']
'''

Pode-se notar que os valores inconsistentes nas variáveis verificadas acima tratam-se de NAN, ou seja, valores ausentes.

Agora, vou checar um resumo estatístico desses atributos, uma vez que fornece dados importantes sobre a média, mediana, valores máximos e mínimos, desvio padrão, bem como os valores de quartis.

# ver resumo estatístico do dados numéricos
df_raw.describe().round(4)

Mais uma vez, na linha count, confirmamos a presença de valores ausentes em alguns atributos. Além disso, em uma análise individual o que mais chamou a atenção foi:

  • em last_amount_borrowed temos que o valor mínimo é de 1000 reais, enquanto o máximo chegou em 35 mil reais.
  • em last_borrowed_in_months, conforme visto anteriormente, temos apenas 2 valores únicos preenchidos aqui, o que, poderia não fazer muito sentido esses valores estatísticos tão redondos, mas, a explicação está nesse fato.
  • em credi_limit podemos ter valores outliers, contudo, é comum que bancos cedam um crédito maior à pessoas que tem mais recursos, logo, não devem ser muitos clientes com um crédito máximo de quase 450 mil reais. Enquanto a média é de cerca de 35 mil reais. Nota-se também que a mediana fica ainda inferior ao valor da média, em aproximadamente 25 mil reais. Com isso, temos que de fato há uma maior quantidade de pessoas com limite inferior à média de 35 mil reais, o que puxa a mediana para 25. Ademais, quanto aos outliers também podem ser percebidos por meio do valor do 3º quartil, que confirma que 75% do conjunto de dados tem um limite de crédito de até 47 mil reais.
  • em income podemos confirmar que temos a presença de clientes com diferenças grande de renda. Podemos ver isso diretamente pelo valor de desvio padrão e pelos valores máximos e mínimos. Ou seja, temos um desvio de 52 mil, o valor mínimo é de 4,8 mil reais, o máximo de 5 bilhões, o que irá distorcer a média que fica em 716 mil e, por isso, a mediana nos dá um valor mais adequado, que é de 61 mil reais.
  • em external_data_provider_credit_checks_last_2_year temos valores iguais a zero, o que faz sentido que esteja preenchido apenas com esse valor, o que também coincide com o que foi visto anteriormente que há a presença de apenas um valor único nesse atributo.
  • em external_data_provider_email_seen_before tem-se que o valor mínimo é de -999 enquanto o valor máximo é de 59. Logo, devem se tratar de outliers e deverão ser substituídos para não atrapalhar a análise.
  • em external_data_provider_fraud_score temos mais um ponto a favor da hipótese de que se trata do score do Serasa, uma vez que seus valores variam de 0 a 1000, assim como os dados do dataset. Portanto, vale notar que temos uma média de 500 pontos, uma mediana bem próxima a esse valor e o 3º quartil em 747.
  • em reported_income temos o valor máximo como infinito, o que significa que temos valores muito altos para esse atributo. Como isso pode prejudicar a análise, será necessário tratar esses dados.
# ver resumo estatístico do dados categóricos
df_raw.describe(include=['O'])

Quanto aos atributos categóricos, destaca-se que:

  • em target_default temos a presença do valor False em cerca de 35 das quase 42 mil entradas, o que indica um desbalanceamento nos dados.
  • quase metade do conjunto de dados possui email registrado pelo domínio do Google (gmail.com).

Vamos checar mais de perto os atributos credit_limit e income por meio do boxplot.

# configurar boxplot para 'credit_limit' e 'income'
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 4))

# 'credit_limit'
sns.boxplot(df_raw.credit_limit, orient='h', showmeans=True,
palette=['#004a8f'], ax=ax1)
ax1.set_title('Credit Limit', loc='left', fontsize=16,
color='#6d6e70', fontweight='bold', pad=20)
ax1.set_yticks([])

# 'income'
sns.boxplot(df_raw.income, orient='h', showmeans=True,
palette=['#004a8f'], ax=ax2)
ax2.set_title('Income', loc='left', fontsize=16,
color='#6d6e70', fontweight='bold', pad=20)
ax2.set_yticks([])

plt.tight_layout();

Mais uma vez, conseguimos mostrar a presença de outliers no gráfico acima. Dessa forma, não vejo necessidade em mexer nos dados de limite de crédito, uma vez que estão melhores distribuídos. Mas no atributo income vejo a necessidade de serem removidos os últimos 10 pontos do conjunto, visto que o boxplot está bem distorcido e são poucos dados que estão tão fora do padrão.

Também quero checar a distribuição de domínios de e-mail.

# plotar gráfico da distribuição do atributo 'email'
fig, ax = plt.subplots(figsize=(10, 5))
sns.countplot(x=df_raw.email)
plt.tight_layout();

Encontramos dois valores com erro de preenchimento: hotmaill.com e gmaill.com, ambos com uma letra l a mais. Isso requer o devido tratamento também.

Por fim, vou checar o balanceamento dos dados em target_default.

# representação da quantidade de 'target_default' em porcentagem
print('Total de (FALSE): {}'.format(df_raw.target_default.value_counts()[0]))
print('Total de (TRUE): {}'.format(df_raw.target_default.value_counts()[1]))
print('-' * 30)
print('O total de default representa {:.2f}% do conjunto de dados.'.format(((df_raw.target_default.value_counts()[1]) * 100) / df_raw.shape[0]))
'''
Total de (FALSE): 35080
Total de (TRUE): 6661
------------------------------
O total de default representa 14.80% do conjunto de dados.
'''

Conforme visto e, também, esperado, temos um valor de default menor do que a quantidade de não-default. Ou seja, a maior parte dos clientes pagou suas dívidas, enquanto 14.8% dos clientes ficaram inadimplentes. Em razão disso, há um certo desbalanceamento nos dados, que deverá ser considerado na fase de tratamento.

Vejamos essa proporção em um gráfico de barras.

# plotar gráfico de barras
## definir eixos
x = ['Não-Default', 'Default']
y = df_raw.target_default.value_counts()

## configurar cores das barras
bar_colors = ['#bdbdbd', '#004a8f']

## plotar gráfico
fig, ax = plt.subplots(figsize=(6, 5))
ax.bar(x, y, color=bar_colors)

### título
ax.text(-0.5, 41000, 'Proporção de Clientes', fontsize=20, color='#004a8f',
fontweight='bold')

### subtítulo
ax.text(-0.5, 39000, 'Quantidade de clientes que foram e que não foram default',
fontsize=8, color='#6d6e70')

### proporção de não-default
ax.text(-0.10, 35300, '85.2%', fontsize=12, color="#6d6e70")

### proporção de default
ax.text(0.9, 7000, '14.8%', fontsize=14, color="#004a8f", fontweight='bold')

### configurar bordas
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)

plt.show()

Isso confirma o desbalanceamento dos dados e, portanto, se faz necessário um tratamento específico. O motivo se deve ao fato de que, se fosse realizado um modelo de machine-learning com os dados em seu estado bruto, causaria impactos negativos nos resultados de previsão uma vez que o modelo seria muito bom em prever não-default, mas ele seria muito ruim na previsão de defaults. Como consequência, muitos casos default seriam consideradas não-default, ao que se denomina de Falsos Negativos. Ou seja, o modelo prevê que um cliente não é default, quando na verdade ele é! E, isso iria contra o objetivo desse estudo e o motivo de construção do modelo de previsão que estamos a elaborar.

7. Limpeza e Tratamento dos Dados

Vimos na seção anterior que o dataset possui alguns problemas que precisam ser corrigidos. Esta etapa é dedicada a resolver esses problemas. São eles:

  1. Remover os atributos
  2. Remover dados ausentes target_default
  3. Tratar erros de tipo em email
  4. Tratar dados ausentes
  5. Modificar tipo das variáveis
  6. Remoção de outliers em income

Além desses procedimentos, também será necessário padronizar os atributos e balancear o conjunto de dados. Contudo, isso será realizado na seção posterior.

E antes de realizar alterações no conjunto de dados, vou copiá-lo, para que as alterações feitas daqui por diante sejam realizadas nessa réplica. Assim, o conjunto original é mantido intacto.

# fazer cópia do dataset
df_clean = df_raw.copy()

7.1. Remoção de atributos

Vou iniciar com a remoção dos atributos ids, external_data_provider_credit_checks_last_2_year, channel, profile_phone_number, application_time_applied e target_fraud.

# remover atributos
df_clean.drop(['ids', 'external_data_provider_credit_checks_last_2_year',
'channel', 'profile_phone_number', 'application_time_applied',
'target_fraud'], axis=1, inplace=True)

# verificar alterações
df_clean.head()

7.2. Remoção dos dados ausentes em target_default

Constatamos a presença de dados ausentes no atributo target_default e, como ele se trata da nossa variável é necessário remover os dados ausentes para que possamos prosseguir com a modelagem do algoritmo preditivo.

# remover dados ausentes de 'target_default'
df_clean.dropna(subset=['target_default'], axis=0, inplace=True)

# verificar a remoção imprimindo a quantidade de dados ausentes
print(df_clean.target_default.isnull().sum())
'''
0
'''

7.3. Tratar erros de tipo em email

Foi encontrado dois erros de tipo nos campos do atributo email (hotmaill.com e gmaill.com). Vou substituir esses valores removendo a letra a mais que ambos os casos possuem.

# corrigir erro de tipo
df_clean.email.replace('gmaill.com', 'gmail.com', inplace=True)
df_clean.email.replace('hotmaill.com', 'hotmail.com', inplace=True)

# verificar
# plotar gráfico da distribuição do atributo 'email'
fig, ax = plt.subplots(figsize=(10, 5))
sns.countplot(x=df_clean.email)
plt.tight_layout();

Com a plotagem do gráfico podemos confirmar a correção dos dados e verificar a distribuição dos dados.

7.4. Tratar dados ausentes

Em fabeook_profile substituiremos os dados ausentes nesse atributo por valores False uma vez que partiremos da premissa que, se o valor não existe é porque o cliente não possui conta no Facebook.

# substituir valores ausentes por 'False'
df_clean.facebook_profile.fillna(False, inplace=True)

# verificar
print(df_clean.facebook_profile.isnull().sum())
'''
0
'''

Nos atributos last_amount_borrowede last_borrowed_in_months inferimos que se uma pessoa nunca fez um empréstimo seria natural que o valor não existisse e nem a quantidade de meses que esse último empréstimo foi realizado, uma vez que nunca ocorreu. O mesmo raciocínio pode ser feito para n_issues. Por isso, os valores nulos nesse caso serão substituídos por zero.

# substituir valores ausentes por zero
df_clean.last_amount_borrowed.fillna(0, inplace=True)
df_clean.last_borrowed_in_months.fillna(0, inplace=True)
df_clean.n_issues.fillna(0, inplace=True)

# verificar
print(df_clean[['last_amount_borrowed', 'last_borrowed_in_months', 'n_issues']].isnull().sum())
'''
last_amount_borrowed 0
last_borrowed_in_months 0
n_issues 0
dtype: int64
'''

As demais variáveis que apresentam dados nulos (ok_since, external_data_provider_credit_checks_last_year, credit_limit, marketing_channel, job_name, external_data_provider_email_seen_before, lat_lon, user_agent, n_bankruptcies, n_defaulted_loans, reason) serão tratadas de acordo com o seu tipo, isto é, as numéricas terão os valores substituídos pela mediana e as variáveis categóricas serão preenchidas com o valor mais frequente.

Vale ressaltar que, conforme visto, as variáveis reported_income possui valores infinitos, que serão substituídos por valores nulos. Bem como em external_data_provider_email_seen_before há valores -999 que também serão trocados por valores nulos. Isso deve ser feito previamente ao tratamento das variáveis ausentes, então, vou começar fazendo isso.

# tratar dados 'inf' em 'reported_income' para NaN
df_clean.reported_income = df_clean.reported_income.replace(np.inf, np.nan)

# tratar dados -999 em 'external_data_provider_email_seen_before' para NaN
df_clean.loc[df_clean.external_data_provider_email_seen_before == -999,
'external_data_provider_email_seen_before'] = np.nan

# verificar
df_clean[['reported_income', 'external_data_provider_email_seen_before']].describe()

Acima, vemos que os valores inf em reported_income e os valores -999, que deveria aparecer no valor mínimo em external_data_provider_email_seen_before não estão mais presentes.

Podemos prosseguir com a segunda parte do tratamento dos dados numéricos e categóricos.

# criar variáveis com atributos numéricos e outra com categóricos
numeric = df_clean.select_dtypes(exclude='object').columns
categorical = df_clean.select_dtypes(include='object').columns

# variáveis numéricas: substituir pela mediana
subs = SimpleImputer(missing_values=np.nan, strategy='median')
subs = subs.fit(df_clean.loc[:, numeric])
df_clean.loc[:, numeric] = subs.transform(df_clean.loc[:, numeric])

# variáveis categóricas: substituir pelo valor mais frequente
subs = SimpleImputer(missing_values=np.nan, strategy='most_frequent')
subs = subs.fit(df_clean.loc[:, categorical])
df_clean.loc[:, categorical] = subs.transform(df_clean.loc[:, categorical])

Por fim, podemos checar se objetivo de não termos mais dados ausentes no dataset foi cumprido com o código abaixo.

# verificar dados ausentes
df_clean.isnull().sum()

7.5. Modificar tipo das variáveis

As variáveis target_default e facebook_profile estão no tipo string mas devem ser convertidas para o tipo booleano.

# converter 'target_default' e 'facebook_profile' em booleano
df_clean.target_default = df_clean.target_default.astype(bool)
df_clean.facebook_profile = df_clean.facebook_profile.astype(bool)

# verificar
print(df_clean[['target_default', 'facebook_profile']].info())

7.6. Remoção de outliers em income

Vamos remover os 10 últimos valores do dataset. Para isso, primeiro identificamos esses valores e então, prosseguimos com a remoção e a verificamos plotando novamente o boxplot da variável.

# identificar os 10 maiores valores em 'income'
top_10_incomes = df_clean.nlargest(10, 'income')

# remover os 10 maiores valores em 'income' do dataset
df_clean = df_clean.drop(top_10_incomes.index)

# verificar
# configurar boxplot para 'income'
fig, ax = plt.subplots(figsize=(10, 3))

sns.boxplot(df_clean.income, orient='h', showmeans=True,
palette=['#004a8f'])
ax.set_title('Income', loc='left', fontsize=16,
color='#6d6e70', fontweight='bold', pad=20)
ax.set_yticks([])

plt.tight_layout();

Agora já conseguimos ter uma melhor visualização do boxplot para esse atributo.

8. Preparação dos dados

Nessa etapa iremos processar os dados para que eles possam ser melhor aproveitados pelos algoritmos de machine-learning e, dessa forma, gerar um melhor modelo na previsão de risco de crédito.

Isso irá incluir:

  1. Tratar variáveis categóricas
  2. Separar a classe-alvo das classes independentes
  3. Conjunto de treino e de teste
  4. Função val_model
  5. Modelo base
  6. Balanceamento dos dados

Vou iniciar fazendo uma cópia do conjunto df_clean para distinguirmos do processamento que será feito nessa seção.

# fazer cópia do dataset
df_proc = df_clean.copy()

8.1. Tratar variáveis categóricas

Na sequência, vamos tratar as variáveis categóricas. Para as variáveis que são do tipo string (object ou bool) será utilizado o Label Encoding que irá converter os dados categóricos em números.

# extrair os atributos categóricos
cat_cols = df_proc.select_dtypes(['object', 'bool']).columns

# aplicar LabelEconder nos atributos categóricos
for col in cat_cols:
df_proc[col+'_encoded'] = LabelEncoder().fit_transform(df_proc[col])
df_proc.drop(col, axis=1, inplace=True)

# verificar alterações
df_proc.head()
# verificar alterações
df_proc.info()

Com essa alteração os atributos tratados agora estão identificados com a adição de _encoded em seus nomes e todos estão em dados numéricos inteiros.

8.2. Separar a classe-alvo das classes independentes

Vamos segregar os dados da classe alvo, isto é, da variável target_default_encoded (antiga target_default que passou pelo tratamento do de Label Encoding) que é a que iremos prever. Isso serve para que os dados não contenham informações que auxiliem o algoritmo a identificar os casos de default. Para isso, primeiro é feito um embaralhamento dos registros (caso eles tenham alguma conexão que não conseguimos identificar, essa relação é quebrada para não ser detectada pelo modelo) e, então, separam-se as variáveis independentes em X, e a variável alvo (target_default_encoded) em uma variável y.

# embaralhar os dados
df_shuffled = df_proc.reindex(np.random.permutation(df_proc.index))

# separar a classe-alvo das classes independentes
X = df_shuffled.drop('target_default_encoded', axis=1)
y = df_shuffled['target_default_encoded']

# verificar tamanho das variáveis
print('As variáveis independentes estão em X: {} registros, {} atributos'.format(X.shape[0], X.shape[1]))
print('A variável alvo "target_default" está em y: {} registros.'.format(y.shape[0]))
'''
As variáveis independentes estão em X: 41731 registros, 36 atributos
A variável alvo "target_default" está em y: 41731 registros.
'''

8.3. Conjunto de treino e de teste

Para se ter um modelo genérico, isto é, que atenda dados do mundo real da melhor maneira possível, é necessário realizar a divisão do conjunto de dados em um grupo de dados de treino, com os quais o modelo irá aprender, e, outro conjunto denominado de testes, que servirá para avaliarmos o desempenho do modelo criado.

Essa divisão deve ocorrer de forma aleatória para evitar amostras enviesadas. Bem como, ela tem que ser feita antes do balanceamento dos dados, para que se possa confirmar que o balanceamento está adequado.

A divisão dos dados em Treino e Teste contém algumas configurações adicionais:

  • o tamanho foi definido em 70:30, o que significa que o conjunto de Treino conterá 80% do total dos dados, enquanto que o conjunto de Testes ficará com 30% do total do conjunto de dados;
  • os conjuntos de Treino e Teste conterão a mesma quantidade de classes, proporcionalmente, ou seja, a mesma quantidade de transações legítimas e transações fraudulentas;
  • a randomização dos dados foi ativada, para misturar os registros e, assim, garantir dados aleatórios nos conjuntos de Treino e de Teste;
  • e, foi indicado um valor de seed que irá proporcionar a reprodução do código sem alterações no resultado.

Por fim, cabe reforçar que com essa divisão nós só iremos utilizar os dados de teste na etapa final desse projeto. O intuito é conseguir uma avaliação mais próxima do que seria obtida com dados reais.

# dividir dados de treino e de teste
## stratify= y (para dividir de forma que as classes tenham a mesma proporção)
## random_state para que o resultado seja replicável
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,
stratify=y, shuffle=True,
random_state=123)

# verificar tamanho dos conjuntos
print('O conjunto de treino tem {} registros.'.format(X_train.shape[0]))
print('O conjunto de testes tem {} registros.'.format(X_test.shape[0]))
'''
O conjunto de treino tem 29211 registros.
O conjunto de testes tem 12520 registros.
'''

8.4. Função val_model

Para iniciar essa etapa, em primeiro lugar vou construir uma função denominada val_model com a construção de um pipeline (fluxo de trabalho) que irá padronizar os dados com o Standard Scaler (uma vez que temos presença de outliers no dataset), classificá-los (de acordo com o algoritmo que for inserido na função pelo parâmetro clf) e fazer a avaliação do modelo por meio da validação cruzada, levando em consideração o valor de Recall obtido. O resultado final dessa função será mostrar o valor médio de Recall encontrado na validação cruzada.

# construir função de avaliação de modelos
def val_model(X, y, clf, quite=False):
"""
Realiza cross-validation com os dados de treino para um determinado modelo.

# Arguments
X: Data Frame, contém as variáveis independentes.
y: Series, vetor contendo a variável alvo.
clf: modelo classificador do scikit-learn.
quite: bool, indicando se a função deve imprimir os resultados ou não.

# Returns
float, média dos scores da cross-validation.
"""

# converter variáveis em arrays
X = np.array(X)
y = np.array(y)

# criação de fluxo de trabalho
## 1. padronizar os dados com o StandardScaler
## 2. classificar os dados
pipeline = make_pipeline(StandardScaler(), clf)

# avaliação do modelo por validação cruzada
## de acordo com o valor de Recall
scores = cross_val_score(pipeline, X, y, scoring='recall')

# mostrar média do valor de Recall e desvio padrão do modelo
if quite == False:
print("Recall: {:.4f} (+/- {:.2f})".format(scores.mean(), scores.std()))

# retornar a média dos valores de Recall obtidos na validação cruzada
return scores.mean()

8.5. Modelo base

Para iniciar essa etapa da construção e avaliação de modelos de machine-learning, vou construir um modelo base que servirá de métrica de avaliação para os modelos seguintes. Ou seja, com o modelo base poderemos utilizá-lo para comparar os modelos criados e avaliar se houve aprimoramento ou não.

Ademais, esse modelo base não incluirá ajustes de hiperparâmetros, nem feature engineering, tão pouco os dados serão balanceados. Isso para que tenhamos o valor base mais bruto possível para também checarmos o quanto é possível melhorar o resultado.

Utilizando a função val_model vamos gerar o modelo de base. Isso será feito com base no classificador Random Forest e, conforme mencionado, nenhum parâmetro adicional será configurado.

# instanciar modelo de base
rf = RandomForestClassifier()

# avaliar desempenho do modelo com a função 'val_model'
score_baseline = val_model(X_train, y_train, rf)
'''
Recall: 0.0290 (+/- 0.00)
'''

O resultado mostra que com o modelo de Random Forest é possível obter um Recall de 0.0290, sendo que nos modelos obtidos durante a validação cruzada houve não houve um desvio padrão significativo.

Com isso, agora temos uma métrica base para comparar os próximos modelos que serão desenvolvidos e avaliar seus respectivos desempenhos em comparação com esse modelo bruto, que não possui nenhum ajuste.

Com a informação do Recall do modelo base, ou seja, nossa métrica de avaliação dos demais modelos, podemos passar para os próximos passos que são: balancear o conjunto de treino e verificar como diferentes classificadores se saem na construção de um modelo para o nosso problema.

8.6. Padronização e balanceamento dos dados

Como visto, o conjunto de dados está desbalanceado e é necessário rebalancear os dados para evitar a criação de um modelo que tenha baixa performance na identificação de default, bem como evitar o overfitting. Dessa forma criamos um bom modelo de machine-learning livre de viés.

O método utilizado para ser aplicado nesse caso é o Undersampling, que reduz a classe majoritária, excluindo esses dados de forma aleatória. Assim, as características da classe minoritária, que nesse caso são os dados de default, são preservados. E esses são os dados mais importantes para a solução do nosso problema.

Também, é necessário padronizar os dados para que estejam na mesma escala. Para isso usamos o StandardScaler.

Essas técnicas são aplicadas apenas no conjunto de treino para que as características do conjunto de testes não sejam desconfiguradas.

# instanciar modelo de padronização
scaler = StandardScaler().fit(X_train)

# aplicar padronização nos dados de treino
## apenas nas variáveis independentes
X_train = scaler.transform(X_train)

# instanciar modelo de undersampling
## random_state para que o resultado seja replicável
rus = RandomUnderSampler(random_state=123)

# aplicar undersampling nos dados de treino
X_train_rus, y_train_rus = rus.fit_resample(X_train, y_train)

# verificar o balanceamento das classes
print(pd.Series(y_train_rus).value_counts())
'''
0 4663
1 4663
Name: target_default_encoded, dtype: int64
'''

Após a aplicação do método Undersampling, vemos que ambas as classes agora tem a mesma proporção. Isto é, para 0 (ou não-default) temos 4663 registros e, para 1 (ou default), o mesmo valor.

9. Métricas de Avaliação de Performance

Para avaliar o desempenho dos modelos a métrica que será utilizada é o valor de Recall. Isso porque quando se tem dados desbalanceados, como é o caso do conjunto em estudo, ainda que ele já tenha sido tratado e balanceado, a acurácia não é uma boa métrica de avaliação. O motivo disso se deve ao fato de que se pode obter um resultado de acurácia muito alto, entretanto, na identificação de default, a detecção ter um resultado muito baixo, o que não nos leva ao nosso objetivo.

O Recall é a métrica que nos fornece a melhor medida para o problema específico em estudo. Isso se deve porque, no caso de default, os Falsos Negativos são mais prejudiciais para uma empresa do que os Falsos Positivos. Em outras palavras, é melhor que o modelo erre ao dizer que um cliente é default, quando na realidade ele não seja. Do que erre ao apontar um cliente como não sendo default, quando na verdade ele é, trazendo prejuízos para os negócios.

Tendo isso em vista, busca-se uma alta taxa de Recall.

O Recall olha para todas os defaults e visa responder: o quanto o modelo acerta? O resultado será um valor entre 0 e 1, sendo que, quanto mais próximo de 1, melhor, pois indica uma baixa taxa de Falsos Negativos.

Seu cálculo é dado por:

Ainda pensando na finalidade desse estudo, outra métrica de avaliação de modelos de classificação que pode ser utilizada é a AUC — Area Under the Curve, traduzindo é a Area abaixo da curva. Ela deriva do ROC — Receiver Operating Characteristic — e indica para nós o quanto o modelo consegue distinguir entre duas coisas. No nosso exemplo, entre um cliente default e não-default. O valor dado por AUC varia entre 0 e 1, sendo que, quanto mais próximo de 1, indica que melhor é o modelo.

Por fim, a matriz de confusão compara os valores previstos com os valores reais, mostrando os erros e acertos do modelo. Sua saída tem 4 valores diferentes, dispostos da seguinte forma:

Sendo que, cada um desses valores corresponde a:

Acertos do Modelo

  • Verdadeiro Positivo: É default e o modelo classificou como default.
  • Verdadeiro Negativo: Não é default e o modelo classificou como não sendo default.

Erros do Modelo

  • Falso Positivo: Não é default, mas o modelo acusa como default.
  • Falso Negativo: É default, mas o modelo classifica como não sendo default.

Portanto, resumidamente, para avaliar o desempenho dos modelos, olharemos prioritariamente para o Recall e, depois para a matriz de confusão e AUC.

10. Criação de modelos de Machine-Learning

Nessa parte serão criados diversos modelos de machine-learning, com o uso da função val_model criada anteriormente. O objetivo é identificar os modelos com melhor desempenho para que sejam comparados com o XGBoost (após o tunning dos hiperparâmetros desse algoritmo).

Os modelos que serão criados e avaliados nessa fase são:

  • Random Forest
  • Decision Tree
  • Stochastic Gradient Descent
  • SVC
  • Regressão Logística
  • XGBoost
  • Light GBM

Reforçando que a função val_model irá: padronizar os dados, aplicar o classificador, fazer a validação cruzada e retornar o valor médio de Recall encontrado.

# instanciar os modelos
rf = RandomForestClassifier()
knn = KNeighborsClassifier()
dt = DecisionTreeClassifier()
sgdc = SGDClassifier()
svc = SVC()
lr = LogisticRegression()
xgb = XGBClassifier()
lgbm = LGBMClassifier()

# criar listas para armazenar:
## o modelo do classificador
model = []
## o valor do Recall
recall = []

# criar loop para percorrer os modelos de classificação
for clf in (rf, knn, dt, sgdc, svc, lr, xgb, lgbm):

# identificar o classificador
model.append(clf.__class__.__name__)

# aplicar função 'val_model' e armazenar o valor de Recall obtido
recall.append(val_model(X_train_rus, y_train_rus, clf, quite=True))

# salvar o resultado de Recall obtido em cada modelo de classificação em uma variável
results = pd.DataFrame(data=recall, index=model, columns=['Recall'])

# mostrar os modelos com base no valor de Recall obtido, do maior para o menor
results.sort_values(by='Recall', ascending=False)

Pode-se verificar que os melhores modelos, no topo da tabela, foram o LGBMClassifier, o XGBoostClassifier e o RanfomForestClassifier pontos. Enquanto o KNeighborsClassifier foi o algoritmo com o pior desempenho.

11. Por que XGBoost?

O XGBoost vem da família de classificadores supervisionados do tipo Árvores de Decisão. Sua sigla significa Extreme Gradient Boosting e ele vem sendo muito utilizado por profissionais da área devido ao seu alto grau de precisão e acurácia na criação dos modelos.

Isso se deve, parte, à grande quantidade de hiperparâmetros que podem ser ajustados, melhorando a performance do modelo e, com isso pode ser aplicado à problemas de diversos tipos, como classificação, regressão e detecção de anomalias, e dos mais diversos setores.

12. XGBoost: Otimização de Hiperparâmetros

Por conta do problema que estamos tentando revolver (ver Objetivo Geral, bem como, pelo desempenho dos algoritmos na sessão anterior. O XGboost possui um potencial de melhorar muito seu resultado por meio de ajustes nos seus hiperparâmetros. O que, comparativamente com a maior parte dos algoritmos acima, é uma quantidade maior e que, portanto, dá essa probabilidade maior também de aprimorar a sua performance.

Em razão da quantidade de hiperparâmetros que precisam ser ajustados, é necessário encontrar o melhor valor para cada um deles em processos distintos. Para iniciar, se definiu a taxa de aprendizado em 0.1. A recomendação é de seja entre 0.05 e 0.3, de acordo com o problema a ser resolvido.

Vamos fazer a busca para o melhor parâmetro para n_estimators que é a quantidade de árvores de decisão que deve conter no algoritmo. E, vamos utilizar um valor de semente para poder reproduzir os resultados.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
xgb = XGBClassifier(learning_rate=0.1, random_state=seed)

# definir dicionário para descobrir a quantidade ideal de árvores
# em um range de 0 a 500 com incremento de 50
param_grid = {'n_estimators':range(0,500,50)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_train_rus, y_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6625 para {'n_estimators': 100}
'''

O melhor resultado encontrado foi com 100 árvores de decisão e, com apenas esse hiperparâmetro já superamos o LGBMClassifier que ficou em primeiro lugar na lista com um Recall de 0.656231.

Com esse parâmetro, como o incremento definido foi de 50, podemos refinar a busca em um intervalo menor e com passos menores:

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
xgb = XGBClassifier(learning_rate=0.1, random_state=seed)

# definir dicionário para descobrir a quantidade ideal de árvores
## em um range de 25 a 125 com incremento de 5
param_grid = {'n_estimators':range(25,125,5)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_train_rus, y_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6633 para {'n_estimators': 85}
'''

Com essa nova busca, o melhor valor encontrado para a quantidade de árvores foi de 85.

Com o melhor parâmetro para n_estimators, podemos defini-lo na atribuição do classificador e prosseguir para determinar os melhores valores para max_depth, que determina a profundidade das árvores de decisão e, para o min_child_weight, ou seja, o peso mínimo do nó para que seja criado um novo nó.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
## n_estimators: 85
xgb = XGBClassifier(learning_rate=0.1, n_estimators=85, random_state=seed)

# definir dicionário para descobrir:
## a profundidade ideal em um range de 1 a 7 com incremento de 1
## o peso mínimo do nó em um range de 1 a 4 com incremento de 1
param_grid = {'max_depth':range(1,8,1),
'min_child_weight':range(1,5,1)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_train_rus, y_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6633 para {'max_depth': 6, 'min_child_weight': 1}
'''

Os valores obtidos para a profundidade (max_depth) foi de 6 e o min_child_weight é de 1. Temos mais 2 parâmetros para ajustar!

Podemos inserir os novos parâmetros encontrados e partir para encontrar o próximo: gamma. Esse parâmetro define a complexidade das árvores no modelo.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
## n_estimators: 85
## max_depth: 6
## min_child_weight: 1
xgb = XGBClassifier(learning_rate=0.1, n_estimators=85, max_depth=6, min_child_weight=1, random_state=seed)

# definir dicionário para descobrir:
## a profundidade ideal em um range de 0.0 a 0.4
param_grid = {'gamma':[i/10.0 for i in range(0,5)]}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_train_rus, y_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6640 para {'gamma': 0.1}
'''

Seguimos inserindo esses valores na atribuição do modelo de classificação e vamos buscar agora o melhor valor para a taxa de aprendizado (learning_rate).

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a semente
## n_estimators: 85
## max_depth: 6
## min_child_weight: 1
## gamma = 0.1
xgb = XGBClassifier(n_estimators=85, max_depth=6, min_child_weight=1, gamma=0.1, random_state=seed)

# definir dicionário para descobrir a taxa de aprendizado ideal
param_grid = {'learning_rate':[0.01, 0.05, 0.1, 0.2]}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_train_rus, y_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6640 para {'learning_rate': 0.1}
'''

Finalmente, agora com os hiperparâmetros definidos podemos usar o modelo gerado para usar o nosso conjunto de testes e verificar como seria o desempenho dele com dados reais.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo final do XGBoost com os melhores hiperparâmetros encontrados
xgb = XGBClassifier(learning_rate=0.1 , n_estimators=85, max_depth=6, min_child_weight=1, gamma=0.1, random_state=seed)

# treinar o modelo com dados de treino
xgb.fit(X_train_rus, y_train_rus)

# padronizar os dados de teste
X_test = scaler.transform(X_test)

# fazer previsões com os dados de teste
y_pred = xgb.predict(X_test)

Com os resultados obtidos, vou gerar um relatório com métricas de avaliação e com o valor da AUC. Vou ainda plotar a matriz de confusão regular, outra matriz com os dados normalizados e o AUC.

# imprimir relatório de métricas de avaliação
print('Relatório de Métricas de Avaliação'.center(65) + ('\n') + ('-' * 65))
print(classification_report(y_test, y_pred, digits=4) + ('\n') + ('-' * 15))

# imprimir AUC
print('AUC: {:.4f} \n'.format(roc_auc_score(y_test, y_pred)) + ('-' * 65))

# plotar gráficos
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 4))

# matriz de confusão normalizada
skplt.metrics.plot_confusion_matrix(y_test, y_pred, normalize=True,
title='Matriz de Confusão Normalizada',
text_fontsize='large', ax=ax[0])

# matriz de confusão
skplt.metrics.plot_confusion_matrix(y_test, y_pred,
title='Matriz de Confusão',
text_fontsize='large', ax=ax[1])

# AUC
y_prob = xgb.predict_proba(X_test)
skplt.metrics.plot_roc(y_test, y_prob, title='AUC', cmap='brg', text_fontsize='small', plot_macro=False, plot_micro=False, ax=ax[2])

### imprimir valor AUC
auc = (roc_auc_score(y_test, y_pred) * 100).round(2)
ax[2].text(0.3, 0.6, auc, fontsize=14, color='#004a8f', fontweight='bold')

plt.show()
'''
Relatório de Métricas de Avaliação
-----------------------------------------------------------------
precision recall f1-score support

0 0.9121 0.6808 0.7796 10522
1 0.2803 0.6547 0.3925 1998

accuracy 0.6766 12520
macro avg 0.5962 0.6677 0.5861 12520
weighted avg 0.8113 0.6766 0.7179 12520

---------------
AUC: 0.6677
-----------------------------------------------------------------
'''

13. Engenharia de Atributos

O processo de feature engineering consiste na criação de novas variáveis a partir dos dados que já existem com o objetivo de melhorar o desempenho de um modelo de machine-learning.

Trouxe ele nessa etapa para que se pudesse comparar os resultados obtidos com e sem o feature engineering, uma vez que esse procedimento demanda conhecimento do problema por parte do analista, bem como tempo para analisar as melhores estratégias a serem abordadas.

Ao final da etapa de Limpeza e Tratamento de Dados temos o dataset df_clean. Vamos retomar sua estrutura:

# verificar tamanho do dataset
df_clean.shape
'''
(41731, 37)
'''
# verificar primeiras entradas do dataset
df_clean.head(3)

Relembrando que esse dataset já teve alguns atributos removidos, tratamento de dados ausentes, modificação de variáveis, correção de erro de tipo, remoção de outliers, etc., que pode ser conferido na etapa de Limpeza e Tratamento dos Dados.

Nessa etapa iremos:

  1. remover variáveis que não obtivemos informações suficientes para interpretá-las
  2. criar novas variáveis

Por fim, toda a etapa de preparação dos dados será feita para prosseguirmos com a criação de modelos de machine-learning.

Para iniciar vou fazer uma cópia desse dataset para trabalhar nele.

# fazer cópia do dataset
df_fe = df_clean.copy()

13.1. Remover variáveis

As variáveis listadas abaixo são aquelas que não pudemos inferir seu significado com maior precisão e precisam de mais estudos, por isso, dessa vez serão removidos do dataset.

  • real_state
  • zip
  • reason
  • job_name
  • external_data_provider_first_name
  • profile_tags
  • state
  • user_agent
# remover atributos
df_fe.drop(['real_state', 'zip', 'reason', 'job_name', 'profile_tags', 'state',
'external_data_provider_first_name', 'user_agent'], axis=1, inplace=True)

# verificar alterações
df_fe.head(3)

13.2. Criar novas variáveis

Vou criar as seguintes novas variáveis:

2.1. lat_long nos fornece informações de localização de latitude e longitude, separadas por vírgula. Vou separar essas informações e reduzir a quantidade de casas decimais para duas, uma vez que com essa quantidade de casas estaremos em distâncias de 1 Km (Gizmodo).

2.2. shipping_state possui informações de país e Estado, também separados por um hífen, vou separar essas informaçãos, confirmar a quantidade de variáveis diferentes (para checar em país, se houver apenas 1 valor o atributo poderá ser removido e, em Estado para checar se não há erros de tipo).

13.2.1. Extrair informações de lat_lon

Para extrair as informações de lat_lon vou usar a função str.extract e inserir as informações, cada qual, em dois novos atributos lat e lon. Por fim, vou verificar o tipo da variável.

# extrair informações de 'lat_lon'
df_fe[['lat', 'lon']] = df_fe['lat_lon'].str.extract(r'\((.*), (.*)\)')

# verificar tipo dos novos atributos
print(df_fe[['lat', 'lon']].dtypes)
'''
lat object
lon object
dtype: object
'''

Os novos atributos estão em tipo string, logo, precisamos alterar para tipo float. Também vou aproveitar para reduzir a quantidade de casas decimais para apenas 2.

# alterar tipo de 'lat' e 'lon' para float
df_fe['lat'] = (df_fe['lat'].astype(float)).round(2)
df_fe['lon'] = (df_fe['lon'].astype(float)).round(2)

# verificar tipo dos novos atributos
print(df_fe[['lat', 'lon']].dtypes)
'''
lat float64
lon float64
dtype: object
'''
# verificar dataset
df_fe.head(3)

Está tudo certo, então, basta remover o atributo lat_lon para não ficar duplicado no dataset.

# remover atributo 'lat_lon'
df_fe.drop(['lat_lon'], axis=1, inplace=True)

# verificar alterações
df_fe.head(3)

13.2.2. Extrair informações de shipping_state

Para extrair as informações de país e estado, vou usar a função str.split e salvar as informações em duas novas variáveis: country e state. Ao final, vou verificar o tipo desses novos atributos criados.

# extrair informações de 'shipping_state'
df_fe[['country', 'state']] = df_fe['shipping_state'].str.split('-', expand=True)

# verificar tipo dos novos atributos
print(df_fe[['country', 'state']].dtypes)
'''
country object
state object
dtype: object
'''
# verificar dataset
df_fe.head(3)

Vamos remover o atributo shipping_state e vamos checar os valores únicos para country e para state. Uma vez que em country talvez possamos encontrar apenas um valor e, nesse caso, poderemos descartar o atributo por não agregar nenhuma informação para a construção do modelo de machine-learning.

# remover atributo 'lat_lon'
df_fe.drop(['shipping_state'], axis=1, inplace=True)

# checar valores únicos de entrada para 'country' e 'state'
df_fe[['country', 'state']].nunique()
'''
country 1
state 25
dtype: int64
'''

Como para país temos apenas um único valor, podemos excluí-lo do dataset sem prejuízos. Além disso, vamos checar os valores em state.

# remover atributo 'country'
df_fe.drop(['country'], axis=1, inplace=True)

# verificar valores em 'state'
df_fe.state.unique()
'''
array(['MT', 'RS', 'RR', 'RN', 'SP', 'AC', 'MS', 'PE', 'AM', 'CE', 'SE',
'AP', 'MA', 'BA', 'TO', 'RO', 'SC', 'GO', 'PR', 'MG', 'ES', 'DF',
'PA', 'PB', 'AL'], dtype=object)
'''

Podemos constatar acima que todos estão bem preenchidos e tratam-se de fato dos estados brasileiros e Distrito Federal.

# verificar alterações
df_fe.head(3)

13.3. Preparação dos dados

A preparação dos dados envolve as seguintes etapas, conforme fizemos anteriormente:

  1. Tratar variáveis categóricas
  2. Separar a classe-alvo das classes independentes
  3. Conjunto de treino e de teste
  4. Modelo base
  5. Padronização e balanceamento dos dados

E, mais uma vez, vou começar copiando do dataset com o qual concluímos a etapa de engenharia de atributos: df_fe.

# fazer cópia do dataset
df_fe_proc = df_fe.copy()

13.3.1. Tratar variáveis categóricas

Aqui, vou alterar um pouco a forma do tratamento das variáveis daquela feita anteriormente, com o objetivo de aprimorar o desempenho do modelo. Agora, apenas as variáveis do tipo booleanas (bool) passarão pelo processo de Label Encoding. As variáveis categóricas (string) passarão pelo processo de conhecido como variáveis dummy. Nesse método, a variável irá assumir um valor 0 ou 1 para indicar a ausência ou a presença de determinada variável.

# identificar os atributos booleanos
bol_var = df_fe_proc.select_dtypes(['bool']).columns

# aplicar LabelEconder nos atributos booleanos
for col in bol_var:
df_fe_proc[col+'_encoded'] = LabelEncoder().fit_transform(df_fe_proc[col])
df_fe_proc.drop(col, axis=1, inplace=True)
## identificar os atributos categóricos
cat_var = df_fe_proc.select_dtypes(['object']).columns

# aplicar variáveis dummy
df_fe_proc = pd.get_dummies(df_fe_proc, columns=cat_var)

# checar alterações no data frame
df_fe_proc.head()

Lembrando que, com essa alteração, os atributos tratados agora estão identificados com a adição de encoded em seus nomes e todos estão em dados numéricos inteiros.

Vamos checar o tamanho que ficou o data frame tratado e processado.

##verificar tamanho do data frame
df_fe_proc.shape
'''
(41731, 105)
'''

Observe que passamos de 37 para 105 atributos.

13.3.2. Separar a classe-alvo das classes independentes

Vamos separar a classe-alvo das classes independentes:

# embaralhar os dados
df_fe_shuffled = df_fe_proc.reindex(np.random.permutation(df_fe_proc.index))

# separar a classe-alvo das classes independentes
X_fe = df_fe_shuffled.drop('target_default_encoded', axis=1)
y_fe = df_fe_shuffled['target_default_encoded']

# verificar tamanho das variáveis
print('As variáveis independentes estão em X: {} registros, {} atributos'.format(X_fe.shape[0], X_fe.shape[1]))
print('A variável alvo "target_default" está em y: {} registros.'.format(y_fe.shape[0]))
'''
As variáveis independentes estão em X: 41731 registros, 104 atributos
A variável alvo "target_default" está em y: 41731 registros.
'''

13.3.3. Conjunto de treino e de teste

Dividimos o conjunto em dados de treino e de teste.

# dividir dados de treino e de teste
## stratify= y (para dividir de forma que as classes tenham a mesma proporção)
## random_state para que o resultado seja replicável
X_fe_train, X_fe_test, y_fe_train, y_fe_test = train_test_split(X_fe, y_fe, test_size=0.3,
stratify=y_fe, shuffle=True,
random_state=123)

# verificar tamanho dos conjuntos
print('O conjunto de treino tem {} registros.'.format(X_fe_train.shape[0]))
print('O conjunto de testes tem {} registros.'.format(X_fe_test.shape[0]))
'''
O conjunto de treino tem 29211 registros.
O conjunto de testes tem 12520 registros.
'''

13.3.4. Modelo base

Assim como foi feito anteriormente, vou gerar novamente o modelo de base com a Random Forest, para comparar o desempenho dos demais algoritmos.

# criar baseline e ver desempenho
fe_rf = RandomForestClassifier()

# avaliar desempenho do modelo com a função 'val_model'
fe_score_baseline = val_model(X_fe_train, y_fe_train, fe_rf)
'''
Recall: 0.0513 (+/- 0.00)
'''

Já pode-se perceber uma melhora significativa entre modelos base, uma vez que sem a engenharia de atributos o valor de Recall foi 0.0290 e, agora temos uma melhora de quase 2 vezes esse valor, em 0.0513!

13.3.5. Padronização e balanceamento dos dados

Vamos prosseguir para o balanceamento dos dados para que possamos gerar mais algoritmos.

# instanciar modelo de padronização
scaler = StandardScaler().fit(X_fe_train)

# aplicar padronização nos dados de treino
## apenas nas variáveis independentes
X_fe_train = scaler.transform(X_fe_train)

# instanciar modelo de undersampling
## random_state para que o resultado seja replicável
fe_rus = RandomUnderSampler(random_state=43)

# aplicar undersampling nos dados de treino
X_fe_train_rus, y_fe_train_rus = fe_rus.fit_resample(X_fe_train, y_fe_train)

# verificar o balanceamento das classes
print(pd.Series(y_fe_train_rus).value_counts())
'''
0 4663
1 4663
Name: target_default_encoded, dtype: int64
'''

13.4. Criação de modelos de Machine-Learning

Vou gerar novamente vários modelos com diferentes classificadores, que serão avaliados pelo valor de Recall. O objetivo será de comparar esses resultados com os anteriores, que não possuem a aplicação de engenharia de atributos, bem como, de comparar os resultados obtidos nos classificadores para que possamos acompanhar o desenvolvimento do modelo XGBoost com otimização dos hiperparâmetros.

# instanciar os modelos
rf_fe = RandomForestClassifier()
knn_fe = KNeighborsClassifier()
dt_fe = DecisionTreeClassifier()
sgdc_fe = SGDClassifier()
svc_fe = SVC()
lr_fe = LogisticRegression()
xgb_fe = XGBClassifier()
lgbm_fe = LGBMClassifier()

# criar listas para armazenar:
## o modelo do classificador
fe_model = []
## o valor do Recall
fe_recall = []

# criar loop para percorrer os modelos de classificação
for fe_clf in (rf_fe, knn_fe, dt_fe, sgdc_fe, svc_fe, lr_fe, xgb_fe, lgbm_fe):

# identificar o classificador
fe_model.append(fe_clf.__class__.__name__)

# aplicar função 'val_model' e armazenar o valor de Recall obtido
fe_recall.append(val_model(X_fe_train_rus, y_fe_train_rus, fe_clf, quite=True))

# salvar o resultado de Recall obtido em cada modelo de classificação em uma variável
fe_results = pd.DataFrame(data=fe_recall, index=fe_model, columns=['Recall'])

# mostrar os modelos com base no valor de Recall obtido, do maior para o menor
fe_results.sort_values(by='Recall', ascending=False)

Vamos comparar:

Percebe-se que a melhora no desempenho ocorreu em apenas metade dos algoritmos treinados. Contudo, quando houve melhora ela foi mais significativa do que a perda em desempenha, nos casos em que o modelo piorou. Vale destacar que no caso do XGBoost o modelo teve uma queda de -0.005567 e que foi a maior diferença entre os modelos com pior desempenho. Contudo, na otimização dos hiperparâmetros a seguir, poderemos checar de fato se a engenharia de atributos atribuirá ou não algum diferencial nos resultados.

14. XGBoost: Engenharia de Atributos + Otimização de Hiperparâmetros

Aqui, vamos repetir o processo de otimização dos hiperparâmetros do algoritmo XGBoost para encontrar os valores que produzem os melhores desempenhos.

Vou começar a busca para o melhor valor para n_estimators que é a quantidade de árvores de decisão que deve conter no algoritmo.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
xgb = XGBClassifier(learning_rate=0.1, random_state=seed)

# definir dicionário para descobrir a quantidade ideal de árvores
# em um range de 0 a 500 com incremento de 50
param_grid = {'n_estimators':range(0,500,50)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_fe_train_rus, y_fe_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6537 para {'n_estimators': 50}
'''

O melhor resultado encontrado foi com 50 árvores de decisão. Com esse parâmetro, como o incremento definido foi de 50, podemos refinar a buscar em torno desse valor, com passos menores:

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
xgb = XGBClassifier(learning_rate=0.1, random_state=seed)

# definir dicionário para descobrir a quantidade ideal de árvores
## em um range de 25 a 75 com incremento de 5
param_grid = {'n_estimators':range(25,75,5)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_fe_train_rus, y_fe_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6620 para {'n_estimators': 25}
'''

Com essa nova busca, o melhor valor encontrado para a quantidade de árvores foi de 25. Dessa vez, apenas com esse ajuste melhoramos o desempenho do algoritmo de 0.642724 para 0.6620. Esse resultado já garantiria o segundo lugar nos modelos base!

Temos, ainda, outros 4 tunnings para ajustar, então, vamos prosseguir.

Com o melhor parâmetro para n_estimators, podemos defini-lo na atribuição do classificador e prosseguir para determinar os melhores valores para max_depth, que determina a profundidade das árvores de decisão e, para min_child_weight, ou seja, o peso mínimo do nó para que seja criado um novo nó.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
## n_estimators: 25
xgb = XGBClassifier(learning_rate=0.1, n_estimators=25, random_state=seed)

# definir dicionário para descobrir:
## a profundidade ideal em um range de 1 a 7 com incremento de 1
## o peso mínimo do nó em um range de 1 a 4 com incremento de 1
param_grid = {'max_depth':range(1,8,1),
'min_child_weight':range(1,5,1)}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_fe_train_rus, y_fe_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6637 para {'max_depth': 6, 'min_child_weight': 2}
'''

Podemos inserir os novos parâmetros encontrados e partir para encontrar o próximo: gamma. Esse parâmetro define a complexidade das árvores no modelo.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a taxa de aprendizado em 0.1 e definir a semente
## n_estimators: 25
## max_depth: 6
## min_child_weight: 1
xgb = XGBClassifier(learning_rate=0.1, n_estimators=25, max_depth=6, min_child_weight=2, random_state=seed)

# definir dicionário para descobrir:
## a profundidade ideal em um range de 0.0 a 0.4
param_grid = {'gamma':[i/10.0 for i in range(0,5)]}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_fe_train_rus, y_fe_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6637 para {'gamma': 0.0}
'''

Mais uma vez, o melhor valor encontrado foi o valor mínimo, então, prosseguimos inserindo esse valor na atribuição do modelo de classificação e buscando o melhor valor para a taxa de aprendizado (learning_rate).

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo XGBoost
# definir a semente
## n_estimators: 25
## max_depth: 6
## min_child_weight: 1
## gamma = 0.1
xgb = XGBClassifier(n_estimators=25, max_depth=6, min_child_weight=2, gamma=0.0, random_state=seed)

# definir dicionário para descobrir a taxa de aprendizado ideal
param_grid = {'learning_rate':[0.01, 0.05, 0.1, 0.2]}

# configurar validação cruzada com 10 dobras estratificadas
# shuffle=True para embaralhar os dados antes de dividir e definir a semente
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)

# configuração da busca de combinações cruzadas com o classificador XGBoost
# parâmetros de árvores do param_grid
# scoring: método de avaliação por Recall
# n_jobs=1 para busca em paralelo (usando todos os núcleos disponíveis)
# cv: estratégia de validação
grid_search = GridSearchCV(xgb, param_grid, scoring="recall", n_jobs=-1, cv=kfold)

# realizar a busca do melhor parâmetro para 'n_estimators'
grid_result = grid_search.fit(X_fe_train_rus, y_fe_train_rus)

# imprimir o melhor parâmetro encontrado para 'n_estimators'
print("Melhor: {:.4f} para {}".format(grid_result.best_score_, grid_result.best_params_))
'''
Melhor: 0.6663 para {'learning_rate': 0.05}
'''

Com os valores de hiperparâmetros ajustados, vou rodar o conjunto de testes com o modelo criado.

# definir uma semente para reprodutibilidade
seed = 123

# instanciar o modelo final do XGBoost com os melhores valores encontrados
xgb = XGBClassifier(learning_rate=0.05 , n_estimators=25, max_depth=6, min_child_weight=2, gamma=0.0)

# treinar o modelo com dados de treino
xgb.fit(X_fe_train_rus, y_fe_train_rus)

# padronizar os dados de teste
X_fe_test = scaler.transform(X_fe_test)

# fazer previsões com os dados de teste
y_fe_pred = xgb.predict(X_fe_test)

Por fim, vou imprimir um relatório com métricas de avaliação e a AUC. Ainda, vou plotar a matriz de confusão regular, com os dados normalizados e a AUC.

# imprimir relatório de métricas de avaliação
print('Relatório de Métricas de Avaliação'.center(65) + ('\n') + ('-' * 65))
print(classification_report(y_fe_test, y_fe_pred, digits=4) + ('\n') + ('-' * 15))

# imprimir AUC
print('AUC: {:.4f} \n'.format(roc_auc_score(y_fe_test, y_fe_pred)) + ('-' * 65))

# plotar gráficos
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 4))

# matriz de confusão normalizada
skplt.metrics.plot_confusion_matrix(y_fe_test, y_fe_pred, normalize=True,
title='Matriz de Confusão Normalizada',
text_fontsize='large', ax=ax[0])

# matriz de confusão
skplt.metrics.plot_confusion_matrix(y_fe_test, y_fe_pred,
title='Matriz de Confusão',
text_fontsize='large', ax=ax[1])

# AUC
y_fe_prob = xgb.predict_proba(X_fe_test)
skplt.metrics.plot_roc(y_fe_test, y_fe_prob, title='AUC', cmap='brg', text_fontsize='small', plot_macro=False, plot_micro=False, ax=ax[2])

### imprimir valor AUC
auc = (roc_auc_score(y_fe_test, y_fe_pred) * 100).round(2)
ax[2].text(0.3, 0.6, auc, fontsize=14, color='#004a8f', fontweight='bold')

plt.show()
'''
Relatório de Métricas de Avaliação
-----------------------------------------------------------------
precision recall f1-score support

0 0.9174 0.6600 0.7677 10522
1 0.2774 0.6872 0.3952 1998

accuracy 0.6644 12520
macro avg 0.5974 0.6736 0.5815 12520
weighted avg 0.8153 0.6644 0.7083 12520

---------------
AUC: 0.6736
-----------------------------------------------------------------
'''

15. Comparativo dos Modelos XGBoost

Nessa sessão vamos plotar os resultados dos modelos preditivos gerados com XGBoost, sem e com a engenharia de atributos, lado-a-lado para que fique mais fácil a comparação entre eles.

15.1. Matriz de Confusão

A matriz de confusão nos entrega 4 valores diferentes. Sendo que, cada um desses valores corresponde a:

Acertos do Modelo

  • Verdadeiro Positivo: É default e o modelo classificou como default.
  • Verdadeiro Negativo: Não é default e o modelo classificou como não default.

Erros do Modelo

  • Falso Positivo: Não é default, mas o modelo acusa como sendo default.
  • Falso Negativo: É default, mas o modelo classifica como não sendo default.

Vou plotar os resultados gerados pela matriz de confusão, em ambos os modelos criados, lado-a-lado, para que fique mais fácil a comparação entre eles.

# plotar matriz de confusão com dados normalizados
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))

# XGBoost
skplt.metrics.plot_confusion_matrix(y_test, y_pred, normalize=True,
title='XGBoost',
text_fontsize='large', ax=ax[0])

# XGBoost com Feature Engineering
skplt.metrics.plot_confusion_matrix(y_fe_test, y_fe_pred, normalize=True,
title='com Feature Engineering',
text_fontsize='large', ax=ax[1])

plt.show()

Outra forma interessante de visualizar a matriz de confusão é por meio dos dados gerados, isto é, dessa forma saberemos quantos clientes foram detectadas como default, quantos o modelo acertou e quanto casos o modelo errou.

# plotar matriz de confusão
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))

# XGBoost
skplt.metrics.plot_confusion_matrix(y_test, y_pred,
title='XGBoost',
text_fontsize='large', ax=ax[0])

# XGBoost com Feature Engineering
skplt.metrics.plot_confusion_matrix(y_fe_test, y_fe_pred,
title='com Feature Engineering',
text_fontsize='large', ax=ax[1])

plt.show()

De um total de 12.520 clientes analisados, observa-se que o modelo XGBoost sem engenharia de atributos acertou as previsões de default em 1.308 dos casos. Já o modelo com feature engineering estava certo 1.373 vezes.

Então, analisando os resultados encontrados em ambos os modelos, se tem que o algoritmo gerado pela XGBoost com a engenharia de atributos é levemente superior ao modelo criado com o uso de feature engineering.

Contudo, se olharmos para os acertos em geral, isto é, os Verdadeiros Positivos mais os Verdadeiros Negativos, teremos que o modelo com a engenharia de atributos é levemente inferior ao modelo sem o feaature engineering e, podemos reparar que isso se deve ao fato do modelo com a engenharia de atributos ter mais erros em Falsos Positivos, ou seja, aponta que um cliente é default, quando na verdade não é. E isso, também pode vir a ser um problema uma vez que a empresa deixar de emprestar dinheiro e lucrar com um cliente que seria um bom pagador.

15.2. AUC

Abaixo, trago os resultados plotados para a curva AUC, bem como o valor encontrado, lado-a-lado, para que fique mais fácil a comparação dessa medida.

# plotar AUC
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))

## XGBoost
skplt.metrics.plot_roc(y_test, y_prob, title='XGBoost',
cmap='brg', text_fontsize='small', plot_macro=False,
plot_micro=False, ax=ax[0])

### imprimir valor AUC para XGBoost
auc = (roc_auc_score(y_test, y_pred) * 100).round(2)
ax[0].text(0.1, 0.8, auc, fontsize=14, color='#004a8f', fontweight='bold')

## XGBoost com Feature Engineering
skplt.metrics.plot_roc(y_fe_test, y_fe_prob, title='com Feature Engineering',
cmap='brg', text_fontsize='small', plot_macro=False,
plot_micro=False, ax=ax[1])

### imprimir valor AUC para XGBoost com Feature Engineering
auc_fe = (roc_auc_score(y_fe_test, y_fe_pred) * 100).round(2)
ax[1].text(0.15, 0.7, auc_fe, fontsize=14, color='#004a8f', fontweight='bold')

plt.show()

Têm-se que o valor de AUC para o algoritmo XGBoost sem aplicação de feature engineering é de 66.77%, ou seja, é inferior ao valor de 67.36% dado ao modelo em que se usou engenharia de atributos.

15.3. Recall

Por fim, os valores de Recall para cada modelo. Lembrando que essa é a métrica que nos dá a melhor medida para avaliar o nosso problema em questão. Portanto, quanto maior seu resultado, melhor o modelo será para identificar default.

# imprimir resultados de Recall
print('RECALL'.center(30) + '\n' + ('-' * 30))

## XGBoost
print('\t\tXGBoost: {:.2f}%'.format((recall_score(y_test, y_pred)) * 100))

# com Feature Engineering
print('com Feature Engineering: {:.2f}%'.format((recall_score(y_fe_test, y_fe_pred)) * 100))
'''
RECALL
------------------------------
XGBoost: 65.47%
com Feature Engineering: 68.72%
'''

O valor de Recall é levemente superior no modelo de XGBoost com o uso de engenharia de atributos.

16. Teste de Hipótese

Apesar de numericamente observarmos que o Recall do modelo com a engenharia de atributos ter sido 3.25% superior ao modelo sem a engenharia, podemos realizar um teste estatístico e comprovar se esse melhor desempenho é significativo ou não.

Para isso, vou utilizar um teste de proporções conhecido como teste Z, para duas proporções, que é o que melhor se adequa ao nosso caso. Ou seja, ele determina se as diferenças entre as proporções de duas amostras independentes são estatisticamente significativas. Para isso, vou considerar as seguintes hipóteses:

  • Hipótese Nula (H0) ▶ p-value> 0.05
    Não há diferença significativa no valor de Recall entre o modelo com e sem engenharia de atributos.
  • Hipótese Alternativa (H1) ▶ p-value<= 0.05
    Rejeita a hipótese nula, ou seja, existe diferença entre os dois modelos avaliados.

Nota

O nível de significância (p-value) é um valor determinado de acordo com cada caso para determinar um limite para aceitar ou rejeitar a hipótese nula. Assim, quanto menor for o valor dado por p-value, mais forte são as evidências contra a hipótese nula. Por exemplo, se o resultado obtido em p-value fosse de 0.04, logo, conseguiríamos rejeitar a hipótese nula e defender que o modelo com engenharia de atributos é superior com um nível de confiança de 96%.

Portanto, se o valor ficar acima do threshold, não conseguiremos rejeitar a hipótese nula, mas isso não é uma verdade absoluta. Significa apenas que não encontramos evidências suficientes para rejeitá-la.

# definir variáveis com os valores necessários para o teste estatístico
## valor do recall do modelo sem engenharia de atributos
recall_model = 0.6547
## valor do recall do modelo com engenharia de atributos
recall_model_fe = 0.6872
## quantidade de clientes que foram avaliados
n_clients = X_test.shape[0]

# quantidade de acertos de cada modelo
model_success = int(recall_model * n_clients)
model_fe_success = int(recall_model_fe * n_clients)

# criar função para calcular o teste z para duas proporções
def z_test(success_a, success_b, n):
# calcular parâmetros
p1 = success_a / n
p2 = success_b / n
p = (success_a + success_b) / (n + n)
# definir fórmula com o threshold de p = 0.05
z_stat = (p1 - p2) / ((p * (1 - p) * (1/n + 1/n))**0.5)

# calcular o valor-p
p_val = (1 - norm.cdf(abs(z_stat))) * 2
return z_stat, p_val

# aplicar a função aos modelos de machine-learning
z_stat, p_val = z_test(model_success, model_fe_success, n_clients)
print(z_stat, p_val)
'''
-5.473686240102473 4.407691878149933e-08
'''

O valor de p-value do teste estatístico realizado foi igual a 4.41e-08, logo o valor de p encontrado é menor do que o valor de nível de significância, de 0.05. Com isso podemos rejeitar a hipótese nula e aceita-se que existe diferença entre os dois modelos de machine-learning criados. E, por isso, podemos dizer com suporte estatístico que o modelo com engenharia de atributos é superior ao modelo sem a engenharia.

17. Conclusão

Esse estudo teve como objetivo criar um modelo de machine-learning que preveja se um novo cliente pode vir a se tornar inadimplente ou, em termos técnicos da área financeira, default. Para isso, foi realizada uma análise exploratória dos dados, onde se tomou conhecimento da estrutura e conteúdo do conjunto de dados. Nesse momento foi detectado o desbalanceamento da classe-alvo, a presença de outliers e de dados ausentes, bem como erros de digitação.

O tratamento dos dados envolveu a remoção de alguns atributos e dos dados ausentes na variável alvo. Ainda nessa etapa houve o tratamento dos erros de tipo, a alteração de tipo de variável, remoção de outliers e tratamento de dados ausentes.

Após isso, se iniciou a preparação dos dados para serem inseridos em modelos de aprendizado de máquina. Nessa etapa foi utilizado o Label Enconding para tratar as variáveis categóricas. Também, os dados foram separados em dois conjuntos X e y, respectivamente a classe-alvo e as classes independentes, para que se pudesse criar um conjunto de treino e outro de teste.

Na sequência foi criada uma função de validação dos modelos, constituída de um fluxo de trabalho, conhecido pelo termo pipeline, para padronizar os dados com o uso do Standard Scaler, aplicar o classificador escolhido no parâmetro e realizar uma validação cruzada avaliando o modelo pelo valor de Recall. Com isso, foi possível criar um modelo base, com Random Forest, cujo valor de avaliação ficou em 0.0290.

Vale ressaltar que a métrica de avaliação de performance dos modelos escolhidas foi o Recall por melhor se adequar ao problema de estudo. Isto é, os casos de falsos negativos são mais prejudiciais para a empresa do que os casos de falsos positivos.

Com o valor do modelo base em mãos, foi feita a padronização e o balanceamento dos dados, por meio do Standard Scaler e do Undersampling, como forma de melhorar o desempenho do modelo. A partir disso criaram-se mais 7 modelos, onde, à título de comparação o Random Forest que antes era de 0.0290, teve uma melhoria e foi para 0.6344. Aqui, o melhor modelo foi dado ao LGBMClassifier com o valor de 0.6562 e, em segundo lugar ficou o XGBoost com 0.6483.

O XGBoost foi escolhido para dar sequência nesse projeto por se tratar de um algoritmo com maior quantidade de hiperparâmetros para serem ajustados, o que tem feito dele uma excelente escolha entre os profissionais de dados para aplicação de problemas de diversas áreas. Por esse motivo o passo seguinte foi a busca dos melhores parâmetros para o nosso problema em questão. Com isso, saímos do valor de Recall de 0.6483 e conseguimos aprimorar para 0.6640.

Para tentar melhorar ainda mais o desempenho do modelo foi realizado a engenharia de atributos, que consistiu na remoção de mais algumas variáveis do conjunto de dados e a criação de 4 novos atributos. Mais uma vez os dados foram preparados e, dessa vez, também se aprimorou o tratamento das variáveis categóricas com o uso de Label Enconding e de variáveis dummy.

Ao rodar o modelo base, tivemos um aprimoramento de 0.0290 para 0.0513. Após a aplicação da padronização e balanceamento dos dados, recriamos os 7 modelos para comparação e se obteve uma melhoria em metade dos modelos. Contudo, a melhora foi mais significativa do que nos casos em que o modelo piorou.

Prosseguiu-se com o XGBoost, que acima teve um valor de Recall de 0.6427, e após a otimização dos hiperparâmetros chegou-se em 0.6663. O melhor valor encontrado até então.

Por fim, comparamos os modelos XGBoost criados, com e sem a engenharia de atributos, aplicados no conjunto de teste. Na matriz de confusão a observação imediata é de que quando se trata dos verdadeiros positivos o modelo com a engenharia de atributos é superior ao modelo sem. Porém, um olhar mais apurado irá perceber que o modelo sem feature engineering tem mais acertos no geral, que é a soma de Verdadeiros Negativos e Verdadeiros Positivos.

Quando olhamos para o AUC, o modelo com engenharia de atributos é superior, apresentando um resultado de 67.36 contra 66.77 do modelo sem a engenharia. E isso se repete de forma ainda mais expressiva quanto ao valor de Recall, que ficou em 0.6547 no modelo sem engenharia e 0.6872 com a feature engineering.

Para comprovar a superioridade do modelo com engenharia de atributos foi realizado um teste estatítstico z que resultou em um valor de p-value de 4.41e-08. Com isso, foi possível rejeitar a hipótese nula e aceitar que um modelo tem melhor desempenho do que o outro.

Cabe apontar ainda que o modelo com feature engineering resultou em uma quantidade de árvores muito menor do que o modelo sem a adição de atributos, apesar dele em si ter uma quantidade maior de variáveis. Ou seja, o modelo com engenharia saltou de 37 para 105 atributos. Contudo, a quantidade de árvores no modelo com engenharia é muito menos do que no modelo sem: 25 comparados aos 85. Isso implica na velocidade com que o modelo irá rodar e gerar os resultados, o que é essencial para o tipo de negócios da Nubank.

Por fim, gostaria de acrescentar algumas sugestões que podem contribuir nesse estudo:

  • poderia ser feita uma análise de correlação entre as variáveis, para encontrar as que melhor se relacionam com o default e, testar a identificação desses casos com essas variáveis.
  • utilizou-se aqui a métrica de avaliação de Recall, contudo, outra métrica interessante de ser considerada seria o F1-Score uma vez que os Falsos Positivos também podem ser prejudiciais para o negócio, já que se deixa de realizar um empréstimo a um cliente que seria na verdade um bom pagador, mas foi erroneamente apontado pelo modelo como sendo default.

Saiba Mais

Esse estudo encontra-se disponível nas plataformas Google Colab e GitHub. Basta clicar nos links das imagens abaixo para ser redirecionado.

[LoffredoDS] Análise de Risco de Crédito.ipynb
raffaloffredo/credit_risk_analysis_portuguese

--

--

Raffaela Loffredo

💡 Transforming data into impactful solutions | Co-founder @BellumGalaxy | Data Scientist | Blockchain Data Analyst