Fase 2 — Wrangling avançado | Pré-requisitos: Aulas 0 a 9 | Duração estimada: 80 minutos


Introdução

Coletar dados é necessário mas não suficiente. Antes de limpar qualquer coisa, você precisa entender o que está errado. Avaliação é o processo sistemático de inspecionar os dados para identificar e documentar problemas de qualidade e estrutura.

A distinção entre avaliação e limpeza é importante e frequentemente ignorada. Analistas iniciantes tendem a limpar enquanto avaliam — encontram um problema e já corrigem, sem ter uma visão completa do dataset. O resultado é limpeza parcial, inconsistente e mal documentada. A abordagem correta é avaliar completamente primeiro, documentar todos os problemas encontrados, e só então planejar e executar a limpeza de forma sistemática.

Esta aula cobre avaliação visual (o que você faz olhando para os dados) e avaliação programática (o que você faz com código para detectar problemas que a inspeção visual não revela).


Avaliação visual versus programática

Avaliação visual significa olhar para os dados diretamente — no Jupyter Notebook, num editor de texto, numa planilha. É onde você começa porque captura rapidamente problemas óbvios: colunas com nomes estranhos, linhas em branco, valores claramente absurdos, formatação inconsistente.

Avaliação programática significa escrever código que detecta problemas sistematicamente em todo o dataset. É onde você aprofunda porque um dataset de 100.000 linhas não pode ser inspecionado visualmente em sua totalidade.

As duas são complementares. A visual guia a programática — você vê algo estranho e escreve código para quantificar o problema. A programática complementa a visual — ela encontra instâncias de problemas que você nunca veria olhando para amostra.


Avaliação visual

import pandas as pd
import numpy as np

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

# Visão geral: primeiras e últimas linhas
df.head(10)
df.tail(10)

# Amostra aleatória — mais representativa que head/tail
df.sample(20, random_state=42)

# Todas as colunas com seus tipos
df.dtypes

# Nomes das colunas
df.columns.tolist()

Durante a avaliação visual, você está procurando por:

Nomes de colunas problemáticos — espaços em branco, caracteres especiais, maiúsculas inconsistentes, nomes ambíguos como "col1" ou "valor".

Valores que parecem errados à primeira vista — números negativos onde não deveriam existir, datas que parecem estar no formato errado, texto onde deveria haver números.

Linhas em branco ou quase em branco — algumas fontes exportam linhas de totais ou subtotais no final que contaminam o dataset.

Mistura de tipos numa coluna — uma coluna que deveria ser numérica mas tem algumas células com texto, ou uma coluna de data com formatos misturados.


Avaliação programática: o checklist completo

1. Dimensões e tipos

# Dimensões
print(f"Linhas: {df.shape[0]:,}")
print(f"Colunas: {df.shape[1]}")
print(f"Células totais: {df.size:,}")

# Tipos de cada coluna
print(df.dtypes)

# Contagem por tipo
print(df.dtypes.value_counts())

# Uso de memória
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")

2. Valores ausentes

# Contagem e porcentagem por coluna
ausentes = pd.DataFrame({
    'contagem': df.isnull().sum(),
    'porcentagem': (df.isnull().sum() / len(df) * 100).round(2)
})
ausentes = ausentes[ausentes['contagem'] > 0].sort_values('porcentagem', ascending=False)
print(ausentes)

# Linhas com pelo menos um ausente
linhas_com_ausente = df.isnull().any(axis=1).sum()
print(f"Linhas com pelo menos 1 ausente: {linhas_com_ausente} ({linhas_com_ausente/len(df)*100:.1f}%)")

# Linhas completamente preenchidas
linhas_completas = df.notnull().all(axis=1).sum()
print(f"Linhas completamente preenchidas: {linhas_completas} ({linhas_completas/len(df)*100:.1f}%)")

# Padrão de ausência: quais colunas costumam ter ausentes juntas?
# Útil para detectar ausência sistemática
df.isnull().corr()

