¿Pandas iterrows tiene problemas de rendimiento?

96

He notado un rendimiento muy bajo al usar iterrows de pandas.

¿Es esto algo que otros experimentan? ¿Es específico de iterrows y debería evitarse esta función para datos de cierto tamaño (estoy trabajando con 2-3 millones de filas)?

Esta discusión en GitHub me llevó a creer que se debe a la mezcla de dtypes en el marco de datos, sin embargo, el ejemplo simple a continuación muestra que está ahí incluso cuando se usa un dtype (float64). Esto toma 36 segundos en mi máquina:

import pandas as pd
import numpy as np
import time

s1 = np.random.randn(2000000)
s2 = np.random.randn(2000000)
dfa = pd.DataFrame({'s1': s1, 's2': s2})

start = time.time()
i=0
for rowindex, row in dfa.iterrows():
    i+=1
end = time.time()
print end - start

¿Por qué las operaciones vectorizadas como se aplican mucho más rápido? Me imagino que también debe haber alguna iteración fila por fila.

No puedo averiguar cómo no usar iterrows en mi caso (esto lo guardaré para una pregunta futura). Por lo tanto, agradecería que me escuchara si ha podido evitar esta iteración de manera constante. Estoy haciendo cálculos basados ​​en datos en marcos de datos separados. ¡Gracias!

--- Editar: a continuación se agregó una versión simplificada de lo que quiero ejecutar ---

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b'],
      'number1':[50,-10]}

t2 = {'letter':['a','a','b','b'],
      'number2':[0.2,0.5,0.1,0.4]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=[0])

#%% Iterate through filtering relevant data, optimizing, returning info
for row_index, row in table1.iterrows():   
    t2info = table2[table2.letter == row['letter']].reset_index()
    table3.ix[row_index,] = optimize(t2info,row['number1'])

#%% Define optimization
def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2']*t1info)
    maxrow = calculation.index(max(calculation))
    return t2info.ix[maxrow]
KieranPC
fuente
7
applyNO está vectorizado. iterrowses incluso peor, ya que encierra todo (con lo que 'el diferencial de rendimiento' apply). Solo debe usarlo iterrowsen muy pocas situaciones. En mi humilde opinión, nunca. Muestre lo que está haciendo realmente iterrows.
Jeff
2
En cambio, el problema al que se vinculó tiene que ver con el encajonamiento de un DatetimeIndexen Timestamps(se implementó en el espacio de Python), y esto se ha mejorado mucho en master.
Jeff
1
Consulte este número para una discusión más completa: github.com/pydata/pandas/issues/7194 .
Jeff
Enlace a la pregunta específica (esta seguirá siendo general): stackoverflow.com/questions/24875096/…
KieranPC
No recomiende el uso de iterrows (). Es un habilitador flagrante del peor anti-patrón en la historia de los pandas.
cs95

Respuestas:

188

Generalmente, iterrowssolo debe usarse en casos muy, muy específicos. Este es el orden general de precedencia para la realización de varias operaciones:

1) vectorization
2) using a custom cython routine
3) apply
    a) reductions that can be performed in cython
    b) iteration in python space
4) itertuples
5) iterrows
6) updating an empty frame (e.g. using loc one-row-at-a-time)

El uso de una rutina Cython personalizada suele ser demasiado complicado, así que omitamos eso por ahora.

1) La vectorización es SIEMPRE, SIEMPRE la primera y mejor opción. Sin embargo, hay un pequeño conjunto de casos (que generalmente involucran una recurrencia) que no pueden vectorizarse de manera obvia. Además, en términos más pequeños DataFrame, puede ser más rápido utilizar otros métodos.

3) apply generalmente puede ser manejado por un iterador en el espacio Cython. Esto lo manejan los pandas internamente, aunque depende de lo que esté sucediendo dentro de la applyexpresión. Por ejemplo, df.apply(lambda x: np.sum(x))se ejecutará con bastante rapidez, aunque, por supuesto, df.sum(1)es incluso mejor. Sin embargo, algo como df.apply(lambda x: x['b'] + 1)se ejecutará en el espacio de Python y, en consecuencia, es mucho más lento.

4) itertuplesno incluye los datos en un archivo Series. Simplemente devuelve los datos en forma de tuplas.

5) iterrowsENCUENTRA los datos en a Series. A menos que realmente lo necesite, utilice otro método.

6) Actualización de un marco vacío de una sola fila a la vez. He visto que este método se usa MUY demasiado. Es de lejos el más lento. Probablemente sea un lugar común (y razonablemente rápido para algunas estructuras de Python), pero DataFramerealiza una buena cantidad de comprobaciones sobre la indexación, por lo que siempre será muy lento actualizar una fila a la vez. Mucho mejor para crear nuevas estructuras y concat.

