¿Cómo funcionan los pandas Rolling objects?


Edit: Condensé esta pregunta dado que probablemente estaba demasiado involucrada para empezar. La esencia de la pregunta está en negrita a continuación.

Me gustaría saber más sobre el objeto que es creado cuando se utiliza DataFrame.rolling o Series.rolling:

print(type(df.rolling))
<class 'pandas.core.window.Rolling'>

Algunos antecedentes: considere la alternativa frecuentemente utilizada con np.as_strided. Este fragmento de código en sí no es importante, pero su resultado es mi punto de referencia para hacer esta pregunta.

def rwindows(a, window):
    if a.ndim == 1:
        a = a.reshape(-1, 1)
    shape = a.shape[0] - window + 1, window, a.shape[-1]
    strides = (a.strides[0],) + a.strides
    windows = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
    return np.squeeze(windows)

Aquí rwindows tomará un 1d o 2d ndarray y construirá "bloques" rodantes iguales al tamaño de ventana especificado (como se muestra a continuación). ¿Cómo se compara un objeto .rolling con la salida ndarray de abajo? Es un iterador, con ciertos atributos almacenados para cada bloque? O algo completamente distinto? He intentado jugar con la terminación de pestañas en el objeto con atributos / métodos como __dict__ y _get_index() y no me dicen mucho. También he visto un _create_blocks método en pandas does ¿se parece en absoluto el strided método?

# as_strided version

a = np.arange(5)
print(rwindows(a, 3))           # 1d input
[[0 1 2]
 [1 2 3]
 [2 3 4]]

b = np.arange(10).reshape(5,2)
print(rwindows(b, 4))           # 2d input
[[[0 1]
  [2 3]
  [4 5]
  [6 7]]

 [[2 3]
  [4 5]
  [6 7]
  [8 9]]]

Parte 2, crédito extra

Usar el enfoque NumPy anterior (implementación de OLS aquí) es necesario por el hecho de que funcdentro de pandas.núcleo.ventana.Rodante.apply must

Produce un solo valor a partir de una entrada ndarray * args y * * kwargs son pasado a la función

Así que el argumento no puede ser otro objeto rodante. Es decir,

def prod(a, b):
    return a * b
df.rolling(3).apply(prod, args=((df + 2).rolling(3),))
-----------------------------------------------------------------------
...
TypeError: unsupported operand type(s) for *: 'float' and 'Rolling'

Así que esto es realmente de donde mi pregunta anterior tallo. ¿Por qué es que la función pasada debe usar una matriz NumPy y producir un solo valor escalar, y qué tiene esto que ver con el diseño de un objeto .rolling?

Author: denfromufa, 2017-07-22

1 answers

Le sugiero que eche un vistazo al código fuente para entrar en lo esencial de lo que hace rolling. En particular, le sugiero que eche un vistazo a las funciones rolling en generic.py y window.py. Desde allí se puede echar un vistazo a la Window class que se utiliza si se especifica un tipo de ventana o el valor predeterminado Rolling clase . El último hereda de _Rolling_and_Expanding y, finalmente, _Rolling y _Window.

Dicho esto, daré mis dos centavos: Todo el mecanismo de rodadura de los Pandas se basa en la función numpyapply_along_axis. En particular se usa aquí en pandas. Se utiliza junto con el windows.pyx módulo cython. En va su serie, sale la ventana de balanceo agregado. Para las funciones de agregación típicas, las maneja de manera eficiente, pero para las personalizadas (usando apply()) utiliza un roll_generic() en windows.pyx.

La función rolling en pandas opera en columnas de marco de datos pandas independiente. No es un iterador de python , y se carga perezosamente, lo que significa que no se calcula nada hasta que se le aplica una función de agregación. Las funciones que realmente aplican la ventana móvil de datos no se utilizan hasta justo antes de que se realice una agregación.

Una fuente de confusión podría ser que esté pensando en el objeto rolling como un dataframe. (Ha nombrado el objeto rodante df en su último fragmento de código). Realmente no lo es. Es un objeto que puede producir dataframes aplicando agregaciones sobre la lógica de ventana que alberga.

La lambda que está suministrando se aplica a cada celda de su nuevo dataframe. Toma una ventana hacia atrás (a lo largo de cada columna) en su dataframe antiguo y la agrega a una sola celda en el nuevo dataframe. La agregación puede ser cosas como sum, mean, algo personalizado que has hecho, etc., sobre un tamaño de ventana, digamos 3. He aquí algunos ejemplos:

a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])
df.rolling(3).mean().dropna()

... que también se puede hacer by:

df.rolling(3).apply(np.mean).dropna()

... y produce:

     a
2  3.0
3  6.0
4  9.0

(La primera columna es el valor del índice y se puede ignorar aquí, y para los siguientes ejemplos.)

Observe cómo suministramos una función de agregación numpy existente. Esa es la idea. Se supone que podemos suministrar cualquier cosa que queramos siempre y cuando se ajuste a lo que hacen las funciones de agregación, es decir, tomar un vector de valores y producir un solo valor a partir de él. Aquí hay otro donde creamos una función de agregación personalizada, en este case la norma L2 de la ventana:

df.rolling(3).apply(lambda x: np.sqrt(x.dot(x))).dropna()

Si no estás familiarizado con las funciones lambda esto es lo mismo que:{[58]]}

def euclidean_dist(x):
    return np.sqrt(x.dot(x))

df.rolling(3).apply(euclidean_dist).dropna()

... rendimiento:

          a
2  2.236068
3  3.741657
4  5.385165

