Pandas Apply: Transforme DataFrames e Series com funções personalizadas
Updated on
A transformação de dados está no centro de cada fluxo de trabalho de análise de dados. Embora o pandas forneça centenas de métodos integrados para operações comuns, dados do mundo real frequentemente exigem lógica personalizada que funções padrão não conseguem lidar. Isso cria um dilema: como você aplica eficientemente transformações complexas e definidas pelo usuário em milhares ou milhões de linhas?
O método apply() resolve este problema permitindo que você execute qualquer função Python em colunas, linhas ou elementos de Series do DataFrame. Seja para limpar formatos de string inconsistentes, implementar lógica de negócios condicional ou projetar recursos para modelos de aprendizado de máquina, apply() fornece a flexibilidade para lidar com operações que ficam fora do kit de ferramentas integrado do pandas. No entanto, esse poder vem com compensações de desempenho que muitos cientistas de dados ignoram, levando a código que executa 10-100x mais lentamente do que alternativas otimizadas.
Este guia revela como usar apply() efetivamente, quando evitá-lo completamente e quais alternativas vetorizadas fornecem os mesmos resultados em uma fração do tempo.
Entendendo os conceitos básicos do pandas apply()
O método apply() existe em duas formas: DataFrame.apply() para operações em colunas ou linhas inteiras, e Series.apply() para transformações elemento por elemento em uma única coluna.
Sintaxe de Series.apply()
Ao trabalhar com uma única coluna (Series), apply() executa uma função em cada elemento:
import pandas as pd
import numpy as np
# Criar dados de exemplo
df = pd.DataFrame({
'price': [29.99, 45.50, 15.75, 89.00],
'quantity': [2, 1, 5, 3],
'product': ['widget', 'gadget', 'tool', 'device']
})
# Aplicar função ao Series
def add_tax(price):
return price * 1.08
df['price_with_tax'] = df['price'].apply(add_tax)
print(df)Saída:
price quantity product price_with_tax
0 29.99 2 widget 32.38920
1 45.50 1 gadget 49.14000
2 15.75 5 tool 17.01000
3 89.00 3 device 96.12000DataFrame.apply() com parâmetro axis
O parâmetro axis controla se apply() processa colunas ou linhas:
axis=0(padrão): Aplicar função a cada coluna (operação vertical)axis=1: Aplicar função a cada linha (operação horizontal)
# axis=0: Processar cada coluna
def get_range(column):
return column.max() - column.min()
ranges = df[['price', 'quantity']].apply(get_range, axis=0)
print(ranges)
# Saída:
# price 73.25
# quantity 4.00# axis=1: Processar cada linha
def calculate_total(row):
return row['price'] * row['quantity']
df['total'] = df.apply(calculate_total, axis=1)
print(df)Saída:
price quantity product total
0 29.99 2 widget 59.98
1 45.50 1 gadget 45.50
2 15.75 5 tool 78.75
3 89.00 3 device 267.00Funções Lambda vs. funções nomeadas
Funções lambda fornecem transformações inline concisas, enquanto funções nomeadas oferecem melhor legibilidade para lógica complexa.
Funções Lambda
Perfeitas para operações simples de uma linha:
# Converter nomes de produtos para maiúsculas
df['product_upper'] = df['product'].apply(lambda x: x.upper())
# Calcular preço com desconto
df['discounted'] = df['price'].apply(lambda x: x * 0.9 if x > 30 else x)
# Combinar várias colunas
df['description'] = df.apply(
lambda row: f"{row['quantity']}x {row['product']} @ ${row['price']}",
axis=1
)
print(df['description'])Saída:
0 2x widget @ $29.99
1 1x gadget @ $45.5
2 5x tool @ $15.75
3 3x device @ $89.0Funções nomeadas
Essenciais para transformações em várias etapas e lógica reutilizável:
def categorize_price(price):
"""Categorizar produtos por faixa de preço"""
if price < 20:
return 'Budget'
elif price < 50:
return 'Standard'
else:
return 'Premium'
df['tier'] = df['price'].apply(categorize_price)
def validate_order(row):
"""Aplicar regras de negócio aos dados do pedido"""
if row['quantity'] > 10:
return 'Bulk Order - Review Required'
elif row['price'] * row['quantity'] > 200:
return 'High Value - Priority Shipping'
else:
return 'Standard Processing'
df['order_status'] = df.apply(validate_order, axis=1)
print(df[['product', 'tier', 'order_status']])Entendendo o parâmetro result_type
O parâmetro result_type (apenas DataFrame.apply()) controla como apply() formata a saída quando as funções retornam vários valores:
| result_type | Comportamento | Caso de uso |
|---|---|---|
| None (padrão) | Infere formato de saída automaticamente | Propósito geral |
| 'expand' | Divide resultados tipo lista em colunas separadas | Vários valores de retorno |
| 'reduce' | Tenta retornar Series em vez de DataFrame | Operações de agregação |
| 'broadcast' | Retorna DataFrame com forma original | Transformações elemento por elemento |
def get_stats(column):
"""Retornar múltiplas estatísticas"""
return [column.mean(), column.std(), column.max()]
# Comportamento padrão (infere estrutura)
stats = df[['price', 'quantity']].apply(get_stats)
print(stats)
# Expandir em linhas separadas
stats_expanded = df[['price', 'quantity']].apply(get_stats, result_type='expand')
print(stats_expanded)Comparação de métodos: apply vs map vs applymap vs transform
Entender quando usar cada método previne gargalos de desempenho:
| Método | Opera em | A função de entrada recebe | Tipo de saída | Melhor para |
|---|---|---|---|---|
Series.apply() | Coluna única | Cada elemento individualmente | Series | Transformações elemento por elemento em uma coluna |
Series.map() | Coluna única | Cada elemento (também aceita dict/Series) | Series | Operações de substituição/busca |
DataFrame.apply() | DataFrame inteiro | Coluna completa (axis=0) ou linha (axis=1) | Series/DataFrame | Operações por coluna/linha |
DataFrame.applymap() (obsoleto) | DataFrame inteiro | Cada elemento individualmente | DataFrame | Elemento por elemento em todas as colunas (use map() em vez disso) |
DataFrame.transform() | DataFrame/GroupBy | Coluna/grupo completo | Mesma forma que entrada | Operações que preservam a forma do DataFrame |
# Series.apply() - função personalizada elemento por elemento
df['price_rounded'] = df['price'].apply(lambda x: round(x, 0))
# Series.map() - mapeamento de substituição
tier_map = {'Budget': 1, 'Standard': 2, 'Premium': 3}
df['tier_code'] = df['tier'].map(tier_map)
# DataFrame.apply() - agregação por coluna
totals = df[['price', 'quantity']].apply(sum, axis=0)
# DataFrame.transform() - preservando forma com groupby
df['price_norm'] = df.groupby('tier')['price'].transform(
lambda x: (x - x.mean()) / x.std()
)Desempenho: Por que apply() é lento
O método apply() processa dados em um loop Python, contornando as implementações otimizadas C/Cython do pandas. Para conjuntos de dados grandes, isso cria penalidades de desempenho severas.
Comparação de desempenho
import time
# Criar conjunto de dados grande
large_df = pd.DataFrame({
'values': np.random.randn(100000)
})
# Método 1: apply() com lambda
start = time.time()
result1 = large_df['values'].apply(lambda x: x ** 2)
apply_time = time.time() - start
# Método 2: Operação vetorizada
start = time.time()
result2 = large_df['values'] ** 2
vectorized_time = time.time() - start
print(f"Tempo apply(): {apply_time:.4f}s")
print(f"Tempo vetorizado: {vectorized_time:.4f}s")
print(f"Aceleração: {apply_time/vectorized_time:.1f}x")Saída típica:
Tempo apply(): 0.0847s
Tempo vetorizado: 0.0012s
Aceleração: 70.6xQuando evitar apply()
Use operações vetorizadas em vez disso quando:
# NÃO FAÇA: Usar apply para aritmética
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
# FAÇA: Usar multiplicação vetorizada
df['total'] = df['price'] * df['quantity']
# NÃO FAÇA: Usar apply para métodos de string
df['upper'] = df['product'].apply(lambda x: x.upper())
# FAÇA: Usar acessor de string integrado
df['upper'] = df['product'].str.upper()
# NÃO FAÇA: Usar apply para condições
df['expensive'] = df['price'].apply(lambda x: 'Yes' if x > 50 else 'No')
# FAÇA: Usar np.where ou comparação direta
df['expensive'] = np.where(df['price'] > 50, 'Yes', 'No')Alternativas vetorizadas
| Operação | Lento (apply) | Rápido (vetorizado) |
|---|---|---|
| Aritmética | .apply(lambda x: x * 2) | * 2 |
| Condicionais | .apply(lambda x: 'A' if x > 10 else 'B') | np.where(condition, 'A', 'B') |
| Operações de string | .apply(lambda x: x.lower()) | .str.lower() |
| Operações de data | .apply(lambda x: x.year) | .dt.year |
| Múltiplas condições | .apply(complex_if_elif_else) | np.select([cond1, cond2], [val1, val2], default) |
# Condicionais complexos com np.select
conditions = [
df['price'] < 20,
(df['price'] >= 20) & (df['price'] < 50),
df['price'] >= 50
]
choices = ['Budget', 'Standard', 'Premium']
df['tier_fast'] = np.select(conditions, choices, default='Unknown')Casos de uso comuns para pandas apply()
Apesar das limitações de desempenho, apply() permanece essencial para operações que não possuem equivalentes vetorizados.
1. Limpeza de strings com lógica complexa
# Dados bagunçados de exemplo
messy_df = pd.DataFrame({
'email': ['John.Doe@COMPANY.com', ' jane_smith@test.co.uk ', 'ADMIN@Site.NET']
})
def clean_email(email):
"""Padronizar formato de email"""
email = email.strip().lower()
# Remover pontos extras
username, domain = email.split('@')
username = username.replace('..', '.')
return f"{username}@{domain}"
messy_df['email_clean'] = messy_df['email'].apply(clean_email)
print(messy_df)2. Lógica condicional com dados externos
# Ajuste de preço baseado em busca externa
discount_rules = {
'widget': 0.10,
'gadget': 0.15,
'device': 0.05
}
def apply_discount(row):
"""Aplicar desconto específico do produto com lógica mínima"""
base_price = row['price']
discount = discount_rules.get(row['product'], 0)
discounted = base_price * (1 - discount)
# Preço mínimo
return max(discounted, 9.99)
df['final_price'] = df.apply(apply_discount, axis=1)3. Engenharia de recursos para ML
# Criar recursos de interação
def create_features(row):
"""Gerar recursos para modelagem preditiva"""
features = {}
features['price_per_unit'] = row['price'] / row['quantity']
features['is_bulk'] = 1 if row['quantity'] > 5 else 0
features['revenue_tier'] = pd.cut(
[row['price'] * row['quantity']],
bins=[0, 50, 150, 300],
labels=['Low', 'Medium', 'High']
)[0]
return pd.Series(features)
feature_df = df.apply(create_features, axis=1)
df = pd.concat([df, feature_df], axis=1)4. Chamadas de API e buscas externas
def geocode_address(address):
"""Chamar API externa para geocodificação"""
# Placeholder para chamada API real
# Na prática: requests.get(f"api.geocode.com?q={address}")
return {'lat': 40.7128, 'lon': -74.0060}
# Aplicar com limitação de taxa
import time
def safe_geocode(address):
time.sleep(0.1) # Limitação de taxa
return geocode_address(address)
# df['coords'] = df['address'].apply(safe_geocode)Técnicas avançadas
Passando argumentos adicionais
Funções podem receber parâmetros extras via args e kwargs:
def apply_markup(price, markup_pct, min_profit):
"""Adicionar margem percentual com lucro mínimo"""
markup_amount = price * (markup_pct / 100)
return price + max(markup_amount, min_profit)
# Passar argumentos adicionais
df['retail_price'] = df['price'].apply(
apply_markup,
args=(25,), # markup_pct=25
min_profit=5.0 # argumento de palavra-chave
)Usando apply() com groupby()
Combinar groupby com apply para agregações complexas:
# Transformações em nível de grupo
def normalize_group(group):
"""Normalização Z-score dentro do grupo"""
return (group - group.mean()) / group.std()
df['price_normalized'] = df.groupby('tier')['price'].apply(normalize_group)
# Agregações de grupo personalizadas
def group_summary(group):
"""Criar estatísticas resumidas para o grupo"""
return pd.Series({
'total_revenue': (group['price'] * group['quantity']).sum(),
'avg_price': group['price'].mean(),
'item_count': len(group)
})
tier_summary = df.groupby('tier').apply(group_summary)
print(tier_summary)Barras de progresso com tqdm
Monitorar operações apply de longa duração:
from tqdm import tqdm
tqdm.pandas()
# Usar progress_apply em vez de apply
# df['result'] = df['column'].progress_apply(slow_function)Tratando erros elegantemente
def safe_transform(value):
"""Aplicar transformação com tratamento de erros"""
try:
return complex_operation(value)
except Exception as e:
return None # ou np.nan, ou valor padrão
df['result'] = df['column'].apply(safe_transform)Erros comuns e depuração
Erro 1: Usar axis=1 quando a vetorização é possível
# ERRADO: Operação lenta por linha
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
# CERTO: Operação vetorizada rápida
df['total'] = df['price'] * df['quantity']Erro 2: Esquecer a declaração return
# ERRADO: Sem valor de retorno
def broken_function(x):
x * 2 # return faltando!
# CERTO: Return explícito
def working_function(x):
return x * 2Erro 3: Modificar DataFrame dentro de apply()
# ERRADO: Tentando modificar DataFrame durante iteração
def bad_function(row):
df.loc[row.name, 'new_col'] = row['price'] * 2 # Inseguro!
return row['price']
# CERTO: Retornar valores e atribuir depois
def good_function(row):
return row['price'] * 2
df['new_col'] = df.apply(good_function, axis=1)Depurando funções apply()
# Testar função em linha/elemento único primeiro
test_row = df.iloc[0]
result = your_function(test_row)
print(f"Resultado do teste: {result}")
# Adicionar prints de debug dentro da função
def debug_function(row):
print(f"Processando linha {row.name}: {row.to_dict()}")
result = complex_logic(row)
print(f"Resultado: {result}")
return result
# Testar em subconjunto pequeno
df.head(3).apply(debug_function, axis=1)Visualize resultados com PyGWalker
Após transformar seu DataFrame com apply(), visualizar os resultados ajuda a validar transformações e descobrir padrões. PyGWalker transforma seu pandas DataFrame em uma interface interativa estilo Tableau diretamente em notebooks Jupyter.
import pygwalker as pyg
# Visualizar dados transformados interativamente
walker = pyg.walk(df)PyGWalker permite análise de arrastar e soltar de suas transformações aplicadas:
- Compare colunas originais vs transformadas com gráficos lado a lado
- Valide lógica condicional através de filtragem e agrupamento
- Detecte outliers em campos calculados através de gráficos de distribuição
- Exporte visualizações para documentação
Explore PyGWalker em github.com/Kanaries/pygwalker (opens in a new tab) para transformar exploração de dados de gráficos estáticos em análise interativa.
FAQ
Como aplico uma função a várias colunas de uma vez?
Use DataFrame.apply() com axis=0 para processar cada coluna selecionada, ou use operações vetorizadas diretamente em várias colunas:
# Aplicar a várias colunas
df[['price', 'quantity']] = df[['price', 'quantity']].apply(lambda x: x * 1.1)
# Ou vetorizado (mais rápido)
df[['price', 'quantity']] = df[['price', 'quantity']] * 1.1Qual é a diferença entre apply() e map()?
apply() executa qualquer função chamável elemento por elemento, enquanto map() é otimizado para substituição via dicionários, Series ou funções. Use map() para buscas e substituições (mais rápido), e apply() para transformações personalizadas que requerem lógica complexa.
Por que minha função apply() é tão lenta?
apply() executa em um loop Python em vez de código C compilado, tornando-o 10-100x mais lento que operações vetorizadas. Sempre verifique se o pandas tem um método integrado (.str, .dt, operadores aritméticos) ou se np.where()/np.select() pode substituir sua lógica antes de usar apply().
Posso usar apply() com funções lambda que acessam várias colunas?
Sim, com axis=1 para processar linhas:
df['result'] = df.apply(lambda row: row['col1'] + row['col2'] * 0.5, axis=1)No entanto, isso é lento para DataFrames grandes. Prefira: df['result'] = df['col1'] + df['col2'] * 0.5
Como retorno várias colunas de uma única chamada apply()?
Retorne uma pd.Series com índices nomeados, e o pandas a expandirá automaticamente em colunas:
def multi_output(row):
return pd.Series({
'sum': row['a'] + row['b'],
'product': row['a'] * row['b']
})
df[['sum', 'product']] = df.apply(multi_output, axis=1)Conclusão
O método pandas apply() fornece flexibilidade essencial para transformações personalizadas que ficam fora das operações integradas do pandas. Embora sua implementação de loop Python crie compensações de desempenho, entender quando usar apply() versus alternativas vetorizadas separa fluxos de trabalho de dados eficientes daqueles que param em conjuntos de dados de produção.
Principais conclusões: Use apply() apenas quando métodos vetorizados, acessores de string ou funções NumPy não puderem realizar seu objetivo. Teste funções em linhas individuais antes de aplicá-las a DataFrames inteiros. Para operações por linha em conjuntos de dados grandes, investigue Cython, compilação JIT Numba ou mudança para polars para execução paralela.
Domine tanto o poder quanto as limitações de apply(), e seu kit de ferramentas de transformação de dados lidará com qualquer desafio que o pandas lançar em seu caminho.