Sunteți pe pagina 1din 81

M INERAÇÃO DE DADOS EM P YTHON

ICMC-USP

Victor Alexandre Padilha


André Carlos Ponce de Leon Ferreira de Carvalho
Instituto de Ciências Matemáticas e de Computação
Universidade de São Paulo

9 de agosto de 2017
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Capítulo

1
Instalação do Python e bibliotecas

A linguagem de programação Python possui bibliotecas que ajudam a escrever


códigos voltados para aplicações em diferentes áreas de conhecimento. Uma dessas áreas
é a mineração de dados. Esta apostila tem por objetivo ilustrar como bibliotecas da lin-
guagem Python podem ser utilizadas para a realização de experimentos de mineração de
dados.
Todos os exemplos a serem apresentados neste material utilizarão a linguagem
de programação Python na versão 3 e algumas de suas principais bibliotecas desenvolvi-
das para dar suporte a análise de dados, aprendizado de máquina e mineração de dados.
Portanto, esta primeira parte apresenta um breve tutorial sobre como instalar essas ferra-
mentas em diferentes sistemas operacionais, Linux e Windows.

1.1. Instalação em Linux


Diversas distribuições atuais do sistema operacional Linux disponibilizam, como parte de
seu conjunto padrão de pacotes, um ambiente para programação na linguagem Python 3.
Para conferir, basta digitar os seguintes comandos no terminal:

$ which python3

$ which pip3

os quais devem retornar algo como /usr/bin/python3 e /usr/bin/pip3, respec-


tivamente. Caso algum erro seja informado, deverão ser instalados os pacotes apropriados
para que seja possível utilizar a linguagem. Para isso, será utilizado o gerenciador de pa-
cotes disponível na distribuição usada. Os dois gerenciadores mais comumente utilizados
são o apt-get (Debian, Ubuntu e derivados) e o yum (RedHat, CentOS e derivados). Por-
tanto, para a instalação em sistemas derivados deles, basta digitar algum dos seguintes
comandos no terminal:

$ sudo apt-get install python3 python3-dev python3-pip

ou

2
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

$ sudo yum install python3 python3-dev python3-pip

Após digitar esses comandos, toda vez que você quiser executar algum script,
basta digitar o comando python3 script.py.
Adicionalmente, nos exemplos apresentados neste material e para os trabalhos
aplicados distribuídos no decorrer da disciplina, serão necessárias quatro bibliotecas am-
plamente utilizadas para relizar experimentos de mineração e ciência de dados:

• NumPy1 ,
• SciPy2
• scikit-learn3
• matplotlib4

Para a instalação dessas bibliotecas, basta digitar no terminal o comando a seguir:

$ pip3 install numpy scipy sklearn matplotlib pandas --user

1.2. Instalação em Windows


Como primeiro passo, a versão mais atualizada e estável do Python 3 deve ser baixada
em https://www.python.org/downloads/windows/. Para a instalação do
gerenciador de pacotes pip, o script get-pip.py5 deve ser baixado e executado no termi-
nal do Windows como python3 get-pip.py. Finalmente, utilizaremos o seguinte
comando para a instalação das bibliotecas NumPy, SciPy, scikit-learn e matplotlib6 :

$ pip3 install numpy scipy sklearn matplotlib pandas

1.3. Referências e tutoriais


Esta primeira parte deste material teve como objetivo descrever os passos necessários
para a instalação das ferramentas que serão necessárias no decorrer da disciplina de Mi-
neração de Dados Biológicos. Nos próximos capítulos, diversos exemplos de códigos
serão apresentados para a exploração de conjunto de dados, visualização de dados, pré-
processamento de bases de dados, construção de modelos preditivos, construção de mode-
los descritivos, além de outros temas que serão cobertos na disciplina. Não será necessário
qualquer conhecimento prévio acerca das bibliotecas citadas nas seções anteriores. Entre-
tanto, alguns bons tutoriais introdutórios estão disponíveis nos websites de cada biblioteca
para que o leitor possa tanto consultar suas funcionalidades como aprender a usar melhor
os seus recursos.
1 http://www.numpy.org/
2 http://www.scipy.org/
3 http://http://scikit-learn.org/stable/
4 http://matplotlib.org/
5 https://bootstrap.pypa.io/get-pip.py
6 Para
o ambiente Windows, se os comandos python3 e pip3 não funcionarem, deve-se testar com os
comandos python e pip, respectivamente.

3
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Capítulo

2
Iniciando o estudo e exploração de dados

Esta parte do material vai ilustrar como a linguagem Python pode ser utilizada
para uma análise exploratória de um conjunto de dados, de forma a encontrar padrões
nos dados e extrair conhecimento desses dados. Existem duas maneiras de explorar um
conjunto de dados: por meio de técnicas estatísticas, mais especificamente da estatística
descritiva, e de técnicas de visualização.
Nesta parte do material, serão apresentadas algumas das principais medidas esta-
tísticas utilizadas para descrever um conjunto de dados, bem como suas funções corres-
pondentes nas bibliotecas da linguagem Python consideradas, além de técnicas simples
capazes de ilustrar graficamente padrões presentes em um conjunto de dados.
Inicialmente, na Seção 2.1, será apresentada uma breve introdução às operações
matemáticas básicas para a manipulação de dados com a biblioteca NumPy. Na Seção
2.2, será discutido como as bases de dados consideradas no presente material estão nor-
malmente estruturadas, e serão apresentadas algumas medidas estatísticas para análise
exploratória de dados. Na Seção 2.3, será discutida a ocorrência de dados multivariados,
que condizem com os cenários estudados em mineração de dados. Na Seção 2.4, serão
demonstrados alguns exemplos simples de gráficos que podem auxiliar em uma análise
inicial dos dados. Por fim, na Seção 2.5, serão propostos alguns exercicios com a finali-
dade de praticar os conceitos apresentados.

2.1. Introdução à NumPy


NumPy é um pacote da linguagem Python que foi desenvolvido para computação cientí-
fica, área que utiliza computação para resolver problemas complexos. Esse pacote, que
será muito utilizado para as análises e experimentos destas notas, possui várias funções
que permitem manipular e descrever dados. Este capítulo irá inicialmente mostrar como
funções desse pacote podem ser aplicadas a arrays, que consistem no tipo básico definido
pelo pacote.

2.1.1. Arrays
O principal tipo de dados disponibilizado pela biblioteca NumPy e que será mais utilizado
no decorrer deste material é conhecido como numpy.array. Um array pode ser ins-
tanciado por meio da chamada numpy.array(lista), na qual lista é um objeto
do tipo list contendo apenas valores numéricos. Por exemplo:

3
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

import numpy
lista = [1, 2, 3, 4, 5]
x = numpy.array(lista)
print(x)

O tipo array fornece facilidades para a realização de operações matemáticas com


escalares. Para fins de ilustração, execute o código abaixo no terminal do Python:

import numpy

x = numpy.array([1, 2, 3, 4, 5])
y = 2

print(x + y)

print(x - y)
print(y - x)

print(x * y)

print(x / y)
print(y / x)

Ao inspecionar os resultados do código anterior, é possível observar que nas operações


de soma, subtração, multiplicação e divisão em que um dos operandos é um escalar e o
outro é um array, cada operação ocorre entre o escalar e cada elemento do array.
A NumPy também possui funções para realizar operações utilizando dois arrays
de mesmo tamanho1 . Como exemplo, execute o código abaixo no terminal do Python:

import numpy

x = numpy.array([1, 2, 3, 4, 5])
y = numpy.array([6, 7, 8, 9, 10])

print(x + y)

print(x - y)
print(y - x)

print(x * y)

1 Na verdade, diversas operações podem ser feitas também com arrays de tamanhos e dimensões distin-
tas. Entretanto, o comportamento da NumPy em tais cenários pode não ser tão intuitivo. Para o leitor inte-
ressado neste tópico, recomenda-se a leitura de: https://scipy.github.io/old-wiki/pages/
EricsBroadcastingDoc.

4
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

print(x / y)
print(y / x)

Ao analisar as saídas do código anterior, pode-se perceber que, quando os operandos são
dois arrays do mesmo tamanho, as operações de soma, subtração, multiplicação e divi-
são ocorrem elemento a elemento. Embora os arrays utilizados nos exemplos anteriores
tenham apenas uma dimensão, as operações podem ser aplicadas a arrays e matrizes com
qualquer número de dimensões.
Por fim, a NumPy fornece ainda várias funções pré-definidas para explorar diver-
sas propriedades de um array qualquer x. Dentre as funções que serão utilizadas ao longo
deste material, pode-se mencionar:

• numpy.sum(x): retorna a soma de todos os elementos de x.

• numpy.max(x): retorna o valor máximo contido em x.

• numpy.min(x): retorna o valor mínimo contido em x.

2.2. Exploração de dados


Tipicamente, para boa parte dos problemas em que técnicas mineração de dados e apren-
dizado de máquina são aplicadas, o conjunto de dados coletados, também conhecido como
base de dados, será representado por meio de uma matriz ou tabela, denotada por X n×m ,
em que n indica o número de objetos observados e m indica o número de características
(ou atributos) que foram coletadas para cada objeto.
Como exemplo a ser utilizado no decorrer deste capítulo, considere a base de
dados Iris, frequentemente utilizada em livros de mineração de dados e de aprendizado
de máquina, originalmente proposta em [Fisher 1936]. Nesta base de dados constam as
informações de 150 exemplos observados de plantas provenientes de três diferentes espé-
cies do gênero Iris: Iris setosa, Iris versicolor e Iris virginica. As características coletadas
para cada exemplo foram: largura da sépala (cm), comprimento da sépala (cm), largura
da pétala (cm) e comprimento da pétala (cm). A Tabela 2.1 apresenta duas observações
(exemplos) de cada espécie (classe).

Tabela 2.1: Dois exemplos de cada classe da base de dados Iris.


Largura sépala Comprimento sépala Largura pétala Comprimento pétala Espécie (classe)
5,1 3,5 1,4 0,2 Iris setosa
4,9 3,0 1,4 0,2 Iris setosa
7,0 3,2 4,7 1,4 Iris versicolor
6,4 3,2 4,5 1,5 Iris versicolor
6,3 3,3 6,0 2,5 Iris virginica
5,8 2,7 5,1 1,9 Iris virginica

A seguir, serão discutidas algumas das principais medidas estatísticas que podem
ser utilizadas para uma exploração inicial dos dados com a finalidade de extrair informa-
ções relevantes de uma base de dados tal como a Iris.

5
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

2.2.1. Dados univariados


Dados univariados são valores de apenas uma variável. No caso de Mineração de Dados
(MD), seriam os valores de um único atributo.

2.2.1.1. Medidas de posição ou localidade

2.2.1.1.1 Média

Considerando a existência de n observações diferentes para uma variável qualquer x, a


média é definida como:

1 n
x̄ = · ∑ xi (1)
n i=1

Na biblioteca NumPy, assumindo que um conjunto de n observações seja repre-


sentado por um array de tamanho n, a média pode ser calculada conforme o exemplo a
seguir:

import numpy

# gerando um array com 100 valores aleatorios


# sorteados entre 0 e 1000
x = numpy.random.randint(low=0, high=100, size=100)

# calculando a media utilizando os metodos


# numpy.sum e len
media = numpy.sum(x) / len(x)

print(media)

# mais facilmente, podemos utilizar o metodo numpy.mean


media = numpy.mean(x)

print(media)

2.2.1.1.2 Mediana

Considerando a existência de n observações para uma variável qualquer x, a mediana pode


ser definida como o valor central das observações ordenadas quando n for ímpar. Para os
casos em que n for par, a mediana é definida como valor médio entre as duas observações
centrais ordenadas [Faceli et al. 2011]. Em Python, a mediana pode ser calculada tal
como apresentado no exemplo abaixo:

mediana = numpy.median(x)
print(mediana)

6
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

2.2.1.1.3 Percentis

Um percentil ou quantil, denotado por py% , representa, dentre m observações, o va-


lor tal que y% das observações são menores do que ele [Magalhães e de Lima 2000,
Faceli et al. 2011]. Esta medida trata-se de uma generalização da mediana (que corres-
ponde a p50% ) e do primeiro e terceiro quartis (que correspondem a p25% e p75% , respec-
tivamente). Para o seu cálculo, a biblioteca NumPy dispõe do seguinte método:

# para calcular p15%


percentil_15 = numpy.percentile(x, 15)
print(percentil_15)

2.2.1.2. Medidas de dispersão ou espalhamento

2.2.1.2.1 Intervalo ou amplitude

O intervalo (também conhecido como amplitude) consiste na diferença entre o valor má-
ximo e mínimo de um conjunto de observações. Em Python, o mesmo pode ser calculado
como:

valor_maximo = numpy.max(x)
valor_minimo = numpy.min(x)
intervalo = valor_maximo - valor_minimo
print(intervalo)

2.2.1.2.2 Variância e desvio-padrão

A variância de um conjunto de observações, denotada por s2 , é definida como:


n
1
s2 = · ∑ (xi − x̄)2 (2)
n − 1 i=1

em que x̄ consiste na média das observações (Equação 1). Para manter a mesma unidade
dos dados originais, o desvio-padrão é definido como [Magalhães e de Lima 2000]:

s = s2 . (3)

O cálculo dessas duas medidas pode ser implementado como:

media = numpy.mean(x)
n = len(x)
diferencas = x - media
variancia = numpy.sum(diferencas * diferencas) / (n - 1)
desvio_padrao = numpy.sqrt(variancia)

# de maneira mais facil, podemos utilizar as

7
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# seguintes funcoes
variancia = numpy.var(x, ddof=1)
desvio_padrao = numpy.std(x, ddof=1)

em que o parâmetro ddof indica o número de graus de liberdade utilizados. Para o


cálculo na Equação (2), as funções numpy.var e numpy.std utilizam como divisor
a fórmula n − ddof. Em todos os cenários estudados neste material será utilizada a
variância amostral e o desvio-padrão amostral. Portanto, o valor considerado para ddof
sempre será 12 .

2.2.1.3. Medidas de distribuição

2.2.1.3.1 Obliquidade

A obliquidade (ou skewness, em inglês) trata-se de uma medida de assimetria de uma


distribuição de probabilidade em torno de sua média. Ela pode assumir valores negativos,
positivos ou próximos de 0. No primeiro caso, a cauda da distribuição é mais alongada
à esquerda e, por consequência, a distribuição dos dados concentra-se mais à direita no
seu respectivo gráfico. No segundo caso, a cauda da distribuição é mais alongada para
a esquerda, o que aponta uma maior concentração dos dados à direita do seu respectivo
gráfico. Por fim, no terceiro caso, a distribuição possui caudas aproximadamente balan-
ceadas e, como resultado, ela terá uma maior simetria. Na Figura 2.1 são apresentados
exemplos para as duas primeiras situações, quando a variável assume valores contínuos.

Figura 2.1: Ilustração de obliquidade negativa e positiva (Fonte: [Hermans 2008]3 ).

O cálculo da obliquidade é definido por:


∑ni=1 (xi − x̄)3
obliquidade = (4)
(n − 1) · s3
sendo x̄ e s definidos tal como nas equações (1) e (3), nessa ordem.
Em Python, a obliquidade dos dados contidos em um array pode ser calculada por
meio da biblioteca SciPy da seguinte maneira:
2 https://en.wikipedia.org/wiki/Degrees_of_freedom_(statistics)
3Aimagem original está disponível sob a licença CC-BY-SA 3.0 (https://creativecommons.
org/licenses/by-sa/3.0/).

8
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

import numpy
import scipy.stats

# gerando uma amostra com 10000 observacoes a partir de


# uma distribuicao normal com media zero e desvio-padrao
# unitario
dados = numpy.random.normal(loc=0.0, scale=1.0, size=10000)

# como os dados foram gerados segundo uma distribuicao


# normal, que eh simetrica, a obliquidade devera resultar
# em algum valor proximo de 0
obliquidade = scipy.stats.skew(dados)
print(obliquidade)

2.2.1.3.2 Curtose

A curtose é uma medida que caracteriza o achatamento da distribuição dos dados. Assim
como a obliquidade, os seus valores podem ser negativos, positivos ou próximos de 0.
No primeiro caso, a distribuição é mais achatada e apresenta picos mais baixos e caudas
mais leves4 quando comparada à distribuição normal. No segundo caso, a distribuição dos
dados apresenta picos mais elevados e caudas mais pesadas5 ao se comparar à distribuição
normal. Por fim, no último caso, a distribuição dos dados apresenta achatamento e caudas
próximas ao que ocorre com a distribuição normal.
A equação para o cálculo da curtose é definida como:

∑ni=1 (xi − x̄)4


curtose = (5)
(n − 1) · s4

sendo x̄ e s definidos, respectivamente, pelas Equações (1) e (3).


Tal como a obliquidade, a curtose pode ser calculada por meio da biblioteca SciPy:

import numpy
import scipy.stats

# gerando uma amostra com 10000 observacoes a partir de


# uma distribuicao normal com media zero e desvio-padrao
# unitario
dados = numpy.random.normal(loc=0.0, scale=1.0, size=10000)

# como os dados foram gerados segundo uma distribuicao


# normal, que eh simetrica, a curtose devera resultar
# em algum valor proximo de 0
4 Isto é, valores extremos (outliers) tendem a ser pouco frequentes. Como um exemplo de extrema
curtose, pode-se citar a distribuição uniforme, a qual não apresenta a ocorrência de outliers.
5 Ou seja, outliers tendem a ser mais frequentes.

9
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

curtose = scipy.stats.kurtosis(dados)
print(curtose)

2.3. Dados multivariados


Dados multivariados podem ser definidos como conjuntos de dados em que cada obser-
vação consiste em um vetor de características e não apenas um único valor, como nos
exemplos apresentados na seção anterior. No caso de MD, cada elemento do vetor corres-
ponde a um atributo do conjunto de dados. Em outras palavras, cada observação i pode
ser definida como um vetor xi = [xi1 xi2 · · · xim ]T , em que m indica o número de
características coletadas para cada observação. Portanto, um conjunto de observações.
 
x1T
xT 
 2
X =  ..  (6)
.
xnT

corresponderá a um conjunto de dados X n×m (Seção 2.2).


Adotando essa definição, todos os conceitos discutidos na seção anterior podem
ser redefinidos. Desse modo, cada medida de posição ou dispersão pode ser reformulada
para calcular como resultado um vetor de comprimento m em que cada posição corres-
ponde ao valor de tal medida para cada atributo [Faceli et al. 2011].

