Pandas: 10 funções para turbinar sua análise… além do básico!

Rocketseat

Rocketseat

7 min de leitura
pandas-analise-de-dados
Usar pandas sem conhecer os atalhos é tipo montar guarda-roupa sem chave Phillips. Funciona, mas você vai suar mais, demorar o triplo do tempo e provavelmente vai querer desistir no meio.
Muitas pessoas que trabalham com análise de dados já dominam o read_csv, head, describe e groupby. Mas quando a análise aperta, o código vira uma colcha de retalhos de for loops, .apply() excessivo e variáveis intermediárias que poluem o notebook.
Vamos mudar isso.
O objetivo aqui é direto: entregar valor. Vamos explorar 10 funções do pandas que funcionam como atalhos, reduzem linhas de código e deixam seu fluxo de análise mais fluido e legível. Menos .apply(), mais operações vetorizadas.
As 10 funções que vamos detalhar são: assign, pipe, query, eval, explode, melt, merge_asof, cut, convert_dtypes e astype('category').
🚀
Bora acelerar essa análise.

1 - Crie colunas sem quebrar o fluxo (assign())

O problema comum: você precisa criar várias colunas novas baseadas nas existentes. A abordagem usual quebra o encadeamento (chaining) e força a criação de dataframes intermediários ou a repetição de df['nova_coluna'] = ....
A solução: assign() permite criar múltiplas colunas dentro de um fluxo encadeado, mantendo o código limpo e legível. Ele sempre retorna um novo dataframe.
Exemplo:
Vamos calcular o imposto e o preço final de alguns produtos.
import pandas as pd import numpy as np df_produtos = pd.DataFrame({ 'produto': ['Teclado', 'Mouse', 'Monitor'], 'preco_base': [300, 150, 1200], 'desconto_perc': [0.05, 0.10, 0.0] }) df_final = ( df_produtos .assign( preco_com_desconto = lambda df: df.preco_base * (1 - df.desconto_perc), imposto = lambda df: df.preco_com_desconto * 0.25, # Podemos referenciar colunas criadas na mesma chamada assign preco_final = lambda df: df.preco_com_desconto + df.imposto ) ) print(df_final)
Note como usamos funções lambda curtas para referenciar o dataframe no estado atual do encadeamento.

Use quando:

  • Você quer manter um fluxo de operações limpo (method chaining).
  • A lógica para criar a nova coluna é simples e pode ser expressa em uma linha.

Evite quando:

  • A lógica é complexa demais para um lambda. Nesse caso, considere usar pipe() (nossa próxima função).

2 - Organize seu código com funções reutilizáveis (pipe())

O problema comum: seu fluxo de análise tem muitos passos (limpeza, normalização, criação de features). Encadeá-los diretamente pode transformar seu código em um "espaguete" difícil de ler e testar.
A solução: pipe() permite aplicar funções externas ao seu dataframe dentro do encadeamento. Você organiza a lógica em funções pequenas, puras e testáveis.
Exemplo:
Vamos criar funções para limpar e preparar dados de vendas.
def limpar_nulos(df, colunas): # Remove linhas onde as colunas especificadas são nulas return df.dropna(subset=colunas) def normalizar_moeda(df, coluna_origem, coluna_destino, taxa): # Converte valores para outra moeda # Usamos assign aqui também para manter a função pura se preferir df_copia = df.copy() df_copia[coluna_destino] = (df_copia[coluna_origem] * taxa).round(2) return df_copia # Dados de exemplo df_vendas = pd.DataFrame({ 'id_pedido': [1, 2, 3, 4], 'valor_brl': [100.0, np.nan, 300.0, 50.0] }) # Taxa de conversão hipotética BRL para USD taxa_usd = 0.18 df_processado = ( df_vendas .pipe(limpar_nulos, colunas=['valor_brl']) .pipe(normalizar_moeda, coluna_origem='valor_brl', coluna_destino='valor_usd', taxa=taxa_usd) ) print(df_processado)
A beleza do pipe() é que limpar_nulos e normalizar_moeda podem ser facilmente testadas isoladamente.

Use quando:

  • Você quer organizar passos complexos de pré-processamento.
  • Você precisa reutilizar a mesma lógica em diferentes dataframes.
  • Você valoriza código testável.

Evite quando:

  • A operação é trivial e pode ser feita diretamente com métodos nativos do pandas (como um simples .rename()).

3 - Filtros legíveis como SQL (query())

O problema comum: filtrar dataframes usando máscaras booleanas pode ficar verboso e cheio de parênteses e operadores (&, |, ~).
# Verboso e propenso a erros de parênteses # df_filtrado = df[(df['categoria'] == 'A') & (df['preco'] > 100) | (df['desconto'] > 0.2)]
A solução: query() permite escrever filtros usando strings declarativas, muito parecidas com a cláusula WHERE do SQL.
Exemplo:
df_catalogo = pd.DataFrame({ 'item': ['A', 'B', 'C', 'D'], 'preco': [50, 150, 90, 200], 'estoque': [10, 5, 0, 12] }) # Filtro legível df_promocao = df_catalogo.query("preco > 100 and estoque > 0") print(df_promocao) # Usando variáveis locais (prefixo @) limite_minimo = 100 df_caros = df_catalogo.query("preco > @limite_minimo")
💜
Dica: se o nome da sua coluna tiver espaços ou caracteres especiais, use crases (backticks) no query():
# df.query("`preço unitário` > 50")

Use quando:

  • Os filtros são complexos e envolvem múltiplas condições.
  • Você quer melhorar a legibilidade do código de filtragem.

Evite quando:

  • Os nomes das colunas não são válidos como nomes de variáveis em Python (e você não quer usar backticks).
  • O filtro envolve tipos de dados complexos que o motor interno do query não suporta bem.

4 - Expressões rápidas e vetorizadas (eval())

O problema comum: você precisa calcular novas colunas baseadas em operações aritméticas entre colunas existentes. Usar .apply() pode ser lento em dataframes grandes, e a sintaxe padrão pode ser um pouco verbosa.
A solução: eval() permite calcular expressões usando strings, similar ao query(), mas focado em computação. Em alguns cenários, eval() pode oferecer melhor performance ao otimizar o uso da memória (evitando a criação de arrays intermediários) e evitar overhead do interpretador Python.
Exemplo:
df_pedidos = pd.DataFrame({ 'preco_unitario': np.random.rand(1000) * 100, 'quantidade': np.random.randint(1, 10, 1000), 'frete': np.random.rand(1000) * 20 }) # Calculando o valor total do pedido em uma única expressão df_pedidos.eval("valor_total = (preco_unitario * quantidade) + frete", inplace=True) # Você também pode fazer múltiplas operações df_pedidos.eval(""" custo = preco_unitario * 0.6 lucro = valor_total - custo - frete """, inplace=True) print(df_pedidos.head())

Use quando:

  • Você está trabalhando com dataframes grandes e performance é uma prioridade em cálculos numéricos.
  • As operações são aritméticas simples entre colunas.

Evite quando:

  • As operações envolvem tipos de dados complexos (listas, objetos) ou funções externas que não são suportadas pelo motor do eval().

5 - Transforme listas em linhas (explode())

O problema comum: você tem dados onde uma célula contém uma lista ou array (ex.: tags de um produto, itens de um pedido) e precisa analisar cada item individualmente.
A solução: explode() "desembrulha" listas em linhas, duplicando as outras colunas conforme necessário.
Exemplo:
df_pedidos_agg = pd.DataFrame({ 'id_pedido': [101, 102], 'cliente': ['Diego', 'Fernanda'], 'itens': [['Teclado', 'Mouse'], ['Monitor', 'Cabo']] }) df_pedidos_detalhado = df_pedidos_agg.explode('itens') print(df_pedidos_detalhado) # id_pedido cliente itens # 0 101 Diego Teclado # 0 101 Diego Mouse # 1 102 Fernanda Monitor # 1 102 Fernanda Cabo
Depois de usar explode(), você pode facilmente fazer um groupby() para contar a frequência de cada item, por exemplo.

Use quando:

  • Você precisa analisar elementos individuais dentro de coleções armazenadas em células.
  • Ótimo combinado com groupby após a explosão.

Evite quando:

  • As células contêm objetos complexos que não são listas/arrays (como dicionários aninhados, onde json_normalize pode ser mais adequado).
  • Seu dataframe é gigantesco e o explode() gerará um número impraticável de linhas.

6 - De wide para long em uma linha (melt())

O problema comum: seus dados estão no formato "wide" (largo), onde cada variável de observação tem sua própria coluna. Por exemplo, colunas separadas para vendas de janeiro, fevereiro, março. Esse formato é ruim para a maioria das análises e visualizações.
A solução: melt() transforma o dataframe do formato wide para o formato "long" (longo). Ele "despivota" as colunas selecionadas em linhas.
Exemplo:
df_vendas_wide = pd.DataFrame({ 'loja': ['Centro', 'Shopping'], 'vendas_jan': [5000, 7000], 'vendas_fev': [5500, 7200] }) # Formato wide (ruim para agrupar por mês) print(df_vendas_wide) df_vendas_long = df_vendas_wide.melt( id_vars=['loja'], # Colunas que permanecem iguais (identificadores) var_name='mes', # Nome da nova coluna que guarda os nomes das colunas antigas value_name='valor_vendas' # Nome da nova coluna que guarda os valores ) # Formato long (ideal para análise) print(df_vendas_long)
melt() é o oposto de pivot_table(). Enquanto pivot_table() agrega e expande (long para wide), melt() despivota e alonga (wide para long).

Use quando:

  • Você precisa preparar dados para visualização (seaborn, matplotlib geralmente preferem formato long).
  • Seu dataframe tem colunas que representam medidas ao longo do tempo ou categorias diferentes da mesma medida.

Evite quando:

  • Seus dados já estão no formato tidy (longo).

7 - Join por tempo mais próximo (merge_asof())

O problema comum: você tem duas séries temporais com timestamps que não se alinham perfeitamente. Por exemplo, dados de transações e dados de cotação de câmbio. Você precisa juntar os dados com base no timestamp mais próximo (ou imediatamente anterior).
A solução: merge_asof() realiza um "join aproximado". Ele busca a correspondência mais próxima na chave de junção (geralmente data/hora), em vez de uma correspondência exata.
Exemplo:
Vamos juntar transações com a cotação do dólar mais recente antes da transação.
# Transações de compra df_transacoes = pd.DataFrame({ 'data_hora': pd.to_datetime(['2025-10-13 10:05:02', '2025-10-13 10:15:30']), 'valor_usd': [100, 250] }) # Cotações do dólar df_cotacoes = pd.DataFrame({ 'data_hora_cotacao': pd.to_datetime(['2025-10-13 10:00:00', '2025-10-13 10:10:00', '2025-10-13 10:20:00']), 'taxa_brl': [5.10, 5.12, 5.09] }) # Os dataframes DEVEM estar ordenados pela chave de junção df_transacoes = df_transacoes.sort_values('data_hora') df_cotacoes = df_cotacoes.sort_values('data_hora_cotacao') df_merged = pd.merge_asof( df_transacoes, df_cotacoes, left_on='data_hora', right_on='data_hora_cotacao', direction='backward' # 'backward' (padrão): cotação mais recente ANTES da transação # 'forward': cotação mais próxima DEPOIS da transação # 'nearest': cotação mais próxima no geral ) print(df_merged)
A transação das 10:05:02 pegou a cotação das 10:00:00 (5.10), e a das 10:15:30 pegou a das 10:10:00 (5.12).
💜
Dica: use o parâmetro tolerance para definir um limite de tempo máximo para a correspondência. Ex: tolerance=pd.Timedelta('2m').

Use quando:

  • Juntando dados de séries temporais com frequências diferentes ou timestamps desalinhados.
  • Dados de sensores (IoT), logs de eventos ou dados financeiros.

Evite quando:

  • Você precisa de correspondências exatas (use merge()).
  • Seus dados não estão ordenados pela chave de junção.

8 - Agrupe dados em faixas (cut())

O problema comum: você tem uma coluna numérica contínua (idade, preço, tempo de resposta) e quer categorizá-la em faixas (bins) para análise de grupo. Fazer isso manualmente com np.where() ou if/else é trabalhoso.
A solução: cut() divide os dados em bins discretos com base em limites definidos por você.
Exemplo:
Categorizando clientes por faixa etária.
df_clientes = pd.DataFrame({ 'nome': ['Mayk', 'Laís', 'Rodrigo', 'Isabela'], 'idade': [38, 22, 45, 16] }) # Definindo os limites das faixas e os rótulos bins = [0, 18, 30, 45, 60, 120] labels = ['0-18', '19-30', '31-45', '46-60', '60+'] df_clientes['faixa_etaria'] = pd.cut( df_clientes.idade, bins=bins, labels=labels, right=True # Inclui o limite direito (ex.: 30 anos entra na faixa 19-30) ) print(df_clientes)
Observação: existe também o qcut(). A diferença é que cut() usa limites fixos definidos por você, enquanto qcut() tenta dividir os dados em bins com o mesmo número de observações (quantis).

Use quando:

  • Você precisa criar faixas etárias, faixas de preço ou outras segmentações claras.
  • Preparando dados para histogramas ou análises categóricas.

Evite quando:

  • Você precisa que cada grupo tenha exatamente o mesmo tamanho (use qcut()).

9. Abrace os dtypes modernos (convert_dtypes())

O problema comum: historicamente, o pandas tinha dificuldade com valores nulos (NaN) em colunas de inteiros ou booleanos. Se uma coluna int tivesse um único NaN, toda a coluna era convertida para float64. Isso é confuso e ineficiente em termos de memória.
A solução: convert_dtypes() automaticamente infere e converte as colunas para os tipos de dados anuláveis mais recentes do pandas (ex.: Int64 com "I" maiúsculo, boolean, string).
Exemplo:
df_sujo = pd.DataFrame({ 'id': [1, 2, 3, None], 'ativo': [True, False, True, None] }) # Dtypes antigos (id virou float porque tem None; ativo virou object) print(df_sujo.dtypes) # id float64 # ativo object # dtype: object df_limpo = df_sujo.convert_dtypes() # Dtypes modernos (id é Int64 anulável, ativo é boolean anulável) print(df_limpo.dtypes) # id Int64 # ativo boolean # dtype: object

Use quando:

  • Logo após carregar um dataset (ex: read_csv().convert_dtypes()), para garantir que você está usando os tipos de dados mais adequados.
  • Quando você tem colunas de inteiros com valores faltantes e quer mantê-las como inteiros.

Evite quando:

  • Você precisa de compatibilidade estrita com bibliotecas antigas que não suportam os novos dtypes anuláveis (um caso raro hoje em dia).

10 - Memória e velocidade para strings (astype('category'))

O problema comum: dataframes grandes com colunas de texto que se repetem muito (ex.: estado, categoria de produto, segmento de cliente). Armazenar essas strings como object consome muita memória desnecessariamente e operações de agrupamento podem ser mais lentas.
A solução: converter essas colunas para o tipo category. O pandas armazena cada valor único apenas uma vez e usa códigos numéricos internamente, economizando memória e acelerando operações de agrupamento e filtragem.
Exemplo:
# Simulando um dataframe grande n_linhas = 100000 df_segmentos = pd.DataFrame({ 'id': range(n_linhas), 'segmento': np.random.choice(['PME', 'Enterprise', 'Startup', 'Governo'], n_linhas) }) # Uso de memória antes da conversão (em MB) print(f"Uso de memória (string): {df_segmentos.memory_usage(deep=True).sum() / 1024**2:.2f} MB") df_segmentos['segmento'] = df_segmentos['segmento'].astype('category') # Uso de memória depois da conversão (em MB) print(f"Uso de memória (category): {df_segmentos.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
A economia de memória é significativa em colunas de baixa cardinalidade (poucos valores únicos comparado ao total de linhas).

Use quando:

  • A coluna contém strings que se repetem frequentemente (baixa cardinalidade).
  • Você está trabalhando com dataframes muito grandes e precisa otimizar o uso de memória.
  • Você quer acelerar operações de groupby() nessas colunas.

Evite quando:

  • A coluna tem alta cardinalidade (quase todos os valores são únicos, como IDs de texto livre).

Mão na massa:

Vamos juntar algumas dessas funções em um fluxo de análise realista. O objetivo é transformar dados brutos de pedidos em insights sobre faixas de preço por mês.
import pandas as pd import numpy as np # 1. Dados brutos (formato wide, sem preços calculados) data = { 'id_pedido': [1, 2, 3, 4], 'produto': ['A', 'B', 'C', 'A'], 'qtde_jan': [10, 5, 2, 8], 'qtde_fev': [12, 6, 3, 9], 'preco_base': [100, 500, 1500, 90] } df_raw = pd.DataFrame(data) # Definindo as faixas de preço bins = [0, 200, 1000, 5000] labels = ['Barato', 'Médio', 'Caro'] # 2. O fluxo de análise encadeado df_analise = ( df_raw # 2.1. Transformar de wide para long (MELT) .melt( id_vars=['id_pedido', 'produto', 'preco_base'], var_name='mes_raw', value_name='quantidade' ) # 2.2. Limpar a coluna de mês e calcular receita (ASSIGN) .assign( mes = lambda df: df.mes_raw.str.replace('qtde_', ''), receita = lambda df: df.quantidade * df.preco_base ) # 2.3. Filtrar pedidos com receita relevante (QUERY) .query("receita > 100") # 2.4. Categorizar por faixa de preço (CUT) .assign( faixa_preco=lambda df: pd.cut(df.preco_base, bins=bins, labels=labels) ) # 2.5. Otimizar tipos de dados (CONVERT_DTYPES e ASTYPE) .convert_dtypes() .astype({'mes': 'category', 'faixa_preco': 'category'}) # 2.6. Selecionar colunas finais [['mes', 'produto', 'receita', 'faixa_preco']] ) print("Dataframe processado:") print(df_analise.head()) # 3. Agregação final print("\nResultado agregado:") # Usamos observed=True para garantir que groupby funcione corretamente com categorias resultado_final = df_analise.groupby(['mes', 'faixa_preco'], observed=True)['receita'].sum().reset_index() print(resultado_final)
Usando melt, assign, query, cut e otimizações de tipo em sequência, transformamos os dados sem criar variáveis intermediárias, mantendo o notebook limpo e o fluxo claro.

Anti-padrões para ficar de olho

Conhecer os atalhos é ótimo, mas também é importante saber quando você está pegando o caminho mais longo.

1 - O gargalo do .apply() em colunas grandes

O .apply(axis=1) (aplicar linha a linha) é flexível, mas lento porque executa a função Python para cada linha. Quase sempre existe uma alternativa vetorizada.
  • Anti-padrão: df.apply(lambda row: row['A'] + row['B'], axis=1)
  • Melhor: df['A'] + df['B'] (vetorizado nativo) ou df.eval("A + B")
Use .apply() apenas quando não houver alternativa vetorizada (ex.: lógica condicional muito complexa que não pode ser expressa com np.where ou np.select).

2 - Merge sem chaves limpas

Fazer um join (merge) com chaves que não estão limpas (tipos diferentes, espaços em branco, maiúsculas/minúsculas inconsistentes) é pedir para ter dados duplicados ou correspondências perdidas. Sempre limpe e normalize suas chaves antes do merge().

3 - Mutabilidade confusa (inplace=True)

O parâmetro inplace=True modifica o dataframe diretamente. Embora economize memória, ele quebra o encadeamento de métodos e torna o código mais difícil de depurar. É mais seguro tratar dataframes como imutáveis no fluxo de análise.
  • Anti-padrão: df.dropna(inplace=True); df.rename(columns={...}, inplace=True)
  • Melhor: df = df.dropna().rename(columns={...}) ou usar encadeamento como demonstrado.

Checklist: acelerando sua análise

Da próxima vez que você estiver escrevendo uma análise com pandas, faça estas perguntas:
  1. Estou usando um for loop ou .iterrows()? Dá para vetorizar?
  1. Estou usando .apply(axis=1)? Posso trocar por operações aritméticas diretas, assign() ou eval()?
  1. Meu filtro está cheio de & e |? O query() deixaria mais legível?
  1. Estou criando muitas variáveis intermediárias? pipe() pode ajudar a organizar o fluxo?
  1. Meus dados estão no formato wide? Preciso usar melt() para facilitar a análise?
  1. Tenho listas nas células que preciso analisar? Hora de usar explode().
  1. Estou juntando dados de tempo que não batem? merge_asof() é a solução?
  1. Preciso criar faixas de valores? cut() ou qcut() resolvem em uma linha?
  1. Meus inteiros viraram float por causa de NaN? Usei convert_dtypes()?
  1. Tenho colunas de texto repetitivas? Converti para astype('category') para economizar memória?

Perguntas frequentes - FAQ

Quando devo usar cut() vs qcut()?
Use cut() quando você conhece os limites exatos das faixas que fazem sentido para o negócio (ex.: faixas etárias definidas, limites de impostos). Use qcut() quando você quer dividir os dados em grupos de tamanhos iguais com base na distribuição (ex.: dividir clientes nos 25% que mais compram).
query() e eval() são sempre mais rápidos?
Em dataframes pequenos (menos de 10.000 linhas), a diferença de performance comparada às operações padrão do pandas pode ser insignificante. O ganho real aparece em dataframes grandes (centenas de milhares ou milhões de linhas), onde a otimização de memória que eles oferecem (especialmente se a biblioteca numexpr estiver instalada) faz diferença.
Quando usar merge_asof() em vez de merge()?
Use merge() quando você precisa de correspondências exatas nas chaves de junção (ex.: juntar tabela de clientes e pedidos pelo id_cliente). Use merge_asof() quando a junção é baseada em proximidade temporal ou numérica, e correspondências exatas são improváveis ou desnecessárias (ex.: juntar logs de eventos com o status do sistema naquele momento aproximado).
Qual a vantagem real de usar pipe()?
A maior vantagem é a organização e a testabilidade. pipe() permite que você divida lógicas complexas em funções pequenas e puras. Você pode escrever testes unitários para cada função, garantindo que seu pipeline de dados seja robusto e fácil de manter.

Próximos passos

Dominar essas funções vai trazer um ganho imediato de produtividade e legibilidade para suas análises. O próximo passo é praticar. Tente aplicar pelo menos uma dessas funções no seu projeto atual.
Quer ir além do notebook e construir projetos de verdade com Python? A Formação em Python da Rocketseat junta prática, programação orientada a objetos e desenvolvimento de APIs com Flask. Você consolida seu conhecimento com quizzes, projetos práticos e micro-certificados para adicionar ao seu portfólio.
🚀
Bora acelerar seus próximos passos? Conheça a trilha completa da Formação em Python.
 

Conheça o Rocketseat Para Empresas

Oferecemos soluções personalizadas para empresas de todos os portes.

Rocketseat

Rocketseat

Ecossistema de educação contínua referência em programação e Inteligência Artificial.

Artigos_

Explore conteúdos relacionados

Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.

Imagem contendo uma carta e um símbolo de check
NewsletterReceba conteúdos inéditos e novidades gratuitamente