2013-01-13 32 views
16

我正在尋找一種方法來做類似於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之間,那麼第三個窗口將包含所有行,其行數在3到7之間等等。窗口的行數不會相同,但在每個窗口中選擇的值的範圍將是相同的。所以輸出應該是這樣的:

>>> 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總是產生不相交的組。我不能用滾動函數來做,因爲它們的窗口總是按行數滾動,而不是按值滾動。那我該怎麼做呢?

回答

12

我想這你想要做什麼:

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 

規範的情況下,別人的玩具例如數據幀想嘗試:

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

謝謝,似乎這樣做。我用一個更一般的版本添加了我自己的答案,但我接受你的答案。 – BrenBarn

11

基於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非常緩慢,但也許這是不可避免的。

+0

如果不是對第二列的值進行篩選,而是對索引的值進行篩選,則這會更快。熊貓指數目前支持非唯一條目,因此您可以通過將基礎列設置爲索引並對其進行過濾來加速解決方案。 –

9

基於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)in平均情況(我認爲)。

我發現它是通過均勻間隔的窗口從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 
+0

爲了正確工作(至少在pandas 0.14中),我認爲你需要用chunk = indexed_what.iloc [indexer]替換chunk = indexed_what [indexer]。否則,切片被視爲索引範圍,但它是一個位置範圍。 – feilchenfeldt

+0

由於它們如何處理打開/關閉時間間隔(請參閱示例中的索引5的結果),它不會返回與Bren的答案相同的結果。它也不允許什麼是DF而不是僅僅是一個系列。 – Luis