2.3.1. Exemplo: Iris


Nesta seção será brevemente apresentado como algumas das medidas discutidas na Seção
2.2.1 podem ser calculadas para dados multivariados na linguagem Python. Para isso,
será utilizado como exemplo o conjunto de dados Iris6 , introduzido na Seção 2.2.
Para carregar o arquivo CSV para o conjunto de dados Iris, recomenda-se a utili-
zação da biblioteca Pandas, a qual dispõe do tipo de dados DataFrame, que tem uma
estrutura tabular, similar àquela apresentada na Tabela 2.1. Assim, pode-se proceder da
seguinte maneira:

import pandas

# carregando iris.csv
dados = pandas.read_csv('iris.csv')

# imprimindo os dez primeiros exemplos da base de dados


print(data.head(10))

Conforme já mencionado, para dados multivariados, as medidas apresentadas na


seção anterior serão calculadas sobre cada atributo do problema a ser tratado. Portanto,
como exemplo, considere o seguinte código para a Iris:
6 Disponível
em: https://raw.githubusercontent.com/pandas-dev/pandas/
master/pandas/tests/data/iris.csv.

10
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# calculando a media para todos os atributos


print(dados.mean())

# calculando a mediana para todos os atributos


print(dados.median())

# calculando percentil 15% para todos os atributos


print(dados.quantile(0.15))

# calculando o intervalo para todos os atributos


print(dados.max(numeric_only=True) - dados.min(numeric_only=True))

# calculando a variancia para todos os atributos


print(data.var())

# calculando o desvio-padrao para todos os atributos


# (na biblioteca pandas, ddof=1 por padrao)
print(data.std())

# calculando a obliquidade para todos os atributos


print(data.skew())

# calculando a curtose para todos os atributos


print(data.kurtosis())

2.4. Visualização de dados


2.4.1. Histogramas
Uma das formas mais simples de ilustrar a distribuição de um conjunto de valores de
uma variável é o uso de histogramas. Neste tipo de gráfico tem-se, no eixo horizontal, o
conjunto (ou intervalos) de valores observados, enquanto que no eixo vertical, apresenta-
se a frequência de ocorrência de cada valor (ou valores dentre de um intervalo) presente
na amostra analisada. O pacote NumPy fornece uma função para calcular o histograma,
que pode ser vista abaixo. Nessa função, bins corresponde ao número de barras verti-
cais. Quando o valor de bins é definido como ’auto’, o número de barras é definido
automaticamente. Como exemplo, considere o código a seguir:

# calculando o histograma para uma variável


import numpy
from matplotlib import pyplot

# Notas na primeira prova


notas = numpy.array([2, 5, 7, 3, 5, 6, 5, 6, 6, 5, 5, 3])

pyplot.hist(notas, bins='auto')
pyplot.title('Histograma')

11
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

pyplot.ylabel('Frequencia')
pyplot.xlabel('Nota')
pyplot.show()

O histograma gerado utilizando os dados do código anterior é ilustrado pela Figura


2.2. As cestas sem barras são os intervalos para os quais não havia nenhum valor.

Figura 2.2: Exemplo de histograma para os valores de uma variável.

2.4.2. Scatter plots


Um scatter plot consiste em um tipo de gráfico comumente utilizado para observar o
comportamento entre duas variáveis de uma base de dados. Na Figura 2.3 é apresentado
um exemplo de scatter plot para a base de dados Iris, em que cada cor indica uma classe
diferente.

Figura 2.3: Exemplo de scatter plot para dois atributos da base de dados Iris.

12
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Um scatter plot pode ser gerado por meio do seguinte código:

import pandas
from matplotlib import pyplot

# carregando iris.csv
dados = pandas.read_csv('iris.csv')

# criando um dicionario para mapear cada classe para uma cor


classe_cor = {'Iris-setosa' : 'red',
'Iris-virginica' : 'blue',
'Iris-versicolor' : 'green'}

# criando uma lista com as cores de cada exemplo


cores = [classe_cor[nome] for nome in dados.Name]

# gerando scatter plot


# no eixo x sera plotado o tamanho da sepala
# no eixo y sera plotado o comprimento da sepala
dados.plot(kind='scatter', x='SepalLength', y='SepalWidth',
c=cores)

pyplot.show()

Alternativamente, pode-se também gerar uma matriz de scatter plots, que irá con-
ter os scatter plots para todos os pares possíveis de atributos. Ademais, na diagonal de
tal matriz, pode-se apresentar informações a respeito de cada atributo (por exemplo, o seu
histograma). Na Figura 2.4 é apresentada a matriz de scatter plots para a base Iris, com
os histogramas dos atributos contidos na diagonal. Tal matriz pode ser gerada por meio
do seguinte código:

import pandas
from pandas.plotting import scatter_matrix
from matplotlib import pyplot

# carregando iris.csv
dados = pandas.read_csv('iris.csv')

# criando um dicionario para mapear cada classe para uma cor


classe_cor = {'Iris-setosa' : 'red',
'Iris-virginica' : 'blue',
'Iris-versicolor' : 'green'}

# criando uma lista com as cores de cada exemplo


cores = [classe_cor[nome] for nome in dados.Name]

13
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# gerando matriz de scatter plots


scatter_matrix(dados, color=cores)
pyplot.show()

Figura 2.4: Matriz de scatter plots para a base de dados Iris.

2.4.3. Box plots


Um box plot é um gráfico apresentado em formato de caixa, em que a aresta inferior
da caixa representa o primeiro quartil (Q1 ), a aresta superior representa o terceiro quartil
(Q3 ) e um traço interno à caixa representa a mediana (Q2 ) de uma amostra. Ademais, uma
linha tracejada delimita o limite entre o terceiro quartil e o maior valor das observações
que é menor ou igual a Q3 + 1,5 · (Q3 − Q1 ) e o limite entre o primeiro quartil e o menor
valor na amostra que é maior ou igual a Q1 − 1,5 · (Q3 − Q1 ). Valores abaixo ou acima de
tal linha tracejada são representados como círculos, e indicam valores extremos (outliers).
Na Figura 2.5 é apresentado um exemplo de box plot para os atributos da base Iris, o qual
pode ser gerado por meio do exemplo de código apresentado a seguir:

import pandas
from pandas.plotting import scatter_matrix
from matplotlib import pyplot

14
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# carregando iris.csv
dados = pandas.read_csv('iris.csv')

# gerando bloxplot para os atributos da Iris


dados.plot(kind='box')
pyplot.show()

Figura 2.5: Box plot para os atributos da base de dados Iris.

2.5. Exercícios
1. Pesquise e explique com as suas palavras sobre o que extraem as medidas de co-
variância e correlação. Qual a utilidade delas? Aplique-as para a base de dados
Iris, apresente e interprete os resultados (dica: use as funções DataFrame.cov7
e DataFrame.corr8 ).

2. Considere a matriz de scatter plots da Figura 2.4. A partir dos plots, o que pode ser
observado? Existe alguma classe mais separada das demais? Existem classes sobre-
postas? Qual a sua opinião sobre as possíveis implicações das classes sobrepostas
para uma tarefa de mineração de dados?

3. Calcule e apresente, para cada atributo da base Iris, sua obliquidade e sua curtose.
Compare os resultados obtidos com o box plot da Figura 2.5. Ademais, gere os
7 https://pandas.pydata.org/pandas-docs/stable/generated/pandas.

DataFrame.cov.html
8 https://pandas.pydata.org/pandas-docs/stable/generated/pandas.

DataFrame.corr.html

15
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

histogramas para cada atributo da base (dica: pesquise sobre os parâmetros neces-
sários para gerar histogramas com a função DataFrame.plot). Que atributos
possuem distribuições mais simétricas? Há a presença de valores extremos (outli-
ers) para algum atributo? Se sim, qual? O que você acha que pode ser a razão para
a ocorrência de tais valores?

Referências
[Faceli et al. 2011] Faceli, K., Lorena, A. C., Gama, J., e Carvalho, A. C. P. L. F. (2011).
Inteligência artificial: Uma abordagem de aprendizado de máquina. Rio de Janeiro:
LTC, 2:192.

[Fisher 1936] Fisher, R. A. (1936). The use of multiple measurements in taxonomic


problems. Annals of human genetics, 7(2):179–188.

[Hermans 2008] Hermans, R. (2008). Negative and positive skew diagrams


(English). https://commons.wikimedia.org/wiki/File:Negative_
and_positive_skew_diagrams_(English).svg. Acesso em: 04 ago.
2017.

[Magalhães e de Lima 2000] Magalhães, M. N. e de Lima, A. C. P. (2000). Noções de


probabilidade e estatística. IME-USP São Paulo:.

16
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Capítulo

3
Pré-processamento

Considerando o grande volume de dados disponível em diversas aplicações, com


frequência os conjuntos de dados não possuirão uma qualidade boa o suficiente para a ex-
tração de conhecimento novo, útil e relevante por algoritmos de aprendizado de máquina
(AM). As principais causas de baixa qualidade de dados incluem a ocorrência de atributos
irrelevantes, valores ausentes ou redundantes. Neste capítulo serão apresentadas algumas
simples abordagens para melhorar a qualidade dos dados, que aumentam as chances de
um bom modelo ser induzido por um algoritmo de AM. Na Seção 3.1 será descrito o
conjunto de dados que será utilizado para ilustrar o uso de técnicas de pré-processamento.
Na Seção 3.2 será apresentado como alguns atributos podem ser removidos manualmente.
Na Seção 3.3 serão apresentadas algumas das técnicas de amostragem de dados mais co-
muns. Na Seção 3.4 será explicado como dados ausentes podem ser tratados. Na Seção
3.5 será abordada a ocorrência de objetos ou atributos redundantes. Por fim, na Seção 3.9
serão propostos alguns exercícios para fixar os conceitos apresentados.

3.1. Conjunto de dados


Para os exemplos apresentados no presente capítulo, serão utilizados dois conjuntos de
dados: Breast Cancer Wisconsin1 [Street et al. 1992, Mangasarian et al. 1995] e Contra-
ceptive Method Choice2 [Lim et al. 2000].
No conjunto Breast Cancer Wisconsin cada objeto consiste em um tecido de massa
mamária e os atributos correspondem a características, extraídas a partir de imagens digi-
talizadas, dos núcleos celulares (raio, textura, perímetro etc.) contidos em cada tecido. As
classes associadas a cada tecido informam o diagnóstico do tecido (maligno ou benigno).
O conjunto Contraceptive Method Choice consiste em amostras de uma pesquisa
realizada na Indonésia em 1987 sobre métodos contraceptivos. Os objetos consistem
em mulheres que não estavam grávidas ou que não sabiam da gravidez ao participarem
da pesquisa. Os atributos consistem em características socio-econômicas. O problema
deste conjunto de dados consiste em classificar o método contraceptivo utilizado em: 1
(nenhum método utilizado), 2 (método de longa duração) ou 3 (método de curta duração).
Todos os atributos são dados qualitativos nominais ou ordinais. Uma descrição completa

1 http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+

%28Diagnostic%29
2 http://archive.ics.uci.edu/ml/datasets/Contraceptive+Method+Choice

18
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

de cada atributo e seus possíveis valores está disponível em http://archive.ics.


uci.edu/ml/machine-learning-databases/cmc/cmc.names.
Os exemplos apresentados a seguir utilizarão os arquivos breast_cancer.csv
e cmc.csv, fornecidos como material suplementar a este texto.

3.2. Eliminação manual de atributos


Diversos conjuntos de dados do mundo real podem ter atributos que, por serem claramente
irrelevantes, não apresentam qualquer benefício para uma tarefa de classificação ou de
extração de conhecimento. Por exemplo, o conjunto de dados Breast Cancer contém o
atributo sample_id, um valor numérico que identifica o tecido analisado. Como tal
valor será único para cada objeto e o mesmo não possui qualquer sentido comparativo,
ele pode ser eliminado. Em Python, isso pode ser realizado por meio do código a seguir.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# removendo a coluna sample_id, a qual nao eh necessaria


# para realizar o diagnostico
del dados['sample_id']

3.3. Amostragem
Diversos algoritmos de AM, sejam pela suas complexidades computacionais ou pelas
quantidades de memória exigida, apresentam dificuldades em tratar conjuntos de dados
de tamanho elevado. Uma quantidade de dados muito grande pode tornar o processo de
treinamento demorado. Um meio de contornar essa dificuldade é a utilização de amostras
do conjuntos de dados original. A utilização de um subconjunto menor de objetos, em
geral tornará mais simples e rápida a geração de um modelo.
Entretanto, um ponto importante a ser levado em consideração está relacionado
ao nível ao qual a amostra representa a distribuição original dos dados. Normalmente,
em casos nos quais a mesma não seja representativa, o modelo de AM induzido não será
capaz de atingir uma eficiência aceitável para o problema.
Um modo para tratar o ponto supracitado, resume-se na utilização de técnicas
de amostragem estatística, as quais aumentam as chances de que as amostras extraídas
da base de dados original possam ser informativas e representativas. Duas das técnicas
mais utilizadas são a amostragem simples e a amostragem estratificada. Ambas serão
introduzidas a seguir.

3.3.1. Amostragem simples


A amostragem simples consiste basicamente em extrair objetos, com ou sem reposição,
do conjunto de dados original com igual probabilidade. No primeiro caso, quando há a
reposição, um objeto pode ocorrer mais de uma vez em uma amostra e a probabilidade
de ocorrência de cada objeto mantém-se constante durante todo o processo. No segundo

19
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

caso, quando não existe reposição, cada objeto poderá acontecer apenas uma vez em uma
amostra. Assim, a probabilidade de ocorrência de cada objeto é alterada a cada passo do
processo. O código abaixo apresenta um exemplo para cada caso.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# gerando uma amostra aleatoria simples de 100 elementos


# da base breast cancer, sem reposicao
dados.sample(100)

# gerando uma amostra aleatoria simples de 100 elementos


# da base breast cancer, com reposicao
dados.sample(100, replace=True)

3.3.2. Amostragem estratificada


A amostragem estratificada é tipicamente utilizada quando há o desbalanceamento na
quantidade de exemplos de cada classe em um conjunto de dados. Ou seja, neste cenário,
normalmente uma ou outra classe possuirá uma quantidade de objetos consideravelmente
maior do que as demais. A fim de gerar um classificador que não seja tendencioso para
uma ou mais classes majoritárias ou, de suavizar a dificuldade de modelar alguma das
classes de um problema, a amostragem estratificada pode ser utilizada de duas maneiras.
Na primeira abordagem para amostragem estratificada, o subconjunto a ser ex-
traído contém a mesma quantidade de objetos para cada classe. O código a seguir ilustra,
de maneira simplificada, esse tratamento.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# tamanho da amostra estratificada


tamanho_amostra = 100

# obtendo as classes da base de dados


classes = dados['diagnosis'].unique()

# calculando a quantidade de amostras por classe


# neste exemplo, serao amostradas as mesmas
# quantidades para cada classe
qtde_por_classe = round(tamanho_amostra / len(classes))

# nesta lista armazenaremos, para cada classe, um


# pandas.DataFrame com suas amostras
amostras_por_classe = []

20
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

for c in classes:
# obtendo os indices do DataFrame
# cujas instancias pertencem a classe c
indices_c = dados['diagnosis'] == c

# extraindo do DataFrame original as observacoes da


# classe c (obs_c sera um DataFrame tambem)
obs_c = dados[indices_c]

# extraindo a amostra da classe c


# caso deseje-se realizar amostragem com reposicao
# ou caso len(obs_c) < qtde_por_classe, pode-se
# informar o parametro replace=True
amostra_c = obs_c.sample(qtde_por_classe)

# armazenando a amostra_c na lista de amostras


amostras_por_classe.append(amostra_c)

# concatenando as amostras de cada classe em


# um único DataFrame
amostra_estratificada = pd.concat(amostras_por_classe)

Na segunda abordagem, a amostra gerada do conjunto de dados original mantém


as mesmas proporções de objetos da base de dados original em cada classe. O exemplo
de código a seguir demonstra como isso pode ser feito.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# tamanho da amostra estratificada


tamanho_amostra = 100

# obtendo as classes da base de dados


classes = dados['diagnosis'].unique()

# nesta lista armazenaremos, para cada classe, um


# pandas.DataFrame com suas amostras
amostras_por_classe = []

for c in classes:
# obtendo os indices do DataFrame
# cujas instancias pertencem a classe c
indices_c = dados['diagnosis'] == c

21
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# extraindo do DataFrame original as observacoes da


# classe c (obs_c sera um DataFrame tambem)
obs_c = dados[indices_c]

# calculando a proporcao de elementos da classe c


# no DataFrame original
proporcao_c = len(obs_c) / len(dados)

# calculando a quantidade de elementos da classe


# c que estarao contidos na amostra estratificada
qtde_c = round(proporcao_c * tamanho_amostra)

# extraindo a amostra da classe c


# caso deseje-se realizar amostra com reposicao ou,
# caso len(obs_c) < qtde_c, pode-se
# informar o parametro replace=True
amostra_c = obs_c.sample(qtde_c)

# armazenando a amostra_c na lista de amostras


amostras_por_classe.append(amostra_c)

# concatenando as amostras de cada classe em


# um único DataFrame
amostra_estratificada = pd.concat(amostras_por_classe)

3.4. Dados ausentes


Dados ausentes podem ocorrer por uma série de motivos em cenários de aplicação reais
(veja, por exemplo, a discussão no Capítulo 3 do livro de [Faceli et al. 2011]). Uma alter-
nativa que pode ser utilizada para a solução de tal problema é a remoção de objetos que
contenham valores ausentes. Entretanto, deve-se perceber que uma grande quantidade de
informações relevantes podem acabar sendo descartadas (por exemplo, se muitos objetos
possuírem valores ausentes, tem-se uma redução significativa da base de dados). Por-
tanto, uma alternativa bastante comum é a inserção automática de tais valores, utilizando
alguma medida capaz de estimá-los. Comumente, tal medida consiste na média, mediana
ou moda do respectivo atributo. Em Python, a inserção automática pode ser realizada por
meio do código abaixo.

import pandas

# carrega uma versao do conjunto de dados contendo valores


# ausentes
dados = pandas.read_csv('breast_cancer_missing.csv')

# cada valor ausente eh indicado por um


# 'Not a Number' (numpy.nan)

