pandas 基于值而不是计数的窗口滚动计算

Posted

技术标签:

【中文标题】pandas 基于值而不是计数的窗口滚动计算【英文标题】:pandas rolling computation with window based on values instead of counts 【发布时间】:2012-12-27 08:52:05 【问题描述】:

我正在寻找一种方法来执行类似于 pandas 的各种 rolling_* 函数的方法,但我希望滚动计算的窗口由一系列值定义(例如,一系列值DataFrame 的列),而不是窗口中的行数。

举个例子,假设我有这个数据:

>>> print d
   RollBasis  ToRoll
0          1       1
1          1       4
2          1      -5
3          2       2
4          3      -4
5          5      -2
6          8       0
7         10     -13
8         12      -2
9         13      -5

如果我执行rolling_sum(d, 5) 之类的操作,我会得到一个滚动总和,其中每个窗口包含 5 行。但我想要的是一个滚动总和,其中每个窗口都包含一定范围的RollBasis 值。也就是说,我希望能够执行d.roll_by(sum, 'RollBasis', 5) 之类的操作,并得到第一个窗口包含RollBasis 介于1 和5 之间的所有行的结果,然后第二个窗口包含RollBasis 的所有行介于 2 和 6 之间,则第三个窗口包含 RollBasis 介于 3 和 7 之间的所有行,依此类推。窗口的行数不会相等,但在每个窗口中选择的 RollBasis 值的范围将是相同的。所以输出应该是这样的:

>>> d.roll_by(sum, 'RollBasis', 5)
    1    -4    # sum of elements with 1 <= Rollbasis <= 5
    2    -4    # sum of elements with 2 <= Rollbasis <= 6
    3    -6    # sum of elements with 3 <= Rollbasis <= 7
    4    -2    # sum of elements with 4 <= Rollbasis <= 8
    # etc.

我不能对groupby 执行此操作,因为groupby 总是产生不相交的组。我不能用滚动函数来做到这一点,因为它们的窗口总是按行数滚动,而不是按值滚动。那我该怎么做呢?

【问题讨论】:

【参考方案1】:

我认为这是你想要的:

In [1]: df
Out[1]:
   RollBasis  ToRoll
0          1       1
1          1       4
2          1      -5
3          2       2
4          3      -4
5          5      -2
6          8       0
7         10     -13
8         12      -2
9         13      -5

In [2]: def f(x):
   ...:     ser = df.ToRoll[(df.RollBasis >= x) & (df.RollBasis < x+5)]
   ...:     return ser.sum()

上述函数采用一个值,在本例中为 RollBasis,然后根据该值索引数据框列 ToRoll。返回的系列由符合 RollBasis + 5 标准的 ToRoll 值组成。最后,对该系列求和并返回。

In [3]: df['Rolled'] = df.RollBasis.apply(f)

In [4]: df
Out[4]:
   RollBasis  ToRoll  Rolled
0          1       1      -4
1          1       4      -4
2          1      -5      -4
3          2       2      -4
4          3      -4      -6
5          5      -2      -2
6          8       0     -15
7         10     -13     -20
8         12      -2      -7
9         13      -5      -5

玩具示例 DataFrame 的代码,以防其他人想尝试:

In [1]: from pandas import *

In [2]: import io

In [3]: text = """\
   ...:    RollBasis  ToRoll
   ...: 0          1       1
   ...: 1          1       4
   ...: 2          1      -5
   ...: 3          2       2
   ...: 4          3      -4
   ...: 5          5      -2
   ...: 6          8       0
   ...: 7         10     -13
   ...: 8         12      -2
   ...: 9         13      -5
   ...: """

In [4]: df = read_csv(io.BytesIO(text), header=0, index_col=0, sep='\s+')

【讨论】:

谢谢,好像可以了。我用更通用的版本添加了我自己的答案,但我接受了你的答案。【参考方案2】:

根据 Zelazny7 的回答,我创建了这个更通用的解决方案:

