Skip to content

vsvasconcelos/ML

Repository files navigation

Projeto: Identificando fraude no Email da Enron. Identificação de suspeitos utilizando Machine Learning

Udacity Data Analyst Nanodegree

Vagner Sanches Vasconcelos vsvasconcelos@gmail.com 27 de maio de 2017

1. Introdução

Este projeto utiliza um conjunto de dados que contém informações de e-mails enviados e/ou recebidos e dados financeiros de funcionários da empresa Enron Corporation, que em 2000 era uma das maiores empresas dos Estados Unidos e dois anos mais tarde faliu depois de um dos maiores escândalos de corrupção da história americana, conforme apresentado no vídeo Enron os mais espertos da sala.

Simpsons

1.1 Objetivo

O objetivo deste trabalho é a construção de um modelo preditivo, utilizando aprendizagem de máquina, para determinar se um funcionário da Enron é ou não um POI (Persons Of Interest), isto é, um suspeito de participar da fraude.
O modelo proposto deve possuir para cada uma das métricas Precision e Recall um valor mínimo de 0.3.

1.2 Ferramental

Todo o projeto foi desenvolvido na linguagem Python com auxílio da biblioteca SciKit-learn, utilizando algoritmos de classificação conforme fluxograma abaixo.
Todo o código produzido, bem como demais arquivos, estão no GitHub do autor.
FlowChart

1.3 Conjunto de dados

A fraude na Enron resultou em uma investigação federal, na qual muitos dados que são normalmente confidenciais, se tornaram públicos, incluindo dezenas de milhares de e-mails e detalhes financeiros dos executivos, em todos os níveis, da empresa. É esse conjunto de dados que é utilizado neste projeto; ele é intitulado: Enron financial and email, é público e pode ser acessado no site da Carnegie Mellon University.

Carregando a base de dados ...

#!/usr/bin/python

import sys
import pickle
import warnings
warnings.filterwarnings('ignore')
sys.path.append("../tools/")

from feature_format import featureFormat, targetFeatureSplit
from tester import dump_classifier_and_data, main
with open("final_project_dataset.pkl", "r") as data_file:
    data_dict = pickle.load(data_file)

Os dados são carregados em uma estrutura de dados tipo dict (dicionário), na qual cada par chave/valor corresponde a um funcionário. A chave do dicionário é o nome do funcionário, e o valor é outro dicionário, que contém o nome de todos os atributos e seus valores para aquele funcionário. Os atributos nos dados possuem basicamente três tipos: atributos financeiros, de email e rótulos POI.

Atributos financeiros: ['salary', 'deferral_payments', 'total_payments', 'loan_advances', 'bonus', 'restricted_stock_deferred', 'deferred_income', 'total_stock_value', 'expenses', 'exercised_stock_options', 'other', 'long_term_incentive', 'restricted_stock', 'director_fees'] (todos em dólares americanos (USD))

Atributos de email: ['to_messages', 'email_address', 'from_poi_to_this_person', 'from_messages', 'from_this_person_to_poi', 'shared_receipt_with_poi'] (as unidades aqui são geralmente em número de emails; a exceção notável aqui é o atributo ‘email_address’, que é uma string)

Rótulo POI: [‘poi’] (atributo objetivo lógico (booleano), representado como um inteiro)

2. Seleção dos atributos (features)

Para sustentar a seleção dos atributos que serão utilizados foi realizado inicialmente uma Análise Exploratória dos Dados (AED) da base.