22
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# isso pode ser verificado conforme abaixo


# onde 'valores_ausentes' sera um DataFrame em que
# cada posicao indica se o valor esta ausente
# (True) ou nao (False)
valores_ausentes = dados.isnull()

# calculando as medias dos atributos


# 'medias' resulta em uma variavel do tipo 'Series',
# o qual consiste em uma implementacao da Pandas
# de um numpy.array com cada elemento contendo
# um rotulo (ou nome)
medias = dados.mean()

# por fim, a insercao de valores sera realizada pela


# funcao fillna, onde o parametro inplace=True indica
# para os valores serem inseridos no próprio DataFrame
# (caso seja falso, uma copia do DataFrame original sera
# retornada contendo os valores preenchidos)
dados.fillna(medias, inplace=True)

3.5. Dados redundantes


Dados redundantes podem ocorrer tanto para os objetos quanto para os atributos de um
conjunto de dados [Faceli et al. 2011]. Quando há a redundância de objetos, dois ou mais
deles possuem valores muito similares (ou até mesmo iguais) para todos os seus atributos.
Isso pode ser um problema pois, ao aplicar um algoritmo de AM, tais objetos irão inserir
uma ponderação artificial aos dados. Objetos redundantes podem ser removidos por meio
do código a seguir.

import pandas

# carregando uma versao do conjunto de dados contendo


# objetos redundantes (repetidos)
dados = pandas.read_csv('breast_cancer_red.csv')

# removendo exemplos redundantes


# observe que o resultado sera a base original
# o parametro inplace determina se a alteracao deve
# ocorrer no proprio DataFrame
# portanto, caso inplace=False, a funcao drop_duplicates
# retorna uma copia de breast_cancer_red com os exemplos
# redundantes removidos
dados.drop_duplicates(inplace=True)

Para o caso de redundância de atributos, tem-se que um deles pode estar forte-
mente correlacionado com outro, o que indica um comportamento similar de suas varia-
ções. Algumas vezes, em tais casos, o valor de um atributo pode ser calculado ou obtido

23
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

por meio de um ou mais dentre os atributos remanescentes. A remoção de atributos redun-


dantes é proposta como exercício na Seção 3.9. Portanto, o respectivo exemplo prático
foi omitido neste texto.

3.6. Ruídos
Ruídos normalmente consistem em valores que aparentemente não pertencem à distribui-
ção que gerou o conjunto de dados à disposição [Faceli et al. 2011]. Vários podem ser
os motivos para a ocorrência dos mesmos, desde erros na digitação ao tabular os dados,
problemas de medição em instrumentos utilizados para coletar os dados ou, até mesmo,
valores pouco comuns mas reais. Portanto, ao descartar objetos que apresentem valores
ruidosos para um ou mais atributos, pode-se perder informações relevantes para o pro-
blema estudado.
A fim de reduzir a influência de ruídos nos atributos do conjunto de dados estu-
dado, diversas técnicas podem ser aplicadas. Dentre elas, uma das mais simples consiste
em dividir os valores de cada atributo em faixas, de modo que cada faixa contenha apro-
ximadamente a mesma quantidade de valores. Em seguida, os valores contidos em cada
faixa são substituídos por alguma medida que os sumarize como, por exemplo, a média.
Um exemplo desta técnica é demonstrado a seguir.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# dividindo o atributo 'mean_radius' em 10 faixas


bins = pandas.qcut(dados['mean_radius'], 10)

# a quantidade de valores aproximadamente igual em


# cada faixa pode ser comprovada pelo metodo
# value_counts
bins.value_counts()

# O metodo groupby permite que valores contidos na


# coluna de um DataFrame sejam agrupados segundo
# algum criterio.
# Neste exemplo, a coluna 'mean_radius' sera agrupada
# pelas faixas definidas pelo metodo qcut.
grupos = dados['mean_radius'].groupby(bins)

# obtendo a media de cada faixa


medias = grupos.mean()

# Obtendo a nova coluna.


# O metodo apply recebe como parametro uma funcao,
# aplica tal funcao a todos os seus elements e retorna
# um pandas.Series contendo os resultados.

24
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# Neste caso, cada elemento de bins consiste


# no intervalo que o respectivo valor de 'mean_radius'
# pertence e, assim, a funcao informada em apply
# retornara a respectiva media de cada intervalo.
novo_mean_radius = bins.apply(lambda x : medias[x])

# por fim, a coluna 'mean_radius' do DataFrame original


# eh atualizada
dados['mean_radius'] = novo_mean_radius

3.7. Transformação de dados


3.7.1. Transformação de dados simbólicos para dados numéricos
Diversas técnicas de AM exigem que os dados de entrada consistam apenas em valores
numéricos. Entretanto, diversos conjuntos reais podem apresentar atributos qualitativos
nominais ou ordinais, tornando assim necessário o emprego de técnicas que permitam a
conversão de tais atributos.
No primeiro caso, quando houver a existência de atributos nominais, a inexistência
de qualquer ordem deve persistir na conversão do atributo. Uma abordagem bastante
conhecida e utililizada para isso consiste na codificação 1-de-c [Faceli et al. 2011]. Nela,
considerando que um atributo possua c valores possíveis, são criados c novos atributos
binários, onde cada posição indica um possível valor do atributo nominal original. Desse
modo, apenas uma posição da nova sequência binária de cada objeto poderá ser igual
a 1, indicando qual é o valor correspondente de um determinado objeto para o atributo
original. Em Python, tal conversão pode ser facilmente realizada por meio do método
pandas.get_dummies, conforme demonstrado pelo exemplo a seguir.

import pandas

dados = pandas.read_csv('cmc.csv')

# O atributo husband_occupation consiste em um atributo


# nominal com 4 valores possiveis.
# A conversao do mesmo para a codificacao 1-de-c eh
# feita como:
occ1dec = pandas.get_dummies(dados['husband_occupation'])

Quando houverem atributos qualitativos ordinais, pode-se também optar por repre-
sentações binárias. Entretanto, as mesmas deverão ser diferentes da representação 1-de-c,
uma vez que as distâncias entre os possíveis valores de um atributo não serão mais iguais
para todos os casos. Para isso, pode-se utilizar o código cinza ou o código termômetro
[Faceli et al. 2011].
O código cinza consiste na transformação dos valores inteiros para as suas respec-
tivas representações binárias. Em Python, tal transformação pode ser realizada conforme
segue.

25
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

import pandas

dados = pandas.read_csv('cmc.csv')

# O atributo wife_education consiste em um atributo


# qualitativo ordinal com 4 valores possiveis.
# A conversao do mesmo para o codigo cinza pode ser
# feita pela aplicacao do metodo 'bin' na respectiva
# coluna do DataFrame. O metodo 'bin' recebe como entrada
# um valor inteiro e retorna uma string com a representacao
# binaria do mesmo.
wife_ed_binario = dados['wife_education'].apply(bin)

O código termômetro realiza a transformação do respectivo atributo qualitativo


ordinal em um vetor de c posições, onde c indica a quantidade de valores possíveis. Desse
modo, cada valor ordinal corresponde a um vetor binário preenchido com uma quantidade
de valores iguais a 1, acumulados sequencialmente da esquerda para a direita ou vice-
versa, equivalente à sua posição na ordem dos valores possíveis do atributo original. Esta
transformação é proposta como exercício na Seção 3.9. Portanto, o respectivo código foi
omitido.

3.7.2. Transformação de dados numéricos para dados simbólicos


Várias de técnicas de AM como, por exemplo, alguns algoritmos de árvore de decisão,
exigem que os dados de entrada assumam valores qualitativos. Portanto, em cenários nos
quais há a presença de atributos com valores contínuos, torna-se importante a aplicação
de técnicas adequadas de discretização.
Existe uma grande quantidade de técnicas de discretização disponíveis na litera-
tura (os estudos de [Kotsiantis e Kanellopoulos 2006, Garcia et al. 2013] sumarizam vá-
rias delas), as quais são baseadas em diferentes suposições e procedimentos. Dentre elas,
as mais simples e intuitivas consistem na divisão de um intervalo original de valores por
faixas, podendo estas terem a mesma largura ou frequências de valores similares. O có-
digo a seguir ilustra ambas.

import pandas

dados = pandas.read_csv('breast_cancer.csv')

# Obtendo a coluna 'mean_radius'.


mean_radius = dados['mean_radius']

# Discretizando a coluna 'mean_radius'.


# O parametro 'bins' define em quantos intervalos
# sera discretizado.
# O parametro labels define o rotulo de cada
# intervalo.

26
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# Neste caso, como labels eh igual a range(10),


# os intervalos serao discretizados com valores
# inteiros entre 0 e 9.
# O metodo pandas.cut discretiza em intervalos
# de larguras iguais. Caso deseje-se discretizar
# com frequencias iguais, deve-se utilizar o
# metodo pandas.qcut.
mean_radius_disc = pandas.cut(dados, bins=10,
labels=range(10))

# Atualizando a respectiva coluna no DataFrame


# original.
dados['mean_radius'] = mean_radius_disc

3.7.3. Normalização de atributos numéricos


Muitos conjuntos de dados reais apresentam atributos contínuos cujos valores espalham-
se por distintas faixas de valores ou que possuem diferentes variações de valores, devido
às suas naturezas ou escalas em que foram medidas. Em alguns problemas, tais diferenças
podem ser importantes e devem ser levadas em conta [Faceli et al. 2011]. Entretanto, em
outras situações pode ser necessária uma normalização dos valores de cada atributo, a fim
de que se evite que algum predomine sobre outro ou que inclua qualquer tipo de ponde-
ração indesejada ao induzir um modelo de AM. Os tipos mais comuns de normalização
consistem em reescala ou padronização.
A normalização por reescala define, através de um valor mínimo e um valor má-
ximo, um novo intervalo onde os valores de um atributo estarão contidos. Tipicamente,
tal intervalo é definido como [0, 1]. Portanto, para este caso, a normalização por reescala
de um atributo j de um objeto xi pode ser calculada como:

xi j − min j
xi j = (1)
max j − min j

sendo min j e max j , nessa ordem, os valores mínimo e máximo do atributo j para o con-
junto de dados considerado.
Na normalização por padronização, os diferentes atributos contínuos poderão abran-
ger diferentes intervalos, mas deverão possuir os mesmos valores para alguma medida de
posição e de espalhamento/variação [Faceli et al. 2011]. Tipicamente, tais medidas irão
consistir na média e no desvio-padrão. Neste caso, o valor normalizado de um atributo j
em um objeto i é dado por:
xi j − x̄· j
xi j = (2)
sj
onde x̄· j e s j representam a média do atributo j e o seu desvio-padrão, respectivamente.
Desse modo, cada cada atributo j terá média zero e desvio-padrão unitário.
Os dois tipos de normalização supramencionados podem ser executados conforme
o código em seguida.

27
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

import pandas
from sklearn.preprocessing import minmax_scale
from sklearn.preprocessing import scale

dados = pandas.read_csv('breast_cancer.csv')

# Obtendo os nomes das colunas do DataFrame


# como uma lista.
cols = list(dados.columns)

# Removendo da lista 'cols' os nomes


# 'sample_id' e 'diagnosis' que sao
# colunas que nao serao normalizadas
cols.remove('sample_id')
cols.remove('diagnosis')

# Copiando os dados e aplicando a normalizacao


# por reescala nas colunas do DataFrame que contem
# valores continuos.
# Por padrao, o metodo minmax_scale reescala
# com min=0 e max=1.
dados_amp = dados.copy()
dados_amp[cols] = dados[cols].apply(minmax_scale)

# Copiando os dados e aplicando a normalizacao


# por padronização a todas as colunas do DataFrame.
# Por padrao, o metodo scale subtrai a media e
# divide pelo desvio-padrao.
dados_dist = dados.copy()
dados_dist[cols] = dados[cols].apply(scale)

3.8. Redução de dimensionalidade


Muitos dos conjuntos de dados tratados em mineração de dados possuem como caracte-
rística um elevado número de atributos. Uma aplicação biológica clássica em que esse
tipo de situação ocorre é na análise de dados de expressão gênica, onde milhares de ge-
nes são tipicamente aferidos em um número consideravelmente menor e mais limitado de
amostras (normalmente de dezenas a algumas centenas).
São poucas as técnicas de AM que são capazes de lidar com uma grande quan-
tidade de atributos de maneira eficiente (tanto do ponto de vista computacional quanto
do ponto de vista de acurácia). Portanto, a utilização de técnicas que sejam capazes de
reduzir a dimensionalidade dos dados, por meio de agregação ou seleção de atributos,
pode auxiliar na indução de um modelo de AM. Nesta seção, serão abordados dois exem-
plos para redução de dimensionalidade: Principal Component Analysis, para agregação
de atributos; e filtragem por um limiar de variância pré-estabelecido, para seleção de atri-
butos.

28
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

3.8.1. Principal Component Analysis (PCA)


O PCA consiste em uma técnica estatística que utiliza uma transformação linear para
reexpressar um conjunto de atributos em um conjunto menor de atributos não correlaci-
onados linearmente, mantendo boa parte das informações contidas nos dados originais
[Dunteman 1989]. Em suma os objetivos do PCA consistem em [Abdi e Williams 2010]:

• Extrair apenas as informações mais relevantes de um conjunto de dados;

• Comprimir o tamanho do conjunto de dados original, simplificando assim sua des-


crição; e

• Permitir a análise da estrutura dos objetos e dos atributos de um conjunto de dados.

Um simples e intuitivo tutorial sobre o PCA está disponível em https://arxiv.


org/abs/1404.1100. Abaixo é apresentado um exemplo da aplicação do PCA no
conjunto Breast Cancer.

import pandas
from sklearn.decomposition import PCA

dados = pd.read_csv('breast_cancer.csv')

# Obtendo os nomes das colunas.


cols = list(dados.columns)

# Removendo colunas que nao serao inclusas


# na reducao de dimensionalidade.
cols.remove('sample_id')
cols.remove('diagnosis')

# Instanciando um PCA. O parametro n_components


# indica a quantidade de dimensoes que a base
# original sera reduzida.
pca = PCA(n_components=2, whiten=True)

# Aplicando o pca na base breast_cancer.


# O atributo 'values' retorna um numpy.array
# de duas dimensões (matriz) contendo apenas
# os valores numericos do DataFrame.
dados_pca = pca.fit_transform(dados[cols].values)

# O metodo fit_transform retorna outro numpy.array


# de dimensao numero_objetos x n_components.
# Apos isso, instancia-se um novo DataFrame contendo
# a base de dados original com dimensionalidade
# reduzida.

29
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

dados_pca = pd.DataFrame(dados_pca,
columns=['comp1', 'comp2'])

3.8.2. Filtragem por limiar de variância


A filtragem por limiar de variância consiste basicamente em manter apenas os atributos
da base de dados original que possuam variância acima de um valor pré-estabelecido. O
código a seguir ilustra o funcionamento da mesma em Python.

import pandas

dados = pd.read_csv('breast_cancer.csv')

# Obtendo os nomes das colunas.


cols = list(dados.columns)

# Removendo colunas que nao serao inclusas


# na reducao de dimensionalidade.
cols.remove('sample_id')
cols.remove('diagnosis')

# Instanciando um VarianceThreshold. Esta


# classe recebe apenas um parâmetro real,
# que indica o valor de limiar desejado.
var_thr = VarianceThreshold(1.0)

# Selecionando apenas os atributos com


# variância maior ou igual a 1.
# 'dados_var_thr' sera um numpy.array com
# duas dimensoes.
dados_var_thr = var_thr.fit_transform(dados[cols].values)

Outros métodos mais sofisticados para seleção de atributos estão disponíveis na


biblioteca scikit-learn por meio do módulo sklearn.feature_selection3 .

3.9. Exercícios
1. Observe que os códigos apresentados na Seção 3.3.2 nem sempre retornam uma
amostra com o tamanho desejado exato (dica: teste ambos os códigos com a base
Iris, por exemplo). Por que isso acontece? Escreva uma função que receba como
entrada um DataFrame, um tamanho de amostra desejado, o tipo de amostragem
estratificada a ser feita e retorne uma amostra estratificada com o tamanho exato
desejado. Quais as implicações desta nova função no resultado da amostragem?
2. Repare que o código apresentado na Seção 3.4 considera os exemplos de todas
as classes para o cálculo dos valores a serem inseridos no DataFrame. Escreva
3 http://scikit-learn.org/stable/modules/feature_selection.html

30
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

uma função que calcule e insira tais valores separadamente para cada classe. Ade-
mais, tal função deverá receber como parâmetro a medida que será calculada (média
ou mediana). Considerando a base de dados breast_cancer_missing.csv,
crie um arquivo CSV com o novo DataFrame gerado para cada caso.

3. Escreva uma função que receba como entrada um DataFrame, calcule a correla-
ção entre todos os seus atributos (através da função DataFrame.corr) e retorne
um novo DataFrame mantendo apenas um atributo dentre aqueles que possuem
correlação acima (ou abaixo, caso a correlação seja negativa) de um limiar infor-
mado como parâmetro.

4. Implemente a representação pelo código termômetro descrita na Seção 3.7.1 e apli-


que a mesma para todos os atributos qualitativos ordinais do conjunto de dados
Contraceptive Method Choice.

5. Repare que no código apresentado na Seção 3.8 o novo DataFrame gerado possui
duas colunas nomeadas ’comp1’ e ’comp2’. Pesquise sobre o PCA e descreva
com as suas palavras o que esses dois novos atributos representam.

6. Aplique o PCA no conjunto de dados Iris, introduzido no capítulo anterior, com


n_components=2. Gere um scatter plot com diferentes cores para cada classe.
O que pode ser observado? Como o PCA pode auxiliar como ferramenta de visua-
lização?

Referências
[Abdi e Williams 2010] Abdi, H. e Williams, L. J. (2010). Principal component analysis.
Wiley interdisciplinary reviews: computational statistics, 2(4):433–459.

[Dunteman 1989] Dunteman, G. H. (1989). Principal components analysis. Number 69.


Sage.

[Faceli et al. 2011] Faceli, K., Lorena, A. C., Gama, J., e Carvalho, A. C. P. L. F. (2011).
Inteligência artificial: Uma abordagem de aprendizado de máquina. Rio de Janeiro:
LTC, 2:192.

[Garcia et al. 2013] Garcia, S., Luengo, J., Sáez, J. A., Lopez, V., e Herrera, F. (2013).
A survey of discretization techniques: Taxonomy and empirical analysis in supervised
learning. IEEE Transactions on Knowledge and Data Engineering, 25(4):734–750.

