如何找到系列中的异常值,矢量化?

Posted

技术标签:

【中文标题】如何找到系列中的异常值,矢量化?【英文标题】:How to find outliers in a series, vectorized? 【发布时间】:2013-12-30 15:27:51 【问题描述】:

我有一个 pandas.Series 正数。我需要找到“异常值”的索引,其值与之前的“规范”相差3 或更多。

如何向量化这个函数:

def baseline(s):
    values = []
    indexes = []
    last_valid = s.iloc[0]
    for idx, val in s.iteritems():
        if abs(val - last_valid) >= 3:
            values.append(val)
            indexes.append(idx)
        else:
            last_valid = val
    return pd.Series(values, index=indexes)

例如,如果输入是:

import pandas as pd
s = pd.Series([7,8,9,10,14,10,10,14,100,14,10])
print baseline(s)

想要的输出是:

4     14
7     14
8    100
9     14

请注意,14s 之后的 10 值不会返回,因为它们是“恢复正常”值。

编辑:

在代码中添加了abs()。数字是正数。 这里的目的是加快代码速度。 不完全模仿代码的答案可能可以接受的。 更改了示例以包含另一个极端情况,其中值缓慢变化 3。

【问题讨论】:

您如何准确确定哪个是异常值?因为您显示的所需输出只是s[s>3] @joris,我添加了准确定义它的代码。 您的代码给出了所需的输出。您的确切需求是什么?更好的解决方案? 这是异常值的不寻常定义。所以pd.Series([10,-1e6,12,13,14]) 应该返回[12, 13, 14]?您是否缺少abs @justhalf:代码确实产生最后 14 个。自己试试吧。 【参考方案1】:

这是我原来的“矢量化”解决方案:

您可以使用shift 和numpy 的where 获得last_valid

In [1]: s = pd.Series([10, 10, 10, 14, 10, 10, 10, 14, 100, 14, 10])

In [2]: last_valid = pd.Series(np.where((s - s.shift()).abs() < 3, s, np.nan))
        last_valid.iloc[0] = s.iloc[0]  # initialize with first value of s
        last_valid.ffill(inplace=True)

In [3]: last_valid
Out[3]:
0      7
1      8
2      9
3     10
4     10
5     10
6     10
7     10
8     10
9     10
10    10
dtype: float64

这使问题变得更加容易。您可以将此与s 进行比较:

In [4]: s - last_valid  # alternatively use (s - last_valid).abs()
Out[4]: 
0      0
1      0
2      0
3      0
4      4
5      0
6      0
7      4
8     90
9      4
10     0
dtype: float64

那些相差大于+3的元素:

In [5]: (s - last_valid).abs() >= 3
Out[5]: 
0     False
1     False
2     False
3     False
4      True
5     False
6     False
7      True
8      True
9      True
10    False
dtype: bool

In [6]: s[(s - last_valid).abs() >= 3]
Out[6]: 
4     14
7     14
8    100
9     14
dtype: int64

如您所愿。 ...至少看起来,@alko 的示例表明这并不完全正确。

更新

正如@alko 所指出的,下面的矢量化方法并不完全正确,特别是对于示例s = pd.Series([10, 14, 11, 10, 10, 12, 14, 100, 100, 14, 10]),我的“矢量化”方法将第二个 100 列为“不是异常值”,即使它在基线中。

这让我(连同@alko)认为这不能被矢量化。作为替代方案,我包含了一个简单的 cython 实现(参见 cython section of pandas docs),它比原生 python 快得多:

%%cython
cimport numpy as np
import numpy as np
cimport cython
@cython.wraparound(False)
@cython.boundscheck(False)
cpdef _outliers(np.ndarray[double] s):
    cdef np.ndarray[Py_ssize_t] indexes
    cdef np.ndarray[double] vals
    cdef double last, val
    cdef Py_ssize_t count
    indexes = np.empty(len(s), dtype='int')
    vals = np.empty(len(s))
    last = s[0]
    count = 0
    for idx, val in enumerate(s):
        if abs(val - last) >= 3:
            indexes[count] = idx
            vals[count] = val
            count += 1
        else:
            last = val
    return vals[:count], indexes[:count]

