2013-01-13 43 views
16

Tôi đang tìm cách thực hiện một số chức năng như rolling_* khác nhau của pandas, nhưng tôi muốn cửa sổ tính toán cán được xác định bởi một dải giá trị (nói, một loạt các giá trị của một cột của DataFrame), không phải bởi số hàng trong cửa sổ.Tính toán cán gấu trúc với cửa sổ dựa trên giá trị thay vì đếm

Như một ví dụ, giả sử tôi có dữ liệu này:

>>> 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 

Nếu tôi làm điều gì đó như rolling_sum(d, 5), tôi nhận được một số tiền cán trong đó mỗi cửa sổ chứa 5 hàng. Nhưng những gì tôi muốn là một tổng số cán trong đó mỗi cửa sổ chứa một phạm vi nhất định các giá trị của RollBasis. Tức là, tôi muốn có thể làm điều gì đó như d.roll_by(sum, 'RollBasis', 5) và nhận được kết quả khi cửa sổ đầu tiên chứa tất cả các hàng có RollBasis nằm trong khoảng từ 1 đến 5, sau đó cửa sổ thứ hai chứa tất cả các hàng có RollBasis nằm trong khoảng từ 2 đến 6, sau đó cửa sổ thứ ba chứa tất cả các hàng có RollBasis nằm trong khoảng từ 3 đến 7, vv Cửa sổ sẽ không có số hàng bằng nhau, nhưng phạm vi giá trị RollBasis được chọn trong mỗi cửa sổ sẽ giống nhau. Vì vậy, sản lượng nên được như:

>>> 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. 

Tôi không thể làm điều này với groupby, vì groupby luôn luôn tạo nhóm rời nhau. Tôi không thể làm điều đó với các chức năng cán, bởi vì cửa sổ của họ luôn luôn cuộn theo số hàng, không phải bởi các giá trị. Vậy làm thế nào tôi có thể làm điều đó?

Trả lời

12

Tôi nghĩ rằng đây có phải những gì bạn muốn:

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() 

Chức năng trên có một giá trị, trong trường hợp này RollBasis, và sau đó chỉ số cột khung dữ liệu ToRoll dựa trên giá trị đó. Chuỗi trả về bao gồm các giá trị ToRoll đáp ứng tiêu chí RollBasis + 5. Cuối cùng, chuỗi đó được tổng hợp và trả về.

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 

Mã cho ví dụ đồ chơi DataFrame trong trường hợp ai đó muốn thử:

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

Cảm ơn, điều đó có vẻ như vậy. Tôi đã thêm câu trả lời của riêng mình với một phiên bản tổng quát hơn về điều này, nhưng tôi chấp nhận câu trả lời của bạn. – BrenBarn

11

Dựa trên câu trả lời Zelazny7, tôi tạo ra giải pháp tổng quát hơn này:

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 

Nó vẫn không lý tưởng vì nó rất chậm so với rolling_apply, nhưng có lẽ điều này là không thể tránh khỏi.

+0

Điều này sẽ nhanh hơn nếu thay vì lọc theo giá trị của cột thứ hai, bạn lọc giá trị của chỉ mục. Các chỉ mục Pandas hiện hỗ trợ các mục không duy nhất, vì vậy bạn có thể tăng tốc giải pháp bằng cách thiết lập cột cơ sở làm chỉ mục và sau đó lọc trên đó. –

9

Dựa trên câu trả lời BrenBarns, nhưng tăng tốc bằng cách sử dụng nhãn dựa indexing hơn là boolean chỉ mục dựa trên:

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 

Đây là nhiều nhanh hơn so với không sử dụng một cột được lập chỉ mục:

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 

Cần lưu ý rằng giải pháp dựa trên chỉ mục là O (n), trong khi phiên bản cắt lôgic là O (n^2) trong trường hợp trung bình (tôi nghĩ).

Tôi thấy hữu ích hơn khi thực hiện điều này trên các cửa sổ cách đều nhau từ giá trị tối thiểu của cơ sở đến giá trị tối đa của cơ sở, thay vì ở mọi giá trị cơ sở.Điều này có nghĩa là thay đổi chức năng như vậy:

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

Để điều này hoạt động chính xác (ít nhất là trong gấu trúc 0,14), tôi nghĩ bạn cần thay thế chunk = indexed_what [indexer] bởi chunk = indexed_what.iloc [indexer]. Nếu không thì slice được coi là một phạm vi chỉ mục nhưng nó là một phạm vi vị trí. – feilchenfeldt

+0

Điều này không trả lại kết quả tương tự như câu trả lời của Bren vì cách chúng xử lý các khoảng mở/đóng (xem kết quả cho chỉ số 5 trong ví dụ). Nó cũng không cho phép những gì là một DF thay vì chỉ là một loạt. – Luis

Các vấn đề liên quan