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

时间:2013-01-13 04:50:57

标签: python pandas

我正在寻找一种方法来执行类似rolling_*的各种pandas函数的方法,但我希望滚动计算的窗口由一系列值定义(例如, 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之间的所有行,等等。窗口的行数不会相等,但{{1在每个窗口中选择的值将是相同的。所以输出应该是:

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始终会生成不相交的组。我无法使用滚动功能,因为它们的窗口总是按行数滚动,而不是按值。那我怎么能这样做呢?

3 个答案:

答案 0 :(得分:14)

我认为这可以满足您的需求:

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+')

答案 1 :(得分:12)

根据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相比非常慢,但也许这是不可避免的。

答案 2 :(得分:11)

基于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)(我认为)。

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

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