Análise de desempenho com timeit e memory_profiler
Meça o tempo de execução com %timeit e o pico de memória com memory_profiler para identificar as partes mais lentas do seu pipeline.
Análise de desempenho com timeit e memory_profiler é uma aula grátis de Pandas & NumPy Academy no CoddyKit. Esta é a aula 1 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Pandas & NumPy Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Pandas & NumPy Academy inclui 4 aulas no total.
Por que analisar o desempenho antes de otimizar?
Um erro comum é otimizar o código intuitivamente — reescrever uma função que parece lenta sem medir se ela é realmente o gargalo. Em fluxos de processamento de dados, 90% do tempo de execução geralmente vem de 10% do código. A análise de desempenho identifica exatamente onde tempo e memória são consumidos, para que você possa concentrar o esforço de otimização no que realmente importa. A regra de ouro é: meça primeiro, otimize depois. Ferramentas como timeit medem o tempo, e memory_profiler mede a RAM.
import pandas as pd
import numpy as np
import timeit
# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})
t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)
print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')timeit: o temporizador integrado do Python
O módulo timeit mede o tempo de execução executando uma instrução várias vezes e retornando o tempo total decorrido. timeit.timeit(stmt, number=n) executa stmt n vezes e retorna o total em segundos. Divida por number para obter o tempo por execução. Para obter medições precisas, escolha number de modo que a execução total leve pelo menos 0,1 segundo — uma única execução de uma operação rápida não é representativa devido às variações do agendamento do sistema.
import timeit
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})
# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)
print(f'Operator **2 per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square() per run: {time_c/1000*1000:.3f} ms')A função mágica %timeit no Jupyter
Nos notebooks Jupyter, a função mágica %timeit seleciona automaticamente o número de repetições e execuções para fornecer uma estimativa estatisticamente estável e, em seguida, informa os melhores tempos e a média com o desvio padrão. É a maneira mais conveniente de fazer rapidamente uma medição de desempenho de uma única linha. Use %%timeit (percentual duplo) para medir uma célula inteira. O formato da saída é: X ns/μs/ms ± Y ns por loop (média ± desvio-padrão de 7 execuções, N loops cada).
# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()
# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')cProfile para análise de desempenho no nível de função
Enquanto timeit mede uma única expressão, cProfile analisa toda a pilha de chamadas — ele mostra quanto tempo é gasto em cada função chamada durante a execução. Isso é essencial para identificar o gargalo em um fluxo de processamento complexo. Execute python -m cProfile -s cumulative script.py na linha de comando ou use cProfile.run('function()') em um script. No Jupyter, use %prun function().
import cProfile
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})
def slow_pipeline(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] * row['b'])
return pd.Series(result)
# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')Medindo o uso de memória
Use df.memory_usage(deep=True) para ver quanta RAM cada coluna consome. O argumento deep=True mede a memória realmente usada pelas colunas do tipo object (strings), que, caso contrário, informam apenas o tamanho do ponteiro. Use .sum() para obter a memória total do DataFrame em bytes. Divida por 1e6 para obter MB ou por 1e9 para obter GB. Este é o ponto de partida para otimizar a memória — não é possível reduzir aquilo que não foi medido.
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({
'id': range(100000),
'name': ['user_' + str(i) for i in range(100000)],
'score': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')memory_profiler para medir a memória linha a linha
O pacote memory_profiler (pip install memory-profiler) mede o uso de RAM linha a linha em uma função. Decore uma função com @profile e execute-a com python -m memory_profiler script.py. No Jupyter, use a função mágica %memit (da extensão do ipython) para medir o pico de memória de uma única expressão. Isso identifica quais estruturas de dados causam aumentos repentinos de memória — geralmente, concatenar dentro de um loop em vez de coletar os dados e concatená-los uma única vez.
# Install: pip install memory-profiler
# Usage as a decorator:
# from memory_profiler import profile
# @profile
# def process_data():
# import pandas as pd, numpy as np
# df = pd.DataFrame({'x': np.random.randn(1000000)})
# result = df['x'].rolling(100).mean() # <-- sees if this spikes RAM
# return result
# Run: python -m memory_profiler script.py
# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()
print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')sys.getsizeof versus memory_usage para objetos
sys.getsizeof(obj) retorna o tamanho da memória de um objeto Python, mas, para DataFrames do pandas, informa apenas a sobrecarga do contêiner (algumas centenas de bytes), não os dados reais. Use sempre df.memory_usage(deep=True).sum() para DataFrames. Para objetos Python individuais, como listas e dicionários, sys.getsizeof é preciso, mas não percorre objetos aninhados — use a biblioteca pympler (asizeof) para obter os tamanhos completos dos objetos.
import sys
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.randn(100000)})
# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')
# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')Padrão de gerenciador de contexto para medir tempo
Para etapas mais longas do fluxo de processamento, nas quais timeit não é adequado (por exemplo, consultas ao banco de dados ou entrada e saída de arquivos), use um gerenciador de contexto simples com time.perf_counter(). Envolva cada etapa do fluxo de processamento em um contexto de temporizador para registrar sua duração. Isso é mais prático do que timeit no código de produção, pois registra o tempo real decorrido, incluindo a espera por entrada e saída, que timeit também mede, mas é mais difícil de integrar a estruturas de registro.
import time
import contextlib
import pandas as pd
import numpy as np
@contextlib.contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'[{name}] {elapsed*1000:.1f} ms')
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})
with timer('rolling mean'):
result1 = df['x'].rolling(30).mean()
with timer('groupby mean'):
df['group'] = df['x'].round(0)
result2 = df.groupby('group')['y'].mean()Analisando sistematicamente as operações do Pandas
Um fluxo de trabalho sistemático de criação de perfis: 1) Comece com uma amostra representativa dos seus dados (10.000 linhas são suficientes para comparações de tempo). 2) Identifique as etapas mais lentas usando um contexto de temporização ou cProfile. 3) Reescreva a etapa mais lenta usando uma abordagem mais rápida (vetorização, dtype Categorical, extensão em C). 4) Verifique a correção com instruções assert. 5) Meça o tempo novamente para confirmar o ganho de velocidade. 6) Repita o processo para o próximo gargalo até que o pipeline atenda ao seu SLA.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})
# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)
print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype: {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')Quando criar perfis: limites práticos
Nem todo script precisa de criação de perfis. Invista nisso quando: um pipeline demora mais de 30 segundos para ser executado e é executado com frequência (diariamente ou mais), uma função processa mais de 100.000 linhas ou o uso de memória excede 50% da RAM disponível. Para scripts executados uma única vez ou em questão de segundos, a legibilidade é mais importante do que a optimização de microssegundos. O custo de manter código muito optimizado e obscuro frequentemente supera o tempo economizado pela optimização.
Resumo rápido: ferramentas de criação de perfis
Uma referência rápida:
- timeit.timeit — mede o tempo de uma única instrução (script)
- %timeit / %%timeit — mede o tempo no Jupyter (repete automaticamente)
- %prun / cProfile — criação de perfis da pilha de chamadas no nível das funções
- df.memory_usage(deep=True) — medição da RAM por coluna
- memory_profiler @profile — medição da RAM linha a linha
- time.perf_counter() — temporizador manual para as etapas do pipeline
Verificação rápida
Teste a sua compreensão das ferramentas de criação de perfis apresentadas nesta lição.
Recapitulação da lição
Nesta lição, aprendeu que timeit.timeit e %timeit medem o tempo de execução de operações específicas, df.memory_usage(deep=True) mede a RAM de cada coluna com precisão e cProfile / memory_profiler fornecem uma análise mais detalhada de pipelines complexos. Crie sempre perfis antes de optimizar e verifique a correção após cada alteração. A seguir, veremos como evitar loops lentos com iterrows e substituí-los por operações vetorizadas.
Perguntas Frequentes
A aula “Análise de desempenho com timeit e memory_profiler” é grátis?
Sim — o texto completo de “Análise de desempenho com timeit e memory_profiler” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Pandas & NumPy Academy, atualize para CoddyKit PRO. O curso de Pandas & NumPy Academy inclui 4 aulas no total.
O que vou aprender em “Análise de desempenho com timeit e memory_profiler”?
Meça o tempo de execução com %timeit e o pico de memória com memory_profiler para identificar as partes mais lentas do seu pipeline. Você pratica Pandas & NumPy Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Pandas & NumPy Academy?
Nenhuma experiência prévia é necessária. Pandas & NumPy Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 1 de 4.
Quanto tempo leva a aula “Análise de desempenho com timeit e memory_profiler”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Pandas & NumPy Academy?
Sim. Cada aula de Pandas & NumPy Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Análise de desempenho com timeit e memory_profiler
- Evitando iterrows e laços Python
- Tipos de dados eficientes para reduzir a memória
- Leitura em partes para arquivos grandes