从 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.00100.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 使处理集合变得轻而易举!

在这个简短的草稿中,我们了解了如何计算集合中所有元素的频率计数,以及如何计算归一化为之间百分比的分布图 01 以及 0100 在Java中。

时间戳记:

更多来自 堆栈滥用