[Kotsiantis e Kanellopoulos 2006] Kotsiantis, S. e Kanellopoulos, D. (2006). Discreti-


zation techniques: A recent survey. GESTS International Transactions on Computer
Science and Engineering, 32(1):47–58.

[Lim et al. 2000] Lim, T.-S., Loh, W.-Y., e Shih, Y.-S. (2000). A comparison of predic-
tion accuracy, complexity, and training time of thirty-three old and new classification
algorithms. Machine learning, 40(3):203–228.

31
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

[Mangasarian et al. 1995] Mangasarian, O. L., Street, W. N., e Wolberg, W. H. (1995).


Breast cancer diagnosis and prognosis via linear programming. Operations Research,
43(4):570–577.

[Street et al. 1992] Street, W. N., Wolberg, W. H., e Mangasarian, O. L. (1992). Nuclear
feature extraction for breast tumor diagnosis.

32
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Capítulo

4
Análise de agrupamento de dados

A análise de agrupamento de dados é um dos principais problemas exploratórios


investigados em mineração de dados. Ao longo das últimas décadas, diversos algoritmos
e formulações foram propostos para os mais variados tipos de aplicação. Neste capítulo
são apresentadas e discutidas alguns dos principais algoritmos da área, bem como alguns
dos critérios para validação das soluções encontradas por eles. Portanto, esta parte está
organizada conforme segue. Na Seção 4.1 será descrito o problema de agrupamento de
dados. Na Seção 4.2 será discutido ... Na Seção 4.3 serão introduzidos os três algoritmos
de agrupamento hierárquico mais conhecidos. Na Seção 4.4 será apresentado o algoritmo
k-means. Na Seção 4.5 será explicado o algoritmo DBSCAN. Por fim, na Seção 4.7, serão
propostos alguns exercícios.

4.1. Aprendizado de máquina não supervisionado


O problema de AM não supervisionado consiste em trabalhar sobre um conjunto de da-
dos, sem utilzar rótulos ou qualquer tipo de informação (ou supervisão) sobre como as
instâncias devem ser manipuladas [Zhu e Goldberg 2009]. Em outras palavras, o con-
junto de dados tratado consiste apenas de instâncias sem qualquer informação de ró-
tulo (ou classe) conhecida a priori. Algumas das principais tarefas neste cenário são
[Zhu e Goldberg 2009]: detecção de novidades, redução de dimensionalidade e agrupa-
mento de dados, sendo esta última de interesse do presente capítulo.
Em suma, considerando um conjunto de dados composto por n objetos descritos
por m características, o problema de agrupamento de dados consiste em segmentar esse
conjunto em k grupos, com k << n, de modo que objetos contidos em um mesmo grupo
sejam similares entre si e dissimilares de objetos contidos nos demais grupos, segundo
alguma medida de (dis)similaridade que leva em conta as m características disponíveis
[Jain e Dubes 1988, Tan et al. 2006].
É importante salientar que não há uma definição universal para o que constitui
um grupo [Everitt et al. 2001, Faceli et al. 2011], sendo esta dependente do algoritmo e
aplicação estudados. Ademais, diversos algoritmos de agrupamento foram propostos ao
longo das últimas décadas, baseadas em diferentes suposições. Por exemplo, o algo-
ritmo k-means, amplamente conhecido e utilizado, foi proposto há mais de cinco décadas
[Jain 2010].
Portanto, o presente capítulo possui um caráter totalmente introdutório, sem o ob-

33
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

jetivo de cobrir de maneira ampla o campo de análise de agrupamento de dados. Assim,


apenas os algoritmos clássicos que buscam por partições rígidas nos dados serão discuti-
dos. Formalmente, dado um conjunto X = {x1 , · · · , xn }, uma partição rígida consiste em
uma coleção de subconjuntos C = {C1 , · · · ,Ck }, satisfazendo as seguintes propriedades
[Xu e Wunsch 2005]:

• C1 ∪C2 ∪ · · · ∪Ck = X;

• Ci 6= 0,
/ ∀i ∈ [1, k]; e

• Ci ∩C j = 0,
/ ∀i, j ∈ [1, k] e i 6= j.

Exercício 4.1. Uma partição rígida, conforme previamente definida, é comumente refe-
renciada na literatura como agrupamento particional exclusivo. Outros tipos de partições
encontradas na literatura são: probabilístico, fuzzy ou particional não exclusivo. No pri-
meiro caso, cada objeto possuirá uma probabilidade de pertencer a cada grupo com a
restrição de que a soma de suas probabilidades é igual a 1. No segundo caso, cada ob-
jeto tem um grau de pertinência no intervalo [0, 1] para cada grupo. Por fim, no terceiro
caso, cada objeto pode ser incluído em mais de um grupo. Escreva, para cada cenário,
as respectivas propriedades, de modo similar ao apresentado anteriormente para o caso
particional exclusivo.

4.2. Medidas de (dis)similaridade


A definição de uma medida de (dis)similaridade para um problema de agrupamento de
dados é de grande importância, uma vez que ela será uma das principais responsáveis
por definir a estrutura de grupos produzida. A escolha de uma medida é uma decisão
importante e deve levar em conta diversos fatores. Dentre eles, um dos mais importantes
envolve os tipos de atributos à disposição (quantitativo, qualitativo nominal, qualitativo
ordinal, misto etc.).
Boa parte das medidas de dissimilaridade para objetos com atributos quantitativos
são baseadas na distância de Minkowski. Considerando dois objetos xi e x j , essa distância
é definida como:
 m 1
p
p
d(xi , x j ) = ∑ |xil − x jl | . (1)
l=1

A escolha do valor de p define variações para essa medida. Dentre elas, os três casos mais
conhecidos são [Faceli et al. 2011]:

• Distância Manhattan (p = 1):

m
d(xi , x j ) = ∑ |xil − x jl |; (2)
l=1

34
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

• Distância euclidiana (p = 2):


s
m
d(xi , x j ) = ∑ (xil − x jl )2; (3)
l=1

• Distância de Chebyshev (p = ∞):

d(xi , x j ) = max |xil − x jl |. (4)


1≤l≤m

Por sua vez, dentre as principais medidas de similaridade, pode-se citar a correla-
ção de Pearson e o cosseno entre dois vetores [Faceli et al. 2011].
Para atributos qualitativos, uma das medidas de dissimilaridade mais conhecidas
é a distância de Hamming, definida como:
m
d(xi , x j ) = ∑ I(xil = x jl ), (5)
l=1

sendo I(·) a função indicadora, a qual retorna valor igual a um quando a condição passada
a ela é verdadeira e zero, caso contrário.
Em Python, diversas medidas de distância podem ser calculadas para um conjunto
de dados por meio da função pdist do módulo scipy.spatial.distance1 . Um
exemplo de código é apresentado a seguir.

from scipy.spatial.distance import pdist, squareform


import pandas

# carregando iris.csv sem a coluna que contem


# os rotulos das classes
dados = pandas.read_csv('iris.csv', usecols=[0, 1, 2, 3])

# O metodo 'pdist' recebe como entrada um numpy.array.


# O atributo 'values' de um pandas.DataFrame retorna
# seus valores em tal formato.
dados = dados.values

# 'pdist' calcula as distancias entre todos os pares


# possiveis de objetos. O parametro 'metric' define
# qual medida de (dis)similaridade sera calculada.
distancias = pdist(dados, metric='euclidean')

# O metodo 'pdist' retorna um numpy.array contendo


# n * (n - 1) / 2 elementos. Para transforma-lo em
1 https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.

distance.pdist.html

35
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# um numpy.array de dimensao n x n pode-se utilizar


# o metodo 'squareform'.
distancias = squareform(distancias)

Exercício 4.2. Considerando os três casos da distância de Minkowski apresentados


nesta seção (p = 1, p = 2 e p = ∞), comente, para cada um, qual o formato da super-
fície gerada por todos os possíveis objetos equidistantes a um objeto central.

4.3. Algoritmos hierárquicos


Enquanto algoritmos particionais geram apenas uma partição, algoritmos hierárquicos
produzem uma sequência de partições rígidas aninhadas, cada uma contendo uma quanti-
dade diferente de grupos. Esses algoritmos podem seguir duas abordagens [Faceli et al. 2011]:

1. Abordagem aglomerativa: o algoritmo inicia com n grupos, cada um formado por


um objeto diferente do conjunto de dados. Em cada passo, os dois grupos mais
próximos, segundo algum critério pré-estabelecido, são unidos. Desse modo, o
procedimento é repetido até que reste apenas um único grupo contendo todos os
objetos.

2. Abordagem divisiva: o algoritmo inicia com apenas um grupo contendo todos os


objetos. Em cada passo, algum dos grupos é dividido em dois novos grupos, se-
gundo algum critério pré-estabelecido. Todo o procedimento é realizado até que
sejam formados n grupos, cada um contendo apenas um objeto do conjunto de da-
dos original.

Os algoritmos hierárquicos clássicos da literatura são baseados na abordagem


aglomerativa e podem ser sumarizados pelo Algoritmo 1.
Algoritmo 1: agrupamento aglomerativo hierárquico.
Entrada: matriz de dissimilaridade S ∈ Rn×n
Saída : agrupamento hierárquico de S
1 enquanto todos os objetos não estiverem em um único grupo faça
2 atualizar as distâncias entre todos os pares possíveis de grupos;
3 encontrar os dois grupos Ci e C j mais próximos;
4 unir Ci e C j em um novo grupo;
5 fim

À execução do passo 3 do algoritmo acima dá-se o nome de linkage (ligação).


A principal diferença entre vários dos algoritmos aglomerativos está no modo como a
atualização das distâncias entre os grupos é feita (passo 2). Os métodos mais conhecidos
são:

• Single-linkage: a distância d(Ci ,C j ) entre dois grupos é dada pela distância mínima

36
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

entre os seus objetos. Ou seja:

d(Ci ,C j ) = min d(xa , xb ). (6)


xa ∈Ci
xb ∈C j

• Complete-linkage: a distância d(Ci ,C j ) entre dois grupos é dada pela distância má-
xima entre os seus objetos. Ou seja:

d(Ci ,C j ) = max d(xa , xb ). (7)


xa ∈Ci
xb ∈C j

• Average-linkage: a distância d(Ci ,C j ) entre dois grupos é dada pela distância média
entre os objetos de diferentes grupos. Ou seja:

1
d(Ci ,C j ) = d(xa , xb ). (8)
|Ci ||C j | xa∑
∈Ci
xb ∈C j

Por fim, uma das principais vantagens de algoritmos de agrupamento hierárquicos


advém da representação dos seus resultados por meio de dendrogramas. Um dendrograma
é uma representação gráfica em formato de árvore que apresenta a hierarquia de partições
obtidas. Na Figura 4.1 é apresentado um exemplo de dendrograma, utilizando o método
complete-linkage, para o conjunto de dados artificial blobs.csv, fornecido como ma-
terial suplementar, que contém 20 instâncias separadas em três grupos correspondentes
a três distribuições normais multivariadas. No eixo horizontal é apresentado o índice de
cada objeto. No eixo vertical é apresentado o valor de distância quando cada par de ob-
jetos foi unido. As possíveis partições são definidas por meio de cortes no dendrograma.
Um exemplo de corte corresponde à linha horizontal pontilhada2 .
Em Python, a biblioteca SciPy fornece as implementações dos três algoritmos de
agrupamento hierárquico citados anteriormente, além de uma função para a plotagem de
dendrogramas. Um exemplo de código, utilizando o conjunto blobs.csv é apresentado
a seguir.

from scipy.cluster.hierarchy import linkage


from scipy.cluster.hierarchy import fcluster
from scipy.cluster.hierarchy import dendrogram
from matplotlib import pyplot
import pandas

# carregando blobs.csv sem a coluna 'label'


dados = pandas.read_csv('blobs.csv', usecols=[0, 1])

# O metodo 'linkage' recebe como entrada um numpy.array.


2 Neste exemplo, o corte sugerido segmenta os três grupos corretamente.

37
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Figura 4.1: Exemplo de dendrograma para um conjunto de dados gerado a partir de três
distribuições normais multivariadas.

# O atributo 'values' de um pandas.DataFrame retorna


# seus valores em tal formato.
dados = dados.values

# Aplicando o agrupamento hierarquico aos dados.


# O parametro 'method' define qual algoritmo sera
# utilizado.
# Varios metodos de agrupamento estao disponiveis. Para
# os exemplos deste material sera utilizado
# method='average', method='complete' ou
# method='single'.
# O parametro 'metric' define a medida de
# (dis)similaridade a ser utilizada. Para uma lista
# de medidas disponiveis recomenda-se a documentacao
# do metodo scipy.spatial.distance.pdist.
h = linkage(dados, method='complete', metric='euclidean')

# Para plotar o dendrograma utiliza-se o metodo


# 'dendrogram'.
dendrogram(h)
pyplot.show()

38
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# O metodo fcluster recebe como entrada uma


# hierarquia gerada pelo metodo linkage e extrai
# grupos da mesma segundo algum criterio.
# Abaixo serao extraidos os grupos por meio de um limiar
# de distancia. Segundo o dendrograma da Figura
# 4.1, o qual foi gerado atraves do metodo
# complete-linkage, um limiar de 7.5 parece ser
# adequado.
# A variavel 'rotulos' sera um numpy.array
# onde o valor contido em rotulos[i] indica o rotulo
# do objeto i.
rotulos_dist = fcluster(h, t=7.5, criterion='distance')

# Caso o criterio escolhido seja o numero de


# grupos, o metodo fcluster estima sozinho um valor
# de distancia de modo que t grupos sejam formados.
# Por exemplo, para extrair 3 grupos:
rotulos_k = fcluster(h, t=3, criterion='maxclust')

Exercício 4.3. Considerando um conjunto de dados X = {x1 , · · · , x5 } e a matriz de dis-


tâncias  
0 3 10 1 4
 3 0 2 2 3
 
10 2 0 8 8
 
 1 2 8 0 8
4 3 8 8 0
Nessa matriz, cada elemento ai j indica a distância entre xi e x j . Aplique à matriz
os três algoritmos hierárquicos descritos nesta seção e descreva, de maneira detalhada, os
cálculos realizados por cada um deles em cada passo.

4.4. Algoritmo k-means


O algoritmo k-means busca por uma partição que minimize a soma dos erros quadráticos
(SSE, do inglês, sum of squared errors) entre os objetos de um conjunto de dados e o
centróide dos seus respectivos grupos [Jain 2010]. A medida SSE é definida como:
k
SSE = ∑ ∑ d(x j , x̄Ci )2 , (9)
i=1 x j ∈Ci

sendo d(·, ·) a distância euclidiana e x̄Ci o centróide de um grupo Ci , calculado como:


1
x̄Ci = x j. (10)
|Ci | x ∑
j ∈Ci

Em [Drineas et al. 2004] é provado que o problema de minimização da SSE é NP-


difícil, inclusive quando k = 2. Portanto, o algoritmo k-means é um procedimento de

39
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

otimização com inicialização aleatória que garante a convergência para um mínimo local
da Equação (9). O mesmo é apresentado no Algoritmo 2.
Algoritmo 2: k-means.
Entrada: conjunto de dados X ∈ Rn×m e o número de grupos k
Saída : agrupamento particional de X em k grupos
1 gerar k centróides aleatoriamente;
2 repita
3 calcular a distância entre cada objeto x j e cada centróide x̄Ci ;
4 atribuir cada objeto x j ao grupo Ci com centróide mais próximo;
5 recalcular o centróide de cada grupo conforme a Equação (10);
6 até que um critério pré-definido seja atingido ou que os objetos não mudem
de grupo;

Um exemplo da execução do k-means para o conjunto blobs.csv é apresentado


abaixo.

from sklearn.cluster import k_means


import pandas

# carregando blobs.csv sem a coluna 'label'


dados = pandas.read_csv('blobs.csv', usecols=[0, 1])

# O metodo 'k_means' recebe como entrada um numpy.array.


# O atributo 'values' de um pandas.DataFrame retorna
# seus valores em tal formato.
dados = dados.values

# Executa o algoritmo k-means.


# 'n_clusters' indica o numero de grupos buscados.
# 'init' indica o tipo de inicializacao.
# 'n_init' indica a quantidade de vezes que o algoritmo
# sera executado. Dentre todas as 'n_init' execucoes,
# eh retornada aquela com o menor valor de sse.
# O metodo retorna tres valores: o primeiro, um
# um numpy.array com k linhas e m colunas,
# contendo os centroides finais; o segundo, um
# numpy.array contendo os rotulos de cada objeto; e,
# por fim, o valor de sse da solucao retornada.
centroides, rotulos, sse = k_means(dados,
n_clusters=3,
init='random',
n_init=100)

Exercício 4.4. Considerando a função objetivo do k-means, apresentada na Equação (9):

40
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

a Discuta quais serão as características dos grupos encontrados por esse algoritmo.

b Descreva em quais cenários o k-means não é capaz de produzir agrupamentos de


boa qualidade.

c Um dos maiores problemas do algoritmo k-means decorre da influência de outliers


em sua performance. Em tais casos, o valor de SSE sofrerá um grande incremento
e os centróides finais não serão tão representativos quanto seriam com a ausência
dos outliers [Tan et al. 2006]. Uma alternativa para isso seria utilizar o algoritmo
k-medians, o qual substitui a média pela mediana no cálculo de um centróide3 .
Embora resultados mais robustos podem ser encontrados, o k-medians possui uma
grande desvantagem em relação ao k-means. Enuncie e discuta tal desvantagem.

4.5. Algoritmo DBSCAN


O algoritmo DBSCAN (Density-Based Spatial Clustering of Applications with Noise)
busca por grupos definidos como regiões com alta densidade de objetos, separados por
regiões de baixa densidade [Tan et al. 2006]. Uma das principais vantagens desse algo-
ritmo advém do fato de não ser necessário informar previamente o número desejado de
grupos. Para isso, ele se baseia na classificação de cada objeto do conjunto de dados em
uma dentre 3 categorias [Ester et al. 1996]:

• Objeto central: todo objeto xi contendo uma quantidade de objetos vizinhos, con-
tando ele próprio, maior ou igual a um parâmetro MinPts. Um vizinho é determi-
nado como todo objeto separado por, no máximo, uma distância ε de xi .

