Fase 1 — Fundamentos | Pré-requisitos: Aulas 0 a 3 | Duração estimada: 90 minutos


Introdução

Carregar e inspecionar dados é o ponto de partida. O trabalho real começa quando você precisa transformar os dados brutos numa forma que permita responder suas perguntas. Isso envolve três famílias de operações: filtragem (selecionar subconjuntos de linhas ou colunas), ordenação (reorganizar linhas por critérios) e reshape (mudar a estrutura do dataset — juntar tabelas, agregar valores, pivotar).

Esta aula cobre essas três famílias com profundidade suficiente para você conseguir fazer análises reais. A Fase 2 do curso vai aprofundar limpeza e wrangling mais complexo. Aqui o foco é em transformações do dia a dia que você vai usar em praticamente toda análise.


Filtragem de linhas

Filtrar linhas significa selecionar apenas as que satisfazem uma condição. Em pandas, isso é feito com indexação booleana: você cria uma Series de verdadeiro ou falso para cada linha e usa ela como máscara.

import pandas as pd
import numpy as np

df = pd.read_csv('vendas.csv')

# Filtro simples: apenas vendas acima de R$ 1000
df[df['valor'] > 1000]

# Filtro com igualdade
df[df['regiao'] == 'Sul']

# Negação
df[df['regiao'] != 'Sul']

# Múltiplas condições com AND (ambas devem ser verdadeiras)
df[(df['valor'] > 1000) & (df['regiao'] == 'Sul')]

# Múltiplas condições com OR (pelo menos uma deve ser verdadeira)
df[(df['regiao'] == 'Sul') | (df['regiao'] == 'Norte')]

# Alternativa mais limpa para OR com muitos valores: isin()
df[df['regiao'].isin(['Sul', 'Norte', 'Centro-Oeste'])]

# Negação do isin
df[~df['regiao'].isin(['Sul', 'Norte'])]

# Filtro em colunas de texto: contém uma substring
df[df['produto'].str.contains('notebook', case=False)]

# Filtro por valores ausentes
df[df['desconto'].isnull()]       # linhas onde desconto é NaN
df[df['desconto'].notnull()]      # linhas onde desconto não é NaN

# Filtro por intervalo com between (inclusivo nos dois extremos)
df[df['valor'].between(500, 2000)]

Um erro muito comum é usar & e | sem parênteses ao redor de cada condição:

# ERRADO — gera erro ou resultado incorreto
df[df['valor'] > 1000 & df['regiao'] == 'Sul']

# CORRETO — cada condição entre parênteses
df[(df['valor'] > 1000) & (df['regiao'] == 'Sul')]

Isso acontece porque & tem precedência maior que > e == em Python. Sem os parênteses, Python tenta avaliar 1000 & df['regiao'] antes de fazer a comparação, o que não faz sentido e gera um erro ou resultado errado.

O método query()

Para filtros complexos, o método query() oferece uma sintaxe mais limpa usando uma string:

# Equivalente a df[(df['valor'] > 1000) & (df['regiao'] == 'Sul')]
df.query("valor > 1000 and regiao == 'Sul'")

# Usando variáveis externas com @
limite = 1000
regiao_alvo = 'Sul'
df.query("valor > @limite and regiao == @regiao_alvo")

query() é especialmente útil quando as condições são longas e aninhadas, tornando o código mais legível.


Criando e modificando colunas

Adicionar colunas novas é uma das operações mais frequentes em análise. Em pandas, você simplesmente atribui a uma nova chave:

# Nova coluna com valor constante
df['pais'] = 'Brasil'

# Nova coluna calculada a partir de colunas existentes
df['valor_com_desconto'] = df['valor'] * (1 - df['desconto'] / 100)

# Nova coluna baseada em condição com np.where
# np.where(condição, valor_se_verdadeiro, valor_se_falso)
df['faixa'] = np.where(df['valor'] > 1000, 'alto', 'baixo')

# Condições múltiplas com np.select
condicoes = [
    df['valor'] < 500,
    df['valor'].between(500, 2000),
    df['valor'] > 2000
]
categorias = ['baixo', 'médio', 'alto']
df['faixa'] = np.select(condicoes, categorias, default='indefinido')

# Modificar uma coluna existente
df['nome'] = df['nome'].str.upper()
df['valor'] = df['valor'].round(2)

O método assign()

assign() cria novas colunas sem modificar o DataFrame original, retornando um novo DataFrame. É preferível quando você quer encadear operações:

