Saltar al contenido principal
Capítulo 1.1 básico numpy statsmodels descomposición componentes simulación

Construye y destruye: componentes desde cero

6 min lectura
Genera cada componente de una serie temporal en Python, combínalos en una serie realista y verifica la descomposición automática con statsmodels.

Entiendes un mecanismo cuando puedes reproducirlo. En este subcapítulo vas a construir una serie temporal desde sus partes más básicas, combinarlas y luego aplicar una descomposición automática para verificar que recuperas lo que pusiste. Este ciclo “construir → observar → descomponer → verificar” es el mejor ejercicio mental para fijar los conceptos del capítulo principal.


Funciones generadoras de componentes

Empecemos definiendo funciones reutilizables para cada componente. Así puedes cambiar los parámetros sin reescribir la lógica cada vez.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose

def generar_tendencia(n: int, pendiente: float, base: float) -> np.ndarray:
    """Tendencia lineal: y = base + pendiente * t."""
    t = np.arange(n)
    return base + pendiente * t


def generar_estacionalidad(n: int, amplitud: float, periodo: int) -> np.ndarray:
    """Patrón sinusoidal que se repite cada 'periodo' pasos."""
    t = np.arange(n)
    return amplitud * np.sin(2 * np.pi * t / periodo)


def generar_ruido(n: int, sigma: float, semilla: int = 42) -> np.ndarray:
    """Ruido blanco gaussiano con desviación estándar 'sigma'."""
    rng = np.random.default_rng(semilla)
    return rng.normal(loc=0, scale=sigma, size=n)

Usar np.random.default_rng en lugar de np.random.seed es la forma moderna y recomendada: el generador de números aleatorios es local a la función y no afecta al estado global.


Componer la serie

Con las tres funciones listas, construir una serie de 60 meses (5 años) es inmediato.

# Parámetros del experimento
N       = 60        # número de períodos (meses)
BASE    = 100.0     # nivel inicial
PEND    = 1.2       # crecimiento mensual en unidades
AMP     = 15.0      # amplitud estacional
PERIODO = 12        # estacionalidad anual (meses por ciclo)
SIGMA   = 5.0       # dispersión del ruido

# Generamos cada componente
tendencia      = generar_tendencia(N, PEND, BASE)
estacionalidad = generar_estacionalidad(N, AMP, PERIODO)
ruido          = generar_ruido(N, SIGMA)

# Modelo aditivo
serie = tendencia + estacionalidad + ruido

# Envolvemos en una Serie de pandas con índice temporal
fechas = pd.date_range(start="2020-01-01", periods=N, freq="ME")
serie_ts = pd.Series(serie, index=fechas, name="valor")

La variable serie_ts ya es una serie temporal válida con un DatetimeIndex. Podemos operar sobre ella directamente.


Visualización de los cuatro paneles

Ver los componentes en paneles separados junto a la serie final es la forma más clara de entender cómo se relacionan.

fig, axes = plt.subplots(4, 1, figsize=(10, 12), sharex=True)

axes[0].plot(fechas, serie, color="#1a1a1a", linewidth=1.5)
axes[0].set_title("Serie completa (T + S + ε)", fontsize=11)
axes[0].grid(True, alpha=0.3)

axes[1].plot(fechas, tendencia, color="#1a1a1a", linewidth=2)
axes[1].set_title("Tendencia (T)", fontsize=11)
axes[1].grid(True, alpha=0.3)

axes[2].plot(fechas, estacionalidad, color="#ffcc00", linewidth=2)
axes[2].axhline(0, color="#aaaaaa", linewidth=0.8, linestyle="--")
axes[2].set_title("Estacionalidad (S)", fontsize=11)
axes[2].grid(True, alpha=0.3)

axes[3].plot(fechas, ruido, color="#ff8b94", linewidth=1.2)
axes[3].axhline(0, color="#aaaaaa", linewidth=0.8, linestyle="--")
axes[3].set_title("Ruido (ε)", fontsize=11)
axes[3].grid(True, alpha=0.3)

fig.suptitle("Descomposición manual: de los componentes a la serie", fontsize=13, fontweight="bold")
plt.tight_layout()
plt.show()

Fíjate en que al sumar los tres componentes, la señal estacional queda parcialmente enmascarada por el ruido, especialmente en los puntos donde el ruido es grande. Eso es exactamente lo que ocurre en datos reales.


Descomposición automática con statsmodels

Ahora aplica seasonal_decompose sobre la serie completa, sin decirle qué tendencia o estacionalidad usaste. El objetivo es ver si recupera los componentes originales.

resultado = seasonal_decompose(
    serie_ts,
    model="additive",
    period=PERIODO   # le indicamos que el ciclo tiene 12 meses
)