A correlação entre ausentes revela padrões estruturais. Se a ausência em coluna_a tem correlação alta com a ausência em coluna_b, provavelmente as duas vêm da mesma fonte ou do mesmo problema de coleta.

3. Duplicatas

# Duplicatas exatas (todas as colunas iguais)
n_duplicatas = df.duplicated().sum()
print(f"Linhas duplicadas: {n_duplicatas} ({n_duplicatas/len(df)*100:.2f}%)")

# Ver as linhas duplicadas
df[df.duplicated(keep=False)].sort_values(list(df.columns))

# Duplicatas por subconjunto de colunas (duplicatas de entidade)
# Por exemplo: mesmo id_cliente com endereços diferentes
n_dup_cliente = df.duplicated(subset=['id_cliente']).sum()
print(f"IDs de cliente duplicados: {n_dup_cliente}")

# Ver os registros com id duplicado
ids_duplicados = df[df.duplicated(subset=['id_cliente'], keep=False)]
ids_duplicados.sort_values('id_cliente')

A diferença entre duplicatas exatas e duplicatas de entidade é importante. Duplicatas exatas são linhas idênticas em todas as colunas — quase sempre erros. Duplicatas de entidade são registros que representam a mesma entidade do mundo real mas com valores diferentes em algumas colunas — podem ser erros ou podem ser atualizações legítimas (o mesmo cliente com dois endereços).

4. Valores únicos e distribuições

# Contagem de valores únicos por coluna
print(df.nunique().sort_values())

# Para colunas categóricas: frequência de cada valor
for col in df.select_dtypes(include=['object', 'category']).columns:
    print(f"\n--- {col} ({df[col].nunique()} valores únicos) ---")
    print(df[col].value_counts(dropna=False).head(15))

# Para colunas numéricas: distribuição
for col in df.select_dtypes(include=['number']).columns:
    desc = df[col].describe()
    print(f"\n--- {col} ---")
    print(desc)
    skew = df[col].skew()
    print(f"Assimetria: {skew:.3f}")

5. Valores fora do domínio válido

# Verificações de domínio específicas por coluna
# (adapte conforme o dataset)

# Idades devem estar entre 0 e 120
if 'idade' in df.columns:
    invalidas = df[(df['idade'] < 0) | (df['idade'] > 120)]
    print(f"Idades fora do domínio [0,120]: {len(invalidas)}")

# Preços devem ser positivos
if 'preco' in df.columns:
    negativos = df[df['preco'] < 0]
    print(f"Preços negativos: {len(negativos)}")

# Porcentagens devem estar entre 0 e 100
if 'desconto_pct' in df.columns:
    invalidos = df[(df['desconto_pct'] < 0) | (df['desconto_pct'] > 100)]
    print(f"Descontos fora de [0,100]: {len(invalidos)}")

# Datas não podem ser no futuro para dados históricos
if 'data_venda' in df.columns:
    df['data_venda'] = pd.to_datetime(df['data_venda'])
    futuras = df[df['data_venda'] > pd.Timestamp.today()]
    print(f"Datas no futuro: {len(futuras)}")

6. Inconsistências entre colunas

# Data de entrega não pode ser anterior à data de compra
if 'data_compra' in df.columns and 'data_entrega' in df.columns:
    df['data_compra'] = pd.to_datetime(df['data_compra'])
    df['data_entrega'] = pd.to_datetime(df['data_entrega'])
    inconsistentes = df[df['data_entrega'] < df['data_compra']]
    print(f"Entregas antes da compra: {len(inconsistentes)}")

# Valor com desconto não pode ser maior que valor original
if 'valor_original' in df.columns and 'valor_final' in df.columns:
    inconsistentes = df[df['valor_final'] > df['valor_original']]
    print(f"Valor final maior que original: {len(inconsistentes)}")

