Construindo a Solução — Análise Exploratória dos Dados

Leonardo Janes Carneiro
10 min readMar 9, 2021

--

O presente artigo é uma continuação do seguinte: Aplicando Machine Learning para Prever Inadimplência em Crédito Pessoal. Ambos fazem parte de uma série cujo objetivo é relatar evolução do aprendizado e absorção do conhecimento de Ciência de Dados durante o bootcamp da Tera, bem como sua aplicação no projeto realizado por Alexandre Doria, Miqueias Lima, Matteus Martins, Adams da Cruz e Leonardo Carneiro.

1. Dados

Os dados utilizados neste projeto foram coletados da FINTECH X (seu nome será mantida em sigilo). A base de dados é composta por um questionário com 15 perguntas, que são enviadas a clientes em potencial. Pessoas com o intuito de abrir uma conta de crédito na fintech recebem apenas 10 destas 15 perguntas, que visam mapear o comportamento do eventual cliente perante endividamento. Cada uma destas 10 perguntas pertence a uma categoria psicológica a ser detectada pelas respostas. São elas: Malicious/Innocent Intention, Meaning of Money, Self-Efficacy, Locus of Control, Self-Esteem, Compulsive Buying/Buying Behavior, Delay Discounting, Impulsiveness, Dark/Light Triad, Behavioral Risk Parameters.

O objetivo do questionário é detectar bons e maus pagadores por meio das respostas a estas perguntas, que variam numa escala de 0 a 10. O indivíduo deve responder quantas pessoas (de 0 a 10) ele acredita que poderiam atrasar (ou não pagar) as parcelas referentes à linha de crédito em cada uma das categorias acima. Contudo, nem sempre um valor mais alto de resposta do cliente a estas perguntas está associado a uma maior probabilidade dele(a) vir a cometer default. Um outro exemplo de pergunta do questionário é o número de pessoas, num grupo de 10, que o cliente acredita que pagaram o empréstimo. Neste caso, valores muito baixos como resposta podem indicar um comportamento inadimplente.

Além destes dados acerca das respostas recolhidas do questionário, obtivemos dados sobre o comportamento destes clientes quanto ao pagamento de suas parcelas das linhas de crédito (se ele(a) pagou dentro do prazo de vencimento, ou não). Esta coluna é preenchida com 0 no caso de o cliente ter devidamente pago e 1, caso tenha atrasado. Nosso objetivo é verificar se algum dos clientes da nossa base de dados atrasou o pagamento de suas parcelas em até 30 dias e como as respostas ao questionário refletem este comportamento.

2. Análise Exploratória

Apesar de termos mais de 5 mil observações no nosso dataset de respostas, nem sempre havia informação a respeito do atraso do pagamento. Em alguns casos, quando ainda não se passaram 30 dias do histórico de pagamento do cliente e este responde o questionário, não é possível detectar se houve ou não atraso no pagamento. Portanto, foi necessário realizar uma limpeza nesta coluna, que representa a variável de interesse para o nosso modelo. Ou, em outras palavras, nossa variável target. Após filtrar a coluna da nossa variável target para valores 0 ou 1 e eliminar linhas duplicadas (linhas com valores idênticos para cada uma das colunas do dataset), obtivemos um dataframe composto por 3.966 observações e 16 variáveis: 15 correspondentes às respostas do questionário, além da nossa variável target.

Em seguida, nos deparamos com outro problema: dentre estas 3.966 observações, apenas 843 referem-se a clientes que cometeram default. Isto representa aproximadamente 21% do total de observações. Logo, 21% das pessoas que responderam ao questionário são más pagadoras enquanto 79% são boas pagadoras.

Número de observações para cada classe (0: bom pagador; 1: mau pagador).

Se utilizarmos esta coluna do jeito como está, nosso modelo poderá acabar tendo dificuldade de prever a classe positiva (cujo valor é indicado por 1), que é exatamente o que queremos com nosso modelo. Caso este problema ocorra com nosso modelo, deveremos aplicar alguma técnica de balanceamento nos nossos dados. Descreveremos com mais detalhes as técnicas utilizadas neste projeto mais adiante.

Em seguida, buscamos observar a distribuição das respostas para cada pergunta do nosso dataset. Tivemos algumas perguntas cujas respostas se concentraram em torno de valores extremos (próximo de 10), mas a maioria das questões apresentou uma concentração de respostas em torno de 5, conforme ilustrado abaixo para as questões 33 e 19.

Distribuição concentrada em valores extremos (10)
Distribuição concentrada em valores intermediários (5)

