Pandas: Otimização de Performance
Última atualização: 2026-08-26
O script apply do Bob levou 10 minutos; Alice mudou para vectorização e terminou em 30 segundos — uma aceleração de 20x. Este não é um caso isolado: 90% dos problemas de performance do Pandas vêm de "usar loops em vez de vectorização". Esta seção cobre o kit completo de ferramentas de performance, desde hábitos de programação até otimização em nível de motor, ajudando você a escrever código Pandas que é rápido e eficiente em memória.
1. O Que Você Vai Aprender
- ❶ Vectorização vs apply vs loops
- ❷ Motor de expressões eval / query
- ❸ Otimização de memória com categorias
- ❹ Compressão numérica com downcast
- ❺ Copy-on-Write e estratégias para big data
2. Os 10 Minutos de apply do Bob
(1) O Problema: Loops Linha por Linha São Lentos Demais
Bob usou iterrows para processar 100K linhas, e levou 10 minutos:
import pandas as pd
import numpy as np
# NÃO FAÇA ISSO — muito lento!
# for i, row in df.iterrows():
# df.loc[i, 'new_col'] = row['a'] * row['b'] + row['c']
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
(2) A Solução: Vectorização em 30 Segundos
▶ Exemplo: Vectorização vs Loops (Dificuldade ⭐)
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000)
})
# ❌ Lento: iterrows (~300s para 100K linhas)
# for i, row in df.iterrows():
# df.loc[i, 'result'] = row['a'] * row['b'] + row['c']
# ✅ Rápido: vetorizado (~0.03s)
df['result'] = df['a'] * df['b'] + df['c']
print(f"Média do resultado: {df['result'].mean():.4f}")
# ~30 segundos vs ~0.03 segundos — 1000x mais rápido!
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
3. Ranking de Performance: Loops → apply → Vectorização
(1) Comparando 4 Métodos
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Comparação de velocidade de 4 métodos (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(50000),
'b': np.random.randn(50000)
})
# Método 1: iterrows (mais lento — NUNCA use para computação)
# ~60 segundos para 50K linhas
# Método 2: apply (lento)
result2 = df.apply(lambda row: row['a'] + row['b'], axis=1)
# Método 3: vetorizado (rápido)
result3 = df['a'] + df['b']
# Método 4: numpy (mais rápido)
result4 = (df['a'].values + df['b'].values)
# Verificar mesmo resultado
print(np.allclose(result2.values, result3.values)) # True
print(np.allclose(result3.values, result4)) # True
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
(2) Tabela de Ranking de Performance
| Rank | Método | Velocidade Relativa | Melhor Para |
|---|---|---|---|
| 1 | Operações NumPy | Mais rápido (1x) | Numérico puro |
| 2 | Vectorização Pandas | Rápido (1-2x) | Rótulos + numérico |
| 3 | map (dicionário) | Médio (5-10x) | Mapeamento um-para-um |
| 4 | apply | Lento (50-100x) | Lógica complexa |
| 5 | itertuples | Muito lento (200x) | Precisa iterar linhas |
| 6 | iterrows | Mais lento (500x) | Nunca use |
4. Motor de Expressões eval / query
(1) eval Acelera Operações Complexas
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Motor de expressões eval (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000),
'd': np.random.randn(100000)
})
# Expressão complexa — eval evita DataFrames intermediários
result1 = df['a'] * df['b'] + df['c'] / df['d'] - df['a'] ** 2
result2 = df.eval('a * b + c / d - a ** 2')
print(np.allclose(result1, result2)) # True
# eval é mais rápido quando:
# 1. Muitas colunas na expressão (>4)
# 2. DataFrame é grande (>10K linhas)
# 3. Memória é limitada (evita intermediários)
# query para filtragem
filtered = df.query('a > 0 and b < 0')
# Equivalente: df[(df['a'] > 0) & (df['b'] < 0)]
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
(2) eval vs Operações Normais
| Cenário | Vantagem do eval | Desvantagem do eval |
|---|---|---|
| Expressões complexas multi-coluna | Menos variáveis intermediárias | Sintaxe limitada |
| DataFrame grande | Economiza memória | Sem benefício para dados pequenos |
| Operações simples | Sem vantagem | Legibilidade ruim |
| Filtragem condicional (query) | Melhor legibilidade | Flexibilidade limitada |
5. Otimização de Memória com categorias
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Compressão com tipo category (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
np.random.seed(42)
# Simular: 100K linhas, 10 cidades únicas
df = pd.DataFrame({
'city': np.random.choice(['NYC', 'LA', 'Chicago', 'Houston', 'Phoenix',
'Philly', 'San Antonio', 'San Diego', 'Dallas', 'Austin'], 100000),
'status': np.random.choice(['Ativo', 'Inativo', 'Pendente', 'Cancelado'], 100000)
})
# Antes: tipo object
mem_before = df.memory_usage(deep=True).sum()
print(f"Antes (object): {mem_before / 1024:.1f} KB")
# Depois: tipo category
df['city'] = df['city'].astype('category')
df['status'] = df['status'].astype('category')
mem_after = df.memory_usage(deep=True).sum()
print(f"Depois (category): {mem_after / 1024:.1f} KB")
print(f"Economia: {(1 - mem_after / mem_before) * 100:.1f}%")
# Tipicamente 80-95% de redução de memória para strings de baixa cardinalidade!
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
(1) Quando Usar category
| Condição | Adequado | Não Adequado |
|---|---|---|
| Valores únicos < 50% do total de linhas | ✅ | ❌ |
| Colunas de string | ✅ | — |
| Ordenação é significativa | ✅ | Strings aleatórias não ordenadas |
| Precisa de métodos de string | ❌ (.str limitado) | ✅ (use objeto) |
| groupby frequente | ✅ (mais rápido) | — |
6. Compressão Numérica com downcast
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Tipos numéricos com downcast (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'id': np.arange(100000),
'age': np.random.randint(0, 120, 100000),
'score': np.random.randint(0, 100, 100000),
'price': np.round(np.random.uniform(0, 1000, 100000), 2),
'flag': np.random.choice([0, 1], 100000)
})
# Verificar tipos atuais e memória
print("Antes:")
print(df.dtypes)
print(f"Memória: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# Downcast de inteiros
df['id'] = pd.to_numeric(df['id'], downcast='unsigned') # uint32 → uint32
df['age'] = pd.to_numeric(df['age'], downcast='unsigned') # int64 → uint8 (0-255)
df['score'] = pd.to_numeric(df['score'], downcast='unsigned')
df['flag'] = pd.to_numeric(df['flag'], downcast='unsigned') # int64 → uint8
# Downcast de floats
df['price'] = pd.to_numeric(df['price'], downcast='float') # float64 → float32
print("\nDepois:")
print(df.dtypes)
print(f"Memória: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# Economia típica: 50-75%
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
7. Copy-on-Write e Estratégias para Big Data
(1) Copy-on-Write (CoW)
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Comportamento do CoW (Dificuldade ⭐⭐)
import pandas as pd
import numpy as np
# Pandas 3.x: CoW é o padrão
pd.options.mode.copy_on_write = True
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# Com CoW: slice retorna uma view, modificação cria uma cópia
subset = df[['a']] # nenhuma cópia até ser modificada
df.loc[0, 'a'] = 99 # modifica df, subset inalterado
print(subset) # ainda [1, 2, 3] — CoW protege
# Padrões seguros sob CoW:
df['c'] = df['a'] + df['b'] # ✅ atribuir nova coluna
df = df.drop(columns='c') # ✅ reatribuir
# Evite:
# subset['a'] = 10 # ❌ SettingWithCopyWarning (problema pré-CoW, corrigido pelo CoW)
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
(2) Estratégias para Big Data
| Tamanho dos Dados | Estratégia |
|---|---|
| < 1GB | Pandas padrão |
| 1-5GB | downcast + category + eval |
| 5-50GB | processamento em chunks com chunksize |
| > 50GB | Considere Dask / Polars / PySpark |
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Chunking com chunksize (Dificuldade ⭐)
import pandas as pd
from io import StringIO
# Simular CSV grande
csv_data = "id,value\n" + "\n".join([f"{i},{i*10}" for i in range(1000)])
# Processar em chunks
total = 0
for chunk in pd.read_csv(StringIO(csv_data), chunksize=200):
total += chunk['value'].sum()
print(f"Total: {total}")
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
8. Exemplo Completo: Otimização End-to-End para Um Milhão de Linhas
(6) ▶ Checklist de Otimização de Performance
graph TB
A[Otimização de Performance] --> B[1. Vectorização sobre loops]
A --> C[2. Aceleração com eval / query]
A --> D[3. Compressão de strings com category]
A --> E[4. Compressão numérica com downcast]
A --> F[5. Chunking com chunksize]
A --> G[6. Copy-on-Write]
B --> H[100-500x de aceleração]
C --> I[Economizar memória, evitar intermediários]
D --> J[80-95% de economia de memória]
E --> K[50-75% de economia de memória]
F --> L[Processar dados além da memória]
G --> M[Fatiamento zero-copy]
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
▶ Exemplo
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
: Do lento ao rápido, fluxo de trabalho completo (Dificuldade ⭐⭐⭐)
import pandas as pd
import numpy as np
# ============================================
# Exemplo abrangente: otimização de 1M de linhas
# iterrows → apply → vetorizado → eval → category
# ============================================
np.random.seed(42)
df = pd.DataFrame({
'city': np.random.choice([f'Cidade_{i}' for i in range(20)], 1000000),
'category': np.random.choice(['Eletrônicos', 'Roupas', 'Casa', 'Esportes'], 1000000),
'price': np.round(np.random.uniform(10, 500, 1000000), 2),
'quantity': np.random.randint(1, 100, 1000000),
'discount': np.random.choice([0, 0.1, 0.2, 0.3], 1000000)
})
# Passo 1: Memória baseline
mem1 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Passo 1 — Memória bruta: {mem1:.1f} MB")
# Passo 2: Downcast de numéricos
df['quantity'] = pd.to_numeric(df['quantity'], downcast='unsigned')
df['price'] = pd.to_numeric(df['price'], downcast='float')
df['discount'] = pd.to_numeric(df['discount'], downcast='float')
mem2 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Passo 2 — Após downcast: {mem2:.1f} MB ({(1-mem2/mem1)*100:.0f}% economizado)")
# Passo 3: Category para strings
df['city'] = df['city'].astype('category')
df['category'] = df['category'].astype('category')
mem3 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"Passo 3 — Após category: {mem3:.1f} MB ({(1-mem3/mem1)*100:.0f}% economizado total)")
# Passo 4: Cálculo vetorizado (rápido)
df['revenue'] = df['price'] * df['quantity'] * (1 - df['discount'])
print(f"Passo 4 — Receita calculada (vetorizado, ~0.01s)")
# Passo 5: Alternativa com eval
df['revenue2'] = df.eval('price * quantity * (1 - discount)')
print(f"Passo 5 — Receita via eval")
# Verificar
print(f"Resultados coincidem: {np.allclose(df['revenue'], df['revenue2'])}")
> **Saída:** Execute isso em um ambiente Python local (pandas 2.x). O servidor Piston não tem pandas pré-instalado. Por favor, instale-o localmente (`pip install pandas`) e acompanhe. Os valores reais podem variar ligeiramente dependendo da versão do pandas.
❓ Perguntas Frequentes
P: Qual é a diferença entre iterrows e itertuples? R: iterrows retorna pares (index, Series), criando uma nova Series a cada vez — extremamente lento (~500x mais lento que a vectorização). itertuples retorna pares (index, namedtuple) sem criar uma Series — 5-10x mais rápido que iterrows, mas ainda 50x mais lento que a vectorização. Use itertuples quando precisar iterar linha por linha, mas primeiro considere se a vectorização é possível.
P: Quando eval é mais rápido? R: eval é mais rápido com dados grandes (>10K linhas) e expressões multi-coluna complexas — ele evita criar DataFrames intermediários e usa o motor numexpr para aceleração. Para dados pequenos ou expressões simples, eval é na verdade mais lento (sobrecarga de inicialização do motor). Regra prática: eval vale a pena apenas para operações compostas com 4+ colunas em grandes conjuntos de dados.
P: inplace realmente economiza memória? R: No Pandas 2.x, inplace geralmente não economiza memória — internamente ele pode ainda criar uma cópia temporária antes de atribuir. Sob o modo Copy-on-Write do Pandas 3.x, operações inplace e non-inplace têm basicamente o mesmo custo de memória. Recomendação: evite inplace; use o padrão de atribuição
df = df.method()para uma semântica mais clara.
P: O que é Copy-on-Write? R: CoW é o comportamento padrão no Pandas 3.x — os dados compartilham a memória subjacente, e uma cópia é criada apenas quando ocorre modificação. Benefícios: o fatiamento não copia (economiza memória), a atribuição encadeada é segura (sem SettingWithCopyWarning). Impacto: algumas operações inplace se comportam de forma diferente e requerem reatribuição.
P: E quanto a arquivos extremamente grandes? R: Estratégia em três níveis: ① chunksize para leitura em chunks + agregação (adequado para estatísticas divisíveis); ② downcast/category para reduzir a memória por linha (caber mais linhas na memória); ③ trocar de ferramentas — Dask (API Pandas distribuída), Polars (DataFrame ultra-rápido escrito em Rust), PySpark (nível de cluster). O limite de memória de uma única máquina é aproximadamente 3-5x o tamanho dos dados.
P: apply é sempre mais lento que a vectorização? R: Na maioria dos casos, sim — apply chama uma função Python linha por linha, enquanto a vectorização é executada no nível C. Exceção rara: quando a vectorização requer criar múltiplos arrays intermediários enormes, apply pode economizar memória (mas não tempo). Estratégia: prefira a vectorização primeiro, tente eval se a performance for insuficiente, e use apply apenas como último recurso.
P: Como usar memory_usage? R:
df.memory_usage(deep=True)retorna a contagem de bytes para cada coluna. deep=True calcula a memória real de strings para colunas objeto (caso contrário, conta apenas tamanhos de ponteiros). Total:df.memory_usage(deep=True).sum(). Use esta função para comparar antes e depois da otimização. Colunas category armazenam apenas códigos inteiros + um dicionário, usando muito menos memória que objeto.
📖 Resumo
- Regra de ouro: vectorização > map > apply > itertuples > iterrows
- eval/query usa o motor numexpr para acelerar expressões complexas — vale a pena para dados grandes com muitas colunas
- O tipo category economiza 80-95% de memória para colunas de string de baixa cardinalidade
- downcast converte int64 → uint8/uint16/uint32, float64 → float32, economizando 50-75%
- Copy-on-Write permite fatiamento zero-copy e atribuição segura
- Estratégia para big data: downcast + category → chunksize → Dask/Polars
📝 Exercícios
- Básico (Dificuldade ⭐): Crie um DataFrame com 10K linhas, calcule uma nova coluna usando apply e vectorização, e compare o memory_usage.
- Intermediário (Dificuldade ⭐⭐): Crie um DataFrame com 100K linhas com colunas objeto, otimize a memória usando category + downcast, e calcule a porcentagem economizada.
- Desafio (Dificuldade ⭐⭐⭐): Crie um DataFrame com 1 milhão de linhas (com colunas category/float/int), e então complete: cálculo vetorizado → cálculo com eval → comparar resultados → acompanhar memory_usage ao longo do processo → produzir um relatório completo de otimização.