Creación de grandes Pandas DataFrames: preallocation vs append vs concat

Estoy confundido por el rendimiento en Pandas cuando se construye una gran porción de dataframe por porción. En Numpy, (casi) siempre vemos un mejor rendimiento preasignando una gran matriz vacía y luego rellenando los valores. Como lo entiendo, esto se debe a que Numpy toma toda la memoria que necesita a la vez, en lugar de tener que reasignar la memoria con cada operación de append .

En Pandas, parece que estoy obteniendo un mejor rendimiento al usar el patrón df = df.append(temp) .

Aquí hay un ejemplo con el tiempo. La definición de la clase Timer sigue. Como usted, vea, ¡encuentro que la preasignación es aproximadamente 10 veces más lenta que usarla! La asignación previa de un dataframe con los valores np.empty del tipo de letra apropiado ayuda mucho, pero el método de anexión sigue siendo el más rápido.

 import numpy as np from numpy.random import rand import pandas as pd from timer import Timer # Some constants num_dfs = 10 # Number of random dataframes to generate n_rows = 2500 n_cols = 40 n_reps = 100 # Number of repetitions for timing # Generate a list of num_dfs dataframes of random values df_list = [pd.DataFrame(rand(n_rows*n_cols).reshape((n_rows, n_cols)), columns=np.arange(n_cols)) for i in np.arange(num_dfs)] ## # Define two methods of growing a large dataframe ## # Method 1 - append dataframes def method1(): out_df1 = pd.DataFrame(columns=np.arange(4)) for df in df_list: out_df1 = out_df1.append(df, ignore_index=True) return out_df1 def method2(): # # Create an empty dataframe that is big enough to hold all the dataframes in df_list out_df2 = pd.DataFrame(columns=np.arange(n_cols), index=np.arange(num_dfs*n_rows)) #EDIT_1: Set the dtypes of each column for ix, col in enumerate(out_df2.columns): out_df2[col] = out_df2[col].astype(df_list[0].dtypes[ix]) # Fill in the values for ix, df in enumerate(df_list): out_df2.iloc[ix*n_rows:(ix+1)*n_rows, :] = df.values return out_df2 # EDIT_2: # Method 3 - preallocate dataframe with np.empty data of appropriate type def method3(): # Create fake data array data = np.transpose(np.array([np.empty(n_rows*num_dfs, dtype=dt) for dt in df_list[0].dtypes])) # Create placeholder dataframe out_df3 = pd.DataFrame(data) # Fill in the real values for ix, df in enumerate(df_list): out_df3.iloc[ix*n_rows:(ix+1)*n_rows, :] = df.values return out_df3 ## # Time both methods ## # Time Method 1 times_1 = np.empty(n_reps) for i in np.arange(n_reps): with Timer() as t: df1 = method1() times_1[i] = t.secs print 'Total time for %d repetitions of Method 1: %f [sec]' % (n_reps, np.sum(times_1)) print 'Best time: %f' % (np.min(times_1)) print 'Mean time: %f' % (np.mean(times_1)) #>> Total time for 100 repetitions of Method 1: 2.928296 [sec] #>> Best time: 0.028532 #>> Mean time: 0.029283 # Time Method 2 times_2 = np.empty(n_reps) for i in np.arange(n_reps): with Timer() as t: df2 = method2() times_2[i] = t.secs print 'Total time for %d repetitions of Method 2: %f [sec]' % (n_reps, np.sum(times_2)) print 'Best time: %f' % (np.min(times_2)) print 'Mean time: %f' % (np.mean(times_2)) #>> Total time for 100 repetitions of Method 2: 32.143247 [sec] #>> Best time: 0.315075 #>> Mean time: 0.321432 # Time Method 3 times_3 = np.empty(n_reps) for i in np.arange(n_reps): with Timer() as t: df3 = method3() times_3[i] = t.secs print 'Total time for %d repetitions of Method 3: %f [sec]' % (n_reps, np.sum(times_3)) print 'Best time: %f' % (np.min(times_3)) print 'Mean time: %f' % (np.mean(times_3)) #>> Total time for 100 repetitions of Method 3: 6.577038 [sec] #>> Best time: 0.063437 #>> Mean time: 0.065770 

Yo uso un buen Timer cortesía de Huy Nguyen:

 # credit: http://www.huyng.com/posts/python-performance-analysis/ import time class Timer(object): def __init__(self, verbose=False): self.verbose = verbose def __enter__(self): self.start = time.clock() return self def __exit__(self, *args): self.end = time.clock() self.secs = self.end - self.start self.msecs = self.secs * 1000 # millisecs if self.verbose: print 'elapsed time: %f ms' % self.msecs 

Si todavía estás siguiendo, tengo dos preguntas:

1) ¿Por qué es más rápido el método de append ? (NOTA: para marcos de datos muy pequeños, es decir, n_rows = 40 , en realidad es más lento).

2) ¿Cuál es la forma más eficiente de construir un gran dataframe a partir de trozos? (En mi caso, todos los fragmentos son archivos csv grandes).

¡Gracias por tu ayuda!

EDIT_1: En mi proyecto del mundo real, las columnas tienen diferentes tipos de tipos. Por lo tanto, no puedo usar el pd.DataFrame(.... dtype=some_type) para mejorar el rendimiento de la preasignación, según la recomendación de BrenBarn. El parámetro dtype obliga a todas las columnas a ser el mismo dtype [Ref. problema 4464]

