Supongamos que tenemos una bar(df) que devuelve una matriz numérica de longitud len(df) dado un marco de datos df .
Ahora considere el modismo
def foo(df): for i in range(N): df['FOO_' + str(i)] = bar(df) return dfUna actualización reciente de pandas comenzó a causar la siguiente advertencia
PerformanceWarning: DataFrame está muy fragmentado. Esto suele ser el resultado de llamar a
frame.insertmuchas veces, lo que tiene un rendimiento deficiente. Considere unir todas las columnas a la vez usando pd.concat(axis=1) en su lugar. Para obtener un marco desfragmentado, usenewframe = frame.copy()
Según tengo entendido, una forma de mitigar esto es cambiar el código anterior por el siguiente idioma
def foo2(df): frames = [df] for i in range(N): frames += [pd.Series(bar(df), index=df.index)] return pd.concat(frames, axis=1)El código anterior corrige la advertencia, pero da como resultado tiempos de ejecución mucho peores.
In [110]: %timeit foo() 1.73 s ± 11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [111]: %timeit foo2() 2.51 s ± 25.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)Una solución para suprimir una advertencia de rendimiento que introduce una sobrecarga adicional parece una tontería. Por lo tanto mi pregunta es
¿Cómo puedo corregir la advertencia y al mismo tiempo obtener un mejor rendimiento? En otras palabras, ¿hay alguna forma de mejorar la función foo2 para ofrecer un mejor rendimiento que foo?
Una oportunidad para optimizar el código es reformular += como una lista de comprensión:
import pandas as pd N = 5000 df = pd.DataFrame(index=[_ for _ in range(100)]) def bar(df): return np.random.rand(len(df)) def foo2_orig(df): frames = [df] for i in range(N): frames += [pd.Series(bar(df), index=df.index)] return pd.concat(frames, axis=1) def foo2_opt(df): frames = pd.concat([pd.Series(bar(df), index=df.index) for i in range(N)], axis=1) return pd.concat([df, frames], axis=1)En mi máquina veo un factor de mejora de rendimiento de 2, aunque no estoy seguro de si las 100 filas y las 5000 columnas son realistas para su situación. El motivo de la aceleración es que las listas de comprensión son más eficientes .
Actualizar:
si se conoce la lista de nombres de columna ( list_col_names ), entonces se pueden asignar nombres de columna personalizados a las series individuales usando el name kwarg:
def foo2_opt(df): frames = pd.concat([pd.Series(bar(df), index=df.index, name=col_name) for i, col_name in zip(range(N), list_col_names)], axis=1) return pd.concat([df, frames], axis=1)Una forma muy eficiente y elegante de construir un marco de datos en columnas a partir de matrices numpy es construir un diccionario de las columnas y luego convertirlas de inmediato al marco de datos deseado, ya que esto significa que el costo de consolidar los datos y administrar la indexación ha disminuido. a pagar una sola vez.
Si extiendo el ejemplo de @SultanOrazbayev con una variante foo2_dict :
def foo2_dict(df): new_columns = pd.DataFrame({f"FOO_{i}": bar(df) for i in range(N)}) return pd.concat([df, new_columns], axis=1) Veo una mejora adicional del factor 6 de foo2_opt (390 ms) a foo2_dict (58 ms), pero esto, por supuesto, depende en gran medida de la implementación real de la función de bar subyacente.
Tenga en cuenta también que la mejora de la velocidad mediante el uso de la comprensión del diccionario es de orden secundario al montón numpy -> conversión de pandas, es decir. un foo2_incremental_dict me toma 59 ms.
def foo2_incremental_dict(df): new_columns = {} for i in range(N): new_columns[f"FOO_{i}"] = bar(df) new_columns = pd.DataFrame(new_columns, index=df.index) return pd.concat([df, new_columns], axis=1)