【问题标题】:Fast way to get rolling percentile ranks获得滚动百分位数排名的快速方法
【发布时间】:2021-10-20 04:14:07
【问题描述】:

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

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

【问题讨论】:

    标签: python pandas numpy scipy rank


    【解决方案1】:

    您可以使用 swifter 包更快地应用百分位数。

    https://github.com/jmcarpenter2/swifter

    【讨论】:

    • 我试过这个:`scores = df.swifter.rolling(10).apply(lambda x: stats.percentileofscore(x, x[-1]))` 但比原来的还要慢代码。我做错了吗?
    • 在应用之前,滚动之后尝试使用 swifter。
    • 我试过 `scores = df.rolling(10).swifter.apply(lambda x: stats.percentileofscore(x, x[-1])) ` 但我得到了这个 AttributeError: 'Rolling'对象没有属性“swifter”
    【解决方案2】:

    这是使用 pandas-only 工具编写此代码的方法,pd.DataFrame.rank() 派上用场:

    df.rolling(10).apply(lambda x: x.rank(pct=True).iloc[-1])
    

    如果这仍然很慢并且您的窗口合理,您可以跨轴连接以生成所有要比较的值,然后使用groupby.rank() 在每组值内进行比较:

    >>> pd.concat({n: df.shift(10 - n) for n in range(10)})
             A     B
    0 0    NaN   NaN
      1    NaN   NaN
      2    NaN   NaN
      3    NaN   NaN
      4    NaN   NaN
    ...    ...   ...
    9 95  17.0   9.0
      96  12.0  11.0
      97  11.0  19.0
      98   4.0  15.0
      99   8.0  17.0
    
    [1000 rows x 2 columns]
    >>> grouped = pd.concat({n: df.shift(n) for n in range(10)}).groupby(level=1)
    >>> grouped.rank(pct=True).loc[0].where(grouped.count().eq(10))
           A     B
    0    NaN   NaN
    1    NaN   NaN
    2    NaN   NaN
    3    NaN   NaN
    4    NaN   NaN
    ..   ...   ...
    95  0.75  0.50
    96  0.60  1.00
    97  0.20  0.60
    98  0.50  0.70
    99  0.75  0.35
    
    [100 rows x 2 columns]
    

    我们可以将其与@w-m 的出色答案进行比较,后者使用总和计算排名,这给出的结果略有不同,可能是在成绩之间的平局的情况下。使用 pandas 的滑动窗口视图计算可能如下所示:

    >>> sum(df.shift(n).le(df) for n in range(10)).div(10)
          A    B
    0   0.1  0.1
    1   0.1  0.2
    2   0.1  0.1
    3   0.2  0.1
    4   0.1  0.4
    ..  ...  ...
    95  0.8  0.5
    96  0.6  1.0
    97  0.2  0.6
    98  0.5  0.7
    99  0.8  0.4
    
    [100 rows x 2 columns]
    

    请注意,您始终可以将 .where(df.index.to_series().ge(10)) 添加到结果数据帧中以删除前 10 行。

    当我比较这些解决方案以及来自@w-m 的帖子时,会发生以下情况:

    您可以看到滑动窗口保持更快。如果您使用的是 pandas,您不妨使用rank(),它不会慢很多并且给您更多的灵活性。 .apply() 技术总是很慢。

    通过以下方式获得的结果:

    import numpy as np, pandas as pd, timeit
    
    glob = {'df': pd.DataFrame(np.random.uniform(0, 10, size=(1000, 3)), columns=list("ABC")), 'pctrank': pctrank, 'pctrank_comp': pctrank_comp, 'sliding_window_view': np.lib.stride_tricks.sliding_window_view, 'pd': pd}
    timeit.timeit('df.rolling(window=10,center=False).apply(pctrank)', globals=glob, number=10) / 10
    timeit.timeit('df.rolling(window=10,center=False).apply(pctrank_comp)', globals=glob, number=100) / 100
    timeit.timeit('data = df.to_numpy(); sw = sliding_window_view(data, 10, axis=0); pd.DataFrame((sw <= sw[..., -1:]).sum(axis=2) / sw.shape[-1], columns=df.columns)', globals=glob, number=10_000) / 10_000
    timeit.timeit('pd.concat({n: df.shift(n).le(n) for n in range(10)}).groupby(level=1).sum()', globals=glob, number=10_000) / 10_000
    timeit.timeit('sum(df.shift(n).le(df) for n in range(10)).div(10)', globals=glob, number=10_000) / 10_000
    timeit.timeit('pd.concat({n: df.shift(n) for n in range(10)}).groupby(level=1).rank(pct=True).loc[0]', globals=glob, number=1000) / 1000
    

    【讨论】:

    • 简洁、简洁、快速,我喜欢!在我的回答中将它们添加到基准中。
    • 奇怪的是,在我的机器上,在大约 10,000 行标记处,您的 pandas sum 解决方案变得比 NumPy 滑动窗口版本更快。对我来说,它比 1M 行的 NumPy 版本快 3 倍。
    • 哇,这太令人惊讶了@w-m,我原以为 sliding_window_view 仍然更有效率……这个名字似乎暗示它是一个 view,这避免了不复制数据。
    • 减速来自将滑动窗口作为最后一个轴。转置滑动窗口(因为..原因,我猜!)使 NumPy 解决方案比 Pandas sum 版本快约 2 倍。噗!
    【解决方案3】:

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

    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)
    

    【讨论】:

    • 谢谢。奇怪的是,在我的数据集(2600 行 * 126 列)上,使用 apply 的不同实现慢了 3 倍,但其他实现几乎是瞬时的。
    猜你喜欢
    • 1970-01-01
    • 2013-01-31
    • 1970-01-01
    • 2016-01-26
    • 2011-04-13
    • 2019-12-29
    • 2017-08-01
    • 1970-01-01
    • 2020-05-22
    相关资源
    最近更新 更多