Agregué algunas líneas a method2() en mi código para cambiar los dtypes columna por columna para que coincidan en los marcos de datos de entrada. Esta operación es costosa y niega los beneficios de tener los dtypes apropiados al escribir bloques de filas.

EDIT_2: Intente preasignar una ttwig de datos utilizando la matriz de marcador de posición np.empty(... dtyp=some_type) . Según la sugerencia de @ Joris.

Su punto de referencia es en realidad demasiado pequeño para mostrar la diferencia real. Anexando, copia CADA vez, por lo que en realidad está copiando un espacio de memoria de tamaño N N * (N-1) veces. Esto es terriblemente ineficiente a medida que crece el tamaño de su dataframe. Esto ciertamente no puede importar en un marco muy pequeño. Pero si tienes cualquier tamaño real, esto importa mucho. Esto se menciona específicamente en los documentos aquí , aunque es una pequeña advertencia.

 In [97]: df = DataFrame(np.random.randn(100000,20)) In [98]: df['B'] = 'foo' In [99]: df['C'] = pd.Timestamp('20130101') In [103]: df.info()  Int64Index: 100000 entries, 0 to 99999 Data columns (total 22 columns): 0 100000 non-null float64 1 100000 non-null float64 2 100000 non-null float64 3 100000 non-null float64 4 100000 non-null float64 5 100000 non-null float64 6 100000 non-null float64 7 100000 non-null float64 8 100000 non-null float64 9 100000 non-null float64 10 100000 non-null float64 11 100000 non-null float64 12 100000 non-null float64 13 100000 non-null float64 14 100000 non-null float64 15 100000 non-null float64 16 100000 non-null float64 17 100000 non-null float64 18 100000 non-null float64 19 100000 non-null float64 B 100000 non-null object C 100000 non-null datetime64[ns] dtypes: datetime64[ns](1), float64(20), object(1) memory usage: 17.5+ MB 

Anexando

 In [85]: def f1(): ....: result = df ....: for i in range(9): ....: result = result.append(df) ....: return result ....: 

Concat

 In [86]: def f2(): ....: result = [] ....: for i in range(10): ....: result.append(df) ....: return pd.concat(result) ....: In [100]: f1().equals(f2()) Out[100]: True In [101]: %timeit f1() 1 loops, best of 3: 1.66 s per loop In [102]: %timeit f2() 1 loops, best of 3: 220 ms per loop 

Tenga en cuenta que ni siquiera me molestaría en tratar de pre-asignar. Es algo complicado, especialmente porque se trata de varios tipos de datos (por ejemplo, podría hacer un marco gigante y simplemente .loc y funcionaría). Pero pd.concat es simplemente simple, funciona de manera confiable y rápida.

Y el tiempo de sus tamaños desde arriba.

 In [104]: df = DataFrame(np.random.randn(2500,40)) In [105]: %timeit f1() 10 loops, best of 3: 33.1 ms per loop In [106]: %timeit f2() 100 loops, best of 3: 4.23 ms per loop 

No especificó ningún tipo de datos para out_df2 , por lo que tiene el tipo de “objeto”. Esto hace que asignarle valores sea muy lento. Especifique el tipo de dato float64:

 out_df2 = pd.DataFrame(columns=np.arange(n_cols), index=np.arange(num_dfs*n_rows), dtype=np.float64) 

Verás una dramática aceleración. Cuando lo probé, el method2 con este cambio es aproximadamente el doble de rápido que el method1 .

@Jeff, pd.concat gana por una milla! Hice un cuarto método de referencia usando pd.concat con num_dfs = 500 . Los resultados son inequívocos:

La definición de method4() :

 # Method 4 - us pd.concat on df_list def method4(): return pd.concat(df_list, ignore_index=True) 

Resultados de perfiles, usando el mismo Timer en mi pregunta original:

 Total time for 100 repetitions of Method 1: 3679.334655 [sec] Best time: 35.570036 Mean time: 36.793347 Total time for 100 repetitions of Method 2: 1569.917425 [sec] Best time: 15.457102 Mean time: 15.699174 Total time for 100 repetitions of Method 3: 325.730455 [sec] Best time: 3.192702 Mean time: 3.257305 Total time for 100 repetitions of Method 4: 25.448473 [sec] Best time: 0.244309 Mean time: 0.254485 

El método pd.concat es 13 veces más rápido que el preasignado con un np.empty(... dtype) .

La respuesta de Jeff es correcta, pero encontré para mi tipo de datos otra solución que funcionó mejor.

 def df_(): return pd.DataFrame(['foo']*np.random.randint(100)).transpose() k = 100 frames = [df_() for x in range(0, k)] def f1(): result = frames[0] for i in range(k-1): result = result.append(frames[i+1]) return result def f2(): result = [] for i in range(k): result.append(frames[i]) return pd.concat(result) def f3(): result = [] for i in range(k): result.append(frames[i]) n = 2 while len(result) > 1: _result = [] for i in range(0, len(result), n): _result.append(pd.concat(result[i:i+n])) result = _result return result[0] 

Mis marcos de datos son una sola fila y de longitud variable; las entradas nulas deben tener algo que ver con la razón por la que f3 () tiene éxito.

 In [33]: f1().equals(f2()) Out[33]: True In [34]: f1().equals(f3()) Out[34]: True In [35]: %timeit f1() 357 ms ± 192 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [36]: %timeit f2() 562 ms ± 68.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [37]: %timeit f3() 215 ms ± 58.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 

Los resultados anteriores siguen siendo para k = 100, pero para k más grande es aún más significativo.