IDENTIFICANDO FRAUDES NO CARTÃO DE CRÉDITO

MACHINE LEARNING

Por Hyan Dias Tavares • JAN 31, 2022

MACHINE LEARNING

CARTÃO CREDITO

FRAUDE

PCA

Combatendo o crime com tecnologia

A um surgimento exponencial de novos métodos de pagamentos, que vem movimentando o mercado financeiro (pix, carteira digital e Qr code), mas apesar dessas mudanças, o cartão de crédito ainda continua sendo um dos preferidos dos brasileiros, movimentando em 2021 50 Bilhões de transações, segundo a BC.

Esse cenário faz com que sejam atraídos bandidos e quadrilhas que usam o sistema para fraudar. Roubam os dados de clientes, se passando pelo mesmo e assim fazendo transações ilegais. Apenas no Brasil, cerca de 12,1 milhões de pessoas já foram vítimas de algum tipo de fraude financeira no último ano. Traduzindo em valores, os golpes financeiros ultrapassaram a cifra de R$ 1,8 bilhão de prejuízo por ano para os últimos 12 meses.

É uma dor de cabeça para os clientes e bancos, assim começa a corrida em busca de métodos para detectar essas transações e impedir assim que aconteça.

Machine learning: detectando padrões

Nesse artigo, vamos utilizar um algoritmo baseado em Machine Learning (máquina de aprendizado) para detectar as fraudes.

“Machine Learning é uma tecnologia onde os computadores têm a capacidade de aprender de acordo com as respostas esperadas por meio associações de diferentes dados, os quais podem ser imagens, números e tudo que essa tecnologia possa identificar”.

Esse mecanismo funciona de muitas maneiras, mas segue um conceito base, na qual serve como um roteiro no momento de desenvolver o algoritmo. 

  1. Condição: Separar os dados que serão treinados e o que serão de testados, tratá-los e padronizá-los.
  2. Seleção de algoritmo: Selecionar o modelo de treino onde a máquina irá aprender.
  3. Treinamento: Utilizando os dados de treinamento e o modelo selecionado, iremos detectar um padrão.
  4. Teste: Com o modelo treinado, irá colocá-lo em um ambiente de teste, e verá se alcançará a eficiência preditiva.

Obtenção dos dados

Os dados que usaremos neste projeto foram disponibilizados por algumas empresas europeias de cartão de crédito. O dataset representa as operações financeiras que aconteceram no período de três dias, onde foram classificadas 492 fraudes em meio a quase 280 mil transações.

Como você pode notar, este é um conjunto de dados extremamente desbalanceado, onde as fraudes representam apenas 0,17% do total.

Ele contém apenas variáveis ​​de entrada numéricas que são o resultado de uma transformação PCA (Principal Component Analysis). Infelizmente, devido a questões de confidencialidade, a instituição que fornece os dados não podem fornecer os recursos originais e mais informações básicas sobre os dados. 

As variáveis V1, V2, … V28 são os principais componentes obtidos com PCA, as únicas características que não foram transformadas com PCA são ‘Time‘ and ‘Amount‘. 

  • O recurso ‘Time‘ contém os segundos decorridos entre cada transação e a primeira transação no conjunto de dados.
  • O recurso ‘Amount‘ é o valor da transação. 
  • O recurso ‘Class’ é a variável de resposta e assume valor 1 em caso de fraude e 0 caso contrário.

Atenção: O portal está em constante mudança, e existe a possibilidade do conjunto de dados utilizados ser atualizado ou removido, não estando mais disponível, o que poderá trazer erros no código.

E para resolver isso, disponibilizei o arquivo Credicard, diretamente do meu Dropbox, para poder replicar esse processo em outros momentos.

Vamos importar todos os pacotes e módulos que iremos utilizar. Borá abrir sua IDE ou o google colab.

In [1]:

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import seaborn as sns

sns.set_theme()

!pip install -q scikit-plot

import scikitplot as skplt

from sklearn.model_selection import train_test_split

from sklearn.preprocessing import StandardScaler

from sklearn.linear_model import LogisticRegression

from sklearn.metrics import classification_report

from sklearn.metrics import precision_recall_curve, average_precision_score, confusion_matrix, auc, roc_curve

from sklearn.metrics import precision_score, recall_score, f1_score, roc_auc_score, accuracy_score, classification_report

A principal biblioteca que vamos importar para construir nossa Machine Learning se chama scikit-lear, que possui diversos métodos, algoritmos e técnicas bem interessantes que simplificam a vida.

Vamos importar os arquivos credcard.csv para os dataframe df.

In [2]:

df = pd.read_csv("https://www.dropbox.com/s/ztwen3072mzwgvf/creditcard.csv?dl=1")

Uma dica para quem usar o Dropbox, quando você gera um link para compartilhar o seu Dataset (csv), note que no final do link vai haver o código dl=0, altere ele para dl=1, assim você irá informar ao Dropbox que queremos o arquivo Excel e não o HTML.

Descobrindo e explorando

Já citamos anteriormente quais variáveis iremos encontrar, que decorrente a transformação feita usando PCA, iremos ter colunas V1, V2, … V28, variáveis transformadas e o Amount, Time e Class que permanecerão inalteradas.

Vamos investigar os dados do dataframe e analisar as 5 primeiras entradas.