• Objeto de borda: todo objeto que não satisfaz as condições para objeto central,
mas que pertence à vizinhança de um objeto central.

• Ruído: todo objeto que não pertence a nenhuma das duas categorias anteriores.

A Figura 4.2 ilustra um exemplo da classificação de um conjunto de objetos con-


siderando MinPts = 3. Nela, os objetos vermelhos são centrais, os amarelos de borda e o
azul é ruído. Os círculos em torno de cada objeto denotam o raio ε que define as vizinhan-
ças. O Algoritmo 3 apresenta o pseudocódigo do algoritmo DBSCAN [Tan et al. 2006].

3Éprovado que este novo algoritmo minimiza a distância L1 , também conhecida como distância de
Manhattan.

41
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Figura 4.2: Exemplo de classificação de objetos feita pelo DBSCAN com MinPts = 3
(Fonte: [Chire 2011]4 ).

Algoritmo 3: DBSCAN.
Entrada: conjunto de dados X ∈ Rn×m , um raio ε e a quantidade mínima de
vizinhos para um objeto ser considerado central MinPts
Saída : agrupamento particional de X
1 rotular cada objeto como central, borda ou ruído;
2 remover objetos rotulados como ruído;
3 ligar todos os objetos rotulados como centrais que estejam dentro de um raio
ε uns dos outros;
4 definir cada componente de objetos centrais conectados como um grupo;
5 inserir cada objeto rotulado como borda ao grupo de algum dos seus objetos
centrais associados, resolvendo empates que venham a ocorrer;

Por fim, um exemplo em Python da aplicação do algoritmo DBSCAN a um con-


junto de dados é apresentado no código a seguir.

from sklearn.cluster import dbscan


import pandas

# carregando blobs.csv sem a coluna 'label'


dados = pandas.read_csv('blobs.csv', usecols=[0, 1])

# O metodo 'dbscan' recebe como entrada um numpy.array.


# O atributo 'values' de um pandas.DataFrame retorna
# seus valores em tal formato.
dados = dados.values

# Executa o algoritmo DBSCAN.


# 'eps' eh o parametro que define o raio de cada objeto.
# 'min_samples' indica a quantidade minima de objetos
# para considerar um objeto como central.
4Aimagem original está disponível sob a licença CC-BY-SA 3.0 (https://creativecommons.
org/licenses/by-sa/3.0/).

42
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# 'metric' define a medida de distancia a ser utilizada


# e, seu valor padrao consiste na distancia euclidiana.
# O metodo retorna dois valores:
# o primeiro eh um numpy.array contendo os indices
# dos objetos classificados como centrais;
# o segundo eh um numpy.array contendo o rotulo de
# grupo de cada objeto
objetos_centrais, rotulos = dbscan(dados,
eps=1.5,
min_samples=3)

Exercício 4.5. Uma das principais desvantagens do DBSCAN decorre de o mesmo não
ser capaz de identificar corretamente um agrupamento quando as densidades variam am-
plamente de grupo para grupo [Tan et al. 2006]. Por que isso acontece? Por que não é
possível contornar tal problema ao aumentar o valor de ε ou alterar o valor de MinPts?

4.6. Validação de agrupamentos


Para avaliar quão bons são os agrupamentos encontrados, uma medida de avaliação ou
validação deve ser utilizada. As medidas ou critérios de validação podem ser internos,
externos ou relativos. A seguir são apresentadas os principais critérios de validação utili-
zados para avaliar agrupamentos gerados por algoritmos de agrupamento de dados.

4.6.1. Critérios de validação relativa


"Qual dentre um conjunto de soluções de agrupamento melhor representa os dados?".
Esta é a pergunta que deve ser respondida por um critério de validação relativa de uma
maneira quantitativa [Jain e Dubes 1988]. Entretanto, é importante observar, conforme
será apresentado a seguir, que, assim como algoritmos de agrupamento, diferentes índi-
ces de validação relativa possuem diferentes suposições e viéses. Desse modo, cabe aos
envolvidos no processo de mineração de dados optarem por aqueles mais adequados na
etapa de validação.

4.6.1.1. Índice de Dunn (ID)

O ID é um critério de validação relativa baseado na ideia de compactação intra-grupo


e separação inter-grupos [Vendramin et al. 2010]. Em outras palavras, ele é apropriado
para identificar agrupamentos que contém grupos cujos objetos estão próximos entre si
e distantes de objetos contidos em outros grupos. O ID pode ser formalmente definido
como: ( )
d(Ci ,C j )
Dunn(C) = min , (11)
1 ≤ i, j ≤ k max {D(Cl )}
i6= j 1≤l≤k

sendo d(Ci ,C j ) e D(Cl ) definidos, respectivamente:


d(Ci ,C j ) = min d(xa , xb ), (12)
xa ∈Ci
xb ∈C j

43
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

D(Cl ) = max d(xa , xb ). (13)


xa ,xb ∈Cl

Portanto, valores altos dessa medida indicam soluções que obedecem seu viés. Ademais,
ao generalizar os cálculos de d(Ci ,C j ) e D(Cl ), variantes dessa medida podem ser geradas
[Vendramin et al. 2010]. Dentre elas, 17 são descritas em [Bezdek e Pal 1998].
A implementação do ID não está disponível nas bibliotecas utilizadas neste mate-
rial. Ela será exigida como exercício na Seção 4.7.

4.6.1.2. Largura de silhueta (LS)

Assim como o ID, a LS baseia-se nos conceitos de compactação intra-grupo e separação


inter-grupos. A silhueta de um objeto individual xi pertencente a um grupo Cl (S(xi )) é
definida por:
b(xi ) − a(xi )
S(xi ) = (14)
max{a(xi ), b(xi )}
sendo a(xi ) e b(xi ) calculados como:
1
a(xi ) = d(xi , x j ), (15)
|Cl | − 1 x j∑
∈ Cl

( )
1
b(xi ) = min d(xi , x j ) . (16)
1≤h≤k |Ch | x j∑
∈ Ch
h 6= l

Com isso, a LS é definida como a silhueta média entre todos os objetos do conjunto de
dados. Ou seja:

1 n
LS(C) = ∑ S(xi ). (17)
n i=1

A LS assume valores no intervalo [−1, 1]. A melhor partição possível segundo


esse critério é aquela que atinge LS(C) = 1.
Um exemplo da LS em Python é apresentado a seguir.

from sklearn.cluster import k_means


from sklearn.metrics import silhouette_score
import pandas

# carregando blobs.csv sem a coluna 'label'


dados = pandas.read_csv('blobs.csv', usecols=[0, 1])
dados = dados.values

# Executando o algoritmo k-means.


centroides, rotulos, sse = k_means(dados,

44
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

n_clusters=3,
init='random',
n_init=100)

# Calculando a largura de silhueta.


# O primeiro parametro eh o conjunto de dados estudado.
# O segundo engloba os rotulos encontrados por um algoritmo.
# O parametro 'metric' indica a medida de distancia
# utilizada. No caso do algoritmo k-means, sera informado
# 'sqeuclidean' que eh a distancia euclidiana ao
# quadrado.
s = silhouette_score(dados, rotulos, metric='sqeuclidean')

Exercício 4.6.1. Os dois índices relativos apresentados nesta parte não são adequados
para validar o algoritmo DBSCAN. Por que?
Para auxiliar na resposta, aplique o algoritmo DBSCAN no conjunto de dados
moons.csv, fornecido como material suplementar (observe que pode ser necessário
ajustar o valor do parâmetro ε para se obter um bom resultado). Gere um scatter plot para
a base de dados com cores para seus rótulos originais e um scatter plot com cores para
os rótulos encontrados pelo DBSCAN. Calcule o valor o ID e o LS para a solução gerada
pelo DBSCAN. Compare os scatter plots com os valores dos índices.

4.6.2. Critérios de validação interna


Índices de validação interna medem o grau em que uma solução de agrupamento é justi-
ficada com base apenas no conjunto de dados original ou em uma matriz de similaridades
ou dissimilaridades calculadas a partir do mesmo [Jain e Dubes 1988]. Assim, um índice
de validação interna pode ser visto como o grau de concordância entre um agrupamento
encontrado por um algoritmo de agrupamento e o próprio conjunto de dados.
Muitas vezes, índices de validação interna são utilizados como função objetivo a
ser otimizada por algoritmos de agrupamento. Como exemplo, tem-se a relação entre o
SSE (Equação 9) e o k-means.

Exercício 4.6.2. Em muitas aplicações, o SSE é utilizado como critério de qualidade na


seleção de uma dentre várias soluções de agrupamento disponíveis. Desse modo, aquela
com valor mínimo de SSE é normalmente escolhida. Com base nisso, responda:

a Para quais formatos geométricos de grupos existentes em um conjunto de dados


esse critério é adequado?

b O SSE normalmente não é adequado para escolher uma dentre várias soluções com
diferentes números de grupos. Por que? (Dica: aplique o k-means em diversos
conjuntos de dados, por exemplo blobs.csv e Iris, considerando vários valores
para o números de grupos e observe o comportamento do SSE).

45
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

4.6.3. Critérios de validação externa


Critérios de validação externa avaliam o grau de concordância entre duas soluções de
agrupamento de dados [Jain e Dubes 1988]. Em várias aplicações práticas, uma das par-
tições comparadas consistirá em uma solução obtida por algum algoritmo, denotada por
Cob = {C1ob , · · · ,Ckob }, enquanto que a partição restante representará uma solução de refe-
re f re f
rência para o conjunto de dados estudado, denotada por Cre f = {C1 , · · · ,Cq }.
Vários dos critérios de similaridade para a comparação de partições baseiam-se
na contagem de pares de objetos. Assim, nesse tipo de abordagem, as seguintes variáveis
são definidas:

• a11 : quantidade de pares de objetos em um mesmo grupo em Cob e Cre f ;

• a10 : quantidade de pares de objetos em um mesmo grupo em Cob mas em grupos


diferentes em Cre f ;

• a01 : quantidade de pares de objetos em grupos diferentes em Cob mas em um


mesmo grupo em Cre f ;

• a00 : quantidade de pares de objetos pertencentes a grupos diferentes tanto em Cob


quanto em Cre f .

A seguir, cinco dos índices de validação externa mais conhecidos para avaliação
de agrupamentos particionais exclusivos serão apresentados. O respectivo código será
apresentado ao final desta seção.

4.6.3.1. Índice Rand (IR)

O critério IR calcula a proporção de acordos no agrupamento de pares de objetos entre


duas partições (a11 e a00 ) em relação ao total de pares possíveis de objetos. Ou seja:

a11 + a00 a11 + a00


IR(Cob ,Cre f ) = = n . (18)
a11 + a10 + a01 + a11 2

Esta medida retorna valores no intervalo [0, 1], com valores mais altos indicando
uma maior similaridade entre duas partições.

4.6.3.2. Índice Jaccard (IJ)

O critério IJ calcula a proporção de pares de objetos agrupados conjuntamente em Cob


e Cre f em relação à quantidade de pares de objetos em um mesmo grupo em Cob ou em
Cre f . Ou seja:

a11
IJ(Cob ,Cre f ) = . (19)
a11 + a01 + a10

46
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Assim como o IR, o IJ está contido no intervalo [0, 1] com valores mais altos
apontando uma maior concordância entre Cob e Cre f .

4.6.3.3. Índice Rand Ajustado (IRA)

Um dos problemas do IR tradicional advém da dificuldade em determinar "quão bom"ou


"quão alto"é um valor obtido na comparação de duas partições. Essa dificuldade existe
pois, ao comparar duas partições geradas aleatoriamente, valores inesperadamente altos
podem ocorrer. Em tais situações, torna-se necessário ajustar o índice para aleatoriadade.
Desse modo, a versão ajustada do IR obedece a definição a seguir:
IR(Cob ,Cre f ) − E[IR(Cob ,Cre f )]
IRA(Cob ,Cre f ) = , (20)
max{IR} − E[IR(Cob ,Cre f )]
onde E[IR(Cob ,Cre f )] indica o valor esperado do IR ao comparar as partições Cob e Cre f
e max{IR} indica o valor máximo atingido por essa medida (ou seja, max{IR} = 1).
Critérios de validação externa, quando ajustados para aleatoriedade, assumem va-
lores no intervalo (−∞, 1]. Assim, valores positivos indicam que a similaridade entre Cob
e Cre f é maior do que o valor esperado ao comparar agrupamentos gerados aleatoriamente
[Horta e Campello 2015]. Em [Hubert e Arabie 1985], o IRA é proposto assumindo uma
distribuição hipergeométrica como modelo nulo. O cálculo do IRA é dado por:

ob re f
a − (a+c)(a+b)
a+b+c+d
IRA(C ,C )= (a+c)+(a+b)
. (21)
2 − (a+c)(a+b)
a+b+c+d

4.6.3.4. Informação Mútua (IM)

A medida IM mede a informação compartilhada entre Cob e Cre f . Em outras palavras,


essa medida quantifica a quantidade de informação sobre Cre f obtida através de Cob e
vice-versa. A IM é calculada como:
|Cob | |Cre f |  
ob re f P(i, j)
IM(C ,C ) = ∑ ∑ P(i, j) log (22)
i=1 j=1 Pob (i) Pre f ( j)

sendo P(i, j), Pob (i) e Pre f ( j) definidos, respectivamente, por:

re f
|Ciob ∩C j |
P(i, j) = , (23)
n

|Ciob |
Pob (i) = , (24)
n

re f
|C j |
Pre f ( j) = . (25)
n

47
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

O valor mínimo para a IM é igual a zero, sendo que valores mais altos indicam uma
melhor concordância entre as partições comparadas. Uma das principais desvatangens
dessa medida é a ausência de um limitante superior. Para fins comparativos, uma versão
normalizada da mesma para o intervalo [0, 1] é mais adequada [Strehl e Ghosh 2002].
Uma das variações mais conhecidas (IMN) é apresentada na próxima subseção.

4.6.3.5. Informação Mútua Normalizada (IMN)

Uma das normalizações mais conhecidas da IM pode ser definida como:

IM(Cob ,Cre f )
IMN(Cob ,Cre f ) = p , (26)
H(Cob ) H(Cre f )

onde H(·) representa a entropia de um agrupamento, calculada por meio da fórmula a


seguir:
|C|
H(C) = − ∑ P(i) log(P(i)). (27)
i=1

Desse modo, a IMN está contida no intervalo [0, 1], com valores mais altos apontando
uma maior similaridade entre as partições comparadas.

4.6.3.6. Exemplos

A seguir é apresentado um exemplo de código em Python para o cálculo do IRA, IM e


IMN. Os critérios IR e o IJ não estão disponíveis nas bibliotecas utilizadas neste material.
Portanto, as respectivas implementações serão exigidas como exercício na Seção 4.7.

from sklearn.cluster import k_means


from sklearn.metrics import adjusted_rand_score
from sklearn.metrics import mutual_info_score
from sklearn.metrics import normalized_mutual_info_score
import pandas

# carregando blobs.csv sem a coluna 'label'


dados = pandas.read_csv('blobs.csv', usecols=[0, 1])
dados = dados.values

# Executando o algoritmo k-means.


centroides, rotulos_kmeans, sse = k_means(dados,
n_clusters=3,
init='random',
n_init=100)

# Calculando o IRA.
ira = adjusted_rand_score(rotulos_dados, rotulos_kmeans)

48
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

# Calculando o IM.
im = mutual_info_score(rotulos_dados, rotulos_kmeans)

# Calculando o IMN.
imn = normalized_mutual_info_score(rotulos_dados,
rotulos_kmeans)

Exercício 4.6.3. O IR possui um forte viés para a comparação de partições contendo


maiores números de grupos. Para tal caso, um dos termos irá dominar o valor calculado
para a medida. Responda:

a Qual é o termo que domina o valor do IR? Por que isso acontece?

b Em qual cenário o valor de IR será igual a zero?

4.7. Exercícios
1. Uma extensão direta do algoritmo k-means, capaz de gerar, divisivamente, uma
hierarquia de grupos, é conhecida por bisecting k-means. Tal extensão é descrita na
Seção 8.2.3 do Capítulo 8 do livro de [Tan et al. 2006]5 . Implemente o bisecting
k-means, de modo que retorne a hierarquia de grupos produzida, e aplique-o ao
conjunto de dados Iris.

2. Implemente o ID, o IR e o IJ. Aplique o k-means para os conjuntos blobs.csv,


moons.csv e Iris. Calcule os valores dos índices para os resultados do k-means.

Referências
[Bezdek e Pal 1998] Bezdek, J. C. e Pal, N. R. (1998). Some new indexes of cluster
validity. IEEE Transactions on Systems, Man, and Cybernetics, Part B (Cybernetics),
28(3):301–315.

[Chire 2011] Chire (2011). DBSCAN Illustration. https://commons.


wikimedia.org/wiki/File:DBSCAN-Illustration.svg. Acesso em:
05 set. 2017.

[Drineas et al. 2004] Drineas, P., Frieze, A., Kannan, R., Vempala, S., e Vinay, V. (2004).
Clustering large graphs via the singular value decomposition. Machine learning,
56(1):9–33.

[Ester et al. 1996] Ester, M., Kriegel, H.-P., Sander, J., Xu, X., et al. (1996). A density-
based algorithm for discovering clusters in large spatial databases with noise. In Kdd,
volume 96, pages 226–231.
5 Este
capítulo está disponível gratuitamente em http://www-users.cs.umn.edu/~kumar/
dmbook/ch8.pdf.

49
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

[Everitt et al. 2001] Everitt, B., Landau, S., Leese, M., e Stahl, D. (2001). Cluster analy-
sis. 2001. Arnold, London.

[Faceli et al. 2011] Faceli, K., Lorena, A. C., Gama, J., e Carvalho, A. C. P. L. F. (2011).
Inteligência artificial: Uma abordagem de aprendizado de máquina. Rio de Janeiro:
LTC, 2:192.

[Horta e Campello 2015] Horta, D. e Campello, R. J. G. B. (2015). Comparing hard and


overlapping clusterings. Journal of Machine Learning Research, 16:2949–2997.

[Hubert e Arabie 1985] Hubert, L. e Arabie, P. (1985). Comparing partitions. Journal of


classification, 2(1):193–218.

[Jain 2010] Jain, A. K. (2010). Data clustering: 50 years beyond k-means. Pattern
recognition letters, 31(8):651–666.

[Jain e Dubes 1988] Jain, A. K. e Dubes, R. C. (1988). Algorithms for clustering data.
Prentice-Hall, Inc.

