快速算法计算百分位数以去除异常值

20
我有一个程序需要重复计算数据集的近似百分位数(顺序统计量),以便在进一步处理之前去除异常值。目前的做法是通过对值数组进行排序并选择适当的元素来实现;虽然这是可行的,但它占用了程序中相当小的一部分,但在性能分析中是明显的短板。
更多信息:
- 数据集包含大约高达100000个浮点数,并且假定为“合理”分布 - 不太可能存在重复项或密度在特定值附近的巨大峰值;如果由于某种奇怪的原因分布很奇怪,则不精确的近似值也可以接受,因为数据可能已经混乱,进一步处理也是可疑的。但是,数据不一定是均匀或正态分布的;它只是非常不可能退化。 - 近似解决方案是可以的,但我需要了解近似如何引入误差,以确保它有效。 - 由于旨在去除异常值,因此始终在同一数据上计算两个百分位数:例如95%和5%。 - 应用程序使用C#编写,其中部分重要工作使用C ++完成;伪代码或现有库都可以。 - 只要合理,完全不同的去除异常值的方法也可以接受。
更新:看起来我正在寻找一种近似的选择算法

虽然这一切都在循环中完成,但是每次数据都有些微的不同,因此像这个问题中所做的那样重用数据结构并不容易。

实现的解决方案

使用Gronim建议的维基百科选择算法,将运行时间缩短了约20倍。

由于我找不到C#的实现,所以这就是我想出的东西。即使对于小输入,它也比Array.Sort更快;而对于1000个元素,它快了25倍。

public static double QuickSelect(double[] list, int k) {
    return QuickSelect(list, k, 0, list.Length);
}
public static double QuickSelect(double[] list, int k, int startI, int endI) {
    while (true) {
        // Assume startI <= k < endI
        int pivotI = (startI + endI) / 2; //arbitrary, but good if sorted
        int splitI = partition(list, startI, endI, pivotI);
        if (k < splitI)
            endI = splitI;
        else if (k > splitI)
            startI = splitI + 1;
        else //if (k == splitI)
            return list[k];
    }
    //when this returns, all elements of list[i] <= list[k] iif i <= k
}
static int partition(double[] list, int startI, int endI, int pivotI) {
    double pivotValue = list[pivotI];
    list[pivotI] = list[startI];
    list[startI] = pivotValue;

    int storeI = startI + 1;//no need to store @ pivot item, it's good already.
    //Invariant: startI < storeI <= endI
    while (storeI < endI && list[storeI] <= pivotValue) ++storeI; //fast if sorted
    //now storeI == endI || list[storeI] > pivotValue
    //so elem @storeI is either irrelevant or too large.
    for (int i = storeI + 1; i < endI; ++i)
        if (list[i] <= pivotValue) {
            list.swap_elems(i, storeI);
            ++storeI;
        }
    int newPivotI = storeI - 1;
    list[startI] = list[newPivotI];
    list[newPivotI] = pivotValue;
    //now [startI, newPivotI] are <= to pivotValue && list[newPivotI] == pivotValue.
    return newPivotI;
}
static void swap_elems(this double[] list, int i, int j) {
    double tmp = list[i];
    list[i] = list[j];
    list[j] = tmp;
}

Performance Graph

感谢Gronim指引我正确的方向!

我知道这是旧的,但我已经实现了它,但如何执行以获取第5和第95百分位数? - tone
如果你想要第5个百分位数,使用QuickSelect(values, (int)(values.Length*0.05+0.5))。如果你想要第95个百分位数,使用QuickSelect(values, (int)(values.Length*0.95+0.5)) - 注意,你必须将0.05和0.95的小数索引四舍五入为整数索引,除非你的列表长度是20的倍数。如果你的列表很短,你可能考虑插值而不是只选择一个索引,但对于大多数用途来说,我认为这并不重要 - 如果你不关心选择5%,那么你可能不在乎它实际上是4.8%或其他什么 - 总的来说,没有确切的第5个百分位数。 - Eamon Nerbonne
10个回答

9

没错,这就是我在寻找的——一个选择算法! - Eamon Nerbonne

6

根据其创建者所说,SoftHeap可以用于:

计算精确或近似的中位数和百分位数最优化。它也适用于近似排序...


@Eamon,SoftHeap及其应用背后的整个理念真的很酷。 - Eugen Constantin Dinca
@EugenConstantinDinca:感谢您的好主意!这个想法是否已经有实际的实现,或者论文/维基百科是唯一的来源? - Legend
@Legend 我在各种语言中找到了几个实现(从C++到Haskell),但是我没有使用过,所以不知道它们有多有用/可用。 - Eugen Constantin Dinca

5

过去我通常通过计算标准差来识别异常值。任何与平均值相比距离超过2倍(或3倍)标准差的数都被认为是异常值。2倍 ≈ 约95%。

由于在计算平均值时就可以很容易地计算标准差,因此速度非常快。

你也可以仅使用数据子集来计算这些数字。


2
数据不服从正态分布。 - Eamon Nerbonne

4
你可以仅使用数据集的一部分(例如前几千个点)来估计百分位数。
如果您能假定数据点是独立的,Glivenko-Cantelli定理可以确保这将是一个相当不错的估计。

