# Onde paramos? O git log conta a historia ate aqui:
!cd ./projeto-vendas && git log --oneline
# E o estado atual? Espera-se uma area de trabalho limpa:
!cd ./projeto-vendas && git statusAula 2 - Introdução aos Dados Tabulares
Motivação
Na aula passada, montamos a bancada: criamos o projeto-vendas, demos memória a ele com o Git, isolamos o ambiente com o uv e registramos as dependências. Ficou faltando uma coisa, e não é uma coisa pequena: os dados. Um projeto chamado projeto-vendas sem uma única venda dentro é uma promessa não cumprida. Hoje nós cumprimos a promessa.
Antes, porém, uma pergunta que parece boba e não é: em que formato vivem os dados do mundo? A resposta, na esmagadora maioria dos casos, é uma só: em tabelas. A planilha do financeiro é uma tabela. O banco de dados do e-commerce guarda tabelas. O extrato do seu banco, o histórico escolar, o prontuário médico, o cadastro de clientes: tabelas, tabelas, tabelas. Estima-se que a maior parte do trabalho aplicado de ciência de dados aconteça sobre dados tabulares, e é por isso que a habilidade de ler, inspecionar e interrogar uma tabela com fluência é o alicerce de tudo o que vem depois: visualização, estatística, machine learning.
Mas há uma segunda razão para esta aula, menos óbvia e mais importante. Dados reais são sujos. Eles chegam com valores faltando, registros duplicados, números negativos onde não deveria haver, colunas com o tipo errado e surpresas que a documentação não conta. O iniciante descobre isso da pior forma: no meio de uma análise, quando o resultado dá errado e ele não sabe por quê. O profissional descobre isso de propósito, logo no início, com um ritual de inspeção que executa em todo dataset novo antes de confiar em qualquer número. Hoje você aprende esse ritual e o aplica em um dataset real, com mais de meio milhão de registros e todos os defeitos que o mundo real oferece.
O plano da aula: primeiro a teoria, para você saber nomear o que vai ver (o que é uma observação, uma variável, um tipo de dado); depois o pandas em escala pequena, com uma tabela de brinquedo em que tudo é visível; e só então o dataset real, o Online Retail, com as vendas de um ano inteiro de um varejo online. Teoria, brinquedo, mundo real, nessa ordem, porque cada etapa dá as ferramentas para sobreviver à seguinte.
Objetivos de aprendizagem
Ao final deste roteiro, você será capaz de:
- Definir dados tabulares e identificar seus elementos: observações (linhas), variáveis (colunas) e valores;
- Classificar variáveis por tipo (numéricas contínuas e discretas, categóricas nominais e ordinais, temporais) e explicar por que o tipo importa para a análise;
- Reconhecer os principais formatos de arquivo tabular (CSV, Excel, Parquet) e seus problemas típicos;
- Criar e manipular Series e DataFrames com o pandas;
- Carregar um dataset real e executar o ritual de inspeção inicial: head, shape, info, describe, value_counts e contagem de ausentes;
- Selecionar colunas e filtrar linhas por condições para responder perguntas de negócio;
- Diagnosticar problemas comuns de qualidade em dados reais, sem ainda tratá-los.
Este roteiro continua o projeto da Aula 1: você precisa do projeto-vendas criado, com o ambiente virtual e o Git funcionando. As células devem ser executadas na ordem. Ao final, três perguntas ajudam a consolidar o aprendizado: tente respondê-las antes de expandir as explicações.
1. Retomando o projeto
Um profissional não começa o dia de trabalho abrindo um arquivo solto: ele abre o projeto. Vamos praticar exatamente isso, reativando os hábitos da aula passada:
Se o log mostra os três commits da Aula 1 e o status diz que não há nada a commitar, o projeto está exatamente como o deixamos. Essa verificação de trinta segundos no início de cada sessão de trabalho evita sustos e é o primeiro reflexo profissional do dia.
O ambiente também precisa de um complemento. Hoje vamos ler um arquivo Excel, e o pandas precisa de uma biblioteca auxiliar para isso, a openpyxl. Como aprendemos: instalar no ambiente, registrar a versão, commitar a receita:
# Instalamos a nova dependencia dentro do ambiente do projeto
!cd ./projeto-vendas && uv pip install openpyxl
# Atualizamos a receita com as versoes exatas...
!cd ./projeto-vendas && uv pip freeze > requirements.txtChecked 1 package in 0.68ms
# ...e registramos a mudanca no historico. Ciclo completo.
!cd ./projeto-vendas && git add requirements.txt
!cd ./projeto-vendas && git commit -m "Adiciona openpyxl para leitura de arquivos Excel"Repare que o ciclo da Aula 1 já está virando rotina: mudou o ambiente, atualizou a receita, commitou. Agora sim, vamos à teoria.
2. O que são dados tabulares
Uma tabela é uma estrutura de dados com uma gramática precisa, e vale a pena nomear as partes, porque esse vocabulário será usado o curso inteiro:
- Cada linha é uma observação (ou registro, ou instância): uma unidade do fenômeno que estamos estudando. Em uma tabela de vendas, cada linha é uma venda; em uma tabela de alunos, cada linha é um aluno.
- Cada coluna é uma variável (ou atributo, ou campo): uma característica medida sobre todas as observações. Preço, data, nome do produto, país do cliente.
- Cada célula é um valor: a medida de uma variável para uma observação específica.
Essa organização, com uma variável por coluna, uma observação por linha e um valor por célula, tem nome na literatura: dados organizados, ou tidy data, no termo consagrado em inglês. Parece óbvio, mas o mundo real está cheio de tabelas que violam esses princípios: planilhas com anos espalhados em colunas, cabeçalhos mesclados, totais no meio dos dados. Reconhecer quando uma tabela está organizada, e quando não está, é uma habilidade em si, e voltaremos a ela quando aprendermos a reestruturar tabelas. Por ora, guarde o princípio.
Tipos de variáveis
Nem toda coluna é igual, e a diferença mais importante entre elas é o tipo da variável. O tipo determina o que faz sentido fazer com ela: que estatística calcular, que gráfico desenhar, que modelo aplicar.
| Tipo | Definição | Exemplos | O que faz sentido |
|---|---|---|---|
| Numérica contínua | Números que podem assumir qualquer valor em um intervalo | Preço, altura, temperatura | Média, mediana, soma, histograma |
| Numérica discreta | Números inteiros de contagem | Quantidade de itens, número de filhos | Média, soma, contagem |
| Categórica nominal | Categorias sem ordem natural | País, cor, nome do produto | Contagem, moda, proporção |
| Categórica ordinal | Categorias com ordem natural | Escolaridade, avaliação (ruim, boa, ótima) | Contagem, mediana, comparação de ordem |
| Temporal | Datas e horários | Data da venda, timestamp | Ordenação, extração de mês e ano, séries temporais |
Duas armadilhas clássicas merecem aviso prévio. A primeira: nem tudo que parece número é numérico. Um CEP, um CPF, um código de produto são compostos de dígitos, mas são categorias: somar dois CEPs não significa nada, e calcular a média de códigos de cliente é um absurdo estatístico. A pergunta certa não é “isso tem dígitos?”, e sim “faz sentido fazer aritmética com isso?”. A segunda: uma nota de 1 a 5 parece numérica, mas é ordinal: a distância entre 1 e 2 não é necessariamente igual à distância entre 4 e 5. Tratá-la como número puro é uma simplificação que às vezes é aceitável e às vezes distorce a análise; o importante é fazer isso conscientemente, não por acidente. Guarde essas duas armadilhas: as duas vão aparecer, na prática, ainda nesta aula.
Formatos de arquivo
Tabelas viajam entre sistemas dentro de arquivos, e os formatos mais comuns têm personalidades diferentes:
- CSV (comma-separated values): texto puro com um separador entre os valores. Universal, legível, versionável, mas frágil: não guarda tipos (tudo é texto até prova em contrário) e sofre com um problema particularmente brasileiro. Em países que usam vírgula como separador decimal, como o Brasil, o CSV frequentemente usa ponto e vírgula como separador de campos, e o pandas precisa ser avisado disso (parâmetros sep e decimal do read_csv). Um CSV brasileiro lido com as configurações padrão americanas vira uma coluna única ilegível, e esse erro recebe todo iniciante de braços abertos.
- Excel (xlsx): o formato das planilhas, onipresente no mundo corporativo. Guarda tipos, múltiplas abas e formatação, mas é pesado e lento de ler em arquivos grandes, além de permitir as travessuras que quebram o princípio tidy: células mescladas, totais no meio, cores com significado.
- Parquet: formato binário colunar, padrão da engenharia de dados moderna. Rápido, compacto e preserva tipos, mas não é legível por humanos. Vamos usá-lo mais adiante no curso.
A regra prática: receba os dados no formato que vierem, mas conheça o custo de cada um. Hoje receberemos um Excel, e você vai sentir o custo na pele: o arquivo demora a abrir.
3. Pandas em escala pequena: Series e DataFrame
O pandas é a biblioteca que traz as tabelas para dentro do Python, e suas duas estruturas centrais têm nomes que você usará todos os dias: a Series, que é uma coluna (uma sequência de valores com um índice), e o DataFrame, que é a tabela inteira (um conjunto de Series que compartilham o mesmo índice).
Antes de enfrentar meio milhão de linhas, vamos construir uma tabela de brinquedo em que cada valor é visível a olho nu. É uma estratégia deliberada: em escala pequena, você confere cada resultado manualmente e constrói confiança no que cada comando faz.
A partir daqui, abra um notebook novo na pasta notebooks/ do projeto (sugestão de nome: 01-introducao-tabulares.ipynb) e acompanhe executando as células. Note que as células a seguir são Python: saímos do terminal e entramos na linguagem.
import pandas as pd
sales = pd.DataFrame(
{
"product": ["Caneca", "Camiseta", "Caneca", "Adesivo", "Camiseta"],
"quantity": [2, 1, 4, 10, 3],
"unit_price": [35.0, 79.9, 35.0, 5.5, 79.9],
"country": ["Brasil", "Brasil", "Portugal", "Brasil", "Argentina"],
}
)
sales| product | quantity | unit_price | country | |
|---|---|---|---|---|
| 0 | Caneca | 2 | 35.0 | Brasil |
| 1 | Camiseta | 1 | 79.9 | Brasil |
| 2 | Caneca | 4 | 35.0 | Portugal |
| 3 | Adesivo | 10 | 5.5 | Brasil |
| 4 | Camiseta | 3 | 79.9 | Argentina |
Cada linha é uma observação (uma venda), cada coluna é uma variável, e a coluna sem nome à esquerda é o índice, o identificador de cada linha, que o pandas criou automaticamente. Exercite a classificação da seção anterior: product e country são categóricas nominais, quantity é numérica discreta, unit_price é numérica contínua.
As três operações fundamentais, em miniatura:
sales["unit_price"]0 35.0
1 79.9
2 35.0
3 5.5
4 79.9
Name: unit_price, dtype: float64
sales["revenue"] = sales["quantity"] * sales["unit_price"]
sales| product | quantity | unit_price | country | revenue | |
|---|---|---|---|---|---|
| 0 | Caneca | 2 | 35.0 | Brasil | 70.0 |
| 1 | Camiseta | 1 | 79.9 | Brasil | 79.9 |
| 2 | Caneca | 4 | 35.0 | Portugal | 140.0 |
| 3 | Adesivo | 10 | 5.5 | Brasil | 55.0 |
| 4 | Camiseta | 3 | 79.9 | Argentina | 239.7 |
brazil_filter = sales["country"].eq("Brasil")
sales.loc[brazil_filter]| product | quantity | unit_price | country | revenue | |
|---|---|---|---|---|---|
| 0 | Caneca | 2 | 35.0 | Brasil | 70.0 |
| 1 | Camiseta | 1 | 79.9 | Brasil | 79.9 |
| 3 | Adesivo | 10 | 5.5 | Brasil | 55.0 |
O filtro da terceira célula merece um comentário, porque ele é uma das operações mais importantes do pandas. A expressão sales["country"].eq("Brasil") produz uma Series de True e False, um valor por linha. Em seguida, .loc[brazil_filter] devolve somente as linhas marcadas com True. Separar a condição em uma variável dá nome ao critério e facilita sua inspeção e reutilização. Condições podem ser combinadas com o operador & (e) e o operador | (ou):
high_revenue_filter = sales["revenue"].gt(50)
sales.loc[brazil_filter & high_revenue_filter]| product | quantity | unit_price | country | revenue | |
|---|---|---|---|---|---|
| 0 | Caneca | 2 | 35.0 | Brasil | 70.0 |
| 1 | Camiseta | 1 | 79.9 | Brasil | 79.9 |
| 3 | Adesivo | 10 | 5.5 | Brasil | 55.0 |
Um aviso que economiza horas de frustração: dentro de filtros do pandas, use & e |, e não as palavras and e or do Python. As palavras funcionam para valores únicos, mas quebram com Series inteiras, e a mensagem de erro que aparece não ajuda em nada. Todo mundo tropeça nisso uma vez; que a sua vez seja agora, em ambiente controlado.
Por fim, o resumo estatístico:
sales.describe().round(2)| quantity | unit_price | revenue | |
|---|---|---|---|
| count | 5.00 | 5.00 | 5.00 |
| mean | 4.00 | 47.06 | 116.92 |
| std | 3.54 | 32.31 | 75.85 |
| min | 1.00 | 5.50 | 55.00 |
| 25% | 2.00 | 35.00 | 70.00 |
| 50% | 3.00 | 35.00 | 79.90 |
| 75% | 4.00 | 79.90 | 140.00 |
| max | 10.00 | 79.90 | 239.70 |
Cinco linhas dão para conferir de cabeça: a média das quantidades bate? O mínimo e o máximo dos preços fazem sentido? Essa conferência manual é impossível com meio milhão de linhas, e é exatamente por isso que o describe existe: ele é o olhar panorâmico quando os olhos não alcançam mais os dados. Hora de testá-lo em escala real.
4. O dataset: Online Retail
Chegou o momento. O dataset que acompanhará as próximas aulas é o Online Retail, mantido pelo repositório de aprendizado de máquina da Universidade da Califórnia em Irvine (UCI), um dos acervos de datasets mais tradicionais da área. Ele contém todas as transações de um varejo online do Reino Unido entre dezembro de 2010 e dezembro de 2011: uma empresa que vende presentes e artigos variados, com muitos clientes atacadistas. São 541.909 registros, e cada linha é um item vendido dentro de uma nota fiscal.
O dicionário de variáveis, segundo a documentação oficial:
| Variável | Descrição |
|---|---|
| invoice_no | Número da nota fiscal, com 6 dígitos. Se começa com a letra C, indica um cancelamento |
| stock_code | Código do produto |
| description | Nome do produto |
| quantity | Quantidade do item na transação |
| invoice_date | Data e hora da transação |
| unit_price | Preço unitário, em libras esterlinas |
| customer_id | Código do cliente, com 5 dígitos |
| country | País de residência do cliente |
Exercite mais uma vez a classificação de tipos, agora com atenção às armadilhas da seção 2: invoice_no, stock_code e customer_id são compostos de dígitos, mas são identificadores, ou seja, categóricos nominais; quantity é numérica discreta; unit_price é numérica contínua; invoice_date é temporal; description e country são categóricas nominais.
Dois registros de responsabilidade antes de baixar. Primeiro, a licença: o dataset é distribuído sob Creative Commons Attribution 4.0, que permite uso e adaptação desde que a fonte seja creditada, e a citação oficial é Chen, D. (2015), Online Retail, UCI Machine Learning Repository. Usar dados alheios com licença verificada e crédito dado é prática profissional básica, e vamos registrar isso no README do projeto. Segundo, um detalhe que guarde para mais tarde: a página oficial do dataset afirma que ele não possui valores ausentes. Guarde essa informação; ela voltará ainda nesta aula.
Baixando os dados para o lugar certo
O arquivo vem compactado e tem cerca de 23 MB. E para onde ele vai? Para a pasta data/, criada justamente para isso na Aula 1. Voltamos ao terminal por um instante:
# Baixamos o arquivo compactado para a pasta data/ do projeto
!cd ./projeto-vendas && curl -L -o data/online_retail.zip "https://archive.ics.uci.edu/static/public/352/online+retail.zip"
# Descompactamos e renomeamos para um nome sem espacos
!cd ./projeto-vendas && unzip -o data/online_retail.zip -d data/
!cd ./projeto-vendas && mv "data/Online Retail.xlsx" data/online_retail.xlsx % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0100 123k 0 123k 0 0 61870 0 --:--:-- 0:00:02 --:--:-- 61897100 347k 0 347k 0 0 116k 0 --:--:-- 0:00:02 --:--:-- 116k100 987k 0 987k 0 0 240k 0 --:--:-- 0:00:04 --:--:-- 240k100 2775k 0 2775k 0 0 540k 0 --:--:-- 0:00:05 --:--:-- 540k100 3839k 0 3839k 0 0 643k 0 --:--:-- 0:00:05 --:--:-- 762k100 4695k 0 4695k 0 0 675k 0 --:--:-- 0:00:06 --:--:-- 933k100 5555k 0 5555k 0 0 690k 0 --:--:-- 0:00:08 --:--:-- 1030k100 6346k 0 6346k 0 0 699k 0 --:--:-- 0:00:09 --:--:-- 1076k100 6842k 0 6842k 0 0 689k 0 --:--:-- 0:00:09 --:--:-- 848k100 7190k 0 7190k 0 0 652k 0 --:--:-- 0:00:11 --:--:-- 662k100 7558k 0 7558k 0 0 629k 0 --:--:-- 0:00:12 --:--:-- 565k100 7954k 0 7954k 0 0 611k 0 --:--:-- 0:00:13 --:--:-- 484k100 8382k 0 8382k 0 0 598k 0 --:--:-- 0:00:13 --:--:-- 414k100 8850k 0 8850k 0 0 590k 0 --:--:-- 0:00:14 --:--:-- 396k100 9354k 0 9354k 0 0 585k 0 --:--:-- 0:00:15 --:--:-- 436k100 9970k 0 9970k 0 0 587k 0 --:--:-- 0:00:16 --:--:-- 486k100 10.5M 0 10.5M 0 0 602k 0 --:--:-- 0:00:17 --:--:-- 578k100 11.7M 0 11.7M 0 0 633k 0 --:--:-- 0:00:18 --:--:-- 730k100 13.3M 0 13.3M 0 0 685k 0 --:--:-- 0:00:19 --:--:-- 972k100 15.6M 0 15.6M 0 0 763k 0 --:--:-- 0:00:20 --:--:-- 1339k100 18.6M 0 18.6M 0 0 870k 0 --:--:-- 0:00:21 --:--:-- 1837k100 22.5M 0 22.5M 0 0 1007k 0 --:--:-- 0:00:22 --:--:-- 2463k100 22.6M 0 22.6M 0 0 1009k 0 --:--:-- 0:00:22 --:--:-- 2790k
Archive: data/online_retail.zip
extracting: data/Online Retail.xlsx
Conferindo:
!cd ./projeto-vendas && ls -lh data/total 46M
-rwx------ 1 oandrefonseca oandrefonseca 23M May 22 2023 online_retail.xlsx
-rw-rw-r-- 1 oandrefonseca oandrefonseca 23M Aug 31 09:50 online_retail.zip
E agora, uma pergunta de aluno atento: os dados entraram no projeto; não deveríamos commitar? Consulte o Git:
!cd ./projeto-vendas && git statusNada a commitar. O Git nem enxerga os arquivos novos, porque a pasta data/ está no .gitignore que escrevemos na Aula 1, e a decisão daquele dia acaba de se provar correta: 23 MB de dados brutos não pertencem ao histórico de código. Mas isso cria uma responsabilidade nova, e ela vai reaparecer nas perguntas do final da aula: se os dados não viajam com o repositório, o repositório precisa ensinar como obtê-los. Vamos cumprir essa responsabilidade agora, documentando a origem no README:
# Documentamos a fonte dos dados e as instrucoes de obtencao no README
!cd ./projeto-vendas && printf "\n## Dados\n\nDataset Online Retail (UCI Machine Learning Repository, id 352).\nLicenca CC BY 4.0. Citacao: Chen, D. (2015). Online Retail. UCI.\nDownload: https://archive.ics.uci.edu/dataset/352/online+retail\nApos baixar, descompacte em data/ e renomeie para online_retail.xlsx\n" >> README.md# Ciclo de sempre: add, commit
!cd ./projeto-vendas && git add README.md
!cd ./projeto-vendas && git commit -m "Documenta a fonte e as instrucoes de download dos dados"5. O ritual de inspeção inicial
De volta ao notebook e ao Python. Vamos abrir o arquivo, com um aviso de gerenciamento de expectativa: ler meio milhão de linhas de um Excel é lento, e a célula abaixo pode levar um ou dois minutos. Sinta o custo do formato; é proposital.
from pathlib import Path
import pandas as pd
data_path = Path("projeto-vendas") / "data" / "online_retail.xlsx"
column_names = {
"InvoiceNo": "invoice_no",
"StockCode": "stock_code",
"Description": "description",
"Quantity": "quantity",
"InvoiceDate": "invoice_date",
"UnitPrice": "unit_price",
"CustomerID": "customer_id",
"Country": "country",
}
1retail = pd.read_excel(data_path).rename(columns=column_names)- 1
-
A renomeação acontece na leitura para que todo o restante da análise use a convenção Python
snake_case, sem alterar o arquivo original.
A partir de agora, execute o ritual que um profissional aplica a todo dataset novo, na ordem, antes de confiar em qualquer número. São cinco passos.
Passo 1: olhe os dados de verdade.
retail.head()| invoice_no | stock_code | description | quantity | invoice_date | unit_price | customer_id | country | |
|---|---|---|---|---|---|---|---|---|
| 0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 | 17850.0 | United Kingdom |
| 1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom |
| 2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 | 17850.0 | United Kingdom |
| 3 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom |
| 4 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom |
retail.tail()| invoice_no | stock_code | description | quantity | invoice_date | unit_price | customer_id | country | |
|---|---|---|---|---|---|---|---|---|
| 541904 | 581587 | 22613 | PACK OF 20 SPACEBOY NAPKINS | 12 | 2011-12-09 12:50:00 | 0.85 | 12680.0 | France |
| 541905 | 581587 | 22899 | CHILDREN'S APRON DOLLY GIRL | 6 | 2011-12-09 12:50:00 | 2.10 | 12680.0 | France |
| 541906 | 581587 | 23254 | CHILDRENS CUTLERY DOLLY GIRL | 4 | 2011-12-09 12:50:00 | 4.15 | 12680.0 | France |
| 541907 | 581587 | 23255 | CHILDRENS CUTLERY CIRCUS PARADE | 4 | 2011-12-09 12:50:00 | 4.15 | 12680.0 | France |
| 541908 | 581587 | 22138 | BAKING SET 9 PIECE RETROSPOT | 3 | 2011-12-09 12:50:00 | 4.95 | 12680.0 | France |
Passo 2: meça o tamanho e confira os tipos.
retail.shape(541909, 8)
retail.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 invoice_no 541909 non-null object
1 stock_code 541909 non-null object
2 description 540455 non-null object
3 quantity 541909 non-null int64
4 invoice_date 541909 non-null datetime64[ns]
5 unit_price 541909 non-null float64
6 customer_id 406829 non-null float64
7 country 541909 non-null object
dtypes: datetime64[ns](1), float64(2), int64(1), object(4)
memory usage: 33.1+ MB
Leia a saída do info com olhos de detetive, porque ela já entrega dois achados. Primeiro, os tipos que o pandas adivinhou: invoice_date virou datetime (ótimo), quantity é inteiro, unit_price é float. Mas observe customer_id: virou float64. Um código de cliente com casas decimais? Estranho, e não é acidente: guarde para a pergunta 1 do final. Segundo, compare a contagem de valores não nulos entre as colunas: se o total de linhas é 541.909 e alguma coluna tem menos que isso, há valores ausentes nela, diga o que disser a documentação.
Passo 3: quantifique os ausentes explicitamente.
retail.isna().sum().sort_values(ascending=False)customer_id 135080
description 1454
stock_code 0
invoice_no 0
quantity 0
invoice_date 0
unit_price 0
country 0
dtype: int64
Lembra da página oficial afirmando que o dataset não tem valores ausentes? Os números na sua tela dizem outra coisa: a coluna customer_id tem dezenas de milhares de vazios, e description também tem os seus. Este é um dos aprendizados mais valiosos da aula, e ele não é sobre pandas: metadados mentem, às vezes por desatualização, às vezes por descuido. A única fonte confiável sobre os dados são os próprios dados, verificados por você.
Passo 4: resuma as variáveis numéricas.
retail.describe()| quantity | invoice_date | unit_price | customer_id | |
|---|---|---|---|---|
| count | 541909.000000 | 541909 | 541909.000000 | 406829.000000 |
| mean | 9.552250 | 2011-07-04 13:34:57.156386048 | 4.611114 | 15287.690570 |
| min | -80995.000000 | 2010-12-01 08:26:00 | -11062.060000 | 12346.000000 |
| 25% | 1.000000 | 2011-03-28 11:34:00 | 1.250000 | 13953.000000 |
| 50% | 3.000000 | 2011-07-19 17:17:00 | 2.080000 | 15152.000000 |
| 75% | 10.000000 | 2011-10-19 11:27:00 | 4.130000 | 16791.000000 |
| max | 80995.000000 | 2011-12-09 12:50:00 | 38970.000000 | 18287.000000 |
| std | 218.081158 | NaN | 96.759853 | 1713.600303 |
Não deslize os olhos pela tabela: interrogue-a. O mínimo de quantity é um número negativo enorme. Quantidade negativa em uma venda? O mínimo de unit_price é zero, e itens gratuitos em um varejo também levantam a sobrancelha. O máximo de quantity, na casa das dezenas de milhares em uma única linha, sugere ou atacado pesado ou erro de digitação. O describe não responde nada disso; o trabalho dele é apontar onde cavar.
Passo 5: conte as categorias.
retail["country"].value_counts().head(10)country
United Kingdom 495478
Germany 9495
France 8557
EIRE 8196
Spain 2533
Netherlands 2371
Belgium 2069
Switzerland 2002
Portugal 1519
Australia 1259
Name: count, dtype: int64
retail["country"].nunique()38
O value_counts é o describe das variáveis categóricas: mostra a distribuição e denuncia problemas como categorias duplicadas com grafias diferentes. Aqui, como esperado de um varejo britânico, o Reino Unido domina com folga.
Ritual completo. Em cinco passos, sem nenhuma análise sofisticada, você já sabe o tamanho, os tipos, os ausentes, as faixas de valores e as categorias dominantes, e já tem uma lista de suspeitas para investigar. É mais do que muita análise apressada descobre em uma semana.
6. Interrogando os dados: seleção e filtragem
Inspeção feita, começam as perguntas de negócio. As ferramentas são as mesmas da tabela de brinquedo, agora em escala real.
germany_filter = retail["country"].eq("Germany")
germany_sales = retail.loc[germany_filter].copy()
germany_sales.shape(9495, 8)
expensive_outside_uk_filter = (
retail["unit_price"].gt(100)
& retail["country"].ne("United Kingdom")
)
result_columns = ["invoice_no", "description", "unit_price", "country"]
expensive_outside_uk = retail.loc[
expensive_outside_uk_filter,
result_columns,
].copy()
expensive_outside_uk.head()| invoice_no | description | unit_price | country | |
|---|---|---|---|---|
| 39777 | 539688 | CARRIAGE | 150.0 | France |
| 45622 | C540271 | Manual | 1126.0 | Spain |
| 54382 | 540946 | VINTAGE BLUE KITCHEN CABINET | 125.0 | Cyprus |
| 54395 | 540946 | VINTAGE RED KITCHEN CABINET | 125.0 | Cyprus |
| 70795 | 542102 | VINTAGE RED KITCHEN CABINET | 125.0 | Singapore |
A segunda célula mostra um padrão que você usará constantemente: construir uma máscara booleana com um nome descritivo e passá-la ao .loc, selecionando linhas e colunas explicitamente. O resultado é uma resposta enxuta, e não uma parede de colunas.
Para seleção posicional e por rótulo, o pandas oferece dois acessores que valem apresentar desde já: o loc seleciona por rótulos e condições (retail.loc[retail[“country”] == “France”, [“invoice_no”, “unit_price”]]), e o iloc seleciona por posições numéricas (retail.iloc[0:5, 0:3] pega as cinco primeiras linhas e as três primeiras colunas). A distinção fica mais importante nas próximas aulas; hoje, basta saber que existem e o que cada um faz.
Agora, três perguntas de negócio para você responder sozinho, no notebook, antes de seguir. Elas usam apenas o que já foi visto:
- Quantas transações registram o país como Brazil? (Descubra: o Brasil aparece neste dataset?)
- Quantas linhas têm quantity negativa? E qual a cara delas? (Filtre e examine com head.)
- Existe alguma linha com unit_price igual a zero e customer_id ausente ao mesmo tempo? (Dica: combine um filtro de igualdade com o método isna() da coluna.)
A segunda pergunta é a ponte para a seção final.
7. A caça aos problemas: diagnóstico de qualidade
Todo achado estranho da inspeção merece agora um exame direto. Importante: nesta aula, apenas diagnosticamos; decidir o que fazer com cada problema (remover? corrigir? preencher?) é o tema da próxima aula, e é uma decisão de análise, não um reflexo automático.
Suspeita 1: as quantidades negativas.
negative_quantity_filter = retail["quantity"].lt(0)
negative_quantity = retail.loc[negative_quantity_filter].copy()
print(f"{len(negative_quantity):,} linhas com quantidade negativa")10,624 linhas com quantidade negativa
Como elas são:
negative_quantity.head()| invoice_no | stock_code | description | quantity | invoice_date | unit_price | customer_id | country | |
|---|---|---|---|---|---|---|---|---|
| 141 | C536379 | D | Discount | -1 | 2010-12-01 09:41:00 | 27.50 | 14527.0 | United Kingdom |
| 154 | C536383 | 35004C | SET OF 3 COLOURED FLYING DUCKS | -1 | 2010-12-01 09:49:00 | 4.65 | 15311.0 | United Kingdom |
| 235 | C536391 | 22556 | PLASTERS IN TIN CIRCUS PARADE | -12 | 2010-12-01 10:24:00 | 1.65 | 17548.0 | United Kingdom |
| 236 | C536391 | 21984 | PACK OF 12 PINK PAISLEY TISSUES | -24 | 2010-12-01 10:24:00 | 0.29 | 17548.0 | United Kingdom |
| 237 | C536391 | 21983 | PACK OF 12 BLUE PAISLEY TISSUES | -24 | 2010-12-01 10:24:00 | 0.29 | 17548.0 | United Kingdom |
Olhe a coluna invoice_no das linhas exibidas: muitas notas começam com a letra C. O dicionário de variáveis explica que esse prefixo indica cancelamento. A quantidade negativa pode representar uma devolução ou um ajuste, mas o prefixo não aparece em todos esses registros; por isso, o código abaixo mede a relação em vez de presumir que as categorias são equivalentes. Eis uma lição central: um valor estranho nem sempre é um defeito; às vezes é uma regra de negócio que você ainda não conhecia. Antes de apagar qualquer coisa, entenda o que ela significa.
negative_invoices = retail.loc[negative_quantity_filter, "invoice_no"]starts_with_c = negative_invoices.str.startswith("C", na=False)
print(f"Começam com C: {starts_with_c.sum():,}")
print(f"Total de negativas: {len(negative_invoices):,}")Começam com C: 9,288
Total de negativas: 10,624
Suspeita 2: os preços zerados.
zero_price_filter = retail["unit_price"].eq(0)
zero_price = retail.loc[zero_price_filter].copy()
print(f"{len(zero_price):,} linhas com preço zero")2,515 linhas com preço zero
zero_price.loc[
:, ["invoice_no", "description", "quantity", "customer_id"]
].head()| invoice_no | description | quantity | customer_id | |
|---|---|---|---|---|
| 622 | 536414 | NaN | 56 | NaN |
| 1970 | 536545 | NaN | 1 | NaN |
| 1971 | 536546 | NaN | 1 | NaN |
| 1972 | 536547 | NaN | 1 | NaN |
| 1987 | 536549 | NaN | 1 | NaN |
Suspeita 3: registros duplicados.
duplicate_count = retail.duplicated().sum()
print(f"{duplicate_count:,} linhas duplicadas")5,268 linhas duplicadas
Duplicatas em dados transacionais podem ser erro de sistema ou podem ser legítimas (o mesmo cliente comprando o mesmo item duas vezes no mesmo minuto é raro, mas não impossível). De novo: diagnóstico agora, decisão depois.
Feche a sessão de trabalho como um profissional: salve o notebook e registre-o no histórico.
# De volta ao terminal, o commit que encerra a sessao:
!cd ./projeto-vendas && git add notebooks/
!cd ./projeto-vendas && git commit -m "Adiciona inspecao inicial e diagnostico do Online Retail"
!cd ./projeto-vendas && git log --onelineLeia o log completo: seis commits, e cada um narra uma etapa real do projeto, das duas aulas. O repositório está contando a história direitinho.
Recapitulando o que você conquistou:
- Nomeou a gramática das tabelas (observações, variáveis, valores) e o princípio dos dados organizados;
- Classificou variáveis por tipo e conheceu as duas armadilhas clássicas: dígitos que não são números e ordinais disfarçadas de numéricas;
- Comparou os formatos CSV, Excel e Parquet, incluindo o problema do CSV brasileiro;
- Operou Series e DataFrames em escala pequena: seleção, coluna derivada, filtro booleano e describe;
- Baixou um dataset real para a pasta certa, documentou a fonte e a licença no README e viu o .gitignore da Aula 1 trabalhar a seu favor;
- Executou o ritual de inspeção em cinco passos e flagrou a documentação oficial em contradição com os dados;
- Diagnosticou quantidades negativas (e descobriu que eram cancelamentos), preços zerados e duplicatas, sem sair apagando nada.
Perguntas para consolidar
Três perguntas para exercitar o que foi aprendido. Tente responder cada uma antes de expandir a explicação.
Pergunta 1: o cliente com casas decimais
O info revelou que a coluna customer_id foi carregada como float64, ou seja, número com casas decimais, e você viu valores como 17850.0. Um código de cliente não deveria ser um inteiro, ou melhor, uma categoria? Explique por que o pandas tomou essa decisão e o que ela revela sobre a relação entre valores ausentes e tipos de dados. (Dica: qual coluna tinha dezenas de milhares de ausentes?)
A causa é a combinação de dois fatos. Primeiro, a coluna customer_id tem milhares de valores ausentes, como o isna().sum() mostrou. Segundo, o tipo inteiro clássico do pandas (int64) não comporta o marcador de valor ausente NaN, que é, tecnicamente, um número de ponto flutuante. Diante de uma coluna de números que precisa acomodar NaN, o pandas promove tudo para float64, e é assim que 17850 vira 17850.0.
O aprendizado tem duas camadas. A técnica: um tipo inesperado em uma coluna é frequentemente um sintoma de outro problema, e aqui o float estranho era a impressão digital dos ausentes. Quando o info mostrar um tipo que não faz sentido, pergunte-se o que o forçou. A conceitual: como discutimos na teoria, customer_id nem deveria ser tratado como número, pois é um identificador, uma variável categórica nominal composta de dígitos. Fazer média de códigos de cliente não significa nada. Na próxima aula, ao tratar os dados, uma das decisões será justamente converter essa coluna para um tipo adequado.
Pergunta 2: a documentação contra os dados
A página oficial do dataset na UCI afirma que ele não possui valores ausentes, mas o seu isna().sum() encontrou dezenas de milhares de vazios em customer_id e milhares em description. Diante de uma contradição entre a documentação e os dados, qual dos dois vence, e que hábito profissional essa experiência deve deixar em você?
Os dados vencem, sempre. A documentação é um relato sobre os dados, escrito por alguém, em algum momento, com algum grau de cuidado; os dados são o fato. Quando os dois divergem, a documentação está desatualizada, incompleta ou simplesmente errada, e qualquer análise construída sobre a afirmação falsa herda o erro.
O hábito profissional é o ceticismo verificador: leia toda a documentação disponível (ela é valiosa, como o dicionário de variáveis provou ao explicar os cancelamentos), mas trate cada afirmação verificável como uma hipótese a testar, não como um fato estabelecido. O ritual de inspeção da aula é exatamente esse teste: barato, rápido e sistemático. Note a assimetria: verificar custou uma linha de código; confiar cegamente custaria uma análise inteira baseada na premissa de que todas as vendas têm cliente identificado, o que é falso para cerca de um quarto do dataset.
E há uma lição de espelho: um dia você será a pessoa que escreve a documentação. O README que atualizamos hoje é o começo dessa responsabilidade, e mantê-lo fiel aos dados é o mesmo dever, visto do outro lado.
Pergunta 3: o repositório sem os dados
Sua colega clonou o projeto-vendas para colaborar na análise. Ela recebeu o código, o requirements.txt e o README, mas a pasta data/ chegou vazia, pois está no .gitignore. Descreva o caminho completo que ela deve percorrer para reproduzir o seu ambiente de trabalho, incluindo os dados, e explique que papel o README atualizado nesta aula cumpre nesse desenho. O projeto continua reprodutível mesmo sem os dados viajarem no repositório?
O caminho dela reúne as duas aulas do curso. Primeiro, a fundação da Aula 1: entrar na pasta do projeto clonado, criar o ambiente com uv venv e instalar as dependências exatas com uv pip install -r requirements.txt, o que já inclui o pandas e a openpyxl registrados hoje. Depois, a novidade desta aula: seguir as instruções da seção Dados do README, baixar o arquivo da UCI, descompactar em data/ e renomear para online_retail.xlsx. A partir daí, os notebooks rodam identicamente nas duas máquinas.
O README cumpre o papel de receita dos dados, exatamente como o requirements.txt é a receita do ambiente. O padrão é o mesmo da Aula 1: o repositório não transporta artefatos pesados ou recriáveis (nem a cozinha, nem os ingredientes brutos), transporta as receitas que permitem reconstruir tudo, e receitas são leves, legíveis e versionáveis. Sim, o projeto continua reprodutível, porque reprodutibilidade não significa embalar tudo junto; significa garantir que qualquer pessoa reconstrua o todo a partir do que o repositório ensina.
Em projetos maduros, essa receita manual evolui: um script de download automatizado, ou ferramentas de versionamento de dados como o DVC, que registram inclusive qual versão exata dos dados foi usada em cada análise. O princípio, porém, você já domina: código e receitas no Git, artefatos reconstruíveis fora dele.
Para casa
- Responda, no seu notebook, às três perguntas de negócio da seção 6, com um filtro para cada uma e uma frase de interpretação em uma célula de texto. Commite o notebook ao terminar.
- Produza a ficha de reconhecimento do dataset: uma célula de texto no notebook resumindo, com suas palavras, o tamanho, os tipos de cada variável (com a classificação da seção 2), os ausentes e os três problemas de qualidade diagnosticados. Essa ficha será o ponto de partida da próxima aula.
- Explore por conta própria: escolha uma pergunta sobre o dataset que desperte a sua curiosidade (um produto, um país, um período) e tente respondê-la só com filtros e seleções. Traga o achado para discutirmos em aula.
Dados reais não são sujos por acidente: eles são o retrato fiel de processos humanos imperfeitos. Limpá-los sem entendê-los é apagar a história que eles contam.
Até a próxima aula, quando deixaremos de apenas diagnosticar e começaremos a tratar: valores ausentes, tipos, duplicatas e as decisões de limpeza que toda análise honesta precisa documentar.