O pandas iterrows tem problemas de desempenho?

92

Tenho notado um desempenho muito fraco ao usar iterrows de pandas.

Isso é algo experimentado por outras pessoas? É específico para iterrows e essa função deve ser evitada para dados de um determinado tamanho (estou trabalhando com 2-3 milhões de linhas)?

Essa discussão no GitHub me levou a acreditar que isso é causado pela mistura de dtypes no dataframe, no entanto, o exemplo simples abaixo mostra que está lá mesmo ao usar um dtype (float64). Isso leva 36 segundos na minha 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 que as operações vetorizadas como aplicar são muito mais rápidas? Eu imagino que deve haver alguma iteração linha por linha acontecendo lá também.

Não consigo descobrir como não usar iterrows no meu caso (vou guardar para uma pergunta futura). Portanto, gostaria de saber se você tem conseguido evitar essa iteração de forma consistente. Estou fazendo cálculos com base em dados em dataframes separados. Obrigado!

--- Editar: versão simplificada do que eu quero executar foi adicionada abaixo ---

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
fonte
7
applyNÃO é vetorizado. iterrowsé ainda pior, pois encaixa tudo (que 'o desempenho difere com apply). Você só deve usar iterrowsem muito poucas situações. IMHO nunca. Mostre com o que você está realmente fazendo iterrows.
Jeff
2
O problema ao qual você vinculou tem a ver com o encaixotamento de um DatetimeIndexem Timestamps(foi implementado no espaço do python), e isso foi muito melhorado no master.
Jeff
1
Veja este problema para uma discussão mais completa: github.com/pydata/pandas/issues/7194 .
Jeff
Link para a pergunta específica (esta permanecerá geral): stackoverflow.com/questions/24875096/…
KieranPC
Por favor, não recomende o uso de iterrows (). É um habilitador flagrante do pior antipadrão da história dos pandas.
cs95

Respostas:

181

Geralmente, iterrowssó deve ser usado em casos muito, muito específicos. Esta é a ordem geral de precedência para o desempenho de várias operações:

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)

Usar uma rotina Cython personalizada geralmente é muito complicado, então vamos pular isso por enquanto.

1) A vetorização é SEMPRE, SEMPRE a primeira e melhor escolha. No entanto, há um pequeno conjunto de casos (geralmente envolvendo uma recorrência) que não podem ser vetorizados de maneiras óbvias. Além disso, em um pequenoDataFrame , pode ser mais rápido usar outros métodos.

3) apply geralmente pode ser manipulado por um iterador no espaço Cython. Isso é tratado internamente pelos pandas, embora dependa do que está acontecendo dentro da applyexpressão. Por exemplo, df.apply(lambda x: np.sum(x))será executado muito rapidamente, embora, claro, df.sum(1)seja ainda melhor. No entanto, algo como df.apply(lambda x: x['b'] + 1)será executado no espaço do Python e, conseqüentemente, é muito mais lento.

4) itertuplesnão encaixar os dados em umSeries . Ele apenas retorna os dados na forma de tuplas.

5) iterrowsENCAIXE os dados em umSeries . A menos que você realmente precise disso, use outro método.

6) Atualizar um quadro vazio uma única linha de cada vez. Eu vi esse método ser muito usado. É de longe o mais lento. Provavelmente é um lugar comum (e razoavelmente rápido para algumas estruturas python), mas a DataFramefaz um bom número de verificações na indexação, portanto, será sempre muito lento atualizar uma linha por vez. Muito melhor para criar novas estruturas e concat.

Jeff
fonte
1
Sim, usei o número 6 (e 5). Eu tenho um aprendizado a fazer. Parece a escolha óbvia para um iniciante.
KieranPC
3
Na minha experiência, a diferença entre 3, 4 e 5 é limitada dependendo do caso de uso.
IanS,
8
Tentei verificar os tempos de execução neste notebook . De alguma forma, itertuplesé mais rápido do que apply:(
Dimgold
1
pd.DataFrame.applygeralmente é mais lento do que itertuples. Além disso, vale a pena considerar as compreensões de lista,, mapos mal nomeados np.vectorizee numba(em nenhuma ordem particular) para cálculos não vetorizáveis , por exemplo, veja esta resposta .
jpp
2
@Jeff, por curiosidade, por que você não adicionou as compreensões de lista aqui? Embora seja verdade que eles não lidam com alinhamento de índice ou dados ausentes (a menos que você use uma função com um try-catch), eles são bons para muitos casos de uso (coisas de string / regex) onde os métodos do pandas não são vetorizados ( no verdadeiro sentido da palavra) implementações. Você acha que vale a pena mencionar que os LCs são uma alternativa mais rápida e de menor sobrecarga para o pandas aplicar e muitas funções de string do pandas?
cs95
17

As operações de vetor em Numpy e pandas são muito mais rápidas do que as operações escalares em Python vanilla por vários motivos:

  • Pesquisa de tipo amortizado : Python é uma linguagem tipada dinamicamente, portanto, há sobrecarga de tempo de execução para cada elemento em um array. No entanto, Numpy (e, portanto, pandas) realizam cálculos em C (geralmente via Cython). O tipo da matriz é determinado apenas no início da iteração; essa economia por si só é uma das maiores vitórias.

  • Melhor cache : a iteração sobre um array C é amigável ao cache e, portanto, muito rápida. Um DataFrame do pandas é uma "tabela orientada a colunas", o que significa que cada coluna é realmente apenas um array. Portanto, as ações nativas que você pode executar em um DataFrame (como somar todos os elementos em uma coluna) terão poucos erros de cache.

  • Mais oportunidades para paralelismo : Um array C simples pode ser operado por meio de instruções SIMD. Algumas partes do Numpy habilitam o SIMD, dependendo da CPU e do processo de instalação. Os benefícios do paralelismo não serão tão dramáticos quanto a digitação estática e melhor armazenamento em cache, mas eles ainda são uma vitória sólida.

Moral da história: use as operações vetoriais em Numpy e pandas. Elas são mais rápidas do que as operações escalares em Python pela simples razão de que essas operações são exatamente o que um programador C teria escrito à mão de qualquer maneira. (Exceto que a noção de array é muito mais fácil de ler do que loops explícitos com instruções SIMD incorporadas.)

chrisaycock
fonte
11

Esta é a maneira de resolver seu problema. Tudo isso é vetorizado.

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
fonte
Resposta muito clara, obrigado. Vou tentar mesclar, mas tenho dúvidas, pois terei 5 bilhões de linhas (2,5 milhões * 2000). A fim de manter este Q geral, criei um Q específico. Ficaria feliz em ver uma alternativa para evitar esta mesa gigante, se você conhece uma: aqui: stackoverflow.com/questions/24875096/…
KieranPC
1
isso não cria o produto cartesiano - é um espaço compactado e bastante eficiente em termos de memória. o que você está fazendo é um problema muito comum. Experimente. (sua pergunta vinculada tem uma solução muito semelhante)
Jeff
7

Outra opção é usar to_records(), que é mais rápido do que ambos itertupleseiterrows .

Mas, para o seu caso, há muito espaço para outros tipos de melhorias.

Aqui está minha versão final otimizada

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'))

Teste de referência:

-- 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)

A versão final é quase 10x mais rápida do que o código original. A estratégia é:

  1. Usar groupby para evitar a comparação repetida de valores.
  2. Usar to_records para acessar objetos numpy.records brutos.
  3. Não opere no DataFrame até que você tenha compilado todos os dados.
Cerveja Polor
fonte
0

Detalhes neste vídeo

Benchmark insira a descrição da imagem aqui

Artoby
fonte