El arsenal de pandas para series temporales
Pandas no es solo una librería de tablas — es una herramienta diseñada para trabajar con datos ordenados en el tiempo. Una vez que tu serie tiene un DatetimeIndex correcto, acceder a rangos, cambiar la frecuencia o crear variables rezagadas se convierte en una sola línea. Este subcapítulo es la guía de referencia que consultarás repetidamente.
Crear un DatetimeIndex desde cero
La función pd.date_range genera secuencias de fechas con una frecuencia determinada. Es el punto de partida para crear series sintéticas o para construir el índice de un dataset que llega sin él.
import pandas as pd
import numpy as np
# Frecuencias más comunes — elige la que coincida con tu granularidad de datos
ejemplos = {
"Horaria": pd.date_range("2024-01-01", periods=24, freq="h"),
"Diaria": pd.date_range("2024-01-01", periods=30, freq="D"),
"Semanal": pd.date_range("2024-01-01", periods=12, freq="W"),
"Mensual": pd.date_range("2024-01-01", periods=12, freq="ME"),
"Trimestral": pd.date_range("2024-01-01", periods=8, freq="QE"),
"Anual": pd.date_range("2020-01-01", periods=5, freq="YE"),
"15 minutos": pd.date_range("2024-01-01", periods=96, freq="15min"),
}
for nombre, idx in ejemplos.items():
print(f"{nombre:12s}: {idx[0]} → {idx[-1]} ({len(idx)} puntos)")
| Código | Frecuencia | Notas |
|---|---|---|
"h" | Horaria | 24 puntos por día |
"D" | Diaria | Incluye fines de semana |
"B" | Días hábiles | Excluye sábado y domingo |
"W" | Semanal | Por defecto el domingo |
"ME" | Fin de mes | Último día del mes (pandas ≥ 2.2) |
"QE" | Fin de trimestre | Mar, Jun, Sep, Dic |
"YE" | Fin de año | 31 de diciembre |
"15min" | 15 minutos | Útil para datos de sensores |
Nota de versión: Los códigos
"ME","QE","YE"son los correctos en pandas ≥ 2.2. En versiones anteriores se usaban"M","Q","Y". Si tu entorno usa una versión antigua, prueba primero con los nuevos y si fallan usa los antiguos.
Cargar datos con fechas desde CSV
El error más frecuente al cargar series temporales: dejar la columna de fecha como object (cadena de texto) en lugar de datetime.
# Forma correcta: parse_dates + index_col en una sola operación
df = pd.read_csv(
"datos.csv",
parse_dates=["fecha"], # convierte esta columna a datetime automáticamente
index_col="fecha" # la convierte en el índice del DataFrame
)
# Ordena por fecha (por si el CSV no viene ordenado cronológicamente)
df = df.sort_index()
print(type(df.index)) # <class 'pandas.core.indexes.datetimes.DatetimeIndex'>
print(df.index.dtype) # datetime64[ns]
Si parse_dates no funciona (formatos no estándar como "31/01/2024" o "Jan 2024"), convierte manualmente:
# Especifica el formato explícitamente cuando pandas no lo detecta solo
df["fecha"] = pd.to_datetime(df["fecha"], format="%d/%m/%Y")
df = df.set_index("fecha").sort_index()
Usa format= siempre que el formato sea ambiguo. "01/02/2024" puede ser 1 de febrero o 2 de enero dependiendo del locale.
Selección de rangos por fecha
Con un DatetimeIndex, seleccionar rangos es más legible que cualquier máscara booleana:
# Datos de ejemplo: 3 años de datos diarios
np.random.seed(42)
fechas = pd.date_range("2022-01-01", "2024-12-31", freq="D")
serie = pd.Series(
100 + np.cumsum(np.random.normal(0, 1, len(fechas))),
index=fechas
)
# Selección por año completo
serie_2023 = serie["2023"]
# Selección por rango de fechas (ambos extremos incluidos)
serie_q2_2023 = serie["2023-04":"2023-06"]
# Selección de un mes concreto
serie_enero = serie["2023-01"]
# Selección de un día exacto
valor_dia = serie["2023-07-15"]
# Usando .loc con Timestamp (útil cuando la fecha viene de una variable)
fecha_inicio = pd.Timestamp("2023-06-01")
subconjunto = serie.loc[fecha_inicio:]
print(f"Datos 2023: {len(serie_2023)} puntos")
print(f"Q2 2023: {len(serie_q2_2023)} puntos")
print(f"Enero 2023: {len(serie_enero)} puntos")
Cambiar la frecuencia con resample
resample es el equivalente temporal de groupby. Convierte una serie de alta frecuencia a baja frecuencia aplicando una función de agregación.
# Datos diarios → mensuales (valor medio de cada mes)
serie_mensual = serie.resample("ME").mean()
# Datos diarios → semanales (suma de la semana)
serie_semanal = serie.resample("W").sum()
# Valor máximo anual
maximo_anual = serie.resample("YE").max()
# Múltiples estadísticos a la vez
resumen_mensual = serie.resample("ME").agg(
media="mean",
maxima="max",
minima="min",
volatilidad="std"
)
print(resumen_mensual.head())
También puedes ir en sentido contrario (upsampling): pasar de datos mensuales a diarios e interpolando los huecos. Esto lo veremos en la sección de datos faltantes.
Ventanas móviles con rolling
rolling calcula estadísticos sobre una ventana deslizante de tamaño fijo. Es la operación más usada para suavizar series y crear features temporales.
# Media móvil: la más útil para ver la tendencia
media_7d = serie.rolling(window=7).mean() # 7 días → elimina variación semanal
media_30d = serie.rolling(window=30).mean() # 30 días → más suave
# Desviación estándar móvil: detecta cambios de volatilidad
std_30d = serie.rolling(window=30).std()
# Mínimo y máximo móvil: rangos de variación reciente
min_14d = serie.rolling(window=14).min()
max_14d = serie.rolling(window=14).max()
Los primeros window - 1 valores serán NaN porque no hay suficientes puntos anteriores. Puedes forzar cálculos con menos datos usando min_periods:
# Calcula la media desde el primer punto disponible
media_flex = serie.rolling(window=30, min_periods=1).mean()
Usa
min_periodssolo cuando quieras ver el comienzo de la serie. Para análisis serio, los NaN iniciales son correctos: no existe media de 30 días cuando solo tienes 5 puntos.
Lags y diferencias con shift
Los lags (valores rezagados) y las diferencias son las transformaciones más básicas para capturar la dependencia temporal.
# Valor del período anterior (lag 1)
serie_lag1 = serie.shift(1)
# Valor de hace 7 días (lag 7 — útil para estacionalidad semanal)
serie_lag7 = serie.shift(7)
# Primera diferencia: cambio respecto al período anterior
serie_diff = serie.diff() # equivale a: serie - serie.shift(1)
# Diferencia estacional: cambio respecto al mismo período del ciclo anterior
serie_diff_anual = serie.diff(365)
# Variación porcentual respecto al período anterior
serie_pct = serie.pct_change() * 100
La primera diferencia es especialmente importante: si la serie original no es estacionaria (tiene tendencia), su primera diferencia suele serlo. El capítulo 2 profundiza en este concepto.
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
ax1.plot(serie["2023"], color="#1a1a1a", linewidth=1.5, label="Serie original")
ax1.set_title("Serie original")
ax1.grid(True, alpha=0.3)
ax2.plot(serie.diff()["2023"], color="#ff8b94", linewidth=1.2, label="Primera diferencia")
ax2.axhline(0, color="#aaaaaa", linewidth=0.8, linestyle="--")
ax2.set_title("Primera diferencia — la tendencia desaparece")
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Datos faltantes en series temporales
Los huecos temporales son más peligrosos que los NaN normales porque no es evidente que falten datos. Un dataset con registros del 1 al 3 de enero y del 6 al 8 podría haber tenido datos los días 4 y 5 que simplemente no se guardaron.
Paso 1: detectar si faltan fechas (no solo NaN, sino filas enteras ausentes):
# Genera el índice completo que debería tener la serie
idx_completo = pd.date_range(start=serie.index.min(), end=serie.index.max(), freq="D")
# Compara con el índice real
fechas_faltantes = idx_completo.difference(serie.index)
print(f"Fechas faltantes: {len(fechas_faltantes)}")
print(fechas_faltantes[:5])
# asfreq inserta NaN en las fechas que no existen
serie_completa = serie.asfreq("D")
print(f"NaN después de asfreq: {serie_completa.isna().sum()}")
Paso 2: rellenar los huecos
# Forward fill: propaga el último valor conocido hacia adelante
serie_ffill = serie_completa.fillna(method="ffill")
# Interpolación temporal: ajuste lineal entre los puntos conocidos
serie_interp = serie_completa.interpolate(method="time")
# Relleno con media del período (útil para estacionalidad conocida)
serie_media = serie_completa.fillna(serie_completa.mean())
¿Cuándo usar cada uno?
| Método | Cuándo usarlo | Limitación |
|---|---|---|
ffill | Sensores, precios (el último dato conocido sigue siendo válido) | Introduce escalones artificiales |
interpolate("time") | Huecos cortos en series suaves | Asume linealidad entre puntos |
| Media | Huecos largos como medida provisional | Borra cualquier variación real del período |
Para huecos largos (más de period // 4 puntos), ninguna imputación es fiable. Lo más honesto es excluir ese rango del análisis o anotarlo explícitamente.
Cheatsheet de referencia rápida
# CREAR
pd.date_range(start, periods, freq) # índice desde cero
pd.to_datetime(col, format) # convertir columna a datetime
# SELECCIONAR
serie["2023"] # año completo
serie["2023-03":"2023-06"] # rango de meses
serie.loc[pd.Timestamp("2023-01-15")] # fecha exacta
serie[serie.index.month == 12] # todos los diciembres
# TRANSFORMAR (no cambian la frecuencia)
serie.shift(n) # lag n períodos
serie.diff(n) # diferencia n períodos
serie.pct_change() # variación porcentual
serie.rolling(w).mean() # media móvil ventana w
serie.rolling(w).std() # volatilidad móvil ventana w
serie.expanding().mean() # media acumulada desde el inicio
# CAMBIAR FRECUENCIA
serie.resample("ME").mean() # diario → mensual
serie.resample("W").sum() # diario → semanal
serie.asfreq("D") # añade NaN en fechas ausentes
# INSPECCIONAR
serie.index.dtype # verificar que es datetime64
serie.index.freq # frecuencia inferida
serie.isna().sum() # NaN explícitos
serie.index.is_monotonic_increasing # verifica que está ordenado
Resumen
Un DatetimeIndex correcto desbloquea todo el poder de pandas para series temporales. Las operaciones más importantes del día a día son:
resample: cambiar la granularidad (diario → mensual).rolling: estadísticos sobre ventanas deslizantes (media móvil, volatilidad).shift/diff: crear lags y diferencias para capturar dependencia temporal.asfreq+fillna/interpolate: detectar y rellenar huecos.
El siguiente subcapítulo usa estos mismos datos para construir las cinco gráficas de diagnóstico que deberías hacer antes de ajustar cualquier modelo.