Convertendo a base para um DataFrame [Pandas](https://en.wikipedia.org/wiki/Pandas_(software).

import pandas as pd
import numpy as np
data_dict_df = pd.DataFrame(data_dict.values(), index=data_dict.keys())

Verificando as dimensões da base de dados:

data_dict_df.shape
(146, 21)

A base possui 146 Observações (linhas) e 21 Variáveis (colunas).

Visão geral da base:

data_dict_df.head(3)
bonus deferral_payments deferred_income director_fees email_address exercised_stock_options expenses from_messages from_poi_to_this_person from_this_person_to_poi ... long_term_incentive other poi restricted_stock restricted_stock_deferred salary shared_receipt_with_poi to_messages total_payments total_stock_value
METTS MARK 600000 NaN NaN NaN mark.metts@enron.com NaN 94299 29 38 1 ... NaN 1740 False 585062 NaN 365788 702 807 1061827 585062
BAXTER JOHN C 1200000 1295738 -1386055 NaN NaN 6680544 11200 NaN NaN NaN ... 1586055 2660303 False 3942714 NaN 267102 NaN NaN 5634343 10623258
ELLIOTT STEVEN 350000 NaN -400729 NaN steven.elliott@enron.com 4890344 78552 NaN NaN NaN ... NaN 12961 False 1788391 NaN 170941 NaN NaN 211725 6678735

3 rows × 21 columns

É possível observar vários valores faltantes NaN (Not a Number); assim, isto deve ser levado em consideração para não influenciar os cálculos, por exemplo, na estatística descritiva.

Convertendo os NaN para o padrão do pacote Numpy.

data_dict_df = data_dict_df.replace('NaN', np.NaN)

Tipos de dados das variáveis:

data_dict_df.dtypes
bonus                        float64
deferral_payments            float64
deferred_income              float64
director_fees                float64
email_address                 object
exercised_stock_options      float64
expenses                     float64
from_messages                float64
from_poi_to_this_person      float64
from_this_person_to_poi      float64
loan_advances                float64
long_term_incentive          float64
other                        float64
poi                             bool
restricted_stock             float64
restricted_stock_deferred    float64
salary                       float64
shared_receipt_with_poi      float64
to_messages                  float64
total_payments               float64
total_stock_value            float64
dtype: object

A grande maioria das variáveis são do tipo numérica (float64); há ainda uma variável do tipo categórica (bool) e uma do tipo texto (object).

Estatística descritiva da base:

data_dict_df.describe()
bonus deferral_payments deferred_income director_fees exercised_stock_options expenses from_messages from_poi_to_this_person from_this_person_to_poi loan_advances long_term_incentive other restricted_stock restricted_stock_deferred salary shared_receipt_with_poi to_messages total_payments total_stock_value
count 8.200000e+01 3.900000e+01 4.900000e+01 1.700000e+01 1.020000e+02 9.500000e+01 86.000000 86.000000 86.000000 4.000000e+00 6.600000e+01 9.300000e+01 1.100000e+02 1.800000e+01 9.500000e+01 86.000000 86.000000 1.250000e+02 1.260000e+02
mean 2.374235e+06 1.642674e+06 -1.140475e+06 1.668049e+05 5.987054e+06 1.087289e+05 608.790698 64.895349 41.232558 4.196250e+07 1.470361e+06 9.190650e+05 2.321741e+06 1.664106e+05 5.621943e+05 1176.465116 2073.860465 5.081526e+06 6.773957e+06
std 1.071333e+07 5.161930e+06 4.025406e+06 3.198914e+05 3.106201e+07 5.335348e+05 1841.033949 86.979244 100.073111 4.708321e+07 5.942759e+06 4.589253e+06 1.251828e+07 4.201494e+06 2.716369e+06 1178.317641 2582.700981 2.906172e+07 3.895777e+07
min 7.000000e+04 -1.025000e+05 -2.799289e+07 3.285000e+03 3.285000e+03 1.480000e+02 12.000000 0.000000 0.000000 4.000000e+05 6.922300e+04 2.000000e+00 -2.604490e+06 -7.576788e+06 4.770000e+02 2.000000 57.000000 1.480000e+02 -4.409300e+04
25% 4.312500e+05 8.157300e+04 -6.948620e+05 9.878400e+04 5.278862e+05 2.261400e+04 22.750000 10.000000 1.000000 1.600000e+06 2.812500e+05 1.215000e+03 2.540180e+05 -3.896218e+05 2.118160e+05 249.750000 541.250000 3.944750e+05 4.945102e+05
50% 7.693750e+05 2.274490e+05 -1.597920e+05 1.085790e+05 1.310814e+06 4.695000e+04 41.000000 35.000000 8.000000 4.176250e+07 4.420350e+05 5.238200e+04 4.517400e+05 -1.469750e+05 2.599960e+05 740.500000 1211.000000 1.101393e+06 1.102872e+06
75% 1.200000e+06 1.002672e+06 -3.834600e+04 1.137840e+05 2.547724e+06 7.995250e+04 145.500000 72.250000 24.750000 8.212500e+07 9.386720e+05 3.620960e+05 1.002370e+06 -7.500975e+04 3.121170e+05 1888.250000 2634.750000 2.093263e+06 2.949847e+06
max 9.734362e+07 3.208340e+07 -8.330000e+02 1.398517e+06 3.117640e+08 5.235198e+06 14368.000000 528.000000 609.000000 8.392500e+07 4.852193e+07 4.266759e+07 1.303223e+08 1.545629e+07 2.670423e+07 5521.000000 15149.000000 3.098866e+08 4.345095e+08

Quantas funcionários há na base?

len(data_dict_df)
146

Existem 146 funcionários na base de dados sendo que cada um deles possui até 21 atributos registrados.
Como existem muitos valores faltantes (NaN) nem todos possuem todos os atributos.

Quantos atributos existem para cada variável?

data_dict_df.count()
bonus                         82
deferral_payments             39
deferred_income               49
director_fees                 17
email_address                111
exercised_stock_options      102
expenses                      95
from_messages                 86
from_poi_to_this_person       86
from_this_person_to_poi       86
loan_advances                  4
long_term_incentive           66
other                         93
poi                          146
restricted_stock             110
restricted_stock_deferred     18
salary                        95
shared_receipt_with_poi       86
to_messages                   86
total_payments               125
total_stock_value            126
dtype: int64

Somente o atributo poi está presente para todos os funcionários da base; algumas outras observações, por exemplo: somente 4 funcionários possuem dados da variável loan_advances e 95 possuem informações da variável salary.

Quantas variáveis faltantes cada funcionário possui?

num_var_func = data_dict_df.isnull().sum(axis=1)
num_var_func.sort_values(ascending=False).head(15)
LOCKHART EUGENE E                20
WROBEL BRUCE                     18
THE TRAVEL AGENCY IN THE PARK    18
GRAMM WENDY L                    18
WHALEY DAVID A                   18
GILLIS JOHN                      17
WODRASKA JOHN                    17
CLINE KENNETH W                  17
SAVAGE FRANK                     17
SCRIMSHAW MATTHEW                17
WAKEHAM JOHN                     17
CHRISTODOULOU DIOMEDES           16
BLAKE JR. NORMAN P               16
LOWRY CHARLES P                  16
GATHMANN WILLIAM D               16
dtype: int64

O funcionário LOCKHART EUGENE E tem 20 variáveis faltantes, como existem ao todos 21 variáveis, ele só possui uma variável lançada. Já o THE TRAVEL AGENCY IN THE PARK não parece ser realmente uma pessoa, além de possuir 18 variáveis faltantes, assim, também será excluido.

Que variável LOCKHART EUGENE E possui?

data_dict_df.loc['LOCKHART EUGENE E']
bonus                          NaN
deferral_payments              NaN
deferred_income                NaN
director_fees                  NaN
email_address                  NaN
exercised_stock_options        NaN
expenses                       NaN
from_messages                  NaN
from_poi_to_this_person        NaN
from_this_person_to_poi        NaN
loan_advances                  NaN
long_term_incentive            NaN
other                          NaN
poi                          False
restricted_stock               NaN
restricted_stock_deferred      NaN
salary                         NaN
shared_receipt_with_poi        NaN
to_messages                    NaN
total_payments                 NaN
total_stock_value              NaN
Name: LOCKHART EUGENE E, dtype: object

Somente a variável poi, portanto, LOCKHART EUGENE E, por isso será retirado da base, uma vez que não possuímos dados sobre este funcionário.

E para THE TRAVEL AGENCY IN THE PARK ?

data_dict_df.loc['THE TRAVEL AGENCY IN THE PARK']
bonus                           NaN
deferral_payments               NaN
deferred_income                 NaN
director_fees                   NaN
email_address                   NaN
exercised_stock_options         NaN
expenses                        NaN
from_messages                   NaN
from_poi_to_this_person         NaN
from_this_person_to_poi         NaN
loan_advances                   NaN
long_term_incentive             NaN
other                        362096
poi                           False
restricted_stock                NaN
restricted_stock_deferred       NaN
salary                          NaN
shared_receipt_with_poi         NaN
to_messages                     NaN
total_payments               362096
total_stock_value               NaN
Name: THE TRAVEL AGENCY IN THE PARK, dtype: object

Quantos funcionários são classificados como POI?

data_dict_df.poi.value_counts()
False    128
True      18
Name: poi, dtype: int64

18 funcionários.

Quem são eles?

data_dict_df[data_dict_df.poi==True]
bonus deferral_payments deferred_income director_fees email_address exercised_stock_options expenses from_messages from_poi_to_this_person from_this_person_to_poi ... long_term_incentive other poi restricted_stock restricted_stock_deferred salary shared_receipt_with_poi to_messages total_payments total_stock_value
HANNON KEVIN P 1500000.0 NaN -3117011.0 NaN kevin.hannon@enron.com 5538001.0 34039.0 32.0 32.0 21.0 ... 1617011.0 11350.0 True 853064.0 NaN 243293.0 1035.0 1045.0 288682.0 6391065.0
COLWELL WESLEY 1200000.0 27610.0 -144062.0 NaN wes.colwell@enron.com NaN 16514.0 40.0 240.0 11.0 ... NaN 101740.0 True 698242.0 NaN 288542.0 1132.0 1758.0 1490344.0 698242.0
RIEKER PAULA H 700000.0 214678.0 -100000.0 NaN paula.rieker@enron.com 1635238.0 33271.0 82.0 35.0 48.0 ... NaN 1950.0 True 283649.0 NaN 249201.0 1258.0 1328.0 1099100.0 1918887.0
KOPPER MICHAEL J 800000.0 NaN NaN NaN michael.kopper@enron.com NaN 118134.0 NaN NaN NaN ... 602671.0 907502.0 True 985032.0 NaN 224305.0 NaN NaN 2652612.0 985032.0
SHELBY REX 200000.0 NaN -4167.0 NaN rex.shelby@enron.com 1624396.0 22884.0 39.0 13.0 14.0 ... NaN 1573324.0 True 869220.0 NaN 211844.0 91.0 225.0 2003885.0 2493616.0
DELAINEY DAVID W 3000000.0 NaN NaN NaN david.delainey@enron.com 2291113.0 86174.0 3069.0 66.0 609.0 ... 1294981.0 1661.0 True 1323148.0 NaN 365163.0 2097.0 3093.0 4747979.0 3614261.0
LAY KENNETH L 7000000.0 202911.0 -300000.0 NaN kenneth.lay@enron.com 34348384.0 99832.0 36.0 123.0 16.0 ... 3600000.0 10359729.0 True 14761694.0 NaN 1072321.0 2411.0 4273.0 103559793.0 49110078.0
BOWEN JR RAYMOND M 1350000.0 NaN -833.0 NaN raymond.bowen@enron.com NaN 65907.0 27.0 140.0 15.0 ... 974293.0 1621.0 True 252055.0 NaN 278601.0 1593.0 1858.0 2669589.0 252055.0
BELDEN TIMOTHY N 5249999.0 2144013.0 -2334434.0 NaN tim.belden@enron.com 953136.0 17355.0 484.0 228.0 108.0 ... NaN 210698.0 True 157569.0 NaN 213999.0 5521.0 7991.0 5501630.0 1110705.0
FASTOW ANDREW S 1300000.0 NaN -1386055.0 NaN andrew.fastow@enron.com NaN 55921.0 NaN NaN NaN ... 1736055.0 277464.0 True 1794412.0 NaN 440698.0 NaN NaN 2424083.0 1794412.0
CALGER CHRISTOPHER F 1250000.0 NaN -262500.0 NaN christopher.calger@enron.com NaN 35818.0 144.0 199.0 25.0 ... 375304.0 486.0 True 126027.0 NaN 240189.0 2188.0 2598.0 1639297.0 126027.0
RICE KENNETH D 1750000.0 NaN -3504386.0 NaN ken.rice@enron.com 19794175.0 46950.0 18.0 42.0 4.0 ... 1617011.0 174839.0 True 2748364.0 NaN 420636.0 864.0 905.0 505050.0 22542539.0
SKILLING JEFFREY K 5600000.0 NaN NaN NaN jeff.skilling@enron.com 19250000.0 29336.0 108.0 88.0 30.0 ... 1920000.0 22122.0 True 6843672.0 NaN 1111258.0 2042.0 3627.0 8682716.0 26093672.0
YEAGER F SCOTT NaN NaN NaN NaN scott.yeager@enron.com 8308552.0 53947.0 NaN NaN NaN ... NaN 147950.0 True 3576206.0 NaN 158403.0 NaN NaN 360300.0 11884758.0
HIRKO JOSEPH NaN 10259.0 NaN NaN joe.hirko@enron.com 30766064.0 77978.0 NaN NaN NaN ... NaN 2856.0 True NaN NaN NaN NaN NaN 91093.0 30766064.0
KOENIG MARK E 700000.0 NaN NaN NaN mark.koenig@enron.com 671737.0 127017.0 61.0 53.0 15.0 ... 300000.0 150458.0 True 1248318.0 NaN 309946.0 2271.0 2374.0 1587421.0 1920055.0
CAUSEY RICHARD A 1000000.0 NaN -235000.0 NaN richard.causey@enron.com NaN 30674.0 49.0 58.0 12.0 ... 350000.0 307895.0 True 2502063.0 NaN 415189.0 1585.0 1892.0 1868758.0 2502063.0
GLISAN JR BEN F 600000.0 NaN NaN NaN ben.glisan@enron.com 384728.0 125978.0 16.0 52.0 6.0 ... 71023.0 200308.0 True 393818.0 NaN 274975.0 874.0 873.0 1272284.0 778546.0

18 rows × 21 columns

Dos funcionários POIs, quem mais recebeu dinheiro (total_payments)?

data_dict_df.sort_values(by='total_payments', ascending=False)[data_dict_df.poi==True]['total_payments']
LAY KENNETH L           103559793.0
SKILLING JEFFREY K        8682716.0
BELDEN TIMOTHY N          5501630.0
DELAINEY DAVID W          4747979.0
BOWEN JR RAYMOND M        2669589.0
KOPPER MICHAEL J          2652612.0
FASTOW ANDREW S           2424083.0
SHELBY REX                2003885.0
CAUSEY RICHARD A          1868758.0
CALGER CHRISTOPHER F      1639297.0
KOENIG MARK E             1587421.0
COLWELL WESLEY            1490344.0
GLISAN JR BEN F           1272284.0
RIEKER PAULA H            1099100.0
RICE KENNETH D             505050.0
YEAGER F SCOTT             360300.0
HANNON KEVIN P             288682.0
HIRKO JOSEPH                91093.0
Name: total_payments, dtype: float64

Os dois que mais receberam, LAY KENNETH L, era o Chairman Board e SKILLING JEFFREY K o CEO.
FASTOW ANDREW S, que era CFO, foi o 7º que mais recebeu.

Dos funcionários que não são POI, quem são o 5 que mais receberam dinheiro?

data_dict_df.sort_values(by='total_payments', ascending=False)[data_dict_df.poi==False]['total_payments'].head()
TOTAL               309886585.0
FREVERT MARK A       17252530.0
BHATNAGAR SANJAY     15456290.0
LAVORATO JOHN J      10425757.0
MARTIN AMANDA K       8407016.0
Name: total_payments, dtype: float64

Hum, problema!
TOTAL não parece ser um funcionário; é um totalizador, temos que exclui-lo também da base e refazer a análise descritiva.

Excluindo 'TOTAL'

data_dict.pop('TOTAL', None)
data_dict_df = data_dict_df.drop(['TOTAL'])

Como o objetivo é identificar POIs, vamos dividir a base em dois grupos POIs (POI) e não POIs (NPOI) buscando comparar os valores médios das variáveis nestes grupos de forma a identificar possíveis atributos para a máquina de aprendizagem.
A razão dos valores POI por NPOI (POI_NPOI) também será calculada.

Calculando ...

data_dict_df_POI = data_dict_df[data_dict_df.poi==True]
data_dict_df_NPOI = data_dict_df[data_dict_df.poi==False]
mean_POI = pd.DataFrame(data_dict_df_POI.mean(), columns = ['POI'])
mean_NPOI = pd.DataFrame(data_dict_df_NPOI.mean(), columns = ['NPOI'])
df_mean = pd.concat([mean_POI, mean_NPOI], axis=1)
df_mean['POI_NPOI'] = df_mean['POI']/df_mean['NPOI']
df_mean.sort_values(by = "POI_NPOI", ascending=False)
POI NPOI POI_NPOI
poi 1.000000e+00 0.000000e+00 inf
loan_advances 8.152500e+07 1.200000e+06 67.937500
exercised_stock_options 1.046379e+07 1.947752e+06 5.372240
total_payments 7.913590e+06 1.725091e+06 4.587347
total_stock_value 9.165671e+06 2.374085e+06 3.860718
restricted_stock 2.318621e+06 9.310073e+05 2.490443
deferred_income -1.035313e+06 -4.459985e+05 2.321339
bonus 2.075000e+06 9.868249e+05 2.102703
other 8.029974e+05 3.831284e+05 2.095896
long_term_incentive 1.204862e+06 6.427090e+05 1.874662
from_this_person_to_poi 6.671429e+01 3.627778e+01 1.838985
shared_receipt_with_poi 1.783000e+03 1.058528e+03 1.684415
from_poi_to_this_person 9.778571e+01 5.850000e+01 1.671551
salary 3.834449e+05 2.621515e+05 1.462684
to_messages 2.417143e+03 2.007111e+03 1.204290
expenses 5.987383e+04 5.284632e+04 1.132980
deferral_payments 5.198942e+05 8.903462e+05 0.583924
from_messages 3.003571e+02 6.687639e+02 0.449123
director_fees NaN 8.982288e+04 NaN
restricted_stock_deferred NaN 6.218928e+05 NaN

Quais são as razões POI/NPOI > 3?

df_mean[df_mean.POI_NPOI > 3].sort_values(by = "POI_NPOI", ascending=False)
POI NPOI POI_NPOI
poi 1.000000e+00 0.000000e+00 inf
loan_advances 8.152500e+07 1.200000e+06 67.937500
exercised_stock_options 1.046379e+07 1.947752e+06 5.372240
total_payments 7.913590e+06 1.725091e+06 4.587347
total_stock_value 9.165671e+06 2.374085e+06 3.860718

Quais são as razões POI/NPOI < 1.0?

df_mean[df_mean.POI_NPOI < 1].sort_values(by = "POI_NPOI", ascending=True)
POI NPOI POI_NPOI
from_messages 300.357143 668.763889 0.449123
deferral_payments 519894.200000 890346.212121 0.583924

Além das relações acima, também chamou atenção as variáveis director_fees e restricted_stock_deferred, pois nenhum POI as possui; contudo, conforme visto anteriormente, em toda a base somente 17 e 18 funcionários, respectivamente, apresentaram essas variáveis, assim, não parece ser um desvio.
A variável que mais chama atenção é a loan_advances, contudo, conforme visto acima, há apenas 4 registros desta variável; exercised_stock_options e total_payments também se destacam.
Uma outra ideia de encontrar candidatos a atributos é identificar quais variáveis possuem mais outliers, uma vez que em fraudes o que mais interessa são as exceções.

Quais as variáveis que mais possuem outliers?

Obs.: calculados conforme Fávero et al. (2009).

Q1 = data_dict_df.quantile(0.25)
Q3 = data_dict_df.quantile(0.75)
IQR = Q3 - Q1
n_outliers = ((data_dict_df < (Q1 - 1.5 * IQR)) | (data_dict_df > (Q3 + 1.5 * IQR))).sum()
n_outliers.sort_values(ascending=False)
email_address                111
total_stock_value             21
poi                           18
from_messages                 17
restricted_stock              14
from_this_person_to_poi       13
exercised_stock_options       11
from_poi_to_this_person       11
total_payments                10
bonus                         10
other                         10
salary                         9
long_term_incentive            7
to_messages                    7
deferral_payments              6
deferred_income                5
director_fees                  4
expenses                       3
restricted_stock_deferred      2
shared_receipt_with_poi        2
loan_advances                  0
dtype: int64

Desconsiderando as variáveis que não são numéricas (float64), as que mais tiveram outliers foram: total_stock_value, from_messages e restricted_stock. Assim, considerando os dois critérios adotados: relação das médias dos POI/NPOI e número de outliers, além da variável poi serão utilizadas como atributos:

Gerando a lista de atributos 1:

features_list_1 = ['poi','loan_advances', 'exercised_stock_options', 'total_payments',
                'total_stock_value', 'from_messages', 'restricted_stock' ]

2.1 Criando novas Features

Podemos pensar também em combinar os atributos (features) originais de forma a buscar melhorar o desempenho do classificador; no caso específico deste projeto, pode-se pensar:

Quem enviou mais e-mails para um POI?
Quem recebeu mais e-mals de um POI?

Seria uma hipótese de que os POIs se comunicam mais entre si, com isto, propomos duas novas features:

- from_this_person_to_poi: razão da quantidade de e-mails enviados para um POI em relação a quantidade total de e-mail enviados;
- from_poi_to_this_person: razão da quantidade de e-mails recebidos de um POI em relação a quantidade total de e-mails recebidos.            

Gerando a lista de atributos 2:

features_list_2 = ['poi','loan_advances', 'exercised_stock_options', 'total_payments',
                   'total_stock_value', 'from_messages', 'restricted_stock','from_this_person_to_poi' , 
                   'from_poi_to_this_person']

No capítulo 4 - Testando os Classificadores - utilizaremos a lista de atributos 2 de forma a verificar o impacto dos novos atributos no desempenho do classificador.

3. Removendo outliers

Vamos remover os outros 2 "funcionários" já identificados, devidos os problemas relatados no item anterior: LOCKHART EUGENE E, e THE TRAVEL AGENCY IN THE PARK.

data_dict.pop('LOCKHART EUGENE E', None)
data_dict_df = data_dict_df.drop(['LOCKHART EUGENE E'])
data_dict.pop('THE TRAVEL AGENCY IN THE PARK', None)
data_dict_df = data_dict_df.drop(['THE TRAVEL AGENCY IN THE PARK'])

Com as remoções, quantos funcionários restaram na base?

len(data_dict_df)
143
len(data_dict)
143

Ok, restaram 143, uma vez que inicialmente haviam 146 e foram excluídos 3. Vamos carregar os dados formatados para utilização na scikit-learn.

Utilizando a lista de atributos 1.

features_list = features_list_1
data = featureFormat(data_dict, features_list)
data.shape
(143, 7)

Vamos verificar as variáveis da features_list em um gráfico tipo boxplot para visualizar os outliers.

%pylab inline
import matplotlib.pyplot as plt
import seaborn as sns
Populating the interactive namespace from numpy and matplotlib
data_df = pd.DataFrame(data = data, columns=features_list)
data_df_sem_poi = data_df.drop('poi', axis=1)
sns.set()
fig, ax = plt.subplots(figsize=(15,10))
ax.set_xlabel('Features', fontsize = 20)
plt.tick_params(labelsize=15)
plt.xticks(rotation=75)
sns.boxplot(data=data_df_sem_poi)
<matplotlib.axes._subplots.AxesSubplot at 0x7fcd1d912fd0>

png

Conforme figura acima, há vários outliers em quase todas as variáveis.

Quantos outiliers há nas variáveis da feature_list?

Q1 = data_df_sem_poi.quantile(0.25)
Q3 = data_df_sem_poi.quantile(0.75)
IQR = Q3  - Q1
n_outliers = ((data_df_sem_poi < (Q1 - 1.5 * IQR)) | (data_df_sem_poi > (Q3 + 1.5 * IQR))).sum()
n_outliers.sort_values(ascending=False)
from_messages              24
total_stock_value          21
exercised_stock_options    18
restricted_stock           16
total_payments              9
loan_advances               3
dtype: int64

Estranho, na contagem de outliers, a variável from_messages foi a que obteve o maior número, contudo, o boxplot não apresenta nenhum, o que pode ter acontecido?

Vamos plotar o boxplot somente desta variável.

sns.boxplot(data=data_df_sem_poi.from_messages)
<matplotlib.axes._subplots.AxesSubplot at 0x7fcd1d8fcc90>

png

Ok, foi só um detalhe das escalas das variáveis; no caso a ordem de grandeza da variável from_messages é muito menor que das outras variáveis. Provavelmente teremos que realizar uma normalização destes dados.

Qual o valor e quem são esses outliers?

data_dict_df.loc[data_dict_df['loan_advances'].idxmax()]
bonus                                        7e+06
deferral_payments                           202911
deferred_income                            -300000
director_fees                                  NaN
email_address                kenneth.lay@enron.com
exercised_stock_options                3.43484e+07
expenses                                     99832
from_messages                                   36
from_poi_to_this_person                        123
from_this_person_to_poi                         16
loan_advances                           8.1525e+07
long_term_incentive                        3.6e+06
other                                  1.03597e+07
poi                                           True
restricted_stock                       1.47617e+07
restricted_stock_deferred                      NaN
salary                                 1.07232e+06
shared_receipt_with_poi                       2411
to_messages                                   4273
total_payments                          1.0356e+08
total_stock_value                      4.91101e+07
Name: LAY KENNETH L, dtype: object
data_dict_df.loc[data_dict_df['exercised_stock_options'].idxmax()]
bonus                                        7e+06
deferral_payments                           202911
deferred_income                            -300000
director_fees                                  NaN
email_address                kenneth.lay@enron.com
exercised_stock_options                3.43484e+07
expenses                                     99832
from_messages                                   36
from_poi_to_this_person                        123
from_this_person_to_poi                         16
loan_advances                           8.1525e+07
long_term_incentive                        3.6e+06
other                                  1.03597e+07
poi                                           True
restricted_stock                       1.47617e+07
restricted_stock_deferred                      NaN
salary                                 1.07232e+06
shared_receipt_with_poi                       2411
to_messages                                   4273
total_payments                          1.0356e+08
total_stock_value                      4.91101e+07
Name: LAY KENNETH L, dtype: object
data_dict_df.loc[data_dict_df['total_payments'].idxmax()]
bonus                                        7e+06
deferral_payments                           202911
deferred_income                            -300000
director_fees                                  NaN
email_address                kenneth.lay@enron.com
exercised_stock_options                3.43484e+07
expenses                                     99832
from_messages                                   36
from_poi_to_this_person                        123
from_this_person_to_poi                         16
loan_advances                           8.1525e+07
long_term_incentive                        3.6e+06
other                                  1.03597e+07
poi                                           True
restricted_stock                       1.47617e+07
restricted_stock_deferred                      NaN
salary                                 1.07232e+06
shared_receipt_with_poi                       2411
to_messages                                   4273
total_payments                          1.0356e+08
total_stock_value                      4.91101e+07
Name: LAY KENNETH L, dtype: object
data_dict_df.loc[data_dict_df['total_stock_value'].idxmax()]
bonus                                        7e+06
deferral_payments                           202911
deferred_income                            -300000
director_fees                                  NaN
email_address                kenneth.lay@enron.com
exercised_stock_options                3.43484e+07
expenses                                     99832
from_messages                                   36
from_poi_to_this_person                        123
from_this_person_to_poi                         16
loan_advances                           8.1525e+07
long_term_incentive                        3.6e+06
other                                  1.03597e+07
poi                                           True
restricted_stock                       1.47617e+07
restricted_stock_deferred                      NaN
salary                                 1.07232e+06
shared_receipt_with_poi                       2411
to_messages                                   4273
total_payments                          1.0356e+08
total_stock_value                      4.91101e+07
Name: LAY KENNETH L, dtype: object
data_dict_df.loc[data_dict_df['from_messages'].idxmax()]
bonus                                          400000
deferral_payments                                 NaN
deferred_income                                   NaN
director_fees                                     NaN
email_address                vince.kaminski@enron.com
exercised_stock_options                        850010
expenses                                        83585
from_messages                                   14368
from_poi_to_this_person                            41
from_this_person_to_poi                           171
loan_advances                                     NaN
long_term_incentive                            323466
other                                            4669
poi                                             False
restricted_stock                               126027
restricted_stock_deferred                         NaN
salary                                         275101
shared_receipt_with_poi                           583
to_messages                                      4607
total_payments                            1.08682e+06
total_stock_value                              976037
Name: KAMINSKI WINCENTY J, dtype: object
data_dict_df.loc[data_dict_df['restricted_stock'].idxmax()]
bonus                                        7e+06
deferral_payments                           202911
deferred_income                            -300000
director_fees                                  NaN
email_address                kenneth.lay@enron.com
exercised_stock_options                3.43484e+07
expenses                                     99832
from_messages                                   36
from_poi_to_this_person                        123
from_this_person_to_poi                         16
loan_advances                           8.1525e+07
long_term_incentive                        3.6e+06
other                                  1.03597e+07
poi                                           True
restricted_stock                       1.47617e+07
restricted_stock_deferred                      NaN
salary                                 1.07232e+06
shared_receipt_with_poi                       2411
to_messages                                   4273
total_payments                          1.0356e+08
total_stock_value                      4.91101e+07
Name: LAY KENNETH L, dtype: object

Os maiores outliers de cada variável pertencem a LAY KENNETH L, com exceção da variável from_messages na qual KAMINSKI WINCENTY J obteve o maior outlier; considerando que LAY KENNETH L era o Chairman Board da Enron, estes outliers parecem ser um freak event, decorrente de fraude, assim, não serão desconsiderados.
Vamos retirá-los somente para verificar um novo gráfico boxplot.

Qual o index da linha do dataframe com os outliers?

data_df_sem_poi.loc[data_df_sem_poi['total_payments'].idxmax()]
loan_advances               81525000.0
exercised_stock_options     34348384.0
total_payments             103559793.0
total_stock_value           49110078.0
from_messages                     36.0
restricted_stock            14761694.0
Name: 65, dtype: float64

Excluindo o index 65 e verificando o novo boxplot.

data_df_sem_poi = data_df_sem_poi.drop([65])
fig, ax = plt.subplots(figsize=(15,10))
ax.set_xlabel('Features', fontsize = 20)
plt.tick_params(labelsize=15)
sns.boxplot(data=data_df_sem_poi)
plt.tick_params(labelsize=15)
plt.xticks(rotation=75)
sns.boxplot(data=data_df_sem_poi)
<matplotlib.axes._subplots.AxesSubplot at 0x7fcd1b312d50>

png

Observando os valores do eixo y, é possível notar a diminuição em uma ordem de grandeza em relação ao boxplot anterior, comprovando a remoção do outlier.

4. Testando os Classificadores

Vamos carregar o dicionário com os dados já tratados e formatá-los conforme a biblioteca scikit-learn necessita; para isso, serão utilizados as funções featureFormat e targetFeatureSplit do programa feature_format.py, que pode ser acessado no GitHub.

Vamos testar inicialmente com a lista de atributos 1.

features_list = features_list_1
my_dataset = data_dict
data = featureFormat(my_dataset, features_list, sort_keys = True)
labels, features = targetFeatureSplit(data)

Para divisão dos conjuntos de treinamento e de teste será utilizado a biblioteca de validação cruzada do sciki-learn.
Usaremos 70% dos dados para treinamento das máquinas de aprendizagem e o restante para os testes.

Por que nós dividimos o conjunto de dados em conjuntos de treinamento e teste?

A justificativa para divisão do conjunto de dados em subconjuntos de treinamento e testes é que com isto, a máquina:

  • Nos fornece uma estimativa de desempenho em um conjunto de dados independente, uma vez que o conjunto de teste só é utilizado para validar o modelo;
  • Nos fornece uma verificação do sobreajuste (overfitting).

A ideia geral é da divisão é buscar o conjunto de treinamento que apresente o melhor resultado de aprendizagem e o conjunto de teste que resulte na melhor validação do aprendizado da máquina. Neste sentido, conforme Stephens e Diesing (2014), "[...] It is an important step to tune the parameters of the models to the training data. The aim is to find a balance between building a model that can classify the training data effectively without overfitting to the random fluctuations in the training data. Some models are more sensitive than others to the parameters chosen."

Na validação, o conjunto de teste é utilizado para verificar as características do modelo com dados que não foram utilizados na fase de treinamento.

4.1 Pré-processamento dos dados

Quando as variáveis possuem ordens de grandeza muito diferentes, que é o que foi constatado na fase de AED anterior, faz-se necessário deixá-las em uma mesma ordem. A biblioteca sciki-learn possui um pacote de pré-processamento de dados que facilitam tarefas como essa.

from sklearn.cross_validation import train_test_split
from sklearn import preprocessing

Normalizando as features:

scaler = preprocessing.MinMaxScaler()
features = scaler.fit_transform(features)

Criando os conjuntos de treinamento e testes:

features_train, features_test, labels_train, labels_test = \
    train_test_split(features, labels, test_size=0.3, random_state=42)

Agora vamos testar vários algoritmos de classificação, todos eles utilizando a biblioteca de aprendizagem de máquina scikit-learn.

4.2 Algoritmos

4.2.1 Naive Bayes (NB)

O primeiro será o Naive Bayes, do qual a documentação pode ser acessada aqui.
Vamos utilizar duas funções do programa tester.py, dump_classifier_and_data e main que irão realizar todo o trabalho de predição dos valores e calcular as métricas do algoritmo. O programa tester.py está disponível no GitHub.

# Importando a biblioteca
from sklearn.naive_bayes import GaussianNB
# Criando o classifcador
clf = GaussianNB()
dump_classifier_and_data(clf, my_dataset, features_list)
main()
GaussianNB(priors=None)
	Accuracy: 0.78393	Precision: 0.20937	Recall: 0.22350	F1: 0.21620	F2: 0.22052
	Total predictions: 15000	True positives:  447	False positives: 1688	False negatives: 1553	True negatives: 11312

4.2.2 Support Vector Machine (SVM)

Agora testaremos a SVM, a documentação pode ser acessada aqui.

from sklearn.svm import SVC
clf=SVC()
dump_classifier_and_data(clf, my_dataset, features_list)
main()
Got a divide by zero when trying out: SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape=None, degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)
Precision or recall may be undefined due to a lack of true positive predicitons.

