Uma das áreas mais perturbadoras para os empresários, sejam grandes ou pequenos, é a inadimplência de alguns clientes. Principalmente num cenário de crise, esta é uma parte que deve ser bem gerenciada pelos administradores do negócio, ou pode levar o mesmo à falência.

Imagine conseguir saber quais clientes vão deixar de pagar apenas observando o comportamento e as características de seus perfis. Tendo esta informação, o gestor pode ajustar seu risco, implementar ações e focar os seus esforços nos clientes com maior chance de causar problemas.

É aí que entra o Machine Learning. Neste artigo quero exemplificar a criação de um sistema simples para prever quais clientes de uma operadora de cartão de crédito deixarão de pagar a fatura do próximo mês.

Estes dados estão disponíveis originalmente no link: https://archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients

Este é o link para o Jupyter Notebook com o código. Ele usa scikit-learn, numpy e XGBoost.

Índice

O Banco de Dados

Este banco de dados, em formato XLS, possui 30 mil registros, e já contém várias informações prontas para aplicarmos Machine Learning. Num caso real, é parte do trabalho do cientista de dados extrair as variáveis que possam ser importantes para a previsão. Aqui este trabalho está basicamente feito para nós.

Temos dados do perfil, como idade, sexo e escolaridade, e também dados comportamentais, como os registros de pagamentos dos últimos meses. Apesar de dados do perfil influenciarem, normalmente estes dados comportamentais que podem ser extraídos de bancos de dados transacionais acabam dando origem às variáveis mais importantes.

Como Saberemos se o Modelo Funciona?

Claro que, na prática, o mais importante é saber se este modelo diminui o prejuízo da empresa. Em alguns casos é possível extrair esta informação dos dados históricos e usar como métrica de avaliação do modelo mas, nestes dados, não temos essa possibilidade. Além disso, é importante avaliar o modelo de vários ângulos diferentes.

Neste caso, escolhi a métrica ROC AUC. Estou assumindo que o objetivo da empresa é saber quais são os clientes com maior chance de inadimplência para poder tomar atitudes específicas (como ligar cobrando ou enviar uma carta) que procurem aumentar a chance dele pagar.

Então, esta métrica, que vai de 0,5 a 1, é melhor quando os clientes que realmente são inadimplentes na amostra recebem uma pontuação prevista maior do que aqueles que pagaram no prazo. Ou seja, em vez de nos preocuparmos em acertar a probabilidade de um cliente pagar ou não, queremos apenas que os inadimplentes sejam rankeados com uma pontuação maior do que os adimplentes.

Fora isso, a decisão de como separar os dados entre treino e teste é muito importante. Originalmente estes são divididos entre usuários. Ou seja, treinamos num grupo de usuários diferentes daqueles que vamos prever, mas todos no mesmo período de tempo.

Na prática eu gosto de levar em conta a característica temporal da tarefa, pois esta maneira é mais próxima do modo como o modelo será usado em produção: separar um período anterior como treino e um posterior como teste. Desta maneira conseguiríamos saber, por exemplo, como o modelo reage a diferentes cenários econômicos e épocas do ano.

Seria possível transformar estes dados de maneira que estivessem em ordem cronológica, e prevermos outras variáveis, mas neste artigo vou me focar na parte de machine learning, então quero apenas deixar este comentário sobre este detalhe.

Precisei somar 2 aos valores de algumas variáveis categóricas porque o OneHotEncoder do scikit-learn não funciona com números negativos, mas isso não afetará nossa previsão.

from sklearn.model_selection import cross_val_score, KFold, train_test_split
import pandas as pd
import numpy as np

data = pd.read_excel('default of credit card clients.xls', header=1, index_col='ID')
data[['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6']] += 2

X = data.drop('default payment next month', axis=1)
y = data['default payment next month'].copy()

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=2)

É importante dividir os dados entre treino e teste antes de começarmos, e só usar o teste quando tivermos o modelo final. Toda a nossa validação será feita utilizando os dados originalmente separados para treino. Assim teremos um conjunto de dados não utilizados durante a criação do modelo que poderá nos dar uma estimativa confiável da performance.

Um Modelo Simples como Base

Para estabelecer uma base, e ver se modelos mais complexos apresentam alguma melhora significativa, vou criar um dos modelos mais simples: a regressão logística.

Para isso, preciso codificar as variáveis categóricas (como sexo, escolaridade, estado civil) com one hot. Isso simplesmente significa que cada valor da variável se tornará uma coluna com o valor igual a um, caso aquele valor esteja presente naquele exemplo, e zero, caso negativo.

Notem que usei o argumento “class_weight” como “balanced”. Como temos menos exemplos da classe positiva, o modelo pode dar mais importância à classe negativa, mas este argumento faz com que o modelo aplique uma penalidade proporcional ao número de exemplos da classe aos erros, tornando-as equivalentes neste sentido.

from sklearn.preprocessing import OneHotEncoder
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
kf = KFold(n_splits=5, random_state=0, shuffle=True)

categs = [1,2,3,5,6,7,8,9,10]
ohe = OneHotEncoder(categorical_features=categs, handle_unknown='ignore')
model = make_pipeline(ohe, LogisticRegression(C=0.5, random_state=1, class_weight='balanced'))
cross_val_score(model, X_train, y_train, n_jobs=-1, cv=kf, scoring='roc_auc').mean()