[Strehl e Ghosh 2002] Strehl, A. e Ghosh, J. (2002). Cluster ensembles—a knowledge


reuse framework for combining multiple partitions. Journal of machine learning rese-
arch, 3(Dec):583–617.

[Tan et al. 2006] Tan, P.-N. et al. (2006). Introduction to data mining. Pearson Education
India.

[Vendramin et al. 2010] Vendramin, L., Campello, R. J., e Hruschka, E. R. (2010). Rela-
tive clustering validity criteria: A comparative overview. Statistical analysis and data
mining: the ASA data science journal, 3(4):209–235.

[Xu e Wunsch 2005] Xu, R. e Wunsch, D. (2005). Survey of clustering algorithms. IEEE
Transactions on neural networks, 16(3):645–678.

[Zhu e Goldberg 2009] Zhu, X. e Goldberg, A. B. (2009). Introduction to semi-


supervised learning. Synthesis lectures on artificial intelligence and machine learning,
3(1):1–130.

50
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

Capítulo

5
Classificação e regressão

Neste capítulo serão apresentados os principais conceitos relacionados a AM pre-


ditivo, que utilizam dados rotulados para a indução de modelos de classificação ou de
regressão.
O presente capítulo está organizado conforme segue. Na Seção 5.2 são apresen-
tados métodos de aprendizado supervisionado baseados em distâncias entre objetos. Na
Seção 5.3 são propostos exercícios para fixar os conceitos apresentados.

5.1. Aprendizado de máquina supervisionado


Em AM supervisionado, algoritmos são utilizados para induzir modelos preditivos por
meio da observação de um conjunto de objetos rotulados [Von Luxburg e Schölkopf 2008],
tipicamente referenciado como conjunto de treinamento. Os rótulos contidos em tal con-
junto correspondem a classes ou valores obtidos por alguma função desconhecida. Desse
modo, um algoritmo de classificação buscará produzir um classificador capaz de genera-
lizar as informações contidas no conjunto de treinamento, com a finalidade de classificar,
posteriormente, objetos cujo rótulo seja desconhecido.
Formalmente, um conjunto de dados de treinamento pode ser definido como uma
coleção de tuplas {(xi , yi )}ni=1 , onde, em cada tupla, xi indica um objeto descrito por m ca-
racterísticas e yi indica o rótulo correspondente a xi . Quando os valores de yi são definidos
por uma quantidade limitada de valores discretos, tem-se um problema de classificação.
Quando tais valores são contínuos, tem-se um problema de regressão.

5.2. Métodos baseados em distâncias entre objetos


Métodos baseados em distâncias adotam a ideia de que objetos que pertencem a uma
mesma classe possuem uma relação de proximidade no espaço de atributos considerados
[Faceli et al. 2011].

5.2.1. k-Nearest Neighbors (kNN)


O algoritmo kNN é um dos algoritmos de classificação mais conhecidos e fáceis de se
implementar na literatura de aprendizado de máquina e mineração de dados. Sua ideia
consiste em, dado um objeto desconhecido, procurar pelos k vizinhos mais próximos a
ele em um conjunto de dados previamente conhecido, segundo uma medida de distância
pré-estabelecida. A classe do novo objeto será assumida como o voto majoritário entre os

18
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

seus k vizinhos.
Devido à sua simplicidade, o kNN possui ampla utilização na solução de diver-
sos problemas do mundo real. Os pseudo-códigos para treinamento e teste do kNN são
apresentados, respectivamente, nos Algoritmos 1 e 2.
Algoritmo 1: Treinamento kNN.
Entrada: conjunto de treinamento T = {xi , yi }ni=1 , valor de k e uma medida
de distância d(·, ·)
Saída : classificador kNN
1 armazenar o conjunto de treinamento e o valor de k;

Algoritmo 2: Teste kNN.


Entrada: classificador kNN e um objeto x cuja classe é desconhecida
Saída : classe y atribuída a x
1 buscar pelos k objetos mais próximos a x no conjunto de dados de
treinamento do classificador kNN informado;
2 dentre os k vizinhos, determinar y como a classe mais frequente entre eles,
resolvendo possíveis empates de maneira arbitrária;

Em Python, pode-se executar o kNN conforme o código que pode ser visto a
seguir.

import pandas
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

# carrega a base de dados iris


dados = pandas.read_csv('iris.csv')

# para criar um classificador, precisaremos separar


# o DataFrame original em dois numpy.arrays:
# - O primeiro deles, bidimensional, contendo os objetos
# e os atributos;
# - O segundo deles, unidimensional, contendo apenas as
# classes representadas por valores inteiros;

# Para obter os objetos e seus atributos, procede-se


# conforme abaixo
colunas = dados.columns.drop('Name')

# obtem numpy.array bidimensional


X = dados[colunas].values

# Para obter as classes como inteiros, utilizamos


# a classe LabelEncoder da scikit-learn

19
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

le = LabelEncoder()
y = le.fit_transform(dados['Name'])

# train_test_split separa o conjunto de dados original


# aleatoriamente em treinamento e teste
# train_size indica a proporcao de objetos presentes
# no conjunto de treinamento (neste caso 70% dos objetos)
# caso deseje-se uma separacao estratificada, deve-se
# informar um parametro adicional stratify=y
X_treino, X_teste, y_treino, y_teste = \
train_test_split(X, y, train_size=0.7, test_size=0.3)

# instancia um knn
# n_neighbors indica a quantidade de vizinhos
# metric indica a medida de distancia utilizada
knn = KNeighborsClassifier(n_neighbors=5,
metric='euclidean')

# treina o knn
knn.fit(X_treino, y_treino)

# testa o knn com X_teste


# y_pred consistira em um numpy.array onde
# cada posicao contem a classe predita pelo knn
# para o respectivo objeto em X_teste
y_pred = knn.predict(X_teste)

Exercício 5.2.1. Discuta sobre os possíveis problemas que podem ocorrer com o kNN
nos cenários a seguir:

a Quando a escala e intervalo de valores entre os atributos é diferente. Por exemplo,


ao considerar atributos que indicam alguma medição em centímetros ou em metros.
b Quando a dimensionalidade dos objetos é alta1 . Por exemplo, o que pode ocorrer
ao buscar pelos k vizinhos mais próximos quando a base de dados possui 50, 100,
200, 300 ou mais atributos?

5.3. Exercícios
1. O kNN pode também ser utilizado para problemas de regressão local [Mitchell 1997,
Faceli et al. 2011]. Com a scikit-learn esse tipo de tarefa pode ser realizada por
meio da classe KNeighborsRegressor2 . Escreva um código que treine um
classificador kNN para regressão considerando os requisitos a seguir:
1 Este é um problema, em aprendizado de máquina, conhecido como "maldição da dimensionalidade"
2 http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.

KNeighborsRegressor.html

20
Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python

• Para o treinamento utilize o arquivo regressao_treino.csv.


• Para o teste, utilize o arquivo regressao_teste.csv.
• Como medida de distância, considere a distância euclidiana.
• Teste o kNN com ponderação uniforme entre seus vizinhos (ou seja, consi-
derando weights=’uniform’) e pelo inverso das distâncias (isto é, utili-
zando weights=’distance’).
• Teste os seguintes valores para k: 3, 5, 10, 20.

Gere um scatter plot para os dados de treinamento utilizando como eixo horizontal
os valores dos exemplos e como eixo vertical os rótulos. Gere um line plot3 para
cada resultado das possíveis combinações entre ponderação e valor de k. Responda:

a Qual das duas ponderações parece ser mais adequada?


b Note que existem alguns pontos ruidosos no treinamento. Qual tipo de pon-
deração é mais influenciado por eles? Por que?
c Ao analisar os gráficos obtidos, responda qual função matemática gerou os
dados.

Referências
[Faceli et al. 2011] Faceli, K., Lorena, A. C., Gama, J., e Carvalho, A. C. P. L. F. (2011).
Inteligência artificial: Uma abordagem de aprendizado de máquina. Rio de Janeiro:
LTC, 2:192.

[Mitchell 1997] Mitchell, T. M. (1997). Machine learning. 1997. Burr Ridge, IL: Mc-
Graw Hill, 45(37):870–877.

[Von Luxburg e Schölkopf 2008] Von Luxburg, U. e Schölkopf, B. (2008). Statistical


learning theory: models, concepts, and results. arXiv preprint arXiv:0810.4752.

3 https://matplotlib.org/users/pyplot_tutorial.html

21
Predictive model selection and evaluation
First of all, we do all necessary imports and load the Breast Cancer Wisconsin Diagnostic dataset.

In [1]:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.preprocessing import LabelEncoder


from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, zero_one_loss
from sklearn.metrics import roc_curve, auc
from sklearn.datasets import load_breast_cancer

from scipy.stats import wilcoxon, friedmanchisquare, rankdata


from Orange.evaluation import compute_CD, graph_ranks

DEFAULT_N_NEIGHBORS = 5

# Random seed. This is needed to make all results reproducible.


seed = 10

# Loading Breast Cancer dataset


breast_cancer = pd.read_csv('data/breast_cancer.csv', index_col=0)
labels = 'diagnosis'
breast_cancer.head()

Out[1]:

mean_radius mean_texture mean_perimeter mean_area mean_smoothness mean_compactness mean_concavity

sample_id

842302 17.99 10.38 122.80 1001.0 0.11840 0.27760 0.3001

842517 20.57 17.77 132.90 1326.0 0.08474 0.07864 0.0869

84300903 19.69 21.25 130.00 1203.0 0.10960 0.15990 0.1974

84348301 11.42 20.38 77.58 386.1 0.14250 0.28390 0.2414

84358402 20.29 14.34 135.10 1297.0 0.10030 0.13280 0.1980

5 rows × 31 columns

Then, we encode the 'diagnosis' str values as int values.

In [16]:

le = LabelEncoder()
breast_cancer[labels] = le.fit_transform(breast_cancer[labels])

Then, we write a simple method to train, test and return a kNN classifier, its predicted results, its accuracy score and its 0-1 loss for a
dataset.

In [3]:

def knn_fit_predict_evaluate(X_train, X_test, y_train, y_test, k=DEFAULT_N_NEIGHBORS):


knn = KNeighborsClassifier(n_neighbors=k, weights='distance', metric='euclidean')
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
loss = zero_one_loss(y_test, y_pred)
return knn, y_pred, accuracy, loss

To train and test sklearn classifiers, we will need the data as numpy arrays. So, we can extract them using the code below.
In [4]:

np.set_printoptions(precision=4, suppress=True)

# Extracting Breast Cancer values without the label column.


X = breast_cancer.drop(labels, axis=1).values
print('X:\n{}\n'.format(X))

# Extracting Breast Cancer labels.


y = breast_cancer[labels].values
print('y:\n{}\n'.format(y))

X:
[[ 17.99 10.38 122.8 ..., 0.2654 0.4601 0.1189]
[ 20.57 17.77 132.9 ..., 0.186 0.275 0.089 ]
[ 19.69 21.25 130. ..., 0.243 0.3613 0.0876]
...,
[ 16.6 28.08 108.3 ..., 0.1418 0.2218 0.0782]
[ 20.6 29.33 140.1 ..., 0.265 0.4087 0.124 ]
[ 7.76 24.54 47.92 ..., 0. 0.2871 0.0704]]

y:
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
0 1 1 1 1 1 1 1 1 0 1 0 0 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1
0 1 0 1 1 0 0 0 1 1 0 1 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 0 0 1 0 0 1 0 0
0 0 0 0 0 0 1 1 1 0 1 1 0 0 0 1 1 0 1 0 1 1 0 1 1 0 0 1 0 0 1 0 0 0 0 1 0
0 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 1 1 1 0 1
0 1 0 0 0 1 0 0 1 1 0 1 1 1 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 1 0 0 1 1 0 0
0 1 0 0 0 0 0 1 1 0 0 1 0 0 1 1 0 1 0 0 0 0 1 0 0 0 0 0 1 0 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0 0 0 0 0 0 1 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 1 1 0 0
0 0 1 0 1 0 1 0 0 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1
1 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 1 0 0
1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0 1 0 1 1
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 1 0]

Simple holdout

The simple holdout method consists of splitting the original dataset in two disjoint subsets:

train: which contains a proportion of p objects from the original dataset;


test: which contains a proportion of 1 − p objects from the original dataset.

The aforementioned split can be performed with the train_test_split method.

In [5]:
# Splitting the original dataset.
# The train subset will contain a proportion of 0.66 objects from the original dataset.
# The test subset will contain a proportion of 0.34 objects from the original dataset.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

# Training, testing and evaluating a kNN classifier with simple holdout.


knn, y_pred, accuracy, loss = knn_fit_predict_evaluate(X_train, X_test, y_train, y_test)
print('Accuracy: {}'.format(accuracy))
print('0-1 loss: {}'.format(loss))

# Since the accuracy and 0-1 loss are complementary measures, their sum must be 1.0.
print('Accuracy + 0-1 loss = {}'.format(accuracy + loss))

Accuracy: 0.9226804123711341
0-1 loss: 0.07731958762886593
Accuracy + 0-1 loss = 1.0

K-fold cross-validation

K-fold cross-validation consists of splitting the original dataset in k disjoint subsets of approximately equal size. Then, at each iteration, k − 1
subsets are used as the training set and the remaining subset is used as the test set. In the end, we can calculate the mean accuracy as
the performance measure.

Sklearn provides several different classes to perform k-fold cross-validation. In this example, we use the StratifiedKFold class, since it breaks
the original dataset in k stratified disjoint subsets. So, each subset will maintain the same proportion of objects in each class as in the original
dataset.
In [6]:

splits = 10
skfold = StratifiedKFold(n_splits=splits, random_state=seed)
trained_knns = []
accuracies = []

# skfold.split(X, y) returns an iterator over tuples.


# In each tuple, the first element consists of the indices of examples from the train set.
# The second element consists of the indices of examples from the test set.
for train_idx, test_idx in skfold.split(X, y):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
knn, y_pred, acc, loss = knn_fit_predict_evaluate(X_train, X_test, y_train, y_test)
trained_knns.append(knn)
accuracies.append(acc)

accuracies = np.array(accuracies)
print('{}-fold cross-validation accuracy mean: {}'.format(splits, np.mean(accuracies)))
print('{}-fold cross-validation accuracy std: {}'.format(splits, np.std(accuracies, ddof=1)))

10-fold cross-validation accuracy mean: 0.9298429262812202


10-fold cross-validation accuracy std: 0.02691043470531831

ROC analysis

For a binary classification problem, where we have a positive ( + ) and a negative ( − ) class, we can obtain the confusion matrix of the
expected and predicted results. The confusion matrix is organized as:

Predicted + Predicted −

Expected + TP FN

Expected − FP TN

From the above matrix, we can extract the following quantities:

True Positives (TP): the number of positive examples that were correctly predicted as positive;
True Negatives (TN): the number of negative examples that were correctly predicted as negative;
False Negatives (FN): the number of positive examples that were wrongly predicted as negative;
False Positives (FP): the number of negative examples that were wrongly predicted as positive.

Then, we can obtain two measures:

True Positive Rate (TPR): also known as sensibility. It measures the hit rate for the positive class. It is calculated as:
TP
TPR = ;
TP + FN

False Positive Rate (FPR): also known as specificity. It measures the hit rate for the negative class. It is calculated as:
FP
FPR = .
TN + FP

Many classifiers output scores (or probabilities) when classifying an unseen example. These scores are usually thresholded in order to
return a binary classification.

The ROC analysis consists of using several thresholds for the output scores of a classifier. Then, for each threshold, the respective TPR and
FPR values can be calculated. By plotting the obtained (TPR, FPR) pairs, we obtain the ROC curve.

Finally, a commonly used measure to compare classifiers is the area under the ROC curve (ROC AUC). Such a measure lies between 0 and 1,
with values close to 1 indicating better results.

We present an example below.


In [7]:

# Splitting X and y in train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

# Creating and training a kNN classifier.


knn = KNeighborsClassifier(n_neighbors=DEFAULT_N_NEIGHBORS, weights='distance', metric='euclidean')
knn.fit(X_train, y_train)

# Predicting probability scores for the test set.


y_prob = knn.predict_proba(X_test)

# Calculatting False Positive Rate and True Positive Rate values for different thresholds.
# The first parameter consists of the expected labels.
# The second parameter consists of the predicted scores for the positive class. In this example
# the positive class is assumed to be the one with label = 1.
false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, y_prob[:, 1])

# Calculating the area under the ROC curve.


roc_auc = auc(false_positive_rate, true_positive_rate)

Finally, we plot the ROC curve. The diagonal line of such a plot indicates a classifier with random predictions.

In [8]:

# setting linewidth = 2
lw = 2

plt.plot(false_positive_rate,
true_positive_rate,
color='blue',
lw=lw,
label='ROC curve (area = {:.4f})'.format(roc_auc))

plt.plot([0, 1], [0, 1], color='red', lw=lw, linestyle='--', label='Random classifier')


plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC')
plt.legend(loc="lower right")

plt.show()

Hypothesis testing

Comparing two classifiers over multiple datasets

Usually, when comparing two classifiers (namely, f1 and f 2 ), the null-hypothesis (H 0 ) states that their performances are equivalent. For this
situation, Demšar (2006) recommends the Wilcoxon signed-rank test.

Next, we present an example extracted from (Demšar, 2006). In such an example, we have the area under the curve (AUC) for the C4.5
algorithm with the parameter m (the minimal number of examples in a leaf) equal to 0 and C4.5 with tunned m (C4.5+m) considering 14
datasets.
In [9]:

# Loading the example DataFrame.


performances = pd.read_csv('data/example_wilcoxon_demsar.csv')
performances

Out[9]:

dataset C4.5 C4.5+m

0 adult(sample) 0.763 0.768

1 breast_cancer 0.599 0.591

2 breast_cancer_wisconsin 0.954 0.971

3 cmc 0.628 0.661

4 ionosphere 0.882 0.888

5 iris 0.936 0.931

6 liver_disorders 0.661 0.668

7 lung_cancer 0.583 0.583

8 lymphography 0.775 0.838

9 mushroom 1.000 1.000

10 primary_tumor 0.940 0.962

11 rheum 0.619 0.666

12 voting 0.972 0.981

13 wine 0.957 0.978

In [10]:

# Getting C4.5 AUC values.


c45 = np.array(performances['C4.5'])

# Getting C4.5+m AUC values.