Observem que apesar de termos um dataset composto de 3.966 observações, quando geramos estes gráficos de barras a contagem de respostas para cada valor no intervalo de 0 a 10 não parece chegar nem perto deste número. De fato, temos uma grande quantidade de valores nulos na nossa base de dados e, por isto, quando filtramos estes valores do nosso dataset sobram bem menos observações. A questão que coletou o maior número de respostas obteve 861 registros, enquanto a questão com menos respostas obteve 598.

Contagem de valores não-nulos no dataset

Isto pode se tornar um problema quando formos treinar nosso modelo de Machine Learning para classificar clientes bons e maus pagadores, uma vez que, para encontrar o melhor algoritmo para este fim iremos comparar diferentes algoritmos que precisam de uma quantidade substancial de observações para aprender adequadamente os padrões de resposta que separam o cliente que poderá se tornar inadimplente, daquele que possui uma menor probabilidade de se tornar inadimplente. Idealmente, separaríamos nosso dataset em treinamento, validadação e teste. Para o dataset de treinamento, iríamos alocar a maior parte das observações para que se possa selecionar as melhores features para o modelo e ajustar os dados de input. Para a etapa de validação, uma outra parcela de dados seria utilizada para realizar uma comparação dos algoritmos utilizados e examinar as principais métricas de avaliação, com a finalidade de se selecionar aquele com melhor performance.

Créditos da imagem: https://medium.com/turing-talks/turing-talks-10-introdu%C3%A7%C3%A3o-%C3%A0-predi%C3%A7%C3%A3o-a75cd61c268d

Por fim, para a etapa de teste, uma nova e inutilizada parcela de dados seria introduzida ao melhor modelo para avaliar sua performance com dados nunca antes "vistos", com a finalidade de se checar um possível overfitting dos dados, que ocorre quando um modelo obtém métricas de avaliação muito boa para um determinado conjunto de dados, mas ao se utilizar uma nova base de dados, sua performance piora razoavelmente. Se isto ocorre, é um indício de que o modelo pode estar incorporando uma quantidade excessiva de ruído nos dados para identificar possíveis padrões.

Contudo, estas quantidades de observações que coletamos podem não ser suficientes para treinar um bom modelo de previsão. Neste caso, pode ocorrer um problema de underfitting no nosso modelo, quando o algoritmo é incapaz de identificar qualquer padrão de comportamento dos dados: tudo é considerado ruído.

Créditos da imagem: https://docs.aws.amazon.com/pt_br/machine-learning/latest/dg/model-fit-underfitting-vs-overfitting.html

Apesar disto tudo, queremos observar o comportamento das respostas separado por classe (bom pagador e mau pagador), para observar se bons pagadores respondem ao questionário de maneira significativamente distinta à dos maus pagadores. Para este fim, plotamos o boxplot das perguntas do questionário. Este gráfico nos informa a respeito da concentração dos valores das respostas para cada uma das classes. Estamos procurando questões cujas respostas possuem valores diferentes para a mediana e variância para cada classe.

Entretanto, temos algumas questões no nosso dataset, como a questão 20 ilustrada abaixo, que apresentam padrão idêntico de respostas para clientes do tipo default (1) e do tipo no-default (0).

Padrão idêntico de respostas

Temos, ainda, questões que parecem apresentar variâncias idênticas, porém medianas distintas, como a questão 9, ilustrada abaixo.

Padrão distinto apenas para a mediana

Temos, também, questões que parecem apresentar variâncias pouco distintas, porém medianas idênticas, como a questão 17, ilustrada abaixo.

Padrão distinto apenas para a variância

Podemos ver, portanto, que o padrão de respostas não difere muito entre as classes, de modo que estes gráficos não poderão servir de base para selecionar as melhores features para nossos modelos de classificação.

Por fim, plotamos a matriz de correlação das 15 perguntas, mais a variável dependente.

Matriz de correlação das variáveis do dataset

Vemos aqui, mais uma vez, o problema das observações nulas do dataset. Não foi possível obter uma matriz cheia (valores de correlação para cada par possível dentre as 15 questões) com as respostas coletadas até o momento. Estas observações nulas são decorrentes do fato de os clientes receberem apenas 10 perguntas selecionadas a partir das 15. Isto dificulta a estimação de um modelo que utilize mais de uma variável feature, dado que possa não haver observações suficientes para treinar os algoritmos (possibilidade de underfitting). Se não selecionarmos com cuidado, podemos, em um caso extremo, não encontrar no dataset um cliente que tenha respondido integralmente a todas as perguntas que se deseja utilizar para o treinamento do modelo.

3. Seleção das Features

