Detecção de Fraudes em Cartões de Crédito: a importância das métricas de avaliação
Você já teve seu cartão de crédito bloqueado por uma transação fraudulenta? Ou talvez você já teve o cartão bloqueado após uma transação que você mesmo fez? Isso acontece devido aos algoritmos de inteligência artificial que foram criados para trazer mais segurança e confiança para as transações financeiras.
Somente em 2019, a inteligência artificial preveniu cerca de 2 bilhões de dólares em fraudes na América Latina e Caribe. Além disso, no Brasil, cerca de 12,1 milhões de pessoas foram vítimas de algum tipo de fraude financeira no último ano, resultando em uma perda de R$ 1,8 bilhão.
Devido a esses números alarmantes, fraudes financeiras têm sido uma grande preocupação para instituições financeiras como bancos e fintechs, que têm investido em Ciência de Dados e Inteligência Artificial para melhorar cada vez mais a assertividade de seus algoritmos. Com grandes bases de dados contendo históricos de transações, um algoritmo de aprendizado de máquina que seja somente um pouco melhor já é capaz de prevenir a perda de milhões de dólares.
Neste artigo, vamos explorar um conjunto de dados disponibilizado por empresas de cartão de crédito europeias durante uma pesquisa em colaboração com o grupo Worldline and the Machine Learning da ULB (Université Libre de Bruxelles) e pode ser encontrado no Kaggle. O conjunto de dados contém operações financeiras ocorridas em um período de dois dias em Setembro de 2013, sendo 492 operações classificadas como fraudes de um total de 290 mil.
Análise Exploratória
Explorando nosso conjunto de dados, podemos observar que ele é altamente desbalanceado, com as transações fraudulentas representando somente 0,173% de nossas entradas, como pode-se observar no gráfico abaixo onde quase não conseguimos ver a quantidade de entradas de transações fraudulentas (1) em comparação às regulares (0).
Além disso, 28 das nossas 31 variáveis são resultado de um processo de PCA (Principal Component Analysis), como informado no site onde foram obtidos os dados. PCA é um método que pode ser utilizado para redução de dimensionalidade, reduzindo o número de variáveis enquanto preserva a maior quantidade de informação possível. Isso é feito encontrando um novo grupo de variáveis chamadas de components. Durante o processo de PCA as variáveis foram padronizadas (subtraídas da média e divididas pelo desvio padrão).
As três variáveis restantes são Time, Amount e Class. A variável Time representa o número de segundos que se passaram entre cada transação e a primeira transação realizada, enquanto Amount é o valor de cada transação. A variável Class nos fala se uma operação foi fraudulenta (1) ou regular (0) e é nossa coluna alvo, a que nossos algoritmos de aprendizado de máquina tentarão prever. Por último, todas as nossas variáveis são numéricas e não apresentam valores faltantes.
Preparação dos Dados
Para que nossos algoritmos tenham uma melhor performance, nós iremos padronizar as variáveis Time e Amount para que elas fiquem na mesma escala que as outras. Esta é uma etapa importante, pois se uma variável apresenta uma variância que seja ordens de grandeza maior que as outras, ela pode dominar a função objetiva e fazer com que o estimador não seja capaz de aprender com as outras varáveis como esperado. Isso significa que uma variável com valores maiores poderia receber maior importância de nossos modelos, mesmo que isso não seja correto.
A fórmula para realização do processo de padronização consiste em subtrair cada valor de uma variável de sua média e dividir pelo desvio padrão, como podemos ver abaixo:
Onde u é a média e s, o desvio padrão. A seguir, segue o código utilizado:
# importando o padronizador do scikit-learn e instanciando-ofrom sklearn.preprocessing import StandardScalerscaler = StandardScaler()# criando uma cópia do conjunto de dados para fazer as mudançasdf_clean = df.copy()# criando novas colunas para os valores padronizadosdf_clean['scaled_Time'] = scaler.fit_transform(df['Time'].values.reshape(-1, 1))df_clean['scaled_Amount'] = scaler.fit_transform(df['Amount'].values.reshape(-1, 1))
# deletando as colunas antigasdf_clean.drop(['Time', 'Amount'], axis=1, inplace=True)
Divisão entre dados de treino e teste
Outra etapa necessária é a divisão de nossos dados no conjunto de variáveis que será utilizados para fazer as previsões (X) e na variável alvo (y). Após isso, iremos dividir nossos dados em um conjunto de treino, o qual será utilizado para treinar nossos modelos, e em um conjunto de teste que será utilizado para avaliar nosso modelo. Abaixo segue o código utilizado para realizar essa etapa:
# importando a função para dividir nosso conjunto de dados em treino e testefrom sklearn.model_selection import train_test_split
# dividindo nosso conjunto de dadosX = df_clean.drop('Class', axis=1)y = df_clean['Class']X_train, X_test, y_train, y_test = train_test_split(X, y)
Undersampling
Agora que nós dividimos nosso conjunto de dados, precisamos lidar com o problema do desbalanceamento. Um dos métodos mais utilizados para isso é o Undersampling.
O Undersampling consiste em preservar todas as entradas da classe positiva (transações fraudulentas em nosso caso) e selecionar aleatoriamente entradas da classe negativa (transações regulares em nosso caso) na mesma quantidade da classe positiva. A vantagem desta técnica é que não há alterações na classe positiva. No entanto, a principal desvantagem está na grande perda de informação da classe negativa.
Um detalhe que devemos sempre ficar atentos é realizar a divisão de nossos dados em treino e teste antes de realizar o Undersampling, senão podemos “vazar” dados de nosso conjunto de treino em nosso conjunto de teste e causar um overfit de nosso modelo. Abaixo estão o código utilizado para realizar o Undersampling e um countplot mostrando a mesma quantidade de dados para ambas as classes.
# importando a classe do Undersampler e instanciandofrom imblearn.under_sampling import RandomUnderSamplerrus = RandomUnderSampler()
# aplicando o undersampler em nosso conjunto de treinoX_rus, y_rus = rus.fit_sample(X_train, y_train);# checando o resultadoprint(pd.Series(y_rus).value_counts());sns.countplot(y=y_rus, orient='h');
Métricas de Avaliação
Antes de construir nossos modelos, vamos entender mais sobre as métricas de avaliação e sua importância.
Matriz de Confusão
A matriz de confusão é uma ótima maneira de identificar onde nosso modelo está performando bem e onde está sua maior concentração de erros. As linhas em uma matriz de confusão representam a classificação verdadeira do conjunto de teste e as colunas são as classificações feitas pelo modelo. Então é possível verificar a quantidade de Positivos Verdadeiros, Negativos Verdadeiros, Positivos Falsos e Negativos Falsos, como pode ser visto na imagem abaixo.
Acurácia
A Acurácia é a métrica mais simples para avaliar modelos de aprendizado de máquina, ela simplesmente soma todas as predições corretas e divide pelo número total de predições. Essa métrica não é recomendada para conjuntos de dados desbalanceados, pois a grande quantidade de predições corretas da classe negativa irá acarretar em um acurácia alta, mesmo que hajam muitas predições incorretas da classe positiva.
Precision e Recall
Precision é o número de positivos verdadeiros divididos pela soma de positivos verdadeiros e positivos falsos. Ela descreve quão bom o modelo é em prever a classe positiva (1).
Recall é a razão entre o número de positivos verdadeiros e a soma de positivos verdadeiros e negativos falsos. Ela indica se a classe positiva (1) está sendo prevista corretamente. Recall também é conhecida como Sensitivity.
Precision e Recall são adequadas para conjuntos de dados desbalanceados, pois elas não envolvem o número de negativos verdadeiros nos cálculos. A única preocupação dessas métricas é com a predição correta da classe positiva (1).
F1-Score
A F1-Score é a média harmônica de Precision e Recall (harmônica pois elas são razões), então ela combina ambas em uma só métrica. Assim como as outras duas, ela é muito utilizada em análises focadas na classe positiva (1) e em dados desbalanceados.
Curva Precision-Recall
Uma curva Precision-Recall é o gráfico de Precision (eixo y) e Recall (eixo x) para diferentes limiares. Um limiar é a probabilidade que uma instância (nova entrada a ser classificada) deve atingir para ser classificada como classe positiva.
Então, enquanto a F1-Score resume a habilidade do modelo para um limiar de probabilidade específico (p.e. 0,5), nós podemos calcular a área sob a curva (AUC) para nos dizer a habilidade do modelo através de diferentes limiares.
Para um melhor entendimento, vamos usar nosso conjunto de dados como exemplo. Em geral, uma instância seria classificada como fraude (1) se a probabilidade de ela ser fraudulenta (P(1)) fosse maior que 50% (P(1) > 0,5), que é nosso limiar. Para esse valor de limiar, nós obtemos um par de Precision-Recall baseado nos valores obtidos de Positivos Verdadeiros, Negativos Verdadeiros, Positivos Falsos e Negativos Falsos.
Porém, se mudarmos o nosso limiar de 0,5, teremos um resultado diferente para o par Precision-Recall. Nós podemos classificar uma transação como fraudulenta para uma probabilidade de 40% (P(1) > 0,4). Isso reduzirá nosso valor de Precision e aumentará o Recall. Nós preferiríamos classificar uma transação como fraudulenta mesma que seja regular para termos certeza que as transações fraudulentas sejam classificadas como tal. Isso representa o balanço (trade-off) entre Precision e Recall.
Modelos de Aprendizado de Máquina
Agora que nosso conjunto de dados foi preparado e entendemos melhor o que significa cada uma das métricas de avaliação, iremos construir três modelos diferentes para comparar e encontrar o mais adequado ao nosso caso.
# importando nossos modelos e métricas de avaliaçãoimport scikitplot as skpltfrom sklearn.metrics import auc, classification_report, confusion_matrix, precision_recall_curvefrom sklearn.linear_model import LogisticRegressionfrom sklearn.tree import DecisionTreeClassifierfrom xgboost import XGBClassifier
Regressão Logística
Regressão Logística é um algoritmo de aprendizado de máquina usado para problemas de classificação. Ele usa a função sigmoidal para transformar a saída e retornar uma probabilidade. Como nós vimos anteriormente, se essa probabilidade for maior que o limiar, a instância será classificada como classe positiva (1), senão, será classificada como classe negativa (0).
# instanciando nossos modelo e ajustando-o ao nosso conjunto de treinolr = LogisticRegression()lr.fit(X_rus, y_rus)# classificando o conjunto de testelr_pred = lr.predict(X_test)lr_prob = lr.predict_proba(X_test)# calculando a curva precision-recallprecision, recall, threshold = precision_recall_curve(y_test, lr_prob[:, 1])# mostrando as métricas de avaliaçãoprint(classification_report(y_test, lr_pred))print(f'AUC: {round(auc(recall, precision), 2)}')skplt.metrics.plot_confusion_matrix(y_test, lr_pred, figsize=(6,6));
Nós podemos observar que apesar de uma alta acurácia (0,97), o F1-Score está muito baixo (0,09). Isso pode ser explicado pelo baixo valor de Precision obtido (0,05). Como podemos verificar na matriz de confusão, o modelo está classificando muitas transações regulares como fraudes (Positivos Falsos).
O Recall está alto (0,96), o que significa que as transações fraudulentas estão sendo previstas corretamente, mas o trade-off entre Precision e Recall não é adequado.
Árvore de Decisão
Árvores de Decisão são ferramentas utilizadas para dar suporte a decisões usando estruturas do tipo fluxograma. Ela expõe as decisões de maneira clara onde cada nó interno representa um teste de atributo (variável), cada ramo representa o resultado do teste, e cada folha representa a classificação da classe, a decisão tomada após computar todos os atributos.
Em algoritmos de aprendizado de máquina, a árvore de decisão é usada pra calcular a probabilidade de uma instância pertencer a cada classe ou, classificar a instância atribuindo-a a classe mais provável. Abaixo está a árvore gerada pelo algoritmo utilizado em nosso conjunto de dados.
# instanciando nosso modelo e ajustando-o ao nosso conjunto de treinodt = DecisionTreeClassifier()dt.fit(X_rus, y_rus)# classificando o conjunto de testedt_pred = dt.predict(X_test)dt_prob = dt.predict_proba(X_test)# calculando a curva precision-recallprecision, recall, threshold = precision_recall_curve(y_test, dt_prob[:, 1])# mostrando os valores obtidos pelas métricas de avaliaçãoprint(classification_report(y_test, dt_pred))print(f'AUC: {round(auc(recall, precision), 2)}')skplt.metrics.plot_confusion_matrix(y_test, dt_pred, figsize=(6,6));
É possível observar que a árvore de decisão obteve uma performance inferior a regressão logística em todas as métricas. Ela apresentou o mesmo problema do modelo anterior, prevendo muitos Positivos Falsos.
XGBoost
XGBoost é um sistema de aprendizado de máquina escalável para aperfeiçoamento de árvore de decisão que é capaz de capturar padrões e ser escalado para lidar com grandes quantidades de dados. Ele produz resultados tidos como estado-da-arte em várias aplicações e problemas. O que o distingue de outros modelos de aprendizado de máquina é sua escalabilidade e a capacidade de rodar mais do que 10x mais rápido que outras soluções existentes.
Um detalhe importante é que não usaremos os dados que passaram por Undersampling, mas todo nosso conjunto de treino. Isso é possível, pois o XGBoost já possui parâmetros que lidam com o desbalanceamento de dados.
# instanciando nosso modelo e ajustando-o ao nosso conjunto de treinoxgb = XGBClassifier()xgb.fit(X_train, y_train)
# classificando nosso conjunto de testexgb_pred = xgb.predict(X_test)xgb_prob = xgb.predict_proba(X_test)
# calculando a curva precision-recallprecision, recall, threshold = precision_recall_curve(y_test, xgb_prob[:, 1])# mostrando os valores obtidos para as métricas de avaliaçãoprint(classification_report(y_test, xgb_pred))print(f'AUC: {round(auc(recall, precision), 2)}')skplt.metrics.plot_confusion_matrix(y_test, xgb_pred, figsize=(6,6));
Nosso modelo de XGBoost performou muito melhor que os outros dois no que diz respeito ao Precision, mesmo que o Recall tenha sido um pouco inferior. Ele obteve uma acurácia muito alta (1,0), um F1-Score de 0,86 e um AUC de 0,89, que foram os melhores entre os três modelos avaliados.
Comparando os modelos
Colocando os valores obtidos para cada métrica de avaliação em nossos 3 modelos em uma tabela, podemos realizar a comparação com maior facilidade.
Se tivéssemos que escolher entre esses três modelos, provavelmente acabaríamos indo com o XGBoost. Mesmo que não conseguiríamos prever tantas fraudes, não estaríamos incomodando tanto nossos clientes com transações regulares sendo classificadas como fraudulentas. O trade-off entre Precision e Recall foi melhor para este modelo. Além disso, o alto AUC indica que ele performa bem para diferentes limiares.
Conclusão
Nesse artigo, passamos por várias fases de um projeto de Data Science, desde a obtenção dos dados até a avaliação do modelo. No fim, o modelo XGBoost, em geral, obteve a melhor performance para nosso caso por apresentar o melhor precision-recall trade-off, além de obter um alto AUC.
Espero que você tenha aprendido bastante sobre como podemos usar as métricas de avaliação para tomada de decisão de qual modelo utilizar e quais as vantagens e desvantagens de priorizar uma métrica em detrimento de outras. Para ter acesso a todo o código e a análise exploratória completa, basta acessar meu Github, lá você também encontrará outros projetos de Data Science.
Certamente os modelos que utilizamos aqui tem espaço para melhoria. Seria possível, por exemplo, testar outros métodos para lidar com o desbalanceamento dos dados como Oversampling e SMITE e/ou aplicar Cross-validation e Grid Search para tunar os hiperparâmetros de nossos modelos.