4.2.3 Decision Tree (DT)

Agora testaremos a Árvore de Decisão, a documentação pode ser acessada aqui.

from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(criterion='entropy',min_samples_split=50)
dump_classifier_and_data(clf, my_dataset, features_list)
main()
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=50, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best')
	Accuracy: 0.86553	Precision: 0.47658	Recall: 0.08650	F1: 0.14642	F2: 0.10343
	Total predictions: 15000	True positives:  173	False positives:  190	False negatives: 1827	True negatives: 12810

4.2.4 Boosted Decision Trees (BDT)

Agora testaremos o Adaboost aplicado a árvores de decisão, também conhecido como BDT, cuja documentação pode ser acessada aqui.

from sklearn.ensemble import AdaBoostClassifier
clf = clf.fit(features_train, labels_train)
dump_classifier_and_data(clf, my_dataset, features_list)
main()
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=50, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best')
	Accuracy: 0.86593	Precision: 0.48509	Recall: 0.08950	F1: 0.15112	F2: 0.10694
	Total predictions: 15000	True positives:  179	False positives:  190	False negatives: 1821	True negatives: 12810

4.2.5 K-means

Todos os outros algoritmos utilizados até aqui se enquadram em uma categoria denominada Aprendizagem Supervisionada, agora utilizaremos o algoritmo K-means, que é do tipo Aprendizagem não-supervisionado. A documentação do K-means pode ser acessada aqui.

