Diagnóstico visual antes de modelar
Nunca empieces a modelar sin mirar los datos. Esa regla, que aplica a todo análisis de datos, es aún más crítica con series temporales: los modelos hacen supuestos fuertes sobre la estructura de la serie, y elegir el modelo equivocado porque no miraste bien los datos es el error más costoso que puedes cometer.
En este subcapítulo aprenderás cinco gráficas de diagnóstico. Juntas responden casi todo lo que necesitas saber antes de decidir qué modelo aplicar.
Datos de trabajo
Para que puedas reproducir todas las gráficas, comenzamos generando una serie de referencia: ventas mensuales de una tienda online durante 5 años, con los tres componentes principales bien diferenciados.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas.plotting import lag_plot
np.random.seed(42)
N = 60
fechas = pd.date_range(start="2020-01-01", periods=N, freq="ME")
t = np.arange(N)
# Componentes con parámetros elegidos para que los diagnósticos sean informativos
tendencia = 200 + 2.0 * t
estacionalidad = 30 * np.sin(2 * np.pi * t / 12)
ruido = np.random.normal(0, 8, N)
serie = pd.Series(tendencia + estacionalidad + ruido, index=fechas, name="ventas")
Gráfica 1: serie + media móvil
La gráfica más básica y más informativa. La serie original revela toda la variabilidad; la media móvil aísla la tendencia eliminando la oscilación estacional.
La clave está en elegir la ventana de la media móvil igual al período estacional. Si sospechas estacionalidad anual en datos mensuales, usa ventana = 12. Así, cada punto de la media móvil promedia exactamente un ciclo completo, cancelando la estacionalidad.
media_movil_12 = serie.rolling(window=12, center=True).mean()
plt.figure(figsize=(12, 5))
plt.plot(serie, color="#1a1a1a", linewidth=1.5, alpha=0.8, label="Ventas mensuales")
plt.plot(
media_movil_12,
color="#ffcc00", linewidth=3,
label="Media móvil 12 meses (tendencia)"
)
plt.title("Serie de ventas — tendencia subyacente con media móvil centrada")
plt.xlabel("Fecha")
plt.ylabel("Ventas")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Qué buscar: ¿La línea amarilla sube, baja o se mantiene plana? ¿La serie oscila regularmente alrededor de esa línea? Si la distancia entre la serie y la media móvil es constante, el modelo aditivo es apropiado. Si esa distancia crece con el nivel, el modelo multiplicativo es mejor.
Usar
center=Truehace que la media móvil esté centrada en el punto, no desplazada hacia la derecha. Para análisis retrospectivo es más correcto; para forecasting en producción, debes usar la versión no centrada (sin ese argumento).
Gráfica 2: seasonal plot (subseries estacionales)
El seasonal plot superpone cada año o ciclo en la misma gráfica. Si el patrón estacional es estable, verás las líneas de distintos años alineadas. Si el patrón está cambiando, las líneas divergirán.
# Creamos un DataFrame con columnas = años, filas = meses
df_pivot = pd.DataFrame({
"mes": serie.index.month,
"anio": serie.index.year,
"valor": serie.values
})
fig, ax = plt.subplots(figsize=(12, 5))
colores = ["#1a1a1a", "#ffcc00", "#a8e6cf", "#ff8b94", "#dbeafe"]
for i, (anio, grupo) in enumerate(df_pivot.groupby("anio")):
color = colores[i % len(colores)]
ax.plot(
grupo["mes"],
grupo["valor"],
marker="o",
markersize=4,
linewidth=2,
color=color,
alpha=0.85,
label=str(anio)
)
ax.set_xticks(range(1, 13))
ax.set_xticklabels(["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"])
ax.set_title("Seasonal plot — ventas por mes, cada año en color distinto")
ax.set_xlabel("Mes")
ax.set_ylabel("Ventas")
ax.legend(title="Año", loc="upper right")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Qué buscar: ¿Los picos y valles caen en los mismos meses año tras año? ¿El patrón se está desplazando? ¿Hay algún año con un comportamiento claramente anómalo (pandemia, crisis, cambio de negocio)?
Gráfica 3: box plot estacional
El box plot estacional es la versión estadística del seasonal plot: muestra la mediana, los cuartiles y los outliers para cada período. Te permite identificar qué meses son más variables y cuáles son más predecibles.
fig, ax = plt.subplots(figsize=(12, 5))
# Agrupamos por mes y dibujamos el boxplot
grupos = [serie[serie.index.month == m].values for m in range(1, 13)]
meses_etiq = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]
bp = ax.boxplot(
grupos,
labels=meses_etiq,
patch_artist=True,
medianprops={"color": "#ffcc00", "linewidth": 2.5},
flierprops={"marker": "o", "markersize": 5, "alpha": 0.5},
)
# Colorear cajas alternando para mayor legibilidad
for i, patch in enumerate(bp["boxes"]):
patch.set_facecolor("#f0f0ea" if i % 2 == 0 else "#e8e8e0")
patch.set_edgecolor("#1a1a1a")
ax.set_title("Box plot estacional — distribución de ventas por mes")
ax.set_xlabel("Mes")
ax.set_ylabel("Ventas")
ax.grid(True, alpha=0.3, axis="y")
plt.tight_layout()
plt.show()
Qué buscar: Los meses con cajas altas (gran diferencia entre Q1 y Q3) son difíciles de predecir — hay mucha variabilidad año a año. Los meses con cajas pequeñas son más predecibles. Los outliers (puntos individuales fuera de los bigotes) pueden indicar eventos excepcionales en años concretos.
Gráfica 4: lag plot
El lag plot grafica cada valor contra su valor rezagado . Es la forma visual más directa de detectar autocorrelación.
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, k in zip(axes, [1, 6, 12]):
lag_plot(serie, lag=k, ax=ax, c="#1a1a1a", alpha=0.6)
# Línea de correlación perfecta como referencia
vmin, vmax = serie.min(), serie.max()
ax.plot([vmin, vmax], [vmin, vmax], color="#ffcc00", linewidth=1.5, linestyle="--", alpha=0.7)
corr = serie.corr(serie.shift(k))
ax.set_title(f"Lag {k} (r = {corr:.2f})")
ax.set_xlabel(f"y(t-{k})")
ax.set_ylabel("y(t)")
ax.grid(True, alpha=0.3)
plt.suptitle("Lag plots — autocorrelación con distintos rezagos", fontsize=12)
plt.tight_layout()
plt.show()
Cómo interpretar:
- Diagonal ascendente (puntos alineados de abajo-izquierda a arriba-derecha): autocorrelación positiva. El valor actual tiende a parecerse al pasado. Es el patrón más común en series con tendencia.
- Diagonal descendente: autocorrelación negativa (poco frecuente, pero ocurre en algunas series cíclicas).
- Nube sin forma: no hay dependencia lineal entre e . Si esto ocurre en lag 1, la serie se comporta casi como ruido blanco.
- Elipse: hay autocorrelación pero también mucha varianza. Un modelo puede ayudar, pero el error será alto.
En lag 12 (un año) verás una correlación alta porque la estacionalidad anual hace que enero de 2021 se parezca a enero de 2020, febrero a febrero, etc.
Gráfica 5: histograma de primeras diferencias
La primera diferencia mide el cambio de un período a otro. Si ese cambio sigue una distribución normal centrada en cero sin patrón temporal, la serie diferenciada es ruido blanco — lo que sugiere que la serie original es integrada de orden 1 (I(1)), y que diferenciando una vez se estabiliza.
diff_serie = serie.diff().dropna()
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
# Izquierda: serie de diferencias en el tiempo
ax1.plot(diff_serie, color="#1a1a1a", linewidth=1.2, alpha=0.8)
ax1.axhline(0, color="#ffcc00", linewidth=1.5, linestyle="--")
ax1.set_title("Primera diferencia a lo largo del tiempo")
ax1.set_xlabel("Fecha")
ax1.set_ylabel("Δ ventas")
ax1.grid(True, alpha=0.3)
# Derecha: distribución de las diferencias
ax2.hist(
diff_serie,
bins=15,
color="#1a1a1a",
edgecolor="white",
alpha=0.8
)
ax2.axvline(diff_serie.mean(), color="#ffcc00", linewidth=2, linestyle="--", label=f"Media = {diff_serie.mean():.1f}")
ax2.set_title("Distribución de las diferencias")
ax2.set_xlabel("Δ ventas")
ax2.set_ylabel("Frecuencia")
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.suptitle("Primera diferencia — ¿la serie es integrada?", fontsize=12)
plt.tight_layout()
plt.show()
Qué buscar: Si las diferencias forman una campana simétrica centrada en cero (o cerca de cero) y la serie temporal de diferencias no muestra tendencia ni patrón estacional, la diferenciación elimina la no-estacionariedad. Si las diferencias siguen teniendo estructura visible, puede ser necesaria una segunda diferenciación o una transformación adicional.
Guía de referencia rápida
| Gráfica | Lo que revela | Señal positiva | Señal de alerta |
|---|---|---|---|
| Línea + media móvil | Tendencia y estacionalidad general | Serie oscila regularmente en torno a la media móvil | La amplitud de las oscilaciones crece → multiplicativo |
| Seasonal plot | Estabilidad del patrón estacional | Líneas de distintos años alineadas | Líneas divergentes → patrón cambiante |
| Box plot estacional | Variabilidad por período | Cajas pequeñas, pocos outliers | Cajas grandes → alta incertidumbre por período |
| Lag plot | Autocorrelación y rezagos relevantes | Diagonal clara → dependencia temporal explotable | Nube sin forma → posible ruido blanco |
| Histograma de diferencias | No-estacionariedad e integración | Campana centrada en 0 → serie diferenciable | Cola larga → puede necesitar transformación logarítmica |
Resumen
Estas cinco gráficas se hacen en menos de 20 líneas de código y responden las preguntas más importantes:
- ¿Tiene la serie tendencia? (gráfica 1)
- ¿El patrón estacional es estable? (gráficas 2 y 3)
- ¿Hay dependencia temporal explotable? (gráfica 4)
- ¿Es la serie estacionaria o necesita diferenciación? (gráfica 5)
Con estas respuestas sobre la mesa, la elección del modelo deja de ser una apuesta y se convierte en una decisión informada. El capítulo 2 parte de ahí: formaliza el concepto de estacionariedad, introduce el test ADF y te guía en la construcción de tu primer modelo ARIMA.