Скользящие вычисления панд с окном, основанным на значениях, а не на счетчиках
Я ищу способ сделать что-то вроде различных 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 и т. д. Окна не будут иметь одинаковое количество строк, но диапазон 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
всегда производит непересекающиеся группы. Я не могу сделать это с функциями прокрутки, потому что их окна всегда катятся по количеству строк, а не по значениям. Так как я могу это сделать?
4 ответа
Я думаю, что это делает то, что вы хотите:
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 на основе этого значения. Возвращенный ряд состоит из значений ToRoll, которые соответствуют критерию RollBasis + 5. Наконец, эта серия суммируется и возвращается.
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+')
Основано на ответе 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
Основываясь на ответе 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
, но, возможно, это неизбежно.
Чтобы расширить ответ , я расширил его таким образом, чтобы его можно было использовать непосредственно в фрейме данных, привязав метод к классу 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@Ian Sudbury , но не такой медленный, как у @BrenBarn, но он обеспечивает большую гибкость в отношении функций, которые можно вызывать на них.