In [3]:

df.head()

Out [3]:

New York

TimeV1V2V3V4V5V6V7V8V9V10V11V12V13V14V15V16V17V18V19V20V21V22V23V24V25V26V27V28AmountClass
00.0-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.3637870.090794-0.551600-0.617801-0.991390-0.3111691.468177-0.4704010.2079710.0257910.4039930.251412-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.021053149.620
10.01.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425-0.1669741.6127271.0652350.489095-0.1437720.6355580.463917-0.114805-0.183361-0.145783-0.069083-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.0147242.690
21.0-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-15146540.2076430.6245010.0660840.717293-0.1659462.345865-28900831.109969-0.121359-22618570.5249800.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.059752378.660
31.0-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1387024-0.054952-0.2264870.1782280.507757-0.287924-0.631418-1.059647-0.6840931.965775-1.232622-0.208038-0.1083000.005274-0.190321-1.1755750.647376-0.2219290.0627230.061458123.500
42.0-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.8177390.753074-0.8228430.5381961.345852-1.1196700.175121-0.451449-0.237033-0.0381950.8034870.408542-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.21515369.990

Entradas e atributos

O próprio site detentor do dataset, já nos diz haver 280 mil transações  e que as fraudes correspondem a  0,17%. E para ter certeza vamos fazer uma análise profunda. 

In [4]:

print(f'Entradas: {df.shape[0]}\nVariáveis: {df.shape[1]}\n')

Out [4]:

Entradas: 284807

Variáveis: 31

Investigando o volume de dados, identificamos que temos 284.807 entradas (transações) e 31 variáveis no total.

Os atributos dos dados ou tipos primitivos, são os formatos dos dados em que a linguagem (python) irá interpretar. E para examinarmos os atributos das nossas variáveis bastas digitar.

In [5]:

df.dtypes

Out [5]:

Time          float64

V1               float64

V2               float64

V3               float64

V4               float64

V5               float64

V6               float64

V7               float64

V8               float64

V9               float64

V10             float64

V11               float64

V12              float64

V13              float64

V14              float64

V15              float64

V16              float64

V17              float64

V12              float64

V18              float64

V19              float64

V20             float64

V21               float64

V22              float64

V23              float64

V24              float64

V25              float64

V26              float64

V27              float64

V28              float64

Amount     float64

Class            intt64

dtype:   object

Todos os tipos são float, ou seja, números reais, com exceção da variável Class que é um número inteiro. Vamos aproveitar e identificar se há valores ausentes.

In [6]:

df.isnull().sum() / df.shape[0]

Out [6]:

Time          0.0

V1               0.0

V2               0.0

V3               0.0

V4               0.0

V5               0.0

V6               0.0

V7               0.0

V8               0.0

V9               0.0

V10             0.0

V11              0.0

V12              0.0

V13              0.0

V14             0.0

V15             0.0

V16             0.0

V17             0.0

V12              0.0

V18             0.0

V19             0.0

V20            0.0

V21             0.0

V22            0.0

V23            0.0

V24            0.0

V25            0.0

V26            0.0

V27            0.0

V28            0.0

Amount    0.0

Class          0.0

dtype:   object

Como é possível perceber, não temos nenhum valor ausente, facilitando nossa vida em relação ao tratamento desses dados. Não podemos esquecer de observar a distribuição das nossas variáveis e se há outliers. Para isso vamos explorar um pouco os parâmetros estatísticos.

In [7]:

df.describe()

Out [7]:

TimeV1V2V3V4V5V6V7V8V9V10V11V12V13V14V15V16V17V18V19V20V21V22V23V24V25V26V27V28AmountClass
count2.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+52.848070e+5
mean94813.8595753.91956E-95.688174E-10-8.769071E-92.782312E-9-1.552563E-92.010663E-9-1.694249E-9-1.927028E-10-3.137024E-91.768627E-99.170318E-10-1.810658E-91.693438E-91.479045E-93.482336E-91.392007E-9-7.528491E-104.328772E-109.049732E-105.085503E-101.537294E-107.959909E-105.36759E-104.458112E-91.453003E-91.699104E-9-3.660161E-10-1.206049E-1088.3496190.001727
std47488.14595519586961651309151625514158691380247133227112370941194353109863210888501020713999201.4995274.2958595.6915316876252.9849337.1838176.2814040.5770925734524725701.6624460.3605647.1521278.1482227403632.5330083.3250.1201090.041527
min0.000000-56407510-72715730-48325590-5683171-113743300-26160510-43557240-73216720-13434070-24588260-4797473-18683710-5791881-19214330-4498945-14129850-25162800-9498746-7213527-54497720-34830380-10933140-44807740-2836627-10295400-2604551-22565680-154300800.0000000.000000
0.2554201.500000-920373.4-598549.9-890364.8-848640.1-691597.1-768295.6-554075.9-208629.7-643097.6-535425.7-762494.2-405571.5-648539.3-425574-582884.3-468036.8-483748.3-498849.8-456298.9-211721.4-228394.9-542350.4-161846.3-354586.1-317145.1-326983.9-70839.53-52959.795.6000000.000000
0.584692.00000018108.865485.56179846.3-19846.53-54335.83-274187.140103.0822358.04-51428.73-92917.38-32757.35140032.6-13568.0650601.3248071.5566413.32-65675.75-3636.3123734.823-62481.09-29450.176781.943-11192.9340976.0616593.5-52139.111342.14611243.8322.0000000.000000
0.75139320.5000001315642803723.91027196743341.3611926.4398564.9570436.1327345.9597139453923.4739593.4618238662505493149.8648820.8523296.3399675500806.7458949.4133040.8186377.2528553.6147642.1439526.6350715.6240952.291045.1278279.9577.1650000.000000
max172792.000000245493022057730938255816875340348016707330163012058950020007210155949902374514012018910784839271268831052677088777421731511092535265041069559197139420900272028401050309022528410458454975195893517346316122003384781025691.1600001.000000