Jeff
fuente
1
Sí, usé el número 6 (y 5). Tengo algo que aprender. Parece la elección obvia para un principiante relativo.
KieranPC
3
En mi experiencia, la diferencia entre 3, 4 y 5 es limitada según el caso de uso.
IanS
8
Intenté comprobar los tiempos de ejecución en este cuaderno . De alguna manera itertupleses más rápido que apply:(
Dimgold
1
pd.DataFrame.applysuele ser más lento que itertuples. Además, vale la pena considerar listas por comprensión, maplas mal nombradas np.vectorizey numba(sin ningún orden en particular) para cálculos no vectorizables , por ejemplo, vea esta respuesta .
jpp
2
@Jeff, por curiosidad, ¿por qué no ha agregado listas por comprensión aquí? Si bien es cierto que no manejan la alineación del índice o los datos faltantes (a menos que use una función con un try-catch), son buenos para muchos casos de uso (cosas de cadenas / expresiones regulares) donde los métodos pandas no se han vectorizado ( en el verdadero sentido de la palabra) implementaciones. ¿Crees que vale la pena mencionar que los LC son una alternativa más rápida y con menos gastos generales a la aplicación de pandas y muchas funciones de cadena de pandas?
cs95
17

Las operaciones vectoriales en Numpy y pandas son mucho más rápidas que las operaciones escalares en vanilla Python por varias razones:

  • Búsqueda de tipos amortizados : Python es un lenguaje escrito dinámicamente, por lo que hay una sobrecarga de tiempo de ejecución para cada elemento en una matriz. Sin embargo, Numpy (y por lo tanto los pandas) realizan cálculos en C (a menudo a través de Cython). El tipo de matriz se determina solo al comienzo de la iteración; solo este ahorro es uno de los mayores logros.

  • Mejor almacenamiento en caché : la iteración sobre una matriz C es compatible con la caché y, por lo tanto, es muy rápida. Un DataFrame de pandas es una "tabla orientada a columnas", lo que significa que cada columna es en realidad solo una matriz. Por lo tanto, las acciones nativas que puede realizar en un DataFrame (como sumar todos los elementos en una columna) tendrán pocas pérdidas de caché.

  • Más oportunidades de paralelismo : se puede operar una matriz C simple mediante instrucciones SIMD. Algunas partes de Numpy habilitan SIMD, según su CPU y el proceso de instalación. Los beneficios del paralelismo no serán tan dramáticos como la escritura estática y el mejor almacenamiento en caché, pero siguen siendo una victoria sólida.

Moraleja de la historia: usa las operaciones vectoriales en Numpy y pandas. Son más rápidas que las operaciones escalares en Python por la sencilla razón de que estas operaciones son exactamente lo que un programador de C habría escrito a mano de todos modos. (Excepto que la noción de matriz es mucho más fácil de leer que los bucles explícitos con instrucciones SIMD integradas).

Chrisaycock
fuente
11

Esta es la forma de resolver su problema. Todo esto está vectorizado.

In [58]: df = table1.merge(table2,on='letter')

In [59]: df['calc'] = df['number1']*df['number2']

In [60]: df
Out[60]: 
  letter  number1  number2  calc
0      a       50      0.2    10
1      a       50      0.5    25
2      b      -10      0.1    -1
3      b      -10      0.4    -4

In [61]: df.groupby('letter')['calc'].max()
Out[61]: 
letter
a         25
b         -1
Name: calc, dtype: float64

In [62]: df.groupby('letter')['calc'].idxmax()
Out[62]: 
letter
a         1
b         2
Name: calc, dtype: int64

In [63]: df.loc[df.groupby('letter')['calc'].idxmax()]
Out[63]: 
  letter  number1  number2  calc
1      a       50      0.5    25
2      b      -10      0.1    -1
Jeff
fuente
Respuesta muy clara gracias. Intentaré fusionar, pero tengo dudas, ya que luego tendré 5 mil millones de filas (2.5 millones * 2000). Para mantener esta Q general, he creado una Q específica.Me encantaría ver una alternativa para evitar esta mesa gigante, si conoce una: aquí: stackoverflow.com/questions/24875096/…
KieranPC
1
esto no crea el producto cartesiano - es un espacio comprimido y es bastante eficiente en memoria. lo que está haciendo es un problema muy estándar. intentalo. (su pregunta vinculada tiene una solución muy similar)
Jeff
7

Otra opción es usar to_records(), que es más rápido que ambos itertuplesy iterrows.

Pero para su caso, hay mucho margen para otro tipo de mejoras.

Aquí está mi versión optimizada final

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        # np.multiply is in general faster than "x * y"
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

Prueba de referencia:

-- iterrows() --
100 loops, best of 3: 12.7 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

-- itertuple() --
100 loops, best of 3: 12.3 ms per loop

-- to_records() --
100 loops, best of 3: 7.29 ms per loop

-- Use group by --
100 loops, best of 3: 4.07 ms per loop
  letter  number2
1      a      0.5
2      b      0.1
4      c      5.0
5      d      4.0

-- Avoid multiplication --
1000 loops, best of 3: 1.39 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

Código completo:

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b','c','d'],
      'number1':[50,-10,.5,3]}

t2 = {'letter':['a','a','b','b','c','d','c'],
      'number2':[0.2,0.5,0.1,0.4,5,4,1]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=table1.index)


print('\n-- iterrows() --')

def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2'] * t1info)
    maxrow_in_t2 = calculation.index(max(calculation))
    return t2info.loc[maxrow_in_t2]

#%% Iterate through filtering relevant data, optimizing, returning info
def iterthrough():
    for row_index, row in table1.iterrows():   
        t2info = table2[table2.letter == row['letter']].reset_index()
        table3.iloc[row_index,:] = optimize(t2info, row['number1'])

%timeit iterthrough()
print(table3)

print('\n-- itertuple() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.itertuples():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.itertuples():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()


print('\n-- to_records() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.to_records():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.to_records():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()

print('\n-- Use group by --')

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    for index, letter, n1 in table1.to_records():
        t2 = table2.iloc[grouped.groups[letter]]
        calculation = t2.number2 * n1
        maxrow = calculation.argsort().iloc[-1]
        ret.append(t2.iloc[maxrow])
    global table3
    table3 = pd.DataFrame(ret)

%timeit iterthrough()
print(table3)

print('\n-- Even Faster --')
def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

%timeit iterthrough()
print(table3)

La versión final es casi 10 veces más rápida que el código original. La estrategia es:

  1. Úselo groupbypara evitar la comparación repetida de valores.
  2. Úselo to_recordspara acceder a objetos numpy.records sin procesar.
  3. No opere en DataFrame hasta que haya compilado todos los datos.
Cerveza Polor
fuente