¿Por qué los pandas rodantes usan ndarray de dimensión única?

Estaba motivado a usar rolling función de rolling pandas para realizar una regresión multifactorial (esta pregunta NO es sobre regresión multifactorial). Esperaba poder utilizar apply después de un df.rolling(2) y tomar el pd.DataFrame resultante, extraer el ndarray con .values Y realizar la multiplicación de la matriz requerida. No funcionó de esa manera.

Esto es lo que encontré:

 import pandas as pd import numpy as np np.random.seed([3,1415]) df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B']) X = np.random.rand(2, 1).round(2) 

¿Cómo se ven los objetos?

 print "\ndf = \n", df print "\nX = \n", X print "\ndf.shape =", df.shape, ", X.shape =", X.shape df = AB 0 0.44 0.41 1 0.46 0.47 2 0.46 0.02 3 0.85 0.82 4 0.78 0.76 X = [[ 0.93] [ 0.83]] df.shape = (5, 2) , X.shape = (2L, 1L) 

La multiplicación de matrices se comporta normalmente:

 df.values.dot(X) array([[ 0.7495], [ 0.8179], [ 0.4444], [ 1.4711], [ 1.3562]]) 

El uso de aplicar para realizar el producto punto por fila se comporta como se espera:

 df.apply(lambda x: x.values.dot(X)[0], axis=1) 0 0.7495 1 0.8179 2 0.4444 3 1.4711 4 1.3562 dtype: float64 

Groupby -> Apply se comporta como yo esperaría:

 df.groupby(level=0).apply(lambda x: x.values.dot(X)[0, 0]) 0 0.7495 1 0.8179 2 0.4444 3 1.4711 4 1.3562 dtype: float64 

Pero cuando corro:

 df.rolling(1).apply(lambda x: x.values.dot(X)) 

Yo obtengo:

AttributeError: el objeto ‘numpy.ndarray’ no tiene atributos ‘valores’

Ok, entonces pandas está usando ndarray directo dentro de su implementación rolling . Yo puedo manejar eso. En lugar de usar .values para obtener el ndarray , intentemos:

 df.rolling(1).apply(lambda x: x.dot(X)) 

formas (1,) y (2,1) no alineadas: 1 (dim 0)! = 2 (dim 0)

¡Espere! ¡¿Qué?!

Así que creé una función personalizada para ver lo que está haciendo Rolling.

 def print_type_sum(x): print type(x), x.shape return x.sum() 

Entonces corrió

 print df.rolling(1).apply(print_type_sum)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,)  (1L,) AB 0 0.44 0.41 1 0.46 0.47 2 0.46 0.02 3 0.85 0.82 4 0.78 0.76 

Mi pd.DataFrame resultante es el mismo, eso es bueno. Pero imprimió 10 objetos ndarray unidimensionales. ¿Qué hay de rolling(2)

 print df.rolling(2).apply(print_type_sum)  (2L,)  (2L,)  (2L,)  (2L,)  (2L,)  (2L,)  (2L,)  (2L,) AB 0 NaN NaN 1 0.90 0.88 2 0.92 0.49 3 1.31 0.84 4 1.63 1.58 

Lo mismo, espera salida pero imprime 8 objetos ndarray . rolling produce una ndarray de longitud de una sola dimensión para cada columna en lugar de lo que esperaba, que era una ndarray de forma (window, len(df.columns)) .

La pregunta es ¿por qué?

Ahora no tengo una manera de ejecutar fácilmente una regresión multifactorial.

Usando el strides views concept on dataframe , aquí hay un enfoque vectorizado:

 get_sliding_window(df, 2).dot(X) # window size = 2 

Prueba de tiempo de ejecución –

 In [101]: df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B']) In [102]: X = np.array([2, 3]) In [103]: rolled_df = roll(df, 2) In [104]: %timeit rolled_df.apply(lambda df: pd.Series(df.values.dot(X))) 100 loops, best of 3: 5.51 ms per loop In [105]: %timeit get_sliding_window(df, 2).dot(X) 10000 loops, best of 3: 43.7 µs per loop 