def rollBy(what, basis, window, func):
    def applyToWindow(val):
        chunk = what[(val<=basis) & (basis<val+window)]
        return func(chunk)
    return basis.apply(applyToWindow)

>>> rollBy(d.ToRoll, d.RollBasis, 5, sum)
0    -4
1    -4
2    -4
3    -4
4    -6
5    -2
6   -15
7   -20
8    -7
9    -5
Name: RollBasis

它仍然不理想,因为它与rolling_apply 相比非常慢,但也许这是不可避免的。

【讨论】:

如果不是过滤第二列的值,而是过滤索引的值,这会快得多。 Pandas 索引目前支持非唯一条目,因此您可以通过将基础列设置为索引然后对其进行过滤来加快您的解决方案。【参考方案3】:

基于 BrenBarns 的回答,但通过使用基于标签的索引而不是基于布尔的索引加快了速度:

def rollBy(what,basis,window,func,*args,**kwargs):
    #note that basis must be sorted in order for this to work properly     
    indexed_what = pd.Series(what.values,index=basis.values)
    def applyToWindow(val):
        # using slice_indexer rather that what.loc [val:val+window] allows
        # window limits that are not specifically in the index
        indexer = indexed_what.index.slice_indexer(val,val+window,1)
        chunk = indexed_what[indexer]
        return func(chunk,*args,**kwargs)
    rolled = basis.apply(applyToWindow)
    return rolled

这比不使用索引列快很多

In [46]: df = pd.DataFrame("RollBasis":np.random.uniform(0,1000000,100000), "ToRoll": np.random.uniform(0,10,100000))

In [47]: df = df.sort("RollBasis")

In [48]: timeit("rollBy_Ian(df.ToRoll,df.RollBasis,10,sum)",setup="from __main__ import rollBy_Ian,df", number =3)
Out[48]: 67.6615059375763

In [49]: timeit("rollBy_Bren(df.ToRoll,df.RollBasis,10,sum)",setup="from __main__ import rollBy_Bren,df", number =3)
Out[49]: 515.0221037864685

值得注意的是,基于索引的解决方案是 O(n),而逻辑切片版本在平均情况下是 O(n^2)(我认为)。

我发现在从 Basis 的最小值到 Basis 的最大值的均匀间隔的窗口上执行此操作比在每个基础值上执行此操作更有用。这意味着改变函数:

def rollBy(what,basis,window,func,*args,**kwargs):
    #note that basis must be sorted in order for this to work properly
    windows_min = basis.min()
    windows_max = basis.max()
    window_starts = np.arange(windows_min, windows_max, window)
    window_starts = pd.Series(window_starts, index = window_starts)
    indexed_what = pd.Series(what.values,index=basis.values)
    def applyToWindow(val):
        # using slice_indexer rather that what.loc [val:val+window] allows
        # window limits that are not specifically in the index
        indexer = indexed_what.index.slice_indexer(val,val+window,1)
        chunk = indexed_what[indexer]
        return func(chunk,*args,**kwargs)
    rolled = window_starts.apply(applyToWindow)
    return rolled

【讨论】:

