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

标签 python pandas numpy scipy rank

假设我们有一个像这样的 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
.....

我想获得所有列的滚动百分位数排名,窗口包含 10 个观察值。 以下代码有效但速度很慢:

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

我也试过这个,但它更慢:

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)

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

最佳答案

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

def pctrank_comp(x):
    x = x.to_numpy()
    smaller_eq = (x <= x[-1]).sum()
    return smaller_eq / len(x)

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

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 版本更快,约为 930x(并且可能使用更少的内存?),但更复杂。对于更大的数据集,它变得比 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 倍,对于 100 万行,比 Pandas 求和版本快约 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)

关于python - 获得滚动百分位数排名的快速方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68831145/

相关文章:

python - 从异常对象中提取回溯信息

python - RuntimeError at/无法缓存函数 '__shear_dense' : no locator available for file '/home/...site-packages/librosa/util/utils.py'

python - 一旦搜索到列表中的字符串,有什么方法可以获取列表的名称吗?

python - 在python中从字符串创建变量

python-3.x - 如何按列分组并标准化?

python - 匀称的交叉点与匀称的关系 - 不准确?

python - Pandas 仅对某些列求和和计数

python - 使用 numpy 仅加载符合特定条件的行

python - 找到第一个 np.nan 值位置的最有效方法是什么?

python - 替换二维 numpy 数组中的一行