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:
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"])
[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:
try:
pd.concat([janeiro, fevereiro], verify_integrity=True)
except ValueError as erro:
print("ValueError:", str(erro)[:34])
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):
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())
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:
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)
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:
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))
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:
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))
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:
print(hasattr(pd.DataFrame, "append"))
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.