Pular para o conteúdo

    Capítulo 28, Avançado

    Desempenho: vetorizar e evitar o laço

    Em Pandas, a mesma conta pode levar milissegundos ou minutos, dependendo de **como** você a escreve. A diferença quase nunca está no computador: está em percorrer as linhas em Python em vez de operar sobre a coluna.

    As quatro maneiras de calcular a mesma coluna

    Quatro formas de calcular a receita de cada linha. Todas dão o mesmo resultado, e os tempos são muito diferentes:

    avancado/cap28_desempenho_pandas.pylinhas 10 a 58
    import time
    
    import numpy as np
    import pandas as pd
    
    rng = np.random.default_rng(0)
    n = 50_000
    base = pd.DataFrame(
        {
            "quantidade": rng.integers(1, 10, n),
            "preco": rng.uniform(5, 500, n).round(2),
            "desconto": rng.choice([0.0, 0.05, 0.1], n),
        }
    )
    
    
    def com_iterrows(d):
        return pd.Series([l["quantidade"] * l["preco"] * (1 - l["desconto"]) for _, l in d.iterrows()], index=d.index)
    
    
    def com_itertuples(d):
        return pd.Series([t.quantidade * t.preco * (1 - t.desconto) for t in d.itertuples()], index=d.index)
    
    
    def com_apply(d):
        return d.apply(lambda l: l["quantidade"] * l["preco"] * (1 - l["desconto"]), axis=1)
    
    
    def vetorizada(d):
        return d["quantidade"] * d["preco"] * (1 - d["desconto"])
    
    
    def medir(funcao, repeticoes=1):
        melhor = float("inf")
        for _ in range(repeticoes):
            inicio = time.perf_counter()
            resultado = funcao(base)
            melhor = min(melhor, time.perf_counter() - inicio)
        return resultado, melhor
    
    
    r_vet, t_vet = medir(vetorizada, repeticoes=20)
    r_tup, t_tup = medir(com_itertuples)
    r_apply, t_apply = medir(com_apply)
    r_iter, t_iter = medir(com_iterrows)
    
    print(all(np.allclose(r_vet, r) for r in (r_tup, r_apply, r_iter)))
    print("itertuples mais rápido que iterrows:", t_tup < t_iter)
    print("vetorizada pelo menos 30 vezes mais rápida que todas:", all(t > 30 * t_vet for t in (t_tup, t_apply, t_iter)))
    
    Saída
    True
    itertuples mais rápido que iterrows: True
    vetorizada pelo menos 30 vezes mais rápida que todas: True
    

    A versão vetorizada é a única que não percorre as linhas em Python: ela entrega a conta inteira ao código compilado, como no NumPy. Eu cronometro a vetorizada com o melhor de 20 repetições (uma só execução a frio a faz parecer mais lenta do que é) e as lentas uma vez, porque já demoram. Na máquina em que testei, o itertuples ficou na casa de 80 vezes mais lento que a versão vetorizada, o apply(axis=1) perto de 500 vezes, e o iterrows acima de 1.000 vezes. Os números variam de máquina para máquina, mas a ordem entre eles não. O iterrows é o mais lento, porque cria uma Series para cada linha. O itertuples é bem mais leve, e o apply(axis=1), por baixo, também percorre linha a linha. Uma regra que serve: se você escreveu axis=1, iterrows ou um for sobre linhas, pare e procure a versão sobre colunas.

    Escrever linha a linha também é lento

    O mesmo vale para atribuir em um laço. Cada df.loc[i, "x"] = valor é uma operação completa de indexação:

    avancado/cap28_desempenho_pandas.pylinhas 63 a 84
    pequena = base.head(5000).copy()
    
    
    def atribuir_no_laco():
        resultado = pequena.copy()
        resultado["total"] = 0.0
        for i in range(len(resultado)):
            resultado.loc[i, "total"] = resultado.loc[i, "quantidade"] * resultado.loc[i, "preco"]
        return resultado
    
    
    def atribuir_vetorizado():
        return pequena.assign(total=pequena["quantidade"] * pequena["preco"])
    
    
    inicio = time.perf_counter()
    a = atribuir_no_laco()
    t_laco = time.perf_counter() - inicio
    inicio = time.perf_counter()
    b = atribuir_vetorizado()
    t_vet2 = time.perf_counter() - inicio
    print(bool(np.allclose(a["total"], b["total"])), t_laco > 100 * t_vet2)
    
    Saída
    True True
    

    `eval` e `query`: a conta como texto

    O eval aceita a expressão como texto e a avalia sobre as colunas, o que deixa expressões longas mais legíveis. O resultado é idêntico ao da conta direta:

    avancado/cap28_desempenho_pandas.pylinhas 89 a 91
    por_eval = base.eval("quantidade * preco * (1 - desconto)")
    print(bool(np.allclose(por_eval, r_vet)))
    print(len(base.query("quantidade >= 5 and desconto > 0")))
    
    Saída
    True
    18417
    

    Com a biblioteca opcional numexpr instalada, o eval pode ser mais rápido em tabelas muito grandes, porque evita criar as colunas intermediárias. Eu não a usei neste curso, então não faço afirmações sobre o ganho. Para tabelas pequenas ou médias, o eval vale pela legibilidade, e não pela velocidade.

    O custo de olhar sem medir

    Antes de otimizar, meça. Uma medição simples basta, e é a única forma de saber onde o tempo vai. Os resultados costumam surpreender: o gargalo quase nunca é onde a intuição aponta.

    avancado/cap28_desempenho_pandas.pylinhas 96 a 111
    def medir_etapas():
        etapas = {}
        inicio = time.perf_counter()
        d = base.assign(receita=vetorizada(base))
        etapas["conta"] = time.perf_counter() - inicio
        inicio = time.perf_counter()
        d.groupby("quantidade")["receita"].sum()
        etapas["groupby"] = time.perf_counter() - inicio
        inicio = time.perf_counter()
        d.sort_values("receita")
        etapas["ordenar"] = time.perf_counter() - inicio
        return etapas
    
    
    etapas = medir_etapas()
    print(sorted(etapas), all(valor > 0 for valor in etapas.values()))
    
    Saída
    ['conta', 'groupby', 'ordenar'] True
    

    A ordem de escolha

    1. Operação vetorizada sobre colunas. 2. Funções internas do Pandas (map, cut, transform("mean"), str.*). 3. np.select e np.where para regras com condições. 4. itertuples, só se for inevitável. 5. apply(axis=1) e iterrows, quase nunca. E, entre um passo e outro, meça.

    Exercício 1

    Trocar o `apply` por uma conta vetorizada

    Escreva margem_vetorizada(d), que calcule (preco * quantidade) * 0.2 - desconto * preco * quantidade sem percorrer as linhas, e confira que o resultado é igual ao da versão com apply(axis=1).