# Funcionário não pode ter sido contratado antes de nascer
if 'data_nascimento' in df.columns and 'data_contratacao' in df.columns:
    df['data_nascimento'] = pd.to_datetime(df['data_nascimento'])
    df['data_contratacao'] = pd.to_datetime(df['data_contratacao'])
    impossivel = df[df['data_contratacao'] < df['data_nascimento']]
    print(f"Contratações antes do nascimento: {len(impossivel)}")

7. Problemas de uniformidade em texto

# Verificar variações de capitalização e espaços
for col in df.select_dtypes(include='object').columns:
    original = df[col].nunique()
    normalizado = df[col].str.lower().str.strip().nunique()

    if original > normalizado:
        print(f"{col}: {original} únicos → {normalizado} após normalização "
              f"({original - normalizado} possíveis duplicatas)")

        # Mostrar exemplos de variações
        valores = df[col].dropna().unique()
        valores_lower = pd.Series(valores).str.lower().str.strip()
        duplicados_mask = valores_lower.duplicated(keep=False)
        if duplicados_mask.any():
            print("  Exemplos de variações:")
            grupos = pd.Series(valores)[duplicados_mask.values]
            print(f"  {grupos.tolist()[:10]}")

8. Outliers

def detectar_outliers_iqr(serie, fator=1.5):
    """Detecta outliers usando o método IQR."""
    q25 = serie.quantile(0.25)
    q75 = serie.quantile(0.75)
    iqr = q75 - q25
    lim_inf = q25 - fator * iqr
    lim_sup = q75 + fator * iqr
    mascara = (serie < lim_inf) | (serie > lim_sup)
    return mascara, lim_inf, lim_sup

def detectar_outliers_zscore(serie, threshold=3):
    """Detecta outliers usando Z-score."""
    z = (serie - serie.mean()) / serie.std()
    return z.abs() > threshold

# Aplicar para todas as colunas numéricas
for col in df.select_dtypes(include='number').columns:
    serie = df[col].dropna()
    mascara_iqr, lim_inf, lim_sup = detectar_outliers_iqr(serie)
    n_outliers = mascara_iqr.sum()

    if n_outliers > 0:
        print(f"\n{col}:")
        print(f"  Outliers IQR: {n_outliers} ({n_outliers/len(serie)*100:.2f}%)")
        print(f"  Limites: [{lim_inf:.2f}, {lim_sup:.2f}]")
        print(f"  Min/Max: [{serie.min():.2f}, {serie.max():.2f}]")
        print(f"  Exemplos acima do limite: {sorted(serie[mascara_iqr].unique())[-5:]}")

9. Problemas estruturais

# Verificar se o dataset parece estar no formato tidy
# Sinal de não-tidy: muitas colunas com nomes que parecem valores de uma variável

# Exemplo: detectar colunas que parecem datas ou períodos
import re
colunas_periodo = [col for col in df.columns
                   if re.match(r'^(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez|q[1-4]|t[1-4]|\d{4})',
                               col.lower())]
if colunas_periodo:
    print(f"Possíveis colunas de período (formato wide): {colunas_periodo}")

# Verificar granularidade: quantas linhas por entidade principal
if 'id_cliente' in df.columns:
    linhas_por_cliente = df.groupby('id_cliente').size()
    print(f"\nLinhas por cliente:")
    print(linhas_por_cliente.describe())
    print(f"Clientes com mais de 10 linhas: {(linhas_por_cliente > 10).sum()}")

Documentando os problemas encontrados

Avaliação sem documentação é trabalho perdido. O produto da avaliação é um registro estruturado de todos os problemas encontrados, que vai guiar a limpeza.

Uma forma prática de documentar no notebook é criar uma célula markdown após cada bloco de avaliação com o que foi encontrado:

## Registro de problemas de qualidade

### Valores ausentes
- `review_score`: 15.3% ausentes — pedidos cancelados não geram avaliação.
  Decisão: manter ausentes para análises de volume; remover para análises de satisfação.
