Fechas: pandas DatetimeIndex

python
pandas
eda
Cómo pandas representa fechas con datetime64 y DatetimeIndex, parsing con pd.to_datetime(), filtrado por slice temporal, resample para agregar por período, accesor .dt y zonas horarias sin sufrir.

Por qué las fechas merecen un tutorial

Las fechas son el tipo de dato más traicionero en análisis. Cinco fuentes de bugs:

  • Parsing ambiguo: 01/03/2024 es 1 de marzo o 3 de enero según el locale.
  • Strings que parecen fechas pero no lo son. df.dtypes dice object, no datetime.
  • Zonas horarias silenciosas, comparar fechas con y sin tz da error o resultado raro.
  • Agregaciones por período (semana, mes) que requieren conocer resample.
  • Operaciones con Timedelta que no son intuitivas en Python.

pandas tiene buena infraestructura para todo esto si conoces los conceptos. Aquí están.

El tipo: datetime64[ns]

pandas representa fechas con datetime64[ns], un tipo nativo con resolución de nanosegundos:

import pandas as pd

df = pd.DataFrame({
    "fecha": ["2024-01-15", "2024-02-20", "2024-03-10"],
    "ventas": [1500, 2100, 1800],
})

df.dtypes
# fecha     object
# ventas     int64

df["fecha"] = pd.to_datetime(df["fecha"])
df.dtypes
# fecha     datetime64[ns]
# ventas             int64

Hasta que la columna no sea datetime64, no es una fecha real para pandas, solo un string que se parece a una fecha. Las operaciones temporales (filtrar por mes, agregar por semana) requieren el tipo correcto.

pd.to_datetime(): el conversor universal

pd.to_datetime("2024-03-15")                   # Timestamp único
pd.to_datetime(["2024-01-01", "2024-06-01"])   # DatetimeIndex
pd.to_datetime(df["fecha"])                    # Series datetime

Formato explícito

Si las fechas no están en formato ISO (YYYY-MM-DD), especifica:

pd.to_datetime(df["fecha"], format="%d/%m/%Y")
# para fechas como "15/03/2024"

Códigos de formato (los mismos de strftime en cualquier lenguaje):

Código Significado
%Y año 4 dígitos (2024)
%y año 2 dígitos (24)
%m mes numérico (03)
%d día (15)
%H hora 24h (14)
%M minuto (30)
%S segundo (45)

Manejo de errores

pd.to_datetime(df["fecha"], errors="coerce")
# las fechas que no se pueden parsear quedan como NaT (Not a Time)

errors="coerce" es el patrón seguro, en lugar de fallar, mete NaT (el NaN de fechas) en los strings inválidos. Después puedes diagnosticar con df[df["fecha"].isna()].

Construir fechas desde columnas

df = pd.DataFrame({"year": [2024, 2024], "month": [3, 4], "day": [15, 20]})
df["fecha"] = pd.to_datetime(df[["year", "month", "day"]])

pandas reconoce las columnas year, month, day (y opcionalmente hour, minute, second) y las combina. Útil cuando los datos vienen en columnas separadas.

DatetimeIndex: cuando la fecha es el índice

El máximo poder de pandas con fechas aparece cuando la fecha está como índice:

df = df.set_index("fecha")

Ahora puedes filtrar con slicing temporal:

df.loc["2024"]              # todo 2024
df.loc["2024-02"]           # todo febrero 2024
df.loc["2024-02-15":"2024-03-15"]   # rango de mes y medio
df.loc["2024-Q1"]           # primer trimestre

Esta sintaxis no funciona si la fecha es una columna normal. Convertir a índice es el paso clave para análisis temporal cómodo.

Para crear un índice de fechas regular:

fechas = pd.date_range("2024-01-01", "2024-12-31", freq="D")
df = pd.DataFrame({"valor": range(len(fechas))}, index=fechas)

freq admite muchos valores: "D" (diario), "W" (semanal), "ME" (fin de mes), "YE" (fin de año), "h" (cada hora), "15min", etc.

resample(): agregación temporal

resample() es como groupby() pero para fechas. Agrupa filas por período y aplica una agregación:

df.resample("ME").sum()        # suma mensual (fin de mes)
df.resample("W").mean()        # media semanal
df.resample("QE").sum()        # suma trimestral
df.resample("YE").mean()       # media anual

Las frecuencias más útiles:

Código Período
"D" Diario
"W" Semanal (acaba en domingo)
"ME" Mensual (fin de mes)
"MS" Mensual (inicio de mes)
"QE" Trimestral (fin de trimestre)
"YE" Anual (fin de año)
"h" Por hora

resample() necesita un DatetimeIndex. Si la fecha está en una columna, primero .set_index("fecha"), o usa df.resample("ME", on="fecha").

Agregaciones múltiples con named aggregations:

df.resample("ME").agg(
    total=("ventas", "sum"),
    media=("ventas", "mean"),
    n=("ventas", "count"),
)

Mismo patrón que groupby().agg(), los conceptos se transfieren.

El accesor .dt: extraer componentes

Para una Series datetime, .dt te da acceso a sus componentes:

df["año"] = df["fecha"].dt.year
df["mes"] = df["fecha"].dt.month
df["día"] = df["fecha"].dt.day
df["dia_semana"] = df["fecha"].dt.day_name()         # "Monday", "Tuesday"...
df["es_finde"] = df["fecha"].dt.dayofweek >= 5       # 5=sábado, 6=domingo
df["semana"] = df["fecha"].dt.isocalendar().week
df["trimestre"] = df["fecha"].dt.quarter
df["inicio_mes"] = df["fecha"].dt.is_month_start

.dt es el equivalente al .str para strings: solo funciona en Series del tipo correcto. Si la columna no es datetime, .dt falla.

Aritmética con fechas: Timedelta

Restar dos fechas da un Timedelta:

df["dias_desde_inicio"] = df["fecha"] - pd.Timestamp("2024-01-01")
# columna de timedelta64

df["dias"] = (df["fecha"] - pd.Timestamp("2024-01-01")).dt.days
# columna de int (número de días)

Sumar tiempo a una fecha:

df["fecha_envio"] = df["fecha"] + pd.Timedelta(days=7)
df["fecha_fin_mes"] = df["fecha"] + pd.offsets.MonthEnd(0)

Timedelta para duraciones simples. pd.offsets para algo más sofisticado (último día del mes, primer lunes…).

Zonas horarias

Por defecto las fechas son naive (sin tz). Para añadir zona:

df["fecha"] = df["fecha"].dt.tz_localize("Europe/Madrid")

Convertir a otra zona:

df["fecha_utc"] = df["fecha"].dt.tz_convert("UTC")

Regla: decide pronto si trabajas con timestamps naive o con tz. Mezclarlos da errores. Para análisis interno suele bastar naive. Para datos de API o producción, conviene tz-aware.

Patrones idiomáticos

Filtrar últimos N días

hoy = pd.Timestamp.now().normalize()
df.loc[df["fecha"] >= hoy - pd.Timedelta(days=30)]

.normalize() pone la hora a 00:00, útil para comparar fechas sin que la hora actual contamine.

Calendario laboral

df["es_laborable"] = df["fecha"].dt.dayofweek < 5

Para festivos, el paquete pandas.tseries.holiday ofrece calendarios construibles. Para España, suele usarse workalendar (externo).

Agrupar por año y mes

df.groupby([df["fecha"].dt.year, df["fecha"].dt.month])["ventas"].sum()

Cuando no quieres meter la fecha al índice pero sí agrupar por año-mes. Más conciso que resample cuando solo necesitas una vez.

Trampas habituales

  • Olvidar pd.to_datetime. Una columna de strings parecidos a fechas no es una fecha. df.dtypes debe decir datetime64[ns]. Si dice object, las operaciones .dt y .resample no funcionan.
  • parse_dates silencioso en read_csv. Si pandas no puede parsear todas las fechas, la columna queda como object sin error fuerte. Siempre df.dtypes después de leer.
  • Formato europeo mal parseado. 01/03/2024 por defecto se parsea como 3 de enero (formato US). Usa format="%d/%m/%Y" explícito.
  • Comparar tz-aware con tz-naive. pandas da error o resultado raro. Mantén toda la columna en un solo modo.
  • resample sin DatetimeIndex. Si la fecha está como columna, usa df.resample("ME", on="fecha") o pon la fecha como índice antes.
  • Timedelta(days=30) para “un mes”. Un mes no son 30 días siempre. Para sumar un mes calendar, usa pd.DateOffset(months=1), sabe que enero tiene 31 y febrero 28.

En la siguiente entrega

Tienes el motor analítico completo. La última pieza antes del caso real: visualización. matplotlib en el nivel justo, seaborn para gráficos rápidos y elegantes, y los patrones idiomáticos de plotting desde pandas. Lo siguiente.