Prévia do material em texto
MINERAÇÃO DE DADOS EM PYTHON 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 e $ 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. 1http://www.numpy.org/ 2http://www.scipy.org/ 3http://http://scikit-learn.org/stable/ 4http://matplotlib.org/ 5https://bootstrap.pypa.io/get-pip.py 6Para o ambiente Windows, se os comandos python3 e pip3 não funcionarem, deve-se testar com os comandos python e pip, respectivamente. 3 https://www.python.org/downloads/windows/ http://www.numpy.org/ http://www.scipy.org/ http://http://scikit-learn.org/stable/ http://matplotlib.org/ https://bootstrap.pypa.io/get-pip.py 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) 1Na 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 nestetópico, recomenda-se a leitura de: https://scipy.github.io/old-wiki/pages/ EricsBroadcastingDoc. 4 https://scipy.github.io/old-wiki/pages/EricsBroadcastingDoc https://scipy.github.io/old-wiki/pages/EricsBroadcastingDoc 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 Xn×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: x̄ = 1 n · n ∑ i=1 xi (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: s2 = 1 n−1 · n ∑ i=1 (xi− x̄)2 (2) 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: obliquidade = ∑ n i=1(xi− x̄)3 (n−1) · s3 (4) 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: 2https://en.wikipedia.org/wiki/Degrees_of_freedom_(statistics) 3A imagem original está disponível sob a licença CC-BY-SA 3.0 (https://creativecommons. org/licenses/by-sa/3.0/). 8 https://en.wikipedia.org/wiki/Degrees_of_freedom_(statistics) https://creativecommons.org/licenses/by-sa/3.0/ https://creativecommons.org/licenses/by-sa/3.0/ 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: curtose = ∑ n i=1(xi− x̄)4 (n−1) · s4 (5) 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 4Isto é, 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. 5Ou 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. X = xT 1 xT 2 ... xT n (6) corresponderá a um conjunto de dados Xn×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: 6Disponível em: https://raw.githubusercontent.com/pandas-dev/pandas/ master/pandas/tests/data/iris.csv. 10 https://raw.githubusercontent.com/pandas-dev/pandas/master/pandas/tests/data/iris.csv https://raw.githubusercontent.com/pandas-dev/pandas/master/pandas/tests/data/iris.csv 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: Matrizde 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 7https://pandas.pydata.org/pandas-docs/stable/generated/pandas. DataFrame.cov.html 8https://pandas.pydata.org/pandas-docs/stable/generated/pandas. DataFrame.corr.html 15 https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.cov.html https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.cov.html https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.corr.html https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.corr.html 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 https://commons.wikimedia.org/wiki/File:Negative_and_positive_skew_diagrams_(English).svg https://commons.wikimedia.org/wiki/File:Negative_and_positive_skew_diagrams_(English).svg 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 1http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+ %28Diagnostic%29 2http://archive.ics.uci.edu/ml/datasets/Contraceptive+Method+Choice 18 http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29 http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Diagnostic%29 http://archive.ics.uci.edu/ml/datasets/Contraceptive+Method+Choice 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 deum 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 http://archive.ics.uci.edu/ml/machine-learning-databases/cmc/cmc.names http://archive.ics.uci.edu/ml/machine-learning-databases/cmc/cmc.names 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_redcom 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 outroou 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 = xi j −min j max j−min j (1) 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 = xi j − x̄· j s j (2) 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 https://arxiv.org/abs/1404.1100 https://arxiv.org/abs/1404.1100 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 3http://scikit-learn.org/stable/modules/feature_selection.html 30 http://scikit-learn.org/stable/modules/feature_selection.html 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 DataFramegerado 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: d(xi,x j) = ( m ∑ l=1 |xil− x jl|p ) 1 p . (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): d(xi,x j) = m ∑ l=1 |xil− x jl|; (2) 34 Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python • Distância euclidiana (p = 2): d(xi,x j) = √ m ∑ l=1 (xil− x jl)2; (3) • Distância de Chebyshev (p = ∞): d(xi,x j) = max 1≤l≤m |xil− x jl|. (4) 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: d(xi,x j) = m ∑ l=1 I(xil = x jl), (5) 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 1https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial. distance.pdist.html 35 https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.pdist.html https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.pdist.html 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 xa∈Ci xb∈C j d(xa,xb). (6) • 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 xa∈Ci xb∈C j d(xa,xb). (7) • 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: d(Ci,C j) = 1 |Ci||C j| ∑ xa∈Ci xb∈C j d(xa,xb). (8) 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. 2Neste 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: SSE = k ∑ i=1 ∑ x j∈Ci d(x j, x̄Ci) 2, (9) sendo d(·, ·) a distância euclidiana e x̄Ci o centróide de um grupo Ci, calculado como: x̄Ci = 1 |Ci| ∑ x j∈Ci x j. (10) 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 ouque 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. 4A imagem original está disponível sob a licença CC-BY-SA 3.0 (https://creativecommons. org/licenses/by-sa/3.0/). 42 https://creativecommons.org/licenses/by-sa/3.0/ https://creativecommons.org/licenses/by-sa/3.0/ 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: Dunn(C) = min 1≤ i, j ≤ k i 6= j { d(Ci,C j) max 1≤ l ≤ k {D(Cl)} } , (11) sendo d(Ci,C j) e D(Cl) definidos, respectivamente: d(Ci,C j) = min xa∈Ci xb∈C j d(xa,xb), (12) 43 Padilha, V. A. e Carvalho, A. C. P. L. F., Mineração de Dados em Python D(Cl) = max xa,xb∈Cl d(xa,xb). (13) 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: S(xi) = b(xi)−a(xi) max{a(xi),b(xi)} (14) sendo a(xi) e b(xi) calculadoscomo: a(xi) = 1 |Cl|−1 ∑ x j ∈Cl d(xi,x j), (15) b(xi) = min 1≤ h≤ k h 6= l { 1 |Ch| ∑ x j ∈Ch d(xi,x j) } . (16) Com isso, a LS é definida como a silhueta média entre todos os objetos do conjunto de dados. Ou seja: LS(C) = 1 n n ∑ i=1 S(xi). (17) 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 = {Cob 1 , · · · ,Cob k }, enquanto que a partição restante representará uma solução de refe- rência para o conjunto de dados estudado, denotada por Cre f = {Cre f 1 , · · · ,Cre f q }. 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: IR(Cob,Cre f ) = a11 +a00 a11 +a10 +a01 +a11 = a11 +a00(n 2 ) . (18) 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: IJ(Cob,Cre f ) = a11 a11 +a01 +a10 . (19) 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: IRA(Cob,Cre f ) = IR(Cob,Cre f )−E[IR(Cob,Cre f )] max{IR}−E[IR(Cob,Cre f )] , (20) 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: IRA(Cob,Cre f ) = a− (a+c)(a+b) a+b+c+d (a+c)+(a+b) 2 − (a+c)(a+b) a+b+c+d . (21) 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: IM(Cob,Cre f ) = |Cob| ∑ i=1 |Cre f | ∑ j=1 P(i, j) log ( P(i, j) Pob(i) Pre f ( j) ) (22) sendo P(i, j), Pob(i) e Pre f ( j) definidos, respectivamente, por: P(i, j) = |Cob i ∩Cre f j | n , (23) Pob(i) = |Cob i | n , (24) Pre f ( j) = |Cre f j | n . (25) 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: IMN(Cob,Cre f ) = IM(Cob,Cre f )√ H(Cob) H(Cre f ) , (26) onde H(·) representa a entropia de um agrupamento, calculada por meio da fórmula a seguir: H(C) =− |C| ∑ i=1 P(i) log(P(i)). (27) 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 é apresentadoum 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. 5Este capítulo está disponível gratuitamente em http://www-users.cs.umn.edu/~kumar/ dmbook/ch8.pdf. 49 https://commons.wikimedia.org/wiki/File:DBSCAN-Illustration.svg https://commons.wikimedia.org/wiki/File:DBSCAN-Illustration.svg http://www-users.cs.umn.edu/~kumar/dmbook/ch8.pdf http://www-users.cs.umn.edu/~kumar/dmbook/ch8.pdf 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)}n i=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}n i=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 # carregaa 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: 1Este é um problema, em aprendizado de máquina, conhecido como "maldição da dimensionalidade" 2http://scikit-learn.org/stable/modules/generated/sklearn.neighbors. KNeighborsRegressor.html 20 http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsRegressor.html 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. 3https://matplotlib.org/users/pyplot_tutorial.html 21 https://matplotlib.org/users/pyplot_tutorial.html 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() 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. 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 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)) 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)) 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 meanaccuracy 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. 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] Accuracy: 0.9226804123711341 0-1 loss: 0.07731958762886593 Accuracy + 0-1 loss = 1.0 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))) 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: TPR = TP TP + FN ; False Positive Rate (FPR): also known as specificity. It measures the hit rate for the negative class. It is calculated as: FPR = FP 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. 10-fold cross-validation accuracy mean: 0.9298429262812202 10-fold cross-validation accuracy std: 0.02691043470531831 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 f2), the null-hypothesis (H0) 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 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') 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 suchan 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. 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 Out[10]: WilcoxonResult(statistic=12.0, pvalue=0.010968496564224731) In [11]: # Loading the example DataFrame. performances = pd.read_csv('data/example_friedman_nemenyi_demsar.csv') performances 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) 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))) Then, we will calculate the critical differences and plot the results of each test (Nemenyi and Bonferroni-Dunn). 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 Out[12]: FriedmanchisquareResult(statistic=51.285714285714278, pvalue=1.7912382226666844e-06) 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 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 (yi | x) = P(yi) P (x1, ⋯, xm | yi) P (x1, ⋯, xm) . Additionally, it assumes that the features are independent from each other (which is the reason why it is called naive): P(x1, ⋯, xm | yi) = ∏m j = 1P (xj | yi). So, we obtain: P(yi | x) = P (yi) ∏m j = 1P (xj | yi) P (x1, ⋯, xm) . For a given object x, Naive Bayes will output the the Maximum a Posteriori (MAP) estimate: ŷ = arg maxy P (y | x) = arg maxy P (y) ∏m j = 1P (xj | y) P (x1, ⋯, xm) . Note that P (x1, ⋯, xm) is constant. Thus, we can drop it to obtain: ŷ = arg maxy P (yi) ∏m j = 1P (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]: cap- shape cap- surface cap- color bruises? odor gill- attachment gill- spacing gill- size gill- color stalk- shape ... stalk- color- above- ring stalk- color- below- ring veil- type veil- color ring- number ring- type 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 | yi) 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)) 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() BernoulliNB accuracy score: 0.9464350343829171 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)) Finally, we can plot the obtained tree to visualize the rules extracted from the dataset. DecisionTreeClassifier accuracy score: 0.9615384615384616 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 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. Out[4]: PetalWidth <= 0.8 gini = 0.6666 samples = 98 value = [33, 32, 33] class = Iris-setosa gini = 0.0 samples = 33 value = [33, 0, 0] class = Iris-setosa True PetalLength <= 4.85 gini = 0.4999 samples = 65 value = [0, 32, 33] class = Iris-virginica False PetalWidth <= 1.45 gini = 0.1207 samples = 31 value = [0, 29, 2] class = Iris-versicolor PetalWidth <= 1.75 gini = 0.1609 samples = 34 value = [0, 3, 31] class = Iris-virginica gini = 0.0 samples = 22 value = [0, 22, 0] class = Iris-versicolor gini = 0.3457 samples = 9 value = [0, 7, 2] class = Iris-versicolor gini = 0.4898 samples = 7 value = [0, 3, 4] class = Iris-virginica gini = 0.0 samples = 27 value = [0, 0, 27] class = Iris-virginica 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) 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)) 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)) 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)) 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') DecisionTreeClassifier accuracy score: 0.9992761491132827 DecisionTreeClassifier max_depth: 6 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 f <= 0.5 gini = 0.2845 samples = 3040 value = [522, 2518] class = e True r <= 0.5 gini = 0.0553 samples = 2321 value = [2255, 66] class = p False h <= 0.5 gini = 0.4826 samples= 880 value = [522, 358] class = p gini = 0.0 samples = 2160 value = [0, 2160] class = e gini = 0.3739 samples = 695 value = [522, 173] class = p gini = 0.0 samples = 185 value = [0, 185] class = e y <= 0.5 gini = 0.0251 samples = 2284 value = [2255, 29] class = p gini = 0.0 samples = 37 value = [0, 37] class = e gini = 0.0035 samples = 2250 value = [2246, 4] class = p gini = 0.3893 samples = 34 value = [9, 25] 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)) 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). Perceptron's AND scores: [ -2.00000000e-01 -1.00000000e-01 -2.77555756e-17 1.00000000e-01] 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() 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. Perceptron's OR scores: [-0.1 0.1 0.1 0.3] 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)) 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. MLP's XOR probabilities: [class0, class1] [[ 0.90713158 0.09286842] [ 0.0837964 0.9162036 ] [ 0.04619978 0.95380022] [ 0.95695933 0.04304067]] 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)) 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.6288659793814433 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 classifiermargin. 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) = (u1v1 + u2v2)2 k(u, v) = u2 1v2 1 + 2u1v1u2v2 + u2 2v2 2 The above calculation can be rewritten as: k(u, v) = u2 1 √2u1u2 u2 2 ⋅ v2 1 √2v1v2 v2 2 , 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)) 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)) 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. SVM's accuracy score: 0.6288659793814433 SVM's accuracy score: 0.9845360824742269 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) # Convertingdataset 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() Instalação em Linux Instalação em Windows Referências e tutoriais Introdução à NumPy Arrays Exploração de dados Dados univariados Medidas de posição ou localidade Medidas de dispersão ou espalhamento Medidas de distribuição Dados multivariados Exemplo: Iris Visualização de dados Histogramas Scatter plots Box plots Exercícios Conjunto de dados Eliminação manual de atributos Amostragem Amostragem simples Amostragem estratificada Dados ausentes Dados redundantes Ruídos Transformação de dados Transformação de dados simbólicos para dados numéricos Transformação de dados numéricos para dados simbólicos Normalização de atributos numéricos Redução de dimensionalidade Principal Component Analysis (PCA) Filtragem por limiar de variância Exercícios (1) Aprendizado de máquina não supervisionado Medidas de (dis)similaridade Algoritmos hierárquicos Algoritmo k-means Algoritmo DBSCAN Validação de agrupamentos Critérios de validação relativa Índice de Dunn (ID) Largura de silhueta (LS) Critérios de validação interna Critérios de validação externa Índice Rand (IR) Índice Jaccard (IJ) Índice Rand Ajustado (IRA) Informação Mútua (IM) Informação Mútua Normalizada (IMN) Exemplos Exercícios (2) Aprendizado de máquina supervisionado Métodos baseados em distâncias entre objetos k-Nearest Neighbors (kNN) Exercícios (3)