As variáveis transformadas pelo PCA, não tiveram um grande desvio padrão, ou seja, variância entre os pontos.

Já a variável Amout, possui um desvio padrão de 250, isso se dá por haver alguns pontos discrepantes, tendo um ponto máximo de  ~$ 25 mil, entretanto, sua média e mediana correspondeu respectivamente $ 88,00 e 22,00, uma diferença bem grande.

E para entender melhor essa distribuição, vamos ver quantas transações são fraudes e quantas são legítimas. Só lembrando que:

0 – Transação  normal

1 – Fraude

In [8]:

print(f'N° Fraude: {df[df.Class == 1].value_counts().sum()} ({(df[df.Class == 1].shape[0] / df.shape[0]) * 100:.3f}%)')

print(f'N° Normal: {df[df.Class == 0].value_counts().sum()} ({(df[df.Class == 0].shape[0] / df.shape[0]) * 100:.3f}%)\n')

fig, ax = plt.subplots()

sns.countplot('Class', data=df, color='#9b59b6')

sns.despine(left=True)

ax.set_title('Distribuição das Classes')

ax.set_facecolor('White')

plt.plot()

Out [8]:

N° Fraude: 492 (0.173%)

N° Normal: 284315 (99.827%)

Há uma diferença muito grande em quantidade de transações normais para fraudes, isso vai impactar quando estivermos desenvolvendo nosso modelo de Machine Learning, requerendo mais tarde balancear essa proporção.

Outro ponto a observar é a distribuição da quantidade de transações por tempo e comparar se há um padrão entre o normal e as fraudes.

In [9]:

fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(12,6))

plt.subplots_adjust(left=0.125, bottom=0.1, right=0.9, top=0.9, wspace=0.2, hspace=0.35)

num_bins = 40

ax[0].hist(df.Time[df.Class == 0], bins=num_bins, color = "#9b59b6")

ax[0].set_title('Normal')

ax[0].set_facecolor('White')

ax[1].hist(df.Time[df.Class == 1], bins=num_bins, color = "silver")

ax[1].set_title('Fraude')

ax[1].set_facecolor('White')

plt.xlabel('Tempo')

plt.ylabel('Transações')

Out [9]:

Ambas às duas possuem um padrão sazonal, daria até em um tópico futuro fazer uma análise de séries temporais, mas para o momento isso basta. Além disso, não conseguimos tirar muitas informações relevantes.

Vamos observar a distribuição do Amount, nesse caso usaremos o boxblot como método de análise para distribuição e identificação de outliers.

In [10]:

ax, fig = plt.subplots(figsize=(16,9))

c = "purple"




box = plt.boxplot([df.Amount[df.Class == 1], df.Amount[df.Class == 0]], labels=['Fraude', 'Normal'], vert=False, patch_artist=True)

plt.xlim((-20, 400))

fig.set_facecolor('White')




colors = ['silver', '#9b59b6']

 

for patch, color in zip(box['boxes'], colors):

    patch.set_facecolor(color)

Out [10]:

As transações normais possuem muito mais pontos discrepantes, e outro fato é que possui uma mediana maior que as transações fraudulentas. Ou seja, os bandidos tentam atacar usando valores baixos para não serem pegos.

Já que analisamos as variáveis mais importantes, vamos dar uma olhada naquelas que houve a transformação PCA e ver se há uma semelhança na distribuição entre fraude e normal, usando uma função de densidade de probabilidade contínua (kdeplot).

In [11]:

dx = ['V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', 'V11',

       'V12', 'V13', 'V14', 'V15', 'V16', 'V17', 'V18', 'V19', 'V20', 'V21',

       'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']

class_0 = df[df.Class == 0]

class_1 = df[df.Class == 1]




fig, ax = plt.subplots(nrows=7, ncols=4, figsize=(18,18))

fig.subplots_adjust(hspace=0.5, wspace=0.2)

i = 0




for n in dx:

  i += 1

  plt.subplot(7, 4, i)

  sns.kdeplot(class_0[n], label='Normal', shade=True, color='purple')

  sns.kdeplot(class_1[n], label='Fraude', shade=True, color='gray')

  plt.legend(loc='upper left')

Out [11]:

As variáveis V13, 15, 22, 24, 25, 26 e 28, são muito semelhantes, isso pode ser um prelúdio, que serão variáveis que não terão um grau de importância para a detecção de fraudes, sendo elas difícil de distinguir ou separar o que é fraude do que não é. Mas claro, isso é só uma hipótese, temos que observar na prática.

Padronização dos dados