from sklearn.cluster import KMeans
clf = KMeans(n_clusters=2, random_state=0).fit(features_train)
dump_classifier_and_data(clf, my_dataset, features_list)
main()
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=2, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=0, tol=0.0001, verbose=0)
	Accuracy: 0.83920	Precision: 0.23791	Recall: 0.09350	F1: 0.13424	F2: 0.10642
	Total predictions: 15000	True positives:  187	False positives:  599	False negatives: 1813	True negatives: 12401

Analisando as saídas acima, temos que o melhor resultado, com relação a métrica accuracy foi obtida pelos algoritmos Boosted Decision Trees (BDT) e Decision Trees (DT); vamos calibrar (tuning) o DT.

  • Resumo dos resultados

      NB      Accuracy: 0.78393	Precision: 0.20937	Recall: 0.22350	F1: 0.21620	F2: 0.22052	            
      SVM     Accuracy: -------    Precision: -------	Recall: -------	F1: -------	F2: -------	 
      DT      Accuracy: 0.86553	Precision: 0.47658	Recall: 0.08650	F1: 0.14642	F2: 0.10343
      BDT     Accuracy: 0.86593	Precision: 0.48509	Recall: 0.08950	F1: 0.15112	F2: 0.10694
      K-means Accuracy: 0.83920	Precision: 0.23791	Recall: 0.09350	F1: 0.13424	F2: 0.10642
    

