Вычислить распределение из коллекции в Java

Преобразование набора чисел (или объектов, поля которых вы хотите проверить) в распределение этих чисел — распространенный статистический метод, который используется в различных контекстах в отчетах и ​​приложениях, управляемых данными.

Учитывая коллекцию:

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

Вы можете проверить их распределение как количество (частоту каждого элемента) и сохранить результаты на карте:

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

Или вы можете нормализовать значения, основанные на общем количестве значений, таким образом, выражая их в процентах:

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

Или даже выразить эти проценты в 0..100 формат вместо 0..1 формат.

В этом руководстве мы рассмотрим, как можно рассчитать распределение по коллекции — как с использованием примитивных типов, так и с объектами, поля которых вы, возможно, захотите указать в своем приложении.

С добавлением поддержки функционального программирования на Java вычисление распределений стало проще, чем когда-либо. Мы будем работать с набором чисел и набором Books:

public class Book {

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

   
}

Рассчитать распределение коллекции в Java

Давайте сначала посмотрим, как можно рассчитать распределение для примитивных типов. Работа с объектами просто позволяет вам вызывать пользовательские методы из ваших доменных классов, чтобы обеспечить большую гибкость вычислений.

По умолчанию мы будем представлять проценты в виде двойного числа от 0.00 в 100.00.

Примитивные типы

Создадим список целых чисел и выведем их распределение:

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

Распределение рассчитывается с помощью:

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

Этот метод принимает список и передает его. Во время потоковой передачи значения сгруппированы по их целочисленное значение – и их значения равны учитываются через Collectors.counting(), прежде чем собрать в Map где ключи представляют входные значения, а двойники представляют их проценты в распределении.

Ключевыми методами здесь являются collect() который принимает два коллекционера. Сборщик ключей собирает, просто группируя по значениям ключей (входным элементам). Сборщик значений собирает через collectingAndThen() метод, который позволяет нам подсчитать значения а затем отформатировать их в другом формате, например count * 100.00 / list.size() что позволяет выразить подсчитанные элементы в процентах:

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

Сортировка распределения по значению или ключу

При создании дистрибутивов обычно требуется сортировать значения. Чаще всего это будет ключ. Ява HashMaps не гарантирует сохранения порядка вставки, поэтому нам придется использовать LinkedHashMap что делает. Кроме того, теперь проще всего выполнить повторную трансляцию карты и собрать ее заново, поскольку она намного меньше по размеру и гораздо более управляема.

Предыдущая операция может быстро свернуть несколько тысяч записей в небольшие карты, в зависимости от количества ключей, с которыми вы имеете дело, поэтому повторная потоковая передача не требует больших затрат:

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

Объекты

Как это можно сделать для объектов? Применяется та же логика! Вместо функции идентификации (Integer::intValue), вместо этого мы будем использовать нужное поле, например, год публикации наших книг. Давайте создадим несколько книг, сохраним их в списке, а затем рассчитаем распределение по годам издания:

Ознакомьтесь с нашим практическим руководством по изучению Git с рекомендациями, принятыми в отрасли стандартами и прилагаемой памяткой. Перестаньте гуглить команды Git и на самом деле изучить это!

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

Давайте посчитаем распределение publishedYear поле:

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

Настроить "%.2f" чтобы установить точность с плавающей запятой. Это приводит к:

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

50% указанных книг (2/4) были опубликованы в 2011 г., 25% (1/4) были опубликованы в 2014 г. и 25% (1/4) в 2017 г. Что, если вы хотите отформатировать этот результат по-другому и нормализовать диапазон в 0..1?

Вычислить нормализованное (процентное) распределение коллекции в Java

Чтобы нормализовать проценты от 0.0...100.0 диапазон до 0..1 диапазон – мы просто адаптируем collectingAndThen() позвонить не умножьте количество на 100.0 перед делением на размер коллекции.

Ранее Long количество возвращено Collectors.counting() было неявно преобразовано в двойное (умножение на двойное значение) — так что на этот раз мы хотим явно получить doubleValue() 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));
}

Настроить "%.4f" чтобы установить точность с плавающей запятой. Это приводит к:

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

Вычислить количество элементов (частоту) коллекции

Наконец, мы можем получить количество элементов (частоту всех элементов) в коллекции, просто не деля количество на размер коллекции! Это полностью ненормализованный счет:

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

Это приводит к:

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

Действительно, есть две книги 2011 года и по одной 2014 и 2017 годов каждая.

Заключение

Вычисление распределений данных — обычная задача в приложениях с большими объемами данных, не требующая использования внешних библиотек или сложного кода. Благодаря поддержке функционального программирования в Java работа с коллекциями стала проще простого!

В этом кратком черновике мы рассмотрели, как можно рассчитать частотность всех элементов в коллекции, а также как рассчитать карты распределения, нормализованные в процентах между 0 и 1 так же как и сигнал 0 и 100 на Яве.

Отметка времени:

Больше от Стекабьюс