- `product_category_name`: 2.1% ausentes — produtos sem categoria cadastrada.
  Decisão: imputar com categoria 'sem_categoria'.
- `delivery_date`: 8.7% ausentes — correlacionado com status != 'delivered'.
  Decisão: remover essas linhas antes de análises de tempo de entrega.

### Duplicatas
- 1.247 linhas completamente duplicadas (1.2% do total).
  Decisão: remover com drop_duplicates().
- 43 id_pedido aparecem mais de uma vez com valores diferentes.
  Investigar: pode ser erro de sistema ou pedidos com múltiplos itens.

### Valores fora do domínio
- `delivery_time_days`: 89 valores negativos (data_entrega < data_compra).
  Decisão: remover — inconsistência lógica impossível.
- `price`: 12 valores = 0 que não são devoluções (sem registro de devolução associado).
  Decisão: investigar com equipe de dados; provisoriamente, tratar como ausente.

### Uniformidade
- `product_category_name`: 'cama_mesa_banho' e 'cama mesa banho' aparecem como
  categorias distintas. Total de 8 categorias com variações de underscore/espaço.
  Decisão: padronizar para underscore.

### Estrutura
- Dataset está no formato tidy. Granularidade é um item por linha de pedido.
  Para análises por pedido, precisará de agregação por order_id.

Esse registro serve como contrato entre a fase de avaliação e a fase de limpeza. Cada problema documentado vai ter uma decisão documentada, e cada decisão vai ser implementada em código rastreável.


Priorizando o que limpar

Nem todo problema de qualidade precisa ser corrigido para toda análise. A priorização deve levar em conta dois critérios: impacto nas perguntas analíticas e esforço de limpeza.

Impacto alto, esforço baixo — faça imediatamente. Remover duplicatas exatas, corrigir tipos de data, padronizar maiúsculas são operações de uma linha com grande impacto.

Impacto alto, esforço alto — planeje e execute. Problemas que afetam variáveis centrais da análise precisam ser resolvidos mesmo que sejam trabalhosos. Uma coluna de categoria com 50 variações de nomenclatura que é a variável principal da análise precisa ser padronizada.

Impacto baixo, esforço baixo — faça se der tempo. Colunas periféricas com pequenas inconsistências que você não vai usar nas análises principais.

Impacto baixo, esforço alto — documente e ignore. Nem todo problema precisa ser resolvido. Um campo de texto livre com formatação inconsistente que você não vai analisar pode ser deixado como está, com uma nota de que não foi limpo.

Uma matriz simples ajuda a visualizar isso:

# Registrar problemas em um DataFrame para priorização
problemas = pd.DataFrame([
    {'problema': 'Duplicatas exatas',
     'colunas': 'todas',
     'n_afetadas': 1247,
     'impacto': 'alto',
     'esforco': 'baixo',
     'decisao': 'remover com drop_duplicates()'},

    {'problema': 'Ausentes em review_score',
     'colunas': 'review_score',
     'n_afetadas': 15420,
     'impacto': 'alto',
     'esforco': 'baixo',
     'decisao': 'filtrar para análises de satisfação'},

    {'problema': 'Categorias com variações',
     'colunas': 'product_category_name',
     'n_afetadas': 3200,
     'impacto': 'alto',
     'esforco': 'medio',
     'decisao': 'padronizar underscore, criar dicionário de mapeamento'},
])

print(problemas.to_string())

Funções de avaliação reutilizáveis

Para projetos onde você avalia dados regularmente, vale encapsular o processo numa função:

def avaliar_dataframe(df, nome='DataFrame'):
    """
    Executa uma avaliação completa de qualidade de um DataFrame
    e imprime um relatório estruturado.
    """
    print(f"{'='*60}")
    print(f"RELATÓRIO DE QUALIDADE: {nome}")
    print(f"{'='*60}")

    # Dimensões
    print(f"\nDimensões: {df.shape[0]:,} linhas × {df.shape[1]} colunas")
    print(f"Memória: {df.memory_usage(deep=True).sum() / 1024**2:.1f} MB")

    # Tipos
    print(f"\nTipos de dados:")
    for dtype, count in df.dtypes.value_counts().items():
        print(f"  {dtype}: {count} coluna(s)")

    # Ausentes
    ausentes = df.isnull().sum()
    ausentes = ausentes[ausentes > 0]
    if len(ausentes) > 0:
        print(f"\nValores ausentes ({len(ausentes)} coluna(s) afetadas):")
        for col, n in ausentes.sort_values(ascending=False).items():
            pct = n / len(df) * 100
            print(f"  {col}: {n:,} ({pct:.1f}%)")
    else:
        print("\nValores ausentes: nenhum")

    # Duplicatas
    n_dup = df.duplicated().sum()
    print(f"\nDuplicatas exatas: {n_dup:,} ({n_dup/len(df)*100:.2f}%)")

    # Valores únicos
    print(f"\nValores únicos por coluna:")
    nunique = df.nunique().sort_values()
    for col, n in nunique.items():
        tipo = str(df[col].dtype)
        print(f"  {col} ({tipo}): {n:,}")

    # Estatísticas numéricas
    numericas = df.select_dtypes(include='number')
    if not numericas.empty:
        print(f"\nEstatísticas numéricas:")
        print(numericas.describe().round(2).to_string())

    print(f"\n{'='*60}")
    return None

# Uso
avaliar_dataframe(df, nome='Dados de Vendas 2024')

Avaliação de múltiplos DataFrames antes de um merge

Antes de combinar DataFrames com merge, é essencial avaliar as chaves de junção:

def avaliar_chaves_merge(df_esq, df_dir, chave, nome_esq='esquerda', nome_dir='direita'):
    """
    Avalia a qualidade das chaves antes de um merge para prever problemas.
    """
    chave_esq = df_esq[chave]
    chave_dir = df_dir[chave]

    print(f"Avaliação da chave '{chave}' antes do merge:")

    # Ausentes nas chaves
    print(f"\nAusentes na chave:")
    print(f"  {nome_esq}: {chave_esq.isnull().sum()}")
    print(f"  {nome_dir}: {chave_dir.isnull().sum()}")

    # Unicidade
    print(f"\nUnicidade da chave:")
    print(f"  {nome_esq}: {chave_esq.nunique():,} únicos em {len(df_esq):,} linhas "
          f"({'única' if chave_esq.nunique() == len(df_esq) else 'duplicatas presentes'})")
    print(f"  {nome_dir}: {chave_dir.nunique():,} únicos em {len(df_dir):,} linhas "
          f"({'única' if chave_dir.nunique() == len(df_dir) else 'duplicatas presentes'})")

    # Cobertura
    apenas_esq = set(chave_esq.dropna()) - set(chave_dir.dropna())
    apenas_dir = set(chave_dir.dropna()) - set(chave_esq.dropna())
    ambos = set(chave_esq.dropna()) & set(chave_dir.dropna())

    print(f"\nCobertura:")
    print(f"  Apenas em {nome_esq}: {len(apenas_esq):,} valores")
    print(f"  Apenas em {nome_dir}: {len(apenas_dir):,} valores")
    print(f"  Em ambos: {len(ambos):,} valores")

    pct_cobertura_esq = len(ambos) / chave_esq.nunique() * 100
    print(f"\n  {pct_cobertura_esq:.1f}% dos valores de {nome_esq} têm correspondência em {nome_dir}")

# Exemplo de uso
avaliar_chaves_merge(df_pedidos, df_clientes, 'id_cliente',
                     nome_esq='pedidos', nome_dir='clientes')

Resumo

