Link para o material da apresentação.
Transcrição
Link para o material da apresentação.
Mineração e Visualização de Dados usando Java 1 Mineração e Visualização de Dados usando Java Sumário 1 Introdução 1.1 2 Sobre este documento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 Weka Informações Gerais 3 3 Weka Representação de Dados 3.1 Criando Relações (arquivos .ARFF) 6 em Aplicações em Java . . . . . . . . . . . . . . . . . . . . . 4 Weka Árvores de Decisão 6 9 4.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 4.2 Usando Árvores de Decisão em Aplicações em Java . . . . . . . . . . . . . . . . . . . . . . . . . 9 5 Weka Redes Neurais 13 Multilayer Perceptron 5.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 5.2 Usando Redes Neurais Multilayer Perceptron em Aplicações em Java . . . . . . . . . . . . . . . . 13 6 Weka Agrupamento com 16 K-Means 6.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 6.2 Usando o Algoritmo K-Means em Aplicações em Java . . . . . . . . . . . . . . . . . . . . . . . . 16 7 Visualização usando Coordenadas Paralelas 18 7.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 7.2 Implementação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 7.3 Possíveis melhorias 29 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Visualização de Grafos com JUNG 30 8.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 8.2 Implementação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 Referências Rafael Santos 47 http://www.lac.inpe.br/∼rafael.santos 2 Mineração e Visualização de Dados usando Java 1 Introdução 1.1 Sobre este documento Este documento é um guia prático para o desenvolvimento de aplicações usando a linguagem Java juntamente com algumas APIs (Application Programming Interfaces) como Weka (Waikato Environment for Knowledge Analysis) [14] e JUNG (Java Universal Network/Graph Framework) [12]. O documento mostra como integrar estas APIs em código Java, possibilitando o desenvolvimento de aplicações especializadas para resolver problemas especícos. Este documento . não é: Um tutorial sobre mineração e visualização de dados em geral tutoriais e muitas referências para estes tópicos podem ser encontradas em [9] e [11]. . Um tutorial sobre o uso dos ambientes grácos interativos do Weka (Explorer, Experimenter e Knowledge Flow). . Um tutorial sobre a linguagem Java e suas APIs, orientação a objetos ou programação com interfaces grácas para isto veja [8] e [10]. . Um tutorial sobre o IDE (Integrated Development Environment, ambiente de desenvolvimento integrado) Eclipse, sugerido para o desenvolvimento. Esta versão do documento foi reescrita e adaptada para uso no treinamento Mineração de Dados e Aplicações para o Centro de Tecnologia da Informação Renato Archer CTI, em Fevereiro/Março de 2010. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 2 3 Weka Informações Gerais Muitos algoritmos podem ser usados para mineração de dados, e vários destes são relativamente clássicos e bem conhecidos, porém sua implementação correta pode ser muito complexa ou custosa. Pesquisadores da Universidade de Waikato, na Nova Zelândia, desenvolveram um pacote de software chamado Weka (Waikato Environment for Knowledge Analysis) [13, 14] que contém uma aplicação com interface gráca onde um usuário pode experimentar com os diversos algoritmos presentes no pacote para tentar extrair conhecimento de seus dados. Figura 1: Interface gráca principal do ambiente Weka. Figura 2: Tela da aplicação Explorer, parte do ambiente Weka. Além da interface gráca para exploração, os algoritmos do pacote podem ser executados a partir de linha de comando e embutidos em aplicações em Java. Os próximos capítulos deste documento mostram como representar dados no Weka em um formato padrão e que pode ser facilmente construído e como usar algoritmos do Weka em suas aplicações. Rafael Santos http://www.lac.inpe.br/∼rafael.santos 4 Mineração e Visualização de Dados usando Java Figura 3: Tela da aplicação Explorer, mostrando um experimento em classicação. Figura 4: Tela da aplicação Explorer, mostrando um experimento em visualização simples. Para usar as classes da API Weka é necessário copiar o arquivo weka.jar para um diretório conhecido (ou no classpath da sua instalaçào Java) e referenciar este arquivo em seu projeto. Para fazer isto usando a IDE Eclipse, http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 5 siga estes passos: 1. Crie um diretório use o nome lib). lib no seu projeto (clique no projeto com o botão direito do mouse e em 2. Copie para este diretório o arquivo a tecla F5 weka.jar. New/Folder, Ao terminar de copiar, selecione o nome do projeto e clique para atualizar os subdiretórios relacionados ao projeto. 3. Inclua este arquivo no projeto selecionando o projeto, clicando com o botão direito do mouse e escolhendo Build Path/Configure Build Path. Clique no botão Add JARs e selecione no diálogo o diretório lib e dentro dele o arquivo weka.jar. Feche todos os diálogos clicando em seus respectivos botões OK. Outras IDEs devem ser conguradas com outros métodos. Consulte a documentação da sua IDE para informações especícas. Rafael Santos http://www.lac.inpe.br/∼rafael.santos 6 Mineração e Visualização de Dados usando Java 3 Weka Representação de Dados Em alguns exemplos deste tutorial usaremos arquivos do tipo diretamente pela API Weka. Um arquivo no formato .ARFF .ARFF (Attribute-Relation File Format), suportados é um arquivo de texto puro, que pode ser editado por qualquer ferramenta básica de edição de texto sem formatação. O conteúdo do arquivo é composto de três partes: . Relação, a primeira linha do arquivo, que deve ser igual a @relation seguida de uma palavra-chave que identique a relação ou tarefa sendo estudada. . Atributos, um conjunto de linhas onde cada uma inicia com @attribute seguida do nome do atributo e seguida do seu tipo, que pode ser nominal (neste caso as alternativas devem aparecer como uma lista separada por vírgulas e cercadas por chaves) ou numérico (neste caso o nome deve ser seguido da palavrachave real). Geralmente, em uma tarefa de classicação supervisionada (onde conhecemos as classes das instâncias usadas para treinamento) o último atributo é a classe para as instâncias e deve ser nominal. . Dados, depois de uma linha contendo @data. Cada linha deve corresponder a uma instância e deve ter valores separados por vírgula correspondentes (e na mesma ordem) dos atributos da seção Atributos. O arquivo também pode conter linhas iniciadas com o sinal de percentagem (%). Estas linhas serão consideradas comentários e não serão processadas. Mais informações sobre o formato (inclusive como criar arquivos de forma programática) podem ser lidas em http://weka.wikispaces.com/ARFF. Como um exemplo, o conteúdo do arquivo weather.arff é mostrado na listagem 1. Este arquivo contém relações (descritores e dados) para uma tarefa simples de previsão de atividade esportiva em função do tempo. Listagem 1: O arquivo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 weather.arff, que contém relações sobre um problema jogar em função do tempo. @relation weather @attribute @attribute @attribute @attribute @attribute outlook { sunny , overcast , rainy } temperature real humidity real windy { TRUE , FALSE } play { yes , no } @data sunny ,85 ,85 , FALSE , no sunny ,80 ,90 , TRUE , no overcast ,83 ,86 , FALSE , yes rainy ,70 ,96 , FALSE , yes rainy ,68 ,80 , FALSE , yes rainy ,65 ,70 , TRUE , no overcast ,64 ,65 , TRUE , yes sunny ,72 ,95 , FALSE , no sunny ,69 ,70 , FALSE , yes rainy ,75 ,80 , FALSE , yes sunny ,75 ,70 , TRUE , yes overcast ,72 ,90 , TRUE , yes overcast ,81 ,75 , FALSE , yes rainy ,71 ,91 , TRUE , no 3.1 Criando Relações (arquivos .ARFF) em Aplicações em Java .ARFF tem estrutura simples, e por ser arquivos de texto, podem facilmente ser editados em editores Por exemplo, podemos converter arquivos .CSV (comma-separated value) para .ARFF com a inclusão Os arquivos de texto. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 7 de um cabeçalho adequado. .ARFF é através da criação de sua estrutura dentro de .ARFF é uma coleção de instâncias onde cada uma é uma Outra forma de criar arquivos uma aplicação em Java. Uma relação em um arquivo coleção de atributos. Os passos para criar uma uma base de dados com a estrutura esperada para ser armazenada como um arquivo .ARFF sãosão os seguintes: 1. Criamos os atributos usados na base de dados (instâncias da classe Attribute). Atributos numéricos são criados com uma chamada ao construtor da classe usando somente o nome do atributo como argumento. Atributos discretos ou nominais devem ser criados usando o construtor que recebe o nome do atributo e uma lista de valores (rótulos) que podem ser usados para aquele atributo. Esta lista deve ser uma instância de FastVector, uma implementação interna do Weka similar à classe devemos usar uma instância de 2. Criamos uma lista (novamente instância de FastVector igual à FastVector) null. dos atributos. Vector. Para atributos textuais Usamos a lista de atributos para criar uma Instances, passando para o construtor da classe um nome para a relação, a lista de atributos Instances corresponde à relação contida em um arquivo .ARFF. e uma capacidade inicial. Esta classe de 3. Com a instância de Instances podemos criar várias instâncias de Instance (no singular), onde cada uma corresponde a somente uma entrada na base de dados (linha da tabela), e popular esta instância de Instance 4. O método com os dados. Devemos também associar a instância de Instance à de Instances. toString da classe Instances pode ser usado para convertê-la para uma representação textual .ARFF. que pode ser armazenada em um arquivo Estes passos são demonstrados pela classe Listagem 2: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 CreateArtificialARFF, CreateArtificialARFF, que é mostrada na listagem 2. que cria uma relação de dados para uso com o Weka. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import weka . core .*; /* * * Esta classe cria uma relação artificial de dados , demonstrando como criar a estrutura * de um arquivo . ARFF usando Java . */ public class CreateArtificialARFF { public static void main ( String [] args ) { // Primeiro precisamos criar os atributos para a relação . Atributos nominais são // criados já com os valores permitidos ; atributos reais não . // Nome : String ( sem valores prévios ). Attribute nome = new Attribute (" nome " ,( FastVector ) null ); // Profissão : nominal com valores prévios . FastVector vProfissões = new FastVector (); vProfissões . addElement (" Professor " ); vProfissões . addElement (" Bolsista " ); vProfissões . addElement (" Motorista " ); vProfissões . addElement (" Médico " ); vProfissões . addElement (" Pesquisador " ); Attribute profissão = new Attribute (" profissao " , vProfissões ); // Saldo médio mensal : real . Attribute saldo = new Attribute (" saldo " ); // Idade : real . Attribute idade = new Attribute (" idade " ); // Anos como correntista : real . Attribute anosConta = new Attribute (" anosComoCorrentista " ); // Já fez empréstimo : nominal com valores prévios . FastVector vSN = new FastVector (); vSN . addElement (" SIM " ); Rafael Santos http://www.lac.inpe.br/∼rafael.santos 8 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 Mineração e Visualização de Dados usando Java vSN . addElement (" NÃO " ); Attribute empréstimoPrévio = new Attribute (" emprestimoPrevio " , vSN ); // Plano de investimentos sugerido : nominal com valores prévios . FastVector vPIS = new FastVector (); vPIS . addElement (" Prata " ); vPIS . addElement (" Ouro " ); vPIS . addElement (" Platina " ); Attribute planoSugerido = new Attribute (" planoSugerido " , vPIS ); // Com todos os atributos podemos criar a relação . FastVector atributos = new FastVector (); atributos . addElement ( nome ); atributos . addElement ( profissão ); atributos . addElement ( saldo ); atributos . addElement ( idade ); atributos . addElement ( anosConta ); atributos . addElement ( empréstimoPrévio ); atributos . addElement ( planoSugerido ); // Criamos a instância de Instances Instances correntistas = new Instances (" correntistas " , atributos ,0); // Agora podemos preencher a relação com instâncias . Instance i1 = new Instance ( correntistas . numAttributes ()); i1 . setValue ( nome ," Sid Sackson " ); i1 . setValue ( profissão ," Médico " ); i1 . setValue ( saldo ,4300.0); i1 . setValue ( idade ,48); i1 . setValue ( anosConta ,7); i1 . setValue ( empréstimoPrévio , " NÃO " ); i1 . setValue ( planoSugerido ," Ouro " ); correntistas . add ( i1 ); // Mais uma instância , só para exemplificar ... Instance i2 = new Instance ( correntistas . numAttributes ()); i2 . setValue ( nome ," Ian Schreiber " ); i2 . setValue ( profissão ," Professor " ); i2 . setValue ( saldo ,3800.0); i2 . setValue ( idade ,52); i2 . setValue ( anosConta ,11); i2 . setValue ( empréstimoPrévio , " SIM " ); i2 . setValue ( planoSugerido ," Platina " ); correntistas . add ( i2 ); // Podemos " imprimir " a relação . System . out . println ( correntistas ); } } A relação criada com a classe CreateArtificialARFF Listagem 3: Relação criada pela classe 1 2 3 4 5 6 7 8 9 10 11 12 13 (listagem 2) é mostrada na listagem 3. CreateArtificialARFF (listagem 2). @relation correntistas @attribute @attribute @attribute @attribute @attribute @attribute @attribute nome string profissao { Professor , Bolsista , Motorista , Médico , Pesquisador } saldo numeric idade numeric anosComoCorrentista numeric emprestimoPrevio { SIM , NÃO } planoSugerido { Prata , Ouro , Platina } @data ' Sid Sackson ' , Médico ,4300 ,48 ,7 , NÃO , Ouro ' Ian Schreiber ' , Professor ,3800 ,52 ,11 , SIM , Platina http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 4 4.1 9 Weka Árvores de Decisão Introdução A API Weka implementa um classicador baseado em árvores de decisão (algoritmo C4.5 [7]) que pode classicar dados com atributos numéricos e nominais (mas com classe sempre representada por atributo nominal). Uma outra vantagem de classicadores baseados em árvores de decisão é que os modelos criados para classicação podem ser facilmente interpretáveis. 4.2 Usando Árvores de Decisão em Aplicações em Java Como um primeiro exemplo simples de uso de árvores de decisão em aplicações, veremos uma aplicação que cria uma árvore de decisão a partir de dados lidos de um arquivo .ARFF e classica estes mesmos dados com a árvore criada, mostrando como resultado a árvore e a quantidade de instâncias classicadas corretamente. Árvores de decisão baseadas no algoritmo C4.5 são implementadas na API Weka como instâncias da classe J48. Instâncias desta classe podem ser construídas e usadas com valores default para parâmetros criação de árvores de decisão, e métodos setXXX podem ser usados para modicação destes parâmetros. classifyInstance da instância de J48 que recebe como arguInstance e retorna o índice da classe para aquela instância. Em instâncias já rotuladas Após o treinamento podemos usar o método mento uma instância de podemos recuperar a classe original para vericar a performance do classicador baseado em redes neurais. A classe ExemploJ48, que é mostrada na listagem 4, implementa estes passos (leitura de instâncias, criação da árvore, classicação dos próprios dados usados para treinamento). Listagem 4: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ExemploJ48, que demonstra o uso de uma árvore de decisão em uma aplicação em Java. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io . FileReader ; import weka . classifiers . trees . J48 ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em árvores de decisão . A classe * lê um arquivo no formato . ARFF , cria o classificador e classifica o próprio arquivo original . */ public class ExemploJ48 { public static void main ( String [] args ) throws Exception { // Usaremos a base de flores íris . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / iris . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (4); // Criamos o classificador baseado em árvores de decisão . J48 árvore = new J48 (); árvore . buildClassifier ( instâncias ); // Mostramos a árvore como texto . System . out . println ( árvore ); // Mostramos a árvore como código . System . out . println ( árvore . toSource ( " ClassificadorJ48 " )); // Agora vamos classificar cada dado original com esta rede . int corretas = 0; for ( int i =0; i < instâncias . numInstances (); i ++) { // Recuperamos cada uma das instâncias . Rafael Santos http://www.lac.inpe.br/∼rafael.santos 10 37 38 39 40 41 42 43 44 45 46 47 48 Mineração e Visualização de Dados usando Java Instance instância = instâncias . instance (i ); // Classificamos esta instância . int classe = ( int )( árvore . classifyInstance ( instância )); if ( classe == ( int ) instância . classValue ()) corretas ++; } // Relatório simples : System . out . println ( " De "+ instâncias . numInstances ()+ " instâncias , "+ corretas + " ("+ (100.*( corretas /(1.0* instâncias . numInstances ())))+ " %) foram classificadas corretamente . " ); // De 150 instâncias , 147 (98.0%) foram classificadas corretamente . } } ExemploJ48 (listagem 4) usamos o método System.out.println para imprimir o conteúdo da instância da classe J48. O resultado da chamada deste método é mostrado na listagem 5. Na linha 29 da aplicação na classe Listagem 5: Conteúdo textual da árvore de decisão criada na classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ExemploJ48. J48 pruned tree -----------------petalwidth <= 0.6: Iris - setosa (50.0) petalwidth > 0.6 | petalwidth <= 1.7 | | petallength <= 4.9: Iris - versicolor (48.0/1.0) | | petallength > 4.9 | | | petalwidth <= 1.5: Iris - virginica (3.0) | | | petalwidth > 1.5: Iris - versicolor (3.0/1.0) | petalwidth > 1.7: Iris - virginica (46.0/1.0) Number of Leaves : Size of the tree : 5 9 Na linha 31 da aplicação na classe ExemploJ48 (listagem 4) executamos o método toSource da classe J48. Este método cria uma representação da árvore na forma de uma classe em Java (cujo nome é dado pelo argumento para o método) que pode ser analisada e incorporada em projetos esta classe contém um método estático (classify) que recebe como argumento um array de A classe ClassificadorJ48, Objects e retorna o índice da classe (como um double). mostrada na listagem 6, mostra a classe criada pelo método toSource, na forma- tação original. Listagem 6: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ClassificadorJ48, criada pela instância de J48 na classe ExemploJ48. class ClassificadorJ48 { public static double classify ( Object [] i) throws Exception { double p = Double . NaN ; p = ClassificadorJ48 . N183f74d0 (i ); return p; } static double N183f74d0 ( Object [] i) { double p = Double . NaN ; if (i [3] == null ) { p = 0; } else if ((( Double ) i [3]). doubleValue () <= 0.6) { p = 0; } else if ((( Double ) i [3]). doubleValue () > 0.6) { p = ClassificadorJ48 . Ne102dc1 (i ); } return p; } static double Ne102dc1 ( Object [] i ) { double p = Double . NaN ; http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 11 if (i [3] == null ) { p = 1; } else if ((( Double ) i [3]). doubleValue () <= 1.7) { p = ClassificadorJ48 . N82c01f2 (i ); } else if ((( Double ) i [3]). doubleValue () > 1.7) { p = 2; } return p; } } static double N82c01f2 ( Object [] i ) { double p = Double . NaN ; if (i [2] == null ) { p = 1; } else if ((( Double ) i [2]). doubleValue () p = 1; } else if ((( Double ) i [2]). doubleValue () p = ClassificadorJ48 . N1337963 (i ); } return p; } static double N1337963 ( Object [] i ) { double p = Double . NaN ; if (i [3] == null ) { p = 2; } else if ((( Double ) i [3]). doubleValue () p = 2; } else if ((( Double ) i [3]). doubleValue () p = 1; } return p; } <= 4.9) { > 4.9) { <= 1.5) { > 1.5) { Embora possamos armazenar a estrutura da árvore de decisão como código, isto pode não ser prático em muitos casos pois requer adaptação e recompilação. Podemos armazenar a estrutura da árvore serializada para uso posterior, separando as funções de criação e aplicação da árvore. O armazenamento da árvore serializada é simples pois a classe J48 implementa a interface Serializable, podendo ser armazenada em disco com poucas linhas de código. Duas classes, mostradas a seguir, demonstram como armazenar uma árvore criada para uso posterior. A classe ExemploJ48_2_Create, lizada em um arquivo usando a classe Listagem 7: A classe .ARFF e ObjectOutputStream. na listagem 7, lê um arquivo ExemploJ48_2_Create, cria uma instância de J48, armazenando-a seria- que demonstra o uso de uma árvore de decisão em uma aplicação em Java. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io .*; import weka . classifiers . trees . J48 ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em árvores de decisão . A classe * lê um arquivo no formato . ARFF , cria o classificador e o armazena para uso posterior . */ public class ExemploJ48_2_Create { public static void main ( String [] args ) throws Exception { // Usaremos a base de flores wine . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / wine . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (13); Rafael Santos http://www.lac.inpe.br/∼rafael.santos 12 25 26 27 28 29 30 31 32 33 34 Mineração e Visualização de Dados usando Java // Criamos o classificador baseado em árvores de decisão . J48 árvore = new J48 (); árvore . buildClassifier ( instâncias ); // Serializamos a árvore . Outros mecanismos de serialização poderiam ser usados . ObjectOutputStream oos = new ObjectOutputStream ( new FileOutputStream (" tree - wine . ser " )); oos . writeObject ( árvore ); oos . close (); } } ExemploJ48_2_Create (listagem 7) é recuperada e usada para classicar ExemploJ48_2_Use (listagem 8). A árvore criada e armazenada na classe dados, como demonstrado na classe Listagem 8: A classe ExemploJ48_2_Use, que demonstra o uso de uma árvore de decisão em uma aplicação em Java. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io .*; import weka . classifiers . trees . J48 ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em árvores de decisão . A classe * lê um arquivo no formato . ARFF , cria o classificador e classifica o próprio arquivo original . */ public class ExemploJ48_2_Use { public static void main ( String [] args ) throws Exception { // Recuperamos a árvore previamente serializada . ObjectInputStream ois = new ObjectInputStream ( new FileInputStream (" tree - wine . ser " )); J48 árvore = ( J48 ) ois . readObject (); ois . close (); // Usaremos a base de flores íris . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / wine . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (13); // Agora vamos classificar cada dado original com esta rede . int corretas = 0; for ( int i =0; i < instâncias . numInstances (); i ++) { // Recuperamos cada uma das instâncias . Instance instância = instâncias . instance (i ); // Classificamos esta instância . int classe = ( int )( árvore . classifyInstance ( instância )); if ( classe == ( int ) instância . classValue ()) corretas ++; } // Relatório simples : System . out . println ( " De "+ instâncias . numInstances ()+ " instâncias , "+ corretas + " ("+ (100.*( corretas /(1.0* instâncias . numInstances ())))+ " %) foram classificadas corretamente . " ); // De 178 instâncias , 176 (98.87640449438202%) foram classificadas corretamente . } } http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 5 5.1 Weka Redes Neurais 13 Multilayer Perceptron Introdução A API Weka implementa um classicador baseado em redes neurais de perceptrons em múltiplas camadas (Multilayer Perceptron) que são capazes de classicar dados cujos atributos sejam numéricos (com classe discreta). Este classicador apresenta excelente capacidade de separação de dados, mas pela sua natureza seu algoritmo de treinamento que pode ser computacionalmente custoso. Uma apresentação dos detalhes do algoritmo e de sua fundamentação matemática estão fora do escopo deste documento as referências [2, 4, 6, 9, 11] apresentam mais detalhes do algoritmo, suas variantes e aplicações. 5.2 Usando Redes Neurais Multilayer Perceptron em Aplicações em Java Como um primeiro exemplo simples de uso de redes neurais (Multilayer Perceptron) em aplicações, veremos uma aplicação que cria uma rede neural, a treina usando dados lidos de um arquivo .ARFF e classica estes mes- mos dados com a rede treinada, mostrando como resultado a quantidade de instâncias classicadas corretamente. MultilayerPerceptron, A classe parte da API Weka, é usada para representar e processar uma rede neural Multilayer Perceptron. Para usar uma instância de MultilayerPerceptron devemos primeiro ter uma instância Instances devidamente populada e com uma indicação de qual atributo deve ser usado como classe. A insMultilayerPerceptron é criada e usamos vários métodos setXXX para congurar a sua arquitetura e parâmetros de criação e treinamento. Finalmente usamos a instância de Instances como argumento para o método buildClassifier da instância da rede para treinar a mesma. de tância de Após o treinamento podemos usar o método instância de Instance classifyInstance da rede que recebe como argumento uma e retorna o índice da classe para aquela instância. Em instâncias já rotuladas podemos recuperar a classe original para vericar a performance do classicador baseado em redes neurais. A classe ExemploNN, que é mostrada na listagem 9, implementa estes passos (leitura de instâncias, criação da rede, treinamento e classicação dos próprios dados usados para treinamento). Listagem 9: A classe ExemploNN, que demonstra o uso de uma rede neural Multilayer Perceptron em uma aplicação em Java. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io . FileReader ; import weka . classifiers . functions . MultilayerPerceptron ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em multilayer perceptrons . A classe * lê um arquivo no formato . ARFF , cria o classificador e classifica o próprio arquivo original . */ public class ExemploNN { public static void main ( String [] args ) throws Exception { // Usaremos a base de flores íris . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / iris . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (4); // Criamos o classificador baseado em Multilayer Perceptrons MultilayerPerceptron mlp = new MultilayerPerceptron (); mlp . setAutoBuild ( true ); mlp . setLearningRate (0.3); mlp . setMomentum (0.2); Rafael Santos http://www.lac.inpe.br/∼rafael.santos 14 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 Mineração e Visualização de Dados usando Java mlp . setTrainingTime (1000); mlp . setHiddenLayers ("5" ); // Treinamos a rede . mlp . buildClassifier ( instâncias ); // Agora vamos classificar cada dado original com esta rede . int corretas = 0; for ( int i =0; i < instâncias . numInstances (); i ++) { // Recuperamos cada uma das instâncias . Instance instância = instâncias . instance (i ); // Classificamos esta instância . int classe = ( int )( mlp . classifyInstance ( instância )); if ( classe == ( int ) instância . classValue ()) corretas ++; } // Relatório simples : System . out . println ( " De "+ instâncias . numInstances ()+ " instâncias , "+ corretas + " ("+ (100.*( corretas /(1.0* instâncias . numInstances ())))+ " %) foram classificadas corretamente . " ); // De 150 instâncias , 148 (98.66666666666667%) foram classificadas corretamente . } } Eventualmente será mais interessante separar o treinamento da rede neural da classicação de dados com ela assim podemos treinar a rede neural com um conjunto adequado de dados, armazenar a rede treinada e usá-la em outros momentos para a classicação. A separação da fase de treinamento da fase de classicação é trivial: como muitas classes da API Weka implementam a interface Serializable, podemos armazenar instâncias inteiras em objetos em arquivos com poucas linhas de código. ExemploNN2_Train, na listagem 10, MultilayerPerceptron, treinando-a e armazenando-a em um usando a classe ObjectOutputStream. Esta técnica é implementada em duas classes, mostradas a seguir. A classe lê um arquivo .ARFF e cria uma instância de arquivo em disco através de serialização Listagem 10: A classe ExemploNN2_Train, que cria e treina uma rede neural Multilayer Perceptron para uso posterior. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io .*; import weka . classifiers . functions . MultilayerPerceptron ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em multilayer perceptrons . A classe * lê um arquivo no formato . ARFF , cria o classificador e o armazena para uso posterior . */ public class ExemploNN2_Train { public static void main ( String [] args ) throws Exception { // Usaremos a base de segmentos de imagens digitais . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / segment - challenge . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (19); // Criamos o classificador baseado em Multilayer Perceptrons MultilayerPerceptron mlp = new MultilayerPerceptron (); mlp . setAutoBuild ( true ); mlp . setLearningRate (0.5); mlp . setMomentum (0.2); mlp . setTrainingTime (2000); mlp . setHiddenLayers (" 10 " ); // Treinamos a rede . mlp . buildClassifier ( instâncias ); // Serializamos a rede treinada . Outros mecanismos de serialização poderiam ser usados . ObjectOutputStream oos = new ObjectOutputStream ( new FileOutputStream ("nn - segment . ser " )); http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 36 37 38 39 40 15 oos . writeObject ( mlp ); oos . close (); } } A classe ExemploNN2_Train, mostrada na listagem 11, recupera a instância de MultilayerPerceptron treinada ExemploNN2_Train usando uma instância de ObjectInputStream, e usa a rede para classicar uma na classe base compatível de dados. Listagem 11: A classe ExemploNN2_Train, que usa a rede treinada anteriormente para classicar um conjunto de dados. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io .*; import weka . classifiers . functions . MultilayerPerceptron ; import weka . core .*; /* * * Esta classe demonstra o uso de um classificador baseado em multilayer perceptrons . A classe * lê uma instância de MultilayerPerceptron previamente criada e serializada e a usa para * classificar dados compatíveis obtidos de um arquivo no formato . ARFF . */ public class ExemploNN2_Use { public static void main ( String [] args ) throws Exception { // Lemos a instância de MultilayerPerceptron previamente serializada . ObjectInputStream ois = new ObjectInputStream ( new FileInputStream ("nn - segment . ser " )); MultilayerPerceptron mlp = ( MultilayerPerceptron ) ois . readObject (); ois . close (); // Usaremos outra base de segmentos de imagens digitais para classificação . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / segment - test . arff " ); Instances instâncias = new Instances ( reader ); instâncias . setClassIndex (19); // Agora vamos classificar cada dado com esta rede . int corretas = 0; for ( int i =0; i < instâncias . numInstances (); i ++) { // Recuperamos cada uma das instâncias . Instance instância = instâncias . instance (i ); // Classificamos esta instância . int classe = ( int )( mlp . classifyInstance ( instância )); if ( classe == ( int ) instância . classValue ()) corretas ++; } // Relatório simples : System . out . println ( " De "+ instâncias . numInstances ()+ " instâncias , "+ corretas + " ("+ (100.*( corretas /(1.0* instâncias . numInstances ())))+ " %) foram classificadas corretamente . " ); // De 810 instâncias , 767 (94.69135802469137%) foram classificadas corretamente . } } Rafael Santos http://www.lac.inpe.br/∼rafael.santos 16 6 Mineração e Visualização de Dados usando Java Weka Agrupamento com 6.1 K-Means Introdução A API Weka implementa também algoritmos de agrupamento; entre eles o K-Means. Este algoritmo possibilita a criação de grupos a partir de instâncias com atributos numéricos. Instâncias semelhantes entre si no espaço de atributos serão incluidas no mesmo grupo; instâncias em outro grupo serão, por denição, distantes no espaço de atributos. Mais explicações sobre este algoritmo podem ser vistas em [6, 9, 11]. 6.2 Usando o Algoritmo K-Means em Aplicações em Java A classe SimpleKMeans, parte da API Weka, é usada para representar e fazer o agrupamento de dados numéricos. Para fazer o agrupamento devemos criar uma instância da classe, indicar o número de grupos a ser criado com setNumClusters, informar outros parâmetros se necessário e executar o agrupamento com buildClusterer que recebe como argumento uma instância de Instances com a base de dados. o método o método getClusterSizes getClusterCentroids. Outros métodos perExemploKMeans, mostrada na listagem 12, mostra Ao nal do agrupamento podemos obter o número de dados em cada grupo com uma chamada a e obter as médias (centróides) dos grupos com uma chamada a mitem obter outras estatísticas sobre agrupamento. A classe como executar estes passos. Listagem 12: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ExemploKMeans, que demonstra o uso do algoritmo K-Means em uma aplicação em Java. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package weka ; import java . io . FileReader ; import weka . clusterers . SimpleKMeans ; import weka . core .*; /* * * Esta classe demonstra o uso de um algoritmo de agrupamento (K - Means ). A classe * lê um arquivo no formato . ARFF , cria o agrupamento e classifica o próprio arquivo original . */ public class ExemploKMeans { public static void main ( String [] args ) throws Exception { // Usaremos a base de flores íris . FileReader reader = new FileReader ("/ home / rafael / Java / APIs / Weka / data / iris . arff " ); Instances instâncias = new Instances ( reader ); // Queremos ignorar a classe das instâncias . instâncias . deleteAttributeAt (4); // Criamos o agrupamento . SimpleKMeans agrupamento = new SimpleKMeans (); agrupamento . setNumClusters (3); agrupamento . setDisplayStdDevs ( true ); agrupamento . buildClusterer ( instâncias ); // Mostramos estatísticas sobre o agrupamento . Instances clusterCenters = agrupamento . getClusterCentroids (); Instances clusterSTDDEVs = agrupamento . getClusterStandardDevs (); int [] clusterSizes = agrupamento . getClusterSizes (); for ( int cluster =0; cluster < clusterCenters . numInstances (); cluster ++) { System . out . println ( " Cluster # " +( cluster +1)+ ": "+ clusterSizes [ cluster ]+ " dados ." ); System . out . println ( " Centróide : "+ clusterCenters . instance ( cluster )); System . out . println ( " STDDEV : " + clusterSTDDEVs . instance ( cluster )); } } } http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java O resultado da execução da classe Listagem 13: A classe ExemploKMeans ExemploKMeans, 17 é mostrado na listagem 13. que demonstra o uso de uma rede neural Multilayer Perceptron em uma aplicação em Java. 1 2 3 4 5 6 7 8 9 Cluster #1: 61 dados . Centróide : 5.888525 ,2.737705 ,4.396721 ,1.418033 STDDEV : 0.448738 ,0.293351 ,0.52693 ,0.272341 Cluster #2: 50 dados . Centróide : 5.006 ,3.418 ,1.464 ,0.244 STDDEV : 0.35249 ,0.381024 ,0.173511 ,0.10721 Cluster #3: 39 dados . Centróide : 6.846154 ,3.082051 ,5.702564 ,2.079487 STDDEV : 0.502544 ,0.279917 ,0.519355 ,0.281144 Rafael Santos http://www.lac.inpe.br/∼rafael.santos 18 7 7.1 Mineração e Visualização de Dados usando Java Visualização usando Coordenadas Paralelas Introdução A técnica Coordenadas Paralelas (Parallel Coordinates) [5] permite a visualização de dados multidimensionais de qualquer natureza (numéricos ou nominais) em um gráco em duas dimensões. As dimensões dos dados são transformadas em eixos paralelos, tradicionalmente apresentados na vertical. Cada dado multidimensional é plotado como uma linha que passa pelos eixos paralelos, cruzando estes na altura dos valores dos atributos correspondentes. Este tipo de gráco permite a visualização de agrupamentos como feixes de dados e outliers como linhas fora destes feixes. Permite também a visualização da distribuição dos valores ao longo de uma dimensão/eixo e da correlação dos valores entre dimensões/eixos, assim como estimativa da separabilidade entre classes em função dos valores dos atributos. O gráco pode ser usado com dados rotulados ou não, numéricos (em qualquer escala, que pode ser diferente para dimensões diferentes) ou nominais. Uma limitação da técnica, pela sua própria natureza, é que os eixos são mostrados paralelos uns aos outros, e correlações entre valores de dimensões diferentes podem ou não aparecer em evidência dependendo de como os eixos são arranjados por exemplo, se existir correlação entre duas dimensões mas os eixos correspondentes estiverem distantes no gráco, a correlação pode não aparecer em evidência. A visualização de dados com coordenadas paralelas é mais facilmente compreendida com um exemplo. A listagem 14 mostra um arquivo .ARFF com uma versão reduzida a relação de dados do problema jogar dependendo do tempo (veja também a listagem 1). Esta relação contém somente três instâncias, com cinco atributos, dos quais o último é considerado a classe ou consequente. A gura 5 mostra o gráco de coordenadas paralelas correspondente a estes dados nesta gura podemos ver os valores dos atributos para cada instância plotadas da forma descrita. Listagem 14: O arquivo weather-3.arff, com uma versão reduzida da relação do problema jogar dependendo do tempo. 1 2 3 4 5 6 7 8 9 10 11 12 @relation weather -3 @attribute @attribute @attribute @attribute @attribute outlook { sunny , overcast , rainy } temperature real humidity real windy { TRUE , FALSE } play { yes , no } @data sunny ,85 ,85 , FALSE , no overcast ,83 ,86 , FALSE , yes rainy ,65 ,70 , TRUE , no Figura 5: Plotagem das coordenadas paralelas da relação http://www.lac.inpe.br/∼rafael.santos weather-3.arff. Rafael Santos Mineração e Visualização de Dados usando Java 7.2 19 Implementação O gráco das coordenadas paralelas é implementado como um componente gráco em Java. Este componente pode ser usado em aplicações grácas para visualização e para criação de grácos em arquivo. O componente Instances, da API Weka, para acessar todos os atributos e valores de uma base de dados Instance contém todos os dados apresentados em um arquivo .ARFF, menos os comentários do usará uma instância de (instâncias de mesmo). O componente permitirá a conguração de sua aparência (cores, margens, fontes, etc.) através de uma classe externa. A classe que implementa o componente é chamada ParallelCoordinatesComponent e é mostrada na listagem 15. Listagem 15: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 ParallelCoordinatesComponent. /* * * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package parcoords ; import java . awt .*; import java . util .*; import javax . swing . JComponent ; import weka . core .*; public class ParallelCoordinatesComponent extends JComponent { // Cópia local das instâncias . private Instances instances ; // Valores extremos para atributos numéricos . private TreeMap < Integer , double [] > extrema ; // Valores extremos globais para atributos numéricos . private double [] globalExtrema ; // Valores discretos para atributos nominais . private TreeMap < Integer , HashSet < String > > nominalValues ; // Devemos usar valores extremos de forma global ? private boolean useGlobalExtrema ; // Devemos usar cores diferentes para as classes ( último atributo )? private boolean useDifferentColors ; // Quanto podemos " sacudir " as linhas para evitar superposição ? private double wiggle ; // Criamos uma isntância local para as constantes do gráfico . private ParallelCoordinatesConstants consts = new ParallelCoordinatesConstants (); /* * * O construtor para a classe , que inicializa os atributos e cria a interface do componente . */ public ParallelCoordinatesComponent ( Instances instances , boolean useGlobalExtrema , boolean useDifferentColors , double wiggle ) { super (); // Copia atributos . this . instances = instances ; this . useGlobalExtrema = useGlobalExtrema ; this . useDifferentColors = useDifferentColors ; this . wiggle = wiggle ; // Calcula estatísticas básicas sobre os dados . int nAxis = instances . numAttributes (); // Alocamos as estruturas - base . extrema = new TreeMap < Integer , double [] >(); nominalValues = new TreeMap < Integer , HashSet < String > >(); globalExtrema = new double [2]; // Alocamos os elementos necessários nos mapas ( dependendo do tipo do atributo ). for ( int a =0; a < nAxis ;a ++) { Attribute attrib = instances . attribute (a ); if ( attrib . isNumeric ()) Rafael Santos http://www.lac.inpe.br/∼rafael.santos 20 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 Mineração e Visualização de Dados usando Java extrema . put (a , new double []{ Double . MAX_VALUE , Double . MIN_VALUE }); else if ( attrib . isNominal ()) nominalValues . put (a , new HashSet < String >()); } // Para cada instância : se numérica atualizamos os extremos , se discreta incluimos os valores . for ( int i =0; i < instances . numInstances (); i ++) { // Recuperamos uma instância ... a API do Weka permite escrever código bizarro ! Instance instance = instances . instance (i ); // Para cada atributo desta instância ... for ( int a =0; a < nAxis ;a ++) { // Se for numérico ... if ( instances . attribute (a ). isNumeric ()) { // Recuperamos o valor e mudamos os extremos para aquele atributo e // os extremos globais . double value = instance . value (a ); extrema . get (a )[0] = Math . min ( extrema . get (a )[0] , value ); extrema . get (a )[1] = Math . max ( extrema . get (a )[1] , value ); globalExtrema [0] = Math . min ( globalExtrema [0] , value ); globalExtrema [1] = Math . max ( globalExtrema [1] , value ); } // Se for nominal ... else if ( instances . attribute (a ). isNominal ()) { // Recuperamos todos os valores para aquele atributo nominal . Enumeration valuesE = instances . attribute (a ). enumerateValues (); // Armazenamos na nossa estrutura . while ( valuesE . hasMoreElements ()) nominalValues . get (a ). add (( String ) valuesE . nextElement ()); } } // Fim do laço para cada atributo ... } // Fim do laço para cada instância ... } /* * * Este método muda os valores extremos globais . */ public void setExtrema ( double min , double max ) { globalExtrema [0] = min ; globalExtrema [1] = max ; } /* * * Este método cria o componente , chamando vários métodos auxiliares . */ protected void paintComponent ( Graphics g) { // Recuperamos as dimensões do componente . int width = getSize (). width ; int height = getSize (). height ; // Recuperamos a área de plotagem ( dimensões menos as bordas ). int plotWidth = width - consts . leftMargin - consts . rightMargin ; int plotHeight = height - consts . topMargin - consts . bottomMargin ; // Usaremos Graphics2D para melhor aparência . Graphics2D g2d = ( Graphics2D )g ; g2d . setRenderingHint ( RenderingHints . KEY_ANTIALIASING , RenderingHints . VALUE_ANTIALIAS_ON ); // Pintamos o fundo do componente . g2d . setColor ( consts . plottingAreaBackground ); g2d . fillRect (0 ,0 , width , height ); // Pintamos o fundo da área de plotagem . g2d . setColor ( consts . plottingAreaColor ); g2d . fillRect ( consts . leftMargin , consts . topMargin , plotWidth , plotHeight ); // Pintamos primeiro os dados ( para que os eixos não desapareçam sob eles !). // Para cada instância ... for ( int inst =0; inst < instances . numInstances (); inst ++) { Instance instance = instances . instance ( inst ); // Calculamos as coordenadas " de " e " para " cada segmento . double x =0 , y =0 , lastX =0 , lastY =0; // Para cada um de seus atributos ... for ( int attr =0; attr < instance . numAttributes (); attr ++) http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 21 { // Recuperamos as coordenadas X e Y de cada atributo ( de forma diferente para // atributos numéricos e nominais ). switch ( instances . attribute ( attr ). type ()) { case Attribute . NUMERIC : { x = getXForAttribute ( attr , plotWidth ); y = getYForNumericAttribute ( instance . value ( attr ), attr , plotHeight ); break ; } case Attribute . NOMINAL : { x = getXForAttribute ( attr , plotWidth ); // Qual é o nome do atributo para recuperar a coordenada Y? String sAttr = instances . instance ( inst ). stringValue ( attr ); y = getYForNominalAttribute ( sAttr , attr , plotHeight ); break ; } } // Devemos sacudir a linha para evitar superposição ? Cada unidade de sacudida // causa uma variação entre -50 e 50 pixels ). if ( wiggle > 0) { double wpix = ( wiggle *50) -( wiggle *100* Math . random ()); y += wpix ; } // Somente plotaremos quando tivermos dois pares de X , Y. if ( attr > 0) { // Usamos uma só cor ou uma para cada classe ? if ( useDifferentColors ) { // Assumimos que o último atributo é a classe , e a cor será selecionada // a partir do valor deste . int colorIndex = ( int ) instance . value ( instance . numAttributes () -1); g2d . setColor ( consts . linesColors [ colorIndex ]); } else g2d . setColor ( consts . plottingLineColor ); // Mudamos o stroke e desenhamos a linha . g2d . setStroke ( consts . stroke ); g2d . drawLine (( int )x ,( int )y ,( int ) lastX ,( int ) lastY ); } // Guardamos as últimas coordenadas usadas para o próximo segmento . lastX = x ; lastY = y; } // Fim do laço para cada atributo . } // Fim do laço para cada instância . // Voltamos ao stroke normal . g2d . setStroke ( new BasicStroke (1 f )); // OK , os dados foram plotados , vamos plotar os eixos paralelos , novamente com métodos // diferentes para atributos numéricos e nominais . for ( int attr =0; attr < instances . numAttributes (); attr ++) { switch ( instances . attribute ( attr ). type ()) { case Attribute . NUMERIC : paintNumericAxis ( g2d , attr , plotWidth , plotHeight ); break ; case Attribute . NOMINAL : paintNominalAxis ( g2d , attr , plotWidth , plotHeight ); break ; } } // Pintamos o título dos atributos . g2d . setFont ( consts . axisTitlesFont ); for ( int attr =0; attr < instances . numAttributes (); attr ++) { String name = instances . attribute ( attr ). name (); g2d . drawString ( name ,( int ) getXForAttribute ( attr , plotWidth ), consts . topMargin -20); } // Pintamos o título do gráfico , que será o da relação no arquivo . ARFF . g2d . setFont ( consts . titleFont ); FontMetrics m = g2d . getFontMetrics (); String name = instances . relationName (); int x = width /2 - m. stringWidth ( name )/2; g2d . drawString ( name ,x , consts . topMargin -45); } /* Rafael Santos http://www.lac.inpe.br/∼rafael.santos 22 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 Mineração e Visualização de Dados usando Java * Calculamos a coordenada X para um atributo específico , considerando a área de plotagem . * O valor da coordenada X depende somente do índice do eixo do atributo correspondente . */ private double getXForAttribute ( int axis , int pwidth ) { int nAxis = instances . numAttributes (); double x = consts . leftMargin + axis *( pwidth /( nAxis -1)); return x; } /* * Calculamos a coordenada Y para um valor e atributo numérico específico , considerando a * área de plotagem . O valor será normalizado dependendo dos valores extremos globais ou do * atributo . */ private double getYForNumericAttribute ( double value , int axis , int pheight ) { double delta , val =0; // Usamos extremos globais ? if ( useGlobalExtrema ) { int nAxis = instances . numAttributes (); delta = globalExtrema [1] - globalExtrema [0]; val = consts . topMargin + pheight *(1 -( value - globalExtrema [0])/ delta ); } else { delta = extrema . get ( axis )[1] - extrema . get ( axis )[0]; val = consts . topMargin + pheight *(1 -( value - extrema . get ( axis )[0])/ delta ); } return val ; } /* * Calculamos a coordenada Y para um valor e atributo nominal específico , considerando a * área de plotagem . */ private double getYForNominalAttribute ( String whichAttr , int axis , int pheight ) { // Muito mais fácil com nominais do que com numéricos ! ArrayList < String > theseValues = new ArrayList < String >( nominalValues . get ( axis )); int index = theseValues . indexOf ( whichAttr ); double y = consts . topMargin + index *( pheight /( nominalValues . get ( axis ). size () -1)); return y; } /* * Desenhamos um eixo para atributo numérico . */ private void paintNumericAxis ( Graphics2D g2d , int axis , int pwidth , int pheight ) { int nAxis = instances . numAttributes (); // Calculamos o valor para espaçamento das marcas no eixo . double delta ; if ( useGlobalExtrema ) delta = ( globalExtrema [1] - globalExtrema [0])/( consts . numMarks -1); else delta = ( extrema . get ( axis )[1] - extrema . get ( axis )[0])/( consts . numMarks -1); // Desenhamos o eixo . g2d . setColor ( consts . plottingAxisColorForeground ); g2d . drawLine ( consts . leftMargin + axis *( pwidth /( nAxis -1)) , consts . topMargin , consts . leftMargin + axis *( pwidth /( nAxis -1)) , pheight + consts . topMargin ); // Desenhamos as marcas e rótulos . g2d . setFont ( consts . axisLabelsFont ); for ( int n =0; n < consts . numMarks ;n ++) { // Marcas . double tic = consts . topMargin +n *( pheight /( consts . numMarks -1)); g2d . drawLine ( consts . leftMargin + axis *( pwidth /( nAxis -1)) -5 ,( int ) tic , consts . leftMargin + axis *( pwidth /( nAxis -1))+5 ,( int ) tic ); // Rótulos . double value ; if ( useGlobalExtrema ) value = globalExtrema [1] -( n* delta ); else value = extrema . get ( axis )[1] -( n* delta ); g2d . setColor ( consts . plottingAxisColorBackground ); g2d . drawString ( String . format (" %5.2 f" , value ), consts . leftMargin + axis *( pwidth /( nAxis -1))+6 ,( int ) tic +1); http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 } 23 g2d . setColor ( consts . plottingAxisColorForeground ); g2d . drawString ( String . format (" %5.2 f" , value ), consts . leftMargin + axis *( pwidth /( nAxis -1))+5 ,( int ) tic ); } /* * Desenhamos um eixo para atributo nominal . */ private void paintNominalAxis ( Graphics2D g2d , int axis , int pwidth , int pheight ) { // Internamente representamos os valores nominais deste eixo como um ArrayList -- é mais fácil . ArrayList < String > theseValues = new ArrayList < String >( nominalValues . get ( axis )); int nAxis = instances . numAttributes (); // Desenhamos o eixo . g2d . setColor ( consts . plottingAxisColorForeground ); g2d . drawLine ( consts . leftMargin + axis *( pwidth /( nAxis -1)) , consts . topMargin , consts . leftMargin + axis *( pwidth /( nAxis -1)) , pheight + consts . topMargin ); // Desenhamos as marcas e rótulos . g2d . setFont ( consts . axisLabelsFont ); for ( int n =0; n < theseValues . size (); n ++) { // Marcas . double tic = consts . topMargin +n *( pheight /( nominalValues . get ( axis ). size () -1)); g2d . drawLine ( consts . leftMargin + axis *( pwidth /( nAxis -1)) -5 ,( int ) tic , consts . leftMargin + axis *( pwidth /( nAxis -1))+5 ,( int ) tic ); // Rótulos . g2d . setColor ( consts . plottingAxisColorBackground ); g2d . drawString ( String . format (" % -20 s" , theseValues . get (n )) , consts . leftMargin + axis *( pwidth /( nAxis -1))+6 ,( int ) tic +1); g2d . setColor ( consts . plottingAxisColorForeground ); g2d . drawString ( String . format (" % -20 s" , theseValues . get (n )) , consts . leftMargin + axis *( pwidth /( nAxis -1))+5 ,( int ) tic ); } } /* * Este método permite mudar o conjunto de constantes para a plotagem . */ public void setConsts ( ParallelCoordinatesConstants consts ) { this . consts = consts ; } } Algumas características interessantes implementadas na classe ParallelCoordinatesComponent (e informadas através de parâmetros passados para seu construtor) são: . Eixos de atributos numéricos são plotados considerando os valores máximo e mínimo do eixo (atributo). Eixos numéricos podem ser normalizados para que indiquem valores entre o máximo e mínimo globais de todos os atributos numéricos. . É possível usar uma única cor para todas as linhas que representam dados ou cores individuais para cada classe ou categoria de cada linha. No caso de bases com rótulos para cada instância, assumimos que o último atributo corresponde a classe, assim se um gráco com cores distintas for criado, cada classe terá uma cor diferente (dentro de um limite de cores denidas, veja a classe ParallelCoordinatesConstants a seguir). No caso de bases sem rótulos ou classes podemos usar uma cor geral (cinza por default). . Em alguns casos podemos ter várias linhas superpostas no gráco isto pode ocorrer quando muitas instâncias ocuparem o mesmo ponto no espaço de atributos ou quando existirem poucos valores discretos para um atributo. Nestes casos podemos especiar um parâmetro chamado wiggle que dene uma pequena perturbação aleatória na posição das coordenadas Y dos dados, justamente para evita que sejam plotados uns sobre os outros (com os mais recentemente plotados ocultando os plotados anteriormente). ParallelCoordinatesComponent (listagem 15) usa vários atributos da classe ParallelCoordinatesConstants, que representa um conjunto básico (modicável) de atributos que controlam a aparência do gráco. A classe ParallelCoordinatesConstants é mostrada na listagem 16. A classe Rafael Santos http://www.lac.inpe.br/∼rafael.santos 24 Mineração e Visualização de Dados usando Java Listagem 16: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 ParallelCoordinatesConstants. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package parcoords ; import java . awt .*; /* * * Esta classe contém campos para uso pelo componente ParallelCoordinatesConstants * que definem aspectos do gráfico que são independentes dos dados em si . * Em uma implementação mais completa deveríamos ter getters e setters e os campos * deveriam ser declarados como privados . */ public class ParallelCoordinatesConstants { // Margens . public int topMargin = 75; public int bottomMargin = 15; public int leftMargin = 15; public int rightMargin = 200; // Número de marcas nos eixos das coordenadas . public int numMarks = 10; // Cores para o gráfico ( comuns ). public Color plottingAreaBackground = Color . WHITE ; public Color plottingAreaColor = new Color (252 ,252 ,255); public Color plottingAxisColorForeground = Color . BLACK ; public Color plottingAxisColorBackground = Color . LIGHT_GRAY ; public Color plottingLineColor = new Color (100 ,100 ,100 ,80); // Tabela de cores para as linhas . Se o conjunto de dados tiver mais de 20 // cores pode ser necessário modificar esta tabela . private int lineTransparency = 100; public Color [] linesColors = { new Color (255 ,0 ,0 , lineTransparency ), // Red new Color (0 ,255 ,0 , lineTransparency ), // Green new Color (0 ,0 ,255 , lineTransparency ), // Blue new Color (255 ,255 ,0 , lineTransparency ), // Yellow new Color (0 ,255 ,255 , lineTransparency ), // Cyan new Color (255 ,0 ,255 , lineTransparency ), // Magenta new Color (140 ,140 ,140 , lineTransparency ), // Medium gray new Color (60 ,60 ,60 , lineTransparency ), // Dark gray new Color (255 ,127 ,0 , lineTransparency ), // Rg . new Color (255 ,0 ,127 , lineTransparency ), // R .B new Color (0 ,127 ,255 , lineTransparency ), // . gB new Color (0 ,255 ,127 , lineTransparency ), // . Gb new Color (127 ,255 ,0 , lineTransparency ), // rG . new Color (127 ,0 ,255 , lineTransparency ), // r .B new Color (127 ,0 ,0 , lineTransparency ), // Dark red new Color (0 ,127 ,0 , lineTransparency ), // Dark green new Color (0 ,0 ,127 , lineTransparency ), // Dark blue new Color (127 ,127 ,0 , lineTransparency ), // Dark yellow new Color (0 ,127 ,127 , lineTransparency ), // Dark cyan new Color (127 ,0 ,127 , lineTransparency ), // Dark magenta }; // Tipo de linha ( por enquanto global ). public Stroke stroke = new BasicStroke (3 f ); // Fontes para o gráfico . public Font titleFont = new Font (" SansSerif " , Font . ITALIC ,24); public Font axisTitlesFont = new Font (" SansSerif " , Font . BOLD ,18); public Font axisLabelsFont = new Font (" Monospaced " , Font . BOLD ,16); } Com estas duas classes (e a API Weka) podemos criar aplicações simples para visualização de dados no formato .ARFF usando coordenadas paralelas. Uma primeira aplicação, que usa o componente praticamente sem modicações para visualizar a base iris.arff, http://www.lac.inpe.br/∼rafael.santos é mostrada na classe ExemploPC1, na listagem 17. Rafael Santos Mineração e Visualização de Dados usando Java Listagem 17: A classe ExemploPC1, 25 que implementa um exemplo simples de visualização com coordenadas paralelas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package parcoords ; import java . awt . Dimension ; import java . io .*; import javax . swing . JFrame ; import weka . core . Instances ; /* * * Exemplo simples de visualização com coordenadas paralelas . */ public class ExemploPC1 { public static void main ( String [] args ) throws IOException { // Usaremos a base iris . arff . FileReader reader = new FileReader (" / home / rafael / Java / APIs / Weka / data / iris . arff " ); Instances instances = new Instances ( reader ); // Criamos o componente com as instâncias , sem normalização global , usando cores diferentes // e sem " wiggle ". ParallelCoordinatesComponent component = new ParallelCoordinatesComponent ( instances , false , true ,0); component . setPreferredSize ( new Dimension (1024 ,500)); // Montamos a UI para visualizar o componente . JFrame frame = new JFrame ( " Parallel Coordinates " ); frame . getContentPane (). add ( component ); frame . pack (); frame . setVisible ( true ); frame . setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } } A gura 6 mostra o resultado (cópia da tela) da aplicação implementada na classe Figura 6: Tela da aplicação da classe Uma variante da visualização da base de dados iris.arff ExemploPC1 ExemploPC1 (listagem 17). (listagem 17). é implementada pela classe ExemploPC2, na lista- gem 18. Esta variante usa normalização global nos eixos numéricos e um pequeno valor de wiggle para visualizar Rafael Santos http://www.lac.inpe.br/∼rafael.santos 26 Mineração e Visualização de Dados usando Java melhor feixes de dados que são concentrados em pontos do eixo do atributo petalwidth, por exemplo. A gura 7 mostra o resultado (cópia da tela) da aplicação implementada na classe Listagem 18: A classe ExemploPC2, ExemploPC2 (listagem 18). que implementa outro exemplo simples de visualização com coordenadas paralelas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package parcoords ; import java . awt . Dimension ; import java . io .*; import javax . swing . JFrame ; import weka . core . Instances ; /* * * Exemplo simples de visualização com coordenadas paralelas . */ public class ExemploPC2 { public static void main ( String [] args ) throws IOException { // Usaremos a base iris . arff . FileReader reader = new FileReader (" / home / rafael / Java / APIs / Weka / data / iris . arff " ); Instances instances = new Instances ( reader ); // Criamos o componente com as instâncias , com normalização global , usando cores diferentes // e com um pequeno " wiggle ". ParallelCoordinatesComponent component = new ParallelCoordinatesComponent ( instances , true , true ,0.2); component . setPreferredSize ( new Dimension (1024 ,500)); // Montamos a UI para visualizar o componente . JFrame frame = new JFrame ( " Parallel Coordinates " ); frame . getContentPane (). add ( component ); frame . pack (); frame . setVisible ( true ); frame . setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } } Figura 7: Tela da aplicação da classe http://www.lac.inpe.br/∼rafael.santos ExemploPC2 (listagem 18). Rafael Santos Mineração e Visualização de Dados usando Java 27 Ainda outro exemplo de uso do componente pode ser visto na classe ExemploPC3 (listagem 19), que mostra os dados de uma relação dos sobreviventes do naufrágio do Titanic dados que só contém atributos nominais, portanto com muitos dados ocupando as mesmas posições no espaço de atributos. Para esta visualização usamos linhas com cores bem mais transparentes e um wiggle bem maior para evitar superposições. O resultado (cópia da tela) pode ser visto na gura 8. ExemploPC3, Listagem 19: A classe que implementa outro exemplo simples de visualização com coordenadas paralelas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package parcoords ; import java . awt .*; import java . io .*; import javax . swing . JFrame ; import weka . core . Instances ; /* * * Exemplo simples de visualização com coordenadas paralelas . */ public class ExemploPC3 { public static void main ( String [] args ) throws IOException { // Usaremos a base titanic . arff . FileReader reader = new FileReader (" / home / rafael / Java / APIs / Weka / data / titanic . arff " ); Instances instances = new Instances ( reader ); // Criamos uma instância de ParallelCoordinatesConstants e modificamos alguns dos // atributos , como as cores para as linhas e algumas margens . ParallelCoordinatesConstants consts = new ParallelCoordinatesConstants (); int transp = 3; // A ordem em que os valores para a classe aparecem no arquivo . arff é { yes , no }. consts . linesColors = new Color []{ new Color (0 ,0 ,255 , transp ), new Color (255 ,0 ,0 , transp )}; consts . bottomMargin = 100; consts . leftMargin = 75; ParallelCoordinatesComponent component = new ParallelCoordinatesComponent ( instances , true , true ,1); component . setConsts ( consts ); component . setPreferredSize ( new Dimension (1200 ,500)); // Montamos a UI para visualizar o componente . JFrame frame = new JFrame ( " Parallel Coordinates " ); frame . getContentPane (). add ( component ); frame . pack (); frame . setVisible ( true ); frame . setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } } A classe ExemploPC4 (listagem 20) mostra como criar um gráco de coordenadas paralelas com área plotada maior do que a tela do computador. A classe lê a relação wine.arff (com dados físico-químicos de vinhos de três origens diferentes), cria um componente para visualização com dimensões somente uma parte do gráco (usando JScrollPanes 4800 × 1200 pixels e mostra para permitir a visualização interativa de outras partes). O gráco como um todo é renderizado em uma imagem e armazenado em um arquivo. Listagem 20: A classe ExemploPC4, que implementa ainda outro exemplo de visualização com coordenadas paralelas. 1 2 3 4 5 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ Rafael Santos http://www.lac.inpe.br/∼rafael.santos 28 Mineração e Visualização de Dados usando Java Figura 8: Tela da aplicação da classe 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ExemploPC3 (listagem 19). package parcoords ; import java . awt .*; import java . awt . image . BufferedImage ; import java . io .*; import javax . imageio . ImageIO ; import javax . swing .*; import weka . core . Instances ; /* * * Exemplo simples de visualização com coordenadas paralelas . O gráfico será armazenado em um arquivo . */ public class ExemploPC4 { public static void main ( String [] args ) throws IOException { // Usaremos a base wine . FileReader reader = new FileReader (" / home / rafael / Java / APIs / Weka / data / wine . arff " ); Instances instances = new Instances ( reader ); // Criamos uma instância de ParallelCoordinatesConstants e escolhemos um tamanho // maior do que a tela do computador . ParallelCoordinatesComponent component = new ParallelCoordinatesComponent ( instances , false , true ,0); component . setPreferredSize ( new Dimension (4800 ,1200)); // Montamos a UI para visualizar o componente . JFrame frame = new JFrame ( " Parallel Coordinates " ); frame . getContentPane (). add ( new JScrollPane ( component )); frame . pack (); frame . setVisible ( true ); frame . setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); // Com o componente visível ( mesmo que parcialmente ), criamos uma imagem para // renderizar seu conteúdo . BufferedImage im = ( BufferedImage ) component . createImage ( component . getWidth () , component . getHeight ()); Graphics2D graphics = ( Graphics2D ) im . createGraphics (); component . paintComponent ( graphics ); // Armazenamos o componente renderizado como uma imagem . ImageIO . write (im ," PNG " , new File (" graficowine . png " )); } } A gura 9 mostra a tela da aplicação implementada na classe ExemploPC4 (listagem 20), que mostra somente uma pequena área da plotagem. A gura 10 mostra a imagem criada com o conteúdo do componente (reduzida http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 29 para caber neste texto). Figura 9: Tela da aplicação da classe ExemploPC4 (listagem 20). Figura 10: Imagem correspondente ao componente criado pela classe 7.3 ExemploPC4 (listagem 20). Possíveis melhorias Nesta seção vimos uma implementação de um componente para mostrar dados plotados em coordenadas paralelas. Algumas possíveis melhorias na implementação são: . O componente é estático, isto é, não permite interação com o usuário depois de construído. Algumas opções de interatividade podem permitir a descoberta de correlações ou mesmo a criação de grácos esteticamente mais agradáveis, como, por exemplo, a reorganização da ordem dos eixos ou eliminação de alguns eixos; ou modicações em rótulos, cores, etc. Outra forma de interatividade que pode ser interessante é a possibilidade de obter mais informações ao clicar em linhas do gráco. . Os eixos das coordenadas tem escala linear, pode-se facilmente implementar opções que possibilitem eixos em escalas logarítmicas (por exemplo) ou invertidas (menores valores em cima; maiores em baixo). . Melhorias nas formas de modicação dos atributos do gráco em si (fontes, cores, tamanhos, etc). . Não foi implementado tratamento de dados incompletos (ex. instâncias com atributos faltando). Rafael Santos http://www.lac.inpe.br/∼rafael.santos 30 Mineração e Visualização de Dados usando Java 8 Visualização de Grafos com JUNG 8.1 Introdução Grafos são representações abstratas de conjuntos de objetos ligados entre si. Os objetos são tradicionalmente chamados de vértices e as ligações entre eles, de arestas. Vértices e arestas podem representar praticamente qualquer tipo de informação. A análise numérica e/ou visualização de um grafo permite a identicação de padrões, outliers, associações, agrupamentos, etc. A API JUNG (Java Universal Network/Graph Framework, [12]) é um conjunto de classes que permite a modelagem, análise e visualização de dados representáveis como grafos ou redes. Neste capítulo veremos exemplos de uso da API para visualização de dados na forma de grafos. Parte deste capítulo é baseada no tutorial de JUNG [3] e em trabalhos de Alessandro Oliveira Arantes e Alexandre Donizeti Alves [1, 9]. Para usar as classes da API JUNG é necessário copiar alguns arquivos referenciar estes arquivos 1. Crie um diretório use o nome lib). .jar lib tecla para um diretório conhecido e no seu projeto (clique no projeto com o botão direito do mouse e em 2. Copie para este diretório os arquivos .jar .jar em seu projeto. Para fazer isto usando a IDE Eclipse, siga estes passos: .jar New/Folder, necessários (para simplicar pode-se copiar todos os arquivos parte da distribuição da API JUNG). Ao terminar de copiar, selecione o nome do projeto e clique a F5 para atualizar os subdiretórios relacionados ao projeto. 3. Inclua estes arquivos no projeto selecionando o projeto, clicando com o botão direito do mouse e escolhendo Build Path/Configure Build Path. Clique no botão Add JARs e selecione no diálogo o diretório lib e todos os arquivos .jar dentro dele. Feche todos os diálogos clicando em seus respectivos botões OK. Outras IDEs devem ser conguradas com outros métodos o que importa é colocar todos os arquivos .jar da distribuição do Jung em um diretório de forma que façam parte dos projetos em desenvolvimento. Consulte a documentação da sua IDE para informações especícas. 8.2 Implementação Para visualizar um grafo em JUNG é necessário primeiro criar a estrutura de grafos no código, então criar um layout para organização dos vértices e arestas e depois criar um componente para exibir o grafo com este layout. Para grafos mais complexos (ou quando quisermos modicar programaticamente a aparência dos vértices e arestas) pode ser necessário modicar atributos do componente para exibição. Como um primeiro exemplo vamos construir um grafo simples, com alguns vértices e arestas, com um layout básico de visualização e com o componente padrão. SparseMultigraph A listagem 21 mostra como criar uma instância de (que permite a representação de grafos esparsos com arestas múltipas entre os vértices), como adicionar vértices (no exemplo, instâncias de cionados com o método addVertex String) e arestas (instâncias de Float). Vértices são adi- da classe que implementa o grafo, e que recebe como parâmetro uma instância da classe denida para os vértices; e arestas são adicionadas com o método addEdge que recebe como parâmetros uma instância da classe denida para a aresta e duas instâncias da classe denida para os vértices, efetivamente criando uma ligação entre estes vértices. A listagem também mostra como criar um layout básico (CircleLayout, que posiciona os vértices com espaçamento regular em um círculo) e nalmente como visualizar este grafo. Listagem 21: A classe 1 2 3 4 5 Grafo1, que implementa um exemplo simples de visualização de grafos com a API JUNG. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 31 package jung ; import java . awt . Dimension ; import javax . swing . JFrame ; import edu . uci . ics . jung . algorithms . layout . CircleLayout ; import edu . uci . ics . jung . graph .*; import edu . uci . ics . jung . visualization . BasicVisualizationServer ; /* * * Exemplo simples de visualização de grafos com a API JUNG . */ public class Grafo1 { public static void main ( String [] args ) { // Criamos um grafo onde os vértices são inteiros e as arestas são Strings . Graph < Integer , String > grafo = new SparseMultigraph < Integer , String >(); // Vamos incluir alguns vértices no grafo . for ( int i =1; i <=5; i ++) grafo . addVertex (i ); // Mais um -- veja que vértices não podem ter identificadores repetidos . grafo . addVertex (12); grafo . addVertex (1); grafo . addVertex (4); // Vamos adicionar algumas arestas ao grafo . for ( int i =1; i <=5; i ++) for ( int j=i +1; j <=5; j ++) grafo . addEdge (" De "+i +" para " +j ,i , j ); // Arestas adicionais -- agora sim , podemos ter mais de uma aresta entre vértices // contanto que seus identificadores sejam diferentes . grafo . addEdge (" Outro de " +1+ " para " +2 ,1 ,2); // Podemos imprimir , no terminal , a lista de vértices e arestas do grafo . System . out . println ( grafo ); // Para visualizar o grafo precisaremos de um layout correspondente ao tipo do grafo . CircleLayout < Integer , String > layout = new CircleLayout < Integer , String >( grafo ); layout . setSize ( new Dimension (400 ,400)); // Precisamos também de um componente para visualização . BasicVisualizationServer < Integer , String > component = new BasicVisualizationServer < Integer , String >( layout ); component . setPreferredSize ( new Dimension (450 ,450)); // Montamos a UI para visualizar o componente . JFrame f = new JFrame (" Graph View " ); f. getContentPane (). add ( component ); f. pack (); f. setVisible ( true ); f. setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } } A listagem 22 mostra o resultado da execução da classe causa da chamada a System.out.println Grafo1, da parte Grafo1). na linha 39 da classe que é mostrada no terminal (por Esta saída (reformatada para este documento) contém todas as informações sobre vértices e arestas em forma textual. Listagem 22: Saída da classe 1 2 3 4 Grafo1 (listagem 21). Vertices :1 ,2 ,3 ,4 ,5 ,12 Edges : Outro de 1 para 2[1 ,2] De 4 para 5[4 ,5] De 1 para 3[1 ,3] De 1 para 2[1 ,2] De 1 para 5[1 ,5] De 1 para 4[1 ,4] De 2 para 4[2 ,4] De 2 para 3[2 ,3] De 3 para 4[3 ,4] De 3 para 5[3 ,5] De 2 para 5[2 ,5] A gura 11 mostra a tela da aplicação implementada na classe Grafo1 (listagem 21). Nesta gura podemos observar os vértices e arestas, com um vértice isolado e uma aresta dupla entre dois dos vértices, o que é possível graças ao uso de SparseMultigraph. Os vértices são posicionados nas bordas de um círculo, uniformemente. Para um exemplo mais complexo de grafo, com aspectos visuais diferentes para vértices e arestas dependendo de seus valores, criaremos uma classe que permite a visualização simples de um grafo de cidades e distâncias entre as mesmas. Rafael Santos Os vértices (cidades) serão do tipo String e as arestas do tipo Float. Para modicar a http://www.lac.inpe.br/∼rafael.santos 32 Mineração e Visualização de Dados usando Java Figura 11: Tela da aplicação da classe Grafo1 (listagem 21). aparência dos vértices e arestas precisaremos de transformadores instâncias de classe que implementa a inter- Transformer<K.V> que mostram como retornar uma instância da classe V em função do valor da instância K. Por exemplo, para mudar a cor do vértice usaremos uma instância de uma classe que implementa Transformer<String,Paint> que contém o método transform que mapeia instâncias de String para Paint face da classe determinando assim, na prática, como o vértice será pintado para um determinado valor dele mesmo. Transformações serão usadas para determinar o preenchimento dos vértices a partir dos valores deles, assim como o estilo de linha e cor das arestas a partir dos valores destas. Estas transformações serão implementadas em classes mostradas depois da principal. Além destas transformações denidas pelo programador, algumas transformações prontas serão usadas para mostrar os rótulos dos vértices e arestas. A classe Grafo2, na listagem 23, cria o grafo e popula o mesmo com os vértices e arestas. Listagem 23: A classe Grafo2, que implementa um exemplo customizado de visualização de grafos com a API JUNG. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import javax . swing . JFrame ; import import import import edu . uci . ics . jung . algorithms . layout . CircleLayout ; edu . uci . ics . jung . graph .*; edu . uci . ics . jung . visualization . BasicVisualizationServer ; edu . uci . ics . jung . visualization . decorators . ToStringLabeller ; /* * * Exemplo simples de visualização de grafos com a API JUNG . */ public class Grafo2 { public static void main ( String [] args ) { // Criamos um grafo onde os vértices são Strings e as arestas são Floats . Graph < String , Float > grafo = new SparseMultigraph < String , Float >(); // Vamos definir algumas cidades . String SJC = " SP / São José dos Campos "; String CP = " SP / Campinas "; String RE = " PE / Recife "; String BE = " PA / Belém " ; http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 33 String VV = " ES / Vila Velha "; // Vamos incluir alguns vértices no grafo . grafo . addVertex ( VV ); grafo . addVertex ( SJC ); grafo . addVertex ( CP ); grafo . addVertex ( RE ); grafo . addVertex ( BE ); // Agora algumas arestas entre estes vértices . grafo . addEdge (153 F , SJC , CP ); grafo . addEdge (2023 F ,BE , RE ); grafo . addEdge (1845 F ,VV , RE ); grafo . addEdge (778 F , SJC , VV ); grafo . addEdge (2604 F ,CP , RE ); grafo . addEdge (993 F ,VV , CP ); // Para visualizar o grafo precisaremos de um layout correspondente ao tipo do grafo . CircleLayout < String , Float > layout = new CircleLayout < String , Float >( grafo ); layout . setSize ( new Dimension (450 ,450)); // Precisamos também de um componente para visualização . BasicVisualizationServer < String , Float > component = new BasicVisualizationServer < String , Float >( layout ); component . setBackground ( Color . WHITE ); component . setPreferredSize ( new Dimension (600 ,500)); // Modificamos os estilos dos vértices e arestas . component . getRenderContext (). setVertexFillPaintTransformer ( new Grafo2_VertexPaint ()); component . getRenderContext (). setEdgeStrokeTransformer ( new Grafo2_EdgeStroke ()); component . getRenderContext (). setEdgeDrawPaintTransformer ( new Grafo2_EdgePaint ()); component . getRenderContext (). setVertexLabelTransformer ( new ToStringLabeller < String >()); component . getRenderContext (). setEdgeLabelTransformer ( new ToStringLabeller < Float >()); // Montamos a UI para visualizar o componente . JFrame f = new JFrame (" Graph View " ); f. getContentPane (). add ( component ); f. pack (); f. setVisible ( true ); f. setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } } Na classe Grafo2 (listagem 23) temos várias chamadas a métodos setXXXTransformer da classe RenderContext, obtida do componente para visualização. Estes métodos indicam ao renderizador do componente como alguns dos elementos do grafo serão renderizados no exemplo da classe Grafo2 temos cinco chamadas a métodos deste tipo, que indicam que classes serão usadas para determinar preenchimento dos vértices (linha 54), estilo da linha da aresta (linha 55), cor da linha da aresta (linha 56), rótulo do vértice (linha 57) e rótulo da aresta (linha 58). Alguns dos métodos setXXXTransformer . setArrowDrawPaintTransformer da classe RenderContext são descritos a seguir. mapeia instâncias da classe da aresta para Paint para determinar a Paint para determinar a Stroke para determinar o cor do desenho das setas em grafos dirigidos. . setArrowFillPaintTransformer mapeia instâncias da classe da aresta para cor ou modo de preenchimento das setas em grafos dirigidos. . setEdgeArrowStrokeTransformer mapeia instâncias da classe da aresta para estilo de linha do desenho das setas em grafos dirigidos. . setEdgeDrawPaintTransformer mapeia instâncias da classe da aresta para Paint para determinar a cor mapeia instâncias da classe da aresta para Paint para determinar a cor do desenho das arestas. . setEdgeFillPaintTransformer ou modo de preenchimento das arestas. . setEdgeStrokeTransformer mapeia instâncias da classe da aresta para Stroke para determinar o estilo de linha das arestas. . setEdgeFontTransformer mapeia instâncias da classe da aresta para Font para determinar a fonte do texto associado às arestas. Rafael Santos http://www.lac.inpe.br/∼rafael.santos 34 Mineração e Visualização de Dados usando Java . setEdgeLabelTransformer mapeia instâncias da classe da aresta para String para determinar o texto associado às arestas. . setVertexDrawPaintTransformer mapeia instâncias da classe do vértice para Paint para determinar a mapeia instâncias da classe do vértice para Paint para determinar a cor do desenho dos vértices. . setVertexFillPaintTransformer cor ou modo de preenchimento dos vértices. . setVertexIconTransformer mapeia instâncias da classe do vértice para Icon para determinar um ícone para desenho dos vértices. . setVertexShapeTransformer mapeia instâncias da classe do vértice para Shape para determinar uma forma geométrica para desenho dos vértices. . setVertexStrokeTransformer mapeia instâncias da classe do vértice para Stroke para determinar o estilo de linha dos vértices. . setVertexFontTransformer mapeia instâncias da classe do vértice para Font para determinar a fonte do texto associado aos vértices. . setVertexLabelTransformer mapeia instâncias da classe do vértice para String para determinar o texto associado aos vértices. Grafo2 (listagem 23). O primeiro (linha 54) mapeia instâncias dos vértices (String) para instâncias de Paint. Para fazer este mapeamento devemos criar uma instância de classe que implementa Transformer<String,Paint> e usá-la como parâmetro para o método setVertexFillPaintTransformer. Esta nova classe, Grafo2_VertexPaint, Para demonstrar como estes mapeamentos são feitos, vejamos os mapeamentos usados na classe é mostrada na listagem 24. Listagem 24: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Grafo2_VertexPaint, que mapeia vértices (String) para Paint. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia Strings para Paint , definindo como vértices serão preenchidos no grafo . */ public class Grafo2_VertexPaint implements Transformer < String , Paint > { // Método para mapear Strings para Paint -- usamos o nome do estado para definir uma cor . public Paint transform ( String s) { if (s. startsWith ( " SP " )) return Color . BLUE ; else if (s. startsWith ( " PE ") || s. startsWith (" PA " )) return Color . MAGENTA ; else return Color . YELLOW ; } } Para determinar que estilos de linha serão usados para desenhar as linhas que representam as arestas precisaremos de uma instância de classe que implementa método setEdgeStrokeTransformer Transformer<Float,Stroke> (linha 55 da listagem 23). e usá-la como parâmetro para o Esta nova classe, Grafo2_EdgeStroke, é mostrada na listagem 25. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java Listagem 25: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 35 Grafo2_EdgeStroke, que mapeia arestas (Float) para Stroke. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia Floats para Stroke , definindo os estilos de linhas para arestas no grafo . */ public class Grafo2_EdgeStroke implements Transformer < Float , Stroke > { // Método para mapear Floats para Stroke -- usamos limiares para determinar os estilos . public Stroke transform ( Float f) { if (f < 800) return new BasicStroke (1f , BasicStroke . CAP_BUTT , BasicStroke . JOIN_MITER , 1f , new float []{2 f } ,0 f ); else if (f > 1500) return new BasicStroke (3 f ); else return new BasicStroke (7 f ); } } Finalmente, para determinar que cores serão usadas para desenhar as linhas que representam as arestas precisaremos de uma instância de classe que implementa o método setEdgeDrawPaintTransformer Transformer<Float,Paint> e usá-la como parâmetro para (linha 56 da listagem 23). Esta nova classe, Grafo2_EdgePaint, é mostrada na listagem 26. Listagem 26: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Grafo2_EdgePaint, que mapeia arestas (Float) para Paint. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia Floats para Paint , definindo as cores das linhas para arestas no grafo . */ public class Grafo2_EdgePaint implements Transformer < Float , Paint > { // Método para mapear Floats para Paint -- usamos limiares para determinar as cores das linhas . public Paint transform ( Float f) { if (f < 800) return Color . BLACK ; else if (f > 1500) return Color . BLUE ; else return Color . RED ; } } ToStringLabeller<String> setVertexLabelTransformer na linha 57 da listagem 23. Esta chamada fará com que a representação textual (obtida através do método toString da class que implementa o vértice seja usada para criar um rótulo para o mesmo. Similarmente, uma nova instância de ToStringLabeller<Float> será passada como parâmetro para o método setEdgeLabelTransformer (linha 58 da listagem 23) para que Além das transformações descritas nestas três classes, usamos uma nova instância de como parâmetro para o método uma representação textual do valor da aresta seja usado como rótulo para a mesma. Rafael Santos http://www.lac.inpe.br/∼rafael.santos 36 Mineração e Visualização de Dados usando Java O resultado da execução da classe Grafo2 (listagem 23) é mostrado na gura 12. Os efeitos das transformações podem ser vistos nas arestas e vértices. Figura 12: Tela da aplicação da classe Grafo2 (listagem 23). Como último exemplo veremos o código necessário para criar um grafo interativo que mostra similaridades entre publicações de um instituto (os dados para este exemplo foram cedidos por Alessandro Oliveira Arantes, veja [1]). As publicações foram selecionadas e comparadas duas a duas usando vários critérios, de forma a criar um índice de similaridade numérico. Os dados usados para gerar os grafos são apresentados em um arquivo de texto, reproduzido parcialmente na listagem 27. Listagem 27: Lista (parcial) de publicações e similaridade entre elas. 1 2 3 4 5 6 7 8 CTA / IEAv - EFO /AP -004/2005 ; 19 ; CTA / IEAv - EFO /AP -010/2005 CTA / IEAv - EFO /AP -010/2005 ; 19 ; CTA / IEAv - EFO /AP -004/2005 CTA / ITA - IEA / TD -010/2003 ; 18 ; CTA / ITA - IEE /TD -001/2003 CTA / ITA - IEE / TD -001/2003 ; 18 ; CTA / ITA - IEA /TD -010/2003 CTA / ITA - IEE / AE -009/2002 ; 17 ; CTA / ITA - IEE /AE -021/2002 ... CTA / ITA - IEA / TC -001/2005 ; 12 ; CTA / ITA - IEE /AE -010/2005 CTA / ITA - IEA / TC -001/2005 ; 12 ; CTA / ITA - IEE /AE -025/2005 Para demonstrar melhor a versatilidade da API JUNG, criaremos classes especícas para representar as arestas e vértices de um grafo de similaridade entre publicações. A classe BiblioVertice, mostrada na listagem 28, representa um vértice no grafo de similaridades. Esta classe contém métodos que retornam informações sobre a publicação (departamento, ano, etc.) e métodos para comparação e identicação de vértices únicos (equals, hashCode). Listagem 28: A classe 1 2 3 4 5 6 7 8 BiblioVertice, que representa um vértice em uma coleção de documentos e referências. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; /* * http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 37 * Esta classe representa um vértice em um grafo de referências bibliográficas ( um documento ). * Cada vértice será identificado por uma String . Seria mais fácil usar uma String diretamente , * mas com uma classe podemos criar atributos e métodos adicionais interessantes . */ public class BiblioVertice implements Comparable < BiblioVertice > { // O identificador do documento . Os documentos são identificados por Strings com quatro // tokens : o primeiro é constante , o segundo é o nome do departamento , o terceiro é o // índice do documento e o quarto é o ano de publicação . private String id ; // Construtor , inicializa os atributos . public BiblioVertice ( String i) { id = i; } // Método que verifica se uma instância desta classe é igual a outra -- este método ( criado // automaticamente pelo Eclipse ) é essencial para algumas operações em grafos . public boolean equals ( Object obj ) { if ( this == obj ) return true ; if ( obj == null ) return false ; if ( getClass () != obj . getClass ()) return false ; BiblioVertice other = ( BiblioVertice ) obj ; if ( id == null ) { if ( other . id != null ) return false ; } else if (! id . equals ( other . id )) return false ; return true ; } // Método que cria o hash code de uma instância da classe -- essencial para criação dos grafos . // Este método foi criado automaticamente pelo Eclipse . public int hashCode () { final int prime = 31; int result = 1; result = prime * result + (( id == null ) ? 0 : id . hashCode ()); return result ; } // Método para comparar uma instância desta classe com outra . public int compareTo ( BiblioVertice o) { return o. id . compareTo ( id ); } // Retorna o identificador armazenado na classe . public String getId () { return id ; } // Retorna o identificador do documento ( terceiro token na identificação ). public String getDocId () { String [] tokens = id . split ( "/" ); return tokens [2]; } // Retorna o instituto ( primeira parte do segundo token na identificação ). public String getInstituto () { String [] tokens1 = id . split ("/" ); String [] tokens2 = tokens1 [1]. split (" -" ); return tokens2 [0]; } // Retorna o ano da publicação ( quarto token na identificação ). public int getAno () { String [] tokens = id . split ( "/" ); return Integer . parseInt ( tokens [3]); } } Rafael Santos http://www.lac.inpe.br/∼rafael.santos 38 Mineração e Visualização de Dados usando Java A classe BiblioAresta, mostrada na listagem 29, representa uma aresta entre dois vértices no grafo. Arestas representam similaridade entre os vértices, e terão associadas a elas um valor numérico (dado pela tabela original com os dados). Como teremos várias arestas com o mesmo valor, é necessário criar um identicador único para as arestas, o que é implementado na classe. Listagem 29: A classe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 BiblioAresta, que representa uma aresta em uma coleção de documentos e referências. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; /* * * Esta classe representa uma aresta em um grafo de similaridade entre documentos . Cada aresta terá * um identificador único e um valor numérico ( peso da similaridade ) associado à aresta . */ public class BiblioAresta { public static int contadorDeArestas ; // Campo estático para criar o identificador único . private int id ; // Identificador único . private double peso ; // Valor da similaridade . // Construtor , inicializa os campos . public BiblioAresta ( double p ) { id = contadorDeArestas ++; peso = p; } // Recupera o valor da similaridade . public double getPeso () { return peso ; } } Com classes para representar aresta e vértice, podemos escrever uma classe que representa um grafo composto destas arestas e vértices e que permite a visualização e interação com este grafo. A classe GrafoBiblio, mostrada na listagem 30, executa os seguintes passos: . Cria o grafo como um grafo direcionado (instância de DirectedSparseMultigraph) e o preenche com os dados lidos do arquivo (veja listagem 27). . Cria um layout para o grafo neste exemplo usaremos uma instância de SpringLayout2 para controlar o posicionamento dos vértices. Este layout se organiza após a construção do grafo e durante a visualização do mesmo, em outras palavras, sua aparência muda do início da visualização para se estabilizar depois de algum tempo de execução da aplicação da visualização. Outras opções de layout serão apresentadas. . Cria o componente para visualização do grafo (instância de VisualizationViewer) e registra listeners de eventos de mouse para o componente. . Modica dicas para a renderização do grafo no componente e adiciona interatividade padrão com o mouse. . Cria os transformadores para controlar a renderização dos vértices e arestas. Além da interatividade padrão com o mouse teremos a possibilidade de gravar em disco cópias da tela exibida. Para isto basta clicar com o botão direito do mouse no componente. Listagem 30: A classe 1 2 3 4 5 6 7 8 GrafoBiblio, que representa uma aresta em uma coleção de documentos e referências. /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import import import import import 39 java . awt . RenderingHints . Key ; java . awt . event .*; java . awt . image . BufferedImage ; java . io .*; java . util . HashMap ; import javax . imageio . ImageIO ; import javax . swing . JFrame ; import import import import edu . uci . ics . jung . algorithms . layout .*; edu . uci . ics . jung . graph .*; edu . uci . ics . jung . visualization . VisualizationViewer ; edu . uci . ics . jung . visualization . control .*; /* * * Esta classe , bem mais completa e complexa , monta um grafo para visualização de uma rede de * similaridades entre artigos científicos . Cada artigo é representado por uma instância de * BiblioVertice e as similaridades por instâncias de BiblioAresta . * A classe implementa algumas técnicas interessantes de visualização e interação com o usuário . */ public class GrafoBiblio implements MouseListener { // O componente usado para visualização interativa do grafo . private VisualizationViewer < BiblioVertice , BiblioAresta > component ; // Construtor da classe , cria o grafo e as estruturas de layout e visualização . public GrafoBiblio () throws IOException { // Nosso grafo será direcionado . Graph < BiblioVertice , BiblioAresta > graph = new DirectedSparseMultigraph < BiblioVertice , BiblioAresta >(); // Vamos ler todas as entradas na tabela de similaridade entre documentos de um arquivo . BufferedReader br = new BufferedReader ( new FileReader ("/ home / rafael / Java / DM / Graphs / Alessandro / tabela4 . txt " )); while ( true ) { // Linha por linha ... String line = br . readLine (); if ( line == null ) break ; // Quebra a linha em seus tokens . String [] tokens = line . split (";" ); // Cada linha contém dois vértices e uma aresta : adicionamos ao grafo . BiblioVertice bv1 = new BiblioVertice ( tokens [0]. trim ()); BiblioVertice bv2 = new BiblioVertice ( tokens [2]. trim ()); BiblioAresta ba = new BiblioAresta ( Integer . parseInt ( tokens [1]. trim ())); graph . addVertex ( bv1 ); graph . addVertex ( bv2 ); graph . addEdge (ba , bv1 , bv2 ); } br . close (); // Para visualizar o grafo precisaremos de um layout adequado -- existe amplo espaço para // experiências aqui . SpringLayout2 < BiblioVertice , BiblioAresta > layout = new SpringLayout2 < BiblioVertice , BiblioAresta >( graph ); layout . setSize ( new Dimension (700 ,700)); // Precisamos também de um componente para visualização . Usaremos um VisualizationViewer // que permite interação com o usuário . component = new VisualizationViewer < BiblioVertice , BiblioAresta >( layout ); component . setPreferredSize ( new Dimension (750 ,750)); component . setBackground ( Color . WHITE ); component . addMouseListener ( this ); // Queremos uma boa qualidade de renderização , com antialiasing e transparência . HashMap < Key , Object > renderingHints = new HashMap < Key , Object >(); renderingHints . put ( RenderingHints . KEY_ALPHA_INTERPOLATION , RenderingHints . VALUE_ALPHA_INTERPOLATION_QUALITY ); renderingHints . put ( RenderingHints . KEY_ANTIALIASING , RenderingHints . VALUE_ANTIALIAS_ON ); component . setRenderingHints ( renderingHints ); // Adicionamos interatividade básica com o mouse para este componente . DefaultModalGraphMouse < BiblioVertice , BiblioAresta > gm = new DefaultModalGraphMouse < BiblioVertice , BiblioAresta >(); gm . setMode ( ModalGraphMouse . Mode . TRANSFORMING ); component . setGraphMouse ( gm ); // Criamos os transformadores para modificar a aparência dos vértices e arestas . component . getRenderContext (). setVertexFillPaintTransformer ( new GrafoBiblio_BiblioVerticePaint ()); component . getRenderContext (). setVertexShapeTransformer ( new GrafoBiblio_BiblioVerticeShape ()); Rafael Santos http://www.lac.inpe.br/∼rafael.santos 40 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 Mineração e Visualização de Dados usando Java component . getRenderContext (). setVertexLabelTransformer ( new GrafoBiblio_BiblioVerticeLabel ()); component . getRenderContext (). setEdgeStrokeTransformer ( new GrafoBiblio_BiblioArestaStroke ()); component . getRenderContext (). setEdgeDrawPaintTransformer ( new GrafoBiblio_BiblioArestaPaint ()); // Montamos a UI para visualizar o componente . JFrame f = new JFrame (" Graph View " ); f. getContentPane (). add ( component ); f. pack (); f. setVisible ( true ); f. setDefaultCloseOperation ( JFrame . EXIT_ON_CLOSE ); } // Se o mouse for clicado ( independente da interatividade já registrada com o componente ), // verificaremos o botão clicado para gravar , em uma imagem , um snapshot do componente . public void mouseClicked ( MouseEvent e) { if (e. getButton () == MouseEvent . BUTTON3 ) { // Criamos uma imagem do tamanho do componente de visualização . BufferedImage im = ( BufferedImage ) component . createImage ( component . getWidth () , component . getHeight ()); // Pegamos seu contexto gráfico . Graphics graphics = im . createGraphics (); // Desenhamos o componente na imagem . component . paintComponents ( graphics ); component . update ( graphics ); // Gravamos a imagem . try { ImageIO . write (im ," PNG " , new File (" bibgraph . png " )); } catch ( IOException e1 ) { e1 . printStackTrace (); } } } // Métodos requeridos pela interface MouseListener . public void mouseEntered ( MouseEvent arg0 ) { } public void mouseExited ( MouseEvent arg0 ) { } public void mousePressed ( MouseEvent arg0 ) { } public void mouseReleased ( MouseEvent arg0 ) { } // Ponto de entrada da aplicação . public static void main ( String [] args ) throws IOException { new GrafoBiblio (); } } A classe GrafoBiblio (listagem 30) usa cinco transformações para modicar a aparência dos vértices e arestas, que são implementadas por classes especiais. A primeira destas classes contém um método que determina as formas geométricas que serão usadas para desenho dos vértices. Cada vértice será desenhado com uma forma determinada a partir do instituto que gerou a publicação correspondente ao vértice. O código para determinação das formas geométricas é implementado na classe Listagem 31: A classe GrafoBiblio_BiblioVerticeShape, na listagem 31. GrafoBiblio_BiblioVerticeShape, que determina que formas geométricas serão usadas para desenhar vértices. 1 2 3 4 5 6 7 8 9 10 11 12 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import import import import java . awt . Polygon ; java . awt . Shape ; java . awt . geom . Ellipse2D ; java . awt . geom . Rectangle2D ; http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 41 import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia BiblioVertice para Shape , definindo como vértices serão desenhados no grafo . */ public class GrafoBiblio_BiblioVerticeShape implements Transformer < BiblioVertice , Shape > { // Método para mapear BiblioVertice para Shape -- usamos o instituto que publicou o documento . public Shape transform ( BiblioVertice a ) { if (a. getInstituto (). equals ( " IEAv " )) // Estrela { int [] x = new int []{0 ,4 ,12 ,4 ,0 , -4 , -12 , -4}; int [] y = new int []{ -12 , -4 ,0 ,4 ,12 ,4 ,0 , -4}; return new Polygon (x ,y , x. length ); } else if (a. getInstituto (). equals ( " ITA " )) // Círculo { return new Ellipse2D . Float ( -12 , -12 ,24 ,24); } else if (a. getInstituto (). equals ( " IAE " )) // Losango { int [] x = new int []{0 ,12 ,0 , -12}; int [] y = new int []{ -12 ,0 ,12 ,0}; return new Polygon (x ,y , x. length ); } else if (a. getInstituto (). equals ( " VDR " )) // Quadrado { return new Rectangle2D . Float ( -10 , -10 ,20 ,20); } else return new Ellipse2D . Float ( -3 , -3 ,6 ,6); // Círculo pequeno } } Outra modicação no grafo deste exemplo será a escolha da cor de preenchimento do vértice. Usaremos o ano da publicação, com uma tabela de cores denida pela classe Listagem 32: A classe GrafoBiblio_BiblioVerticePaint, GrafoBiblio_BiblioVerticePaint, na listagem 32. que determina que cores serão usadas para pintar vértices. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia BiblioVertice para Paint , definindo como vértices serão preenchidos no grafo . */ public class GrafoBiblio_BiblioVerticePaint implements Transformer < BiblioVertice , Paint > { // Método para mapear BiblioVertice para Paint -- usamos o ano da publicação . public Paint transform ( BiblioVertice v ) { Color c = Color . LIGHT_GRAY ; switch (v . getAno ()) { case 2001: c = Color . RED ; break ; case 2002: c = Color . ORANGE ; break ; case 2003: c = Color . YELLOW ; break ; case 2004: c = Color . GREEN ; break ; case 2005: c = Color . CYAN ; break ; } return c; } } Rafael Santos http://www.lac.inpe.br/∼rafael.santos 42 Mineração e Visualização de Dados usando Java A terceira modicação na aparência dos vértices será a dos rótulos que serão desenhados junto dos vértices. Ao invés de usarmos toda a informação sobre documentos, usaremos somente os identicadores simples. Esta modicação é controlada pela classe Listagem 33: A classe GrafoBiblio_BiblioVerticeLabel GrafoBiblio_BiblioVerticeLabel, (listagem 33). que determina que rótulos serão desenhados junto aos vértices. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia BiblioVertice para String , definindo como vértices serão identificados * ( com rótulos ) no grafo . */ public class GrafoBiblio_BiblioVerticeLabel implements Transformer < BiblioVertice , String > { // Método para mapear BiblioVertice para String -- usamos o identificador do documento . public String transform ( BiblioVertice v) { return v. getDocId (); } } Finalmente, teremos duas modicações na aparência das arestas. A primeira será o estilo da linha da aresta entre dois vértices (duas publicações), que será determinado pelo peso ou similaridade entre estas duas publicações. O mapeamento do peso para o tipo de linha será feito pela classe GrafoBiblio_BiblioArestaStroke, na listagem 34. Listagem 34: A classe GrafoBiblio_BiblioArestaStroke, que mapeia similaridade entre vértices para estilo de linha das arestas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt . BasicStroke ; import java . awt . Stroke ; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia BiblioAresta para Stroke , definindo estilos de linha para arestas no grafo . */ public class GrafoBiblio_BiblioArestaStroke implements Transformer < BiblioAresta , Stroke > { // Método para mapear BiblioAresta para Stroke -- usamos o peso da similaridade . public Stroke transform ( BiblioAresta a ) { double espessura = Math . max (0 , a. getPeso () -10); return new BasicStroke (( float )( espessura )); } } A última transformação para modicar a aparência dos vértices e arestas determina a cor da aresta também em função da similaridade entre os vértices, e é implementada pela classe GrafoBiblio_BiblioArestaPaint, na listagem 35. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java Listagem 35: A classe 43 GrafoBiblio_BiblioArestaPaint, que mapeia similaridade entre vértices para cor de linha das arestas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* * Parte do treinamento em Mineração e Visualização de Dados usando Java * Rafael Santos ( rafael . santos@lac . inpe . br ) * http :// www . lac . inpe . br /~ rafael . santos / */ package jung ; import java . awt .*; import org . apache . commons . collections15 . Transformer ; /* * * Esta classe mapeia BiblioAresta para Paint , definindo cor de linha para arestas no grafo . */ public class GrafoBiblio_BiblioArestaPaint implements Transformer < BiblioAresta , Paint > { // Método para mapear BiblioAresta para Paint -- usamos o peso da similaridade . public Paint transform ( BiblioAresta a) { if (a. getPeso () >= 18) return new Color (0 ,0 ,210); else if (a. getPeso () >= 15) return new Color (0 ,0 ,105); else return Color . BLACK ; } } Alguns dos resultados da execução da classe GrafoBiblio (listagem 30) (com demonstração das interações com usuário) são mostrados nas guras a seguir. Figura 13: Grafo de publicações e similaridades con- Figura 14: Grafo de publicações e similaridades con- guração inicial. guração mais estável. Uma apresentação sobre os tipos de layout e suas principais características está fora do escopo deste documento, mas para comparação, seguem guras do mesmo grafo renderizados com diferentes tipos de layout implementados na API JUNG para mudar o tipo de layout basta modicar a classe usada nas linhas 61 e GrafoBiblio Rafael Santos 62 da classe (listagem 30). Para mais informações verique a documentação on-line em [12]. http://www.lac.inpe.br/∼rafael.santos 44 Mineração e Visualização de Dados usando Java Figura 15: Ampliação de um setor do grafo, mostrando Figura 16: Ampliação de outro setor do grafo. o que podem ser cliques de publicações. Figura 17: O gráco pode ser girado em torno do centro do componente, preservando as orientações dos Figura 18: O grafo pode ser inclinado, também preservando orientações dos rótulos. rótulos. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java Figura 19: Grafo renderizado com o layout implementado pela classe CircleLayout. Figura 21: Grafo renderizado com o layout implementado pela classe Rafael Santos FRLayout. 45 Figura 20: Grafo renderizado com o layout implementado pela classe ISOMLayout. Figura 22: Grafo renderizado com o layout implementado pela classe FRLayout2. http://www.lac.inpe.br/∼rafael.santos 46 Mineração e Visualização de Dados usando Java Figura 23: Grafo renderizado com o layout implementado pela classe KKLayout. Figura 24: Grafo renderizado com o layout implementado pela classe SpringLayout. É importante observar que alguns destes layouts não mostram de forma imediata os vértices e arestas organizados, demorando algum tempo para organizar os vértices e arestas até uma conguração estável, o que pode causar longos tempos de execução em aplicações que tem muitos vértices e/ou arestas. http://www.lac.inpe.br/∼rafael.santos Rafael Santos Mineração e Visualização de Dados usando Java 47 Referências [1] Alessandro cas Oliveira baseado em Arantes. avaliação Um de sistema de conteúdo. recomendação de publicações cientí- http://mtc-m18.sid.inpe.br/rep/sid.inpe.br/mtc- m18@80/2009/10.01.19.32?languagebutton=pt-BR, 2009. Visitado em Fevereiro de 2010. [2] R. Beale e T. Jackson. Neural Computing: An Introduction. MIT Press, 1990. [3] Greg Bernstein. JUNG 2.0 Tutorial. http://www.grotto-networking.com/JUNG/JUNG2-Tutorial.pdf, 2010. Visitado em Março de 2010. [4] Laurene V. Fausett. Fundamentals of Neural Networks. Prentice Hall, 1994. [5] A. Inselbert e B. Dimsdale. Parallel Coordinates for Visualizing Multi-Dimensional Geometry. Em Proc. Computer Graphics International Conference, 1987. [6] Carl G. Looney. Pattern Recognition Using Neural Networks. Oxford University Press, 1st edição, 1997. [7] J. Ross Quinlan. C4.5: Programs for Machine Learning. Morgan Kaufmann, 1993. [8] Rafael Santos. Introdução à Programação Orientada a Objetos Usando Java. Campus, 2003. [9] Rafael Santos. Material da disciplina CAP-359: Princípios e Aplicações de Mineração de Dados. http://www.lac.inpe.br/∼rafael.santos/cap359.jsp, 2010. Visitado em Fevereiro de 2010. [10] Rafael Santos. Material de Apresentações e Tutoriais sobre a Linguagem Java. http://www.lac.inpe.br/∼rafael.santos/javaapresentacoes.jsp, 2010. Visitado em Fevereiro de 2010. [11] Rafael Santos. Material de Apresentações e Tutoriais sobre Mineração de Dados. http://www.lac.inpe.br/∼rafael.santos/dmapresentacoes.jsp, 2010. Visitado em Fevereiro de 2010. [12] Jung Development Team. Jung Java Universal Network/Graph Framework. http://jung.sourceforge.net/index.html, 2010. Visitado em Fevereiro de 2010. [13] Ian H. Witten e Eibe Frank. Weka the waikato environment for knowledge analysis. http://www.cs.waikato.ac.nz/ml/weka/. Visitado em Março de 2010. [14] Ian H. Witten e Eibe Frank. Data Mining - Practical Machine Learning Tools and Techniques. Morgan Kaufmann Publishers, 2nd edição, 2005. Rafael Santos http://www.lac.inpe.br/∼rafael.santos