不幸的是,数据点不是独立的,它们是按外部标准排序的 - 但我可以随机迭代。我不明白连接定理如何实际让我估计百分位数 - 你可以举个例子吗?例如正态分布? - Eamon Nerbonne
@Eamon:链接的定理简单地说明了,经验分布函数(在基于数据计算百分位数时隐含使用)是真实分布的良好估计。实际上,您不必使用它 =) - Jens
啊,好的,我明白你的意思了 :-) - Eamon Nerbonne

3
将数据的最小值和最大值之间的区间分成(比如说)1000个箱子,并计算一个直方图。然后建立部分总和,看它们何时首次超过5000或95000。

不错啊...快速排序,然后截取前后的5000。如果不了解分布情况,不知道还有什么更好的方法。 - John
桶排序更加合适。 - Brian
1
这听起来非常实用,虽然并不总是有效。一些极端的异常值可能会真正扭曲您的箱... - Eamon Nerbonne

1

我可以想到几种基本方法。首先是计算范围(通过查找最高值和最低值),将每个元素投影到百分位数((x-min)/范围),并丢弃任何评估为低于0.05或高于0.95的元素。

第二种方法是计算平均值和标准差。从平均值开始,两倍标准差的跨度(向两个方向)将包含95%的正态分布样本空间,这意味着你的异常值将在<2.5和>97.5百分位数中。计算一系列的平均值是线性的,标准偏差也是如此(每个元素与平均值的差的平方和的平方根)。然后,从平均值中减去2标准差,加上2标准差,你就得到了异常值的限制。

这两种方法都可以在大约线性时间内计算完成;第一个需要两次遍历,第二个需要三次遍历(一旦获得了限制,你仍然必须丢弃异常值)。由于这是基于列表的操作,我认为你不会找到具有对数或常量复杂度的任何东西;任何进一步的性能提升都需要优化迭代和计算,或通过对子样本(例如每三个元素)执行计算来引入误差。


第一个建议并不是排除最外层的5个百分点,而是基于最极端的异常值进行某些操作,这种方法非常不稳定。第二个建议假设数据服从正态分布,但实际上并不是这样。 - Eamon Nerbonne

1
一个解决你问题的好方法似乎是RANSAC。给定一个模型和一些嘈杂的数据,该算法可以有效地恢复模型的参数。
你需要选择一个能够映射你的数据的简单模型。任何平滑的东西都应该没问题。比如说几个高斯混合物。RANSAC将设置您模型的参数并同时估计一组内点。然后丢弃任何不适合模型的内容。

我有一组数字 - 不是什么复杂的模型 - RANSAC看起来会很慢且容易出错,对于这样一个简单的情况,存在更好的解决方案。 - Eamon Nerbonne

1

即使数据不服从正态分布,您仍可以过滤掉2或3个标准差;至少,这将以一种一致的方式完成,这应该很重要。

随着您删除异常值,标准偏差将发生变化,您可以在循环中执行此操作,直到标准偏差的变化最小为止。是否要这样做取决于您为什么要以这种方式操纵数据。有些统计学家强烈反对去除异常值。但是有些人会删除异常值以证明数据相当接近正态分布。


如果数据主要位于极端位置 - 也就是与正常相反的位置 - 那么这种方法可能会删除大量数据。我真的不想删除超过数据的一小部分,最好只删除那些异常值。我抑制异常值是因为它们会分散注意力 - 它们只是从可视化中裁剪出来,而不是从实际数据中删除。 - Eamon Nerbonne
根据定义,您的数据只有一小部分可能处于极端情况。根据切比雪夫不等式,只有分布的1/9可以超过3个标准差;只有1/16可以超过4个标准差。而这些限制仅在您的分布是两个尖峰的退化情况下才会达到。因此,在O(N)中计算偏差是一种有效和高效的过滤异常值的方法。 - MSalters
@MSalters:(回复一个三年前的评论)切比雪夫不等式的精度不够实用,如果要截取至少95%的数据集,我需要做4.5 sigmas;但是,如果数据恰好是正常分布,则会显示99.999%的数据-与目标相差甚远。换句话说,我会以2.25倍的因数缩小,即显示比必要范围大5倍的区域,从而使有趣的部分变得微不足道。如果数据比正常分布更具尖锐性,情况会更糟。所以,当然,这可以是绝对最低限度,但它并不是很好的近似值。 - Eamon Nerbonne

0

虽然我不是专家,但我的记忆告诉我:

  • 要精确确定百分位点,您需要进行排序和计数
  • 从数据中取样并计算百分位值听起来像是一个不错的计划,如果您能得到一个好的样本,则可以得到相当准确的近似值
  • 如果没有,正如 Henrik 建议的那样,您可以通过桶分类和计数来避免完全排序

0

一组包含100k个元素的数据几乎不需要时间来进行排序,因此我假设您需要重复执行此操作。如果数据集是相同的集合,只是稍微更新了一下,那么最好建立一棵树(O(N log N)),然后在新点到达时删除和添加新点(O(K log N),其中K是更改的点数)。否则,已经提到的第k大元素解决方案为每个数据集提供O(N)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接