Pular para o conteúdo

    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:

    intermediario/cap22_reformatar.pylinhas 10 a 30
    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))
    
    Saída
    (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:

    intermediario/cap22_reformatar.pylinhas 35 a 40
    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())))
    
    Saída
    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:

    intermediario/cap22_reformatar.pylinha 42
    print(vendas.pivot_table(index="categoria", values="receita", aggfunc=["sum", "count"]).round(0))
    
    Saída
                      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:

    intermediario/cap22_reformatar.pylinhas 47 a 49
    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))
    
    Saída
    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:

    intermediario/cap22_reformatar.pylinhas 51 a 54
    try:
        vendas.pivot(index="categoria", columns="canal", values="quantidade")
    except ValueError as erro:
        print("ValueError:", erro)
    
    Saída
    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:

    intermediario/cap22_reformatar.pylinhas 59 a 61
    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))
    
    Saída
    (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:

    intermediario/cap22_reformatar.pylinhas 66 a 67
    print(pd.crosstab(vendas["segmento"], vendas["canal"]))
    print(pd.crosstab(vendas["segmento"], vendas["canal"], normalize="index").round(2))
    
    Saída
    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:

    intermediario/cap22_reformatar.pylinhas 72 a 75
    agrupado = vendas.groupby(["segmento", "canal"])["receita"].sum()
    largo2 = agrupado.unstack("canal")
    print(largo2.round(0))
    print(largo2.stack().equals(agrupado))
    
    Saída
    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):

    intermediario/cap22_reformatar.pylinhas 80 a 83
    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())
    
    Saída
    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.

    QueroUso
    Resumo agregado, como uma tabela dinâmicapivot_table
    Só reorganizar pares únicospivot
    Largo para longomelt
    Contar combinações (e proporções)crosstab
    Levar um nível do índice às colunasunstack

    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.