Calcular distribuição da coleção em Java

Nó Fonte: 1734738

Transformar uma coleção de números (ou objetos cujos campos você gostaria de inspecionar) em uma distribuição desses números é uma técnica estatística comum e é empregada em vários contextos em relatórios e aplicativos orientados a dados.

Dada uma coleção:

1, 1, 2, 1, 2, 3, 1, 4, 5, 1, 3

Você pode inspecionar sua distribuição como uma contagem (frequência de cada elemento) e armazenar os resultados em um mapa:

{
"1": 5,
"2": 2,
"3": 2,
"4": 1,
"5": 1
}

Ou você pode normalizar os valores com base no número total de valores – expressando-os assim em porcentagens:

{
"1": 0.45,
"2": 0.18,
"3": 0.18,
"4": 0.09,
"5": 0.09
}

Ou ainda expressar essas porcentagens em um 0..100 formato em vez de um 0..1 formato.

Neste guia, veremos como você pode calcular uma distribuição de uma coleção – usando tipos primitivos e objetos cujos campos você pode querer relatar em seu aplicativo.

Com a adição do suporte de programação funcional em Java – o cálculo de distribuições é mais fácil do que nunca. Estaremos trabalhando com uma coleção de números e uma coleção de Books:

public class Book {

    private String id;
    private String name;
    private String author;
    private long pageNumber;
    private long publishedYear;

   
}

Calcular a distribuição da coleção em Java

Vamos primeiro dar uma olhada em como você pode calcular uma distribuição para tipos primitivos. Trabalhar com objetos simplesmente permite que você chame métodos personalizados de suas classes de domínio para fornecer mais flexibilidade nos cálculos.

Por padrão, representaremos as porcentagens como o dobro de 0.00 para 100.00.

Tipos primitivos

Vamos criar uma lista de inteiros e imprimir sua distribuição:

List integerList = List.of(1, 1, 2, 1, 2, 3, 1, 4, 5, 1, 3);
System.out.println(calculateIntegerDistribution(integerList));

A distribuição é calculada com:

public static Map calculateIntegerDistribution(List list) {
    return list.stream()
            .collect(Collectors.groupingBy(Integer::intValue,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count * 100.00 / list.size()))))));
}

Este método aceita uma lista e a transmite. Durante a transmissão, os valores são agrupado por seu valor inteiro - e seus valores são contados utilização Collectors.counting(), antes de ser recolhido em Map onde as chaves representam os valores de entrada e os duplos representam suas porcentagens na distribuição.

Os principais métodos aqui são collect() que aceita dois colecionadores. O coletor de chaves coleta simplesmente agrupando pelos valores de chave (elementos de entrada). O cobrador de valor coleta através do collectingAndThen() método, que nos permite conte os valores e depois formatá-los em outro formato, como count * 100.00 / list.size() que nos permite expressar os elementos contados em porcentagens:

{1=45.45, 2=18.18, 3=18.18, 4=9.09, 5=9.09}

Classificar distribuição por valor ou chave

Ao criar distribuições – você normalmente desejará classificar os valores. Na maioria das vezes, isso será por chave. Java HashMaps não garanta preservar a ordem de inserção, então teremos que usar um LinkedHashMap que faz. Além disso, é mais fácil retransmitir o mapa e recolhê-lo agora que é um tamanho muito menor e muito mais gerenciável.

A operação anterior pode recolher rapidamente vários milhares de registros em pequenos mapas, dependendo do número de chaves com as quais você está lidando, portanto, o retransmissão não é caro:

public static Map calculateIntegerDistribution(List list) {
    return list.stream()
            .collect(Collectors.groupingBy(Integer::intValue,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count.doubleValue() / list.size()))))))
            
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

objetos

Como isso pode ser feito para objetos? A mesma lógica se aplica! Em vez de uma função de identificação (Integer::intValue), usaremos o campo desejado, como o ano de publicação de nossos livros. Vamos criar alguns livros, armazená-los em uma lista e depois calcular as distribuições dos anos de publicação:

Confira nosso guia prático e prático para aprender Git, com práticas recomendadas, padrões aceitos pelo setor e folha de dicas incluída. Pare de pesquisar comandos Git no Google e realmente aprender -lo!

Book book1 = new Book("001", "Our Mathematical Universe", "Max Tegmark", 432, 2014);
Book book2 = new Book("002", "Life 3.0", "Max Tegmark", 280, 2017);
Book book3 = new Book("003", "Sapiens", "Yuval Noah Harari", 443, 2011);
Book book4 = new Book("004", "Steve Jobs", "Water Isaacson", 656, 2011);

List books = Arrays.asList(book1, book2, book3, book4);

Vamos calcular a distribuição do publishedYear campo:

public static Map calculateDistribution(List books) {
    return books.stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count * 100.00 / books.size()))))))
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Ajusta a "%.2f" para definir a precisão do ponto flutuante. Isto resulta em:

{2011=50.0, 2014=25.0, 2017=25.0}

50% dos livros fornecidos (2/4) foram publicados em 2011, 25% (1/4) foram publicados em 2014 e 25% (1/4) em 2017. E se você quiser formatar esse resultado de forma diferente e normalizar o intervalo em 0..1?

Calcular a distribuição normalizada (porcentagem) da coleção em Java

Para normalizar as porcentagens de um 0.0...100.0 alcance para um 0..1 intervalo - vamos simplesmente adaptar o collectingAndThen() ligar para não multiplique a contagem por 100.0 antes de dividir pelo tamanho da coleção.

Anteriormente, o Long contagem retornada por Collectors.counting() foi convertido implicitamente em um double (multiplicação com um valor double) – então, desta vez, queremos obter explicitamente o doubleValue() da count:

    public static Map calculateDistributionNormalized(List books) {
        return books.stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.4f", count.doubleValue() / books.size()))))))
            
            .entrySet()
            .stream()
            .sorted(comparing(e -> e.getKey()))
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Ajusta a "%.4f" para definir a precisão do ponto flutuante. Isto resulta em:

{2011=0.5, 2014=0.25, 2017=0.25}

Calcular Contagem de Elementos (Frequência) de Coleta

Finalmente – podemos obter a contagem de elementos (frequência de todos os elementos) na coleção simplesmente não dividindo a contagem pelo tamanho da coleção! Esta é uma contagem totalmente não normalizada:

   public static Map calculateDistributionCount(List books) {
        return books
            .stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Integer.parseInt(String.format("%s", count.intValue()))))))
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Isto resulta em:

{2011=2, 2014=1, 2017=1}

De fato, há dois livros de 2011 e um de 2014 e um de 2017 cada.

Conclusão

Calcular distribuições de dados é uma tarefa comum em aplicativos ricos em dados e não requer o uso de bibliotecas externas ou código complexo. Com suporte de programação funcional, o Java facilitou muito o trabalho com coleções!

Neste pequeno rascunho, analisamos como você pode calcular as contagens de frequência de todos os elementos em uma coleção, bem como calcular mapas de distribuição normalizados para porcentagens entre 0 e 1 assim como 0 e 100 em Java.

Carimbo de hora:

Mais de Abuso de pilha