Verificar resultados –

 In [106]: rolled_df.apply(lambda df: pd.Series(df.values.dot(X))) Out[106]: 0 1 1 2.70 4.09 2 4.09 2.52 3 2.52 1.78 4 1.78 3.50 In [107]: get_sliding_window(df, 2).dot(X) Out[107]: array([[ 2.7 , 4.09], [ 4.09, 2.52], [ 2.52, 1.78], [ 1.78, 3.5 ]]) 

¡Gran mejora allí, que espero que se note en arreglos más grandes!

Quería compartir lo que he hecho para solucionar este problema.

Dado un pd.DataFrame y una ventana, genero un ndarray astackdo usando np.dstack ( ver respuesta ). Luego lo convierto a un pd.Panel y usando pd.Panel.to_frame convierto a un pd.DataFrame . En este punto, tengo un pd.DataFrame que tiene un nivel adicional en su índice en relación con el pd.DataFrame original y el nuevo nivel contiene información sobre cada período enrollado. Por ejemplo, si la ventana de desplazamiento es 3, el nuevo nivel de índice contendrá ser [0, 1, 2] . Un artículo para cada período. Ahora puedo groupby level=0 y devolver el objeto groupby. Esto ahora me da un objeto que puedo manipular mucho más intuitivamente.

Función de rollo

 import pandas as pd import numpy as np def roll(df, w): roll_array = np.dstack([df.values[i:i+w, :] for i in range(len(df.index) - w + 1)]).T panel = pd.Panel(roll_array, items=df.index[w-1:], major_axis=df.columns, minor_axis=pd.Index(range(w), name='roll')) return panel.to_frame().unstack().T.groupby(level=0) 

Demostración

 np.random.seed([3,1415]) df = pd.DataFrame(np.random.rand(5, 2).round(2), columns=['A', 'B']) print df AB 0 0.44 0.41 1 0.46 0.47 2 0.46 0.02 3 0.85 0.82 4 0.78 0.76 

sum

 rolled_df = roll(df, 2) print rolled_df.sum() major AB 1 0.90 0.88 2 0.92 0.49 3 1.31 0.84 4 1.63 1.58 

Para echar un vistazo debajo del capó, podemos ver la estructura:

 print rolled_df.apply(lambda x: x) major AB roll 1 0 0.44 0.41 1 0.46 0.47 2 0 0.46 0.47 1 0.46 0.02 3 0 0.46 0.02 1 0.85 0.82 4 0 0.85 0.82 1 0.78 0.76 

Pero ¿qué pasa con el propósito para el que construí esto? Regresión multifactorial. Pero me conformo con la multiplicación de matrices por ahora.

 X = np.array([2, 3]) print rolled_df.apply(lambda df: pd.Series(df.values.dot(X))) 0 1 1 2.11 2.33 2 2.33 0.98 3 0.98 4.16 4 4.16 3.84 

Realicé las siguientes modificaciones a la respuesta anterior, ya que necesitaba devolver la ventana completa como se hace en pd.DataFrame.rolling ()

 def roll(df, w): roll_array = np.dstack([df.values[i:i+w, :] for i in range(len(df.index) - w + 1)]).T roll_array_full_window = np.vstack((np.empty((w-1 ,len(df.columns), w)), roll_array)) panel = pd.Panel(roll_array_full_window, items=df.index, major_axis=df.columns, minor_axis=pd.Index(range(w), name='roll')) return panel.to_frame().unstack().T.groupby(level=0) 

Desde pandas v0.23 ahora es posible pasar una Series lugar de una ndarray a Rolling.apply () . Sólo establece raw=False .

raw : bool, por defecto Ninguno

False : pasa cada fila o columna como una serie a la función.

True o None : la función pasada recibirá objetos ndarray en su lugar. Si solo está aplicando una función de reducción NumPy, esto logrará un rendimiento mucho mejor. El parámetro en bruto es obligatorio y mostrará una advertencia de futuro si no se pasa. En el futuro, el valor predeterminado será Falso.

Nuevo en la versión 0.23.0.

Como se ha señalado; Si solo necesita una dimensión única, pasarla en crudo es obviamente más eficiente. Esta es probablemente la respuesta a tu pregunta; Rolling.apply () se creó inicialmente para pasar un ndarray solo porque es el más eficiente.