Pandas: ¿Por qué debería ser más lento agregar un marco de datos de flotantes e ints que si estuviera lleno de NaN?

7

Estoy tomando datos de un archivo que recibe datos de barras OHLCVT de 5 segundos de Interactive Brokers a través de Sierra Chart.

Siguiendo los consejos de publicaciones anteriores, en lugar de agregar cada nueva fila al marco de datos, construyo un marco de datos con el archivo histórico y le agrego 5000 registros "en blanco" con las marcas de tiempo correctas. Luego escribo cada nueva fila sobre una fila en blanco, llenando cualquier fila si faltan marcas de tiempo y actualizando punteros.

Esto funciona bien. Aquí están las clases y funciones actuales . Mi versión inicial creó 5000 líneas de NaN (OHLCVxyz). Pensé que sería más ordenado comenzar con los tipos de datos finales, así que convertí los registros "en blanco" a ceros con OHLC flotando y Vxyz siendo ints usando:

dg.iloc[0:5000] = 0.0
dg[[v, x, y, z]] = dg[[v, x, y, z]].astype('int')

Esto solo ocurre una vez por cada 5000 líneas adicionales (una vez al día para HSI). Lo que me sorprendió fue el impacto en los bucles de lectura / escritura. Pasaron de 0,8 ms a 3,4 ms por fila. El único cambio fue de NaN a ceros.

Esta imagen muestra una ejecución inicial con un cuadro con relleno de cero (consulte las marcas de tiempo 0,0038) y luego una ejecución con un cuadro con relleno de NaN (marcas de tiempo 0,0008).

¿Alguien puede proporcionar información sobre por qué podría agregar tanto tiempo escribir en campos de [0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0] en lugar de [NaN, NaN, NaN, NaN, NaN, NaN, NaN? , NaN]?

Cualquier comentario sobre mejoras en el código también es bienvenido. :)

Gracias

EDITAR +17 horas

Siguiendo las preguntas de @BrenBarn, construí un modelo más simple que cualquier persona sin datos podría ejecutar. Al hacerlo, eliminé la pregunta de si los NaN lo impactan. En esta versión pude escribir 0.0s en ambas versiones y la diferencia fue la misma:

  • la matriz que tiene 8 columnas de flotantes se agrega 10 veces más rápido que la matriz que tiene 4 columnas de flotantes y 4 de int64s.
  • en cada caso, la fila que se agregó fue [1.0, 2.0, 3.0, 4.0, 5, 6, 7, 8]
  • la adición se realiza 10000 veces con self.df.iloc [self.end] = datarow e increment end.

Entonces, a menos que me equivoque (siempre es posible), parece que agregar a un marco de datos con 4 columnas de flotantes y 4 de ints toma 10 veces más tiempo. ¿Es esto un problema para los pandas o simplemente lo que uno debería esperar?

Aquí está el código de prueba y aquí está la imagen de salida

Creo que tener una matriz de 350,000 filas de 8 columnas antes de agregarle hace una diferencia significativa. Mis pruebas iniciales que se agregaron a 10 filas no mostraron ningún impacto; debo volver atrás y volver a probarlas.

EDITAR +10 minutos

No, volví y creé la matriz inicial con solo 10 filas y el impacto en los bucles de adición no cambió, por lo que no es el tamaño de la matriz / marco de datos original. Es probable que en mi prueba anterior pensé que había convertido las columnas en ints, pero no lo había hecho; verificar esto demostró que el comando que pensé que haría esto no lo hizo.

da = SierraFrame(range(10), np.zeros((10,8)))
da.extend_frame1()

EDITAR y posible respuesta +35 minutos

¿No debería responderse esta pregunta con más detalle?

En este punto, mi hipótesis es que la funcionalidad subyacente para agregar [1.0, 2.0, 3.0, 4.0, 5, 6, 7, 8] a una línea libre en el marco de datos es diferente si el df comprende todo un tipo que si comprende columnas de flotadores e ints. Lo probé con todos los int64s y el agregado promedio fue 0.41ms vs 0.37ms para todos los flotantes y 2.8ms para un marco de datos mixto. Int8s tomó 0.39ms. Supongo que la mezcla afecta la capacidad de los pandas para optimizar su acción, por lo que si la eficiencia es muy importante, la mejor opción es un marco de datos con todas las columnas del mismo tipo (probablemente float64).

Pruebas realizadas en Linux x64 con Python 3.3.1