# Gráfico de la descomposición estimada
fig = resultado.plot()
fig.set_size_inches(10, 10)
plt.suptitle("Descomposición automática (seasonal_decompose)", y=1.01, fontsize=13)
plt.tight_layout()
plt.show()

La función devuelve un objeto con cuatro atributos: .observed, .trend, .seasonal y .resid. Los primeros y últimos period // 2 valores de .trend serán NaN porque la media móvil centrada no tiene suficientes puntos en los extremos.


Validar: ¿cuánto se parecen los componentes reales a los estimados?

El test más útil es cuantitativo: correlación entre lo que pusimos y lo que recuperó la descomposición.

# Eliminamos los NaN del extremo de la tendencia estimada
tendencia_estimada = resultado.trend.dropna()
tendencia_original = tendencia[len(tendencia) - len(tendencia_estimada):]
# Ajustamos los índices para alinear
idx_start = PERIODO // 2
idx_end   = N - PERIODO // 2
tendencia_original_recortada = tendencia[idx_start:idx_end]

corr_tendencia = np.corrcoef(tendencia_original_recortada, tendencia_estimada.values)[0, 1]
print(f"Correlación tendencia real vs estimada: {corr_tendencia:.4f}")

# Para la estacionalidad: comparamos el patrón promedio por mes
estacional_estimada = resultado.seasonal
# El patrón estacional promedio por mes en los datos originales
meses = np.arange(PERIODO)
estacional_manual = AMP * np.sin(2 * np.pi * meses / PERIODO)
estacional_decompose = np.array([
    estacional_estimada[estacional_estimada.index.month == m + 1].mean()
    for m in range(PERIODO)
])
corr_estacional = np.corrcoef(estacional_manual, estacional_decompose)[0, 1]
print(f"Correlación estacionalidad real vs estimada: {corr_estacional:.4f}")

Si el ruido es razonablemente pequeño (sigma pequeña relativa a la amplitud), obtendrás correlaciones por encima de 0.99. Si aumentas el ruido, las correlaciones bajan. Prueba con SIGMA = 20 y observa cómo la descomposición empieza a confundir ruido con estacionalidad.


El residual de una buena descomposición

Si la descomposición captura correctamente la tendencia y la estacionalidad, el residual debería parecerse al ruido blanco que pusiste. Una forma rápida de verificarlo es el test de Ljung-Box.

from statsmodels.stats.diagnostic import acorr_ljungbox

residual = resultado.resid.dropna()

# Test de Ljung-Box: H0 = no hay autocorrelación en los residuos
lb_test = acorr_ljungbox(residual, lags=[10, 20], return_df=True)
print(lb_test)

Si el p-value es mayor que 0.05, no rechazas la hipótesis nula: los residuos son estadísticamente compatibles con el ruido blanco. Cuando el p-value es muy pequeño, significa que queda estructura en los residuos que el modelo no capturó — un síntoma de que el período está mal especificado o de que la serie tiene componentes adicionales.

# Visualización rápida del residual
plt.figure(figsize=(10, 3))
plt.plot(residual, color="#ff8b94", linewidth=1.2, alpha=0.8)
plt.axhline(0, color="#1a1a1a", linewidth=1, linestyle="--")
plt.title("Residual de la descomposición — debe parecerse al ruido blanco")
plt.xlabel("Fecha")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Experimenta cambiando los parámetros

El código de este subcapítulo está diseñado para ser explorado. Aquí tienes tres experimentos concretos:

Experimento 1 — Ruido dominante: Cambia SIGMA = 25. Observa cómo la descomposición confunde ruido con estacionalidad y cómo bajan las correlaciones de validación. En datos reales con mucho ruido, las descomposiciones clásicas son poco fiables.

Experimento 2 — Modelo multiplicativo: Reemplaza la suma por:

serie = tendencia * (1 + estacionalidad / 100) * (1 + ruido / 100)

y cambia model="multiplicative" en seasonal_decompose. Esto simula una serie donde los picos estacionales crecen con el nivel.

Experimento 3 — Período incorrecto: Cambia el argumento period=6 en seasonal_decompose aunque la serie tiene período 12. Observa que la descomposición extraerá un patrón semestral incorrecto y el residual tendrá autocorrelación significativa.


Resumen

  • Generar los componentes por separado y combinarlos es la forma más pedagógica de entender el modelo aditivo.
  • seasonal_decompose recupera los componentes con buena precisión cuando el ruido es pequeño relativo a la estacionalidad y la tendencia está bien especificada.
  • El residual de una buena descomposición debe parecerse al ruido blanco: si tiene estructura, el modelo es incompleto.

El siguiente subcapítulo cubre las herramientas de pandas que necesitas para trabajar con series temporales reales: carga de datos, selección por fecha, resampleo y manejo de huecos.