A regressão logística fica com AUC por volta de 0,73.

Aplicando Modelos mais Complexos

A regressão logística é um modelo bem simples, capaz de capturar padrões lineares. Mas e se tivermos interações não-lineares entre nossas variáveis? Um exemplo seria pessoas abaixo de 25 anos e solteiras oferecerem um risco diferente de inadimplência. Se quisermos que uma regressão logística capture este padrão, precisamos criar uma variável específica.

Uma outra maneira de capturar estes padrões é usar um modelo não-linear, com maior capacidade de capturar padrões complexos. Neste caso, escolhi um modelo que, em geral, sem muitos ajustes, já dá uma boa performance: a Random Forest.

A Random Forest é um conjunto de árvores de decisão criadas em partes aleatórias dos dados e das variáveis. Cada árvore é um modelo fraco, mas quando calculamos a média das previsões, elas se tornam um modelo poderoso.

Uma das vantagens de árvores é que, em casos de variáveis categóricas que não possuem alta cardinalidade (muitos valores diferentes), elas conseguem aproximar padrões sem que precisemos transformar para one-hot, podemos usar o formato ordinal. Então, vamos usar o formato original destas variáveis.

from sklearn.ensemble import RandomForestClassifier
kf = KFold(n_splits=5, random_state=0, shuffle=True)

model = RandomForestClassifier(n_jobs=-1, n_estimators=500, max_features=6, min_samples_leaf=20, random_state=0, class_weight='balanced')
cross_val_score(model, X_train, y_train, n_jobs=1, cv=kf, scoring='roc_auc').mean()

A Random Forest com os parâmetros padrões do Scikit-learn atingiu AUC 0,763. Após ajustar os parâmetros, consegui chegar a um AUC de 0,777. Parece uma melhora pequena, mas quando falamos de milhares de exemplos, isso faz diferença.

É Possível Melhorar Mais?

Um dos modelos mais poderosos de conjuntos de árvores é o Gradient Boosted Trees. Em vez de criar várias árvores aleatoriamente, este modelo cria árvores dando mais peso a exemplos nos quais o conjunto atual comete mais erros.

Ele é mais complicado de usar do que a Random Forest, então temos mais parâmetros para ajustar. Mas quando bem utilizado, costuma apresentar uma performance melhor.

A implementação que utilizei foi o XGBoost, que junto com o LightGBM, são ferramentas de código aberto, que rodam em paralelo, escalam para grandes volumes de dados, e oferecem uma performance muito boa.

import xgboost as xgb

kf = KFold(n_splits=5, random_state=0, shuffle=True)

model = xgb.XGBClassifier(learning_rate=0.009, max_depth=6, min_child_weight=3,
                     subsample=0.15, colsample_bylevel=0.85, n_estimators=500)
cross_val_score(model, X_train, y_train, n_jobs=1, cv=kf, scoring='roc_auc').mean()

Este modelo, com os parâmetros originais, atingiu o AUC de 0,775. Após ajustar os parâmetros o AUC foi para 0,781.

Verificando os Resultados nos Dados de Teste

Agora que temos nosso modelo final, o Gradient Boosted Trees com os parâmetros que atingiram o melhor resultado em nossa validação cruzada, vamos treinar este modelo em todos os nossos dados de treino e ver qual a performance nos dados de teste, que não utilizamos até agora.

Caso os resultados sejam muito diferentes, é sinal que fizemos algo errado durante a construção do modelo, ou que os padrões presentes no treino não são tão fortes no teste.

from sklearn.metrics import roc_auc_score

model = xgb.XGBClassifier(learning_rate=0.009, max_depth=6, min_child_weight=3,
                     subsample=0.15, colsample_bylevel=0.85, n_estimators=500)
model.fit(X_train, y_train)
p = model.predict_proba(X_test)
roc_auc_score(y_test, p[:, 1])

No teste temos o AUC 0,789, bastante próximo do que vimos na validação cruzada, o que é um forte indicador que nosso modelo vai funcionar para novos dados, desde que eles sejam distribuídos da mesma maneira como separamos treino e teste.

Nenhum modelo é perfeito, mas o mais interessante é que, se olharmos os 100 exemplos do teste com maior pontuação indicando inadimplência, 83 deles realmente não fizeram o pagamento. Se olharmos os 100 exemplos com menor pontuação, apenas 3 foram inadimplentes.

Os Próximos Passos

Neste artigo iniciamos com um modelo simples de regressão logística, e avançamos até o modelo mais complexo, que ofereceu uma melhora significativa. Estes modelos poderiam ser usados em produção sem grandes esforços de engenharia.

Para melhorar, seria interessante ter acesso a mais usuários e a outros bancos de dados dos quais pudéssemos extrair mais variáveis que podem ser importantes. Além disso, se os recursos computacionais disponíveis suportarem, podemos criar vários modelos e usar o conjunto de suas previsões, atingindo uma performance ainda melhor.

Mas este é um ponto de partida.

Seja o primeiro a saber das novidades em Machine Learning. Me siga no LinkedIn.