df_novo = (df
    .assign(valor_com_desconto = df['valor'] * (1 - df['desconto'] / 100))
    .assign(margem = lambda x: x['valor_com_desconto'] * 0.3)
)

O uso de lambda no assign() permite referenciar colunas que foram criadas no mesmo encadeamento — no exemplo, margem usa valor_com_desconto que acabou de ser criada.

Renomeando e removendo colunas

# Renomear colunas específicas
df = df.rename(columns={
    'val': 'valor',
    'reg': 'regiao',
    'qt': 'quantidade'
})

# Renomear todas as colunas (lista deve ter o mesmo tamanho)
df.columns = ['data', 'produto', 'valor', 'quantidade', 'regiao']

# Remover colunas
df = df.drop(columns=['coluna_inutil', 'outra_coluna'])

# Remover linhas por índice
df = df.drop(index=[0, 5, 10])

Ordenação

# Ordenar por uma coluna (crescente por padrão)
df.sort_values('valor')

# Decrescente
df.sort_values('valor', ascending=False)

# Múltiplas colunas: primeiro por regiao, depois por valor decrescente
df.sort_values(['regiao', 'valor'], ascending=[True, False])

# Ordenar e salvar no mesmo DataFrame
df = df.sort_values('data').reset_index(drop=True)

O parâmetro reset_index(drop=True) após o sort é importante: sort_values() reordena as linhas mas mantém os índices originais. Se você queria um índice sequencial de 0 a n após a ordenação, precisa resetar. O drop=True evita que o índice antigo vire uma coluna.

# Pegar os N maiores ou menores valores
df.nlargest(10, 'valor')   # top 10 maiores valores
df.nsmallest(5, 'valor')   # 5 menores valores

Agrupamento e agregação

groupby() é uma das operações mais poderosas do pandas. Ela divide o DataFrame em grupos baseados nos valores de uma ou mais colunas, aplica uma função a cada grupo e combina os resultados.

# Soma do valor por região
df.groupby('regiao')['valor'].sum()

# Múltiplas agregações de uma vez
df.groupby('regiao')['valor'].agg(['sum', 'mean', 'count', 'std'])

# Agregações diferentes por coluna
df.groupby('regiao').agg(
    total_valor=('valor', 'sum'),
    media_valor=('valor', 'mean'),
    total_itens=('quantidade', 'sum'),
    num_vendas=('valor', 'count')
)

# Agrupar por múltiplas colunas
df.groupby(['regiao', 'produto'])['valor'].sum()

# Resetar o índice para ter um DataFrame flat
df.groupby(['regiao', 'produto'])['valor'].sum().reset_index()

A sintaxe agg() com dicionário de tuplas (coluna, função) é a mais explícita e recomendada: você nomeia cada coluna de resultado, especifica de qual coluna de origem vem e qual função aplicar. Isso evita colunas com nomes ambíguos.

# Funções de agregação mais comuns
# sum — soma
# mean — média
# median — mediana
# std — desvio padrão
# var — variância
# min, max — mínimo e máximo
# count — contagem de não-nulos
# nunique — contagem de valores únicos
# first, last — primeiro e último valor do grupo

transform() versus agg()

agg() retorna um DataFrame com uma linha por grupo. transform() retorna uma Series do mesmo tamanho do DataFrame original, útil para criar colunas que dependem do grupo:

# Média do grupo de volta para cada linha
df['media_regiao'] = df.groupby('regiao')['valor'].transform('mean')

# Isso permite calcular, por exemplo, o desvio de cada venda em relação à média da sua região
df['desvio_da_media'] = df['valor'] - df['media_regiao']

Tratamento de valores ausentes

Antes de avançar para reshape, é preciso cobrir o básico de valores ausentes, porque muitas operações de manipulação dependem de como você os trata.

# Verificar ausentes
df.isnull().sum()

# Remover linhas com qualquer valor ausente
df.dropna()

# Remover linhas onde colunas específicas têm ausentes
df.dropna(subset=['valor', 'regiao'])

# Remover apenas colunas que têm mais de 50% de ausentes
df.dropna(axis=1, thresh=int(len(df) * 0.5))

# Preencher ausentes com um valor fixo
df['desconto'].fillna(0)

# Preencher com a média da coluna
df['valor'].fillna(df['valor'].mean())

# Preencher com o valor anterior (forward fill) — útil para séries temporais
df['preco'].ffill()

# Preencher com o valor seguinte (backward fill)
df['preco'].bfill()