Solo para asegurarnos, podemos comprobar manualmente que np.sqrt(0**2 + 1**2 + 2**2) es efectivamente 2.236068.

[En su edición original, en el] último fragmento de código, es probable que su código esté fallando antes de lo esperado. Está fallando antes de la invocación de df.apply(...) Está tratando de agregar un objeto rodante llamado df al número 2 antes de que se pase a df.apply(...). El objeto rodante no es algo en lo que se realizan operaciones. La función de agregación que ha suministrado tampoco se ajusta a una función de agregación en general. a es una lista con los valores de una ventana, b sería un parámetro adicional constante que se pasa. Puede ser un objeto rodante si lo desea, pero normalmente no sería algo que le gustaría hacer. Para que quede más claro, aquí hay algo que es similar a lo que estaba haciendo en su edición original, pero obras:

a = np.arange(8)
df = pd.DataFrame(a, columns=['a'])
n = 4
rol = df.rolling(n)

def prod(window_list, constant_rol):
    return window_list.dot(constant_rol.sum().dropna().head(n))

rol.apply(prod, args=(rol,)).dropna()

# [92.0, 140.0, 188.0, 236.0, 284.0]

Es un ejemplo artificial, pero lo estoy mostrando para demostrar que puedes pasar lo que quieras como una constante, incluso el objeto rodante que estás usando. La parte dinámica es el primer argumento a en su caso o window_list en mi caso. Todas las ventanas definidas, en forma de listas individuales, se pasan a esa función una por una.

Basado en sus comentarios de seguimiento, esto podría ser lo que está buscando:

import numpy as np
import pandas as pd

n = 3
a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])

def keep(window, windows):
    windows.append(window.copy())
    return window[-1]

windows = list()
df['a'].rolling(n).apply(keep, args=(windows,))
df = df.tail(n)
df['a_window'] = windows

Que añade matrices / vectores a cada bloque rodante produciendo así:

   a         a_window
2  2  [0.0, 1.0, 2.0]
3  3  [1.0, 2.0, 3.0]
4  4  [2.0, 3.0, 4.0]

Tenga en cuenta que solo funciona si lo hace en una columna a la vez. Si quieres hacer algunos cálculos en la ventana antes de guardarla en keep, también está bien.

Dicho esto, sin más información sobre exactamente lo que está tratando de lograr, es difícil construir un ejemplo que se adapte a sus necesidades.

Si tu objetivo final es crear un dataframe de variables rezagadas, entonces usaría columnas reales usando shift():

import numpy as np
import pandas as pd

a = np.arange(5)

df = pd.DataFrame(a, columns=['a'])
for i in range(1,3):
    df['a-%s' % i] = df['a'].shift(i)

df.dropna()

... dando:

   a  a-1  a-2
2  2  1.0  0.0
3  3  2.0  1.0
4  4  3.0  2.0

(Puede haber alguna forma más hermosa de hacerlo, pero hace el trabajo.)

Con respecto a su variable b en su primer fragmento de código, recuerde que los DataFrames en pandas no se manejan típicamente como tensores de dimensiones/objetos arbitrarios. Probablemente puedes meter lo que quieras en él, pero en última instancia, cadenas, objetos de tiempo, ints y flotadores es lo que se espera. Esa podría ser la razón por la que los diseñadores de pandas no lo han hecho molestado con permitir la agregación rodante a valores no escalares. Ni siquiera parece que se permita una cadena simple como salida de la función de agregación.

De todos modos, espero que esto responda a algunas de sus preguntas. Si no házmelo saber, y voy a tratar de ayudarte en los comentarios, o una actualización.


Nota final sobre la función _create_blocks() de los objetos rolling.

La función _create_blocks() maneja el reindexing y binning cuando se utiliza el argumento freq de rolling.

Si usas frecuencia con, digamos, semanas tales que freq=W:

import pandas as pd

a = np.arange(50)
df = pd.DataFrame(a, columns=['a'])
df.index = pd.to_datetime('2016-01-01') + pd.to_timedelta(df['a'], 'D')
blocks, obj, index = df.rolling(4, freq='W')._create_blocks(how=None)
for b in blocks:
    print(b)

... luego obtenemos los datos originales binned (no rolling) semana a semana:

               a
a               
2016-01-03   2.0
2016-01-10   9.0
2016-01-17  16.0
2016-01-24  23.0
2016-01-31  30.0
2016-02-07  37.0
2016-02-14  44.0
2016-02-21   NaN

Tenga en cuenta que esta no es la salida del balanceo agregado. Esto es simplemente los nuevos bloques en los que trabaja. Después de esto. Hacemos una agregación como sum y obtenemos:

                a
a                
2016-01-03    NaN
2016-01-10    NaN
2016-01-17    NaN
2016-01-24   50.0
2016-01-31   78.0
2016-02-07  106.0
2016-02-14  134.0
2016-02-21    NaN

... que se comprueba con una suma de prueba: 50 = 2 + 9 + 16 + 23.

Si no utiliza freq como argumento, simplemente devuelve estructura de datos original:

import pandas as pd
a = np.arange(5)
df = pd.DataFrame(a, columns=['a'])
blocks, obj, index = df.rolling(3)._create_blocks(how=None)

for b in blocks:
    print(b)

... que produce ...

            a
a            
2016-01-01  0
2016-01-02  1
2016-01-03  2
2016-01-04  3
2016-01-05  4

... y se utiliza para la agregación de ventanas rodantes.

 24
Author: André C. Andersen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-24 21:00:24