Fase 1 — Fundamentos | Pré-requisitos: Aulas 0, 1 e 2 | Duração estimada: 75 minutos


Introdução

pandas é a biblioteca central da análise de dados em Python. O nome vem de "panel data", um termo da econometria para conjuntos de dados com dimensões de tempo e entidade. Na prática, pandas é uma ferramenta para trabalhar com dados tabulares — tabelas com linhas e colunas, como uma planilha ou uma tabela de banco de dados.

O que torna pandas poderoso não é apenas o que ele faz, mas como ele foi projetado: operações que levariam dezenas de linhas em Python puro se tornam uma ou duas linhas com pandas, sem abrir mão de legibilidade. Além disso, pandas é construído sobre NumPy, o que significa que suas operações são rápidas mesmo com datasets de milhões de linhas.

Esta aula cobre as operações fundamentais de carregamento e inspeção de dados. Você vai aprender a trazer dados para o Python, a entender rapidamente o que tem nas mãos e a fazer as primeiras perguntas sobre a estrutura e qualidade do dataset.


As duas estruturas de dados do pandas

pandas tem duas estruturas de dados principais que você vai usar em quase tudo.

Series é uma sequência unidimensional de valores com um índice associado. Pense nela como uma coluna de tabela com rótulos nas linhas.

DataFrame é uma estrutura bidimensional — uma tabela com linhas e colunas, onde cada coluna é uma Series. É a estrutura que você vai usar na maioria das análises.

import pandas as pd
import numpy as np

# Criando uma Series manualmente
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print(s)
# a    10
# b    20
# c    30
# d    40
# dtype: int64

# Criando um DataFrame manualmente a partir de um dicionário
df = pd.DataFrame({
    'nome': ['Alice', 'Bruno', 'Carla', 'Diego'],
    'idade': [25, 32, 28, 41],
    'cidade': ['São Paulo', 'Rio de Janeiro', 'Belo Horizonte', 'Curitiba'],
    'salario': [4500.0, 7200.0, 5100.0, 9800.0]
})
print(df)

Na prática, você raramente cria DataFrames manualmente. Você os carrega de arquivos ou bancos de dados. Mas entender que um DataFrame é essencialmente um dicionário de Series — onde cada chave é o nome da coluna — ajuda a entender como as operações funcionam.


Carregando dados de arquivos CSV

A função mais usada para carregar dados é pd.read_csv(). No caso mais simples, você passa apenas o caminho do arquivo:

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

Mas read_csv tem dezenas de parâmetros que controlam como o arquivo é interpretado. Os mais importantes:

df = pd.read_csv(
    'vendas.csv',
    sep=';',              # separador (padrão é vírgula, mas ';' é comum em arquivos brasileiros)
    encoding='utf-8',     # codificação do arquivo (utf-8 ou latin-1 para arquivos com acentos)
    decimal=',',          # separador decimal (Brasil usa vírgula)
    thousands='.',        # separador de milhar
    header=0,             # linha que contém os cabeçalhos (0 = primeira linha)
    index_col=0,          # coluna a usar como índice do DataFrame
    usecols=['data', 'produto', 'valor'],  # carregar apenas essas colunas
    nrows=1000,           # carregar apenas as primeiras 1000 linhas
    skiprows=2,           # pular as primeiras 2 linhas
    parse_dates=['data'], # interpretar a coluna 'data' como datetime automaticamente
    na_values=['N/A', '-', 'null']  # valores adicionais a tratar como NaN
)

Um erro comum ao trabalhar com arquivos gerados no Brasil é esquecer que o separador decimal é vírgula e o separador de milhar é ponto, o inverso do padrão americano que o pandas assume. Se uma coluna de valores deveria ser numérica mas está sendo lida como texto, esse é provavelmente o motivo.

Para outros formatos de arquivo:

# Excel
df = pd.read_excel('arquivo.xlsx', sheet_name='Plan1')

# JSON
df = pd.read_json('dados.json')

# A partir de uma query SQL (requer conexão com banco de dados)
import sqlite3
conn = sqlite3.connect('banco.db')
df = pd.read_sql('SELECT * FROM vendas WHERE ano = 2024', conn)

# Diretamente de uma URL
df = pd.read_csv('https://url.com/dados.csv')

Inspeção inicial: as primeiras coisas a fazer