Nunca use dropna() sem pensar. Remover linhas com ausentes é uma decisão analítica — você precisa entender por que o valor está ausente antes de decidir o que fazer. A Aula 11 cobre imputação com profundidade. Por ora, o mínimo é documentar o que você fez e por quê.


Junção de DataFrames (merge e concat)

Na prática, os dados que você precisa raramente estão numa única tabela. Você vai precisar combinar múltiplos DataFrames.

merge() — junção por chave comum (equivalente ao JOIN do SQL)

# Dois DataFrames de exemplo
vendas = pd.DataFrame({
    'id_venda': [1, 2, 3, 4],
    'id_cliente': [101, 102, 101, 103],
    'valor': [500, 1200, 800, 300]
})

clientes = pd.DataFrame({
    'id_cliente': [101, 102, 103],
    'nome': ['Ana', 'Bruno', 'Carla'],
    'cidade': ['SP', 'RJ', 'BH']
})

# Inner join — mantém apenas linhas com correspondência nos dois DataFrames
pd.merge(vendas, clientes, on='id_cliente', how='inner')

# Left join — mantém todas as linhas da esquerda
pd.merge(vendas, clientes, on='id_cliente', how='left')

# Right join — mantém todas as linhas da direita
pd.merge(vendas, clientes, on='id_cliente', how='right')

# Outer join — mantém todas as linhas dos dois DataFrames
pd.merge(vendas, clientes, on='id_cliente', how='outer')

# Quando as colunas-chave têm nomes diferentes nos dois DataFrames
pd.merge(vendas, clientes, left_on='id_cliente', right_on='cliente_id')

Os tipos de join seguem exatamente a lógica do SQL. Inner join é o padrão e retorna apenas as linhas que têm correspondência nos dois DataFrames. Left join é o mais usado na prática quando você quer manter todos os registros da tabela principal e adicionar informações da segunda quando disponíveis.

concat() — empilhar DataFrames

# Empilhar verticalmente (adicionar linhas)
df_2023 = pd.read_csv('vendas_2023.csv')
df_2024 = pd.read_csv('vendas_2024.csv')

df_total = pd.concat([df_2023, df_2024], ignore_index=True)
# ignore_index=True recria o índice de 0 a n, evitando duplicatas de índice

# Empilhar horizontalmente (adicionar colunas)
pd.concat([df1, df2], axis=1)

A diferença entre merge() e concat() é conceitual: merge() combina DataFrames por correspondência de valores em colunas-chave (como um JOIN). concat() simplesmente empilha ou coloca lado a lado, sem buscar correspondências.


Reshape: pivot e melt

Reshape significa mudar a forma do DataFrame sem alterar os dados. As duas operações fundamentais são pivotar (de long para wide) e derreter (de wide para long).

pivot_table() — de long para wide

# DataFrame no formato long
# (uma linha por combinação de região + produto)
df_long = pd.DataFrame({
    'regiao': ['Sul', 'Sul', 'Norte', 'Norte'],
    'produto': ['A', 'B', 'A', 'B'],
    'valor': [100, 200, 150, 180]
})

# Pivotar: regiões nas linhas, produtos nas colunas
df_wide = df_long.pivot_table(
    index='regiao',
    columns='produto',
    values='valor',
    aggfunc='sum'
)
#          A      B
# Norte  150    180
# Sul    100    200

# Com múltiplas funções de agregação
df_wide = df_long.pivot_table(
    index='regiao',
    columns='produto',
    values='valor',
    aggfunc=['sum', 'mean']
)

melt() — de wide para long

melt() é a operação inversa: transforma colunas em linhas. É necessária quando você recebe dados num formato wide (comum em planilhas) mas precisa do formato long para análise ou visualização.

# DataFrame no formato wide
df_wide = pd.DataFrame({
    'produto': ['A', 'B', 'C'],
    'jan': [100, 200, 150],
    'fev': [120, 180, 160],
    'mar': [130, 220, 140]
})

# Derreter: transformar colunas de meses em linhas
df_long = df_wide.melt(
    id_vars='produto',       # colunas que permanecem como identificadores
    value_vars=['jan', 'fev', 'mar'],  # colunas a transformar em linhas
    var_name='mes',          # nome da nova coluna que conterá os nomes das colunas
    value_name='valor'       # nome da nova coluna que conterá os valores
)
#   produto  mes  valor
# 0       A  jan    100
# 1       B  jan    200
# 2       C  jan    150
# 3       A  fev    120
# ...

O formato long é geralmente preferido para análise e visualização porque é mais fácil de filtrar, agrupar e plotar. O formato wide é mais fácil de ler como humano. Saber transitar entre os dois é uma habilidade fundamental.