Enfim chegamos a um momento crítico e importante, padronizar os dados é uma etapa importantíssima que consiste na transformação dos dados, é uma prática para evitar que seu algoritmo fique enviesado para as variáveis com maior ordem de grandeza.

É um método que tem o objetivo de transformar todas as variáveis na mesma ordem de grandeza, resultando em uma média igual a 0 e um desvio padrão igual a 1. Usaremos o código StandardScaler()

Matematicamente falando o código StandardScaler() usa a fórmula z-score para fazer a padronização dos dados: 

z=\frac{x-\mu}{\sigma}

Vamos fazer uma cópia do dataframa, padronizar as colunas Amount e times, por fim vamos mostrar os dados transformados.

In [12]:

# Copia do dataframe

df_clean = df.copy()




# Padronizar as colunas Time e Amount

scaler = StandardScaler()

df_clean['Amount'] = scaler.fit_transform(df_clean['Amount'].values.reshape(-1,1))

df_clean['Time'] = scaler.fit_transform(df_clean['Time'].values.reshape(-1, 1))




#ver as 5 primeiras linhas

df_clean.head()

Out [12]:

TimeV1V2V3V4V5V6V7V8V9V10V11V12V13V14V15V16V17V18V19V20V21V22V23V24V25V26V27V28AmountClass
0-1.996583-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.3637870.090794-0.551600-0.617801-0.991390-0.3111691.468177-0.4704010.2079710.0257910.4039930.251412-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.0210530.2449640
1-1.9965831.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425-0.1669741.6127271.0652350.489095-0.1437720.6355580.463917-0.114805-0.183361-0.145783-0.069083-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.014724-0.3424750
2-1.996562-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-1.5146540.2076430.6245010.0660840.717293-0.1659462.345865-28900831.109969-0.121359-22618570.5249800.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.0597521.1606860
3-1.996562-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1.387024-0.054952-0.2264870.1782280.507757-0.287924-0.631418-1.059647-0.6840931.965775-1232622-0.208038-0.1083000.005274-0.190321-11755750.647376-0.2219290.0627230.0614580.1405340
4-1.996541-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.8177390.753074-0.8228430.5381961.345852-11196700.175121-0.451449-0.237033-0.0381950.8034870.408542-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.215153-0.0734030

Treino e teste

É uma convenção separar uma quantidade de dados do dataframe para treinar o modelo de Machine Learning e com outra parte usar para testá-la, ou seja, ver se irá atingir um bom desempenho na verificação de fraudes

Os dados de treino representam cerca de 70% da totalidade, enquanto os dados de teste representam 30%. Para fazermos isso vamos separar o dataframe em dois:

X – Corresponde a todas as variáveis (Amount, time, V1 à V28)

y – Corresponde a resposta (Class).

O passo seguinte é usar a função train_test_split que irá separar dados  em treino e teste correspondendo respectivamente as variáveis independentes (X_train, X_test) e variáveis dependentes  (y_train, y_test).

In [13]:

X = df_clean.drop('Class', axis=1)

y = df_clean.Class




#separar treino e teste

X_train, X_test, y_train, y_test = train_test_split(X, y)

Regressão logística

Escolher o modelo de Machine Learning é uma ciência, onde é possível haver vários resultados diferentes dependendo do modelo escolhido. Para meios de simplificação do nosso estudo, vamos utilizar a regressão logística, por entender que o problema proposto é uma solução binária 0 ou 1, caso esse característico da regressão logística, mas abro aspas “árvore de decisão e kmeans são modelos que poderiam ser utilizados no estudo e podem ser até mais eficientes que a regressão logística“, se tornando esse, um fator a mais quando pensamos em comparar modelos de treino.

A Regressão Logística é um algoritmo de Aprendizado de Máquina utilizado para a classificação de problemas, é um algoritmo de análise preditiva e baseado no conceito de probabilidade.

Alguns dos exemplos de problemas de classificação são spam de e-mail ou não spam, transações online fraude ou não fraude, tumor maligno ou benigno. A regressão logística transforma sua saída usando a função sigmóide logística para retornar um valor de probabilidade.

Usaremos a função LogisticRegression() para treinar o modelo, mas antes disso, mostrarei o conceito matemático por trás do código.

E esta função vai ser usada para estimar a probabilidade da classe. Ou seja, vou calcular a probabilidade de y ser igual a zero para os dados que observei os atributos (X). O mesmo acontecerá para avaliar a probabilidade de y ser igual a 1.

p\left (y=0|X \right)    e  p\left (y=1|X \right)  

Vamos usar equação logística ou também conhecida como curva sigmóide, pois nessa característica dos pontos, é ela que se encaixa melhor, conforme é observado na imagem acima.

 Função sigmóide ou logística

f\left ( z \right )=\frac{1}{1+e^{-z}}

Temos que também relembrar e trazer os elementos da regressão linear para podermos calcular, já que este método utiliza de coeficientes para ajustar a curva:

\widehat{y}=z=\theta ^{T}X

Vamos calcular as probabilidades substituindo o z na curva sigmoide para ambas as probabilidades:

p\left ( y=1|X \right )         p = \frac{1}{1+e^{-\theta ^{T}X}} 

p\left ( y=0|X \right )         1-p =1- \frac{1}{1+e^{-\theta ^{T}X}} 