c45m = np.array(performances['C4.5+m'])

# Running Wilcoxon test. When zero_method='zsplit' the zero ranks are splitted between positive and negative ones.
wilcoxon(c45, c45m, zero_method='zsplit')

Out[10]:
WilcoxonResult(statistic=12.0, pvalue=0.010968496564224731)

The Wilcoxon signed-rank test outputs a p-value close to 0.01. If we consider a significance level (α) of 0.05 we can conclude that C4.5 and
C4.5+m performances are not equivalent.

Comparing multiple classifiers over multiple datasets

The Wilcoxon signed-rank test was not designed to compare multiple random variables. So, when comparing multiple classifiers, an
"intuitive" approach would be to apply the Wilcoxon test to all possible pairs. However, when multiple tests are conducted, some of them will
reject the null hypothesis only by chance (Demšar, 2006).

For the comparison of multiple classifiers, Demšar (2006) recommends the Friedman test.

The Friedman test ranks the algorithms from best to worst on each dataset with respect to their performances. Its null-hypothesis (H0 )
states that all algorithms are equivalent and their mean ranks are equal.

Next, we present an example extracted from (Demšar, 2006). In such an example, we have the AUC for four classifiers: C4.5 with m = 0 and
the confidence interval parameter cf = 0.25, C4.5 with tunned m, C4.5 with tunned cf and C4.5 with both parameters tunned.
In [11]:

# Loading the example DataFrame.


performances = pd.read_csv('data/example_friedman_nemenyi_demsar.csv')
performances

Out[11]:

dataset C4.5 C4.5+m C4.5+cf C4.5+m+cf

0 adult(sample) 0.763 0.768 0.771 0.798

1 breast_cancer 0.599 0.591 0.590 0.569

2 breast_cancer_wisconsin 0.954 0.971 0.968 0.967

3 cmc 0.628 0.661 0.654 0.657

4 ionosphere 0.882 0.888 0.886 0.898

5 iris 0.936 0.931 0.916 0.931

6 liver_disorders 0.661 0.668 0.609 0.685

7 lung_cancer 0.583 0.583 0.563 0.625

8 lymphography 0.775 0.838 0.866 0.875

9 mushroom 1.000 1.000 1.000 1.000

10 primary_tumor 0.940 0.962 0.965 0.962

11 rheum 0.619 0.666 0.614 0.669

12 voting 0.972 0.981 0.975 0.975

13 wine 0.957 0.978 0.946 0.970

In [12]:

# First, we extract the algorithms names.


algorithms_names = performances.drop('dataset', axis=1).columns

# Then, we extract the performances as a numpy.ndarray.


performances_array = performances[algorithms_names].values

# Finally, we apply the Friedman test.


friedmanchisquare(*performances_array)

Out[12]:
FriedmanchisquareResult(statistic=51.285714285714278, pvalue=1.7912382226666844e-06)

The Friedman test outputs a very small p-value. For many significance levels (α) we can conclude that the performances of all algorithms are
not equivalent.

Considering that the null-hypothesis was rejected, we usually have two scenarios for a post-hoc test (Demšar, 2006):

All classifiers are compared to each other. In this case we apply the Nemenyi post-hoc test.
All classifiers are compared to a control classifier. In this scenario we apply the Bonferroni-Dunn post-hoc test.

To perform both of the aformentioned post-hoc tests, we need the average rank of each algorithm,

In [13]:
# Calculating the ranks of the algorithms for each dataset. The value of p is multipled by -1
# because the rankdata method ranks from the smallest to the greatest performance values.
# Since we are considering AUC as our performance measure, we want larger values to be best ranked.
ranks = np.array([rankdata(-p) for p in performances_array])

# Calculating the average ranks.


average_ranks = np.mean(ranks, axis=0)

print('\n'.join('{} average rank: {}'.format(a, r) for a, r in zip(algorithms_names, average_ranks)))

C4.5 average rank: 3.142857142857143


C4.5+m average rank: 2.0
C4.5+cf average rank: 2.9285714285714284
C4.5+m+cf average rank: 1.9285714285714286

Then, we will calculate the critical differences and plot the results of each test (Nemenyi and Bonferroni-Dunn).
In [14]:

# This method computes the critical difference for Nemenyi test with alpha=0.1.
# For some reason, this method only accepts alpha='0.05' or alpha='0.1'.
cd = compute_CD(average_ranks,
n=len(performances),
alpha='0.1',
test='nemenyi')

# This method generates the plot.


graph_ranks(average_ranks,
names=algorithms_names,
cd=cd,
width=10,
textspace=1.5,
reverse=True)

plt.show()

In [15]:

# This method computes the critical difference for Bonferroni-Dunn test with alpha=0.05.
# For some reason, this method only accepts alpha='0.05' or alpha='0.1'.
cd = compute_CD(average_ranks,
n=len(performances),
alpha='0.05',
test='bonferroni-dunn')

# This method generates the plot.


graph_ranks(average_ranks,
names=algorithms_names,
cd=cd,
cdmethod=0,
width=10,
textspace=1.5,
reverse=True)

plt.show()

References
Demšar, J. (2006). Statistical comparisons of classifiers over multiple data sets. Journal of Machine learning research, 7, 1-30.
Naive Bayes
Naive Bayes is a very simple but powerful classification method. For a given object x, Naive Bayes calculates x's probability to belong to each
class yi (i = 1, ⋯, k), using the Bayes' theorem:

P(y i) P (x 1 , ⋯, xm | yi)
P (y i | x) = .
P (x1 , ⋯, x m)

Additionally, it assumes that the features are independent from each other (which is the reason why it is called naive):

P(x1 , ⋯, x m | y i) = ∏m
j=1
P (xj | y i).

So, we obtain:

P (y i) ∏ m
j=1
P (x j | y i)
P(yi | x) = .
P (x 1 , ⋯, xm)

For a given object x, Naive Bayes will output the the Maximum a Posteriori (MAP) estimate:
m
P (y) ∏ j = 1 P (x j | y)
ŷ = arg maxy P (y | x) = arg max y .
P (x1 , ⋯, x m)

Note that P (x 1 , ⋯, x m) is constant. Thus, we can drop it to obtain:

m
ŷ = arg maxy P (yi) ∏j = 1 P (xj | y).

Practical example

First of all, we do all the necessary imports and load the Mushroom dataset.

In [1]:

import pandas as pd
import matplotlib.pyplot as plt

from sklearn.preprocessing import LabelEncoder


from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import BernoulliNB
from sklearn.metrics import accuracy_score, roc_curve, auc

# setting random seed


seed = 10

data = pd.read_csv('data/mushroom.csv')

# We drop the 'stalk-root' feature because it is the only one containing missing values.
data = data.drop('stalk-root', axis=1)
data.head()

Out[1]:

stalk- stalk-
cap- cap- cap- gill- gill- gill- gill- stalk- color- color- veil- veil- ring- ring-
bruises? odor ...
shape surface color attachment spacing size color shape above- below- type color number type
ring ring

0 x s n t p f c n k e ... w w p w o p

1 x s y t a f c b k e ... w w p w o p

2 b s w t l f c b n e ... w w p w o p

3 x y w t p f c n n e ... w w p w o p

4 x s g f n f w b k t ... w w p w o e

5 rows × 22 columns
Unfortunately, scikit-learn does not implement the classical Naive Bayes algorithm which calculates the conditional probabilities P(xj | y i) as the
proportion of objects from class yi that assume each particular categorical value for feature j. However, scikit-learn contains the BernoulliNB
class which assumes that data is distributed according to multivariate Bernoulli distributions.

So, for the Mushroom dataset, we can transform each categorical feature to dummy variables. Note that such a conversion clearly
violates the indepence assumption between features. However, Naive Bayes has been proven to achieve good performance in
several applications where indepence is violated (for example, in text classication).

In [2]:

# Creating a new DataFrame representation for each feature as dummy variables.


dummies = [pd.get_dummies(data[c]) for c in data.drop('label', axis=1).columns]

# Concatenating all DataFrames containing dummy variables.


binary_data = pd.concat(dummies, axis=1)

# Getting binary_data as a numpy.array.


X = binary_data.values

# Getting the labels.


le = LabelEncoder()
y = le.fit_transform(data['label'].values)

# Splitting the binary dataset into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

# Creates a BernoulliNB. binarize=None indicates that there is no need to binarize the input data.
nb = BernoulliNB(binarize=None)
nb.fit(X_train, y_train)
y_pred = nb.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print('BernoulliNB accuracy score: {}'.format(accuracy))

BernoulliNB accuracy score: 0.9464350343829171

In [3]:

# Getting the probabilities for each class.


y_prob = nb.predict_proba(X_test)

# Calculating ROC curve and ROC AUC.


false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, y_prob[:, 1])
roc_auc = auc(false_positive_rate, true_positive_rate)

# Plotting ROC curve.


lw = 2
plt.plot(false_positive_rate, true_positive_rate, color='blue', lw=lw, label='ROC curve (area = {:.4f})'.format(roc_auc)
)
plt.plot([0, 1], [0, 1], color='red', lw=lw, linestyle='--', label='Random classifier')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC')
plt.legend(loc="lower right")

plt.show()
Decision Trees
Decision Trees are classification methods that are able to extract simple rules about the data features which are inferred from the input
dataset. Several algorithms for decision tree induction are available in the literature. Scikit-learn contains the implementation of the CART
(Classification and Regression Trees) induction algorithm.

Practical examples
Fist of all, we do all necessary imports.

In [1]:

import pandas as pd
import graphviz

from sklearn.preprocessing import LabelEncoder


from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# Setting random seed.


seed = 10

Dataset with continuous features

Next, we load the Iris dataset, extract its values and labels and split them into train and test sets.

In [2]:

# Loading Iris dataset.


data = pd.read_csv('data/iris.csv')

# Creating a LabelEncoder and fitting it to the dataset labels.


le = LabelEncoder()
le.fit(data['Name'].values)

# Converting dataset str labels to int labels.


y = le.transform(data['Name'].values)

# Extracting the instances data.


X = data.drop('Name', axis=1).values

# Splitting into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

Then, we will fit and test a DecisionTreeClassifier. Scikit-learn does not implement any post-prunning step. So, to avoid overfitting, we can
control the tree size with the parameters min_samples_leaf, min_samples_split and max_depth.

In [3]:
# Creating a DecisionTreeClassifier.
# The criterion parameter indicates the measure used (possible values: 'gini' for the Gini index and
# 'entropy' for the information gain).
# The min_samples_leaf parameter indicates the minimum of objects required at a leaf node.
# The min_samples_split parameter indicates the minimum number of objects required to split an internal node.
# The max_depth parameter controls the maximum tree depth. Setting this parameter to None will grow the
# tree until all leaves are pure or until all leaves contain less than min_samples_split samples.
tree = DecisionTreeClassifier(criterion='gini',
min_samples_leaf=5,
min_samples_split=5,
max_depth=None,
random_state=seed)

tree.fit(X_train, y_train)

y_pred = tree.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print('DecisionTreeClassifier accuracy score: {}'.format(accuracy))

DecisionTreeClassifier accuracy score: 0.9615384615384616

Finally, we can plot the obtained tree to visualize the rules extracted from the dataset.
In [4]:

def plot_tree(tree, dataframe, label_col, label_encoder, plot_title):


label_names = pd.unique(dataframe[label_col])

# Obtaining plot data.


graph_data = export_graphviz(tree,
feature_names=dataframe.drop(label_col, axis=1).columns,
class_names=label_names,
filled=True,
rounded=True,
out_file=None)

# Generating plot.
graph = graphviz.Source(graph_data)
graph.render(plot_title)
return graph

tree_graph = plot_tree(tree, data, 'Name', le, 'Iris')


tree_graph

Out[4]:

PetalWidth <= 0.8


gini = 0.6666
samples = 98
value = [33, 32, 33]
class = Iris-setosa
False
True

PetalLength <= 4.85


gini = 0.0
gini = 0.4999
samples = 33
samples = 65
value = [33, 0, 0]
value = [0, 32, 33]
class = Iris-setosa
class = Iris-virginica

PetalWidth <= 1.45 PetalWidth <= 1.75


gini = 0.1207 gini = 0.1609
samples = 31 samples = 34
value = [0, 29, 2] value = [0, 3, 31]
class = Iris-versicolor class = Iris-virginica

gini = 0.0 gini = 0.3457 gini = 0.4898 gini = 0.0


samples = 22 samples = 9 samples = 7 samples = 27
value = [0, 22, 0] value = [0, 7, 2] value = [0, 3, 4] value = [0, 0, 27]
class = Iris-versicolor class = Iris-versicolor class = Iris-virginica class = Iris-virginica

Dataset with categorical features

Unfortunately, the DecisionTreeClassifier class does not handle categorical features directly. So, we might consider to transform them to
dummy variables. However, this approach must be taken with a grain of salt because decision trees tend to overfit on data
with a large number of features.
In [5]:

# Loading Mushroom dataset.


data = pd.read_csv('data/mushroom.csv')

# We drop the 'stalk-root' feature because it is the only one containing missing values.
data = data.drop('stalk-root', axis=1)

# Creating a new DataFrame representation for each feature as dummy variables.


dummies = [pd.get_dummies(data[c]) for c in data.drop('label', axis=1).columns]

# Concatenating all DataFrames containing dummy variables.


binary_data = pd.concat(dummies, axis=1)

# Getting binary_data as a numpy.array.


X = binary_data.values

# Getting the labels.


le = LabelEncoder()
y = le.fit_transform(data['label'].values)

# Splitting the binary dataset into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

# Creating a DecisionTreeClassifier.
tree = DecisionTreeClassifier(criterion='gini',
min_samples_leaf=5,
min_samples_split=5,
max_depth=None,
random_state=seed)

tree.fit(X_train, y_train)

Out[5]:

DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None,


max_features=None, max_leaf_nodes=None,
min_impurity_split=1e-07, min_samples_leaf=5,
min_samples_split=5, min_weight_fraction_leaf=0.0,
presort=False, random_state=10, splitter='best')

Now, we will apply the obtained tree on the test set.

In [6]:

y_pred = tree.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print('DecisionTreeClassifier accuracy score: {}'.format(accuracy))

DecisionTreeClassifier accuracy score: 0.9992761491132827

We can observe that the above decision tree is pretty accurate.

Now, let's check its depth.

In [7]:

print('DecisionTreeClassifier max_depth: {}'.format(tree.tree_.max_depth))

DecisionTreeClassifier max_depth: 6

What if we fit a decision tree with a smaller depth?

In [8]:

# Creating a DecisionTreeClassifier.
tree = DecisionTreeClassifier(criterion='gini',
min_samples_leaf=5,
min_samples_split=5,
max_depth=3,
random_state=seed)

tree.fit(X_train, y_train)
y_pred = tree.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print('DecisionTreeClassifier accuracy score: {}'.format(accuracy))

DecisionTreeClassifier accuracy score: 0.9659790083242852


We can observe that the new tree is almost as accurate as the first one. Apparently both trees are able to handle the mushroom data pretty
well. The second three might be preferred, since it is a simpler and computationally cheaper model.

Finally, we plot the second tree.

In [9]:

# Appending 'label' column to binary DataFrame.


binary_data['label'] = data['label']

tree_graph = plot_tree(tree, binary_data, 'label', le, 'Mushroom')


tree_graph

Out[9]:

n <= 0.5
gini = 0.4994
samples = 5361
value = [2777, 2584]
class = p
True False

f <= 0.5 r <= 0.5


gini = 0.2845 gini = 0.0553
samples = 3040 samples = 2321
value = [522, 2518] value = [2255, 66]
class = e class = p

h <= 0.5 y <= 0.5


gini = 0.0 gini = 0.0
gini = 0.4826 gini = 0.0251
samples = 2160 samples = 37
samples = 880 samples = 2284
value = [0, 2160] value = [0, 37]
value = [522, 358] value = [2255, 29]
class = e class = e
class = p class = p

gini = 0.3739 gini = 0.0 gini = 0.0035 gini = 0.3893


samples = 695 samples = 185 samples = 2250 samples = 34
value = [522, 173] value = [0, 185] value = [2246, 4] value = [9, 25]
class = p class = e class = p class = e
Artificial Neural Networks

Perceptron

The Perceptron is a very simple linear binary classifier. It basically maps and input vector x to a binary output f(x).

Given a weight vector w, the Perceptron's classfication rule is: f(x) = 1 if w ⋅ x + b > 0 or f(x) = 0 otherwise. Here, b is a bias value which is
responsible for shifting the Perceptron's hyperplane away from the origin.

Practical example

First we do all necessary imports.

In [1]:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from matplotlib.colors import ListedColormap

from sklearn.preprocessing import LabelEncoder, StandardScaler


from sklearn.linear_model import Perceptron
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# Setting random seed.


seed = 10

The most simple examples for Perceptron are the basic logic operations, such as: AND, OR and XOR.

The AND operation is defined as:

x0 x1 y

0 0 0

1 0 0

0 1 0

1 1 1

Below we run the Perceptron to learn the logical AND.

In [2]:

# Setting the input samples.


X = np.array([[0, 0],
[0, 1],
[1, 0],
[1, 1]],
dtype=np.double)

# Setting the expected outputs.


y_AND = np.array([0, 0, 0, 1])

# Creating and training a Perceptron.


p = Perceptron(random_state=seed, eta0=0.1, max_iter=1000)
p.fit(X, y_AND)

# Obtaining f(x) scores.


pred_scores = p.decision_function(X)
print("Perceptron's AND scores: {}".format(pred_scores))

Perceptron's AND scores: [ -2.00000000e-01 -1.00000000e-01 -2.77555756e-17 1.00000000e-01]

Then, we plot the Perceptron's decision boundary. The colorbar to the left shows the scores achieved by w ⋅ x + b. Each point color indicates
a different class (blue = 1, red = 0).
In [3]:

# Method to plot the Perceptron's decision boundary.


# This code is based on http://scikit-learn.org/stable/auto_examples/classification/plot_classifier_comparison.html
def plot_decision_boundary(classifier, X, y, title):
xmin, xmax = np.min(X[:, 0]) - 0.05, np.max(X[:, 0]) + 0.05
ymin, ymax = np.min(X[:, 1]) - 0.05, np.max(X[:, 1]) + 0.05
step = 0.01

cm = plt.cm.coolwarm_r
#cm = plt.cm.RdBu

