Pular para o conteúdo

    Capítulo 20, Intermediário

    Combinar tabelas: concat

    Os dados raramente chegam em um arquivo só: um arquivo por mês, uma planilha por filial. O `concat` empilha tabelas (ou as coloca lado a lado), e os erros dele são silenciosos.

    Empilhar linhas

    O concat junta tabelas uma embaixo da outra. Por padrão, mantém o índice original de cada parte, o que gera índices repetidos. O ignore_index=True renumera, e o keys marca de onde cada linha veio:

    intermediario/cap20_concat.pylinhas 10 a 20
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    
    janeiro = pd.DataFrame({"produto": ["A", "B"], "quantidade": [10, 5]})
    fevereiro = pd.DataFrame({"produto": ["A", "C"], "quantidade": [7, 3]})
    
    print(pd.concat([janeiro, fevereiro]).index.tolist())
    print(pd.concat([janeiro, fevereiro], ignore_index=True))
    print(pd.concat([janeiro, fevereiro], keys=["jan", "fev"]).loc["fev"])
    
    Saída
    [0, 1, 0, 1]
      produto  quantidade
    0       A          10
    1       B           5
    2       A           7
    3       C           3
      produto  quantidade
    0       A           7
    1       C           3
    

    Para impedir índices repetidos por acidente, o verify_integrity=True levanta um erro:

    intermediario/cap20_concat.pylinhas 22 a 25
    try:
        pd.concat([janeiro, fevereiro], verify_integrity=True)
    except ValueError as erro:
        print("ValueError:", str(erro)[:34])
    
    Saída
    ValueError: Indexes have overlapping values: I
    

    Colunas diferentes

    Quando as tabelas têm colunas diferentes, o concat usa a união das colunas e preenche com NaN o que faltou, sem avisar. Aqui, a tabela de março não tem quantidade, e a coluna de inteiros, ao ganhar um ausente, vira decimal (capítulo 3):

    intermediario/cap20_concat.pylinhas 30 a 34
    marco = pd.DataFrame({"produto": ["D"], "desconto": [0.1]})
    junto = pd.concat([janeiro, marco], ignore_index=True)
    print(junto)
    print(junto.dtypes.astype(str).to_dict())
    print(pd.concat([janeiro, marco], join="inner", ignore_index=True).columns.tolist())
    
    Saída
      produto  quantidade  desconto
    0       A        10.0       NaN
    1       B         5.0       NaN
    2       D         NaN       0.1
    {'produto': 'str', 'quantidade': 'float64', 'desconto': 'float64'}
    ['produto']
    

    Isso é conveniente e perigoso: se um arquivo de março chegar com uma coluna renomeada (qtd em vez de quantidade), o concat não reclama, e você passa a ter duas colunas pela metade, cheias de NaN. O join="inner" mantém só as colunas comuns, e a melhor defesa é conferir as colunas antes de juntar:

    intermediario/cap20_concat.pylinhas 36 a 46
    def juntar_conferindo(partes):
        colunas = [set(p.columns) for p in partes]
        if any(c != colunas[0] for c in colunas):
            raise ValueError("as tabelas têm colunas diferentes")
        return pd.concat(partes, ignore_index=True)
    
    
    try:
        juntar_conferindo([janeiro, marco])
    except ValueError as erro:
        print("ValueError:", erro)
    
    Saída
    ValueError: as tabelas têm colunas diferentes
    

    Juntar muitos arquivos: uma vez só

    Quando há muitas partes, a tentação é juntar uma de cada vez em um laço. Cada concat copia tudo o que já foi acumulado, e o custo cresce com o quadrado do número de partes. O correto é coletar em uma lista e juntar uma única vez:

    intermediario/cap20_concat.pylinhas 51 a 77
    import time
    
    pedidos = pd.read_csv("dados/pedidos.csv", parse_dates=["data_pedido"])
    meses = [parte for _, parte in pedidos.groupby(pedidos["data_pedido"].dt.month)]
    print(len(meses), pd.concat(meses).equals(pedidos))
    
    pequenas = [pedidos.iloc[i : i + 3] for i in range(0, 600, 3)]
    
    
    def no_laco():
        acumulado = pequenas[0]
        for parte in pequenas[1:]:
            acumulado = pd.concat([acumulado, parte])
        return acumulado
    
    
    def de_uma_vez():
        return pd.concat(pequenas)
    
    
    def medir(funcao):
        inicio = time.perf_counter()
        funcao()
        return time.perf_counter() - inicio
    
    
    print(no_laco().equals(de_uma_vez()), "uma vez só foi mais rápido:", medir(no_laco) > 2 * medir(de_uma_vez))
    
    Saída
    12 True
    True uma vez só foi mais rápido: True
    

    O resultado é idêntico, e a versão de uma vez só é bem mais rápida. O 12 do começo mostra que os pedidos separados por mês e juntados de novo reconstroem a tabela original, igualzinha.

    Lado a lado: o alinhamento por índice

    Com axis=1, o concat coloca as tabelas lado a lado, e alinha pelo índice, e não pela posição. Se os índices diferem, aparecem NaN onde não há par:

    intermediario/cap20_concat.pylinhas 82 a 84
    nomes = pd.Series(["Ana", "Bruno", "Carla"], index=[10, 11, 12], name="nome")
    salarios = pd.Series([4500, 6500], index=[11, 12], name="salario")
    print(pd.concat([nomes, salarios], axis=1))
    
    Saída
         nome  salario
    10    Ana      NaN
    11  Bruno   4500.0
    12  Carla   6500.0
    

    A Ana ficou sem salário, porque o índice 10 não existe nos salários. Para juntar tabelas por uma coluna (um id), a ferramenta é o merge, do próximo capítulo.

    O `append` que não existe mais

    Muito material antigo usa df.append(outra). Esse método foi removido no Pandas 2, e continua ausente no Pandas 3:

    intermediario/cap20_concat.pylinha 89
    print(hasattr(pd.DataFrame, "append"))
    
    Saída
    False
    

    O substituto é o pd.concat([df, outra]).

    Exercício 1

    Juntar com segurança

    Escreva empilhar(partes), que junte uma lista de tabelas com ignore_index=True, mas recuse (com ValueError) se alguma parte tiver colunas diferentes da primeira. Teste com duas tabelas iguais e uma com coluna renomeada.