Operações em strings

Pandas tem um acessor .str que aplica operações de string a colunas inteiras de forma vetorizada:

# Converter para minúsculas / maiúsculas
df['cidade'] = df['cidade'].str.lower()
df['nome'] = df['nome'].str.upper()

# Remover espaços em branco nas extremidades
df['nome'] = df['nome'].str.strip()

# Substituir substrings
df['telefone'] = df['telefone'].str.replace('-', '', regex=False)

# Verificar se contém uma substring
df['produto'].str.contains('notebook', case=False)

# Extrair parte de uma string
df['ano'] = df['data'].str[:4]  # primeiros 4 caracteres

# Dividir uma string e pegar uma parte
df['primeiro_nome'] = df['nome_completo'].str.split(' ').str[0]

# Verificar se começa ou termina com
df['codigo'].str.startswith('BR')
df['arquivo'].str.endswith('.csv')

Operações com datas

Colunas de data têm um acessor .dt com propriedades e métodos específicos:

# Primeiro, garantir que a coluna é datetime
df['data'] = pd.to_datetime(df['data'])

# Extrair componentes da data
df['ano'] = df['data'].dt.year
df['mes'] = df['data'].dt.month
df['dia'] = df['data'].dt.day
df['dia_semana'] = df['data'].dt.dayofweek  # 0=segunda, 6=domingo
df['nome_mes'] = df['data'].dt.month_name()

# Diferença entre datas
df['dias_desde_cadastro'] = (pd.Timestamp.today() - df['data_cadastro']).dt.days

# Filtrar por período
df[df['data'].dt.year == 2024]
df[df['data'].between('2024-01-01', '2024-06-30')]

Encadeamento de operações

Uma das características mais elegantes do pandas é a capacidade de encadear operações usando o ponto. Isso permite escrever transformações complexas como uma sequência legível de passos:

resultado = (
    df
    .query("regiao == 'Sul' and valor > 0")
    .assign(valor_liquido = lambda x: x['valor'] * (1 - x['desconto'] / 100))
    .groupby('produto')
    .agg(
        total=('valor_liquido', 'sum'),
        media=('valor_liquido', 'mean'),
        contagem=('valor_liquido', 'count')
    )
    .sort_values('total', ascending=False)
    .reset_index()
    .head(10)
)

Este encadeamento é equivalente a uma série de atribuições intermediárias, mas mais legível e sem criar variáveis temporárias. Os parênteses externos permitem quebrar em múltiplas linhas sem o \ de continuação.


Resumo

Filtragem usa indexação booleana com &, | e ~, ou a sintaxe mais limpa do query(). Colunas novas são criadas por atribuição direta, np.where() para condições binárias, ou np.select() para múltiplas condições. groupby().agg() é a operação central de sumarização por grupo. merge() combina DataFrames por chaves como um JOIN do SQL; concat() empilha DataFrames. pivot_table() e melt() convertem entre formatos wide e long. Encadeamento com parênteses torna transformações complexas legíveis. O acesso a strings e datas via .str e .dt vetoriza operações que seriam lentas em loops.