Para irmos a próxima etapa, precisamos evocar a distribuição de Bernoulli, aplicada em casos de experimentos repetidos, onde existem dois possíveis resultados [0 e 1]. Ela nos fornecerá os termo matemáticos para cada umas das hipóteses.

p\left ( x=k \right )=p^{k}\left ( 1-p \right )^{1-k}           para  k=\left [ 1,0 \right ]

Se  p\left ( x=0 \right )=p^{0}\left ( 1-p \right )^{1-0}=\left ( 1-p \right )

Se  p\left ( x=1 \right )=p^{1}\left ( 1-p \right )^{1-1}=p

Podemos em fim calcular usando a função de verossimilhança, com base em Bernoulli.

L\left ( \theta \right )=\prod_{i=1}^{n}p^{y_{i}}\left ( 1-p \right )^{1-y_{i}}

Agora o que temos que fazer é maximizar essa função de verossimilhança, da forma que possa ajustar a curva aos dados. Esse é um método de inferência estatística, onde poderemos fazer afirmações a partir de um conjunto de valores representativo sobre uma amostra.

E para resolver o produtório, vamos utilizar logaritmos.

p=\widehat{y}

log L\left ( \theta \right )=\sum_{i=1}^{n}y_{i}log\left ( \widehat{y} \right )+\left ( 1-y_{i} \right )log\left ( 1-\widehat{y} \right )

-log L\left ( \theta \right )        Cross entropy

y_{i}log\left ( \widehat{y} \right )+\left ( 1-y_{i} \right )log\left ( 1-\widehat{y} \right )      Cost function

Conseguimos calcular o Cost Function. Ela representa o objetivo de otimização, ou seja, criamos uma função de custo e a minimizamos para podermos desenvolver um modelo preciso com erro mínimo. Agora o algoritmo usará um método numérico como o Newton-Raphson, iterando até encontrar o menor custo.

Vamos treinar o modelo.

In [14]:

model = LogisticRegression()

model.fit(X_train, y_train)