Toda vez que você carrega um dataset novo, existe uma sequência de inspeções que deve se tornar um reflexo automático. O objetivo é responder três perguntas em menos de dois minutos: qual é a forma do dataset, quais são as colunas e seus tipos, e há problemas óbvios de qualidade?

Forma e primeiras linhas

# Quantas linhas e colunas tem o DataFrame
print(df.shape)
# (1000, 8) significa 1000 linhas e 8 colunas

# Primeiras 5 linhas (padrão)
df.head()

# Últimas 5 linhas
df.tail()

# 10 linhas aleatórias — útil para ver dados do meio do dataset
df.sample(10)

# Primeiras n linhas
df.head(20)

head() e tail() são os métodos mais usados para uma primeira olhada. Mas cuidado: eles mostram apenas as extremidades. Dados problemáticos costumam estar no meio. sample() dá uma visão mais representativa.

Informações sobre colunas e tipos

# Resumo do DataFrame: colunas, tipos e valores não-nulos
df.info()

O output de info() é extremamente informativo. Ele mostra o nome de cada coluna, o número de valores não-nulos (o que revela diretamente quantos valores faltantes há), e o tipo de dado de cada coluna.

Os tipos mais comuns que você vai encontrar:

int64 — inteiros. Sem casas decimais, sem valores ausentes (valores ausentes em colunas de inteiro forçam o pandas a usar float64).

float64 — números com ponto flutuante. Inclui colunas que deveriam ser inteiras mas têm valores ausentes.

object — na maioria das vezes significa texto (strings). Mas também pode ser uma coluna numérica que foi lida como texto por causa de formatação.

bool — verdadeiro ou falso.

datetime64 — datas e horários. Só aparece se você usou parse_dates no carregamento ou converteu explicitamente.

category — tipo especial para colunas com poucos valores únicos. Economiza memória.

Uma coluna numérica com tipo object é quase sempre um sinal de problema: vírgula no lugar do ponto decimal, cifrão, espaço em branco, ou outro caractere não numérico embutido nos valores.

Valores ausentes

# Total de valores ausentes por coluna
df.isnull().sum()

# Porcentagem de ausentes por coluna
(df.isnull().sum() / len(df) * 100).round(2)

# Verificar se há qualquer valor ausente no dataset
df.isnull().any().any()

# Linhas que têm pelo menos um valor ausente
df[df.isnull().any(axis=1)]

A porcentagem de valores ausentes por coluna é uma das primeiras métricas a registrar. Uma coluna com 2% de ausentes pode ser tratada de várias formas. Uma coluna com 80% de ausentes provavelmente não serve para a análise.

Estatísticas descritivas básicas

# Estatísticas para colunas numéricas
df.describe()

describe() retorna count, mean, std, min, 25%, 50%, 75% e max para cada coluna numérica. É a inspeção mais rápida para detectar problemas: um salário mínimo de -500, uma idade máxima de 999, um percentil 75 igual ao máximo — tudo isso aparece aqui.

# Para incluir colunas de texto também
df.describe(include='all')

# Para uma coluna específica
df['salario'].describe()

Valores únicos e frequências

# Quantos valores únicos cada coluna tem
df.nunique()

# Valores únicos de uma coluna específica
df['cidade'].unique()

# Contagem de cada valor (colunas categóricas)
df['cidade'].value_counts()

# Com porcentagens
df['cidade'].value_counts(normalize=True).round(3)

value_counts() é especialmente útil para colunas categóricas. Ela revela distribuições desequilibradas, categorias com nomes inconsistentes ("São Paulo" vs "Sao Paulo") e valores raros que podem ser outliers ou erros.


Acessando colunas e linhas

Para selecionar uma coluna, use colchetes com o nome:

# Retorna uma Series
df['nome']

# Retorna um DataFrame de uma coluna (colchetes duplos)
df[['nome']]

# Múltiplas colunas
df[['nome', 'salario', 'cidade']]

Para selecionar linhas e colunas simultaneamente, pandas tem dois indexadores principais:

loc — seleciona por rótulo (nome do índice ou nome da coluna). iloc — seleciona por posição inteira (como índices de lista Python).

# loc: linha com índice 0, coluna 'nome'
df.loc[0, 'nome']

# loc: linhas 0 a 3, colunas 'nome' e 'salario'
df.loc[0:3, ['nome', 'salario']]

# iloc: primeira linha, segunda coluna (por posição)
df.iloc[0, 1]

