Capítulo 22, Intermediário
Reformatar: pivot, melt e crosstab
A mesma informação pode estar em formato **longo** (uma linha por observação) ou **largo** (uma coluna por categoria). Quem faz relatório quer o largo, e quem analisa e desenha gráficos quer o longo. Aprender a ir e voltar é o que este capítulo ensina.
O ponto de partida: as vendas, em uma tabela
Daqui até o fim do nível, o modelo de vendas aparece já junto: pedidos, mais produtos (para o preço), mais clientes (para o segmento). A função reúne o que o capítulo 21 mostrou, e nunca descarta pedido:
import numpy as np
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
def carregar_vendas() -> pd.DataFrame:
clientes = pd.read_csv("dados/clientes.csv")
produtos = pd.read_csv("dados/produtos.csv")
pedidos = pd.read_csv("dados/pedidos.csv", parse_dates=["data_pedido"])
v = pedidos.merge(produtos, on="id_produto", how="left", validate="m:1")
v = v.merge(clientes[["id_cliente", "segmento"]], on="id_cliente", how="left", validate="m:1")
v["segmento"] = v["segmento"].fillna("Sem cadastro")
v["receita"] = v["quantidade"] * v["preco_unitario"] * (1 - v["desconto"].fillna(0))
v["mes"] = v["data_pedido"].dt.month
return v
vendas = carregar_vendas()
print(vendas.shape, round(float(vendas["receita"].sum()), 2))
(800, 13) 970851.1
`pivot_table`: agregar e abrir em colunas
O pivot_table faz o que uma tabela dinâmica do Excel faz: agrupa por uma ou mais chaves nas linhas, abre outra chave em colunas e agrega os valores. O margins=True acrescenta os totais:
tabela = vendas.pivot_table(
index="categoria", columns="canal", values="receita",
aggfunc="sum", margins=True, margins_name="Total",
)
print(tabela.round(0))
print(bool(np.isclose(tabela.loc["Total", "Total"], vendas["receita"].sum())))
canal app loja site Total
categoria
Casa 53988.0 77803.0 105830.0 237621.0
Esporte 51362.0 95554.0 124000.0 270915.0
Informática 43765.0 79425.0 124154.0 247345.0
Papelaria 43095.0 69274.0 102601.0 214970.0
Total 192210.0 322056.0 456585.0 970851.0
True
O total no canto fecha com a soma de toda a receita (o True), que é a conferência de sempre. Para mais de uma agregação, passe uma lista, e as colunas ganham um nível a mais:
print(vendas.pivot_table(index="categoria", values="receita", aggfunc=["sum", "count"]).round(0))
sum count
receita receita
categoria
Casa 237621.0 205
Esporte 270915.0 206
Informática 247345.0 196
Papelaria 214970.0 193
`pivot`: reorganizar, sem agregar
O pivot só reorganiza: não agrega nada, e por isso exige que cada par (linha, coluna) apareça uma vez. Antes dele, eu agrego com o groupby para garantir isso:
por_mes_e_canal = vendas.groupby(["mes", "canal"])["receita"].sum().reset_index()
largo = por_mes_e_canal.pivot(index="mes", columns="canal", values="receita")
print(largo.round(0).head(3))
canal app loja site
mes
1 16034.0 38612.0 27511.0
2 11320.0 23562.0 28582.0
3 13786.0 22658.0 56152.0
Sem a agregação prévia, o pivot falha, porque há muitos pedidos para o mesmo par categoria e canal:
try:
vendas.pivot(index="categoria", columns="canal", values="quantidade")
except ValueError as erro:
print("ValueError:", erro)
ValueError: Index contains duplicate entries, cannot reshape
A regra para escolher: se há repetições a combinar, use pivot_table (que agrega). Se cada par já é único, o pivot basta.
`melt`: do largo para o longo
O melt faz o caminho de volta: transforma colunas em linhas. É o formato que bibliotecas de gráficos e o groupby preferem:
longo = largo.reset_index().melt(id_vars="mes", var_name="canal", value_name="receita")
print(longo.shape, round(float(longo["receita"].sum()), 2) == round(float(largo.sum().sum()), 2))
print(longo.head(3).round(1))
(36, 3) True
mes canal receita
0 1 app 16033.6
1 2 app 11320.5
2 3 app 13786.3
São 12 meses por 3 canais, 36 linhas, e a receita total não mudou: reformatar não cria nem perde informação.
`crosstab`: contar combinações
O crosstab conta quantas vezes cada combinação ocorre, sem precisar de uma coluna de valores. O normalize converte a contagem em proporção, por linha, por coluna ou do total:
print(pd.crosstab(vendas["segmento"], vendas["canal"]))
print(pd.crosstab(vendas["segmento"], vendas["canal"], normalize="index").round(2))
canal app loja site
segmento
Atacado 30 43 69
Online 71 103 137
Sem cadastro 2 4 6
Varejo 64 108 163
canal app loja site
segmento
Atacado 0.21 0.30 0.49
Online 0.23 0.33 0.44
Sem cadastro 0.17 0.33 0.50
Varejo 0.19 0.32 0.49
Na segunda tabela, cada linha soma 1: ela diz, para cada segmento, a proporção de pedidos por canal. É a forma de responder "o perfil de canais muda de um segmento para outro?", que a contagem bruta esconde quando os segmentos têm tamanhos diferentes.
`unstack` e `stack`
O unstack leva um nível do índice para as colunas, e o stack faz o inverso. Depois de um groupby com duas chaves, o unstack é o atalho para a tabela larga:
agrupado = vendas.groupby(["segmento", "canal"])["receita"].sum()
largo2 = agrupado.unstack("canal")
print(largo2.round(0))
print(largo2.stack().equals(agrupado))
canal app loja site
segmento
Atacado 34940.0 49348.0 74926.0
Online 83440.0 128267.0 170309.0
Sem cadastro 3771.0 5218.0 8071.0
Varejo 70059.0 139222.0 203279.0
True
Percentual de cada linha
Uma tabela larga de valores fica mais útil como participação. O div com axis=0 divide cada linha pelo seu total, alinhando pelo índice (o / simples dividiria alinhando pelas colunas, e erraria):
por_categoria = vendas.pivot_table(index="categoria", columns="canal", values="receita", aggfunc="sum")
participacao = por_categoria.div(por_categoria.sum(axis=1), axis=0).round(2)
print(participacao)
print(participacao.sum(axis=1).round(2).tolist())
canal app loja site
categoria
Casa 0.23 0.33 0.45
Esporte 0.19 0.35 0.46
Informática 0.18 0.32 0.50
Papelaria 0.20 0.32 0.48
[1.01, 1.0, 1.0, 1.0]
Cada linha soma 1 antes de arredondar. O 1.01 da primeira categoria é só o efeito de arredondar cada célula para duas casas: não há erro na conta.
| Quero | Uso |
|---|---|
| Resumo agregado, como uma tabela dinâmica | pivot_table |
| Só reorganizar pares únicos | pivot |
| Largo para longo | melt |
| Contar combinações (e proporções) | crosstab |
| Levar um nível do índice às colunas | unstack |
Exercício 1
A receita por categoria e mês
Escreva receita_por_categoria_e_mes(tabela), que devolva uma tabela com as categorias nas linhas e os 12 meses nas colunas, com 0 onde não houve venda, e confira que a soma de tudo é igual à receita total.