Преобразование набора чисел (или объектов, поля которых вы хотите проверить) в распределение этих чисел — распространенный статистический метод, который используется в различных контекстах в отчетах и приложениях, управляемых данными.
Учитывая коллекцию:
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 вычисление распределений стало проще, чем когда-либо. Мы будем работать с набором чисел и набором Book
s:
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}
Сортировка распределения по значению или ключу
При создании дистрибутивов обычно требуется сортировать значения. Чаще всего это будет ключ. Ява HashMap
s не гарантирует сохранения порядка вставки, поэтому нам придется использовать 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
на Яве.