domingo, 24 de novembro de 2019

Java Crossfit - Code Duel - Histograma em Java


#oBomProgramador #codeDuel #DevXFit #Java
Uma implementação de histograma com Stream API! Vou mostrar com usar a Stream API e AWT/Swig para criar histogramas como este.





Como criar histogramas


Um histograma é baseado em uma tabela de frequências, ou seja, a quantidade de ocorrências por faixas ou classes de valores.

Para começar, precisamos avaliar nossa amostra e determinar em quantas classes desejamos dividir os dados. Muitas classes podem tornar o histograma confuso, poucas classes podem ocultar informações importantes. Existem várias heurísticas para sugerir a quantidade de classes, como esta:

  • abaixo de 50 dados de amostra, de 5 a 7 classes;
  • entre 50 e 100, de 6 a 10 classes;
  • entre 100 e 250, de 7 a 12 classes;
  • acima de 250 dados de amostra, de 10 a 20 classes.

Porém, esta heurística pode gerar alguns problemas, dependendo da amplitude e do tipo de valores. Valores discretos, valores contínuos, variações pequenas etc.

Pode fazer mais sentido tentar estabelecer as faixas de valores de acordo com a natureza dos dados. Vamos supor o exemplo em questão: Médias dos alunos. Faz sentido termos 10 classes de média? Certamente não. Teríamos uma granularidade muito baixa e valores dispersos. Faz sentido dividirmos em 2 classes? Talvez se o objetivo for mostrar apenas os aprovados e reprovados, sem analisar mais nada.

Podemos pensar em um belo número de classes para este problema: 5, agrupando as notas em:

  • Péssimo: [0, 2];
  • Ruim: (2, 4];
  • Regular: (4, 6];
  • Bom: (6, 8];
  • Ótimo: (8, 10];
O código-fonte e os arquivos estão no repositório: 


Criar um histograma em planilha é muito simples. Você pode usar a função “frequency” para isso, conforme demonstra este post: https://wikieducator.org/OpenOffice/Calc_3/Histogram

Mas eu quis criar uma demonstração realmente interessante, e a planilha anexa faz isto.



Agora, como fazer isso em Java? E, ainda por cima, usando a Stream API?


Implementação


A classe Histogram.java faz exatamente isso: Gera um conjunto aleatório de 100 notas, arredonda para 2 casas decimais, e calcula a frequência nas faixas determinadas. Eu usei a raiz 42 na classe Random() para fixar a geração de números. Eis a frequência das classes de médias:

{0.0=16, 2.0=21, 4.0=24, 6.0=20, 8.0=19}

Para começar, eu criei um mapa (na verdade um TreeMap, que é ordenado) com a nota inferior da faixa e a quantidade de alunos. Isto foi feito com este único comando:

TreeMap<Double, Integer> map = new TreeMap<Double, Integer>(Stream.of(new Object[][] {
{0.0, 0},{2.0, 0},{4.0, 0},{6.0, 0},{8.0, 0}
}).collect(Collectors.toMap(data -> (Double) data[0], data -> (Integer) data[1])));

Não se impressione muito, pois é apenas uma maneira “fashion” de inicializar um mapa. Cada entrada tem a média inferior da classe e a quantidade de alunos, que começa em zero.

Depois, eu criei a lista de notas, arredondando em 2 decimais:

List<Double> averages = (new Random(42)).doubles(100, 0, 1)
.map(n -> BigDecimal.valueOf(n*10).setScale(2, RoundingMode.HALF_UP).doubleValue())
.boxed().collect(Collectors.toList());

Parece muita coisa, mas não é. O método “doubles()”, da classe Random gera um vetor com “n” elementos entre o inicial e o final. Eu escolhi entre zero e um, pois vou multiplicar por 10 e arredondar em 2 decimais. O resultado é algo assim:

7.28, 6.83, 3.09, 2.77, 6.66, 9.03, 3.69, 2.76, 4.64,…

O interessante aqui é o uso da classe BigDecimal para arredondar, além do “boxed()”, já que o array gerado precisa ser empacotado em Double []. Note que usei um “collector” para gerar uma lista.

Finalmente, a parte onde a “mágica” acontece. Eu distribuo a frequência das médias de acordo com o limite inferior com este código:

averages.forEach(average -> {
Double key = map.floorKey(average);
map.put(key, map.get(key) + 1);
});

É exatamente este código que impede o programa de ficar totalmente funcional, já que ele usa um método “side effect” que altera o estado do programa (a variável “map”). Tive que usar isso devido ao método “floorKey()”. Este método maravilhoso encontra a primeira chave no mapa que seja menor ou igual a um valor dado. Então, eu agrupo as notas das faixas desta maneira.

Plotando o histograma

Quem programa em Java sabe que lidar com UI é uma tarefa problemática. É claro que existem componentes e bibliotecas para lidar com gráficos, e existe o Java FX. Eu queria fazer algo bem simples e não perder tempo com a renderização, então, procurei um código pronto em AWT/Swing que gera um gráfico de barras. Eu o encontrei neste artigo:


E gera um gráfico bem interessante:


Legal, não? Agora, tente melhorar este código! Você fez diferente? Tente gerar 100 notas com raiz 42 e veja se o gráfico ficou igual!

Cleuton Sampaio, M.Sc.


Nenhum comentário:

Postar um comentário