Capítulo 31, Avançado
Alinhamento e junções por tempo
O Pandas alinha por **rótulo** em quase tudo, e isso é uma qualidade e uma armadilha. Aqui estão os lugares onde o alinhamento age sem avisar, e a junção por tempo, que resolve um problema que o `merge` comum não resolve.
Atribuir uma coluna alinha pelo índice
Atribuir uma Series a uma coluna não copia na ordem: o Pandas procura, para cada linha, o valor com o mesmo rótulo. Se a Series chega em outra ordem, os valores vão para as linhas certas pelo rótulo, e o que parece um erro é o comportamento correto:
import numpy as np
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
base = pd.DataFrame({"nome": ["Ana", "Bruno", "Carla"]}, index=[0, 1, 2])
notas = pd.Series([9, 7, 8], index=[2, 0, 1])
base["nota_por_rotulo"] = notas
base["nota_por_posicao"] = notas.to_numpy()
print(base)
nome nota_por_rotulo nota_por_posicao
0 Ana 7 9
1 Bruno 8 7
2 Carla 9 8
Na coluna nota_por_rotulo, a Ana (índice 0) recebeu 7, a nota que tinha o rótulo 0. Na coluna nota_por_posicao, ela recebeu 9, a primeira da lista. As duas estão "certas" para o seu modelo, e a confusão entre os dois é a causa clássica de notas trocadas.
E um rótulo que não existe vira NaN, sem erro:
parcial = pd.Series([1, 2], index=[0, 5])
base["parcial"] = parcial
print(base["parcial"].tolist())
[1.0, nan, nan]
Rótulos repetidos quebram o alinhamento
O alinhamento só faz sentido se cada rótulo for único. Com repetição, o Pandas se recusa a adivinhar:
com_repeticao = pd.Series([1, 2, 3], index=[0, 0, 1])
try:
com_repeticao.reindex([0, 1])
except ValueError as erro:
print("ValueError:", erro)
print(base.index.is_unique, com_repeticao.index.is_unique)
ValueError: cannot reindex on an axis with duplicate labels
True False
Antes de uma operação que alinha, index.is_unique é uma conferência barata.
`reindex`, `align` e `combine_first`
O reindex impõe um novo conjunto de rótulos, e o que falta vira NaN (ou o fill_value). O combine_first preenche os buracos de uma tabela com os valores de outra:
principal = pd.Series([10.0, np.nan, 30.0], index=["a", "b", "c"])
reserva = pd.Series([1.0, 2.0, 3.0, 4.0], index=["a", "b", "c", "d"])
print(principal.reindex(["a", "b", "c", "d"]).tolist())
print(principal.combine_first(reserva).to_dict())
[10.0, nan, 30.0, nan]
{'a': 10.0, 'b': 2.0, 'c': 30.0, 'd': 4.0}
`merge_asof`: o valor mais recente
Um problema comum: cada pedido precisa da taxa de câmbio vigente na data dele, mas a tabela de câmbio só tem uma linha por trimestre. Um merge comum só casa datas idênticas. O merge_asof casa cada linha com a mais recente anterior (direction="backward"):
pedidos = pd.read_csv("dados/pedidos.csv", parse_dates=["data_pedido"]).sort_values("data_pedido")
cambio = pd.DataFrame(
{
"data": pd.to_datetime(["2025-01-01", "2025-04-01", "2025-07-01", "2025-10-01"]),
"usd": [6.1, 5.8, 5.5, 5.9],
}
)
com_cambio = pd.merge_asof(pedidos, cambio, left_on="data_pedido", right_on="data", direction="backward")
print(com_cambio.groupby("usd").size().to_dict())
print(com_cambio.loc[com_cambio["data_pedido"] == "2025-03-31", "usd"].unique().tolist())
{5.5: 209, 5.8: 215, 5.9: 201, 6.1: 175}
[6.1]
Um pedido de 31 de março recebe a taxa de 1º de janeiro (6,1), porque a de abril ainda não existia. Com o tolerance, você diz até quanto tempo para trás uma taxa ainda vale, e pedidos mais distantes ficam sem par:
com_prazo = pd.merge_asof(
pedidos, cambio, left_on="data_pedido", right_on="data",
direction="backward", tolerance=pd.Timedelta("30D"),
)
print(int(com_prazo["usd"].isna().sum()) > 0, int(com_cambio["usd"].isna().sum()))
True 0
As duas tabelas precisam estar ordenadas pela chave. Sem isso, o Pandas se recusa:
try:
pd.merge_asof(pedidos.sort_values("quantidade"), cambio, left_on="data_pedido", right_on="data")
except ValueError as erro:
print("ValueError:", erro)
ValueError: left keys must be sorted
`explode`: uma célula com vários valores
Uma coluna com listas (ou textos separados por vírgula) vira uma linha por valor com o explode, o que permite contar, agrupar e juntar:
produtos = pd.DataFrame({"produto": ["A", "B", "C"], "tags": ["novo,oferta", "oferta", "novo,oferta,verao"]})
em_linhas = produtos.assign(tag=produtos["tags"].str.split(",")).explode("tag")
print(len(em_linhas), em_linhas["tag"].value_counts().to_dict())
6 {'oferta': 3, 'novo': 2, 'verao': 1}
Exercício 1
O desconto vigente
Dada uma tabela tabela_descontos com data e percentual, e os pedidos, use o merge_asof para dar a cada pedido o desconto vigente na sua data, e confira que nenhum pedido anterior à primeira data da tabela ganha desconto.