thr = 0.0
xx, yy = np.meshgrid(np.arange(xmin - thr, xmax + thr, step), np.arange(ymin - thr, ymax + thr, step))

if hasattr(classifier, 'decision_function'):
Z = classifier.decision_function(np.hstack((xx.ravel()[:, np.newaxis], yy.ravel()[:, np.newaxis])))
else:
Z = classifier.predict_proba(np.hstack((xx.ravel()[:, np.newaxis], yy.ravel()[:, np.newaxis])))[:, 1]

Z = Z.reshape(xx.shape)

plt.contourf(xx, yy, Z, cmap=cm, alpha=0.8)


plt.colorbar()

plt.scatter(X[:, 0], X[:, 1], c=y, cmap=ListedColormap(['#FF0000', '#0000FF']), alpha=0.6)

plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)

plt.xticks((0.0, 1.0))
plt.yticks((0.0, 1.0))

plt.title(title)

# Plotting Perceptron decision boundary.


# The colorbar shows the scores obtained for f(x).
plot_decision_boundary(p, X, y_AND, 'AND decision boundary')
plt.show()

The OR operation is defined as:

x0 x1 y

0 0 0

1 0 1

0 1 1

1 1 1

Below we run the Perceptron to the logical OR, print its achieved scores and plot its decision boundary.
In [4]:

y_OR = np.array([0, 1, 1, 1])


p.fit(X, y_OR)

# Obtaining f(x) scores.


pred_scores = p.decision_function(X)
print("Perceptron's OR scores: {}".format(pred_scores))

plot_decision_boundary(p, X, y_OR, 'OR decision boundary')


plt.show()

Perceptron's OR scores: [-0.1 0.1 0.1 0.3]

Finally, we analyze the XOR operation, which is defined as:

x0 x1 y

0 0 0

1 0 1

0 1 1

1 1 0

Below we plot XOR.

In [5]:

y_XOR = np.array([0, 1, 1, 0])


plt.scatter(X[:, 0], X[:, 1], c=y_XOR, cmap=ListedColormap(['#FF0000', '#0000FF']), alpha=1.0)
plt.show()

Clearly, this is not a linear separable problem. In other words, it is not possible to separate the two classes with a single hyperplane.

This kind of problem motivates us to use Multilayer Perceptrons (MLPs), which are shown in the sequence.

Multilayer Perceptron (MLP)

A MLP is a neural network which is composed by at least three different layers: an input layer, a hidden layer and an output layer. Except for
the input layer, the remaining ones are composed by Perceptrons with nonlinear activation functions (e.g., sigmoid or tanh).

MLPs are usually trained using the backpropagation algorithm and are able to deal with not linearly separable problems.

Below we present an example for the XOR problem.


Practical example - XOR

In [6]:

# Creating a MLPClassifier.
# hidden_layer_sizes receive a tuple where each position i indicates the number of neurons
# in the ith hidden layer
# activation specifies the activation function (other options are: 'identity', 'logistic' and 'relu')
# max_iter indicates the maximum number of training iterations
# There are other parameters which can also be changed.
# See http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html
mlp = MLPClassifier(hidden_layer_sizes=(5,),
activation='tanh',
max_iter=10000,
random_state=seed)

# Training and plotting the decision boundary.


mlp.fit(X, y_XOR)
plot_decision_boundary(mlp, X, y_XOR, 'XOR')
plt.show()

pred = mlp.predict_proba(X)
print("MLP's XOR probabilities:\n[class0, class1]\n{}".format(pred))

MLP's XOR probabilities:


[class0, class1]
[[ 0.90713158 0.09286842]
[ 0.0837964 0.9162036 ]
[ 0.04619978 0.95380022]
[ 0.95695933 0.04304067]]

Practical example - Breast Cancer

First of all, we load the dataset, encode its labels as int values and split it into training and test sets.

In [7]:

# Loading Breast Cancer dataset.


data = pd.read_csv('data/breast_cancer.csv')

# Creating a LabelEncoder and transforming the dataset labels.


le = LabelEncoder()
y = le.fit_transform(data['diagnosis'].values)

# Extracting the instances data.


X = data.drop('diagnosis', axis=1).values

# Splitting into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

Then, we create, train and test a MLPClassifier.


In [8]:

mlp = MLPClassifier(hidden_layer_sizes=(10,),
activation='tanh',
max_iter=10000,
random_state=seed)

mlp.fit(X_train, y_train)
y_pred = mlp.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("MLP's accuracy score: {}".format(accuracy))

MLP's accuracy score: 0.6288659793814433

We can observe that its accuracy score was rather low.

Unfortunately, MLPs are very sensitive to different feature scales. So, it is normally necessary to normalize or rescale the input data.

In [9]:

# Creating a StandardScaler. This object normalizes features to zero mean and unit variance.
scaler = StandardScaler()
scaler.fit(X_train)

# Normalizing train and test data.


X_train_scaled, X_test_scaled = scaler.transform(X_train), scaler.transform(X_test)

# Training MLP with normalized data.


mlp.fit(X_train_scaled, y_train)

# Testing MLP with normalized data.


y_pred = mlp.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
print("MLP's accuracy score: {}".format(accuracy))

MLP's accuracy score: 0.979381443298969


Support Vector Machines (SVMs)
SVMs are very powerful binary classifiers, based on the Statistical Learning Theory (SLT) framework. SVMs can be used to solve hard
classification problems, where they look for an optimal hyperplane able to maximize the classifier margin.

Practical example - classifier margin

First of all we do all necessary imports.

In [1]:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D

from sklearn.datasets import make_blobs, make_circles


from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.svm import SVC

# Setting random seed.


seed = 10

Then, we generate a very simple linear separable dataset and plot it.

In [2]:

# Generating a linear separable dataset with 50 samples and 2 classes.


X, y = make_blobs(n_samples=50, centers=2, center_box=[-7.5, 7.5], random_state=seed)

# Method to plot the linear separable dataset.


def plot_data(X, y):
class0 = np.where(y == 0)[0]
plt.scatter(X[class0, 0], X[class0, 1], c='red', marker='s')

class1 = np.where(y == 1)[0]


plt.scatter(X[class1, 0], X[class1, 1], c='blue', marker='o')

plot_data(X, y)
plt.show()

Next, we train a SVM classifier with linear kernel and plot the optimal hyperplane as well as the classifier margins.
In [3]:

svm = SVC(C=100, kernel='linear', random_state=seed)


svm.fit(X, y)

plot_data(X, y)

# Method to plot SVMs' hyperplane and margins.


# This code is based on http://scikit-learn.org/stable/auto_examples/svm/plot_separating_hyperplane.html
def plot_margins(svm, X, y):
xmin, xmax = plt.xlim()
ymin, ymax = plt.ylim()

# create grid to evaluate model


xx = np.linspace(xmin, xmax, 30)
yy = np.linspace(ymin, ymax, 30)
XX, YY = np.meshgrid(xx, yy)
xy = np.vstack([XX.ravel(), YY.ravel()]).T
Z = svm.decision_function(xy).reshape(XX.shape)

# plot decision boundary and margins


plt.contour(XX, YY, Z, colors='black', levels=[-1, 0, 1], alpha=1.0, linestyles=['--', '-', '--'])

plot_margins(svm, X, y)
plt.show()

In the above plot, the filled line represents the optimal hyperplane found while the dashed lines represent the hyperplanes defined by the
support vectors. The margin of the classifier is the distance between the optimal hyperplane and any of the support vector hyperplanes.

Practical example - Non-linear decision boundary

SVMs are linear classifiers. Since most of the real world problems are not linearly separable, how can we deal with them?

Next, we will show a very simple application of the Kernel Trick, which ables us to learn non-linear decision boundaries.

First, we generate a very simple and not linearly separable dataset.

In [14]:

X, y = make_circles(n_samples=100, noise=0.05, factor=0.5, random_state=seed)


plot_data(X, y)
plt.show()

Then, we try to fit a custom SVM with linear kernel. Clearly, this classifier will not achieve good results.
In [5]:

svm = SVC(C=100, kernel='linear', random_state=seed)


svm.fit(X, y)

plot_data(X, y)
plot_margins(svm, X, y)
plt.show()

However, if we apply a polynomial kernel of degree 2, we are able to learn the optimal decision boundary for this dataset.

In [6]:
svm = SVC(C=100, kernel='poly', degree=2, random_state=seed)
svm.fit(X, y)

plot_data(X, y)
plot_margins(svm, X, y)
plt.show()

The Kernel Trick consists of implicitly mapping a lower dimensional dataset, which is not linearly separable, to a higher dimensional space
where the data becomes linearly separable.

In the above example, the standard linear kernel calculates the standard dot product as the similarity between two vectors u and v. That is:
k(u, v) = u ⋅ v.

When we apply a polynomial kernel of degree 2, we are calculating the similarity between two vectors u and v as:
k(u, v) = (u ⋅ v) 2
k(u, v) = (u 1 v1 + u 2 v2 ) 2
k(u, v) = u 21 v21 + 2u 1 v1 u 2 v 2 + u 22 v 22

The above calculation can be rewritten as:

[ ][ ]
u 21 v 21

k(u, v) = √2u1 u2 ⋅ √2v 1v 2 ,


2 2
u2 v2

which is a dot product of two three dimensional vectors.

Finally, we will plot the original dataset in this new three dimensional space.
In [24]:

to3d = lambda x : [x[0] ** 2, np.sqrt(2) * x[0] * x[1], x[1] ** 2]


X_3D = np.array(list(map(to3d, X)))

fig = plt.figure(1, figsize=(8, 6))


ax = Axes3D(fig, elev=-150, azim=115)

class0 = np.where(y == 0)[0]


ax.scatter(X_3D[class0, 0], X_3D[class0, 1], X_3D[class0, 2], c='red', marker='s')

class1 = np.where(y == 1)[0]


ax.scatter(X_3D[class1, 0], X_3D[class1, 1], X_3D[class1, 2], c='blue', marker='o')

ax.set_xlabel('x[0] ** 2')
ax.set_ylabel('np.sqrt(2) * x[0] * x[1]')
ax.set_zlabel("x[1] ** 2")

ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_zticklabels([])

plt.show()

As it can be seen, the original dataset is linear separable in this new three dimensional space.

Practical example - Breast Cancer

Finally, as a last example, we will apply SVMs on the Breast Cancer dataset.

In [8]:
# Loading Breast Cancer dataset.
data = pd.read_csv('data/breast_cancer.csv')

# Creating a LabelEncoder and transforming the dataset labels.


le = LabelEncoder()
y = le.fit_transform(data['diagnosis'].values)

# Extracting the instances data.


X = data.drop('diagnosis', axis=1).values

# Splitting into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

Since this dataset has high dimensionality and probably is not linearly separable, we will apply a SVM with Radial Basis Function (RBF) kernel.
In [9]:

svm = SVC(kernel='rbf')
svm.fit(X_train, y_train)

y_pred = svm.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("SVM's accuracy score: {}".format(accuracy))

SVM's accuracy score: 0.6288659793814433

Unfortunately, SVMs are sensitive to data scale. Thus, we will standartize the dataset and train the SVM again.

In [10]:

scaler = StandardScaler()
scaler.fit(X_train)

# Normalizing train and test data.


X_train_scaled, X_test_scaled = scaler.transform(X_train), scaler.transform(X_test)

# Training SVM with normalized data.


svm.fit(X_train_scaled, y_train)

# Testing SVM with normalized data.


y_pred = svm.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
print("SVM's accuracy score: {}".format(accuracy))

SVM's accuracy score: 0.9845360824742269

Multiclass problems

SVMs were designed to deal with binary classification problems. Several approaches are available to deal with multiclass problems. Some of
them are:

One-vs-one classifiers: suppose the classification problem is composed by k classes. Thus, k(k − 1) /2 SVMs are fitted, each one for a
different pair of classes. For prediction, the class that received most of the votes is returned as output.
One-vs-all classifiers: suppose the classification problem is composed by k classes. Then, k different classifiers are fitted, one for
each class.

The sklearn.svm.SVC class implements the one-vs-one scheme.


Ensemble methods
Ensemble methods combine several base classifiers in order to improve their robustness when compared to their predictions alone. Several
methods have been proposed in the machine learning literature. Scikit-learn provides us several classes to fit ensemble method, for
example, VotingClassifier, BaggingClassifier, AdaBoostClassifier and RandomForestClassifier, to name a few. These classes will be explained
in the sequence.

First we do all necessary imports, load the breast cancer dataset and define a method to plot a classifier's decision boundary.

In [1]:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.ensemble import VotingClassifier, BaggingClassifier, AdaBoostClassifier, RandomForestClassifier


from sklearn.tree import DecisionTreeClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# Random seed.
seed = 10

# Loading Iris dataset.


data = pd.read_csv('data/iris.csv')

# Creating a LabelEncoder and fitting it to the dataset labels.


le = LabelEncoder()
le.fit(data['Name'].values)

# Converting dataset str labels to int labels.


y = le.transform(data['Name'].values)

# Extracting the instances data. In this example we will consider only the first two features to be able to
# plot the data and the decision boundaries of the classifiers.
X = data.drop('Name', axis=1).values[:, :2]

# Splitting into train and test sets.


X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.34, stratify=y, random_state=seed)

# Method to plot a classifier's decision boundary.


# This code is based on:
# http://scikit-learn.org/stable/auto_examples/semi_supervised/plot_label_propagation_versus_svm_iris.html
def plot_decision_boundary(classifier, X, y, title):
xmin, xmax = np.min(X[:, 0]) - 0.05, np.max(X[:, 0]) + 0.05
ymin, ymax = np.min(X[:, 1]) - 0.05, np.max(X[:, 1]) + 0.05
step = 0.01

xx, yy = np.meshgrid(np.arange(xmin, xmax, step), np.arange(ymin, ymax, step))

Z = classifier.predict(np.hstack((xx.ravel()[:, np.newaxis], yy.ravel()[:, np.newaxis])))


Z = Z.reshape(xx.shape)

colormap = plt.cm.Paired
plt.contourf(xx, yy, Z, cmap=colormap)

color_map_samples = {0: (0, 0, .9), 1: (1, 0, 0), 2: (.8, .6, 0)}


colors = [color_map_samples[c] for c in y]
plt.scatter(X[:, 0], X[:, 1], c=colors, edgecolors='black')

plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)

plt.title(title)

Voting classifier

The idea of the Voting Classifier is to combine different types of classifiers and to produce a prediction as the majority vote among them or
the argmax of the mean probability of a class. In scikit-learn, this approach is implemented in the VotingClassifier class.
In [2]:

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

# Fitting a Decision Tree.


tree = DecisionTreeClassifier(min_samples_split=5, min_samples_leaf=3, random_state=seed)
tree.fit(X_train, y_train)
plt.subplot(2, 2, 1)
plot_decision_boundary(tree, X_train, y_train, 'Decision Tree decision boundary')

# Fitting a MLP.
mlp = MLPClassifier(hidden_layer_sizes=(10,), max_iter=10000, random_state=seed)
mlp.fit(X_train, y_train)
plt.subplot(2, 2, 2)
plot_decision_boundary(mlp, X_train, y_train, 'MLP decision boundary')

# Fitting a kNN.
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)
plt.subplot(2, 2, 3)
plot_decision_boundary(knn, X_train, y_train, 'kNN decision boundary')

# Fitting a Voting Classifier by combining the three above classifiers.


voting_clf = VotingClassifier(estimators=[('Tree', tree), ('MLP', mlp), ('kNN', knn)], voting='hard')
voting_clf.fit(X_train, y_train)
plt.subplot(2, 2, 4)
plot_decision_boundary(voting_clf, X_train, y_train, 'Voting Classifier decision boundary')

plt.tight_layout()

plt.show()

Bagging classifier

Bagging applies the same classifier on subsamples (usually with the same size) of the original dataset with replacement. In scikit-learn, this
method is implemented through BaggingClassifier class. Its predictions return the label with highest mean probability among the base
classifiers. If the base classifiers do not implement the predict_proba method, this class predicts the label by majority voting.

As mentioned in scikit-learn's documentation, the bagging method usually works well with more complex models (such as fully fitted
decision trees).
In [3]:

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

tree = DecisionTreeClassifier(random_state=seed)
tree.fit(X_train, y_train)
plt.subplot(1, 2, 1)
plot_decision_boundary(tree, X_train, y_train, 'Decision Tree decision boundary')

bagging_clf = BaggingClassifier(base_estimator=tree, n_estimators=50, random_state=seed)


bagging_clf.fit(X_train, y_train)
plt.subplot(1, 2, 2)
plot_decision_boundary(bagging_clf, X_train, y_train, 'Bagging Classifier decision boundary')

plt.tight_layout()

plt.show()

Boosting classifier

The Boosting method tries to combine several weak classifiers (i.e., classifiers that are slightly better than random classifiers) into a strong
classifier. At each step, the procedure fits a new classifier with different weights on the objects from the training set. The idea is simple,
objects that are assigned the wrong label will have their weights increased in the next iteration, while the others will have their weights
decreased in the next iteration.

The most popular boosting algorithm of is AdaBoost. In scikit-learn, it is implemented in the AdaBoostClassifier class.

In [4]:

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

tree = DecisionTreeClassifier(min_samples_split=5, min_samples_leaf=5, max_depth=3, random_state=seed)


tree.fit(X_train, y_train)
plt.subplot(1, 2, 1)
plot_decision_boundary(tree, X_train, y_train, 'Decision Tree decision boundary')

boosting_clf = AdaBoostClassifier(n_estimators=50)
boosting_clf.fit(X_train, y_train)
plt.subplot(1, 2, 2)
plot_decision_boundary(boosting_clf, X_train, y_train, 'AdaBoost Classifier decision boundary')

plt.tight_layout()

plt.show()
Random Forest classifier

Random Forest consists of an ensemble method composed by multiple decision trees. Each tree is trained with a subsample with
replacement from the original dataset and, at each step, a node split is performed by choosing the best split among a random subset of the
features instead of the best split overall.

Many experimental machine learning studies suggest that Random Forest is one of the best classifiers from the literature. In scikit-learn, this
algorithm is implemented through RandomForestClassifier class.

In [5]:

random_forest_clf = RandomForestClassifier(n_estimators=50, random_state=seed)


random_forest_clf.fit(X_train, y_train)

plt.figure(figsize=(5, 5))
plot_decision_boundary(random_forest_clf, X_train, y_train, 'Random Forest Classifier decision boundary')
plt.show()

S-ar putea să vă placă și