Saltar al contenido principal
Capítulo 1.2 básico pandas DatetimeIndex resample rolling datos faltantes

El arsenal de pandas para series temporales

8 min lectura
Domina el DatetimeIndex: creación, frecuencias, selección por fecha, resample, rolling, shift y manejo de datos faltantes.

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ódigoFrecuenciaNotas
"h"Horaria24 puntos por día
"D"DiariaIncluye fines de semana
"B"Días hábilesExcluye sábado y domingo
"W"SemanalPor defecto el domingo
"ME"Fin de mesÚltimo día del mes (pandas ≥ 2.2)
"QE"Fin de trimestreMar, Jun, Sep, Dic
"YE"Fin de año31 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_periods solo 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étodoCuándo usarloLimitación
ffillSensores, precios (el último dato conocido sigue siendo válido)Introduce escalones artificiales
interpolate("time")Huecos cortos en series suavesAsume linealidad entre puntos
MediaHuecos largos como medida provisionalBorra 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.