def outliers(s):
    return pd.Series(*_outliers(s.values.astype('float')))

一些时间指示:

In [11]: s = pd.Series([10,10,12,14,100,100,14,10])

In [12]: %timeit baseline(s)
10000 loops, best of 3: 132 µs per loop

In [13]: %timeit outliers(s)
10000 loops, best of 3: 46.8 µs per loop

In [21]: s = pd.Series(np.random.randint(0, 100, 100000))

In [22]: %timeit baseline(s)
10 loops, best of 3: 161 ms per loop

In [23]: %timeit outliers(s)
100 loops, best of 3: 9.43 ms per loop

有关更多信息,请参阅 pandas 文档的 cython (enhancing performance) section。

【讨论】:

不过,这不是当前代码所做的:它给出 3 14、7 14、8 100、9 14。 s = pd.Series([10,14,11,10,10,10,11,14,100,14,10]) 是另一个很好的边缘案例 重点是问题中的last_valid 是递归定义的,恕我直言无法用矢量化方法评估 修复了这两种边缘情况,将 last_valid 初始化为 s.iloc[0]。如果您发现更多边缘案例,请告诉我! :) @AndyHayden 我仍然不相信 last_valid 可以用矢量化方法计算。我的下一次尝试:) s = pd.Series([10,14,11,10,10,12,14,100,100,14,10])【参考方案2】:

我解决这个问题的尝试是将它表述为 last_valid 的循环表达式,然后遵循 Andy Hayden 路径。 last_valid 的表达式为:

lv[i] = lv[i-1] if diff >= 3 else v[i]

在哪里

diff = abs(v[i] - lv[i-1]))

其中 i 是迭代次数。为了在 DataFrame 中实现这一点,我使用了来自 ctype 的指针。 通过取消对指向列 lv (last_valid) 的指针的移位列的引用,在一个迭代步骤中计算的值可以在下一个迭代步骤中访问。

from ctypes import *
import pandas as pd

df = pd.DataFrame('s_num': [7,8,9,10,14,10,10,14,100,14,10])
df['s'] = df['s_num'].apply(c_long)
df['lv'] = [c_long(0) for x in range(len(df))]
df.ix[0, 'lv'] = df.ix[0, 's']
df['p_lv'] = df['lv'].apply(pointer)
df['p_lv_m1'] = df['p_lv'].shift()

def ff(x):
    if not pd.isnull(x['p_lv_m1']):
        diff = abs(x['s'].value - x['p_lv_m1'].contents.value)
        x['p_lv'][0] = x['s'] if diff < 3 else x['p_lv_m1'][0]
    return None

df.apply(ff, 1)
df['lv_num'] = df['lv'].apply(lambda x: x.value)
df = df[['s_num', 'lv_num']]

print df.ix[(df['s_num'] - df['lv_num']).abs() >= 3, 's_num']

【讨论】:

很高兴了解 ctypes。请注意,Andy 的解决方案要快 10 倍。 @Yariv 我没有尝试为必须应用递归定义操作的问题构建快速解决方案,而是构建通用解决方案。优点是,如果您决定更改异常值标准以使用多个点(平滑),那么这很容易做到。 使用 ctype 有什么好处吗?我也没见过它用过。我见过cython used to get similar-style speed boosts,一个好处是您可以直接访问numpy 数组(.apply(c_long) 在这里看起来像是一个黑客,我认为您会失去性能)。想投票,但这个解决方案对我来说太冗长/令人困惑/神奇!需要一些解释:) 如果 python 将支持 id() 并定义解引用操作,它就不会那么混乱。整个技巧在 ff func 中:通过指针可以访问前一行的值。抱歉没时间再说了,也许明年晚些时候我会有机会更好地解释自己。

以上是关于如何找到系列中的异常值,矢量化?的主要内容,如果未能解决你的问题,请参考以下文章

如何获取显示列异常值的行?

如何向线性回归数据集添加异常值?

查找一组双精度值中的异常值

检测零膨胀和过度分散计数数据中的异常值

如何删除 Python boxplot 图形图像中的异常值?

我们如何区分数据挖掘中的噪声和异常值