为了使其正常工作(至少在 pandas 0.14 中),我认为您需要将 chunk = indexed_what[indexer] 替换为 chunk = indexed_what.iloc[indexer]。否则,切片被视为索引范围,但它是位置范围。 这不会返回与 Bren 的答案相同的结果,因为它们如何处理开/闭区间(参见示例中索引 5 的结果)。它也不允许什么是 DF 而不仅仅是一个系列。 将此与日期时间/时间戳一起使用,它仍然有效。不过需要一点按摩。至于 Luis 关于不同结果的评论,只需将 window int 减一,因为 slice_indexer 在开始和结束时都包含在内。 (>=,=, 另一个让事情正常工作的解决方案是将chunk = indexed_what[indexer]替换为chunk = indexed_what.index[indexer]。我还没有测试这是否比@Luis 的 iloc 解决方案更快。【参考方案4】:

为了扩展@Ian Sudbury 的答案,我将其扩展为可以通过将方法绑定到 DataFrame 类直接在数据帧上使用它(我希望我的代码速度,因为我不知道如何访问类的所有内部)。

我还添加了面向后向窗口和居中窗口的功能。它们只有在您远离边缘时才能完美运行。

import pandas as pd
import numpy as np

def roll_by(self, basis, window, func, forward=True, *args, **kwargs):
    the_indexed = pd.Index(self[basis])
    def apply_to_window(val):
        if forward == True:
            indexer = the_indexed.slice_indexer(val, val+window)
        elif forward == False:
            indexer = the_indexed.slice_indexer(val-window, val)
        elif forward == 'both':
            indexer = the_indexed.slice_indexer(val-window/2, val+window/2)
        else:
            raise RuntimeError('Invalid option for "forward". Can only be True, False, or "both".')
        chunck = self.iloc[indexer]
        return func(chunck, *args, **kwargs)
    rolled = self[basis].apply(apply_to_window)
    return rolled

pd.DataFrame.roll_by = roll_by

对于其他测试,我使用了以下定义:

def rollBy_Ian_iloc(what,basis,window,func,*args,**kwargs):
    #note that basis must be sorted in order for this to work properly   
    indexed_what = pd.Series(what.values,index=basis.values)
    def applyToWindow(val):
        # using slice_indexer rather that what.loc [val:val+window] allows
        # window limits that are not specifically in the index
        indexer = indexed_what.index.slice_indexer(val,val+window,1)
        chunk = indexed_what.iloc[indexer]
        return func(chunk,*args,**kwargs)
    rolled = basis.apply(applyToWindow)
    return rolled

def rollBy_Ian_index(what,basis,window,func,*args,**kwargs):
    #note that basis must be sorted in order for this to work properly   
    indexed_what = pd.Series(what.values,index=basis.values)
    def applyToWindow(val):
        # using slice_indexer rather that what.loc [val:val+window] allows
        # window limits that are not specifically in the index
        indexer = indexed_what.index.slice_indexer(val,val+window,1)
        chunk = indexed_what[indexed_what.index[indexer]]
        return func(chunk,*args,**kwargs)
    rolled = basis.apply(applyToWindow)
    return rolled

def rollBy_Bren(what, basis, window, func):
    def applyToWindow(val):
        chunk = what[(val<=basis) & (basis<val+window)]
        return func(chunk)
    return basis.apply(applyToWindow)

时间安排和测试:

df = pd.DataFrame("RollBasis":np.random.uniform(0,100000,10000), "ToRoll": np.random.uniform(0,10,10000)).sort_values("RollBasis")
In [14]: %timeit rollBy_Ian_iloc(df.ToRoll,df.RollBasis,10,sum)
    ...: %timeit rollBy_Ian_index(df.ToRoll,df.RollBasis,10,sum)
    ...: %timeit rollBy_Bren(df.ToRoll,df.RollBasis,10,sum)
    ...: %timeit df.roll_by('RollBasis', 10, lambda x: x['ToRoll'].sum())
    ...: 
484 ms ± 28.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.58 s ± 10.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
3.12 s ± 22.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.48 s ± 45.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

结论:绑定的方法不如@Ian Sudbury的方法快,但也没有@BrenBarn的慢,但它确实为调用它们的函数提供了更大的灵活性。

【讨论】:

以上是关于pandas 基于值而不是计数的窗口滚动计算的主要内容,如果未能解决你的问题,请参考以下文章

pandas使用rolling函数计算dataframe指定数据列特定窗口下的滚动有效数值计数(rolling count)自定义指定滚动窗口的大小(window size)

使用窗口函数计算滚动计数

如何有效地计算熊猫时间序列中的滚动唯一计数?

Pandas 按天滚动时间窗口而不是单个行

计算 Pandas 列中值的 3 个月滚动计数

pandas使用groupby函数计算dataframe数据中每个分组的N个数值的滚动计数个数(rolling count)例如,计算某公司的多个店铺每N天(5天)的滚动销售额计数个数