使用Python进行有效的滚动修整

时间:2018-09-02 09:26:11

标签: python pandas numpy moving-average

用Python计算滚动(即移动窗口)修剪均值的最有效方法是什么?

例如,对于一个5万行的数据集和一个窗口大小为50的数据,对于每一行,我需要获取最后的50行,删除顶部和底部的3个值(窗口大小的5%,四舍五入) ,然后获取其余44个值的平均值。

当前,我要为每一行切片以获取窗口,对窗口进行排序,然后进行切片以对其进行修剪。它的工作速度很慢,但是必须有一种更有效的方法。

示例

[10,12,8,13,7,18,19,9,15,14] # data used for example, in real its a 50k lines df

Example data set and results的窗口大小为5。对于每一行,我们查看最后5行,对它们进行排序,并丢弃1顶和1底行(5的5%= 0.25,向上舍入为1)。然后我们平均剩余的中间行。

用于将此示例集生成为DataFrame的代码

pd.DataFrame({
    'value': [10, 12, 8, 13, 7, 18, 19, 9, 15, 14],
    'window_of_last_5_values': [
        np.NaN, np.NaN, np.NaN, np.NaN, '10,12,8,13,7', '12,8,13,7,18',
        '8,13,7,18,19', '13,7,18,19,9', '7,18,19,9,15', '18,19,9,15,14'
    ],
    'values that are counting for average': [
        np.NaN, np.NaN, np.NaN, np.NaN, '10,12,8', '12,8,13', '8,13,18',
        '13,18,9', '18,9,15', '18,15,14'
    ],
    'result': [
        np.NaN, np.NaN, np.NaN, np.NaN, 10.0, 11.0, 13.0, 13.333333333333334,
        14.0, 15.666666666666666
    ]
})

天真的实现示例代码

window_size = 5
outliers_to_remove = 1

for index in range(window_size - 1, len(df)):
    current_window = df.iloc[index - window_size + 1:index + 1]
    trimmed_mean = current_window.sort_values('value')[
        outliers_to_remove:window_size - outliers_to_remove]['value'].mean()
    # save the result and the window content somewhere

关于DataFrame vs list vs NumPy数组的说明

仅通过将数据从DataFrame移动到列表中,我就可以使用相同的算法获得3.5倍的速度提升。有趣的是,使用NumPy数组也可以提供几乎相同的速度提升。尽管如此,仍然必须有一种更好的方法来实现这一目标并实现数量级的提升。

3 个答案:

答案 0 :(得分:9)

可能会派上用场的一个观察结果是,您无需在每一步中对所有值进行排序。相反,如果您确保窗口始终处于排序状态,那么您要做的就是在相关位置插入新值,然后从旧位置删除旧值,这两个操作都可以在O(log_2 (window_size))使用bisect。实际上,这看起来像

def rolling_mean(data):
    x = sorted(data[:49])
    res = np.repeat(np.nan, len(data))
    for i in range(49, len(data)):
        if i != 49:
            del x[bisect.bisect_left(x, data[i - 50])]
        bisect.insort_right(x, data[i])
        res[i] = np.mean(x[3:47])
    return res

现在,在这种情况下,额外的好处实际上少于scipy.stats.trim_mean所依赖的矢量化所获得的好处,因此,特别是,它仍然比@ChrisA的解决方案慢,但是进一步优化性能的有用起点。

> data = pd.Series(np.random.randint(0, 1000, 50000))
> %timeit data.rolling(50).apply(lambda w: trim_mean(w, 0.06))
727 ms ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
> %timeit rolling_mean(data.values)
812 ms ± 42.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

值得注意的是,Numba的抖动在这种情况下通常很有用,但也没有任何好处:

> from numba import jit
> rolling_mean_jit = jit(rolling_mean)
> %timeit rolling_mean_jit(data.values)
1.05 s ± 183 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

以下看似并非最佳的方法要优于上面考虑的其他两种方法:

def rolling_mean_np(data):
    res = np.repeat(np.nan, len(data))
    for i in range(len(data)-49):
        x = np.sort(data[i:i+50])
        res[i+49] = x[3:47].mean()
    return res

时间:

> %timeit rolling_mean_np(data.values)
564 ms ± 4.44 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

更重要的是,这次,JIT编译确实有用:

> rolling_mean_np_jit = jit(rolling_mean_np)
> %timeit rolling_mean_np_jit(data.values)
94.9 ms ± 605 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

我们正在努力,让我们快速验证一下它是否确实达到了我们的预期:

> np.all(rolling_mean_np_jit(data.values)[49:] == data.rolling(50).apply(lambda w: trim_mean(w, 0.06)).values[49:])
True