4.3 Testando o Desempenho com os Novos Atributos Criados

Verificaremos agora o impacto no algoritmo DT com a Lista de Atributos 2, que contém os 2 novos atributos criados.

Criando os novos Atributos:

data_dict_df_2 = data_dict_df
data_dict_df_2['from_this_person_to_poi_ratio'] = \
                                        data_dict_df_2['from_this_person_to_poi']/data_dict_df_2['from_messages']
data_dict_df_2['from_poi_to_this_person_ratio'] = \
                                        data_dict_df_2['from_poi_to_this_person']/data_dict_df_2['from_messages']
filled_df = data_dict_df_2.fillna(value='NaN')
data_dict_2 = filled_df.to_dict(orient='index')
data_dict_df_2.shape
(143, 23)

Agora a base ficou com 23 atributos.

my_dataset_2 = data_dict_2
data_2 = featureFormat(my_dataset_2, features_list_2, sort_keys = True)
data_2[np.isnan(data_2)]=0
labels, features = targetFeatureSplit(data_2)

scaler = preprocessing.MinMaxScaler()
features = scaler.fit_transform(features)

features_train, features_test, labels_train, labels_test = \
    train_test_split(features, labels, test_size=0.3, random_state=42)

from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(criterion='entropy',min_samples_split=50)
dump_classifier_and_data(clf, my_dataset_2, features_list_2)
main()
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=50, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best')
	Accuracy: 0.86413	Precision: 0.42910	Recall: 0.05750	F1: 0.10141	F2: 0.06955
	Total predictions: 15000	True positives:  115	False positives:  153	False negatives: 1885	True negatives: 12847

