获得滚动百分位排名的快速方法

you*_*tti 3 python numpy scipy rank pandas

假设我们有一个像这样的 pandas df :

        A    B    C
day1  2.4  2.1  3.0
day2  4.0  3.0  2.0
day3  3.0  3.5  2.5
day4  1.0  3.1  3.0
.....
Run Code Online (Sandbox Code Playgroud)

我想要获得所有列的滚动百分位数排名,窗口中有 10 个观察值。下面的代码可以工作,但是速度很慢:

scores = pd.DataFrame().reindex_like(df).replace(np.nan, '', regex=True)
scores = df.rolling(10).apply(lambda x: stats.percentileofscore(x, x[-1]))
Run Code Online (Sandbox Code Playgroud)

我也尝试过这个,但速度更慢:

def pctrank(x):
    n = len(x)
    temp = x.argsort()
    ranks = np.empty(n)
    ranks[temp] = (np.arange(n) + 1) / n
    return ranks[-1]
scores = df.rolling(window=10,center=False).apply(pctrank)
Run Code Online (Sandbox Code Playgroud)

有更快的解决方案吗?谢谢

w-m*_*w-m 5

由于您想要滚动窗口中单个元素的排名,因此不需要在每一步都进行排序。您可以将最后一个值与窗口中的所有其他值进行比较:

\n
def pctrank_comp(x):\n    x = x.to_numpy()\n    smaller_eq = (x <= x[-1]).sum()\n    return smaller_eq / len(x)\n
Run Code Online (Sandbox Code Playgroud)\n

要消除应用开销,您可以使用NumPy v1.20 中的slip_tricks在 NumPy 中重写相同的内容:

\n
from numpy.lib.stride_tricks import sliding_window_view\ndata = df.to_numpy()\nsw = sliding_window_view(data, 10, axis=0)\nscores_np = (sw <= sw[..., -1:]).sum(axis=2) / sw.shape[-1]\nscores_np_df = pd.DataFrame(scores_np, columns=df.columns)\n
Run Code Online (Sandbox Code Playgroud)\n

这不包含每列的前 9 个 NaN 值,作为您的解决方案,如果需要,我将让您自行修复该问题。

\n

将滑动窗口轴从最后一个轴切换到第一个轴会带来另一个性能改进:

\n
sw = sliding_window_view(data, 10, axis=0).T\nscores_np = (sw <= sw[-1:, ...]).sum(axis=0).T / sw.shape[0]\n
Run Code Online (Sandbox Code Playgroud)\n

为了进行基准测试,一些具有 1000 行的测试数据:

\n
df = pd.DataFrame(np.random.uniform(0, 10, size=(1000, 3)), columns=list("ABC"))\n
Run Code Online (Sandbox Code Playgroud)\n

问题的原始解决方案在 381 毫秒内出现:

\n
%timeit scores = df.rolling(window=10,center=False).apply(pctrank)\n381 ms \xc2\xb1 2.62 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n
Run Code Online (Sandbox Code Playgroud)\n

使用 apply 进行差异实现,在我的机器上速度提高了约 5 倍:

\n
%timeit scores_comp = df.rolling(window=10,center=False).apply(pctrank_comp)\n71.9 ms \xc2\xb1 318 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

来自Cimbali 答案的groupby 解决方案在我的机器上快了约 45 倍:

\n
%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))\n8.49 ms \xc2\xb1 182 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

来自 @Cimbali 的 Pandas 滑动窗口,速度快约 105 倍:

\n
%timeit scores_concat = pd.concat({n: df.shift(n).le(df) for n in range(10)}).groupby(level=1).sum() / 10\n3.63 ms \xc2\xb1 136 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

来自 @Cimbali 的求和移位版本,快约 141 倍:

\n
%timeit scores_sum = sum(df.shift(n).le(df) for n in range(10)).div(10)\n2.71 ms \xc2\xb1 70.7 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

上面的 Numpy 滑动窗口解决方案。对于 1000 个元素,它比 Pandas 版本快约 930 倍(并且可能使用更少的内存?),但更复杂。对于较大的数据集,它会比 Pandas 版本慢。

\n
%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)\n409 \xc2\xb5s \xc2\xb1 4.43 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 1000 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

最快的解决方案是移动轴,对于 1000 行,比原始版本快 2800 倍,对于 1M 行,比 Pandas sum 版本快约 2 倍:

\n
%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)\n132 \xc2\xb5s \xc2\xb1 750 ns per loop (mean \xc2\xb1 std. dev. of 7 runs, 10000 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n