1. 概述

在Java中分析数据时,计算百分位数是理解数值数据集统计分布和特性的基础任务。

在这个教程中,我们将逐步讲解如何在Java中计算百分位数,提供代码示例和解释。

2. 百分位数的理解

在深入讨论实现细节之前,先来了解一下什么是百分位数以及它们在数据分析中的常见应用。

百分位数是统计学中的一种度量,表示给定百分比的数据观察值位于或低于该值。 例如,50th百分位数(也称为中位数)代表50%的数据点落在该值之下。

值得注意的是,百分位数的表达单位与输入数据集相同,而不是以百分比形式。例如,如果数据集指的是月工资,对应的百分位数将以美元、欧元或其他货币单位表示。

接下来,我们看几个具体的例子:

Input: A dataset with numbers 1-100 unsorted
-> sorted dataset: [1, 2, ... 49, (50), 51, 52, ..100] 
-> The 50th percentile: 50

Input: [-1, 200, 30, 42, -5, 7, 8, 92]
-> sorted dataset: [-2, -1, 7, (8), 30, 42, 92, 200]
-> The 50th percentile: 8

百分位数常用于理解数据分布、识别异常值以及比较不同数据集。在处理大型数据集或简洁概括数据集特性时尤其有用。

现在,让我们看看如何在Java中计算百分位数。

3. 从Collection中计算百分位数

了解了百分位数后,我们来总结一个步骤来实现百分位数计算:

  1. 对给定的数据集按升序排序
  2. 计算所需百分位数的排名,即 percentile / 100 * dataset.size
  3. 取排名的上界值,因为排名可能是一个小数
  4. 最终结果是在排序后的数据集中索引为 ceiling(rank) - 1 的元素

接下来,我们创建一个泛型方法来实现上述逻辑:

static <T extends Comparable<T>> T getPercentile(Collection<T> input, double percentile) {
    if (input == null || input.isEmpty()) {
        throw new IllegalArgumentException("The input dataset cannot be null or empty.");
    }
    if (percentile < 0 || percentile > 100) {
        throw new IllegalArgumentException("Percentile must be between 0 and 100 inclusive.");
    }
    List<T> sortedList = input.stream()
      .sorted()
      .collect(Collectors.toList());

    int rank = percentile == 0 ? 1 : (int) Math.ceil(percentile / 100.0 * input.size());
    return sortedList.get(rank - 1);
}

如您所见,上述实现相当直接。但值得一提的是:

  • 需要验证percentile参数(0 <= percentile <= 100
  • 我们使用Stream API对输入数据集进行排序,并将排序结果收集到新的列表中,以避免修改原始数据集

现在,让我们测试getPercentile()方法。

4. 测试getPercentile()方法

首先,方法应抛出IllegalArgumentException,如果百分位数超出有效范围:

assertThrows(IllegalArgumentException.class, () -> getPercentile(List.of(1, 2, 3), -1));
assertThrows(IllegalArgumentException.class, () -> getPercentile(List.of(1, 2, 3), 101));

我们使用了**assertThrows()方法来验证是否抛出了预期的异常**。

接下来,我们用一个1到100的列表作为输入,验证方法能否产生预期结果:

List<Integer> list100 = IntStream.rangeClosed(1, 100)
  .boxed()
  .collect(Collectors.toList());
Collections.shuffle(list100);
 
assertEquals(1, getPercentile(list100, 0));
assertEquals(10, getPercentile(list100, 10));
assertEquals(25, getPercentile(list100, 25));
assertEquals(50, getPercentile(list100, 50));
assertEquals(76, getPercentile(list100, 75.3));
assertEquals(100, getPercentile(list100, 100));

在这段代码中,我们通过IntStream准备了输入列表,并使用shuffle()方法随机排序这100个数字

此外,我们也测试了另一种数据集输入:

List<Integer> list8 = IntStream.of(-1, 200, 30, 42, -5, 7, 8, 92)
  .boxed()
  .collect(Collectors.toList());

assertEquals(-5, getPercentile(list8, 0));
assertEquals(-5, getPercentile(list8, 10));
assertEquals(-1, getPercentile(list8, 25));
assertEquals(8, getPercentile(list8, 50));
assertEquals(92, getPercentile(list8, 75.3));
assertEquals(200, getPercentile(list8, 100));

5. 从数组中计算百分位数

有时,给定的数据集输入是一个数组,而非Collection。在这种情况下,我们可以**首先将输入数组转换为List**,然后使用getPercentile()方法计算所需的百分位数。

接下来,我们演示如何通过一个long数组作为输入实现这一点:

long[] theArray = new long[] { -1, 200, 30, 42, -5, 7, 8, 92 };
 
//convert the long[] array to a List<Long>
List<Long> list8 = Arrays.stream(theArray)
  .boxed()
  .toList();
 
assertEquals(-5, getPercentile(list8, 0));
assertEquals(-5, getPercentile(list8, 10));
assertEquals(-1, getPercentile(list8, 25));
assertEquals(8, getPercentile(list8, 50));
assertEquals(92, getPercentile(list8, 75.3));
assertEquals(200, getPercentile(list8, 100));

代码显示,**由于我们的输入是基本类型的数组(long[]),我们使用Arrays.stream()将其转换为List<Long>**。然后,我们可以将转换后的List传递给getPercentile()获取预期结果。

6. 总结

在这个文章中,我们首先讨论了百分位数的基本原理,然后探讨了如何在Java中为数据集计算百分位数。

如往常一样,所有示例的完整源代码可在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-math-4