cómputo de pandas con ventana basada en valores en lugar de conteos

Estoy buscando una manera de hacer algo como las diversas funciones rolling_* de los pandas , pero quiero que la ventana del cálculo rolling_* se defina mediante un rango de valores (por ejemplo, un rango de valores de una columna del DataFrame) , no por el número de filas en la ventana.

Como ejemplo, supongamos que tengo estos datos:

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

Si hago algo como rolling_sum(d, 5) , obtengo una sum rolling_sum(d, 5) en la que cada ventana contiene 5 filas. Pero lo que quiero es una sum RollBasis en la que cada ventana contenga un cierto rango de valores de RollBasis . Es decir, me gustaría poder hacer algo como d.roll_by(sum, 'RollBasis', 5) , y obtener un resultado donde la primera ventana contenga todas las filas cuyo RollBasis esté entre 1 y 5, luego la segunda ventana contiene todas las filas cuyo RollBasis está entre 2 y 6, luego la tercera ventana contiene todas las filas cuyo RollBasis está entre 3 y 7, etc. Las ventanas no tendrán el mismo número de filas, pero el rango de valores de RollBasis seleccionado en cada ventana será lo mismo. Así que la salida debería ser como:

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

No puedo hacer esto con groupby , porque groupby siempre produce grupos disjuntos. No puedo hacerlo con las funciones de desplazamiento, porque sus ventanas siempre se desplazan por número de filas, no por valores. Entonces, ¿cómo puedo hacerlo?

Creo que esto hace lo que quieres:

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

La función anterior toma un valor, en este caso RollBasis, y luego indexa la columna de dataframe ToRoll en función de ese valor. La serie devuelta consta de valores de ToRoll que cumplen con el criterio de RollBasis + 5. Finalmente, esa serie es sumda y devuelta.

 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 

Código para el ejemplo de juguete DataFrame en caso de que alguien más quiera probar:

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

Basándome en la respuesta de Zelazny7, creé esta solución más general:

 def rollBy(what, basis, window, func): def applyToWindow(val): chunk = what[(val<=basis) & (basis>> 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 

Todavía no es ideal, ya que es muy lento en comparación con rolling_apply , pero quizás esto sea inevitable.

Basado en la respuesta de BrenBarns, pero acelerado mediante el uso de indexación basada en tags en lugar de indexación basada en Boolean:

 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 

Esto es mucho más rápido que no usar una columna indexada:

 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 

Vale la pena señalar que la solución basada en índices es O (n), mientras que la versión de corte lógico es O (n ^ 2) en el caso promedio (creo).

Me parece más útil hacer esto en ventanas espaciadas uniformemente desde el valor mínimo de Base hasta el valor máximo de Base, en lugar de a cada valor de base. Esto significa alterar la función así:

 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