由于您想要滚动窗口中单个元素的排名,因此您不需要在每一步都进行排序。您可以将最后一个值与窗口中的所有其他值进行比较:
def pctrank_comp(x):
x = x.to_numpy()
smaller_eq = (x <= x[-1]).sum()
return smaller_eq / len(x)
要消除应用开销,您可以使用 NumPy v1.20 中的 slide_tricks 在 NumPy 中重写相同的开销:
from numpy.lib.stride_tricks import sliding_window_view
data = df.to_numpy()
sw = sliding_window_view(data, 10, axis=0)
scores_np = (sw <= sw[..., -1:]).sum(axis=2) / sw.shape[-1]
scores_np_df = pd.DataFrame(scores_np, columns=df.columns)
这不包含每列的前 9 个 NaN 值,作为您的解决方案,如果需要,我会留给您解决。
将滑动窗口轴从最后一个轴切换到第一个轴会带来另一个性能提升:
sw = sliding_window_view(data, 10, axis=0).T
scores_np = (sw <= sw[-1:, ...]).sum(axis=0).T / sw.shape[0]
为了进行基准测试,一些 1000 行的测试数据:
df = pd.DataFrame(np.random.uniform(0, 10, size=(1000, 3)), columns=list("ABC"))
问题的原始解决方案出现在 381 毫秒:
%timeit scores = df.rolling(window=10,center=False).apply(pctrank)
381 ms ± 2.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用 apply 实现差异化,在我的机器上快约 5 倍:
%timeit scores_comp = df.rolling(window=10,center=False).apply(pctrank_comp)
71.9 ms ± 318 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Cimbali's answer 的 groupby 解决方案,在我的机器上快了约 45 倍:
%timeit grouped = pd.concat({n: df.shift(n) for n in range(10)}).groupby(level=1); scores_grouped = grouped.rank(pct=True).loc[0].where(grouped.count().eq(10))
8.49 ms ± 182 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
@Cimbali 的 Pandas 滑动窗口,速度快了约 105 倍:
%timeit scores_concat = pd.concat({n: df.shift(n).le(df) for n in range(10)}).groupby(level=1).sum() / 10
3.63 ms ± 136 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
来自@Cimbali 的求和版本,速度快了约 141 倍:
%timeit scores_sum = sum(df.shift(n).le(df) for n in range(10)).div(10)
2.71 ms ± 70.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
上面的 Numpy 滑动窗口解决方案。对于 1000 个元素,它比 Pandas 版本更快,大约为 930 倍(并且可能使用更少的内存?),但更复杂。对于较大的数据集,它会比 Pandas 版本慢。
%timeit data = df.to_numpy(); sw = sliding_window_view(data, 10, axis=0); scores_np = (sw <= sw[..., -1:]).sum(axis=2) / sw.shape[-1]; scores_np_df = pd.DataFrame(scores_np, columns=df.columns)
409 µs ± 4.43 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
最快的解决方案是移动轴,1000 行的速度比原始版本快 2800 倍,1M 行的速度比 Pandas sum 版本快约 2 倍:
%timeit data = df.to_numpy(); sw = sliding_window_view(data, 10, axis=0).T; scores_np = (sw <= sw[-1:, ...]).sum(axis=0).T / sw.shape[0]; scores_np_df = pd.DataFrame(scores_np, columns=df.columns)
132 µs ± 750 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)