Exercícios

  1. Por que o código abaixo gera um erro e como corrigi-lo?

    df[df['valor'] > 500 & df['ativo'] == True]
    

    ✓ Resposta:

    O problema é a precedência de operadores. Em Python, & tem precedência maior que > e ==. Então Python avalia 500 & df['ativo'] antes de fazer qualquer comparação, o que tenta fazer um AND bit a bit entre o inteiro 500 e uma Series booleana — uma operação sem sentido que gera um erro ou resultado incorreto.

    A correção é envolver cada condição em parênteses:

    df[(df['valor'] > 500) & (df['ativo'] == True)]
    

    Uma versão mais idiomática em Python evita comparar com True explicitamente quando a coluna já é booleana:

    df[(df['valor'] > 500) & df['ativo']]
    
  2. Qual a diferença entre groupby().agg() e groupby().transform()? Dê um exemplo de situação em que você usaria cada um.

    ✓ Resposta:

    agg() colapsa os grupos numa única linha por grupo, retornando um DataFrame menor com uma linha por valor único do agrupamento. Você o usa quando quer um resumo: total de vendas por região, média de salário por departamento, contagem de clientes por cidade.

    transform() retorna uma Series do mesmo tamanho do DataFrame original, alinhada com cada linha. Você o usa quando quer adicionar informação do grupo de volta ao DataFrame original sem reduzir suas dimensões.

    Exemplo de agg():

    # Resultado: um DataFrame com uma linha por região
    receita_por_regiao = df.groupby('regiao')['valor'].agg(total='sum')
    

    Exemplo de transform():

    # Resultado: uma Series com o mesmo número de linhas que df
    # Cada linha recebe a média da sua região
    df['media_da_regiao'] = df.groupby('regiao')['valor'].transform('mean')
    
    # Agora posso calcular o quanto cada venda desvia da média da sua região
    df['desvio_percentual'] = (df['valor'] - df['media_da_regiao']) / df['media_da_regiao'] * 100
    

    O segundo caso seria impossível com agg() diretamente, porque o resultado teria dimensões diferentes do DataFrame original.

  3. Quando usar merge() e quando usar concat()? Descreva um cenário real de análise que justificaria cada escolha.

    ✓ Resposta:

    concat() é a escolha quando você quer empilhar ou colocar lado a lado DataFrames com a mesma estrutura, sem buscar correspondências entre eles.

    Cenário para concat(): você tem 12 arquivos CSV, um por mês do ano, todos com as mesmas colunas. Você quer criar um DataFrame único com os dados do ano inteiro. pd.concat([jan, fev, mar, ...], ignore_index=True) faz exatamente isso.

    merge() é a escolha quando você quer combinar DataFrames com estruturas diferentes, encontrando correspondências por uma coluna-chave comum — exatamente como um JOIN no SQL.

    Cenário para merge(): você tem uma tabela de pedidos com id_cliente e uma tabela de clientes com informações cadastrais. Para analisar pedidos por cidade ou faixa etária do cliente, você precisa combinar as duas tabelas. pd.merge(pedidos, clientes, on='id_cliente', how='left') adiciona as informações do cliente a cada linha de pedido.

  4. Dado o DataFrame abaixo no formato wide, escreva o código para convertê-lo para o formato long usando melt(), de forma que o resultado tenha as colunas loja, trimestre e vendas.

    df = pd.DataFrame({
        'loja': ['Loja A', 'Loja B', 'Loja C'],
        'Q1': [120000, 95000, 140000],
        'Q2': [135000, 102000, 128000],
        'Q3': [118000, 110000, 155000],
        'Q4': [160000, 125000, 170000]
    })
    

    ✓ Resposta:
    df_long = df.melt(
        id_vars='loja',
        value_vars=['Q1', 'Q2', 'Q3', 'Q4'],
        var_name='trimestre',
        value_name='vendas'
    )
    
    print(df_long)
    #      loja trimestre  vendas
    # 0  Loja A        Q1  120000
    # 1  Loja B        Q1   95000
    # 2  Loja C        Q1  140000
    # 3  Loja A        Q2  135000
    # 4  Loja B        Q2  102000
    # 5  Loja C        Q2  128000
    # 6  Loja A        Q3  118000
    # ...
    

    O formato long resultante tem 12 linhas (3 lojas × 4 trimestres), em vez das 3 linhas originais. Agora você pode facilmente agrupar por trimestre, filtrar por loja, ou plotar a evolução de cada loja ao longo dos trimestres com uma única chamada ao seaborn ou matplotlib.

  5. Escreva um encadeamento de operações pandas que, a partir de um DataFrame df com colunas data, categoria, valor e quantidade, produza o seguinte resultado: o top 5 categorias por receita total (valor × quantidade) no ano de 2024, com as colunas categoria, receita_total e ticket_medio, ordenadas da maior para a menor receita.

    ✓ Resposta:
    resultado = (
        df
        # garantir que data é datetime
        .assign(data=pd.to_datetime(df['data']))
        # filtrar apenas 2024
        .query("data.dt.year == 2024")
        # calcular receita por linha
        .assign(receita=lambda x: x['valor'] * x['quantidade'])
        # agrupar por categoria
        .groupby('categoria')
        .agg(
            receita_total=('receita', 'sum'),
            ticket_medio=('valor', 'mean')
        )
        # arredondar ticket médio
        .assign(ticket_medio=lambda x: x['ticket_medio'].round(2))
        # ordenar pela maior receita
        .sort_values('receita_total', ascending=False)
        # top 5
        .head(5)
        # trazer categoria de volta como coluna (estava no índice após groupby)
        .reset_index()
    )
    
    print(resultado)
    

    Nota: o query("data.dt.year == 2024") funciona com pandas 1.5 ou superior. Em versões mais antigas, substitua por .loc[df['data'].dt.year == 2024] antes do groupby, ou use .pipe() para aplicar o filtro no encadeamento.

Referências