7
  • ¿Cuál es el tipo de datos que está escribiendo en esas celdas? Además, ¿qué estás cronometrando exactamente? ¿Es posible que la configuración de los ceros en sí sea parte de lo que se está cronometrando? 17 de junio de 2013 a las 7:07
  • La configuración de conversión de ceros / tipo tiene lugar cuando se lee el archivo existente y se crea el marco. Después de eso, un ciclo inicia el temporizador (cada iteración) prueba para obtener nuevos datos y, si hay datos, los convierte en una lista llamada datarow con cuatro flotantes y cuatro enteros que se escribe sobre la fila actual del marco de datos usando self.df.iloc [ self.end] = fila de datos. El cambio en el marco de datos también se escribe en un archivo. Solo si había datos, se agrega la diferencia de tiempo actual a time_list para crear estadísticas. 17 de junio de 2013 a las 7:40
  • Para eliminar la posibilidad de que la escritura del archivo después de la lectura y la conversión lo hayan afectado, lo comenté. Esto reduce ambos resultados en ~ 0,4 ms. 17 de junio de 2013 a las 7:57
  • ¿Puedes exactamente lo que estás haciendo? Ambos casos (con 0 o con nan) tiempo al mismo tiempo
    Jeff
    17 de junio de 2013 a las 12:04
  • Creé una versión de prueba que genera un df a partir de una matriz numpy, luego agrega 10,000 filas para llenar con datos entrantes (ficticios). Esto se describe en la Edición y puede ejecutarlo usted mismo para ver si el caso 1 (escribiendo en df de float 0.0s) difiere del caso 2 (escribiendo en df con la mitad de las columnas float 0 y la mitad de ellas int64 0). Aprecio mucho tus preguntas ... me han impulsado a simplificar la prueba tanto como sea posible. 17/06/2013 a las 22:59
5

Como se describe en esta publicación de blog del autor principal de pandas , un DataFrame de pandas se compone internamente de "bloques". Un bloque es un grupo de columnas que tienen el mismo tipo de datos. Cada bloque se almacena como una matriz numerosa de su tipo de bloque. Entonces, si tiene cinco columnas int y luego cinco columnas flotantes, habrá un bloque int y un bloque flotante.

Agregar a una matriz de varios tipos requiere agregar a cada una de las matrices numéricas subyacentes. Agregar matrices numpy es lento, porque requiere crear una matriz numpy completamente nueva. Por lo tanto, tiene sentido que agregar a un DataFrame de varios tipos sea lento: si todas las columnas son de un tipo, solo tiene que crear una nueva matriz numpy, pero si son de diferentes tipos, tiene que crear varias matrices numpy nuevas.

Es cierto que mantener todos los datos del mismo tipo acelerará este proceso. Sin embargo, diría que la conclusión principal no es "si la eficiencia es importante, mantenga todas sus columnas del mismo tipo". La conclusión es que si la eficiencia es importante, no intente agregar a sus arreglos / DataFrames .

Así es como funciona Numpy. La parte más lenta de trabajar con matrices numpy es crearlas en primer lugar. Tienen un tamaño fijo, y cuando "agregas" a uno, realmente estás creando uno completamente nuevo con el nuevo tamaño, lo cual es lento. Si es absolutamente necesario agregarlos, puede probar cosas como jugar con tipos para aliviar un poco el dolor. Pero en última instancia, solo tiene que aceptar que cada vez que intente agregar un DataFrame (o una matriz numerosa en general), es probable que sufra un impacto sustancial en el rendimiento.

5
  • Gracias por eso BrenBarn. Puedo ver que @Jeff tiene razón y mi pregunta original se ha vuelto demasiado larga. De hecho, solo agrego cada 5000 filas y el ciclo que se está cronometrando en realidad está escribiendo datos en una fila agregada cuando se leyó el archivo principal para crear el marco de datos. 18/06/2013 a las 21:35
  • La adición se realiza con self.df.iloc [self.end] = datarow donde datarow es siempre una lista de 4 flotantes y 4 ints. Sin embargo, su respuesta explica por qué escribirlo sobre la fila existente tomaría un orden de magnitud más largo; parece probable que los pandas tengan que usar un código adecuado para escribir la lista en dos matrices diferentes en lugar de una matriz (todos los flotantes), tal vez el código no está tan optimizado. Voy a intentar una cosa más: agregar 4 flotadores a la primera mitad y 4 pulgadas a la segunda mitad como dos escrituras separadas para ver cuál es el tiempo. 18/06/2013 a las 21:38
  • @ John9631: Sin embargo, se aplica la misma lógica. Si escribe datos en un DataFrame con varios tipos, en realidad tiene que hacer dos escrituras en dos matrices numpy separadas. No esperaría que esta desaceleración sea tanto como el anexo, pero aún implica dos operaciones numéricas separadas en lugar de una. 18/06/2013 a las 21:39
  • Intenté dividir la escritura para que escribiera una lista de 4 a los 4 flotadores en blanco y luego de 4 a las 4 entradas en blanco usando el código a continuación, pero el resultado fue idéntico. Entonces parece que debe ser que esta operación en la matriz de tipos múltiples es mucho más lenta. Gracias por la información. ... ... ... self.df.iloc [self.end, 0: 4] = [1.0, 2.0, 3.0, 4.0] ... self.df.iloc [self.end, 4: 8] = [11, 12, 13, 14] 18 de junio de 2013 a las 22:08
  • Ups ... veo que respondiste. He hecho el experimento y he demostrado que tienes razón :) ... Donde la velocidad es importante, usaré marcos de datos de un solo tipo para permitir que las optimizaciones de Wes (y del equipo) funcionen de la mejor manera. 18/06/2013 a las 22:16