Logotipo de Zephyrnet

Calcular la distribución de la colección en Java

Fecha:

Convertir una colección de números (u objetos cuyos campos le gustaría inspeccionar) en una distribución de esos números es una técnica estadística común y se emplea en varios contextos en informes y aplicaciones basadas en datos.

Dada una colección:

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

Puede inspeccionar su distribución como un conteo (frecuencia de cada elemento) y almacenar los resultados en un mapa:

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

O tu puedes normalizar los valores con base en el número total de valores, expresándolos así en porcentajes:

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

O incluso expresar estos porcentajes en un 0..100 formato en lugar de un 0..1 formato.

En esta guía, veremos cómo puede calcular una distribución a partir de una colección, tanto utilizando tipos primitivos como objetos cuyos campos podría querer informar en su aplicación.

Con la adición de soporte de programación funcional en Java, calcular distribuciones es más fácil que nunca. Estaremos trabajando con una colección de números y una colección de Books:

public class Book {

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

   
}

Calcular la distribución de la colección en Java

Primero echemos un vistazo a cómo puede calcular una distribución para tipos primitivos. Trabajar con objetos simplemente le permite llamar a métodos personalizados desde sus clases de dominio para brindar más flexibilidad en los cálculos.

Por defecto, representaremos los porcentajes como un doble de 0.00 a 100.00.

Tipos primitivos

Vamos a crear una lista de enteros e imprimir su distribución:

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

La distribución se calcula con:

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 acepta una lista y la transmite. Mientras se transmite, los valores son agrupados por su valor entero – y sus valores son contados usando Collectors.counting(), antes de ser recogido en un Map donde las claves representan los valores de entrada y los dobles representan sus porcentajes en la distribución.

Los métodos clave aquí son collect() que acepta dos coleccionistas. El recopilador de claves recopila simplemente agrupando por los valores clave (elementos de entrada). El coleccionista de valor recoge a través de la collectingAndThen() método que nos permite contar los valores y luego darles formato en otro formato, como count * 100.00 / list.size() lo que nos permite expresar los elementos contados en porcentajes:

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

Ordenar distribución por valor o clave

Al crear distribuciones, normalmente querrá ordenar los valores. La mayoría de las veces, esto será por clave. Java HashMaps no garantiza conservar el orden de inserción, entonces tendremos que usar un LinkedHashMap que hace Además, es más fácil volver a transmitir el mapa y recopilarlo ahora que tiene un tamaño mucho más pequeño y mucho más manejable.

La operación anterior puede colapsar rápidamente varios miles de registros en pequeños mapas, dependiendo de la cantidad de claves con las que esté tratando, por lo que la retransmisión no es costosa:

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

¿Cómo se puede hacer esto para los objetos? ¡Se aplica la misma lógica! En lugar de una función de identificación (Integer::intValue), usaremos el campo deseado en su lugar, como el año de publicación de nuestros libros. Creemos algunos libros, almacenémoslos en una lista y luego calculemos las distribuciones de los años de publicación:

Consulte nuestra guía práctica y práctica para aprender Git, con las mejores prácticas, los estándares aceptados por la industria y la hoja de trucos incluida. Deja de buscar en Google los comandos de Git y, de hecho, aprenden ella!

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);

Calculemos la distribución de la 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));
}

Ajustar el "%.2f" para establecer la precisión de punto flotante. Esto resulta en:

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

El 50 % de los libros proporcionados (2/4) se publicaron en 2011, el 25 % (1/4) se publicaron en 2014 y el 25 % (1/4) en 2017. ¿Qué sucede si desea formatear este resultado de manera diferente y normalizar el rango en 0..1?

Calcule la distribución normalizada (porcentaje) de la colección en Java

Para normalizar los porcentajes de un 0.0...100.0 rango a un 0..1 rango - simplemente adaptaremos el collectingAndThen() llamar a no multiplicar la cuenta por 100.0 antes de dividir por el tamaño de la colección.

Anteriormente, el Long recuento devuelto por Collectors.counting() se convirtió implícitamente en un doble (multiplicación con un valor doble), por lo que esta vez, querremos obtener explícitamente el doubleValue() de las 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));
}

Ajustar el "%.4f" para establecer la precisión de punto flotante. Esto resulta en:

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

Calcular el recuento de elementos (frecuencia) de la colección

Finalmente, ¡podemos obtener el conteo de elementos (frecuencia de todos los elementos) en la colección simplemente sin dividir el conteo por el tamaño de la colección! Este es un conteo completamente no normalizado:

   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));
}

Esto resulta en:

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

De hecho, hay dos libros de 2011 y uno de 2014 y 2017 cada uno.

Conclusión

El cálculo de distribuciones de datos es una tarea común en aplicaciones ricas en datos y no requiere el uso de bibliotecas externas o código complejo. ¡Con soporte de programación funcional, Java hizo que trabajar con colecciones fuera muy fácil!

En este breve borrador, hemos analizado cómo puede calcular los recuentos de frecuencia de todos los elementos de una colección, así como también cómo calcular los mapas de distribución normalizados a porcentajes entre 0 y 1 al igual que 0 y 100 en Java

punto_img

Información más reciente

punto_img