Avaliação deve preceder a limpeza — o produto da avaliação é um registro completo de problemas que guia a limpeza sistemática. Avaliação visual captura problemas óbvios; avaliação programática detecta problemas em escala. O checklist completo cobre: dimensões e tipos, valores ausentes e seus padrões, duplicatas exatas e de entidade, distribuições e outliers, valores fora do domínio, inconsistências entre colunas, problemas de uniformidade em texto e problemas estruturais. Cada problema encontrado deve ser documentado com o que é, quantas linhas afeta e qual a decisão tomada. Priorize limpeza por impacto nas perguntas analíticas e esforço necessário. Antes de qualquer merge, avalie as chaves de junção para prever perda de dados.

Exercícios

  1. Por que é importante avaliar completamente o dataset antes de começar a limpeza, em vez de limpar conforme os problemas são encontrados?

    ✓ Resposta:

    Avaliar antes de limpar permite tomar decisões de limpeza mais informadas e consistentes. Quando você limpa conforme encontra problemas, cada decisão é feita sem visibilidade do quadro completo, o que leva a três problemas concretos.

    Primeiro, inconsistência: você pode tratar ausentes em coluna_a de uma forma e ausentes em coluna_b de outra, sem perceber que as duas colunas têm o mesmo padrão de ausência e deveriam ser tratadas da mesma forma.

    Segundo, limpeza desnecessária: você pode gastar tempo limpando uma coluna que depois descobre ser irrelevante para as perguntas analíticas. Ter o mapa completo dos problemas permite priorizar.

    Terceiro, interação entre limpezas: algumas limpezas afetam outras. Por exemplo, se você remove linhas com ausentes antes de verificar duplicatas, pode remover uma linha que era a cópia "correta" e manter a duplicata problemática. Avaliar tudo primeiro permite planejar a ordem correta das operações.

    Em termos práticos, a avaliação completa leva 30 a 60 minutos para um dataset típico e poupa horas de trabalho inconsistente na limpeza.

  2. Qual a diferença entre duplicata exata e duplicata de entidade? Dê um exemplo de situação em que uma duplicata de entidade pode ser legítima e não deve ser removida.

    ✓ Resposta:

    Duplicata exata é quando duas ou mais linhas têm valores idênticos em todas as colunas. É quase sempre um erro — o mesmo registro foi inserido mais de uma vez.

    Duplicata de entidade é quando duas ou mais linhas representam a mesma entidade do mundo real mas têm valores diferentes em algumas colunas. Por exemplo, o mesmo cliente com dois endereços cadastrados, ou o mesmo produto com dois preços históricos diferentes.

    Exemplo legítimo de duplicata de entidade que não deve ser removida: uma tabela de histórico de preços onde cada linha registra o preço de um produto numa data específica. O mesmo produto aparece múltiplas vezes, uma por período. Isso não é uma duplicata — é a estrutura correta para dados temporais. Remover "duplicatas" por id_produto destruiria o histórico.

    # Tabela de histórico de preços — duplicatas de produto são intencionais
    historico = pd.DataFrame({
        'id_produto': [1, 1, 1, 2, 2],
        'data_vigencia': ['2024-01-01', '2024-06-01', '2024-11-01', '2024-01-01', '2024-09-01'],
        'preco': [29.90, 34.90, 39.90, 15.00, 18.00]
    })
    # Aplicar drop_duplicates(subset='id_produto') aqui seria um erro grave
    

    A chave para distinguir é entender a granularidade esperada do dataset: qual combinação de colunas deveria identificar unicamente cada linha?

  3. Você avaliou um dataset e encontrou que a coluna salario tem 23% de valores ausentes. Antes de decidir o que fazer, descreva o processo de investigação do padrão de ausência e como o resultado da investigação influenciaria a decisão de tratamento.

    ✓ Resposta:

    O processo de investigação tem três etapas.

    Primeira etapa — verificar se a ausência é aleatória ou sistemática. Compare o perfil das linhas com salário ausente versus presente em outras variáveis:

    # Comparar distribuição de outras variáveis por presença/ausência de salário
    df['salario_ausente'] = df['salario'].isnull()
    df.groupby('salario_ausente')[['idade', 'anos_empresa']].mean()
    df.groupby('salario_ausente')['departamento'].value_counts(normalize=True)
    df.groupby('salario_ausente')['tipo_contrato'].value_counts(normalize=True)
    

    Segunda etapa — verificar correlação com outras variáveis ausentes. Se salario e cargo estão ausentes nas mesmas linhas, provavelmente vêm da mesma fonte com problema.

    Terceira etapa — entender o contexto de negócio. Funcionários terceirizados têm salário registrado? Estagiários? Diretores?

    A decisão de tratamento depende do resultado:

    Se a ausência é aleatória e não correlacionada com outras variáveis, imputação pela mediana ou pela média do grupo (por departamento, por cargo) é razoável.

    Se a ausência é sistemática — por exemplo, 95% dos ausentes são de um departamento específico — imputar pela mediana geral introduziria viés grave. A decisão correta pode ser tratar como uma categoria separada, usar um modelo de imputação mais sofisticado, ou simplesmente excluir da análise com documentação clara da limitação.

    Se a ausência é informativa — funcionários de alto escalão cujo salário é confidencial — criar uma variável binária salario_informado pode ser mais analiticamente correto do que imputar.

  4. Escreva o código para detectar, num DataFrame de pedidos com colunas id_pedido, data_pedido, data_entrega, valor e status, todos os tipos de inconsistência entre colunas que você consegue imaginar.

    ✓ Resposta:
    import pandas as pd
    
    # Garantir tipos corretos primeiro
    df['data_pedido'] = pd.to_datetime(df['data_pedido'])
    df['data_entrega'] = pd.to_datetime(df['data_entrega'])
    
    print("=== Verificações de consistência entre colunas ===\n")
    
    # 1. Data de entrega anterior à data do pedido
    cond1 = df['data_entrega'] < df['data_pedido']
    n1 = cond1.sum()
    print(f"1. Entrega antes do pedido: {n1} linhas")
    if n1 > 0:
        print(df[cond1][['id_pedido', 'data_pedido', 'data_entrega']].head())
    
    # 2. Pedidos com status 'entregue' mas sem data de entrega
    cond2 = (df['status'] == 'entregue') & df['data_entrega'].isnull()
    n2 = cond2.sum()
    print(f"\n2. Status 'entregue' sem data de entrega: {n2} linhas")
    
    # 3. Pedidos com data de entrega mas status não é 'entregue'
    cond3 = df['data_entrega'].notnull() & (df['status'] != 'entregue')
    n3 = cond3.sum()
    print(f"\n3. Data de entrega preenchida mas status != 'entregue': {n3} linhas")
    if n3 > 0:
        print(df[cond3]['status'].value_counts())
    
    # 4. Valor negativo em pedidos não-cancelados
    cond4 = (df['valor'] < 0) & (df['status'] != 'cancelado')
    n4 = cond4.sum()
    print(f"\n4. Valor negativo em pedido não-cancelado: {n4} linhas")
    
    # 5. Valor zero em pedidos entregues (improvável — pode ser erro)
    cond5 = (df['valor'] == 0) & (df['status'] == 'entregue')
    n5 = cond5.sum()
    print(f"\n5. Valor zero em pedido entregue: {n5} linhas")
    
    # 6. Tempo de entrega implausível (mais de 365 dias)
    df['tempo_entrega_dias'] = (df['data_entrega'] - df['data_pedido']).dt.days
    cond6 = df['tempo_entrega_dias'] > 365
    n6 = cond6.sum()
    print(f"\n6. Tempo de entrega > 365 dias: {n6} linhas")
    
    # 7. Pedidos cancelados com data de entrega preenchida
    cond7 = (df['status'] == 'cancelado') & df['data_entrega'].notnull()
    n7 = cond7.sum()
    print(f"\n7. Pedido cancelado com data de entrega: {n7} linhas")
    
    # Resumo
    total_inconsistentes = (cond1 | cond2 | cond3 | cond4 | cond5 | cond6 | cond7).sum()
    print(f"\nTotal de linhas com pelo menos uma inconsistência: {total_inconsistentes} "
          f"({total_inconsistentes/len(df)*100:.1f}%)")
    
  5. Por que avaliar as chaves de junção antes de um merge é importante? O que pode acontecer se você fizer o merge sem essa avaliação?

    ✓ Resposta:

    As chaves de junção determinam quais linhas de cada DataFrame serão combinadas. Sem avaliar a qualidade dessas chaves, dois problemas sérios podem ocorrer silenciosamente — sem erros, sem avisos.

    Problema 1 — perda de dados não detectada. Se 15% dos valores da chave no DataFrame da esquerda não existem no DataFrame da direita, um inner join vai silenciosamente descartar essas linhas. Você termina com um DataFrame menor do que esperava, sem nenhum aviso. Se você não verificou o tamanho esperado antes e depois do merge, nunca vai perceber.

    Problema 2 — explosão de linhas. Se a chave não é única no DataFrame da direita — por exemplo, um cliente tem múltiplos registros de endereço — cada linha do DataFrame da esquerda vai ser duplicada para cada correspondência na direita. Um merge de 100.000 pedidos com 200.000 registros de cliente pode produzir 350.000 linhas. Novamente, sem aviso.

    A avaliação prévia detecta esses problemas:

    # Antes do merge
    print(f"Pedidos: {len(df_pedidos):,} linhas")
    print(f"Clientes: {len(df_clientes):,} linhas")
    print(f"IDs únicos em pedidos: {df_pedidos['id_cliente'].nunique():,}")
    print(f"IDs únicos em clientes: {df_clientes['id_cliente'].nunique():,}")
    cobertura = df_pedidos['id_cliente'].isin(df_clientes['id_cliente']).mean()
    print(f"Cobertura: {cobertura*100:.1f}% dos pedidos têm cliente cadastrado")
    
    # Depois do merge
    df_merged = pd.merge(df_pedidos, df_clientes, on='id_cliente', how='left')
    print(f"Resultado do merge: {len(df_merged):,} linhas")
    
    # Verificar se o número de linhas faz sentido
    assert len(df_merged) == len(df_pedidos), \
        f"Merge criou linhas extras: {len(df_merged)} != {len(df_pedidos)}"
    

    A assert ao final é uma forma de documentar a expectativa e ser alertado imediatamente se ela for violada.