Após analisar os gráficos de distribuição das respostas, bem como os boxplots, e tendo em vista a grande quantidade de observações nulas no dataset, utilizaremos o algoritmo de regressão logística da biblioteca scikit-learn para treinar modelos de classificação para cada questão do dataset individualmente, visando identificar aquelas que apresentem matrizes de confusão com maior número de True Positives (TP), ou verdadeiros positivos, que são as observações corretamente previstas pelo modelo como pertencentes à classe default; e menor razão de False Negatives (FN) / False Positives (FP). As questões serão rankeadas de acordo com a seguinte métrica:

Métrica de seleção de features

Onde TP é a quantidade de clientes corretamente identificados como inadimplentes pelo modelo, FN é a quantidade de clientes identificados equivocadamente como bons pagadores e FP é a quantidade de clientes identificados equivocadamente como inadimplentes.

Tendo em vista que o objetivo da FINTECH X é prever inadimplência, é importante que o modelo consiga identificar corretamente os clientes pertencentes à classe default. Logo, buscamos maximizar TP. Quanto ao denominador, sempre que o modelo não for capaz de identificar corretamente qualquer uma das classes estará cometendo erros do tipo I e do tipo II, exemplificados por FN e FP. Em ambos os casos, haverá um custo associado a estes erros que o modelo pode cometer. Sempre que se for aprovado um empréstimo para um cliente mau pagador a fintech perde não apenas o valor do empréstimo, mas também juros associados a atrasos do pagamento das parcelas. Este erro será o mais custoso financeiramente para a fintech e, portanto, deveremos minimizá-lo. Finalmente, a respeito de FP, este ocorre quando o modelo identifica equivocadamente clientes maus pagadores. Aqui, a fintech estaria perdendo um cliente para a concorrência por acreditar que este será inadimplente, quando na realidade se trata de um bom pagador. Não é um custo tão oneroso quanto o custo associado a FN, mas a fintech estará perdendo mercado e deixando de faturar. Sendo assim, também queremos minimizar FN.

5. Técnicas de Balanceamento

Conforme mencionado anteriormente, temos um dataset levemente desbalanceado em favor da classe negativa (no-default). Primeiramente, utilizamos o hiperparâmetro class_weight da regressão logística do scikit-learn para balancear nosso dataset. Utilizamos a razão do custos anuais associados a FN/FP como regra de balanceamento. Estimamos que esta razão anualizada seja aproximadamente igual a 5. Com isto, as observações correspondentes à classe minoritária (default) receberão peso 5, enquanto as demais receberão peso 1.

Ademais, como base para comparação, utilizamos a configuração 'balanced' do hiperparâmetro class_weight e o algoritmo SMOTE como técnicas de balanceamento.

4. Treinando o Modelo

O dataset foi separado em treino e teste com a função train_test_split do scikit-learn. Como estamos passando para os modelos apenas uma única feature, tivemos que aplicar o método reshape da biblioteca numpy com hiperparâmetros (-1, 1) para transformar o array de features em um formato que seja implementável pelo método fit do algoritmo de LogisticRegression. Ilustramos abaixo o código utilizado para treinar e ajustar os modelos. Utilizamos como exemplo o código aplicado à questão 39.

Treinamento do modelo

Em seguida, ajustamos o dataset de treino ao algoritmo do modelo.

Hiperparâmetros configurados do modelo de peso 5

Para o caso dos modelos que utilizam o hiperparâmetro class_weight = 'balanced', o dataset de treino e teste é separado identicamente ao caso anterior. A diferença se dá na configuração do modelo, apenas.

Hiperparâmetros configurados do modelo de peso 'balanced'

Por fim, temos o código utilizado para treinar datasets desbalanceados com o algoritmo SMOTE. Como configuração padrão deste algoritmo, busca-se aplicar uma reamostragem na classe minoritária por meio de interpolação baseada numa técnica parecida com a de k-nearest neighbors.

Algoritmo de balanceamento SMOTE utilizado

6. Comparando as Métricas de Seleção

Após treinar os modelos, calculamos previsões com o método predict dos modelos e calculamos as matrizes de confusão utilizando a função plot_confusion_matrix.

As questões que obtiveram as melhores métricas de seleção, conforme especificado acima, foram aquelas que cujos modelos conseguiram zerar a quantidade de FN. Com isto, a razão TP/(FN/FP) tende a infinito e estas questões obtiveram o melhor rankeamento. Como critério de desempate, utilizamos a quantidade de TP calculados por cada modelo. Com isto, temos 8 questões cuja métrica tende a infinito e dentre estas, temos 2 empatadas em número de TP: questão nº 45 e questão nº 33, com 44 TPs cada.

Matriz de confusão para a questão 45
Matriz de confusão para a questão 33

--

--

Leonardo Janes Carneiro

Economist, aspiring data scientist. Looking for the right questions to ask.