importance = model.coef_[0

Precisamos tentar prever os valores para os dados não treinados (X_teste), testar o nosso modelo, e é quando a função predict() entra em cena.

In [15]:

y_pred = model.predict(X_test)

Matriz de confusão

Uma das principais maneiras de verificar o desempenho do algoritmo é por meio da Matriz de Confusão. Para cada classe, ela informa quais os valores reais (True label) e os valores previstos pelo modelo (predicted label).

  • Verdadeiro positivo (TP): Por exemplo, quando a transação é normal e o modelo classifica como normal.
  • Falso positivo (FP): Por exemplo, quando a transação é normal, mas o modelo classifica como fraude.
  • Falso negativo (FN): Por exemplo, quando a transação é fraudulenta e o modelo classifica como normal.
  • Verdadeiro negativo (TN): Por exemplo, quando a transação é fraudulenta e o modelo classifica como fraude.

Esse é o modelo da matriz de confusão, ele deixa claro o percentual de cada análise e erros. Assim conseguimos identificar qual o percentual de acerto que o nosso modelo teve na hora de detectar as fraudes, essa métrica é mais relevante, mas claro, não é só isso, precisamos analisar outras métricas para tirar uma conclusão mais precisa.

Acurácia

Definitivamente, é a métrica mais intuitiva e fácil para se entender. A acurácia mostra diretamente a porcentagem de acertos do nosso modelo.

A=\frac{PrevisoesCorretas}{PrevisoesTotais}

Apesar de ser muito direta e intuitiva, a acurácia não é uma boa métrica quando você lida, por exemplo, com dados desbalanceados.

Precision

A precisão diz respeito à quantidade (proporcional) de identificações positivas feita corretamente, e é obtida pela equação.

Precision=\frac{TP}{TP+FP}

Recall

Mostra a proporção de positivos encontrados corretamente. Matematicamente, você calcula o recall da seguinte maneira:

Rcall=\frac{TP}{TP+FN}

F1-score

É a média harmônica entre precisão e recall. O melhor valor possível para 0 F1-score é 1 e o pior é 0. É calculado por.

F1-score=2\frac{Precision*Recall}{Precision +Recall}

ROC_AUC

É a área sob a curva característica de Operação do Receptor. Resumindo é uma curva de probabilidade que representa o grau ou medida de separabilidade, ou seja, quanto maior o AUC, melhor é o desempenho do modelo em distinguir fraudes e não fraudes.

Para gerar o relatório  de classificação juntamente com a matriz de confusão, faremos o seguinte código.

In [16]:

# plotar a matrix de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred, normalize=True, cmap='BuPu')

sns.despine(left=True, bottom=True)




# imprimir relatório de classificação

print("Relatório de Classificação:\n", classification_report(y_test, y_pred, digits=4))




# imprimir a área sob da curva

print("AUC: {:.4f}\n".format(roc_auc_score(y_test, y_pred)))

Out [16]:

Relatório de Classificação:

                      Precision     recall     f1-score     support     

              0         0.9994      0.9998     0.9996        71083

               1          0.8172      0.6387      0.7170            119

 

accuracy                                             0.9992         71202

macro avg     0.9083      0.8192      0.8583          71202

weighted avg 0.9991     0.9992     0.9991           71202

AUC: 0.8192

Já percebemos que mesmo o nosso modelo dando uma acurácia bem alta, ele não foi eficaz em detectar as fraudes, conseguiu acertar apenas 64% delas. E um dos motivos, é que treinamos o nosso modelo com dados desbalanceados. Sendo assim, precisamos explorar métodos de balanceamento e ver se irá melhorar o desempenho do nosso modelo.

Modelos de balanceamento

A classe desbalanceada ocorre quando temos um dataset que possui muitos exemplos de uma classe e poucos exemplos da outra classe.

Essa tendência no conjunto de dados de treinamento pode influenciar muitos algoritmos de aprendizado de máquina, levando a ignorar completamente a classe minoritária, sendo um problema, pois normalmente é a classe minoritária em que as previsões são mais importantes.

Para isso levantamos 4 modelos de balanceamento de classe:

  • RandomUnderSampling (RUS)
  • SMOTE (Over-Sampling)
  • ADASYN
  • Near Miss (version=1)

Para cada uma iremos além de balancear, treinar o modelo utilizando a regressão logística e verificar o desempenho observando a matriz de confusão e as outras métricas.

RandomUnderSampling (RUS)

As técnicas de subamostragem ou Random Undersampling removem os dados aleatórios do conjunto de dados de treinamento que pertencem à classe majoritária (classe 0) para equilibrar melhor a distribuição da classe.

Vamos importar o módulo do RUS, separar os dados de treino e checar à distribuição após o balanceamento.

In [17]:

#RUS (Random Under Sampling)

from imblearn.under_sampling import RandomUnderSampler

rus = RandomUnderSampler()

X_rus, y_rus = rus.fit_resample(X_train, y_train)

# Checar o balanceamento das classes

print(pd.Series(y_rus).value_counts())

# Plotar a nova distribuição de Classes

flatui = ["#9b59b6""#b3b3b3"]

sns.set_palette(flatui)

sns.countplot(y_rus);

sns.despine(left=True)

plt.show()

Out [17]:

1    373

0   373

Ele fez o balanço, definindo as fraudes como a classe minoritária, e assim reduzindo os dados das transações normais. Agora vamos treinar o modelo e verificar o seu desempenho.

In [18]:

#modelo de regressão logística

model = LogisticRegression()

model.fit(X_rus, y_rus)

importance_rus = model.coef_[0]




# fazer as previsões em cima dos dados de teste

y_pred_rus = model.predict(X_test)




# plotar a matrix de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred_rus, normalize=True, cmap='BuPu')

sns.despine(left=True, bottom=True)




# imprimir relatório de classificação

print("Relatório de Classificação:\n", classification_report(y_test, y_pred_rus, digits=4))




# imprimir a área sob da curva

print("AUC: {:.4f}\n".format(roc_auc_score(y_test, y_pred_rus)))

Out [18]:

Relatório de Classificação:

                        Precision     recall     f1-score     support     

              0          0.9998      0.9681      0.9837        71083

               1          0.0443     0.8824      0.0844            119

accuracy                                              0.9680         71202

macro avg       0.5221      0.9252       0.5340         71202

weighted avg 0.9982     0.9680      0.9822          71202

AUC: 0.9252

Comparando com o modelo desbalanceado, tivemos uma redução na acurácia global, mas aumentou bastante a assertividade na detecção de fraudes (88%) e reduziu o percentual de falsos negativos.

SMOTE (Over-Sampling)

Synthetic Minority Oversampling Technique ou Técnica de sobreamostragem de minoria sintética é a abordagem mais simples e envolve a duplicação de exemplos na classe minoritária, embora esses exemplos não adiciona nenhuma informação nova ao modelo, esses novos exemplos podem ser sintetizados a partir dos exemplos existentes.

Seguindo o mesmo processo da linha de código da técnica anterior, vamos analisar o balanceamento.

In [19]:

# Balanceamento dos Dados com Over-Sampling (SMOTE)

from imblearn.over_sampling import SMOTE


smo = SMOTE()

X_smo, y_smo = smo.fit_resample(X_train, y_train)


# Checar o balanceamento das classes

print(pd.Series(y_smo).value_counts())


# Plotar a nova distribuição de Classes

flatui = ["#9b59b6", "#b3b3b3"]

sns.set_palette(flatui)

sns.countplot(y_smo);

sns.despine(left=True)

Out [19]:

1   213232

0  213232

Como é possível notar tivemos um aumento no volume de dados que são fraudes. agora vamos avaliar a eficiência do modelo.

In [20]:

#modelo de regressão logistica

model = LogisticRegression()

model.fit(X_smo, y_smo)

importance_smo = model.coef_[0]




# fazer as previsões em cima dos dados de teste

y_pred_smo = model.predict(X_test)




# plotar a matrix de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred_smo, normalize=True, cmap='BuPu')

sns.despine(left=True, bottom=True)




# imprimir relatório de classificação

print("Relatório de Classificação:\n", classification_report(y_test, y_pred_smo, digits=4))




# imprimir a área sob da curva

print("AUC: {:.4f}\n".format(roc_auc_score(y_test, y_pred_smo)))

Out [20]:

Relatório de Classificação:

                        Precision     recall     f1-score     support     

              0          0.9998      0.9761      0.9878        71083

               1          0.0577     0.8739       0.1083            119

accuracy                                              0.9760         71202

macro avg       0.5288      0.9252      0.5481         71202

weighted avg 0.9982     0.9760      0.9863         71202

AUC: 0.9250

Comparando novamente com o modelo desbalanceado a acurácia foi menor, mas teve maior eficiência na detecção de fraudes (87%).

ADASYN

ADASYN (Abordagem de amostragem sintética adaptável para aprendizado desequilibrado), ele é um método muito semelhante ao Smote, mas depois de criar a amostra, ele adiciona valores aleatórios pequenos aos pontos, tornando-os mais realistas. Em outras palavras, em vez de toda a amostra ser linearmente correlacionada à origem, eles têm um pouco mais de variância, ou seja, elas são dispersas.

In [21]:

from imblearn.over_sampling import ADASYN




ada = ADASYN()

X_ada, y_ada = ada.fit_resample(X_train, y_train)




# Checar o balanceamento das classes

print(pd.Series(y_ada).value_counts())




# Plotar a nova distribuição de Classes

flatui = ["#9b59b6", "#b3b3b3"]

sns.set_palette(flatui)

sns.countplot(y_ada);

sns.despine(left=True)

Out [21]:

1  213232

0  213229

A quantidade de valores de cada classe é muito semelhante ao SMOTE, temos que analisar se essas pequenas diferenças irão alterar no desempenho do modelo.

In [22]:

#modelo de regressão logistica

model = LogisticRegression()

model.fit(X_ada, y_ada)

importance_ada = model.coef_[0]




# fazer as previsões em cima dos dados de teste

y_pred_ada = model.predict(X_test)




# plotar a matrix de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred_ada, normalize=True, cmap='BuPu')

sns.despine(left=True, bottom=True)




# imprimir relatório de classificação

print("Relatório de Classificação:\n", classification_report(y_test, y_pred_ada, digits=4))




# imprimir a área sob da curva

print("AUC: {:.4f}\n".format(roc_auc_score(y_test, y_pred_ada)))

Out [22]:

Relatório de Classificação:

                        Precision     recall     f1-score     support     

              0          0.9999      0.9178      0.9571        71083

               1          0.0185     0.9244       0.0362            119

accuracy                                              0.9178         71202

macro avg       0.5092     0.9211      0.4966         71202

weighted avg 0.9982     0.9178      0.9555         71202

AUC: 0.9211

Quando comparamos com o modelo desbalanceado e ao SMOTE, notamos que a acurácia é menor, mas é maior a eficiência em detectar fraudes. Ou seja, pequenas inserções de valores aleatórios fez o modelo ter um bom ganho de desempenho.

Near Miss

O algoritmo é semelhante ao RUS, a diferença é que ele observa a distribuição de classes e elimina aleatoriamente amostras da classe majoritária. Quando dois pontos pertencentes a classes diferentes estão muito próximos um do outro na distribuição, esse algoritmo elimina o ponto de dados da classe majoritária, tentando equilibrar a distribuição.

In [23]:

from imblearn.under_sampling import NearMiss



nr = NearMiss()

X_nr, y_nr = nr.fit_resample(X_train, y_train)




# Checar o balanceamento das classes

print(pd.Series(y_nr).value_counts())




# Plotar a nova distribuição de Classes

flatui = ["#9b59b6", "#b3b3b3"]

sns.set_palette(flatui)

sns.countplot(y_nr);

sns.despine(left=True)

Out [23]:

1 373

0 373

A quantidade de amostra é igual ao modelo do RUS, vamos analisar o desempenho do modelo.

In [24]:

#modelo de regressão logistica

model = LogisticRegression()

model.fit(X_nr, y_nr)

importance_nr = model.coef_[0]




# fazer as previsões em cima dos dados de teste

y_pred_nr = model.predict(X_test)




# plotar a matrix de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred_nr, normalize=True, cmap='BuPu')

sns.despine(left=True, bottom=True)




# imprimir relatório de classificação

print("Relatório de Classificação:\n", classification_report(y_test, y_pred_nr, digits=4))




# imprimir a área sob da curva

print("AUC: {:.4f}\n".format(roc_auc_score(y_test, y_pred_nr)))

Out [24]:

Relatório de Classificação:

                        Precision     recall     f1-score     support     

              0          0.9997      0.5563      0.7148        71083

               1          0.0034     0.9160       0.0069           119

accuracy                                              0.5569         71202

macro avg       0.5016     0.7361      0.3608         71202

weighted avg 0.9981     0.5569      0.7136         71202

AUC: 0.7361

De todos os modelos esse é o que tem a menor acurácia, apesar de ter uma grande eficiência em detectar fraudes (92%), ele falhou bastante em detectar falsos positivos.

Comparando os desempenho dos modelos

É admissível dizer que o balanceamento dos dados é um fator extremamente relevante no contexto de ter uma Machine Learning eficiente, mesmo sua acurácia sendo menor, os modelos balanceados demonstram uma maior eficiência na detecção de fraudes.

O modelo NearMiss, apesar de possuir uma eficiência boa em detectar fraudes, pecou com os falsos positivos (44%). 

O RUS teve seu desempenho muito melhor que seu companheiro de modelo NearMiss, tendo uma eficiência de 88% na detecção de fraudes, mas mesmo assim falhou com os falsos negativos (12%).

O ADASYN e o SMOTE foram os dois melhores modelos, com desempenho ótimos, dando ênfase ao SMOTE, que teve uma acurácia de 97,6%, com uma eficiência de 87% na hora de detectar fraudes.

Avaliando o contexto geral, ele é o modelo  que escolheríamos para desenvolver nosso sistema de detecção de fraudes, mas antes de terminar nossa análise, um fator importante é entender quais as variáveis que mais influenciam no desempenho dos nossos modelos.

Para isso vamos plotar uma matriz de correlação para cada um dos nossos modelos.

In [25]:

# plotar a matriz de correlação

corr = X_train.corr()

corr_rus = pd.DataFrame(X_rus).corr()

corr_smo = pd.DataFrame(X_smo).corr()

corr_ada = pd.DataFrame(X_ada).corr()

corr_nr = pd.DataFrame(X_nr).corr()



fig, ax = plt.subplots(nrows=1, ncols=5, figsize = (30,8))

fig.suptitle('Matriz de Correlação')




sns.heatmap(corr, xticklabels=corr.columns, yticklabels=corr.columns,

            linewidths=.1, cmap="BuPu", ax=ax[0], mask=np.triu(corr))

ax[0].set_title('Desbalanceado')



sns.heatmap(corr_rus, xticklabels=corr.columns, yticklabels=corr.columns,

            linewidths=.1, cmap="BuPu", ax=ax[1],  mask=np.triu(corr_rus))

ax[1].set_title('Balanceado com RUS')




sns.heatmap(corr_smo, xticklabels=corr.columns, yticklabels=corr.columns,

            linewidths=.1, cmap="BuPu", ax=ax[2], mask=np.triu(corr_smo))

ax[2].set_title('Balanceado com SMOTE')




sns.heatmap(corr_ada, xticklabels=corr.columns, yticklabels=corr.columns,

            linewidths=.1, cmap="BuPu", ax=ax[3], mask=np.triu(corr_ada))

ax[3].set_title('Balanceado com ADASYN')



sns.heatmap(corr_nr, xticklabels=corr.columns, yticklabels=corr.columns,

            linewidths=.1, cmap="BuPu", ax=ax[4], mask=np.triu(corr_nr))

ax[4].set_title('Balanceado com NearMiss')



plt.tight_layout()

Out [25]:

Conseguimos agora ver quais as variáveis se correlacionam proporcional e inversamente. Mas para compreender a influência de cada variável no desempenho do modelo, precisamos explorar os conceitos de coeficientes.

Os modelos de regressão logística são instanciados e se ajustam da mesma forma utilizando o atributo coef_ que contém os coeficientes encontrados para cada variável de entrada. Esses coeficientes podem fornecer a base para uma pontuação bruta de importância do recurso.

Para isso vamos buscar no nosso modelo o coef[1] do nosso melhor modelo (SMOTE), já coletado  e assim gerar um plot com as variáveis mais importantes.

In [26]:

importances = pd.Series(data=importance_smo, index=X.columns.values)

importances = importances.sort_values(ascending=False)




plt.figure(figsize=(12,8))

ax = sns.barplot(x=importances, y=importances.index, orient='h', color='#9b59b6')

ax.set_title('Importância de cada feature')

Out [26]:

Este é um problema de classificação com classes 0 e 1. Observe que os coeficientes são positivos e negativos. As pontuações positivas indicam uma característica que prevê a classe 1, enquanto as pontuações negativas indicam uma característica que prevê a classe 0.

E analisando nosso gráfico, notamos que a variável Amount, é a que mais impacta na detecção de fraudes, já o V14 é a mais importante para predizer as transações normais.

Resumo final da ópera

Desenvolver um modelo de Machine Learning realmente não é fácil, ele possui algumas complexidades na qual é necessário explorar para identificar pontos que prejudicaram o desenvolvimento do modelo, como o caso do balanceamento dos dados e a transformação PCA.

O que esse trabalho impacta para quem trabalha com marketing? 

Muita coisa meu caro. Comercial e marketing andam lado a lado, e negócios como e-commerces recebem compras diariamente, imagine essa Machine Learning, ela poderia tornar-se um produto para eles, em que facilitaria a identificação de compras com cartões de créditos fraudados e evitaria complicações para seus clientes, se tornando um diferencial da sua marca.

Sei que foi uma caminhada longa nesse artigo, mas esse é o trabalho de quem vê o mundo através dos dados e assim gera inteligência com uma base forte. Então, meu caro marketeiro.

Prontos para começar do zero?

Arquivo Completo no Github

Deixe seu comentário

Oi, sou Hyan e aqui você vai encontrar tudo sobre marketing e tecnologia, assuntos esses na qual eu me dedico a aprender e me desafiar todas as manhãs.

CATEGORIAS

Machine learning • Cartão de crédito • PCA • Sklearn • Fraudes • Transação • Balanceamento • Regressão logística • Matriz de confusão • Acurácia • SMOTE • Comparativo • Transformação

CRÉDITOS

Produção: Zero.ai

Texto: Hyan Dias Tavares

Revisão: Rafael Duarte

Ilustração: Hyan Dias Tavares

Inspiração: Sigmoidal

NEWSLATTER