Referências

  • Rahm, Erhard; Do, Hong Hai. "Data Cleaning: Problems and Current Approaches". IEEE Data Engineering Bulletin, 2000. Artigo clássico que classifica e formaliza os tipos de problemas de qualidade de dados. Boa referência para o vocabulário da área.
  • Wickham, Hadley. "Tidy Data". Journal of Statistical Software, 2014. O artigo que formalizou o conceito de dados tidy e os tipos de problemas estruturais mais comuns. Disponível em jstatsoft.org/article/view/v059i10
  • Documentação do pandas — Working with missing data: pandas.pydata.org/docs/user_guide/missing_data.html Referência completa para detecção e tratamento de valores ausentes.
  • Documentação do pandas — Duplicates: pandas.pydata.org/docs/reference/api/pandas.DataFrame.duplicated.html Referência do método `duplicated` com exemplos de todos os parâmetros.
  • Breck, Eric et al. "The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction". IEEE Big Data, 2017. Artigo do Google sobre testes de qualidade de dados em pipelines de machine learning. Aplicável também a análise de dados em geral.
  • Van den Broeck, Jan et al. "Data Cleaning: Detecting, Diagnosing, and Editing Data Abnormalities". PLOS Medicine, 2005. Artigo metodológico com abordagem sistemática para detecção e diagnóstico de problemas de qualidade, com exemplos de dados de saúde.