# iloc: primeiras 3 linhas, primeiras 2 colunas
df.iloc[:3, :2]

# Seleção por condição (filtro booleano)
df[df['salario'] > 6000]

# Múltiplas condições
df[(df['salario'] > 6000) & (df['idade'] < 40)]

A diferença entre loc e iloc parece sutil mas importa quando o índice do DataFrame não é sequencial. Se você filtrou linhas e o índice original foi mantido (o que é o padrão), iloc[0] sempre retorna a primeira linha do DataFrame atual, enquanto loc[0] retorna a linha cujo índice é 0 — que pode não existir mais se a linha 0 foi filtrada.


O índice do DataFrame

Todo DataFrame tem um índice — os rótulos das linhas. Por padrão, é uma sequência de inteiros começando em 0. Mas o índice pode ser qualquer coisa: datas, strings, IDs de clientes.

# Ver o índice atual
df.index

# Definir uma coluna como índice
df = df.set_index('nome')

# Resetar o índice (transforma o índice de volta em coluna)
df = df.reset_index()

Um erro comum de iniciantes é usar reset_index() sem entender que ele cria uma nova coluna com o índice antigo. Se você reset o índice depois de um groupby ou sort, esse comportamento é importante de saber.


Um fluxo de inspeção completo

Juntando tudo, este é o fluxo que você deve executar toda vez que carregar um dataset novo:

import pandas as pd

# 1. Carregar
df = pd.read_csv('dados.csv', encoding='utf-8')

# 2. Forma
print("Shape:", df.shape)

# 3. Primeiras linhas
print(df.head())

# 4. Tipos e valores ausentes
print(df.info())

# 5. Estatísticas descritivas
print(df.describe())

# 6. Valores ausentes por coluna
print(df.isnull().sum())

# 7. Valores únicos por coluna
print(df.nunique())

# 8. Frequências das colunas categóricas principais
for col in df.select_dtypes(include='object').columns:
    print(f"\n{col}:")
    print(df[col].value_counts().head(10))

Este fluxo leva menos de um minuto para executar e já revela a maioria dos problemas sérios num dataset.


Resumo

pandas organiza dados tabulares em DataFrames (bidimensional) e Series (unidimensional). read_csv() é a principal função de carregamento, com parâmetros importantes para codificação, separadores e parsing de datas. A inspeção inicial usa shape, head(), info(), describe(), isnull().sum() e value_counts() para revelar estrutura, tipos e problemas de qualidade. Seleção de dados usa colchetes para colunas, loc para seleção por rótulo e iloc para seleção por posição. O fluxo de inspeção inicial deve se tornar automático ao abrir qualquer dataset novo.

