Capítulo 18, Intermediário
Datas e séries temporais
Uma data guardada como texto não ordena, não subtrai e não agrupa por mês. Convertida em data de verdade, ela abre três ferramentas: o acessor `.dt`, o `resample` e as operações de deslocamento.
Ler datas direto na leitura
O parse_dates converte as colunas já no read_csv, e o acessor .dt entrega as peças de cada data:
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
pedidos = pd.read_csv("dados/pedidos.csv", parse_dates=["data_pedido"])
print(pedidos["data_pedido"].dtype, pedidos["data_pedido"].min().date(), pedidos["data_pedido"].max().date())
data = pedidos["data_pedido"]
print(data.dt.year.iloc[0], data.dt.month.iloc[0], data.dt.dayofweek.iloc[0], data.dt.quarter.iloc[0])
dias = {0: "seg", 1: "ter", 2: "qua", 3: "qui", 4: "sex", 5: "sáb", 6: "dom"}
print(data.dt.dayofweek.map(dias).value_counts().reindex(list(dias.values())).tolist())
datetime64[us] 2025-01-01 2025-12-31
2025 1 2 1
[126, 136, 125, 83, 109, 108, 113]
O dayofweek vai de 0 (segunda) a 6 (domingo). Eu uso um dicionário para os nomes em português, em vez do day_name(locale=...), que depende de a máquina ter esse idioma instalado.
Reamostrar: de dias para meses
O resample é um groupby para períodos de tempo: ele precisa de um índice de datas, e você escolhe a frequência. Aqui, a quantidade pedida por mês:
por_mes = pedidos.set_index("data_pedido")["quantidade"].resample("MS").sum()
print(len(por_mes), por_mes.index[0].date(), por_mes.iloc[:3].tolist())
print(por_mes.head(3))
12 2025-01-01 [366, 286, 367]
data_pedido
2025-01-01 366
2025-02-01 286
2025-03-01 367
Freq: MS, Name: quantidade, dtype: int64
| Frequência | Significa |
|---|---|
D | Dia |
W | Semana |
MS | Início de cada mês |
ME | Fim de cada mês |
QS | Início de cada trimestre |
YS | Início de cada ano |
O código antigo usava "M" para mês, e isso não funciona mais no Pandas 3:
try:
pedidos.set_index("data_pedido")["quantidade"].resample("M").sum()
except ValueError as erro:
print("ValueError:", str(erro)[:35])
ValueError: Invalid frequency: M. Failed to par
Variação entre períodos
Com a série mensal, shift, diff e pct_change comparam cada mês com o anterior, sem laço:
resumo = pd.DataFrame({"quantidade": por_mes})
resumo["mes_anterior"] = resumo["quantidade"].shift(1)
resumo["variacao"] = resumo["quantidade"].diff()
resumo["variacao_pct"] = (resumo["quantidade"].pct_change() * 100).round(1)
print(resumo.head(4))
quantidade mes_anterior variacao variacao_pct
data_pedido
2025-01-01 366 NaN NaN NaN
2025-02-01 286 366.0 -80.0 -21.9
2025-03-01 367 286.0 81.0 28.3
2025-04-01 317 367.0 -50.0 -13.6
O primeiro mês fica sem comparação (NaN), porque não tem anterior.
Datas em formatos misturados
No arquivo de funcionários, três datas estão escritas como dd/mm/aaaa, e as demais como aaaa-mm-dd. O Pandas infere o formato da primeira data e falha ao encontrar a primeira que não bate:
f = pd.read_csv("dados/funcionarios_100.csv")
try:
pd.to_datetime(f["data_admissao"])
except ValueError as erro:
print("ValueError:", str(erro).splitlines()[0])
ValueError: time data "29/09/2019" doesn't match format "%Y-%m-%d". You might want to try:
A "correção" que mais se encontra por aí é format="mixed" com dayfirst=True. Ela não dá erro, e por isso parece resolver. Veja o que ela faz de verdade, comparando com a conversão explícita: primeiro o formato ISO, depois o brasileiro nas que sobraram, e NaT (a data ausente) para o que não casar com nenhum:
iso = pd.to_datetime(f["data_admissao"], format="%Y-%m-%d", errors="coerce")
br = pd.to_datetime(f["data_admissao"], format="%d/%m/%Y", errors="coerce")
explicito = iso.fillna(br)
print(int(iso.isna().sum()), int(explicito.isna().sum()))
tentacao = pd.to_datetime(f["data_admissao"], format="mixed", dayfirst=True)
trocadas = tentacao != explicito
print(int(trocadas.sum()), len(f))
linha = trocadas.idxmax()
print(f.loc[linha, "data_admissao"], tentacao[linha].date(), explicito[linha].date())
3 0
32 104
2019-08-07 2019-07-08 2019-08-07
A conversão explícita acha as 3 datas brasileiras e não deixa nenhuma sem data. Já o dayfirst=True trocou o dia pelo mês em 32 das 104 datas: a data ISO 2019-08-07 (7 de agosto) virou 2019-07-08 (8 de julho). Isso acontece com toda data ISO cujo dia seja 12 ou menos, porque o dayfirst manda tratar o primeiro número depois do ano como dia. É a pior espécie de erro: o código roda, as datas continuam válidas, e um terço delas está errado.
O format="mixed" sem o dayfirst acerta aqui, mas só por sorte: as três datas brasileiras têm dia maior que 12, então não podem ser lidas como mês. Uma data como 04/05/2020 seria lida como abril:
ambiguas = pd.Series(["2020-01-31", "04/05/2020"])
print(pd.to_datetime(ambiguas, format="mixed").dt.strftime("%Y-%m-%d").tolist())
['2020-01-31', '2020-04-05']
A data 04/05/2020 brasileira é 4 de maio, e o Pandas a leu como 5 de abril. A regra que eu sigo: nunca adivinhe um formato de data. Declare cada formato que você aceita, converta por grupos com errors="coerce", conte o que ficou como NaT e investigue. Uma data recusada é um problema visível, e uma data trocada é um problema invisível.
Tempo de casa e deslocamentos
A subtração de duas datas dá uma duração (Timedelta). Com uma data de referência fixa, o resultado é reproduzível: um relatório que usa hoje() muda a cada dia, e isso quebra qualquer teste.
referencia = pd.Timestamp("2026-01-01")
admissao = explicito
tempo_de_casa_anos = ((referencia - admissao).dt.days // 365)
print(tempo_de_casa_anos.describe()[["min", "max"]].astype(int).tolist())
print(pd.Timestamp("2025-01-31") + pd.offsets.MonthEnd(1), pd.Timestamp("2025-01-10") + pd.Timedelta(days=30))
print(pd.date_range("2025-01-01", periods=4, freq="W").strftime("%d/%m").tolist())
[0, 10]
2025-02-28 00:00:00 2025-02-09 00:00:00
['05/01', '12/01', '19/01', '26/01']
Fuso horário
Uma data sem fuso é ambígua. O tz_localize diz em que fuso ela foi registrada, e o tz_convert a leva para outro. A regra de ouro: guarde em UTC e converta só para mostrar.
registro = pd.to_datetime(pd.Series(["2025-01-10 12:00", "2025-07-10 12:00"]))
em_sp = registro.dt.tz_localize("America/Sao_Paulo")
print(em_sp.dt.tz_convert("UTC").astype(str).tolist())
['2025-01-10 15:00:00+00:00', '2025-07-10 15:00:00+00:00']
Exercício 1
Pedidos por dia da semana
Escreva pedidos_por_dia_da_semana(tabela), que devolva uma Series com o número de pedidos de cada dia da semana (índice de 0 a 6), e confira que a soma é igual ao total de pedidos.