Ale*_*man 7 python numpy moving-average dataframe pandas
用Python计算滚动(又称移动窗口)修剪平均值的最有效方法是什么?
例如,对于50K行和窗口大小为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
Run Code Online (Sandbox Code Playgroud)
窗口大小为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
]
})
Run Code Online (Sandbox Code Playgroud)
天真实现的示例代码
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
Run Code Online (Sandbox Code Playgroud)
关于DataFrame vs list vs NumPy数组的注释
只需将数据从DataFrame移动到列表,我就可以使用相同的算法获得3.5倍的速度提升.有趣的是,使用NumPy阵列也可以提供几乎相同的速度提升.但是,必须有更好的方法来实现这一目标并实现数量级的提升.
一个可以派上用场的观察是你不需要在每一步中对所有值进行排序.相反,如果您确保窗口始终排序,您需要做的就是在相关位置插入新值,并从原来的位置删除旧值,这两个操作都可以在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
Run Code Online (Sandbox Code Playgroud)
现在,在这种情况下,额外的好处是比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)
Run Code Online (Sandbox Code Playgroud)
值得注意的是,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)
Run Code Online (Sandbox Code Playgroud)
以下似乎远非最优的方法优于上述两种方法:
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
Run Code Online (Sandbox Code Playgroud)
定时:
> %timeit rolling_mean_np(data.values)
564 ms ± 4.44 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Run Code Online (Sandbox Code Playgroud)
更重要的是,这一次,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)
Run Code Online (Sandbox Code Playgroud)
虽然我们正在努力,但我们只是快速验证这实际上是否符合我们的预期:
> np.all(rolling_mean_np_jit(data.values)[49:] == data.rolling(50).apply(lambda w: trim_mean(w, 0.06)).values[49:])
True
Run Code Online (Sandbox Code Playgroud)
事实上,通过帮助分拣机一点点,我们可以挤出另一个因子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
Run Code Online (Sandbox Code Playgroud)
现在,在这个例子中正在进行的"排序"当然只是归结为将新元素放在正确的位置,同时将所有内容移到一个之间.手动执行此操作将使纯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
Run Code Online (Sandbox Code Playgroud)
在这一点上,大部分时间都花在了np.searchsorted,所以让我们让搜索本身对JIT友好.采用源代码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
Run Code Online (Sandbox Code Playgroud)
这将我们降低到12毫秒,比原始熊猫+ SciPy方法提高了x60:
> %timeit rolling_mean_np_jitted_search(data.values)
12 ms ± 210 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Run Code Online (Sandbox Code Playgroud)
您可以尝试使用scipy.stats.trim_mean:
from scipy.stats import trim_mean
df['value'].rolling(5).apply(lambda x: trim_mean(x, 0.2))
Run Code Online (Sandbox Code Playgroud)
[输出]
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
Run Code Online (Sandbox Code Playgroud)
请注意,我必须使用rolling(5)和proportiontocut=0.2您的玩具数据集.
对于您的真实数据,您应该使用rolling(50)和trim_mean(x, 0.06)从滚动窗口中删除顶部和底部3值.
| 归档时间: |
|
| 查看次数: |
752 次 |
| 最近记录: |