Comparando os resultados da máquina DT sem e com os atributos criados, observa-se piora em todas as métricas; assim, esses atributos não serão incluídas na sintonia do algoritmo.

  • Resumo dos resultados
    DT sem atributos criados -> Accuracy: 0.86553 Precision: 0.47658 Recall: 0.08650 F1: 0.14642 F2: 0.10343
    DT com atributos criados -> Accuracy: 0.86413 Precision: 0.42910 Recall: 0.05750 F1: 0.10141 F2: 0.06955

5. Calibrando a Máquina Decision Trees (tuning)

A calibração (tuning) consiste na otimização da máquina de aprendizagem, na qual são testados várias parametrizações; seu objetivo é que o algoritmo apresente os melhores resultados (métricas) possíveis.

Conforme Birattari (2009), um excesso de ajustes pode provocar um problema semelhante ao (overfitting), conhecido como over-tuning.

Para realizar a calibração (afinação de parâmetros) utilizamos a biblioteca GridSearchCV do sciki-learn, que é uma forma de analisar sistematicamente múltiplas combinações de parâmetros, fazendo validação cruzada ao longo do processo, para determinar qual calibragem (parametrização) apresenta o melhor desempenho.

Calibrando o modelo ...

from sklearn.model_selection import GridSearchCV
parameters = {'criterion':('gini', 'entropy'),
              'max_depth': range(1,5),
              'max_features': range(1,5),
              'class_weight':(None, 'balanced'),
              'random_state': [42],
              'min_samples_split': range(3,5)
             }