实际上,只需一点点帮助分拣器,我们就可以将因素压缩为2,使总时间减少到57毫秒:

def rolling_mean_np_manual(data):
    x = np.sort(data[:50])
    res = np.repeat(np.nan, len(data))
    for i in range(50, len(data)+1):
        res[i-1] = x[3:47].mean()
        if i != len(data):
            idx_old = np.searchsorted(x, data[i-50])
            x[idx_old] = data[i]
            x.sort()
    return res

> %timeit rolling_mean_np_manual(data.values)
580 ms ± 23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
> rolling_mean_np_manual_jit = jit(rolling_mean_np_manual)
> %timeit rolling_mean_np_manual_jit(data.values)
57 ms ± 5.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
> np.all(rolling_mean_np_manual_jit(data.values)[49:] == data.rolling(50).apply(lambda w: trim_mean(w, 0.06)).values[49:])
True

现在,在此示例中进行的“排序”当然可以归结为将新元素放置在正确的位置,同时将所有元素之间移动一遍。手动执行此操作将使纯Python代码变慢,但jitted版本的系数提高了2倍,使我们在30毫秒以下:

def rolling_mean_np_shift(data):
    x = np.sort(data[:50])
    res = np.repeat(np.nan, len(data))
    for i in range(50, len(data)+1):
        res[i-1] = x[3:47].mean()
        if i != len(data):
            idx_old, idx_new = np.searchsorted(x, [data[i-50], data[i]])
            if idx_old < idx_new:
                x[idx_old:idx_new-1] = x[idx_old+1:idx_new]
                x[idx_new-1] = data[i]
            elif idx_new < idx_old:
                x[idx_new+1:idx_old+1] = x[idx_new:idx_old]
                x[idx_new] = data[i]
            else:
                x[idx_new] = data[i]
    return res

> %timeit rolling_mean_np_shift(data.values)
937 ms ± 97.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
> rolling_mean_np_shift_jit = jit(rolling_mean_np_shift)
> %timeit rolling_mean_np_shift_jit(data.values)
26.4 ms ± 693 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
> np.all(rolling_mean_np_shift_jit(data.values)[49:] == data.rolling(50).apply(lambda w: trim_mean(w, 0.06)).values[49:])
True

在这一点上,大多数时间都花在np.searchsorted中,因此让我们使搜索本身对JIT友好。采用the source code for bisect,我们让

@jit
def binary_search(a, x):
    lo = 0
    hi = 50
    while lo < hi:
        mid = (lo+hi)//2
        if a[mid] < x: lo = mid+1
        else: hi = mid
    return lo

@jit
def rolling_mean_np_jitted_search(data):
    x = np.sort(data[:50])
    res = np.repeat(np.nan, len(data))
    for i in range(50, len(data)+1):
        res[i-1] = x[3:47].mean()
        if i != len(data):
            idx_old = binary_search(x, data[i-50])
            idx_new = binary_search(x, data[i])
            if idx_old < idx_new:
                x[idx_old:idx_new-1] = x[idx_old+1:idx_new]
                x[idx_new-1] = data[i]
            elif idx_new < idx_old:
                x[idx_new+1:idx_old+1] = x[idx_new:idx_old]
                x[idx_new] = data[i]
            else:
                x[idx_new] = data[i]
    return res

这使我们降低到12毫秒,比原始熊猫+ SciPy方法提高了60倍:

> %timeit rolling_mean_np_jitted_search(data.values)
12 ms ± 210 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

答案 1 :(得分:6)

您可以尝试使用 scipy.stats.trim_mean

from scipy.stats import trim_mean

df['value'].rolling(5).apply(lambda x: trim_mean(x, 0.2))

[输出]

0          NaN
1          NaN
2          NaN
3          NaN
4    10.000000
5    11.000000
6    13.000000
7    13.333333
8    14.000000
9    15.666667

请注意,我必须为玩具数据集使用rolling(5)proportiontocut=0.2

对于真实数据,应使用rolling(50)trim_mean(x, 0.06)从滚动窗口中删除前3个值。

答案 2 :(得分:0)

我敢打赌,对窗口的每一步进行切片和排序都是比较慢的部分。不必每次都切片,而要单独列出50个(或5个)值。在开始时进行一次排序,然后在添加和删除值(移动窗口)时在正确的位置添加新值,以保留排序顺序(与插入排序算法中的操作非常相似)。然后根据该列表中的值子集计算修整后的均值。 您需要一种方法来保持列表相对于整个集合的位置信息,我认为一个int变量就足够了。