Exercícios

  1. Qual a diferença entre df['coluna'] e df[['coluna']]? Por que essa diferença importa na prática?

    ✓ Resposta:

    df['coluna'] retorna uma Series — a estrutura unidimensional do pandas. df[['coluna']] retorna um DataFrame com uma única coluna — a estrutura bidimensional.

    Isso importa porque algumas funções do pandas aceitam apenas DataFrame como input, não Series. Se você passa uma Series para uma função que espera um DataFrame, recebe um TypeError. Além disso, operações de concatenação e merge esperam DataFrames.

    Há também diferença no output visual no Jupyter: uma Series é exibida verticalmente sem cabeçalho de coluna no mesmo estilo, enquanto um DataFrame de uma coluna exibe o nome da coluna como cabeçalho. Para análise cotidiana isso raramente importa, mas ao encadear operações é bom saber qual tipo você tem.

  2. Você carrega um CSV e executa df.info(). A coluna preco aparece com tipo object em vez de float64. Liste pelo menos três razões possíveis para isso acontecer e como você investigaria cada uma.

    ✓ Resposta:

    Razão 1: o separador decimal é vírgula (padrão brasileiro). Um valor como "1.250,99" não pode ser convertido para float diretamente. Para investigar, execute df['preco'].head(20) e observe o formato dos valores. Para corrigir, use pd.read_csv(..., decimal=',', thousands='.').

    Razão 2: há caracteres não numéricos na coluna, como "R$", "%" ou espaços em branco. Para investigar, execute df['preco'].unique() para ver os valores distintos, ou df['preco'].str.contains('[^0-9.,]', regex=True).sum() para contar quantas linhas têm caracteres não numéricos. Para corrigir, use df['preco'].str.replace('R\$', '', regex=True).str.strip() antes de converter.

    Razão 3: há valores de texto misturados na coluna, como "N/A", "indisponível" ou células em branco que vieram como string vazia. Para investigar, execute df['preco'].value_counts(dropna=False) para ver todos os valores incluindo os nulos. Para corrigir, especifique esses valores no parâmetro na_values do read_csv.

  3. Qual a diferença prática entre loc e iloc? Dado o DataFrame abaixo, o que retornam df.loc[2, 'nome'] e df.iloc[2, 0]?

    df = pd.DataFrame({
        'nome': ['Ana', 'Bia', 'Caio', 'Duda'],
        'nota': [8.5, 7.0, 9.2, 6.8]
    }, index=[10, 20, 30, 40])
    

    ✓ Resposta:

    loc usa os rótulos do índice. Neste DataFrame, o índice é [10, 20, 30, 40]. Portanto df.loc[2, 'nome'] tentaria encontrar a linha com rótulo de índice 2, que não existe — isso geraria um KeyError.

    iloc usa posições inteiras baseadas em zero, ignorando os rótulos. df.iloc[2, 0] retorna o valor na terceira linha (posição 2) e primeira coluna (posição 0), que é 'Caio'.

    Este exemplo ilustra exatamente quando a diferença importa: quando o índice não é a sequência padrão 0, 1, 2, 3. Após um set_index(), um sort_values() ou um filtro que preserva os índices originais, loc e iloc se comportam de forma completamente diferente.

  4. Por que df.describe() é insuficiente para detectar todos os problemas de qualidade dos dados? Cite dois tipos de problema que ele não revela.

    ✓ Resposta:

    describe() calcula estatísticas sumárias para colunas numéricas (e opcionalmente para todas as colunas com include='all'). Ele é útil para detectar outliers extremos, escala inesperada e distribuições distorcidas. Mas não revela:

    Problema 1: inconsistências em colunas de texto. describe() para uma coluna categórica mostra apenas count, unique, top (valor mais frequente) e freq. Ele não mostra que "São Paulo", "sao paulo", "S.Paulo" e "SP" estão sendo tratados como quatro categorias diferentes em vez de uma. Para isso você precisa de value_counts().

    Problema 2: duplicatas. describe() não indica se há linhas duplicadas no dataset. Uma linha duplicada 100 vezes inflaria a média e a contagem sem nenhum sinal visível no output de describe(). Para detectar duplicatas, use df.duplicated().sum().

    Outros problemas não detectados incluem: valores ausentes (para isso use isnull().sum()), tipos incorretos de dados (para isso use info()), e relações impossíveis entre colunas como uma data de nascimento posterior à data de contratação.

  5. Escreva o código para carregar um arquivo chamado clientes.csv com as seguintes características: separador ponto e vírgula, codificação latin-1, coluna data_cadastro deve ser interpretada como data, e os valores "ND" e "—" devem ser tratados como ausentes. Depois, escreva o código do fluxo completo de inspeção inicial para esse DataFrame.

    ✓ Resposta:
    import pandas as pd
    
    # Carregamento com os parâmetros corretos
    df = pd.read_csv(
        'clientes.csv',
        sep=';',
        encoding='latin-1',
        parse_dates=['data_cadastro'],
        na_values=['ND', '—']
    )
    
    # Fluxo de inspeção inicial
    
    # 1. Forma do dataset
    print("Shape:", df.shape)
    print()
    
    # 2. Primeiras linhas para ver a estrutura
    print("Primeiras linhas:")
    print(df.head())
    print()
    
    # 3. Tipos de dados e contagem de não-nulos
    print("Info:")
    df.info()
    print()
    
    # 4. Estatísticas descritivas das colunas numéricas
    print("Estatísticas descritivas:")
    print(df.describe())
    print()
    
    # 5. Valores ausentes por coluna
    print("Valores ausentes por coluna:")
    print(df.isnull().sum())
    print()
    
    # 6. Porcentagem de ausentes
    print("Porcentagem de ausentes:")
    print((df.isnull().sum() / len(df) * 100).round(2))
    print()
    
    # 7. Valores únicos por coluna
    print("Valores únicos por coluna:")
    print(df.nunique())
    print()
    
    # 8. Frequências das colunas de texto
    for col in df.select_dtypes(include='object').columns:
        print(f"\nFrequências de '{col}':")
        print(df[col].value_counts().head(10))
    

Referências