cdt = DecisionTreeClassifier()
clf = GridSearchCV(cdt, parameters)
dump_classifier_and_data(clf, my_dataset, features_list)
main()
GridSearchCV(cv=None, error_score='raise',
       estimator=DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=2, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best'),
       fit_params={}, iid=True, n_jobs=1,
       param_grid={'max_features': [1, 2, 3, 4], 'random_state': [42], 'criterion': ('gini', 'entropy'), 'min_samples_split': [3, 4], 'max_depth': [1, 2, 3, 4], 'class_weight': (None, 'balanced')},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=None, verbose=0)
	Accuracy: 0.86160	Precision: 0.39205	Recall: 0.06900	F1: 0.11735	F2: 0.08261
	Total predictions: 15000	True positives:  138	False positives:  214	False negatives: 1862	True negatives: 12786

Abaixo seguem os resultados antes e depois da calibração do algoritmo DT, no qual se observa piora em todas as métricas.

Accuracy: 0.86553 Precision: 0.47658 Recall: 0.08650 F1: 0.14642 F2: 0.10343
Accuracy: 0.86160 Precision: 0.39205 Recall: 0.06900 F1: 0.11735 F2: 0.08261

Como o objetivo é que os valores das métricas Precision e Recall sejam de pelo menos 0.3, até aqui isso não foi conseguido. Assim, temos que tentar melhorar a máquina de aprendizagem. A biblioteca SciKit possui mais ferramentas que podem nos auxiliar, vamos utilizá-las.

5.1 Reavaliando as features

Agora ao invés de usarmos tão somente nossa intuição para definir as features de entrada, vamos utilizar a biblioteca Feature selection do sciki-learn, mais especificamente a classe SelectKBest que por meio do teste da Análise de Variância ANOVA- ANAlysis of VAriance determina quais as features mais importantes.

from get_Best_features import get_best
best_features = get_best(data_dict)
best_features
feature score
4 exercised_stock_options 24.815080
9 total_stock_value 24.182899
5 bonus 20.792252
0 salary 18.289684
16 deferred_income 11.458477
17 long_term_incentive 9.922186
6 restricted_stock 9.212811
3 total_payments 8.772778
7 shared_receipt_with_poi 8.589421
11 loan_advances 7.184056
10 expenses 6.094173
18 from_poi_to_this_person 5.243450
13 other 4.187478
14 from_this_person_to_poi 2.382612
15 director_fees 2.126328
1 to_messages 1.646341
2 deferral_payments 0.224611
12 from_messages 0.169701
8 restricted_stock_deferred 0.065500

De forma a permitir comparação com as features anteriores, adotaremos a mesma quantidade de features (sete), selecionado as que possuírem maior score

features_list = ['poi','exercised_stock_options', 'total_stock_value', 'bonus',
                 'salary', 'deferred_income', 'long_term_incentive']
my_dataset = data_dict
data = featureFormat(my_dataset, features_list, sort_keys = True)
labels, features = targetFeatureSplit(data)
#Normalizando as features:
scaler = preprocessing.MinMaxScaler()
features = scaler.fit_transform(features)
from sklearn import tree
from sklearn.model_selection import GridSearchCV
parameters = {'criterion':('gini', 'entropy'),
              'max_depth': range(1,5),
              'max_features': range(1,5),
              'class_weight':(None, 'balanced'),
              'random_state': [42],
              'min_samples_split': range(3,5),
             }
cdt = DecisionTreeClassifier()
clf = GridSearchCV(cdt, parameters)
dump_classifier_and_data(clf, my_dataset, features_list)
main()
GridSearchCV(cv=None, error_score='raise',
       estimator=DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_split=1e-07, min_samples_leaf=1,
            min_samples_split=2, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best'),
       fit_params={}, iid=True, n_jobs=1,
       param_grid={'max_features': [1, 2, 3, 4], 'random_state': [42], 'criterion': ('gini', 'entropy'), 'min_samples_split': [3, 4], 'max_depth': [1, 2, 3, 4], 'class_weight': (None, 'balanced')},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=None, verbose=0)
	Accuracy: 0.84429	Precision: 0.35531	Recall: 0.11050	F1: 0.16857	F2: 0.12816
	Total predictions: 14000	True positives:  221	False positives:  401	False negatives: 1779	True negatives: 11599

Conforme resultado, essa máquina ainda não atende o requisito de Precision e Recall mínimo de 0.3.

5.2 Combinando tudo (Pipeline)

Vamos utilizar agora outra funcionalidade da biblioteca SciKit que é o pipeline, que possibilita combinar múltiplos métodos sucessivamente, combinaremos:

a) Normalização dos dados, utilizando StandardScaler;
b) Seleção das Features mais importantes, utilizando SelectKBest;
c) Redução de dimensionalidade dos dados, utilizando PCA;
d) Otimização, utilizando GridSearchCV; e
e) Validação cruzada, utilizando StratifiedShuffleSplit.

from sklearn.pipeline import Pipeline
from sklearn import preprocessing
from sklearn.decomposition import PCA
from sklearn.model_selection import GridSearchCV, StratifiedShuffleSplit
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.feature_selection import SelectKBest

Carregando a lista com todos os atributos ...

features_list = ['poi',
                 'exercised_stock_options',
                 'total_stock_value',
                 'bonus',
                 'salary',
                 'deferred_income',
                 'long_term_incentive',
                 'restricted_stock',
                 'total_payments',
                 'shared_receipt_with_poi',
                 'loan_advances',
                 'expenses',
                 'from_poi_to_this_person',
                 'other',
                 'from_this_person_to_poi',
                 'director_fees',
                 'to_messages',
                 'deferral_payments',
                 'from_messages',
                 'restricted_stock_deferred'
                ]
my_dataset = data_dict
data = featureFormat(my_dataset, features_list, sort_keys = True)
labels, features = targetFeatureSplit(data)
features_train, features_test, labels_train, labels_test = \
    train_test_split(features, labels, test_size=0.3, random_state=42)

Parametrização que será testada ...

CRITERION = ['gini','entropy']
SPLITTER = ['best', 'random']
MIN_SAMPLES_SPLIT = [2,4,8,16]
CLASS_WEIGHT = ['balanced', None]
MIN_SAMPLES_LEAF = [1,2,4,8,16]
MAX_DEPTH = [None,1,2,4,8,16]
SCALER = [None, preprocessing.StandardScaler()]
SELECTOR__K = [10, 13, 15, 18, 'all']
REDUCER__N_COMPONENTS = [2, 4, 6, 8, 10]
pipe = Pipeline([
        ('scaler', preprocessing.StandardScaler()),
        ('selector', SelectKBest()),
        ('reducer', PCA(random_state=42)),
        ('classifier', DecisionTreeClassifier())
    ])
param_grid = {
    'scaler': SCALER,
    'selector__k': SELECTOR__K,
    'reducer__n_components': REDUCER__N_COMPONENTS,
    'classifier__criterion': CRITERION,
    'classifier__splitter': SPLITTER,
    'classifier__min_samples_split': MIN_SAMPLES_SPLIT,
    'classifier__class_weight': CLASS_WEIGHT,
    'classifier__min_samples_leaf': MIN_SAMPLES_LEAF,
    'classifier__max_depth': MAX_DEPTH,
}
sss = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
grid_search = GridSearchCV(pipe, param_grid, scoring='f1', cv=sss)
grid = grid_search.fit(features_train,labels_train)
best_clf = grid_search.best_estimator_
best_clf 
Pipeline(steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('selector', SelectKBest(k=18, score_func=<function f_classif at 0x7fcd1a2087d0>)), ('reducer', PCA(copy=True, iterated_power='auto', n_components=2, random_state=42,
  svd_solver='auto', tol=0.0, whiten=False)), ('classifi...it=2, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best'))])
from sklearn.externals import joblib
joblib.dump(clf, 'dt_clf.pkl') 
joblib.dump(best_clf, 'best_clf.pkl')
dump_classifier_and_data(best_clf, my_dataset, features_list)
grid.best_score_
0.565959595959596
grid.score(features_test, labels_test)
0.34782608695652178
main()
Pipeline(steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('selector', SelectKBest(k=18, score_func=<function f_classif at 0x7fcd1a2087d0>)), ('reducer', PCA(copy=True, iterated_power='auto', n_components=2, random_state=42,
  svd_solver='auto', tol=0.0, whiten=False)), ('classifi...it=2, min_weight_fraction_leaf=0.0,
            presort=False, random_state=None, splitter='best'))])
	Accuracy: 0.67947	Precision: 0.28591	Recall: 0.93750	F1: 0.43819	F2: 0.64398
	Total predictions: 15000	True positives: 1875	False positives: 4683	False negatives:  125	True negatives: 8317

Com isto, atendemos ao requisito dos valores mínimos para precision e recall, arrendondano para uma casa decimal.

Accuracy: 0.67947 Precision: 0.28591 Recall: 0.93750 F1: 0.43819 F2: 0.64398

  • Precision = 0.3
  • Recall = 0.9

6. Interpretação e Discussão dos Resultados

Lembrando que o objetivo deste projeto é determinar se um funcionário da Enron é ou não um POI (Persons Of Interest), ou seja, estamos buscando a performance para uma classe específicas, os POIs. Em casos deste tipo, as métricas mais significativas são a recall e a precision.

  • Recall: de todos os casos que deveriam ser rotulados como positivos, isto é POIs, quantos foram corretamente classificados?
    A equação para isto é:
    $$ Recall = {{True Positives\over { True Positives + False Negatives} }} $$

  • Precision: de todos os itens que foram rotulados como positivos, quantos foram corretamente classificados?
    A equação para isto é:
    $$ Precison = {{True Positives\over { True Positives + False Positives} }} $$

  • A métrica F1, utilizada como parâmetro em GridSearchCV relaciona as métricas recall e precision, na qual ela é a média harmônica destas duas, sendo sua expressão matemática.

$$ F1 = 2*{{Precision * Recall}\over { Precision + Recall} } $$

No contexto deste projeto, um Falso Positivo é um inocente que será investigado; considerando uma investigação justa, ele , se inocente, poderá comprovar isto sem problemas no processo judicial. Já um Falso Negativo é um POI que não será investigado, isto é, um provável criminoso que ficará impune, portanto, algo indesejável. Com isto, analisando as equações acima, a métrica recall alta é o mais desejável neste caso. Sendo assim, a interpretação dos valores obtidos com a máquina de aprendizado proposta significam que:

  • 90% (recall = 0.9) das vezes que um funcionário, que de fato é um POI, é submetido ao classificador, a máquina o prediz como tal, ou seja, um POI. Em outras palavras, há 10% de Falsos Negativo, também conhecido como Erro Tipo II;
  • 30% (precision = 0.3) dos funcionários classificados como POI são realmente pessoas suspeitas de participarem da fraude, em outras palavras, 70% de Falsos Positivos, também conhecido como Erro Tipo I.

Portanto, se a máquina proposta fosse utilizada como tomadora de decisão em processar ou não um funcionário da Enron, teríamos que 10% dos culpados sairiam ilesos e 70% dos processados seriam inocentes.

Referências

Comparação de algoritmos de aprendizagem de máquina para construção de modelos preditivos de diabetes não diagnosticado

A Comparison of Supervised Classification Methods for the Prediction of Substrate Type Using Multibeam Acoustic and Legacy Grain-Size Data

Ajuste de Parâmetros em Algoritmos de Aprendizado de Máaquina Utilizando Transferência de Aprendizado

API design for machine learning software: experiences from the scikit-learn project

Distributed Tuning of Machine Learning Algorithms using MapReduce Clusters

Python Data Science Class

SciKit-learn

Sebastian Raschka

Tuning Metaheuristics: A Machine Learning Perspective

Nicholas Hoh

About

Projeto: Machine Learning

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published